package main import ( "crypto/sha512" "encoding/base64" "fmt" "log" "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, err := module.findLicense(mod.Dir) if err != nil { log.Printf("ERROR: %v", err) continue } 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(directOnly bool) { for _, dependency := range module.Dependencies { if directOnly && !dependency.Direct { continue } 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, error) { 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, nil } return NewLicense(content), nil } } } } return nil, fmt.Errorf("No license found in '%s'", dir) }