| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- package main
- import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "io/fs"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strings"
- )
- type Module struct {
- Path string
- Version string
- Dir string
- Main bool
- Replace *Module
- }
- func main() {
- // 1) 모듈 루트 탐색
- moduleRoot, err := findModuleRoot("..")
- must(err, "모듈 루트를 찾는 중 오류")
- fmt.Println("[info] module root:", moduleRoot)
- // 2) 출력 폴더: 모듈 루트/THIRD_PARTY_LICENSES
- licenseDir := filepath.Join(moduleRoot, "THIRD_PARTY_LICENSES")
- _ = os.MkdirAll(licenseDir, 0o755)
- // 3) 모듈 캐시 준비 (다운로드)
- must(runCmd(moduleRoot, "go", "mod", "download", "-json", "all"), "go mod download 실패")
- // 4) 모듈 목록 가져오기
- out, err := runCmdOut(moduleRoot, "go", "list", "-m", "-json", "all")
- must(err, "go list 실패")
- dec := json.NewDecoder(bytes.NewReader(out))
- var mods []Module
- for {
- var m Module
- if err := dec.Decode(&m); err != nil {
- if errors.Is(err, fs.ErrClosed) || errors.Is(err, os.ErrClosed) || err.Error() == "EOF" {
- break
- }
- panic(fmt.Errorf("json decode 실패: %w", err))
- }
- if m.Replace != nil {
- m = *m.Replace
- }
- if m.Main || m.Dir == "" {
- continue
- }
- mods = append(mods, m)
- }
- // 정렬(안정적 출력)
- sort.Slice(mods, func(i, j int) bool {
- if mods[i].Path == mods[j].Path {
- return mods[i].Version < mods[j].Version
- }
- return mods[i].Path < mods[j].Path
- })
- // 5) 라이선스 수집
- var missing []string
- for _, m := range mods {
- files := findLicenseFiles(m.Dir)
- if len(files) == 0 {
- // 부모에도 있는 경우가 있어 한 번 더 탐색
- files = findLicenseFiles(filepath.Dir(m.Dir))
- }
- if len(files) == 0 {
- missing = append(missing, fmt.Sprintf("%s@%s (Dir: %s)", m.Path, m.Version, m.Dir))
- continue
- }
- if err := saveModuleLicenses(licenseDir, m, files); err != nil {
- fmt.Fprintf(os.Stderr, "[warn] %s 저장 실패: %v\n", m.Path, err)
- }
- }
- // 6) 누락 목록 남기기
- if len(missing) > 0 {
- _ = os.WriteFile(filepath.Join(licenseDir, "_missing_licenses.txt"),
- []byte(strings.Join(missing, "\n")+"\n"), 0o644)
- fmt.Println("[warn] 라이선스 파일을 찾지 못한 모듈이 있습니다. _missing_licenses.txt 참조.")
- }
- fmt.Println("[done] THIRD_PARTY_LICENSES 폴더 생성 완료")
- }
- func findModuleRoot(start string) (string, error) {
- dir, err := filepath.Abs(start)
- if err != nil {
- return "", err
- }
- for {
- if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
- return dir, nil
- }
- parent := filepath.Dir(dir)
- if parent == dir {
- break
- }
- dir = parent
- }
- return "", fmt.Errorf("go.mod를 찾을 수 없습니다")
- }
- func runCmd(dir string, name string, args ...string) error {
- cmd := exec.Command(name, args...)
- cmd.Dir = dir
- var stderr bytes.Buffer
- cmd.Stderr = &stderr
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("%v\nstderr:\n%s", err, stderr.String())
- }
- return nil
- }
- func runCmdOut(dir string, name string, args ...string) ([]byte, error) {
- cmd := exec.Command(name, args...)
- cmd.Dir = dir
- var stderr bytes.Buffer
- cmd.Stderr = &stderr
- out, err := cmd.Output()
- if err != nil {
- return nil, fmt.Errorf("%v\nstderr:\n%s", err, stderr.String())
- }
- return out, nil
- }
- func findLicenseFiles(dir string) []string {
- candidates := []string{
- "LICENSE", "LICENSE.txt", "LICENSE.md",
- "COPYING", "COPYING.txt",
- "NOTICE", "NOTICE.txt",
- "UNLICENSE", "LICENCE", "LICENCE.txt",
- }
- entries, err := os.ReadDir(dir)
- if err != nil {
- return nil
- }
- var res []string
- for _, e := range entries {
- if e.IsDir() {
- continue
- }
- up := strings.ToUpper(e.Name())
- for _, c := range candidates {
- if up == strings.ToUpper(c) {
- res = append(res, filepath.Join(dir, e.Name()))
- }
- }
- // LICENSE-APACHE, NOTICE-MIT 등 접두 허용
- if strings.HasPrefix(up, "LICENSE") || strings.HasPrefix(up, "NOTICE") || strings.HasPrefix(up, "COPYING") {
- res = append(res, filepath.Join(dir, e.Name()))
- }
- }
- sort.Strings(res)
- return res
- }
- func saveModuleLicenses(licenseDir string, m Module, files []string) error {
- fileName := strings.ReplaceAll(m.Path, "/", "_") + "@" + m.Version + ".txt"
- outPath := filepath.Join(licenseDir, fileName)
- var b bytes.Buffer
- fmt.Fprintf(&b, "%s @ %s\n\n", m.Path, m.Version)
- for _, f := range files {
- data, err := os.ReadFile(f)
- if err != nil {
- fmt.Fprintf(&b, "[warn] read fail: %s: %v\n\n", f, err)
- continue
- }
- fmt.Fprintf(&b, "----- %s -----\n\n", filepath.Base(f))
- // 개행 정규화
- text := strings.ReplaceAll(string(data), "\r\n", "\n")
- b.WriteString(text)
- if !strings.HasSuffix(text, "\n") {
- b.WriteString("\n")
- }
- b.WriteString("\n")
- }
- return os.WriteFile(outPath, b.Bytes(), 0o644)
- }
- func must(err error, ctx string) {
- if err != nil {
- panic(fmt.Errorf("%s: %w", ctx, err))
- }
- }
|