123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719 |
- //go:generate go run .
- package main
- import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "log"
- "net/http"
- "os"
- "os/exec"
- "path/filepath"
- "regexp"
- "strings"
- "sync"
- "time"
- "github.com/ulikunitz/xz"
- )
- type License struct {
- Name string `json:"name"`
- License string `json:"license"`
- URL string `json:"url"`
- Version string `json:"version"`
- }
- type ComponentInfo struct {
- Backend []License `json:"backend"`
- Frontend []License `json:"frontend"`
- }
- func main() {
- log.Println("Generating license information...")
- var info ComponentInfo
- // Generate backend licenses
- backendLicenses, err := generateBackendLicenses()
- if err != nil {
- log.Printf("Error generating backend licenses: %v", err)
- } else {
- info.Backend = backendLicenses
- log.Printf("INFO: Backend license collection completed: %d components", len(backendLicenses))
- }
- // Generate frontend licenses
- frontendLicenses, err := generateFrontendLicenses()
- if err != nil {
- log.Printf("Error generating frontend licenses: %v", err)
- } else {
- info.Frontend = frontendLicenses
- log.Printf("INFO: Frontend license collection completed: %d components", len(frontendLicenses))
- }
- log.Println("INFO: Serializing license data to JSON...")
- // Marshal to JSON
- jsonData, err := json.MarshalIndent(info, "", " ")
- if err != nil {
- log.Fatalf("Error marshaling JSON: %v", err)
- }
- log.Printf("INFO: JSON size: %d bytes", len(jsonData))
- log.Println("INFO: Compressing license data with xz...")
- // Compress with xz
- var compressed bytes.Buffer
- writer, err := xz.NewWriter(&compressed)
- if err != nil {
- log.Fatalf("Error creating xz writer: %v", err)
- }
- _, err = writer.Write(jsonData)
- if err != nil {
- log.Fatalf("Error writing compressed data: %v", err)
- }
- err = writer.Close()
- if err != nil {
- log.Fatalf("Error closing xz writer: %v", err)
- }
- log.Printf("INFO: Compressed size: %d bytes (%.1f%% of original)",
- compressed.Len(), float64(compressed.Len())/float64(len(jsonData))*100)
- // Write compressed data to file
- outputPath := "internal/license/licenses.xz"
- log.Printf("INFO: Writing compressed data to %s", outputPath)
- err = os.MkdirAll(filepath.Dir(outputPath), 0755)
- if err != nil {
- log.Fatalf("Error creating output directory: %v", err)
- }
- err = os.WriteFile(outputPath, compressed.Bytes(), 0644)
- if err != nil {
- log.Fatalf("Error writing output file: %v", err)
- }
- log.Printf("SUCCESS: License data generated successfully!")
- log.Printf(" - Backend components: %d", len(info.Backend))
- log.Printf(" - Frontend components: %d", len(info.Frontend))
- log.Printf(" - Total components: %d", len(info.Backend)+len(info.Frontend))
- log.Printf(" - Compressed size: %d bytes", compressed.Len())
- log.Printf(" - Output file: %s", outputPath)
- }
- func generateBackendLicenses() ([]License, error) {
- var licenses []License
- log.Println("INFO: Collecting backend Go modules...")
- // Get only direct and indirect dependencies, exclude workspace modules
- cmd := exec.Command("go", "mod", "graph")
- output, err := cmd.Output()
- if err != nil {
- return nil, fmt.Errorf("failed to run go mod graph: %v", err)
- }
- // Parse module graph to get unique dependencies
- depMap := make(map[string]string) // path -> version
- lines := strings.Split(string(output), "\n")
- for _, line := range lines {
- line = strings.TrimSpace(line)
- if line == "" {
- continue
- }
- parts := strings.Fields(line)
- if len(parts) != 2 {
- continue
- }
- // Extract dependency info from "module@version dependency@version"
- dep := parts[1]
- if dep == "" || !strings.Contains(dep, "@") {
- continue
- }
- atIndex := strings.LastIndex(dep, "@")
- if atIndex == -1 {
- continue
- }
- path := dep[:atIndex]
- version := dep[atIndex+1:]
- // Skip our own module and workspace modules
- if path == "" ||
- strings.HasPrefix(path, "github.com/0xJacky/Nginx-UI") ||
- strings.Contains(path, "git.uozi.org") ||
- strings.Contains(path, "apple-store-helper") {
- continue
- }
- // Only keep the first version we see (go mod graph shows all versions)
- if _, exists := depMap[path]; !exists {
- depMap[path] = version
- }
- }
- // Convert map to slice
- var allMods []struct {
- Path string `json:"Path"`
- Version string `json:"Version"`
- }
- for path, version := range depMap {
- allMods = append(allMods, struct {
- Path string `json:"Path"`
- Version string `json:"Version"`
- }{
- Path: path,
- Version: version,
- })
- }
- // Add Go language itself
- goLicense := License{
- Name: "Go Programming Language",
- Version: getGoVersion(),
- URL: "https://golang.org",
- License: "BSD-3-Clause",
- }
- licenses = append(licenses, goLicense)
- log.Printf("INFO: Found %d backend dependencies (+ Go language)", len(allMods))
- // Process modules in parallel
- const maxWorkers = 64
- jobs := make(chan struct {
- Path string
- Version string
- Index int
- }, len(allMods))
- results := make(chan License, len(allMods))
- // Progress tracking
- var processed int32
- var mu sync.Mutex
- // Start workers
- var wg sync.WaitGroup
- for i := 0; i < maxWorkers; i++ {
- wg.Add(1)
- go func(workerID int) {
- defer wg.Done()
- for job := range jobs {
- license := License{
- Name: job.Path,
- Version: job.Version,
- URL: fmt.Sprintf("https://%s", job.Path),
- }
- // Try to get license info from various sources
- licenseText := tryGetLicenseFromGit(job.Path)
- if licenseText == "" {
- licenseText = detectCommonLicense(job.Path)
- }
- license.License = licenseText
- mu.Lock()
- processed++
- currentCount := processed
- mu.Unlock()
- log.Printf("INFO: [%d/%d] Backend: %s -> %s", currentCount, len(allMods), job.Path, licenseText)
- results <- license
- }
- }(i)
- }
- // Send jobs
- go func() {
- for i, mod := range allMods {
- jobs <- struct {
- Path string
- Version string
- Index int
- }{
- Path: mod.Path,
- Version: mod.Version,
- Index: i,
- }
- }
- close(jobs)
- }()
- // Wait for workers and close results
- go func() {
- wg.Wait()
- close(results)
- }()
- // Collect results
- for license := range results {
- licenses = append(licenses, license)
- }
- return licenses, nil
- }
- func generateFrontendLicenses() ([]License, error) {
- var licenses []License
- log.Println("INFO: Collecting frontend npm packages...")
- // Read package.json
- packagePath := "app/package.json"
- if _, err := os.Stat(packagePath); os.IsNotExist(err) {
- return nil, fmt.Errorf("package.json not found at %s", packagePath)
- }
- data, err := os.ReadFile(packagePath)
- if err != nil {
- return nil, err
- }
- var pkg struct {
- Dependencies map[string]string `json:"dependencies"`
- DevDependencies map[string]string `json:"devDependencies"`
- }
- if err := json.Unmarshal(data, &pkg); err != nil {
- return nil, err
- }
- log.Printf("INFO: Found %d frontend dependencies", len(pkg.Dependencies))
- // Convert map to slice for easier parallel processing
- var packages []struct {
- Name string
- Version string
- Index int
- }
- i := 0
- for name, version := range pkg.Dependencies {
- packages = append(packages, struct {
- Name string
- Version string
- Index int
- }{
- Name: name,
- Version: version,
- Index: i,
- })
- i++
- }
- // Process packages in parallel
- const maxWorkers = 64
- jobs := make(chan struct {
- Name string
- Version string
- Index int
- }, len(packages))
- results := make(chan License, len(packages))
- // Progress tracking
- var processed int32
- var mu sync.Mutex
- // Start workers
- var wg sync.WaitGroup
- for i := 0; i < maxWorkers; i++ {
- wg.Add(1)
- go func(workerID int) {
- defer wg.Done()
- for job := range jobs {
- license := License{
- Name: job.Name,
- Version: job.Version,
- URL: fmt.Sprintf("https://www.npmjs.com/package/%s", job.Name),
- }
- // Try to get license info
- licenseText := tryGetNpmLicense(job.Name)
- if licenseText == "" {
- licenseText = "Unknown"
- }
- license.License = licenseText
- mu.Lock()
- processed++
- currentCount := processed
- mu.Unlock()
- log.Printf("INFO: [%d/%d] Frontend: %s -> %s", currentCount, len(packages), job.Name, licenseText)
- results <- license
- }
- }(i)
- }
- // Send jobs
- go func() {
- for _, pkg := range packages {
- jobs <- pkg
- }
- close(jobs)
- }()
- // Wait for workers and close results
- go func() {
- wg.Wait()
- close(results)
- }()
- // Collect results
- for license := range results {
- licenses = append(licenses, license)
- }
- return licenses, nil
- }
- func tryGetLicenseFromGitHub(modulePath string) string {
- // Extract GitHub info from module path
- if !strings.HasPrefix(modulePath, "github.com/") {
- return ""
- }
- parts := strings.Split(modulePath, "/")
- if len(parts) < 3 {
- return ""
- }
- owner := parts[1]
- repo := parts[2]
- // Try common license files and branches
- licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING", "COPYING.txt", "License", "license"}
- branches := []string{"master", "main", "HEAD"}
- for _, branch := range branches {
- for _, file := range licenseFiles {
- url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", owner, repo, branch, file)
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Get(url)
- if err != nil {
- continue
- }
- defer resp.Body.Close()
- if resp.StatusCode == 200 {
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- continue
- }
- // Extract license type from content
- content := string(body)
- licenseType := extractLicenseType(content)
- if licenseType != "Custom" {
- return licenseType
- }
- }
- }
- }
- return ""
- }
- func tryGetLicenseFromGit(modulePath string) string {
- // Try different Git hosting platforms
- if strings.HasPrefix(modulePath, "github.com/") {
- return tryGetLicenseFromGitHub(modulePath)
- }
- if strings.HasPrefix(modulePath, "gitlab.com/") {
- return tryGetLicenseFromGitLab(modulePath)
- }
- if strings.HasPrefix(modulePath, "gitee.com/") {
- return tryGetLicenseFromGitee(modulePath)
- }
- if strings.HasPrefix(modulePath, "bitbucket.org/") {
- return tryGetLicenseFromBitbucket(modulePath)
- }
- // Try to use go.mod info or pkg.go.dev API
- return tryGetLicenseFromPkgGoDev(modulePath)
- }
- func tryGetLicenseFromGitLab(modulePath string) string {
- parts := strings.Split(modulePath, "/")
- if len(parts) < 3 {
- return ""
- }
- owner := parts[1]
- repo := parts[2]
- // GitLab raw file URL format
- licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING", "License"}
- branches := []string{"master", "main"}
- for _, branch := range branches {
- for _, file := range licenseFiles {
- url := fmt.Sprintf("https://gitlab.com/%s/%s/-/raw/%s/%s", owner, repo, branch, file)
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Get(url)
- if err != nil {
- continue
- }
- defer resp.Body.Close()
- if resp.StatusCode == 200 {
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- continue
- }
- content := string(body)
- licenseType := extractLicenseType(content)
- if licenseType != "Custom" {
- return licenseType
- }
- }
- }
- }
- return ""
- }
- func tryGetLicenseFromGitee(modulePath string) string {
- parts := strings.Split(modulePath, "/")
- if len(parts) < 3 {
- return ""
- }
- owner := parts[1]
- repo := parts[2]
- // Gitee raw file URL format
- licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING"}
- branches := []string{"master", "main"}
- for _, branch := range branches {
- for _, file := range licenseFiles {
- url := fmt.Sprintf("https://gitee.com/%s/%s/raw/%s/%s", owner, repo, branch, file)
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Get(url)
- if err != nil {
- continue
- }
- defer resp.Body.Close()
- if resp.StatusCode == 200 {
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- continue
- }
- content := string(body)
- licenseType := extractLicenseType(content)
- if licenseType != "Custom" {
- return licenseType
- }
- }
- }
- }
- return ""
- }
- func tryGetLicenseFromBitbucket(modulePath string) string {
- parts := strings.Split(modulePath, "/")
- if len(parts) < 3 {
- return ""
- }
- owner := parts[1]
- repo := parts[2]
- // Bitbucket raw file URL format
- licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING"}
- branches := []string{"master", "main"}
- for _, branch := range branches {
- for _, file := range licenseFiles {
- url := fmt.Sprintf("https://bitbucket.org/%s/%s/raw/%s/%s", owner, repo, branch, file)
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Get(url)
- if err != nil {
- continue
- }
- defer resp.Body.Close()
- if resp.StatusCode == 200 {
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- continue
- }
- content := string(body)
- licenseType := extractLicenseType(content)
- if licenseType != "Custom" {
- return licenseType
- }
- }
- }
- }
- return ""
- }
- func tryGetLicenseFromPkgGoDev(modulePath string) string {
- // Try to get license info from pkg.go.dev API
- url := fmt.Sprintf("https://api.deps.dev/v3alpha/systems/go/packages/%s", modulePath)
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Get(url)
- if err != nil {
- return ""
- }
- defer resp.Body.Close()
- if resp.StatusCode != 200 {
- return ""
- }
- var apiResponse struct {
- Package struct {
- License []struct {
- Type string `json:"type"`
- } `json:"license"`
- } `json:"package"`
- }
- if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
- return ""
- }
- if len(apiResponse.Package.License) > 0 {
- return apiResponse.Package.License[0].Type
- }
- return ""
- }
- func tryGetNpmLicense(packageName string) string {
- // Try to get license from npm registry
- url := fmt.Sprintf("https://registry.npmjs.org/%s/latest", packageName)
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Get(url)
- if err != nil {
- return ""
- }
- defer resp.Body.Close()
- if resp.StatusCode != 200 {
- return ""
- }
- var pkg struct {
- License interface{} `json:"license"`
- }
- if err := json.NewDecoder(resp.Body).Decode(&pkg); err != nil {
- return ""
- }
- switch v := pkg.License.(type) {
- case string:
- return v
- case map[string]interface{}:
- if t, ok := v["type"].(string); ok {
- return t
- }
- }
- return ""
- }
- func extractLicenseType(content string) string {
- content = strings.ToUpper(content)
- licensePatterns := map[string]*regexp.Regexp{
- "MIT": regexp.MustCompile(`MIT\s+LICENSE`),
- "Apache-2.0": regexp.MustCompile(`APACHE\s+LICENSE.*VERSION\s+2\.0`),
- "GPL-3.0": regexp.MustCompile(`GNU\s+GENERAL\s+PUBLIC\s+LICENSE.*VERSION\s+3`),
- "BSD-3": regexp.MustCompile(`BSD\s+3-CLAUSE`),
- "BSD-2": regexp.MustCompile(`BSD\s+2-CLAUSE`),
- "ISC": regexp.MustCompile(`ISC\s+LICENSE`),
- "AGPL-3.0": regexp.MustCompile(`GNU\s+AFFERO\s+GENERAL\s+PUBLIC\s+LICENSE`),
- }
- for license, pattern := range licensePatterns {
- if pattern.MatchString(content) {
- return license
- }
- }
- return "Custom"
- }
- func detectCommonLicense(modulePath string) string {
- // Common patterns for detecting license types based on module paths
- commonLicenses := map[string]string{
- "golang.org/x": "BSD-3-Clause",
- "google.golang.org": "Apache-2.0",
- "gopkg.in": "Various",
- "go.uber.org": "MIT",
- "go.etcd.io": "Apache-2.0",
- "go.mongodb.org": "Apache-2.0",
- "go.opentelemetry.io": "Apache-2.0",
- "k8s.io": "Apache-2.0",
- "sigs.k8s.io": "Apache-2.0",
- "cloud.google.com": "Apache-2.0",
- "go.opencensus.io": "Apache-2.0",
- "contrib.go.opencensus.io": "Apache-2.0",
- "github.com/golang/": "BSD-3-Clause",
- "github.com/google/": "Apache-2.0",
- "github.com/grpc-ecosystem/": "Apache-2.0",
- "github.com/prometheus/": "Apache-2.0",
- "github.com/coreos/": "Apache-2.0",
- "github.com/etcd-io/": "Apache-2.0",
- "github.com/go-kit/": "MIT",
- "github.com/sirupsen/": "MIT",
- "github.com/stretchr/": "MIT",
- "github.com/spf13/": "Apache-2.0",
- "github.com/gorilla/": "BSD-3-Clause",
- "github.com/gin-gonic/": "MIT",
- "github.com/labstack/": "MIT",
- "github.com/julienschmidt/": "BSD-2-Clause",
- }
- for prefix, license := range commonLicenses {
- if strings.HasPrefix(modulePath, prefix) {
- return license
- }
- }
- return "Unknown"
- }
- func getGoVersion() string {
- cmd := exec.Command("go", "version")
- output, err := cmd.Output()
- if err != nil {
- return "Unknown"
- }
- // Parse "go version go1.21.5 darwin/amd64" to extract "go1.21.5"
- parts := strings.Fields(string(output))
- if len(parts) >= 3 {
- return parts[2] // "go1.21.5"
- }
- return "Unknown"
- }
|