First version of a tool to list used dependencies and their licenses.

This commit is contained in:
Erik Brakkee 2024-11-24 23:27:55 +01:00
parent f269c95ce4
commit 0a4ed3a95f
3 changed files with 242 additions and 0 deletions

40
cmd/golicenses/input.go Normal file
View File

@ -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
}

17
cmd/golicenses/main.go Normal file
View File

@ -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] + "..."
}

185
cmd/golicenses/output.go Normal file
View File

@ -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
}