license.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io/fs"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "sort"
  12. "strings"
  13. )
  14. type Module struct {
  15. Path string
  16. Version string
  17. Dir string
  18. Main bool
  19. Replace *Module
  20. }
  21. func main() {
  22. // 1) 모듈 루트 탐색
  23. moduleRoot, err := findModuleRoot("..")
  24. must(err, "모듈 루트를 찾는 중 오류")
  25. fmt.Println("[info] module root:", moduleRoot)
  26. // 2) 출력 폴더: 모듈 루트/THIRD_PARTY_LICENSES
  27. licenseDir := filepath.Join(moduleRoot, "THIRD_PARTY_LICENSES")
  28. _ = os.MkdirAll(licenseDir, 0o755)
  29. // 3) 모듈 캐시 준비 (다운로드)
  30. must(runCmd(moduleRoot, "go", "mod", "download", "-json", "all"), "go mod download 실패")
  31. // 4) 모듈 목록 가져오기
  32. out, err := runCmdOut(moduleRoot, "go", "list", "-m", "-json", "all")
  33. must(err, "go list 실패")
  34. dec := json.NewDecoder(bytes.NewReader(out))
  35. var mods []Module
  36. for {
  37. var m Module
  38. if err := dec.Decode(&m); err != nil {
  39. if errors.Is(err, fs.ErrClosed) || errors.Is(err, os.ErrClosed) || err.Error() == "EOF" {
  40. break
  41. }
  42. panic(fmt.Errorf("json decode 실패: %w", err))
  43. }
  44. if m.Replace != nil {
  45. m = *m.Replace
  46. }
  47. if m.Main || m.Dir == "" {
  48. continue
  49. }
  50. mods = append(mods, m)
  51. }
  52. // 정렬(안정적 출력)
  53. sort.Slice(mods, func(i, j int) bool {
  54. if mods[i].Path == mods[j].Path {
  55. return mods[i].Version < mods[j].Version
  56. }
  57. return mods[i].Path < mods[j].Path
  58. })
  59. // 5) 라이선스 수집
  60. var missing []string
  61. for _, m := range mods {
  62. files := findLicenseFiles(m.Dir)
  63. if len(files) == 0 {
  64. // 부모에도 있는 경우가 있어 한 번 더 탐색
  65. files = findLicenseFiles(filepath.Dir(m.Dir))
  66. }
  67. if len(files) == 0 {
  68. missing = append(missing, fmt.Sprintf("%s@%s (Dir: %s)", m.Path, m.Version, m.Dir))
  69. continue
  70. }
  71. if err := saveModuleLicenses(licenseDir, m, files); err != nil {
  72. fmt.Fprintf(os.Stderr, "[warn] %s 저장 실패: %v\n", m.Path, err)
  73. }
  74. }
  75. // 6) 누락 목록 남기기
  76. if len(missing) > 0 {
  77. _ = os.WriteFile(filepath.Join(licenseDir, "_missing_licenses.txt"),
  78. []byte(strings.Join(missing, "\n")+"\n"), 0o644)
  79. fmt.Println("[warn] 라이선스 파일을 찾지 못한 모듈이 있습니다. _missing_licenses.txt 참조.")
  80. }
  81. fmt.Println("[done] THIRD_PARTY_LICENSES 폴더 생성 완료")
  82. }
  83. func findModuleRoot(start string) (string, error) {
  84. dir, err := filepath.Abs(start)
  85. if err != nil {
  86. return "", err
  87. }
  88. for {
  89. if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
  90. return dir, nil
  91. }
  92. parent := filepath.Dir(dir)
  93. if parent == dir {
  94. break
  95. }
  96. dir = parent
  97. }
  98. return "", fmt.Errorf("go.mod를 찾을 수 없습니다")
  99. }
  100. func runCmd(dir string, name string, args ...string) error {
  101. cmd := exec.Command(name, args...)
  102. cmd.Dir = dir
  103. var stderr bytes.Buffer
  104. cmd.Stderr = &stderr
  105. if err := cmd.Run(); err != nil {
  106. return fmt.Errorf("%v\nstderr:\n%s", err, stderr.String())
  107. }
  108. return nil
  109. }
  110. func runCmdOut(dir string, name string, args ...string) ([]byte, error) {
  111. cmd := exec.Command(name, args...)
  112. cmd.Dir = dir
  113. var stderr bytes.Buffer
  114. cmd.Stderr = &stderr
  115. out, err := cmd.Output()
  116. if err != nil {
  117. return nil, fmt.Errorf("%v\nstderr:\n%s", err, stderr.String())
  118. }
  119. return out, nil
  120. }
  121. func findLicenseFiles(dir string) []string {
  122. candidates := []string{
  123. "LICENSE", "LICENSE.txt", "LICENSE.md",
  124. "COPYING", "COPYING.txt",
  125. "NOTICE", "NOTICE.txt",
  126. "UNLICENSE", "LICENCE", "LICENCE.txt",
  127. }
  128. entries, err := os.ReadDir(dir)
  129. if err != nil {
  130. return nil
  131. }
  132. var res []string
  133. for _, e := range entries {
  134. if e.IsDir() {
  135. continue
  136. }
  137. up := strings.ToUpper(e.Name())
  138. for _, c := range candidates {
  139. if up == strings.ToUpper(c) {
  140. res = append(res, filepath.Join(dir, e.Name()))
  141. }
  142. }
  143. // LICENSE-APACHE, NOTICE-MIT 등 접두 허용
  144. if strings.HasPrefix(up, "LICENSE") || strings.HasPrefix(up, "NOTICE") || strings.HasPrefix(up, "COPYING") {
  145. res = append(res, filepath.Join(dir, e.Name()))
  146. }
  147. }
  148. sort.Strings(res)
  149. return res
  150. }
  151. func saveModuleLicenses(licenseDir string, m Module, files []string) error {
  152. fileName := strings.ReplaceAll(m.Path, "/", "_") + "@" + m.Version + ".txt"
  153. outPath := filepath.Join(licenseDir, fileName)
  154. var b bytes.Buffer
  155. fmt.Fprintf(&b, "%s @ %s\n\n", m.Path, m.Version)
  156. for _, f := range files {
  157. data, err := os.ReadFile(f)
  158. if err != nil {
  159. fmt.Fprintf(&b, "[warn] read fail: %s: %v\n\n", f, err)
  160. continue
  161. }
  162. fmt.Fprintf(&b, "----- %s -----\n\n", filepath.Base(f))
  163. // 개행 정규화
  164. text := strings.ReplaceAll(string(data), "\r\n", "\n")
  165. b.WriteString(text)
  166. if !strings.HasSuffix(text, "\n") {
  167. b.WriteString("\n")
  168. }
  169. b.WriteString("\n")
  170. }
  171. return os.WriteFile(outPath, b.Bytes(), 0o644)
  172. }
  173. func must(err error, ctx string) {
  174. if err != nil {
  175. panic(fmt.Errorf("%s: %w", ctx, err))
  176. }
  177. }