package main import ( "encoding/json" "fmt" "log" "net/http" "os" "regexp" "sort" "strings" "text/template" "time" "github.com/scylladb/go-set/strset" ) // This program generates license_list.go. const ( source = "license_list.go" url = "https://spdx.org/licenses/licenses.json" ) var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. // This file was generated by robots at {{ .Timestamp }} // using data from {{ .URL }} package spdxlicense const Version = {{ printf "%q" .Version }} var licenseIDs = map[string]string{ {{- range $k, $v := .LicenseIDs }} {{ printf "%q" $k }}: {{ printf "%q" $v }}, {{- end }} } `)) var versionMatch = regexp.MustCompile(`-([0-9]+)\.?([0-9]+)?\.?([0-9]+)?\.?`) func main() { if err := run(); err != nil { fmt.Println(err.Error()) os.Exit(1) } } func run() error { resp, err := http.Get(url) if err != nil { return fmt.Errorf("unable to get licenses list: %w", err) } var result LicenseList if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { return fmt.Errorf("unable to decode license list: %w", err) } defer func() { if err := resp.Body.Close(); err != nil { log.Fatalf("unable to close body: %+v", err) } }() f, err := os.Create(source) if err != nil { return fmt.Errorf("unable to create %q: %w", source, err) } defer func() { if err := f.Close(); err != nil { log.Fatalf("unable to close %q: %+v", source, err) } }() licenseIDs := processSPDXLicense(result) err = tmp.Execute(f, struct { Timestamp time.Time URL string Version string LicenseIDs map[string]string }{ Timestamp: time.Now(), URL: url, Version: result.Version, LicenseIDs: licenseIDs, }) if err != nil { return fmt.Errorf("unable to generate template: %w", err) } return nil } // Parsing the provided SPDX license list necessitates a three pass approach. // The first pass is only related to what SPDX considers the truth. We use license info to // find replacements for deprecated licenses. // The second pass attempts to generate known short/long version listings for each key. // For info on some short name conventions see this document: // https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name. // The short long listing generation attempts to build all license permutations for a given key. // The new keys are then also associated with their relative SPDX value. If a key has already been entered // we know to ignore it since it came from the first pass which is considered the SPDX source of truth. // We also sort the licenses for the second pass so that cases like `GPL-1` associate to `GPL-1.0` and not `GPL-1.1`. // The third pass is for overwriting deprecated licenses with replacements, for example GPL-2.0+ is deprecated // and now maps to GPL-2.0-or-later. func processSPDXLicense(result LicenseList) map[string]string { // first pass build map var licenseIDs = make(map[string]string) for _, l := range result.Licenses { cleanID := strings.ToLower(l.ID) if _, exists := licenseIDs[cleanID]; exists { log.Fatalf("duplicate license ID found: %q", cleanID) } licenseIDs[cleanID] = l.ID } // The order of variations/permutations of a license ID matters because of we how shuffle its digits, // that is because the permutation code can generate the same value for two difference licenses, // for example: The licenses `ABC-1.0` and `ABC-1.1` can both map to `ABC-1`, // so we need to guarantee the order they are created to avoid mapping them wrongly. So we use a sorted list. // To overwrite deprecated licenses during the first pass we would later on rely on map order, // [which in go is not consistent by design](https://stackoverflow.com/a/55925880). sort.Slice(result.Licenses, func(i, j int) bool { return result.Licenses[i].ID < result.Licenses[j].ID }) // second pass to build exceptions and replacements replaced := strset.New() for _, l := range result.Licenses { var multipleID []string cleanID := strings.ToLower(l.ID) var replacement *License if l.Deprecated { replacement = result.findReplacementLicense(l) if replacement != nil { licenseIDs[cleanID] = replacement.ID } } multipleID = append(multipleID, buildLicensePermutations(cleanID)...) for _, id := range multipleID { // don't make replacements for IDs that have already been replaced. Since we have a sorted license list // the earliest replacement is correct (any future replacements are not. // e.g. replace lgpl-2 with LGPL-2.1-only is wrong, but with LGPL-2.0-only is correct) if replacement == nil || replaced.Has(id) { if _, exists := licenseIDs[id]; !exists { licenseIDs[id] = l.ID } } else { // a useful debugging line during builds log.Printf("replacing %s with %s\n", id, replacement.ID) licenseIDs[id] = replacement.ID replaced.Add(id) } } } return licenseIDs }