// Command licensegen writes the third-party licence inventory quarry
// serves at /licenses.
//
// Generated rather than hand-written: a hand-kept list goes stale the
// first time somebody adds a dependency, and it goes stale SILENTLY —
// the page keeps rendering, it just stops being true. A generator plus
// a freshness test makes that a build failure instead.
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
type mod struct {
Path string
Version string
Dir string
Main bool
}
type entry struct {
Module string `json:"module"`
Version string `json:"version"`
License string `json:"license"`
Text string `json:"text"`
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "licensegen:", err)
os.Exit(1)
}
}
// platforms are every target the forge ships for. The build graph is
// platform-bound — the DuckDB bindings are one module per OS and
// architecture — so an inventory taken on one machine was current
// there and stale on the next; the first Linux CI run failed on it.
// The union over the targets is the same list everywhere.
var platforms = [][2]string{{"darwin", "arm64"}, {"darwin", "amd64"}, {"linux", "amd64"}, {"linux", "arm64"}}
func run() error {
// One entry per MODULE, not per package: a module with forty
// packages has one licence.
seen := map[string]mod{}
for _, p := range platforms {
cmd := exec.Command("go", "list", "-deps", "-json", "./...")
cmd.Env = append(os.Environ(), "GOOS="+p[0], "GOARCH="+p[1])
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("go list (%s/%s): %w", p[0], p[1], err)
}
dec := json.NewDecoder(strings.NewReader(string(out)))
for {
var pkg struct{ Module *mod }
if err := dec.Decode(&pkg); err != nil {
break
}
if pkg.Module == nil || pkg.Module.Main || pkg.Module.Dir == "" {
continue
}
seen[pkg.Module.Path] = *pkg.Module
}
}
var entries []entry
for _, m := range seen {
name, text := licenseIn(m.Dir)
if text == "" {
// A dependency whose licence cannot be found is REPORTED
// rather than skipped: an incomplete inventory that looks
// complete is worse than one that names its own gap.
name = "unknown — no licence file in the module"
}
entries = append(entries, entry{Module: m.Path, Version: m.Version,
License: name, Text: text})
}
sort.Slice(entries, func(i, j int) bool { return entries[i].Module < entries[j].Module })
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(entries)
}
// licenseIn finds a module's licence file and classifies it from its own
// text, so the label is read rather than guessed.
func licenseIn(dir string) (string, string) {
names, err := os.ReadDir(dir)
if err != nil {
return "", ""
}
for _, n := range names {
lower := strings.ToLower(n.Name())
if !strings.HasPrefix(lower, "licence") && !strings.HasPrefix(lower, "license") &&
!strings.HasPrefix(lower, "copying") {
continue
}
b, err := os.ReadFile(filepath.Join(dir, n.Name()))
if err != nil {
continue
}
return classify(string(b)), string(b)
}
return "", ""
}
func classify(text string) string {
head := strings.ToLower(text)
if len(head) > 900 {
head = head[:900]
}
switch {
case strings.Contains(head, "apache license"):
return "Apache-2.0"
case strings.Contains(head, "mit license"), strings.Contains(head, "mit licence"):
return "MIT"
case strings.Contains(head, "mozilla public license"):
return "MPL-2.0"
case strings.Contains(head, "gnu general public"):
return "GPL"
case strings.Contains(head, "isc license"):
return "ISC"
case strings.Contains(head, "mit no attribution"):
return "MIT-0"
// The MIT text without its own title: chroma and others ship the
// body only, so match the clause that identifies it.
case strings.Contains(head, "permission is hereby granted, free of charge"):
return "MIT"
case strings.Contains(head, "redistribution and use in source and binary forms"):
if strings.Contains(head, "neither the name") {
return "BSD-3-Clause"
}
return "BSD-2-Clause"
}
return "see full text"
}