2024-11-24 22:27:55 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha512"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
2024-12-13 21:24:18 +00:00
|
|
|
"log"
|
2024-11-24 22:27:55 +00:00
|
|
|
"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
|
|
|
|
}
|
2024-12-13 21:24:18 +00:00
|
|
|
license, err := module.findLicense(mod.Dir)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("ERROR: %v", err)
|
|
|
|
continue
|
|
|
|
}
|
2024-11-24 22:27:55 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-13 21:24:18 +00:00
|
|
|
func (module Module) DumpText(directOnly bool) {
|
2024-11-24 22:27:55 +00:00
|
|
|
for _, dependency := range module.Dependencies {
|
2024-12-13 21:24:18 +00:00
|
|
|
if directOnly && !dependency.Direct {
|
|
|
|
continue
|
|
|
|
}
|
2024-11-24 22:27:55 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-13 21:24:18 +00:00
|
|
|
func (module *Module) findLicense(dir string) (*License, error) {
|
2024-11-24 22:27:55 +00:00
|
|
|
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 {
|
2024-12-13 21:24:18 +00:00
|
|
|
return license, nil
|
2024-11-24 22:27:55 +00:00
|
|
|
}
|
2024-12-13 21:24:18 +00:00
|
|
|
return NewLicense(content), nil
|
2024-11-24 22:27:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-12-13 21:24:18 +00:00
|
|
|
return nil, fmt.Errorf("No license found in '%s'", dir)
|
2024-11-24 22:27:55 +00:00
|
|
|
}
|