diff --git a/cmd/golicenses/input.go b/cmd/golicenses/input.go new file mode 100644 index 0000000..234c6fa --- /dev/null +++ b/cmd/golicenses/input.go @@ -0,0 +1,40 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "strings" +) + +type ModuleDependency struct { + Path string + Version string + Dir string + Indirect bool + License string +} + +func pareseGoMod() []ModuleDependency { + // Get list of all modules + cmd := exec.Command("go", "list", "-m", "-json", "all") + output, err := cmd.Output() + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting module list: %v\n", err) + os.Exit(1) + } + + // Split the JSON objects (one per line) + modules := []ModuleDependency{} + decoder := json.NewDecoder(strings.NewReader(string(output))) + for decoder.More() { + var mod ModuleDependency + if err := decoder.Decode(&mod); err != nil { + fmt.Fprintf(os.Stderr, "Error decoding module: %v\n", err) + continue + } + modules = append(modules, mod) + } + return modules +} diff --git a/cmd/golicenses/main.go b/cmd/golicenses/main.go new file mode 100644 index 0000000..ff3dd27 --- /dev/null +++ b/cmd/golicenses/main.go @@ -0,0 +1,17 @@ +package main + +func main() { + modules := pareseGoMod() + module := NewModule(modules) + module.GenerateLicenseNames() + module.DumpOverview() + + module.DumpText() +} + +func truncateString(s string, length int) string { + if len(s) <= length { + return s + } + return s[:length-3] + "..." +} diff --git a/cmd/golicenses/output.go b/cmd/golicenses/output.go new file mode 100644 index 0000000..f04cdce --- /dev/null +++ b/cmd/golicenses/output.go @@ -0,0 +1,185 @@ +package main + +import ( + "crypto/sha512" + "encoding/base64" + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +type LicenseHash string + +type License struct { + LicenseType string + Text string + // unique name of a license. + LicenseName string +} + +func hash(data string) string { + hasher := sha512.New() + hasher.Write([]byte(data)) + hash := hasher.Sum(nil) + return base64.StdEncoding.EncodeToString(hash) +} + +func detectLicenseType(content string) string { + content = strings.ToLower(content) + licenses := map[string]string{ + "mit": "MIT License", + "apache license": "Apache License", + "bsd": "BSD License", + "gnu general public": "GPL", + "mozilla public": "Mozilla Public License", + "isc": "ISC License", + "creative commons": "Creative Commons", + "unlicense": "Unlicense", + } + + for key, license := range licenses { + if strings.Contains(content, key) { + return license + } + } + return "" +} + +func NewLicense(text string) *License { + licenseType := detectLicenseType(text) + return &License{ + LicenseType: licenseType, + Text: text, + } +} + +type Licenses map[string]*License + +type Dependency struct { + Module string + Version string + Direct bool + License *License +} + +func NewDependency(module string, version string, direct bool, license *License) *Dependency { + return &Dependency{ + Module: module, + Version: version, + Direct: direct, + License: license, + } +} + +type Dependencies []*Dependency + +type Module struct { + Licenses Licenses + Dependencies Dependencies +} + +func NewModule(modules []ModuleDependency) *Module { + module := &Module{ + Licenses: Licenses{}, + Dependencies: Dependencies{}, + } + for _, mod := range modules { + if mod.Dir == "" { + continue // Skip modules without local copies + } + license := module.findLicense(mod.Dir) + module.Licenses[hash(license.Text)] = license + dependency := NewDependency(mod.Path, mod.Version, !mod.Indirect, license) + module.Dependencies = append(module.Dependencies, dependency) + } + sort.Slice(module.Dependencies, func(i, j int) bool { + direct1 := module.Dependencies[i].Direct + direct2 := module.Dependencies[j].Direct + if direct1 != direct2 { + if direct1 { + return true + } + return false + } + return module.Dependencies[i].Module < module.Dependencies[j].Module + }) + + return module +} + +func (module Module) GenerateLicenseNames() { + usedNames := make(map[string]bool) + for _, license := range module.Licenses { + basename := strings.ReplaceAll(license.LicenseType, " ", "_") + i := 0 + name := fmt.Sprintf("%s-%d", basename, i) + for usedNames[name] { + i++ + name = fmt.Sprintf("%s-%d", basename, i) + } + license.LicenseName = name + usedNames[name] = true + } +} + +func (module Module) DumpOverview() { + // Find and print license information for each module + fmt.Printf("%-50s %-20s %-10s %-20s %-20s\n", "MODULE", "VERSION", "DIRECT", "LICENSE", "HASH") + fmt.Println() + for _, dependency := range module.Dependencies { + fmt.Printf("%-50s %-20s %-10v %-20s %-20s\n", + truncateString(dependency.Module, 49), + truncateString(dependency.Version, 19), + dependency.Direct, + dependency.License.LicenseType, + dependency.License.LicenseName) + } +} + +func (module Module) DumpText() { + for _, dependency := range module.Dependencies { + fmt.Println(strings.Repeat("=", 80)) + if dependency.Direct { + fmt.Printf("Direct dependency") + } else { + fmt.Printf("Indirect dependency") + } + fmt.Printf(" %s %s\n", dependency.Module, dependency.Version) + fmt.Printf("LICENSE\n\n %s\n\n", + dependency.License.Text) + } +} + +func (module *Module) findLicense(dir string) *License { + licenseFiles := []string{ + "LICENSE", + "LICENSE.txt", + "LICENSE.md", + "license", + "license.txt", + "license.md", + } + + for _, file := range licenseFiles { + path := filepath.Join(dir, file) + if _, err := os.Stat(path); err == nil { + // Read first line of license file + contentBytes, err := os.ReadFile(path) + if err == nil { + content := string(contentBytes) + content = strings.TrimSpace(content) + if len(content) > 0 { + hashcode := hash(content) + license := module.Licenses[hashcode] + if license != nil { + return license + } + return NewLicense(content) + } + } + } + } + return nil +}