main.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. //go:generate go run .
  2. package main
  3. import (
  4. "bytes"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "log"
  9. "net/http"
  10. "os"
  11. "os/exec"
  12. "path/filepath"
  13. "regexp"
  14. "strings"
  15. "sync"
  16. "time"
  17. "github.com/ulikunitz/xz"
  18. )
  19. type License struct {
  20. Name string `json:"name"`
  21. License string `json:"license"`
  22. URL string `json:"url"`
  23. Version string `json:"version"`
  24. }
  25. type ComponentInfo struct {
  26. Backend []License `json:"backend"`
  27. Frontend []License `json:"frontend"`
  28. }
  29. func main() {
  30. log.Println("Generating license information...")
  31. var info ComponentInfo
  32. // Generate backend licenses
  33. backendLicenses, err := generateBackendLicenses()
  34. if err != nil {
  35. log.Printf("Error generating backend licenses: %v", err)
  36. } else {
  37. info.Backend = backendLicenses
  38. log.Printf("INFO: Backend license collection completed: %d components", len(backendLicenses))
  39. }
  40. // Generate frontend licenses
  41. frontendLicenses, err := generateFrontendLicenses()
  42. if err != nil {
  43. log.Printf("Error generating frontend licenses: %v", err)
  44. } else {
  45. info.Frontend = frontendLicenses
  46. log.Printf("INFO: Frontend license collection completed: %d components", len(frontendLicenses))
  47. }
  48. log.Println("INFO: Serializing license data to JSON...")
  49. // Marshal to JSON
  50. jsonData, err := json.MarshalIndent(info, "", " ")
  51. if err != nil {
  52. log.Fatalf("Error marshaling JSON: %v", err)
  53. }
  54. log.Printf("INFO: JSON size: %d bytes", len(jsonData))
  55. log.Println("INFO: Compressing license data with xz...")
  56. // Compress with xz
  57. var compressed bytes.Buffer
  58. writer, err := xz.NewWriter(&compressed)
  59. if err != nil {
  60. log.Fatalf("Error creating xz writer: %v", err)
  61. }
  62. _, err = writer.Write(jsonData)
  63. if err != nil {
  64. log.Fatalf("Error writing compressed data: %v", err)
  65. }
  66. err = writer.Close()
  67. if err != nil {
  68. log.Fatalf("Error closing xz writer: %v", err)
  69. }
  70. log.Printf("INFO: Compressed size: %d bytes (%.1f%% of original)",
  71. compressed.Len(), float64(compressed.Len())/float64(len(jsonData))*100)
  72. // Write compressed data to file
  73. outputPath := "internal/license/licenses.xz"
  74. log.Printf("INFO: Writing compressed data to %s", outputPath)
  75. err = os.MkdirAll(filepath.Dir(outputPath), 0755)
  76. if err != nil {
  77. log.Fatalf("Error creating output directory: %v", err)
  78. }
  79. err = os.WriteFile(outputPath, compressed.Bytes(), 0644)
  80. if err != nil {
  81. log.Fatalf("Error writing output file: %v", err)
  82. }
  83. log.Printf("SUCCESS: License data generated successfully!")
  84. log.Printf(" - Backend components: %d", len(info.Backend))
  85. log.Printf(" - Frontend components: %d", len(info.Frontend))
  86. log.Printf(" - Total components: %d", len(info.Backend)+len(info.Frontend))
  87. log.Printf(" - Compressed size: %d bytes", compressed.Len())
  88. log.Printf(" - Output file: %s", outputPath)
  89. }
  90. func generateBackendLicenses() ([]License, error) {
  91. var licenses []License
  92. log.Println("INFO: Collecting backend Go modules...")
  93. // Get only direct and indirect dependencies, exclude workspace modules
  94. cmd := exec.Command("go", "mod", "graph")
  95. output, err := cmd.Output()
  96. if err != nil {
  97. return nil, fmt.Errorf("failed to run go mod graph: %v", err)
  98. }
  99. // Parse module graph to get unique dependencies
  100. depMap := make(map[string]string) // path -> version
  101. lines := strings.Split(string(output), "\n")
  102. for _, line := range lines {
  103. line = strings.TrimSpace(line)
  104. if line == "" {
  105. continue
  106. }
  107. parts := strings.Fields(line)
  108. if len(parts) != 2 {
  109. continue
  110. }
  111. // Extract dependency info from "module@version dependency@version"
  112. dep := parts[1]
  113. if dep == "" || !strings.Contains(dep, "@") {
  114. continue
  115. }
  116. atIndex := strings.LastIndex(dep, "@")
  117. if atIndex == -1 {
  118. continue
  119. }
  120. path := dep[:atIndex]
  121. version := dep[atIndex+1:]
  122. // Skip our own module and workspace modules
  123. if path == "" ||
  124. strings.HasPrefix(path, "github.com/0xJacky/Nginx-UI") ||
  125. strings.Contains(path, "git.uozi.org") ||
  126. strings.Contains(path, "apple-store-helper") {
  127. continue
  128. }
  129. // Only keep the first version we see (go mod graph shows all versions)
  130. if _, exists := depMap[path]; !exists {
  131. depMap[path] = version
  132. }
  133. }
  134. // Convert map to slice
  135. var allMods []struct {
  136. Path string `json:"Path"`
  137. Version string `json:"Version"`
  138. }
  139. for path, version := range depMap {
  140. allMods = append(allMods, struct {
  141. Path string `json:"Path"`
  142. Version string `json:"Version"`
  143. }{
  144. Path: path,
  145. Version: version,
  146. })
  147. }
  148. // Add Go language itself
  149. goLicense := License{
  150. Name: "Go Programming Language",
  151. Version: getGoVersion(),
  152. URL: "https://golang.org",
  153. License: "BSD-3-Clause",
  154. }
  155. licenses = append(licenses, goLicense)
  156. log.Printf("INFO: Found %d backend dependencies (+ Go language)", len(allMods))
  157. // Process modules in parallel
  158. const maxWorkers = 64
  159. jobs := make(chan struct {
  160. Path string
  161. Version string
  162. Index int
  163. }, len(allMods))
  164. results := make(chan License, len(allMods))
  165. // Progress tracking
  166. var processed int32
  167. var mu sync.Mutex
  168. // Start workers
  169. var wg sync.WaitGroup
  170. for i := 0; i < maxWorkers; i++ {
  171. wg.Add(1)
  172. go func(workerID int) {
  173. defer wg.Done()
  174. for job := range jobs {
  175. license := License{
  176. Name: job.Path,
  177. Version: job.Version,
  178. URL: fmt.Sprintf("https://%s", job.Path),
  179. }
  180. // Try to get license info from various sources
  181. licenseText := tryGetLicenseFromGit(job.Path)
  182. if licenseText == "" {
  183. licenseText = detectCommonLicense(job.Path)
  184. }
  185. license.License = licenseText
  186. mu.Lock()
  187. processed++
  188. currentCount := processed
  189. mu.Unlock()
  190. log.Printf("INFO: [%d/%d] Backend: %s -> %s", currentCount, len(allMods), job.Path, licenseText)
  191. results <- license
  192. }
  193. }(i)
  194. }
  195. // Send jobs
  196. go func() {
  197. for i, mod := range allMods {
  198. jobs <- struct {
  199. Path string
  200. Version string
  201. Index int
  202. }{
  203. Path: mod.Path,
  204. Version: mod.Version,
  205. Index: i,
  206. }
  207. }
  208. close(jobs)
  209. }()
  210. // Wait for workers and close results
  211. go func() {
  212. wg.Wait()
  213. close(results)
  214. }()
  215. // Collect results
  216. for license := range results {
  217. licenses = append(licenses, license)
  218. }
  219. return licenses, nil
  220. }
  221. func generateFrontendLicenses() ([]License, error) {
  222. var licenses []License
  223. log.Println("INFO: Collecting frontend npm packages...")
  224. // Read package.json
  225. packagePath := "app/package.json"
  226. if _, err := os.Stat(packagePath); os.IsNotExist(err) {
  227. return nil, fmt.Errorf("package.json not found at %s", packagePath)
  228. }
  229. data, err := os.ReadFile(packagePath)
  230. if err != nil {
  231. return nil, err
  232. }
  233. var pkg struct {
  234. Dependencies map[string]string `json:"dependencies"`
  235. DevDependencies map[string]string `json:"devDependencies"`
  236. }
  237. if err := json.Unmarshal(data, &pkg); err != nil {
  238. return nil, err
  239. }
  240. log.Printf("INFO: Found %d frontend dependencies", len(pkg.Dependencies))
  241. // Convert map to slice for easier parallel processing
  242. var packages []struct {
  243. Name string
  244. Version string
  245. Index int
  246. }
  247. i := 0
  248. for name, version := range pkg.Dependencies {
  249. packages = append(packages, struct {
  250. Name string
  251. Version string
  252. Index int
  253. }{
  254. Name: name,
  255. Version: version,
  256. Index: i,
  257. })
  258. i++
  259. }
  260. // Process packages in parallel
  261. const maxWorkers = 64
  262. jobs := make(chan struct {
  263. Name string
  264. Version string
  265. Index int
  266. }, len(packages))
  267. results := make(chan License, len(packages))
  268. // Progress tracking
  269. var processed int32
  270. var mu sync.Mutex
  271. // Start workers
  272. var wg sync.WaitGroup
  273. for i := 0; i < maxWorkers; i++ {
  274. wg.Add(1)
  275. go func(workerID int) {
  276. defer wg.Done()
  277. for job := range jobs {
  278. license := License{
  279. Name: job.Name,
  280. Version: job.Version,
  281. URL: fmt.Sprintf("https://www.npmjs.com/package/%s", job.Name),
  282. }
  283. // Try to get license info
  284. licenseText := tryGetNpmLicense(job.Name)
  285. if licenseText == "" {
  286. licenseText = "Unknown"
  287. }
  288. license.License = licenseText
  289. mu.Lock()
  290. processed++
  291. currentCount := processed
  292. mu.Unlock()
  293. log.Printf("INFO: [%d/%d] Frontend: %s -> %s", currentCount, len(packages), job.Name, licenseText)
  294. results <- license
  295. }
  296. }(i)
  297. }
  298. // Send jobs
  299. go func() {
  300. for _, pkg := range packages {
  301. jobs <- pkg
  302. }
  303. close(jobs)
  304. }()
  305. // Wait for workers and close results
  306. go func() {
  307. wg.Wait()
  308. close(results)
  309. }()
  310. // Collect results
  311. for license := range results {
  312. licenses = append(licenses, license)
  313. }
  314. return licenses, nil
  315. }
  316. func tryGetLicenseFromGitHub(modulePath string) string {
  317. // Extract GitHub info from module path
  318. if !strings.HasPrefix(modulePath, "github.com/") {
  319. return ""
  320. }
  321. parts := strings.Split(modulePath, "/")
  322. if len(parts) < 3 {
  323. return ""
  324. }
  325. owner := parts[1]
  326. repo := parts[2]
  327. // Try common license files and branches
  328. licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING", "COPYING.txt", "License", "license"}
  329. branches := []string{"master", "main", "HEAD"}
  330. for _, branch := range branches {
  331. for _, file := range licenseFiles {
  332. url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", owner, repo, branch, file)
  333. client := &http.Client{Timeout: 10 * time.Second}
  334. resp, err := client.Get(url)
  335. if err != nil {
  336. continue
  337. }
  338. defer resp.Body.Close()
  339. if resp.StatusCode == 200 {
  340. body, err := io.ReadAll(resp.Body)
  341. if err != nil {
  342. continue
  343. }
  344. // Extract license type from content
  345. content := string(body)
  346. licenseType := extractLicenseType(content)
  347. if licenseType != "Custom" {
  348. return licenseType
  349. }
  350. }
  351. }
  352. }
  353. return ""
  354. }
  355. func tryGetLicenseFromGit(modulePath string) string {
  356. // Try different Git hosting platforms
  357. if strings.HasPrefix(modulePath, "github.com/") {
  358. return tryGetLicenseFromGitHub(modulePath)
  359. }
  360. if strings.HasPrefix(modulePath, "gitlab.com/") {
  361. return tryGetLicenseFromGitLab(modulePath)
  362. }
  363. if strings.HasPrefix(modulePath, "gitee.com/") {
  364. return tryGetLicenseFromGitee(modulePath)
  365. }
  366. if strings.HasPrefix(modulePath, "bitbucket.org/") {
  367. return tryGetLicenseFromBitbucket(modulePath)
  368. }
  369. // Try to use go.mod info or pkg.go.dev API
  370. return tryGetLicenseFromPkgGoDev(modulePath)
  371. }
  372. func tryGetLicenseFromGitLab(modulePath string) string {
  373. parts := strings.Split(modulePath, "/")
  374. if len(parts) < 3 {
  375. return ""
  376. }
  377. owner := parts[1]
  378. repo := parts[2]
  379. // GitLab raw file URL format
  380. licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING", "License"}
  381. branches := []string{"master", "main"}
  382. for _, branch := range branches {
  383. for _, file := range licenseFiles {
  384. url := fmt.Sprintf("https://gitlab.com/%s/%s/-/raw/%s/%s", owner, repo, branch, file)
  385. client := &http.Client{Timeout: 10 * time.Second}
  386. resp, err := client.Get(url)
  387. if err != nil {
  388. continue
  389. }
  390. defer resp.Body.Close()
  391. if resp.StatusCode == 200 {
  392. body, err := io.ReadAll(resp.Body)
  393. if err != nil {
  394. continue
  395. }
  396. content := string(body)
  397. licenseType := extractLicenseType(content)
  398. if licenseType != "Custom" {
  399. return licenseType
  400. }
  401. }
  402. }
  403. }
  404. return ""
  405. }
  406. func tryGetLicenseFromGitee(modulePath string) string {
  407. parts := strings.Split(modulePath, "/")
  408. if len(parts) < 3 {
  409. return ""
  410. }
  411. owner := parts[1]
  412. repo := parts[2]
  413. // Gitee raw file URL format
  414. licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING"}
  415. branches := []string{"master", "main"}
  416. for _, branch := range branches {
  417. for _, file := range licenseFiles {
  418. url := fmt.Sprintf("https://gitee.com/%s/%s/raw/%s/%s", owner, repo, branch, file)
  419. client := &http.Client{Timeout: 10 * time.Second}
  420. resp, err := client.Get(url)
  421. if err != nil {
  422. continue
  423. }
  424. defer resp.Body.Close()
  425. if resp.StatusCode == 200 {
  426. body, err := io.ReadAll(resp.Body)
  427. if err != nil {
  428. continue
  429. }
  430. content := string(body)
  431. licenseType := extractLicenseType(content)
  432. if licenseType != "Custom" {
  433. return licenseType
  434. }
  435. }
  436. }
  437. }
  438. return ""
  439. }
  440. func tryGetLicenseFromBitbucket(modulePath string) string {
  441. parts := strings.Split(modulePath, "/")
  442. if len(parts) < 3 {
  443. return ""
  444. }
  445. owner := parts[1]
  446. repo := parts[2]
  447. // Bitbucket raw file URL format
  448. licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING"}
  449. branches := []string{"master", "main"}
  450. for _, branch := range branches {
  451. for _, file := range licenseFiles {
  452. url := fmt.Sprintf("https://bitbucket.org/%s/%s/raw/%s/%s", owner, repo, branch, file)
  453. client := &http.Client{Timeout: 10 * time.Second}
  454. resp, err := client.Get(url)
  455. if err != nil {
  456. continue
  457. }
  458. defer resp.Body.Close()
  459. if resp.StatusCode == 200 {
  460. body, err := io.ReadAll(resp.Body)
  461. if err != nil {
  462. continue
  463. }
  464. content := string(body)
  465. licenseType := extractLicenseType(content)
  466. if licenseType != "Custom" {
  467. return licenseType
  468. }
  469. }
  470. }
  471. }
  472. return ""
  473. }
  474. func tryGetLicenseFromPkgGoDev(modulePath string) string {
  475. // Try to get license info from pkg.go.dev API
  476. url := fmt.Sprintf("https://api.deps.dev/v3alpha/systems/go/packages/%s", modulePath)
  477. client := &http.Client{Timeout: 10 * time.Second}
  478. resp, err := client.Get(url)
  479. if err != nil {
  480. return ""
  481. }
  482. defer resp.Body.Close()
  483. if resp.StatusCode != 200 {
  484. return ""
  485. }
  486. var apiResponse struct {
  487. Package struct {
  488. License []struct {
  489. Type string `json:"type"`
  490. } `json:"license"`
  491. } `json:"package"`
  492. }
  493. if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
  494. return ""
  495. }
  496. if len(apiResponse.Package.License) > 0 {
  497. return apiResponse.Package.License[0].Type
  498. }
  499. return ""
  500. }
  501. func tryGetNpmLicense(packageName string) string {
  502. // Try to get license from npm registry
  503. url := fmt.Sprintf("https://registry.npmjs.org/%s/latest", packageName)
  504. client := &http.Client{Timeout: 10 * time.Second}
  505. resp, err := client.Get(url)
  506. if err != nil {
  507. return ""
  508. }
  509. defer resp.Body.Close()
  510. if resp.StatusCode != 200 {
  511. return ""
  512. }
  513. var pkg struct {
  514. License interface{} `json:"license"`
  515. }
  516. if err := json.NewDecoder(resp.Body).Decode(&pkg); err != nil {
  517. return ""
  518. }
  519. switch v := pkg.License.(type) {
  520. case string:
  521. return v
  522. case map[string]interface{}:
  523. if t, ok := v["type"].(string); ok {
  524. return t
  525. }
  526. }
  527. return ""
  528. }
  529. func extractLicenseType(content string) string {
  530. content = strings.ToUpper(content)
  531. licensePatterns := map[string]*regexp.Regexp{
  532. "MIT": regexp.MustCompile(`MIT\s+LICENSE`),
  533. "Apache-2.0": regexp.MustCompile(`APACHE\s+LICENSE.*VERSION\s+2\.0`),
  534. "GPL-3.0": regexp.MustCompile(`GNU\s+GENERAL\s+PUBLIC\s+LICENSE.*VERSION\s+3`),
  535. "BSD-3": regexp.MustCompile(`BSD\s+3-CLAUSE`),
  536. "BSD-2": regexp.MustCompile(`BSD\s+2-CLAUSE`),
  537. "ISC": regexp.MustCompile(`ISC\s+LICENSE`),
  538. "AGPL-3.0": regexp.MustCompile(`GNU\s+AFFERO\s+GENERAL\s+PUBLIC\s+LICENSE`),
  539. }
  540. for license, pattern := range licensePatterns {
  541. if pattern.MatchString(content) {
  542. return license
  543. }
  544. }
  545. return "Custom"
  546. }
  547. func detectCommonLicense(modulePath string) string {
  548. // Common patterns for detecting license types based on module paths
  549. commonLicenses := map[string]string{
  550. "golang.org/x": "BSD-3-Clause",
  551. "google.golang.org": "Apache-2.0",
  552. "gopkg.in": "Various",
  553. "go.uber.org": "MIT",
  554. "go.etcd.io": "Apache-2.0",
  555. "go.mongodb.org": "Apache-2.0",
  556. "go.opentelemetry.io": "Apache-2.0",
  557. "k8s.io": "Apache-2.0",
  558. "sigs.k8s.io": "Apache-2.0",
  559. "cloud.google.com": "Apache-2.0",
  560. "go.opencensus.io": "Apache-2.0",
  561. "contrib.go.opencensus.io": "Apache-2.0",
  562. "github.com/golang/": "BSD-3-Clause",
  563. "github.com/google/": "Apache-2.0",
  564. "github.com/grpc-ecosystem/": "Apache-2.0",
  565. "github.com/prometheus/": "Apache-2.0",
  566. "github.com/coreos/": "Apache-2.0",
  567. "github.com/etcd-io/": "Apache-2.0",
  568. "github.com/go-kit/": "MIT",
  569. "github.com/sirupsen/": "MIT",
  570. "github.com/stretchr/": "MIT",
  571. "github.com/spf13/": "Apache-2.0",
  572. "github.com/gorilla/": "BSD-3-Clause",
  573. "github.com/gin-gonic/": "MIT",
  574. "github.com/labstack/": "MIT",
  575. "github.com/julienschmidt/": "BSD-2-Clause",
  576. }
  577. for prefix, license := range commonLicenses {
  578. if strings.HasPrefix(modulePath, prefix) {
  579. return license
  580. }
  581. }
  582. return "Unknown"
  583. }
  584. func getGoVersion() string {
  585. cmd := exec.Command("go", "version")
  586. output, err := cmd.Output()
  587. if err != nil {
  588. return "Unknown"
  589. }
  590. // Parse "go version go1.21.5 darwin/amd64" to extract "go1.21.5"
  591. parts := strings.Fields(string(output))
  592. if len(parts) >= 3 {
  593. return parts[2] // "go1.21.5"
  594. }
  595. return "Unknown"
  596. }