diff --git a/.github/instructions/license-range-review.instructions.md b/.github/instructions/license-range-review.instructions.md new file mode 100644 index 0000000..f57e98b --- /dev/null +++ b/.github/instructions/license-range-review.instructions.md @@ -0,0 +1,12 @@ +--- +description: "Use when reviewing generated SPDX license range changes, especially additions or removals of complete license range families." +applyTo: "spdxexp/spdxlicenses/license_ranges.go" +--- + +# License Range Family Review + +- Treat the addition or removal of a complete license range family as requiring human review. +- Always leave an inline review comment on the added or removed family block, or on the nearest changed line when the family was removed. Start the comment with **Human review required: license range family changed** and identify the family. +- Ask the human reviewer to verify that the family change is intended and that its version grouping and ordering are correct. +- Compare the diff with the **License range families** summary in the pull request description and report any discrepancy. +- Do not leave this comment for a version added to or removed from an existing family unless the grouping or ordering is incorrect. \ No newline at end of file diff --git a/.github/workflows/fetch-licenses.yaml b/.github/workflows/fetch-licenses.yaml index 226ae9a..9ce5700 100644 --- a/.github/workflows/fetch-licenses.yaml +++ b/.github/workflows/fetch-licenses.yaml @@ -13,6 +13,10 @@ on: jobs: fetch-licenses: runs-on: ubuntu-latest + env: + DT_STAMP: '' + GENFILES_CHANGED: 'false' + SRC_CHANGED: 'false' permissions: contents: write pull-requests: write @@ -84,6 +88,10 @@ jobs: fi echo "GENFILES_CHANGED=$genfiles_changed" >> $GITHUB_ENV + - name: Generate Pull Request description + if: ${{ env.GENFILES_CHANGED == 'true' || github.event.inputs.force_run == 'true' }} + run: go run ./cmd pr-body -o "${RUNNER_TEMP}/license-pr-body.md" + - name: Create Pull Request if: ${{ env.GENFILES_CHANGED == 'true' || github.event.inputs.force_run == 'true' }} uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 @@ -93,5 +101,5 @@ jobs: branch: auto-update-licenses base: main title: "Update SPDX license files (${{ env.DT_STAMP }})" - body: "The files in this PR are auto-generated by the [fetch-licenses](./.git/workflows/fetch-license.yaml) workflow when it runs the `extract` command defined in [cmd/main.go](./cmd/main.go). It updates SPDX licenses based on the latest released set in the [spdx/license-list-data](https://github.com/spdx/license-list-data) repository maintained by [SPDX](https://spdx.org/licenses/). \n\nTODO: [spdxexp/spdxlicenses/license_ranges.go](./spdxexp/spdxlicenses/license_range.go) has to be updated manually." + body-path: ${{ runner.temp }}/license-pr-body.md labels: 'auto-update,licenses' diff --git a/cmd/doc.go b/cmd/doc.go index 33da144..c53df81 100644 --- a/cmd/doc.go +++ b/cmd/doc.go @@ -4,9 +4,8 @@ The source data needs to be manually updated by copying the licenses.json file f https://github.com/spdx/license-list-data/blob/main/json/licenses.json and exceptions.json file from https://github.com/spdx/license-list-data/blob/main/json/exceptions.json. -After running the extract command, the license_ids.json, deprecated_ids.json, and exception_ids.json -files will be overwritten with the extracted ids. These license ids can then be used to update the -spdxexp/license.go file. +After running the extract command, the generated license, deprecated license, exception, and license +range Go files in spdxexp/spdxlicenses will be overwritten with data derived from the SPDX license list. Command to run all extractions (run command from the /cmd directory): @@ -16,7 +15,7 @@ Command to run all extractions (run command from the /cmd directory): Usage options: -h: prints this help message - -l: Extract license ids + -l: Extract licenses and ranges -e: Extract exception ids */ package main diff --git a/cmd/license.go b/cmd/license.go index 22f1366..3fb334f 100644 --- a/cmd/license.go +++ b/cmd/license.go @@ -24,11 +24,9 @@ type License struct { IsOsiApproved bool `json:"isOsiApproved"` } -// extractLicenseIDs reads the official licenses.json file copied from spdx/license-list-data -// and writes two files, license_ids.json and deprecated_license_ids.json, containing just -// the license IDs and deprecated license IDs, respectively. It returns an error if it -// encounters one. -func extractLicenseIDs() error { +// extractLicenses reads the official licenses.json file copied from spdx/license-list-data +// and generates the active license, deprecated license, and license range Go files. +func extractLicenses() error { // open file file, err := os.Open("licenses.json") if err != nil { @@ -56,13 +54,19 @@ func extractLicenseIDs() error { } // generate the GetLicenses() function in get_licenses.go - getLicensesContents := []byte(`package spdxlicenses + getLicensesContents := fmt.Appendf(nil, `package spdxlicenses // Code generated by go-spdx cmd/license.go. DO NOT EDIT. // Source: https://github.com/spdx/license-list-data specifies official SPDX license list. import "strings" +// LicenseListVersion returns the SPDX License List version from which the +// generated license IDs were sourced. +func LicenseListVersion() string { + return %q +} + // IsActiveLicense does a case-insensitive lookup for the license id in the active licenses map. // It returns true and the case-sensitive ID if found, otherwise false and the original id. func IsActiveLicense(id string) (bool, string) { @@ -85,7 +89,7 @@ func GetLicensesMap() map[string]string { // GetLicenses returns a slice of active license IDs. func GetLicenses() []string { return []string{ -`) +`, licenseData.Version) for _, id := range activeLicenseIDs { getLicensesContents = append(getLicensesContents, ` "`+id+`", `...) @@ -175,5 +179,9 @@ func GetDeprecated() []string { } fmt.Println("Writing `../spdxexp/spdxlicenses/get_deprecated.go`... COMPLETE") + if err := generateLicenseRanges(licenseData.Licenses, "../spdxexp/spdxlicenses/license_ranges.go"); err != nil { + return fmt.Errorf("generate license ranges: %w", err) + } + return nil } diff --git a/cmd/license_ranges.go b/cmd/license_ranges.go new file mode 100644 index 0000000..d25c2f2 --- /dev/null +++ b/cmd/license_ranges.go @@ -0,0 +1,191 @@ +package main + +import ( + "fmt" + "go/format" + "os" + "regexp" + "sort" + "strconv" + "strings" +) + +type licenseRangeFamily struct { + base string + suffix string + inheritBaseVersions bool +} + +var includedLicenseRangeFamilies = []licenseRangeFamily{ + {base: "Brian-Gladman", suffix: "-Clause"}, + {base: "MPL", suffix: "-no-copyleft-exception", inheritBaseVersions: true}, +} + +var excludedLicenseRangeFamilyBases = map[string]struct{}{ + "Spencer": {}, +} + +const licenseVersionPattern = `\d+(?:\.\d+)*(?:[a-z])?` + +var standardLicenseVersionPattern = regexp.MustCompile("^(.+)-(" + licenseVersionPattern + ")$") + +func generateLicenseRanges(licenses []License, outputPath string) error { + ranges := buildLicenseRanges(licenses) + contents := []byte(`package spdxlicenses + +// Code generated by go-spdx cmd/license_ranges.go. DO NOT EDIT. +// Source: https://github.com/spdx/license-list-data specifies official SPDX license list. + +// LicenseRanges returns license version groups in ascending version order. +// IDs within the same version group are equivalent (for example, GPL-2.0 and GPL-2.0-only). +func LicenseRanges() [][][]string { + return [][][]string{ +`) + for _, family := range ranges { + contents = append(contents, "\t\t{\n"...) + for _, version := range family { + contents = append(contents, "\t\t\t{\n"...) + for _, id := range version { + contents = append(contents, fmt.Sprintf("\t\t\t\t%q,\n", id)...) + } + contents = append(contents, "\t\t\t},\n"...) + } + contents = append(contents, "\t\t},\n"...) + } + contents = append(contents, "\t}\n}\n"...) + + contents, err := format.Source(contents) + if err != nil { + return fmt.Errorf("format generated license_ranges.go: %w", err) + } + if err := os.WriteFile(outputPath, contents, 0600); err != nil { + return err + } + fmt.Println("Writing `../spdxexp/spdxlicenses/license_ranges.go`... COMPLETE") + return nil +} + +func buildLicenseRanges(licenses []License) [][][]string { + licenseIDs := make(map[string]struct{}, len(licenses)) + for _, license := range licenses { + licenseIDs[license.LicenseID] = struct{}{} + } + + families := buildLicenseRangeFamilies(licenses, includedLicenseRangeFamilies, excludedLicenseRangeFamilyBases) + ranges := make([][][]string, 0, len(families)) + for _, family := range families { + pattern := regexp.MustCompile("^" + regexp.QuoteMeta(family.base) + "-(" + licenseVersionPattern + ")" + regexp.QuoteMeta(family.suffix) + "$") + versions := make(map[string][]string) + for id := range licenseIDs { + matches := pattern.FindStringSubmatch(id) + if matches == nil { + continue + } + versions[matches[1]] = append(versions[matches[1]], id) + if family.suffix == "" { + onlyID := id + "-only" + if _, ok := licenseIDs[onlyID]; ok { + versions[matches[1]] = append(versions[matches[1]], onlyID) + } + } + } + if family.inheritBaseVersions { + basePattern := regexp.MustCompile("^" + regexp.QuoteMeta(family.base) + "-(" + licenseVersionPattern + ")$") + for id := range licenseIDs { + matches := basePattern.FindStringSubmatch(id) + if matches != nil { + if _, overridden := versions[matches[1]]; !overridden { + versions[matches[1]] = []string{id} + } + } + } + } + + versionNames := make([]string, 0, len(versions)) + for version := range versions { + versionNames = append(versionNames, version) + } + sort.Slice(versionNames, func(i, j int) bool { + return compareLicenseVersions(versionNames[i], versionNames[j]) < 0 + }) + + licenseGroup := make([][]string, 0, len(versionNames)) + for _, version := range versionNames { + sort.Strings(versions[version]) + licenseGroup = append(licenseGroup, versions[version]) + } + if len(licenseGroup) > 1 { + ranges = append(ranges, licenseGroup) + } + } + return ranges +} + +func buildLicenseRangeFamilies(licenses []License, included []licenseRangeFamily, excluded map[string]struct{}) []licenseRangeFamily { + versionsByBase := make(map[string]map[string]struct{}) + for _, license := range licenses { + matches := standardLicenseVersionPattern.FindStringSubmatch(license.LicenseID) + if matches == nil { + continue + } + if _, excluded := excluded[matches[1]]; excluded { + continue + } + if versionsByBase[matches[1]] == nil { + versionsByBase[matches[1]] = make(map[string]struct{}) + } + versionsByBase[matches[1]][matches[2]] = struct{}{} + } + + familiesByKey := make(map[string]licenseRangeFamily, len(versionsByBase)+len(included)) + for base, versions := range versionsByBase { + if len(versions) > 1 { + familiesByKey[base+"\x00"] = licenseRangeFamily{base: base} + } + } + for _, family := range included { + familiesByKey[family.base+"\x00"+family.suffix] = family + } + + families := make([]licenseRangeFamily, 0, len(familiesByKey)) + for _, family := range familiesByKey { + families = append(families, family) + } + sort.Slice(families, func(i, j int) bool { + firstBase := strings.ToLower(families[i].base) + secondBase := strings.ToLower(families[j].base) + if firstBase != secondBase { + return firstBase < secondBase + } + if families[i].base != families[j].base { + return families[i].base < families[j].base + } + return families[i].suffix < families[j].suffix + }) + return families +} + +func compareLicenseVersions(first, second string) int { + firstParts := strings.FieldsFunc(first, func(r rune) bool { return r == '.' }) + secondParts := strings.FieldsFunc(second, func(r rune) bool { return r == '.' }) + for i := 0; i < len(firstParts) && i < len(secondParts); i++ { + firstNumber, firstSuffix := splitVersionPart(firstParts[i]) + secondNumber, secondSuffix := splitVersionPart(secondParts[i]) + if firstNumber != secondNumber { + return firstNumber - secondNumber + } + if firstSuffix != secondSuffix { + return strings.Compare(firstSuffix, secondSuffix) + } + } + return len(firstParts) - len(secondParts) +} + +func splitVersionPart(part string) (int, string) { + index := 0 + for index < len(part) && part[index] >= '0' && part[index] <= '9' { + index++ + } + number, _ := strconv.Atoi(part[:index]) + return number, part[index:] +} diff --git a/cmd/license_ranges_test.go b/cmd/license_ranges_test.go new file mode 100644 index 0000000..03b3c24 --- /dev/null +++ b/cmd/license_ranges_test.go @@ -0,0 +1,124 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateLicenseRanges(t *testing.T) { + configureExampleLicenseRangeFamilies(t) + + outputPath := filepath.Join(t.TempDir(), "license_ranges.go") + licenses := []License{ + {LicenseID: "Example-10.0"}, + {LicenseID: "Example-2.0-only"}, + {LicenseID: "Example-1.0-or-later"}, + {LicenseID: "Example-1.0"}, + {LicenseID: "Example-2.0"}, + {LicenseID: "Example-3.0-special"}, + {LicenseID: "Example-1.0-special"}, + {LicenseID: "SingleVersion-2.0"}, + } + + require.NoError(t, generateLicenseRanges(licenses, outputPath)) + // #nosec G304 -- path is created within t.TempDir for this test. + generated, err := os.ReadFile(outputPath) + require.NoError(t, err) + golden, err := os.ReadFile("testdata/license_ranges.golden") + require.NoError(t, err) + + assert.Equal(t, string(golden), string(generated)) +} + +func TestBuildLicenseRanges(t *testing.T) { + configureExampleLicenseRangeFamilies(t) + + licenses := []License{ + {LicenseID: "Example-10.0"}, + {LicenseID: "Example-2.0-only"}, + {LicenseID: "Example-1.0-or-later"}, + {LicenseID: "Example-1.0"}, + {LicenseID: "Example-2.0"}, + {LicenseID: "Example-3.0-special"}, + {LicenseID: "Example-1.0-special"}, + {LicenseID: "SingleVersion-2.0"}, + } + + licenseRanges := buildLicenseRanges(licenses) + + assert.Equal(t, [][][]string{ + {{"Example-1.0"}, {"Example-2.0", "Example-2.0-only"}, {"Example-10.0"}}, + {{"Example-1.0-special"}, {"Example-2.0"}, {"Example-3.0-special"}, {"Example-10.0"}}, + }, licenseRanges) + + assert.NotContains(t, licenseRanges, [][]string{{"SingleVersion-2.0"}}) +} + +func configureExampleLicenseRangeFamilies(t *testing.T) { + previousIncluded := includedLicenseRangeFamilies + previousExcluded := excludedLicenseRangeFamilyBases + includedLicenseRangeFamilies = []licenseRangeFamily{ + {base: "Example"}, + {base: "Example", suffix: "-special", inheritBaseVersions: true}, + } + excludedLicenseRangeFamilyBases = nil + t.Cleanup(func() { + includedLicenseRangeFamilies = previousIncluded + excludedLicenseRangeFamilyBases = previousExcluded + }) +} + +func TestBuildLicenseRangeFamilies(t *testing.T) { + licenses := []License{ + {LicenseID: "Apache-1.0"}, + {LicenseID: "Apache-2.0"}, + {LicenseID: "ASWF-1.0"}, + {LicenseID: "ASWF-2.0"}, + {LicenseID: "Example-1.0"}, + {LicenseID: "Example-2.0"}, + {LicenseID: "Excluded-1.0"}, + {LicenseID: "Excluded-2.0"}, + {LicenseID: "Singleton-1.0"}, + {LicenseID: "Special-2-Clause"}, + {LicenseID: "Special-3-Clause"}, + } + included := []licenseRangeFamily{{base: "Special", suffix: "-Clause"}} + excluded := map[string]struct{}{"Excluded": {}} + + tests := []struct { + name string + licenses []License + included []licenseRangeFamily + excluded map[string]struct{} + licenseRangeFamilies []licenseRangeFamily + }{ + {"no version", []License{{LicenseID: "Example"}}, nil, nil, []licenseRangeFamily{}}, + {"only one version", []License{{LicenseID: "Singleton-1.0"}}, nil, nil, []licenseRangeFamily{}}, + {"multiple versions", []License{{LicenseID: "Example-1.0"}, {LicenseID: "Example-2.0"}}, nil, nil, []licenseRangeFamily{{base: "Example"}}}, + {"included with suffix", []License{{LicenseID: "Special-2-Clause"}, {LicenseID: "Special-3-Clause"}}, included, nil, []licenseRangeFamily{{base: "Special", suffix: "-Clause"}}}, + {"excluded base", []License{{LicenseID: "Excluded-1.0"}, {LicenseID: "Excluded-2.0"}}, nil, excluded, []licenseRangeFamily{}}, + {"multiple families", licenses, included, excluded, []licenseRangeFamily{ + {base: "Apache"}, + {base: "ASWF"}, + {base: "Example"}, + {base: "Special", suffix: "-Clause"}, + }}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + licenseRangeFamilies := buildLicenseRangeFamilies(test.licenses, test.included, test.excluded) + assert.Equal(t, test.licenseRangeFamilies, licenseRangeFamilies) + }) + } +} + +func TestCompareLicenseVersions(t *testing.T) { + assert.Negative(t, compareLicenseVersions("1.3a", "1.3c")) + assert.Negative(t, compareLicenseVersions("2.0", "10.0")) + assert.Zero(t, compareLicenseVersions("2.0", "2.0")) +} diff --git a/cmd/main.go b/cmd/main.go index 31731ca..a8c587b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,8 +18,9 @@ func main() { } flagSet := flag.NewFlagSet("run", flag.ExitOnError) - extractLicenses := flagSet.Bool("l", false, "Should license ids be extracted?") + shouldExtractLicenses := flagSet.Bool("l", false, "Should licenses and ranges be extracted?") extractExceptions := flagSet.Bool("e", false, "Should exception ids be extracted?") + outputPath := flagSet.String("o", "", "Output path") help := flagSet.Bool("h", false, "Show help") err := flagSet.Parse(argsRemainder) @@ -30,16 +31,16 @@ func main() { switch cmd { case "extract": - if *help || (!*extractLicenses && !*extractExceptions) { + if *help || (!*shouldExtractLicenses && !*extractExceptions) { writeHelpMessage() os.Exit(0) } - if *extractLicenses { + if *shouldExtractLicenses { fmt.Println("-------------------------") - fmt.Println("Extracting license ids...") - err := extractLicenseIDs() + fmt.Println("Extracting licenses and ranges...") + err := extractLicenses() if err != nil { - fmt.Printf("error extracting license ids: %v\n", err) + fmt.Printf("error extracting licenses and ranges: %v\n", err) os.Exit(1) } fmt.Println("Done!") @@ -55,6 +56,15 @@ func main() { fmt.Println("Done!") fmt.Println("---------------------------") } + case "pr-body": + if *outputPath == "" { + fmt.Println("error generating PR body: -o is required") + os.Exit(1) + } + if err := generatePRBody(*outputPath); err != nil { + fmt.Printf("error generating PR body: %v\n", err) + os.Exit(1) + } default: writeHelpMessage() os.Exit(0) @@ -68,16 +78,15 @@ func writeHelpMessage() { fmt.Println("https://github.com/spdx/license-list-data/blob/main/json/licenses.json and exceptions.json") fmt.Println("file from https://github.com/spdx/license-list-data/blob/main/json/exceptions.json.") fmt.Println("") - fmt.Println("After running the extract command, the license_ids.json, deprecated_ids.json, and exception_ids.json") - fmt.Println("files will be overwritten with the extracted ids. These license ids can then be used to update the") - fmt.Println("spdxexp/license.go file.") + fmt.Println("After running the extract command, the generated license, deprecated license, exception, and license") + fmt.Println("range Go files in spdxexp/spdxlicenses will be overwritten with data derived from the SPDX license list.") fmt.Println("") fmt.Println("Command to run all extractions (run command from the /cmd directory):") fmt.Println(" `go run . extract -l -e`") fmt.Println("") fmt.Println("Usage options:") fmt.Println(" -h: prints this help message") - fmt.Println(" -l: Extract license ids") + fmt.Println(" -l: Extract licenses and ranges") fmt.Println(" -e: Extract exception ids") fmt.Println("") os.Exit(0) diff --git a/cmd/pr_body.go b/cmd/pr_body.go new file mode 100644 index 0000000..e95e74e --- /dev/null +++ b/cmd/pr_body.go @@ -0,0 +1,206 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" +) + +func generatePRBody(outputPath string) error { + repositoryRootOutput, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + if err != nil { + return fmt.Errorf("find repository root: %w", err) + } + repositoryRoot := strings.TrimSpace(string(repositoryRootOutput)) + + var previousLicenses, currentLicenses LicenseData + if err := decodePreviousJSON(repositoryRoot, "cmd/licenses.json", &previousLicenses); err != nil { + return err + } + if err := decodeJSONFile(filepath.Join(repositoryRoot, "cmd/licenses.json"), ¤tLicenses); err != nil { + return err + } + + var previousExceptions, currentExceptions ExceptionData + if err := decodePreviousJSON(repositoryRoot, "cmd/exceptions.json", &previousExceptions); err != nil { + return err + } + if err := decodeJSONFile(filepath.Join(repositoryRoot, "cmd/exceptions.json"), ¤tExceptions); err != nil { + return err + } + + body := buildPRBody(previousLicenses, currentLicenses, previousExceptions, currentExceptions) + if err := os.WriteFile(outputPath, []byte(body), 0600); err != nil { + return fmt.Errorf("write PR body: %w", err) + } + return nil +} + +func decodePreviousJSON(repositoryRoot, path string, destination any) error { + // #nosec G204 -- repositoryRoot is discovered by Git and path is an internal constant. + contents, err := exec.Command("git", "-C", repositoryRoot, "show", "HEAD:"+path).Output() + if err != nil { + return fmt.Errorf("read previous %s: %w", path, err) + } + if err := json.Unmarshal(contents, destination); err != nil { + return fmt.Errorf("decode previous %s: %w", path, err) + } + return nil +} + +func decodeJSONFile(path string, destination any) error { + // #nosec G304 -- path is built from the Git repository root and an internal constant. + contents, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + if err := json.Unmarshal(contents, destination); err != nil { + return fmt.Errorf("decode %s: %w", path, err) + } + return nil +} + +func buildPRBody(previousLicenses, currentLicenses LicenseData, previousExceptions, currentExceptions ExceptionData) string { + var body bytes.Buffer + body.WriteString("> [!WARNING]\n") + body.WriteString("> Do not push commits directly to this PR's `auto-update-licenses` branch or manually edit this description. The nightly workflow updates both and may overwrite manual changes. For changes that depend on this update, open a stacked PR with `auto-update-licenses` as its base. For generator or workflow fixes that must persist, open a separate PR against `main`, merge it, and rerun this workflow.\n\n") + body.WriteString("This PR was generated by the [fetch-licenses](./.github/workflows/fetch-licenses.yaml) workflow from [spdx/license-list-data](https://github.com/spdx/license-list-data).\n\n") + fmt.Fprintf(&body, "- **Previous SPDX License List Version:** `%s`\n", previousLicenses.Version) + fmt.Fprintf(&body, "- **New SPDX License List Version:** `%s`\n\n", currentLicenses.Version) + + body.WriteString("## SPDX changes\n\n") + + previousLicenseStatuses := licenseStatuses(previousLicenses.Licenses) + currentLicenseStatuses := licenseStatuses(currentLicenses.Licenses) + writeChanges(&body, "Licenses", addedIDs(previousLicenseStatuses, currentLicenseStatuses), removedIDs(previousLicenseStatuses, currentLicenseStatuses)) + + previousExceptionStatuses := exceptionStatuses(previousExceptions.Exceptions) + currentExceptionStatuses := exceptionStatuses(currentExceptions.Exceptions) + writeChanges(&body, "Exceptions", addedIDs(previousExceptionStatuses, currentExceptionStatuses), removedIDs(previousExceptionStatuses, currentExceptionStatuses)) + + body.WriteString("### Deprecations\n\n") + writeIDList(&body, "Newly deprecated licenses", changedStatusIDs(previousLicenseStatuses, currentLicenseStatuses, true)) + writeIDList(&body, "Reactivated licenses", changedStatusIDs(previousLicenseStatuses, currentLicenseStatuses, false)) + writeIDList(&body, "Newly deprecated exceptions", changedStatusIDs(previousExceptionStatuses, currentExceptionStatuses, true)) + writeIDList(&body, "Reactivated exceptions", changedStatusIDs(previousExceptionStatuses, currentExceptionStatuses, false)) + + body.WriteString("\n### License range families\n\n") + writeIDList(&body, "Added", changedLicenseRangeFamilies(previousLicenses.Licenses, currentLicenses.Licenses)) + writeIDList(&body, "Removed", removedLicenseRangeFamilies(previousLicenses.Licenses, currentLicenses.Licenses)) + + body.WriteString("\n## Review instructions (human and Copilot)\n\n") + body.WriteString("Use the summary above as the expected change set and report any discrepancy between it, the source JSON, and the generated files. Focus review on these invariants:\n\n") + body.WriteString("- Active and deprecated license IDs appear in the matching generated collection. Non-deprecated exception IDs appear in the generated exception collection; deprecated exceptions are intentionally excluded.\n") + body.WriteString("- Any added license range family contains at least two versions, keeps equivalent `-only` IDs in the same version group, and is sorted in semantic version order.\n") + body.WriteString("- Copilot: If **Added** or **Removed** under **License range families** is not `None`, leave an inline review comment on the relevant generated family block beginning with **Human review required: license range family changed**.\n") + body.WriteString("- Generated files contain no unrelated changes. Changes should be made in the generator or source data, not directly in generated files.\n") + body.WriteString("- Pay particular attention to IDs that moved between active and deprecated collections; these are status changes, not additions or removals.\n") + return body.String() +} + +func licenseStatuses(licenses []License) map[string]bool { + statuses := make(map[string]bool, len(licenses)) + for _, license := range licenses { + statuses[license.LicenseID] = license.IsDeprecated + } + return statuses +} + +func exceptionStatuses(exceptions []Exception) map[string]bool { + statuses := make(map[string]bool, len(exceptions)) + for _, exception := range exceptions { + statuses[exception.LicenseID] = exception.IsDeprecated + } + return statuses +} + +func addedIDs(previous, current map[string]bool) []string { + return mapDifference(current, previous) +} + +func removedIDs(previous, current map[string]bool) []string { + return mapDifference(previous, current) +} + +func mapDifference(first, second map[string]bool) []string { + ids := make([]string, 0) + for id := range first { + if _, exists := second[id]; !exists { + ids = append(ids, id) + } + } + sort.Strings(ids) + return ids +} + +func changedStatusIDs(previous, current map[string]bool, deprecated bool) []string { + ids := make([]string, 0) + for id, currentStatus := range current { + previousStatus, exists := previous[id] + if exists && previousStatus != currentStatus && currentStatus == deprecated { + ids = append(ids, id) + } + } + sort.Strings(ids) + return ids +} + +func changedLicenseRangeFamilies(previous, current []License) []string { + previousFamilies := buildLicenseRangeFamilies(previous, includedLicenseRangeFamilies, excludedLicenseRangeFamilyBases) + currentFamilies := buildLicenseRangeFamilies(current, includedLicenseRangeFamilies, excludedLicenseRangeFamilyBases) + previousKeys := make(map[string]struct{}, len(previousFamilies)) + for _, family := range previousFamilies { + previousKeys[family.base+"\x00"+family.suffix] = struct{}{} + } + + added := make([]string, 0) + for _, family := range currentFamilies { + if _, exists := previousKeys[family.base+"\x00"+family.suffix]; !exists { + added = append(added, family.base+"-"+family.suffix) + } + } + return added +} + +func removedLicenseRangeFamilies(previous, current []License) []string { + previousFamilies := buildLicenseRangeFamilies(previous, includedLicenseRangeFamilies, excludedLicenseRangeFamilyBases) + currentFamilies := buildLicenseRangeFamilies(current, includedLicenseRangeFamilies, excludedLicenseRangeFamilyBases) + currentKeys := make(map[string]struct{}, len(currentFamilies)) + for _, family := range currentFamilies { + currentKeys[family.base+"\x00"+family.suffix] = struct{}{} + } + + removed := make([]string, 0) + for _, family := range previousFamilies { + if _, exists := currentKeys[family.base+"\x00"+family.suffix]; !exists { + removed = append(removed, family.base+"-"+family.suffix) + } + } + return removed +} + +func writeChanges(body *bytes.Buffer, heading string, added, removed []string) { + fmt.Fprintf(body, "### %s\n\n", heading) + writeIDList(body, "Added", added) + writeIDList(body, "Removed", removed) + body.WriteString("\n") +} + +func writeIDList(body *bytes.Buffer, label string, ids []string) { + fmt.Fprintf(body, "- **%s:** ", label) + if len(ids) == 0 { + body.WriteString("None\n") + return + } + formatted := make([]string, len(ids)) + for index, id := range ids { + formatted[index] = "`" + id + "`" + } + body.WriteString(strings.Join(formatted, ", ")) + body.WriteByte('\n') +} diff --git a/cmd/pr_body_test.go b/cmd/pr_body_test.go new file mode 100644 index 0000000..9fcdcf3 --- /dev/null +++ b/cmd/pr_body_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuildPRBody(t *testing.T) { + previousLicenses := LicenseData{ + Version: "3.28.0", + Licenses: []License{ + {LicenseID: "Example-1.0"}, + {LicenseID: "Removed-1.0"}, + {LicenseID: "Now-Deprecated"}, + {LicenseID: "Reactivated", IsDeprecated: true}, + }, + } + currentLicenses := LicenseData{ + Version: "3.29.0", + Licenses: []License{ + {LicenseID: "Example-1.0"}, + {LicenseID: "Example-2.0"}, + {LicenseID: "Now-Deprecated", IsDeprecated: true}, + {LicenseID: "Reactivated"}, + }, + } + previousExceptions := ExceptionData{Exceptions: []Exception{{LicenseID: "Old-exception"}}} + currentExceptions := ExceptionData{Exceptions: []Exception{{LicenseID: "New-exception"}}} + + body := buildPRBody(previousLicenses, currentLicenses, previousExceptions, currentExceptions) + + assert.True(t, strings.HasPrefix(body, "> [!WARNING]\n")) + assert.Contains(t, body, "open a stacked PR with `auto-update-licenses` as its base") + assert.Contains(t, body, "- **Previous SPDX License List Version:** `3.28.0`") + assert.Contains(t, body, "- **New SPDX License List Version:** `3.29.0`") + assert.Contains(t, body, "- **Added:** `Example-2.0`") + assert.Contains(t, body, "- **Removed:** `Removed-1.0`") + assert.Contains(t, body, "- **Added:** `New-exception`") + assert.Contains(t, body, "- **Removed:** `Old-exception`") + assert.Contains(t, body, "- **Newly deprecated licenses:** `Now-Deprecated`") + assert.Contains(t, body, "- **Reactivated licenses:** `Reactivated`") + assert.Contains(t, body, "- **Added:** `Example-`") + assert.Contains(t, body, "Human review required: license range family changed") +} diff --git a/cmd/testdata/license_ranges.golden b/cmd/testdata/license_ranges.golden new file mode 100644 index 0000000..b2f3c25 --- /dev/null +++ b/cmd/testdata/license_ranges.golden @@ -0,0 +1,37 @@ +package spdxlicenses + +// Code generated by go-spdx cmd/license_ranges.go. DO NOT EDIT. +// Source: https://github.com/spdx/license-list-data specifies official SPDX license list. + +// LicenseRanges returns license version groups in ascending version order. +// IDs within the same version group are equivalent (for example, GPL-2.0 and GPL-2.0-only). +func LicenseRanges() [][][]string { + return [][][]string{ + { + { + "Example-1.0", + }, + { + "Example-2.0", + "Example-2.0-only", + }, + { + "Example-10.0", + }, + }, + { + { + "Example-1.0-special", + }, + { + "Example-2.0", + }, + { + "Example-3.0-special", + }, + { + "Example-10.0", + }, + }, + } +} diff --git a/spdxexp/license_test.go b/spdxexp/license_test.go index 5ec8d8e..f8213e1 100644 --- a/spdxexp/license_test.go +++ b/spdxexp/license_test.go @@ -121,22 +121,28 @@ func TestGetLicenseRange(t *testing.T) { tests := []struct { name string id string - licenseRange *licenseRange + licenses []string + versionGroup int + licenseIndex int }{ - {"no multi-element ranges", "Apache-2.0", &licenseRange{ - licenses: []string{"Apache-2.0"}, - location: map[uint8]int{licenseGroup: 2, versionGroup: 2, licenseIndex: 0}}}, - {"multi-element ranges", "GFDL-1.2-only", &licenseRange{ - licenses: []string{"GFDL-1.2", "GFDL-1.2-only"}, - location: map[uint8]int{licenseGroup: 21, versionGroup: 1, licenseIndex: 1}}}, - {"no range", "Bison-exception-2.2", nil}, + {"no multi-element ranges", "Apache-2.0", []string{"Apache-2.0"}, 2, 0}, + {"multi-element ranges", "GFDL-1.2-only", []string{"GFDL-1.2", "GFDL-1.2-only"}, 1, 1}, + {"no range", "Bison-exception-2.2", nil, 0, 0}, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { licenseRange := getLicenseRange(test.id) - assert.Equal(t, test.licenseRange, licenseRange) + if test.licenses == nil { + assert.Nil(t, licenseRange) + return + } + if assert.NotNil(t, licenseRange) { + assert.Equal(t, test.licenses, licenseRange.licenses) + assert.Equal(t, test.versionGroup, licenseRange.location[versionGroup]) + assert.Equal(t, test.licenseIndex, licenseRange.location[licenseIndex]) + } }) } } diff --git a/spdxexp/satisfies_test.go b/spdxexp/satisfies_test.go index 6955d95..a7da479 100644 --- a/spdxexp/satisfies_test.go +++ b/spdxexp/satisfies_test.go @@ -559,7 +559,7 @@ func TestSatisfies(t *testing.T) { {"MIT AND Apache-2.0 satisfies [MIT, Apache-1.0, Apache-2.0]", "MIT AND Apache-2.0", []string{"MIT", "Apache-1.0", "Apache-2.0"}, true, nil}, {"Apache-1.0+ satisfies [Apache-2.0]", "Apache-1.0+", []string{"Apache-2.0"}, true, nil}, - {"Apache-1.0+ satisfies [Apache-2.0+]", "Apache-1.0+", []string{"Apache-2.0+"}, true, nil}, // TODO: Fails here but passes js + {"Apache-1.0+ satisfies [Apache-2.0+]", "Apache-1.0+", []string{"Apache-2.0+"}, true, nil}, {"! Apache-1.0 satisfies [Apache-2.0+]", "Apache-1.0", []string{"Apache-2.0+"}, false, nil}, {"Apache-2.0 satisfies [Apache-2.0+]", "Apache-2.0", []string{"Apache-2.0+"}, true, nil}, {"! Apache-3.0 satisfies [Apache-2.0+]", "Apache-3.0", []string{"Apache-2.0+"}, false, errors.New("unknown license 'Apache-3.0' at offset 0")}, @@ -578,8 +578,8 @@ func TestSatisfies(t *testing.T) { {"MIT satisfies [ISC, MIT]", "MIT", []string{"ISC", "MIT"}, true, nil}, {"Zlib satisfies [ISC, MIT, Zlib]", "Zlib", []string{"ISC", "MIT", "Zlib"}, true, nil}, {"! GPL-3.0 satisfies [ISC, MIT]", "GPL-3.0", []string{"ISC", "MIT"}, false, nil}, - {"GPL-2.0 satisfies [GPL-2.0+]", "GPL-2.0", []string{"GPL-2.0+"}, true, nil}, // TODO: Fails here but passes js - {"GPL-2.0 satisfies [GPL-2.0-or-later]", "GPL-2.0", []string{"GPL-2.0-or-later"}, true, nil}, // TODO: Fails here and js + {"GPL-2.0 satisfies [GPL-2.0+]", "GPL-2.0", []string{"GPL-2.0+"}, true, nil}, + {"GPL-2.0 satisfies [GPL-2.0-or-later]", "GPL-2.0", []string{"GPL-2.0-or-later"}, true, nil}, {"GPL-3.0 satisfies [GPL-2.0+]", "GPL-3.0", []string{"GPL-2.0+"}, true, nil}, {"GPL-1.0-or-later satisfies [GPL-2.0-or-later]", "GPL-1.0-or-later", []string{"GPL-2.0-or-later"}, true, nil}, {"GPL-1.0+ satisfies [GPL-2.0+]", "GPL-1.0+", []string{"GPL-2.0+"}, true, nil}, @@ -613,7 +613,7 @@ func TestSatisfies(t *testing.T) { {"MIT satisfies [GPL-2.0, MIT, MIT, ISC]", "MIT", []string{"GPL-2.0", "MIT", "MIT", "ISC"}, true, nil}, {"MIT AND ICU satisfies [MIT, GPL-2.0, ISC, Apache-2.0, ICU]", - "MIT AND ICU", []string{"MIT", "GPL-2.0", "ISC", "Apache-2.0", "ICU"}, true, nil}, // TODO: This says true and the js version returns true, but it shouldn't. + "MIT AND ICU", []string{"MIT", "GPL-2.0", "ISC", "Apache-2.0", "ICU"}, true, nil}, {"! (MIT AND GPL-2.0) satisfies [ISC, GPL-2.0]", "(MIT AND GPL-2.0)", []string{"ISC", "GPL-2.0"}, false, nil}, {"! MIT AND (GPL-2.0 OR ISC) satisfies [MIT]", @@ -737,121 +737,122 @@ func TestExpandOr(t *testing.T) { {andOrAndExpression().name, andOrAndExpression().node, andOrAndExpression().expanded}, {oPAndCPOrOPAndCPExpression().name, oPAndCPOrOPAndCPExpression().node, oPAndCPOrOPAndCPExpression().expanded}, - // TODO: Uncomment kitchen sink test when license plus, exception, license ref, and document ref are supported. - // {"kitchen sink", - // // " (MIT AND Apache-1.0+) OR DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2 OR (GPL-2.0 WITH Bison-exception-2.2)", - // &node{ - // role: expressionNode, - // exp: &expressionNodePartial{ - // left: &node{ - // role: expressionNode, - // exp: &expressionNodePartial{ - // left: &node{ - // role: licenseNode, - // exp: nil, - // lic: &licenseNodePartial{ - // license: "MIT", - // hasPlus: false, - // hasException: false, - // exception: "", - // }, - // ref: nil, - // }, - // conjunction: "and", - // right: &node{ - // role: licenseNode, - // exp: nil, - // lic: &licenseNodePartial{ - // license: "Apache-1.0", - // hasPlus: true, - // hasException: false, - // exception: "", - // }, - // ref: nil, - // }, - // }, - // lic: nil, - // ref: nil, - // }, - // conjunction: "or", - // right: &node{ - // role: expressionNode, - // exp: &expressionNodePartial{ - // left: &node{ - // role: licenseRefNode, - // exp: nil, - // lic: nil, - // ref: &referenceNodePartial{ - // hasDocumentRef: true, - // documentRef: "spdx-tool-1.2", - // licenseRef: "MIT-Style-2", - // }, - // }, - // conjunction: "or", - // right: &node{ - // role: licenseNode, - // exp: nil, - // lic: &licenseNodePartial{ - // license: "GPL-2.0", - // hasPlus: false, - // hasException: true, - // exception: "Bison-exception-2.2", - // }, - // ref: nil, - // }, - // }, - // lic: nil, - // ref: nil, - // }, - // }, - // lic: nil, - // ref: nil, - // }, - // // [][]string{{"MIT", "Apache-1.0+"}, {"DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2"}, {"GPL-2.0 with Bison-exception-2.2"}}}, - // [][]*node{ - // { - // { - // role: licenseNode, - // exp: nil, - // lic: &licenseNodePartial{ - // license: "MIT", hasPlus: false, - // hasException: false, exception: ""}, - // ref: nil, - // }, - // { - // role: licenseNode, - // exp: nil, - // lic: &licenseNodePartial{ - // license: "Apache-1.0", hasPlus: true, - // hasException: false, exception: ""}, - // ref: nil, - // }, - // }, - // { - // { - // role: licenseRefNode, - // exp: nil, - // lic: nil, - // ref: &referenceNodePartial{ - // hasDocumentRef: true, - // documentRef: "spdx-tool-1.2", - // licenseRef: "MIT-Style-2", - // }, - // }, - // { - // role: licenseNode, - // exp: nil, - // lic: &licenseNodePartial{ - // license: "GPL-2.0", - // hasPlus: false, - // hasException: true, - // exception: "Bison-exception-2.2", - // }, - // ref: nil, - // }, - // }, - // }, - // }, + {"kitchen sink", + // " (MIT AND Apache-1.0+) OR DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2 OR (GPL-2.0 WITH Bison-exception-2.2)", + &node{ + role: expressionNode, + exp: &expressionNodePartial{ + left: &node{ + role: expressionNode, + exp: &expressionNodePartial{ + left: &node{ + role: licenseNode, + exp: nil, + lic: &licenseNodePartial{ + license: "MIT", + hasPlus: false, + hasException: false, + exception: "", + }, + ref: nil, + }, + conjunction: "and", + right: &node{ + role: licenseNode, + exp: nil, + lic: &licenseNodePartial{ + license: "Apache-1.0", + hasPlus: true, + hasException: false, + exception: "", + }, + ref: nil, + }, + }, + lic: nil, + ref: nil, + }, + conjunction: "or", + right: &node{ + role: expressionNode, + exp: &expressionNodePartial{ + left: &node{ + role: licenseRefNode, + exp: nil, + lic: nil, + ref: &referenceNodePartial{ + hasDocumentRef: true, + documentRef: "spdx-tool-1.2", + licenseRef: "MIT-Style-2", + }, + }, + conjunction: "or", + right: &node{ + role: licenseNode, + exp: nil, + lic: &licenseNodePartial{ + license: "GPL-2.0", + hasPlus: false, + hasException: true, + exception: "Bison-exception-2.2", + }, + ref: nil, + }, + }, + lic: nil, + ref: nil, + }, + }, + lic: nil, + ref: nil, + }, + // [][]string{{"MIT", "Apache-1.0+"}, {"DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2"}, {"GPL-2.0 with Bison-exception-2.2"}}}, + [][]*node{ + { + { + role: licenseNode, + exp: nil, + lic: &licenseNodePartial{ + license: "MIT", hasPlus: false, + hasException: false, exception: ""}, + ref: nil, + }, + { + role: licenseNode, + exp: nil, + lic: &licenseNodePartial{ + license: "Apache-1.0", hasPlus: true, + hasException: false, exception: ""}, + ref: nil, + }, + }, + { + { + role: licenseRefNode, + exp: nil, + lic: nil, + ref: &referenceNodePartial{ + hasDocumentRef: true, + documentRef: "spdx-tool-1.2", + licenseRef: "MIT-Style-2", + }, + }, + }, + { + { + role: licenseNode, + exp: nil, + lic: &licenseNodePartial{ + license: "GPL-2.0", + hasPlus: false, + hasException: true, + exception: "Bison-exception-2.2", + }, + ref: nil, + }, + }, + }, + }, } for _, test := range tests { diff --git a/spdxexp/spdxlicenses/get_licenses.go b/spdxexp/spdxlicenses/get_licenses.go index 9a071fd..85b3c25 100644 --- a/spdxexp/spdxlicenses/get_licenses.go +++ b/spdxexp/spdxlicenses/get_licenses.go @@ -5,6 +5,12 @@ package spdxlicenses import "strings" +// LicenseListVersion returns the SPDX License List version from which the +// generated license IDs were sourced. +func LicenseListVersion() string { + return "3.29.0" +} + // IsActiveLicense does a case-insensitive lookup for the license id in the active licenses map. // It returns true and the case-sensitive ID if found, otherwise false and the original id. func IsActiveLicense(id string) (bool, string) { diff --git a/spdxexp/spdxlicenses/license_ranges.go b/spdxexp/spdxlicenses/license_ranges.go index e5b01be..9aec14f 100644 --- a/spdxexp/spdxlicenses/license_ranges.go +++ b/spdxexp/spdxlicenses/license_ranges.go @@ -1,12 +1,10 @@ package spdxlicenses -// LicenseRanges returns a list of license ranges. -// -// Ranges are organized into groups (referred to as license groups) of the same base license (e.g. GPL). -// Groups have sub-groups of license versions (referred to as the range) where each member is considered -// to be the same version (e.g. {GPL-2.0, GPL-2.0-only}). The sub-groups are in ascending order within -// the license group, such that the first sub-group is considered to be less than the second sub-group, -// and so on. (e.g. {{GPL-1.0}, {GPL-2.0, GPL-2.0-only}} implies {GPL-1.0} < {GPL-2.0, GPL-2.0-only}). +// Code generated by go-spdx cmd/license_ranges.go. DO NOT EDIT. +// Source: https://github.com/spdx/license-list-data specifies official SPDX license list. + +// LicenseRanges returns license version groups in ascending version order. +// IDs within the same version group are equivalent (for example, GPL-2.0 and GPL-2.0-only). func LicenseRanges() [][][]string { return [][][]string{ { @@ -29,6 +27,7 @@ func LicenseRanges() [][][]string { { { "AGPL-1.0", + "AGPL-1.0-only", }, { "AGPL-3.0", @@ -92,6 +91,14 @@ func LicenseRanges() [][][]string { "Brian-Gladman-3-Clause", }, }, + { + { + "bzip2-1.0.5", + }, + { + "bzip2-1.0.6", + }, + }, { { "CC-BY-1.0", @@ -202,6 +209,14 @@ func LicenseRanges() [][][]string { "CDDL-1.1", }, }, + { + { + "CDLA-Permissive-1.0", + }, + { + "CDLA-Permissive-2.0", + }, + }, { { "CECILL-1.0", @@ -212,6 +227,25 @@ func LicenseRanges() [][][]string { { "CECILL-2.0", }, + { + "CECILL-2.1", + }, + }, + { + { + "CERN-OHL-1.1", + }, + { + "CERN-OHL-1.2", + }, + }, + { + { + "copyleft-next-0.3.0", + }, + { + "copyleft-next-0.3.1", + }, }, { { @@ -252,6 +286,9 @@ func LicenseRanges() [][][]string { { "EUPL-1.1", }, + { + "EUPL-1.2", + }, }, { { @@ -263,11 +300,8 @@ func LicenseRanges() [][][]string { "GFDL-1.2-only", }, { - "GFDL-1.1-or-later", - "GFDL-1.2-or-later", "GFDL-1.3", "GFDL-1.3-only", - "GFDL-1.3-or-later", }, }, { @@ -280,11 +314,8 @@ func LicenseRanges() [][][]string { "GPL-2.0-only", }, { - "GPL-1.0-or-later", - "GPL-2.0-or-later", "GPL-3.0", "GPL-3.0-only", - "GPL-3.0-or-later", }, }, { @@ -295,6 +326,14 @@ func LicenseRanges() [][][]string { "HP-1989", }, }, + { + { + "LAL-1.2", + }, + { + "LAL-1.3", + }, + }, { { "LGPL-2.0", @@ -305,11 +344,16 @@ func LicenseRanges() [][][]string { "LGPL-2.1-only", }, { - "LGPL-2.0-or-later", - "LGPL-2.1-or-later", "LGPL-3.0", "LGPL-3.0-only", - "LGPL-3.0-or-later", + }, + }, + { + { + "libpng-1.6.35", + }, + { + "libpng-2.0", }, }, { @@ -359,6 +403,22 @@ func LicenseRanges() [][][]string { "MPL-2.0-no-copyleft-exception", }, }, + { + { + "MulanPSL-1.0", + }, + { + "MulanPSL-2.0", + }, + }, + { + { + "NLOD-1.0", + }, + { + "NLOD-2.0", + }, + }, { { "NPL-1.0", @@ -375,6 +435,17 @@ func LicenseRanges() [][][]string { "OFL-1.1", }, }, + { + { + "OGL-UK-1.0", + }, + { + "OGL-UK-2.0", + }, + { + "OGL-UK-3.0", + }, + }, { { "OLDAP-1.1", @@ -442,6 +513,14 @@ func LicenseRanges() [][][]string { "OSL-3.0", }, }, + { + { + "Parity-6.0.0", + }, + { + "Parity-7.0.0", + }, + }, { { "PHP-3.0", @@ -450,6 +529,14 @@ func LicenseRanges() [][][]string { "PHP-3.01", }, }, + { + { + "Python-2.0", + }, + { + "Python-2.0.1", + }, + }, { { "RPL-1.1", @@ -471,21 +558,26 @@ func LicenseRanges() [][][]string { }, { { - "YPL-1.0", + "SHL-0.5", }, { - "YPL-1.1", + "SHL-0.51", }, }, { { - "ZPL-1.1", + "TU-Berlin-1.0", }, { - "ZPL-2.0", + "TU-Berlin-2.0", }, + }, + { { - "ZPL-2.1", + "YPL-1.0", + }, + { + "YPL-1.1", }, }, { @@ -498,10 +590,13 @@ func LicenseRanges() [][][]string { }, { { - "bzip2-1.0.5", + "ZPL-1.1", }, { - "bzip2-1.0.6", + "ZPL-2.0", + }, + { + "ZPL-2.1", }, }, }