From 19c8f41da04d9a2b8e1541a5cf575271b44b6b4d Mon Sep 17 00:00:00 2001 From: Gareth Watts Date: Fri, 9 May 2025 19:50:46 +0000 Subject: [PATCH] Use license in repo root, when appropriate resolves #73, resolves #186 For modules that are not at the root of the repo, Go will make a copy of the LICENSE file from the repo root into the module's directory when creating the .zip file (see https://go.dev/ref/mod#vcs-license) This throws off URL generation, as FileURL will create a link to a repo URL to the license that doesn't exist as the file was synthesized by Go. Attempt to detect this case by examining the zip file that Go creates - As it stands, it appends the LICENSE file to the zip's file directory; we can generally assume that if we see such an addition, it's a result of Go copying it there and therefore it's not a real file in the repo, instead being copied from the root. If we find that to be the case, generate a link to the original LICENSE file at the root of the repo instead. The complex e2e test here exercises this nicely (updated links didn't work before, now they do). --- .../pkgsite/source/source_patch.go | 27 +++++++++++++ licenses/library.go | 38 ++++++++++++++++++- licenses/module.go | 7 +++- testdata/modules/complex/licenses.csv | 24 ++++++------ 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/internal/third_party/pkgsite/source/source_patch.go b/internal/third_party/pkgsite/source/source_patch.go index 3bce5e67..28bc46b6 100644 --- a/internal/third_party/pkgsite/source/source_patch.go +++ b/internal/third_party/pkgsite/source/source_patch.go @@ -14,6 +14,11 @@ package source +import ( + "path" + "strings" +) + // This file includes all local additions to source package for google/go-licenses use-cases. // SetCommit overrides commit to a specified commit. Usually, you should pass your version to @@ -31,3 +36,25 @@ func (i *Info) SetCommit(commit string) { } i.commit = commit } + +// ModuleDir returns the sub-directory of the repo that holds the module, or +// an empty string if located at the root. +func (i *Info) ModuleDir() string { + return i.moduleDir +} + +// RepoURL returns a URL for a file whose pathname is relative to the root of the repo. +func (i *Info) RepoURL(pathname string) string { + if i == nil { + return "" + } + dir, base := path.Split(pathname) + return expand(i.templates.File, map[string]string{ + "repo": i.repoURL, + "importPath": path.Join(strings.TrimPrefix(i.repoURL, "https://"), dir), + "commit": i.commit, + "dir": dir, + "file": pathname, + "base": base, + }) +} diff --git a/licenses/library.go b/licenses/library.go index fa59120f..4c14e21e 100644 --- a/licenses/library.go +++ b/licenses/library.go @@ -15,6 +15,7 @@ package licenses import ( + "archive/zip" "context" "fmt" "go/build" @@ -397,8 +398,22 @@ func (l *Library) FileURL(ctx context.Context, cl *source.Client, filePath strin if err != nil { return "", wrap(err) } - // TODO: there are still rare cases this may result in an incorrect URL. - // https://github.com/google/go-licenses/issues/73#issuecomment-1005587408 + + // If the module isn't located at the repo root, Go may have copied the LICENSE file from + // the root to the module sub-directory. Attempt to detect this so we can correctly link + // to the license url that's located at the root of the repo. + // + // This check takes advantage of the fact that the copied license is appended to the end + // of the cached zip file; if it's the last file, it's likely that Go copied it there. + // + // See https://go.dev/ref/mod#vcs-license + if remote.ModuleDir() != "" && m.ZipPath != "" { + if isLast, err := hasAppendedLicense(m.ZipPath); isLast { + return remote.RepoURL("LICENSE"), nil + } else if err != nil { + klog.Warningf("failed to open cached module zip %s: %w", m.ZipPath, err) + } + } return remote.FileURL(relativePath), nil } @@ -430,3 +445,22 @@ func isStdLib(pkg *packages.Package) bool { func isTestBinary(pkg *packages.Package) bool { return strings.HasSuffix(pkg.PkgPath, ".test") } + +// hasAppendedLicense opens the zip file supplied by fn and checks to see if the +// last file in the .zip is called "LICENSE". +func hasAppendedLicense(fn string) (bool, error) { + reader, err := zip.OpenReader(fn) + if err != nil { + return false, err + } + defer reader.Close() + + if len(reader.File) == 0 { + return false, fmt.Errorf("zip file is empty") + } + + lastFile := reader.File[len(reader.File)-1] + lastFileName := filepath.Base(lastFile.Name) + + return lastFileName == "LICENSE", nil +} diff --git a/licenses/module.go b/licenses/module.go index 99abc16b..a23df623 100644 --- a/licenses/module.go +++ b/licenses/module.go @@ -30,6 +30,7 @@ type Module struct { Path string // module path Version string // module version Dir string // directory holding files for this module, if any + ZipPath string // path to cached zip file, if any } func newModule(mod *packages.Module) *Module { @@ -59,9 +60,13 @@ func newModule(mod *packages.Module) *Module { // The +incompatible suffix does not affect module version. // ref: https://golang.org/ref/mod#incompatible-versions tmp.Version = strings.TrimSuffix(tmp.Version, "+incompatible") - return &Module{ + m := &Module{ Path: tmp.Path, Version: tmp.Version, Dir: tmp.Dir, } + if tmp.GoMod != "" { + m.ZipPath = strings.TrimSuffix(tmp.GoMod, ".mod") + ".zip" + } + return m } diff --git a/testdata/modules/complex/licenses.csv b/testdata/modules/complex/licenses.csv index 6cb1c9d5..bbab491b 100644 --- a/testdata/modules/complex/licenses.csv +++ b/testdata/modules/complex/licenses.csv @@ -1,10 +1,10 @@ github.com/Azure/azure-sdk-for-go,https://github.com/Azure/azure-sdk-for-go/blob/v68.0.0/LICENSE.txt,MIT -github.com/Azure/go-autorest/autorest,https://github.com/Azure/go-autorest/blob/autorest/v0.11.29/autorest/LICENSE,Apache-2.0 -github.com/Azure/go-autorest/autorest/adal,https://github.com/Azure/go-autorest/blob/autorest/adal/v0.9.23/autorest/adal/LICENSE,Apache-2.0 -github.com/Azure/go-autorest/autorest/date,https://github.com/Azure/go-autorest/blob/autorest/date/v0.3.0/autorest/date/LICENSE,Apache-2.0 -github.com/Azure/go-autorest/autorest/to,https://github.com/Azure/go-autorest/blob/autorest/to/v0.4.0/autorest/to/LICENSE,Apache-2.0 -github.com/Azure/go-autorest/logger,https://github.com/Azure/go-autorest/blob/logger/v0.2.1/logger/LICENSE,Apache-2.0 -github.com/Azure/go-autorest/tracing,https://github.com/Azure/go-autorest/blob/tracing/v0.6.0/tracing/LICENSE,Apache-2.0 +github.com/Azure/go-autorest/autorest,https://github.com/Azure/go-autorest/blob/autorest/v0.11.29/LICENSE,Apache-2.0 +github.com/Azure/go-autorest/autorest/adal,https://github.com/Azure/go-autorest/blob/autorest/adal/v0.9.23/LICENSE,Apache-2.0 +github.com/Azure/go-autorest/autorest/date,https://github.com/Azure/go-autorest/blob/autorest/date/v0.3.0/LICENSE,Apache-2.0 +github.com/Azure/go-autorest/autorest/to,https://github.com/Azure/go-autorest/blob/autorest/to/v0.4.0/LICENSE,Apache-2.0 +github.com/Azure/go-autorest/logger,https://github.com/Azure/go-autorest/blob/logger/v0.2.1/LICENSE,Apache-2.0 +github.com/Azure/go-autorest/tracing,https://github.com/Azure/go-autorest/blob/tracing/v0.6.0/LICENSE,Apache-2.0 github.com/Azure/go-ntlmssp,https://github.com/Azure/go-ntlmssp/blob/754e69321358/LICENSE,MIT github.com/akamai/AkamaiOPEN-edgegrid-golang,https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/v1.2.2/LICENSE,Apache-2.0 github.com/armon/go-metrics,https://github.com/armon/go-metrics/blob/v0.4.1/LICENSE,MIT @@ -17,13 +17,13 @@ github.com/davecgh/go-spew/spew,https://github.com/davecgh/go-spew/blob/d8f796af github.com/digitalocean/godo,https://github.com/digitalocean/godo/blob/v1.99.0/LICENSE.txt,MIT github.com/digitalocean/godo,https://github.com/digitalocean/godo/blob/v1.99.0/LICENSE.txt,BSD-3-Clause github.com/emicklei/go-restful/v3,https://github.com/emicklei/go-restful/blob/v3.9.0/LICENSE,MIT -github.com/evanphx/json-patch/v5,https://github.com/evanphx/json-patch/blob/v5.6.0/v5/LICENSE,BSD-3-Clause +github.com/evanphx/json-patch/v5,https://github.com/evanphx/json-patch/blob/v5.6.0/LICENSE,BSD-3-Clause github.com/fatih/color,https://github.com/fatih/color/blob/v1.15.0/LICENSE.md,MIT github.com/fsnotify/fsnotify,https://github.com/fsnotify/fsnotify/blob/v1.6.0/LICENSE,BSD-3-Clause github.com/go-asn1-ber/asn1-ber,https://github.com/go-asn1-ber/asn1-ber/blob/v1.5.4/LICENSE,MIT github.com/go-jose/go-jose/v3,https://github.com/go-jose/go-jose/blob/v3.0.0/LICENSE,Apache-2.0 github.com/go-jose/go-jose/v3/json,https://github.com/go-jose/go-jose/blob/v3.0.0/json/LICENSE,BSD-3-Clause -github.com/go-ldap/ldap/v3,https://github.com/go-ldap/ldap/blob/v3.4.5/v3/LICENSE,MIT +github.com/go-ldap/ldap/v3,https://github.com/go-ldap/ldap/blob/v3.4.5/LICENSE,MIT github.com/go-logr/logr,https://github.com/go-logr/logr/blob/v1.2.4/LICENSE,Apache-2.0 github.com/go-openapi/jsonpointer,https://github.com/go-openapi/jsonpointer/blob/v0.19.6/LICENSE,Apache-2.0 github.com/go-openapi/jsonreference,https://github.com/go-openapi/jsonreference/blob/v0.20.1/LICENSE,Apache-2.0 @@ -57,8 +57,8 @@ github.com/hashicorp/go-uuid,https://github.com/hashicorp/go-uuid/blob/v1.0.3/LI github.com/hashicorp/go-version,https://github.com/hashicorp/go-version/blob/v1.6.0/LICENSE,MPL-2.0 github.com/hashicorp/golang-lru,https://github.com/hashicorp/golang-lru/blob/v0.5.4/LICENSE,MPL-2.0 github.com/hashicorp/hcl,https://github.com/hashicorp/hcl/blob/v1.0.1-vault-5/LICENSE,MPL-2.0 -github.com/hashicorp/vault/api,https://github.com/hashicorp/vault/blob/api/v1.9.2/api/LICENSE,MPL-2.0 -github.com/hashicorp/vault/sdk,https://github.com/hashicorp/vault/blob/sdk/v0.9.1/sdk/LICENSE,MPL-2.0 +github.com/hashicorp/vault/api,https://github.com/hashicorp/vault/blob/api/v1.9.2/LICENSE,MPL-2.0 +github.com/hashicorp/vault/sdk,https://github.com/hashicorp/vault/blob/sdk/v0.9.1/LICENSE,MPL-2.0 github.com/hashicorp/yamux,https://github.com/hashicorp/yamux/blob/0bc27b27de87/LICENSE,MPL-2.0 github.com/imdario/mergo,https://github.com/imdario/mergo/blob/v0.3.6/LICENSE,BSD-3-Clause github.com/josharian/intern,https://github.com/josharian/intern/blob/v1.0.0/license.md,MIT @@ -107,9 +107,9 @@ golang.org/x/sys,https://cs.opensource.google/go/x/sys/+/v0.10.0:LICENSE,BSD-3-C golang.org/x/term,https://cs.opensource.google/go/x/term/+/v0.10.0:LICENSE,BSD-3-Clause golang.org/x/text,https://cs.opensource.google/go/x/text/+/v0.11.0:LICENSE,BSD-3-Clause golang.org/x/time/rate,https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE,BSD-3-Clause -gomodules.xyz/jsonpatch/v2,https://github.com/gomodules/jsonpatch/blob/v2.3.0/v2/LICENSE,Apache-2.0 +gomodules.xyz/jsonpatch/v2,https://github.com/gomodules/jsonpatch/blob/v2.3.0/LICENSE,Apache-2.0 google.golang.org/api,https://github.com/googleapis/google-api-go-client/blob/v0.130.0/LICENSE,BSD-3-Clause -google.golang.org/genproto/googleapis/rpc/status,https://github.com/googleapis/go-genproto/blob/9506855d4529/googleapis/rpc/LICENSE,Apache-2.0 +google.golang.org/genproto/googleapis/rpc/status,https://github.com/googleapis/go-genproto/blob/9506855d4529/LICENSE,Apache-2.0 google.golang.org/grpc,https://github.com/grpc/grpc-go/blob/v1.56.1/LICENSE,Apache-2.0 google.golang.org/protobuf,https://github.com/protocolbuffers/protobuf-go/blob/v1.31.0/LICENSE,BSD-3-Clause gopkg.in/inf.v0,https://github.com/go-inf/inf/blob/v0.9.1/LICENSE,BSD-3-Clause