Skip to content
quarry Artifact [f69f7a0709]
Login

Artifact [f69f7a0709]

Artifact f69f7a0709b763d7993c2a49b552c09d0a82c4332c0cf765514e117a0ab0ccf6:


// 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"
}