main.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  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. // Read go.mod file directly
  94. goModPath := "go.mod"
  95. data, err := os.ReadFile(goModPath)
  96. if err != nil {
  97. return nil, fmt.Errorf("failed to read go.mod: %v", err)
  98. }
  99. // Parse go.mod content to extract dependencies
  100. depMap := make(map[string]string) // path -> version
  101. lines := strings.Split(string(data), "\n")
  102. inRequireBlock := false
  103. inReplaceBlock := false
  104. replaceMap := make(map[string]string) // original -> replacement
  105. for _, line := range lines {
  106. line = strings.TrimSpace(line)
  107. // Handle require block
  108. if strings.HasPrefix(line, "require (") {
  109. inRequireBlock = true
  110. continue
  111. }
  112. if strings.HasPrefix(line, "replace (") {
  113. inReplaceBlock = true
  114. continue
  115. }
  116. if line == ")" {
  117. inRequireBlock = false
  118. inReplaceBlock = false
  119. continue
  120. }
  121. // Parse replace directives
  122. if inReplaceBlock || strings.HasPrefix(line, "replace ") {
  123. if strings.Contains(line, "=>") {
  124. parts := strings.Split(line, "=>")
  125. if len(parts) == 2 {
  126. original := strings.TrimSpace(parts[0])
  127. replacement := strings.TrimSpace(parts[1])
  128. // Remove "replace " prefix if present
  129. original = strings.TrimPrefix(original, "replace ")
  130. // Extract module path (before version if present)
  131. if strings.Contains(original, " ") {
  132. original = strings.Fields(original)[0]
  133. }
  134. if strings.Contains(replacement, " ") {
  135. replacement = strings.Fields(replacement)[0]
  136. }
  137. replaceMap[original] = replacement
  138. }
  139. }
  140. continue
  141. }
  142. // Parse dependencies in require block or single require line
  143. if inRequireBlock || strings.HasPrefix(line, "require ") {
  144. // Remove "require " prefix if present
  145. line = strings.TrimPrefix(line, "require ")
  146. // Remove comments
  147. if idx := strings.Index(line, "//"); idx != -1 {
  148. line = line[:idx]
  149. }
  150. line = strings.TrimSpace(line)
  151. if line == "" {
  152. continue
  153. }
  154. // Parse "module version" format
  155. parts := strings.Fields(line)
  156. if len(parts) >= 2 {
  157. path := parts[0]
  158. version := parts[1]
  159. if path == "" {
  160. continue
  161. }
  162. // Apply replacements if they exist
  163. if replacement, exists := replaceMap[path]; exists {
  164. path = replacement
  165. }
  166. depMap[path] = version
  167. }
  168. }
  169. }
  170. // Convert map to slice
  171. var allMods []struct {
  172. Path string `json:"Path"`
  173. Version string `json:"Version"`
  174. }
  175. for path, version := range depMap {
  176. allMods = append(allMods, struct {
  177. Path string `json:"Path"`
  178. Version string `json:"Version"`
  179. }{
  180. Path: path,
  181. Version: version,
  182. })
  183. }
  184. // Add Go language itself
  185. goLicense := License{
  186. Name: "Go Programming Language",
  187. Version: getGoVersion(),
  188. URL: "https://golang.org",
  189. License: "BSD-3-Clause",
  190. }
  191. licenses = append(licenses, goLicense)
  192. log.Printf("INFO: Found %d backend dependencies (+ Go language)", len(allMods))
  193. // Process modules in parallel
  194. const maxWorkers = 64
  195. jobs := make(chan struct {
  196. Path string
  197. Version string
  198. Index int
  199. }, len(allMods))
  200. results := make(chan License, len(allMods))
  201. // Progress tracking
  202. var processed int32
  203. var mu sync.Mutex
  204. // Start workers
  205. var wg sync.WaitGroup
  206. for i := 0; i < maxWorkers; i++ {
  207. wg.Add(1)
  208. go func(workerID int) {
  209. defer wg.Done()
  210. for job := range jobs {
  211. license := License{
  212. Name: job.Path,
  213. Version: job.Version,
  214. URL: fmt.Sprintf("https://%s", job.Path),
  215. }
  216. // Try to get license info from various sources
  217. licenseText := tryGetLicenseFromGit(job.Path)
  218. if licenseText == "" {
  219. licenseText = detectCommonLicense(job.Path)
  220. }
  221. license.License = licenseText
  222. mu.Lock()
  223. processed++
  224. currentCount := processed
  225. mu.Unlock()
  226. log.Printf("INFO: [%d/%d] Backend: %s -> %s", currentCount, len(allMods), job.Path, licenseText)
  227. results <- license
  228. }
  229. }(i)
  230. }
  231. // Send jobs
  232. go func() {
  233. for i, mod := range allMods {
  234. jobs <- struct {
  235. Path string
  236. Version string
  237. Index int
  238. }{
  239. Path: mod.Path,
  240. Version: mod.Version,
  241. Index: i,
  242. }
  243. }
  244. close(jobs)
  245. }()
  246. // Wait for workers and close results
  247. go func() {
  248. wg.Wait()
  249. close(results)
  250. }()
  251. // Collect results
  252. for license := range results {
  253. licenses = append(licenses, license)
  254. }
  255. return licenses, nil
  256. }
  257. func generateFrontendLicenses() ([]License, error) {
  258. var licenses []License
  259. log.Println("INFO: Collecting frontend npm packages...")
  260. // Read package.json
  261. packagePath := "app/package.json"
  262. if _, err := os.Stat(packagePath); os.IsNotExist(err) {
  263. return nil, fmt.Errorf("package.json not found at %s", packagePath)
  264. }
  265. data, err := os.ReadFile(packagePath)
  266. if err != nil {
  267. return nil, err
  268. }
  269. var pkg struct {
  270. Dependencies map[string]string `json:"dependencies"`
  271. DevDependencies map[string]string `json:"devDependencies"`
  272. }
  273. if err := json.Unmarshal(data, &pkg); err != nil {
  274. return nil, err
  275. }
  276. log.Printf("INFO: Found %d frontend dependencies", len(pkg.Dependencies))
  277. // Convert map to slice for easier parallel processing
  278. var packages []struct {
  279. Name string
  280. Version string
  281. Index int
  282. }
  283. i := 0
  284. for name, version := range pkg.Dependencies {
  285. packages = append(packages, struct {
  286. Name string
  287. Version string
  288. Index int
  289. }{
  290. Name: name,
  291. Version: version,
  292. Index: i,
  293. })
  294. i++
  295. }
  296. // Process packages in parallel
  297. const maxWorkers = 64
  298. jobs := make(chan struct {
  299. Name string
  300. Version string
  301. Index int
  302. }, len(packages))
  303. results := make(chan License, len(packages))
  304. // Progress tracking
  305. var processed int32
  306. var mu sync.Mutex
  307. // Start workers
  308. var wg sync.WaitGroup
  309. for i := 0; i < maxWorkers; i++ {
  310. wg.Add(1)
  311. go func(workerID int) {
  312. defer wg.Done()
  313. for job := range jobs {
  314. license := License{
  315. Name: job.Name,
  316. Version: job.Version,
  317. URL: fmt.Sprintf("https://www.npmjs.com/package/%s", job.Name),
  318. }
  319. // Try to get license info
  320. licenseText := tryGetNpmLicense(job.Name)
  321. if licenseText == "" {
  322. licenseText = "Unknown"
  323. }
  324. license.License = licenseText
  325. mu.Lock()
  326. processed++
  327. currentCount := processed
  328. mu.Unlock()
  329. log.Printf("INFO: [%d/%d] Frontend: %s -> %s", currentCount, len(packages), job.Name, licenseText)
  330. results <- license
  331. }
  332. }(i)
  333. }
  334. // Send jobs
  335. go func() {
  336. for _, pkg := range packages {
  337. jobs <- pkg
  338. }
  339. close(jobs)
  340. }()
  341. // Wait for workers and close results
  342. go func() {
  343. wg.Wait()
  344. close(results)
  345. }()
  346. // Collect results
  347. for license := range results {
  348. licenses = append(licenses, license)
  349. }
  350. return licenses, nil
  351. }
  352. func tryGetLicenseFromGitHub(modulePath string) string {
  353. // Extract GitHub info from module path
  354. if !strings.HasPrefix(modulePath, "github.com/") {
  355. return ""
  356. }
  357. parts := strings.Split(modulePath, "/")
  358. if len(parts) < 3 {
  359. return ""
  360. }
  361. owner := parts[1]
  362. repo := parts[2]
  363. // Try common license files and branches
  364. licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING", "COPYING.txt", "License", "license"}
  365. branches := []string{"master", "main", "HEAD"}
  366. for _, branch := range branches {
  367. for _, file := range licenseFiles {
  368. url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", owner, repo, branch, file)
  369. client := &http.Client{Timeout: 10 * time.Second}
  370. resp, err := client.Get(url)
  371. if err != nil {
  372. continue
  373. }
  374. defer resp.Body.Close()
  375. if resp.StatusCode == 200 {
  376. body, err := io.ReadAll(resp.Body)
  377. if err != nil {
  378. continue
  379. }
  380. // Extract license type from content
  381. content := string(body)
  382. licenseType := extractLicenseType(content)
  383. if licenseType != "Custom" {
  384. return licenseType
  385. }
  386. }
  387. }
  388. }
  389. return ""
  390. }
  391. func tryGetLicenseFromGit(modulePath string) string {
  392. // Try different Git hosting platforms
  393. if strings.HasPrefix(modulePath, "github.com/") {
  394. return tryGetLicenseFromGitHub(modulePath)
  395. }
  396. if strings.HasPrefix(modulePath, "gitlab.com/") {
  397. return tryGetLicenseFromGitLab(modulePath)
  398. }
  399. if strings.HasPrefix(modulePath, "gitee.com/") {
  400. return tryGetLicenseFromGitee(modulePath)
  401. }
  402. if strings.HasPrefix(modulePath, "bitbucket.org/") {
  403. return tryGetLicenseFromBitbucket(modulePath)
  404. }
  405. // Try to use go.mod info or pkg.go.dev API
  406. return tryGetLicenseFromPkgGoDev(modulePath)
  407. }
  408. func tryGetLicenseFromGitLab(modulePath string) string {
  409. parts := strings.Split(modulePath, "/")
  410. if len(parts) < 3 {
  411. return ""
  412. }
  413. owner := parts[1]
  414. repo := parts[2]
  415. // GitLab raw file URL format
  416. licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING", "License"}
  417. branches := []string{"master", "main"}
  418. for _, branch := range branches {
  419. for _, file := range licenseFiles {
  420. url := fmt.Sprintf("https://gitlab.com/%s/%s/-/raw/%s/%s", owner, repo, branch, file)
  421. client := &http.Client{Timeout: 10 * time.Second}
  422. resp, err := client.Get(url)
  423. if err != nil {
  424. continue
  425. }
  426. defer resp.Body.Close()
  427. if resp.StatusCode == 200 {
  428. body, err := io.ReadAll(resp.Body)
  429. if err != nil {
  430. continue
  431. }
  432. content := string(body)
  433. licenseType := extractLicenseType(content)
  434. if licenseType != "Custom" {
  435. return licenseType
  436. }
  437. }
  438. }
  439. }
  440. return ""
  441. }
  442. func tryGetLicenseFromGitee(modulePath string) string {
  443. parts := strings.Split(modulePath, "/")
  444. if len(parts) < 3 {
  445. return ""
  446. }
  447. owner := parts[1]
  448. repo := parts[2]
  449. // Gitee raw file URL format
  450. licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING"}
  451. branches := []string{"master", "main"}
  452. for _, branch := range branches {
  453. for _, file := range licenseFiles {
  454. url := fmt.Sprintf("https://gitee.com/%s/%s/raw/%s/%s", owner, repo, branch, file)
  455. client := &http.Client{Timeout: 10 * time.Second}
  456. resp, err := client.Get(url)
  457. if err != nil {
  458. continue
  459. }
  460. defer resp.Body.Close()
  461. if resp.StatusCode == 200 {
  462. body, err := io.ReadAll(resp.Body)
  463. if err != nil {
  464. continue
  465. }
  466. content := string(body)
  467. licenseType := extractLicenseType(content)
  468. if licenseType != "Custom" {
  469. return licenseType
  470. }
  471. }
  472. }
  473. }
  474. return ""
  475. }
  476. func tryGetLicenseFromBitbucket(modulePath string) string {
  477. parts := strings.Split(modulePath, "/")
  478. if len(parts) < 3 {
  479. return ""
  480. }
  481. owner := parts[1]
  482. repo := parts[2]
  483. // Bitbucket raw file URL format
  484. licenseFiles := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING"}
  485. branches := []string{"master", "main"}
  486. for _, branch := range branches {
  487. for _, file := range licenseFiles {
  488. url := fmt.Sprintf("https://bitbucket.org/%s/%s/raw/%s/%s", owner, repo, branch, file)
  489. client := &http.Client{Timeout: 10 * time.Second}
  490. resp, err := client.Get(url)
  491. if err != nil {
  492. continue
  493. }
  494. defer resp.Body.Close()
  495. if resp.StatusCode == 200 {
  496. body, err := io.ReadAll(resp.Body)
  497. if err != nil {
  498. continue
  499. }
  500. content := string(body)
  501. licenseType := extractLicenseType(content)
  502. if licenseType != "Custom" {
  503. return licenseType
  504. }
  505. }
  506. }
  507. }
  508. return ""
  509. }
  510. func tryGetLicenseFromPkgGoDev(modulePath string) string {
  511. // Try to get license info from pkg.go.dev API
  512. url := fmt.Sprintf("https://api.deps.dev/v3alpha/systems/go/packages/%s", modulePath)
  513. client := &http.Client{Timeout: 10 * time.Second}
  514. resp, err := client.Get(url)
  515. if err != nil {
  516. return ""
  517. }
  518. defer resp.Body.Close()
  519. if resp.StatusCode != 200 {
  520. return ""
  521. }
  522. var apiResponse struct {
  523. Package struct {
  524. License []struct {
  525. Type string `json:"type"`
  526. } `json:"license"`
  527. } `json:"package"`
  528. }
  529. if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
  530. return ""
  531. }
  532. if len(apiResponse.Package.License) > 0 {
  533. return apiResponse.Package.License[0].Type
  534. }
  535. return ""
  536. }
  537. func tryGetNpmLicense(packageName string) string {
  538. // Try to get license from npm registry
  539. url := fmt.Sprintf("https://registry.npmjs.org/%s/latest", packageName)
  540. client := &http.Client{Timeout: 10 * time.Second}
  541. resp, err := client.Get(url)
  542. if err != nil {
  543. return ""
  544. }
  545. defer resp.Body.Close()
  546. if resp.StatusCode != 200 {
  547. return ""
  548. }
  549. var pkg struct {
  550. License interface{} `json:"license"`
  551. }
  552. if err := json.NewDecoder(resp.Body).Decode(&pkg); err != nil {
  553. return ""
  554. }
  555. switch v := pkg.License.(type) {
  556. case string:
  557. return v
  558. case map[string]interface{}:
  559. if t, ok := v["type"].(string); ok {
  560. return t
  561. }
  562. }
  563. return ""
  564. }
  565. func extractLicenseType(content string) string {
  566. content = strings.ToUpper(content)
  567. licensePatterns := map[string]*regexp.Regexp{
  568. "MIT": regexp.MustCompile(`MIT\s+LICENSE`),
  569. "Apache-2.0": regexp.MustCompile(`APACHE\s+LICENSE.*VERSION\s+2\.0`),
  570. "GPL-3.0": regexp.MustCompile(`GNU\s+GENERAL\s+PUBLIC\s+LICENSE.*VERSION\s+3`),
  571. "BSD-3": regexp.MustCompile(`BSD\s+3-CLAUSE`),
  572. "BSD-2": regexp.MustCompile(`BSD\s+2-CLAUSE`),
  573. "ISC": regexp.MustCompile(`ISC\s+LICENSE`),
  574. "AGPL-3.0": regexp.MustCompile(`GNU\s+AFFERO\s+GENERAL\s+PUBLIC\s+LICENSE`),
  575. }
  576. for license, pattern := range licensePatterns {
  577. if pattern.MatchString(content) {
  578. return license
  579. }
  580. }
  581. return "Custom"
  582. }
  583. func detectCommonLicense(modulePath string) string {
  584. // Common patterns for detecting license types based on module paths
  585. commonLicenses := map[string]string{
  586. "golang.org/x": "BSD-3-Clause",
  587. "google.golang.org": "Apache-2.0",
  588. "gopkg.in": "Various",
  589. "go.uber.org": "MIT",
  590. "go.etcd.io": "Apache-2.0",
  591. "go.mongodb.org": "Apache-2.0",
  592. "go.opentelemetry.io": "Apache-2.0",
  593. "k8s.io": "Apache-2.0",
  594. "sigs.k8s.io": "Apache-2.0",
  595. "cloud.google.com": "Apache-2.0",
  596. "go.opencensus.io": "Apache-2.0",
  597. "contrib.go.opencensus.io": "Apache-2.0",
  598. "github.com/golang/": "BSD-3-Clause",
  599. "github.com/google/": "Apache-2.0",
  600. "github.com/grpc-ecosystem/": "Apache-2.0",
  601. "github.com/prometheus/": "Apache-2.0",
  602. "github.com/coreos/": "Apache-2.0",
  603. "github.com/etcd-io/": "Apache-2.0",
  604. "github.com/go-kit/": "MIT",
  605. "github.com/sirupsen/": "MIT",
  606. "github.com/stretchr/": "MIT",
  607. "github.com/spf13/": "Apache-2.0",
  608. "github.com/gorilla/": "BSD-3-Clause",
  609. "github.com/gin-gonic/": "MIT",
  610. "github.com/labstack/": "MIT",
  611. "github.com/julienschmidt/": "BSD-2-Clause",
  612. }
  613. for prefix, license := range commonLicenses {
  614. if strings.HasPrefix(modulePath, prefix) {
  615. return license
  616. }
  617. }
  618. return "Unknown"
  619. }
  620. func getGoVersion() string {
  621. cmd := exec.Command("go", "version")
  622. output, err := cmd.Output()
  623. if err != nil {
  624. return "Unknown"
  625. }
  626. // Parse "go version go1.21.5 darwin/amd64" to extract "go1.21.5"
  627. parts := strings.Fields(string(output))
  628. if len(parts) >= 3 {
  629. return parts[2] // "go1.21.5"
  630. }
  631. return "Unknown"
  632. }