From 07e802d616dc7a6d35275a1372af621dee0e2e47 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 24 Feb 2026 15:22:52 +0000 Subject: [PATCH 1/4] fix(script/licenses): generate licenses in separate dirs Signed-off-by: Babak K. Shandiz --- script/licenses | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/licenses b/script/licenses index ecdcbcdf4b8..01f87897ea3 100755 --- a/script/licenses +++ b/script/licenses @@ -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 " echo " $0 --check" From ad5ebc6ad976b68d2cb0b56783c60168a13a6dc6 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 24 Feb 2026 15:23:18 +0000 Subject: [PATCH 2/4] chore(script/licenses): fix indentation Signed-off-by: Babak K. Shandiz --- script/licenses | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/licenses b/script/licenses index 01f87897ea3..1c9debf6136 100755 --- a/script/licenses +++ b/script/licenses @@ -3,7 +3,7 @@ # Generate third-party license information for embedding in the binary. # # Usage: -# ./script/licenses Generate licenses for a single platform +# ./script/licenses 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 From 8fe4ddd7ce7eeb09f5642e4ccd90f3ae0313ee3e Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 25 Feb 2026 14:58:56 +0000 Subject: [PATCH 3/4] chore(gitignore): ignore generated license files Signed-off-by: Babak K. Shandiz --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index a4b73ac7a50..b82a00c7274 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ # Windows resource files /cmd/gh/*.syso +# Third-party licenses +/internal/licenses/embed/*/* +!/internal/licenses/embed/*/PLACEHOLDER + # VS Code .vscode From c9543e748919e9f1955e731d1b4436e4753dfb7f Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 25 Feb 2026 14:59:52 +0000 Subject: [PATCH 4/4] fix(licenses): implement VCS-friendly embedding Signed-off-by: Babak K. Shandiz --- .../licenses/embed/darwin-amd64/PLACEHOLDER | 0 .../licenses/embed/darwin-arm64/PLACEHOLDER | 0 internal/licenses/embed/linux-386/PLACEHOLDER | 0 .../licenses/embed/linux-amd64/PLACEHOLDER | 0 internal/licenses/embed/linux-arm/PLACEHOLDER | 0 .../licenses/embed/linux-arm64/PLACEHOLDER | 0 internal/licenses/embed/report.txt | 1 - .../licenses/embed/third-party/PLACEHOLDER | 1 - .../licenses/embed/windows-386/PLACEHOLDER | 0 .../licenses/embed/windows-amd64/PLACEHOLDER | 0 .../licenses/embed/windows-arm64/PLACEHOLDER | 0 internal/licenses/embed_darwin_amd64.go | 8 + internal/licenses/embed_darwin_arm64.go | 8 + internal/licenses/embed_default.go | 15 ++ internal/licenses/embed_linux_386.go | 8 + internal/licenses/embed_linux_amd64.go | 8 + internal/licenses/embed_linux_arm.go | 8 + internal/licenses/embed_linux_arm64.go | 8 + internal/licenses/embed_windows_386.go | 8 + internal/licenses/embed_windows_amd64.go | 8 + internal/licenses/embed_windows_arm64.go | 8 + internal/licenses/licenses.go | 49 +++-- internal/licenses/licenses_test.go | 205 ++++++++++++------ 23 files changed, 252 insertions(+), 91 deletions(-) create mode 100644 internal/licenses/embed/darwin-amd64/PLACEHOLDER create mode 100644 internal/licenses/embed/darwin-arm64/PLACEHOLDER create mode 100644 internal/licenses/embed/linux-386/PLACEHOLDER create mode 100644 internal/licenses/embed/linux-amd64/PLACEHOLDER create mode 100644 internal/licenses/embed/linux-arm/PLACEHOLDER create mode 100644 internal/licenses/embed/linux-arm64/PLACEHOLDER delete mode 100644 internal/licenses/embed/report.txt delete mode 100644 internal/licenses/embed/third-party/PLACEHOLDER create mode 100644 internal/licenses/embed/windows-386/PLACEHOLDER create mode 100644 internal/licenses/embed/windows-amd64/PLACEHOLDER create mode 100644 internal/licenses/embed/windows-arm64/PLACEHOLDER create mode 100644 internal/licenses/embed_darwin_amd64.go create mode 100644 internal/licenses/embed_darwin_arm64.go create mode 100644 internal/licenses/embed_default.go create mode 100644 internal/licenses/embed_linux_386.go create mode 100644 internal/licenses/embed_linux_amd64.go create mode 100644 internal/licenses/embed_linux_arm.go create mode 100644 internal/licenses/embed_linux_arm64.go create mode 100644 internal/licenses/embed_windows_386.go create mode 100644 internal/licenses/embed_windows_amd64.go create mode 100644 internal/licenses/embed_windows_arm64.go diff --git a/internal/licenses/embed/darwin-amd64/PLACEHOLDER b/internal/licenses/embed/darwin-amd64/PLACEHOLDER new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/licenses/embed/darwin-arm64/PLACEHOLDER b/internal/licenses/embed/darwin-arm64/PLACEHOLDER new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/licenses/embed/linux-386/PLACEHOLDER b/internal/licenses/embed/linux-386/PLACEHOLDER new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/licenses/embed/linux-amd64/PLACEHOLDER b/internal/licenses/embed/linux-amd64/PLACEHOLDER new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/licenses/embed/linux-arm/PLACEHOLDER b/internal/licenses/embed/linux-arm/PLACEHOLDER new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/licenses/embed/linux-arm64/PLACEHOLDER b/internal/licenses/embed/linux-arm64/PLACEHOLDER new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/licenses/embed/report.txt b/internal/licenses/embed/report.txt deleted file mode 100644 index 002dfeeec8a..00000000000 --- a/internal/licenses/embed/report.txt +++ /dev/null @@ -1 +0,0 @@ -License information is only available in official release builds. diff --git a/internal/licenses/embed/third-party/PLACEHOLDER b/internal/licenses/embed/third-party/PLACEHOLDER deleted file mode 100644 index 48cdce85287..00000000000 --- a/internal/licenses/embed/third-party/PLACEHOLDER +++ /dev/null @@ -1 +0,0 @@ -placeholder diff --git a/internal/licenses/embed/windows-386/PLACEHOLDER b/internal/licenses/embed/windows-386/PLACEHOLDER new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/licenses/embed/windows-amd64/PLACEHOLDER b/internal/licenses/embed/windows-amd64/PLACEHOLDER new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/licenses/embed/windows-arm64/PLACEHOLDER b/internal/licenses/embed/windows-arm64/PLACEHOLDER new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/licenses/embed_darwin_amd64.go b/internal/licenses/embed_darwin_amd64.go new file mode 100644 index 00000000000..9da7398c61e --- /dev/null +++ b/internal/licenses/embed_darwin_amd64.go @@ -0,0 +1,8 @@ +package licenses + +import "embed" + +const rootDir = "embed/darwin-amd64" + +//go:embed all:embed/darwin-amd64 +var embedFS embed.FS diff --git a/internal/licenses/embed_darwin_arm64.go b/internal/licenses/embed_darwin_arm64.go new file mode 100644 index 00000000000..844a51ab948 --- /dev/null +++ b/internal/licenses/embed_darwin_arm64.go @@ -0,0 +1,8 @@ +package licenses + +import "embed" + +const rootDir = "embed/darwin-arm64" + +//go:embed all:embed/darwin-arm64 +var embedFS embed.FS diff --git a/internal/licenses/embed_default.go b/internal/licenses/embed_default.go new file mode 100644 index 00000000000..387f4285fd8 --- /dev/null +++ b/internal/licenses/embed_default.go @@ -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 diff --git a/internal/licenses/embed_linux_386.go b/internal/licenses/embed_linux_386.go new file mode 100644 index 00000000000..f6f34313ee9 --- /dev/null +++ b/internal/licenses/embed_linux_386.go @@ -0,0 +1,8 @@ +package licenses + +import "embed" + +const rootDir = "embed/linux-386" + +//go:embed all:embed/linux-386 +var embedFS embed.FS diff --git a/internal/licenses/embed_linux_amd64.go b/internal/licenses/embed_linux_amd64.go new file mode 100644 index 00000000000..8c944d61377 --- /dev/null +++ b/internal/licenses/embed_linux_amd64.go @@ -0,0 +1,8 @@ +package licenses + +import "embed" + +const rootDir = "embed/linux-amd64" + +//go:embed all:embed/linux-amd64 +var embedFS embed.FS diff --git a/internal/licenses/embed_linux_arm.go b/internal/licenses/embed_linux_arm.go new file mode 100644 index 00000000000..61ba21d7d94 --- /dev/null +++ b/internal/licenses/embed_linux_arm.go @@ -0,0 +1,8 @@ +package licenses + +import "embed" + +const rootDir = "embed/linux-arm" + +//go:embed all:embed/linux-arm +var embedFS embed.FS diff --git a/internal/licenses/embed_linux_arm64.go b/internal/licenses/embed_linux_arm64.go new file mode 100644 index 00000000000..99013dc98ad --- /dev/null +++ b/internal/licenses/embed_linux_arm64.go @@ -0,0 +1,8 @@ +package licenses + +import "embed" + +const rootDir = "embed/linux-arm64" + +//go:embed all:embed/linux-arm64 +var embedFS embed.FS diff --git a/internal/licenses/embed_windows_386.go b/internal/licenses/embed_windows_386.go new file mode 100644 index 00000000000..1976ab9f13c --- /dev/null +++ b/internal/licenses/embed_windows_386.go @@ -0,0 +1,8 @@ +package licenses + +import "embed" + +const rootDir = "embed/windows-386" + +//go:embed all:embed/windows-386 +var embedFS embed.FS diff --git a/internal/licenses/embed_windows_amd64.go b/internal/licenses/embed_windows_amd64.go new file mode 100644 index 00000000000..3e9fb0b5d60 --- /dev/null +++ b/internal/licenses/embed_windows_amd64.go @@ -0,0 +1,8 @@ +package licenses + +import "embed" + +const rootDir = "embed/windows-amd64" + +//go:embed all:embed/windows-amd64 +var embedFS embed.FS diff --git a/internal/licenses/embed_windows_arm64.go b/internal/licenses/embed_windows_arm64.go new file mode 100644 index 00000000000..4afd13825ab --- /dev/null +++ b/internal/licenses/embed_windows_arm64.go @@ -0,0 +1,8 @@ +package licenses + +import "embed" + +const rootDir = "embed/windows-arm64" + +//go:embed all:embed/windows-arm64 +var embedFS embed.FS diff --git a/internal/licenses/licenses.go b/internal/licenses/licenses.go index 09c8058f773..1499a0722fc 100644 --- a/internal/licenses/licenses.go +++ b/internal/licenses/licenses.go @@ -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 - } - - // 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} + dir := path.Dir(filePath) + if _, ok := modules[dir]; !ok { + modules[dir] = &moduleFiles{path: dir} } - 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 } diff --git a/internal/licenses/licenses_test.go b/internal/licenses/licenses_test.go index b3b81d666b8..befb03e5fb5 100644 --- a/internal/licenses/licenses_test.go +++ b/internal/licenses/licenses_test.go @@ -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 + + `), }, - } - - 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"), + { + 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 + + `), }, - "third-party/github.com/aaa/pkg/LICENSE": &fstest.MapFile{ - Data: []byte("AAA License"), + { + 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 + + `), }, - } - - 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"), + { + 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 + + `), }, - "third-party/example.com/mod/NOTICE": &fstest.MapFile{ - Data: []byte("Copyright 2024 Example Corp"), + { + 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, "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("")}, + 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") - - require.True(t, strings.HasPrefix(actualContent, "header\n"), "expected output to start with report header") }