Merge pull request #12774 from cli/babakks/isolate-generated-licenses

fix(licenses): isolate generated licenses per platform (os/arch)
This commit is contained in:
Babak K. Shandiz 2026-03-03 12:12:33 +00:00 committed by GitHub
commit 7a590331b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 263 additions and 98 deletions

4
.gitignore vendored
View file

@ -18,6 +18,10 @@
# Windows resource files
/cmd/gh/*.syso
# Third-party licenses
/internal/licenses/embed/*/*
!/internal/licenses/embed/*/PLACEHOLDER
# VS Code
.vscode

View file

@ -1 +0,0 @@
License information is only available in official release builds.

View file

@ -1 +0,0 @@
placeholder

View file

@ -0,0 +1,8 @@
package licenses
import "embed"
const rootDir = "embed/darwin-amd64"
//go:embed all:embed/darwin-amd64
var embedFS embed.FS

View file

@ -0,0 +1,8 @@
package licenses
import "embed"
const rootDir = "embed/darwin-arm64"
//go:embed all:embed/darwin-arm64
var embedFS embed.FS

View file

@ -0,0 +1,15 @@
// This file is necessary to allow building on platforms that we do not have
// official release builds for. Without this, `go build` or `go install` calls
// would fail due to undefined symbols that are expected to be included in the
// build.
//go:build !(darwin && (amd64 || arm64)) && !(linux && (386 || amd64 || arm || arm64)) && !(windows && (386 || amd64 || arm64))
package licenses
import "embed"
const rootDir = ""
// embedFS is left empty to indicate there's no embedded content.
var embedFS embed.FS

View file

@ -0,0 +1,8 @@
package licenses
import "embed"
const rootDir = "embed/linux-386"
//go:embed all:embed/linux-386
var embedFS embed.FS

View file

@ -0,0 +1,8 @@
package licenses
import "embed"
const rootDir = "embed/linux-amd64"
//go:embed all:embed/linux-amd64
var embedFS embed.FS

View file

@ -0,0 +1,8 @@
package licenses
import "embed"
const rootDir = "embed/linux-arm"
//go:embed all:embed/linux-arm
var embedFS embed.FS

View file

@ -0,0 +1,8 @@
package licenses
import "embed"
const rootDir = "embed/linux-arm64"
//go:embed all:embed/linux-arm64
var embedFS embed.FS

View file

@ -0,0 +1,8 @@
package licenses
import "embed"
const rootDir = "embed/windows-386"
//go:embed all:embed/windows-386
var embedFS embed.FS

View file

@ -0,0 +1,8 @@
package licenses
import "embed"
const rootDir = "embed/windows-amd64"
//go:embed all:embed/windows-amd64
var embedFS embed.FS

View file

@ -0,0 +1,8 @@
package licenses
import "embed"
const rootDir = "embed/windows-arm64"
//go:embed all:embed/windows-arm64
var embedFS embed.FS

View file

@ -1,28 +1,31 @@
package licenses
import (
"embed"
"fmt"
"io/fs"
"path/filepath"
"path"
"sort"
"strings"
)
//go:embed embed/report.txt
var report string
//go:embed all:embed/third-party
var thirdParty embed.FS
// Content returns the full license report, including the main report and all
// third-party licenses.
func Content() string {
return content(report, thirdParty, "embed/third-party")
return content(embedFS, rootDir)
}
func content(report string, thirdPartyFS fs.ReadFileFS, root string) string {
func content(embedFS fs.ReadFileFS, rootDir string) string {
var b strings.Builder
b.WriteString(report)
reportPath := path.Join(rootDir, "report.txt")
thirdPartyPath := path.Join(rootDir, "third-party")
report, err := fs.ReadFile(embedFS, reportPath)
if err != nil {
return "License information is only available in official release builds.\n"
}
b.Write(report)
b.WriteString("\n")
// Walk the third-party directory and output each license/notice file
@ -32,8 +35,13 @@ func content(report string, thirdPartyFS fs.ReadFileFS, root string) string {
files []string
}
thirdPartyFS, err := fs.Sub(embedFS, thirdPartyPath)
if err != nil {
return b.String()
}
modules := map[string]*moduleFiles{}
fs.WalkDir(thirdPartyFS, root, func(filePath string, d fs.DirEntry, err error) error {
fs.WalkDir(thirdPartyFS, ".", func(filePath string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("failed to read embedded file %s: %w", filePath, err)
}
@ -42,18 +50,11 @@ func content(report string, thirdPartyFS fs.ReadFileFS, root string) string {
return nil
}
name := d.Name()
if name == "PLACEHOLDER" {
return nil
dir := path.Dir(filePath)
if _, ok := modules[dir]; !ok {
modules[dir] = &moduleFiles{path: dir}
}
// Module path is the directory relative to root
dir := filepath.Dir(filepath.FromSlash(filePath))
rel, _ := filepath.Rel(filepath.FromSlash(root), dir)
if _, ok := modules[rel]; !ok {
modules[rel] = &moduleFiles{path: rel}
}
modules[rel].files = append(modules[rel].files, filePath)
modules[dir].files = append(modules[dir].files, filePath)
return nil
})
@ -71,7 +72,7 @@ func content(report string, thirdPartyFS fs.ReadFileFS, root string) string {
b.WriteString("================================================================================\n\n")
for _, filePath := range mod.files {
data, err := thirdPartyFS.ReadFile(filePath)
data, err := fs.ReadFile(thirdPartyFS, filePath)
if err != nil {
continue
}

View file

@ -1,85 +1,160 @@
package licenses
import (
"path/filepath"
"strings"
"io/fs"
"testing"
"testing/fstest"
"github.com/MakeNowJust/heredoc"
"github.com/stretchr/testify/require"
)
func TestContent_reportOnly(t *testing.T) {
report := "dep1 (v1.0.0) - MIT - https://example.com\n"
fsys := fstest.MapFS{
"third-party/PLACEHOLDER": &fstest.MapFile{Data: []byte("placeholder")},
}
actualContent := content(report, fsys, "third-party")
require.True(t, strings.HasPrefix(actualContent, report), "expected output to start with report")
require.NotContains(t, actualContent, "PLACEHOLDER")
require.NotContains(t, actualContent, "====")
func TestContent(t *testing.T) {
// This test is to ensure that we don't accidentally commit actual license
// files in the repo. The embedded content is only included in release builds,
// so in a normal test build we should get a default message.
require.Equal(t, "License information is only available in official release builds.\n", Content())
}
func TestContent_singleModule(t *testing.T) {
report := "example.com/mod (v1.0.0) - MIT - https://example.com\n"
fsys := fstest.MapFS{
"third-party/example.com/mod/LICENSE": &fstest.MapFile{
Data: []byte("MIT License\n\nCopyright (c) 2024"),
func TestContent_tableTests(t *testing.T) {
tests := []struct {
name string
fsys fstest.MapFS
expected string
}{
{
name: "report only",
fsys: fstest.MapFS{
"embed/os-arch/PLACEHOLDER": &fstest.MapFile{}, // Checked-in placeholder, so it's always there.
"embed/os-arch/report.txt": &fstest.MapFile{Data: []byte("dep1 (v1.0.0) - MIT - https://example.com\n")},
},
expected: heredoc.Doc(`
dep1 (v1.0.0) - MIT - https://example.com
`),
},
{
name: "empty third-party dir",
fsys: fstest.MapFS{
"embed/os-arch/PLACEHOLDER": &fstest.MapFile{}, // Checked-in placeholder, so it's always there.
"embed/os-arch/report.txt": &fstest.MapFile{Data: []byte("dep1 (v1.0.0) - MIT - https://example.com\n")},
"embed/os-arch/third-party": &fstest.MapFile{Data: []byte{}, Mode: fs.ModeDir},
},
expected: heredoc.Doc(`
dep1 (v1.0.0) - MIT - https://example.com
`),
},
{
name: "unknown file at root ignored",
fsys: fstest.MapFS{
"embed/os-arch/PLACEHOLDER": &fstest.MapFile{}, // Checked-in placeholder, so it's always there.
"embed/os-arch/report.txt": &fstest.MapFile{Data: []byte("dep1 (v1.0.0) - MIT - https://example.com\n")},
"embed/os-arch/unknown": &fstest.MapFile{
Data: []byte("MIT License\n\nCopyright (c) 2024"),
},
},
expected: heredoc.Doc(`
dep1 (v1.0.0) - MIT - https://example.com
`),
},
{
name: "unknown directory at root ignored",
fsys: fstest.MapFS{
"embed/os-arch/PLACEHOLDER": &fstest.MapFile{}, // Checked-in placeholder, so it's always there.
"embed/os-arch/report.txt": &fstest.MapFile{Data: []byte("dep1 (v1.0.0) - MIT - https://example.com\n")},
"embed/os-arch/unknown/example.com/mod/LICENSE": &fstest.MapFile{
Data: []byte("MIT License\n\nCopyright (c) 2024"),
},
},
expected: heredoc.Doc(`
dep1 (v1.0.0) - MIT - https://example.com
`),
},
{
name: "single module",
fsys: fstest.MapFS{
"embed/os-arch/PLACEHOLDER": &fstest.MapFile{}, // Checked-in placeholder, so it's always there.
"embed/os-arch/report.txt": &fstest.MapFile{Data: []byte("example.com/mod (v1.0.0) - MIT - https://example.com\n")},
"embed/os-arch/third-party/example.com/mod/LICENSE": &fstest.MapFile{
Data: []byte("MIT License\n\nCopyright (c) 2024"),
},
},
expected: heredoc.Doc(`
example.com/mod (v1.0.0) - MIT - https://example.com
================================================================================
example.com/mod
================================================================================
MIT License
Copyright (c) 2024
`),
},
{
name: "multiple modules sorted alphabetically",
fsys: fstest.MapFS{
"embed/os-arch/PLACEHOLDER": &fstest.MapFile{}, // Checked-in placeholder, so it's always there.
"embed/os-arch/report.txt": &fstest.MapFile{Data: []byte("example.com/mod (v1.0.0) - MIT - https://example.com\n")},
"embed/os-arch/third-party/github.com/zzz/pkg/LICENSE": &fstest.MapFile{
Data: []byte("ZZZ License"),
},
"embed/os-arch/third-party/github.com/aaa/pkg/LICENSE": &fstest.MapFile{
Data: []byte("AAA License"),
},
},
expected: heredoc.Doc(`
example.com/mod (v1.0.0) - MIT - https://example.com
================================================================================
github.com/aaa/pkg
================================================================================
AAA License
================================================================================
github.com/zzz/pkg
================================================================================
ZZZ License
`),
},
{
name: "license and notice files",
fsys: fstest.MapFS{
"embed/os-arch/PLACEHOLDER": &fstest.MapFile{}, // Checked-in placeholder, so it's always there.
"embed/os-arch/report.txt": &fstest.MapFile{Data: []byte("example.com/mod (v1.0.0) - MIT - https://example.com\n")},
"embed/os-arch/third-party/example.com/mod/LICENSE": &fstest.MapFile{
Data: []byte("Apache License 2.0"),
},
"embed/os-arch/third-party/example.com/mod/NOTICE": &fstest.MapFile{
Data: []byte("Copyright 2024 Example Corp"),
},
},
expected: heredoc.Doc(`
example.com/mod (v1.0.0) - MIT - https://example.com
================================================================================
example.com/mod
================================================================================
Apache License 2.0
Copyright 2024 Example Corp
`),
},
}
actualContent := content(report, fsys, "third-party")
require.Contains(t, actualContent, filepath.FromSlash("example.com/mod"))
require.Contains(t, actualContent, "MIT License")
}
func TestContent_multipleModulesSortedAlphabetically(t *testing.T) {
report := "header\n"
fsys := fstest.MapFS{
"third-party/github.com/zzz/pkg/LICENSE": &fstest.MapFile{
Data: []byte("ZZZ License"),
},
"third-party/github.com/aaa/pkg/LICENSE": &fstest.MapFile{
Data: []byte("AAA License"),
},
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := content(tt.fsys, "embed/os-arch")
require.Equal(t, tt.expected, got)
})
}
actualContent := content(report, fsys, "third-party")
aIdx := strings.Index(actualContent, filepath.FromSlash("github.com/aaa/pkg"))
zIdx := strings.Index(actualContent, filepath.FromSlash("github.com/zzz/pkg"))
require.NotEqual(t, -1, aIdx, "expected aaa module in output")
require.NotEqual(t, -1, zIdx, "expected zzz module in output")
require.Less(t, aIdx, zIdx, "expected modules to be sorted alphabetically")
}
func TestContent_licenseAndNoticeFiles(t *testing.T) {
report := "header\n"
fsys := fstest.MapFS{
"third-party/example.com/mod/LICENSE": &fstest.MapFile{
Data: []byte("Apache License 2.0"),
},
"third-party/example.com/mod/NOTICE": &fstest.MapFile{
Data: []byte("Copyright 2024 Example Corp"),
},
}
actualContent := content(report, fsys, "third-party")
require.Contains(t, actualContent, "Apache License 2.0")
require.Contains(t, actualContent, "Copyright 2024 Example Corp")
}
func TestContent_emptyThirdPartyDir(t *testing.T) {
report := "header\n"
fsys := fstest.MapFS{
"third-party/empty": &fstest.MapFile{Data: []byte("")},
}
actualContent := content(report, fsys, "third-party")
require.True(t, strings.HasPrefix(actualContent, "header\n"), "expected output to start with report header")
}

View file

@ -3,7 +3,7 @@
# Generate third-party license information for embedding in the binary.
#
# Usage:
# ./script/licenses <GOOS> <GOARCH> Generate licenses for a single platform
# ./script/licenses <GOOS> <GOARCH> Generate licenses for a single platform
# ./script/licenses --check Verify generation works for all release platforms
#
# The single-platform mode is called by goreleaser pre-build hooks to generate
@ -75,8 +75,8 @@ if [ "$1" = "--check" ]; then
echo "License generation verified for all platforms."
elif [ $# -eq 2 ]; then
generate_licenses "$1" "$2" "internal/licenses/embed"
echo "Licenses written to internal/licenses/embed/"
generate_licenses "$1" "$2" "internal/licenses/embed/${1}-${2}"
echo "Licenses written to internal/licenses/embed/${1}-${2}"
else
echo "Usage: $0 <GOOS> <GOARCH>"
echo " $0 --check"