Skip to content
quarry Artifact [db69b7af09]
Login

Artifact [db69b7af09]

Artifact db69b7af09bfad2dbf270eb8ccd9f08a19e853a22db568281adf2ac019d7944c:


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

func run() error {
	out, err := exec.Command("go", "list", "-deps", "-json", "./...").Output()
	if err != nil {
		return fmt.Errorf("go list: %w", err)
	}
	// One entry per MODULE, not per package: a module with forty
	// packages has one licence.
	seen := map[string]mod{}
	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"
}