main.go 16 KB

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