Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2: list dependencies from either import path or go binary #71

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7f8ce88
set up v2 folder
Bobgy Jun 19, 2021
e020f62
v2: i2: list deps in go binary
Bobgy Jun 19, 2021
55b8538
fix
Bobgy Jun 19, 2021
a571fd5
update
Bobgy Jun 20, 2021
3ea68bd
fix
Bobgy Jun 20, 2021
2eafb6e
cleanup
Bobgy Jun 20, 2021
682cc82
merge to package gocli
Bobgy Jun 20, 2021
5bd2c4a
fix unit tests
Bobgy Jun 20, 2021
b85f547
rename
Bobgy Jun 20, 2021
acfe58a
cleanup
Bobgy Jun 22, 2021
6f64046
fix
Bobgy Jun 22, 2021
f885c2c
extract more metadata from binary
Bobgy Jun 22, 2021
b9da23c
cleanup
Bobgy Jun 22, 2021
6973c71
cleanup
Bobgy Jun 22, 2021
517e811
address comments
Bobgy Jun 26, 2021
3cd4f5f
clean up
Bobgy Jun 26, 2021
0749a9a
update README with current status
Bobgy Jul 24, 2021
cee56ce
add comments clarifying what tests/modules are
Bobgy Jul 24, 2021
4f9aff3
test: simplify test module
Bobgy Jul 31, 2021
1f90f0e
address feedback: add comments & clean up temp files
Bobgy Aug 15, 2021
fff0db5
vendor go/runtime/debug
Bobgy Aug 15, 2021
89f12c3
test: add version to gocli.ExtractBinaryMetadata test
Bobgy Aug 15, 2021
8d1a5b8
modify go/runtime/debug to parse build info from go version -m comman…
Bobgy Aug 15, 2021
1fb170d
rm third_party/uw-labs/lichen
Bobgy Aug 15, 2021
7b29d8f
add example build info data
Bobgy Aug 15, 2021
6371c33
refactor: expose our own Module type that does not have a replace field
Bobgy Aug 15, 2021
2357868
add more comments
Bobgy Aug 15, 2021
2d930b0
add another approach to get go modules using packages.Visit
Bobgy Aug 30, 2021
801af76
trim +incompatible from version
Bobgy Sep 4, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions v2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# built binary
go-licenses

# distribution folder
dist

# MacOS common ignores
.DS_Store
16 changes: 16 additions & 0 deletions v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# go-licenses v2

A tool to automate license management workflow for go module project's dependencies and transitive dependencies.

## **THIS IS STILL UNDER DEVELOPMENT**

The v2 package is being developed and currently incomplete, @Bobgy is
upstreaming changes from his fork in <https://github.com/Bobgy/go-licenses/blob/main/v2>.

Tracking issue where you can find the roadmap and progress:
<https://github.com/google/go-licenses/issues/70>.

The major changes from v1 are:

* V2 only supports go modules, it can get license URL for modules without a need for you to vendor your dependencies.
* V2 does not assume each module has a single license, v2 will scan all the files for each module to find licenses.
13 changes: 13 additions & 0 deletions v2/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module github.com/google/go-licenses/v2

go 1.15

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hashicorp/go-multierror v1.1.1
github.com/kr/pretty v0.1.0 // indirect
github.com/stretchr/testify v1.4.0
golang.org/x/tools v0.1.4
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
50 changes: 50 additions & 0 deletions v2/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
112 changes: 112 additions & 0 deletions v2/gocli/go_binary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gocli

import (
"fmt"

"github.com/google/go-licenses/v2/third_party/go/runtime/debug"
)

// Module metadata extracted from binary and local go module workspace.
type BinaryMetadata struct {
// The main module used to build the binary.
// e.g. github.com//google/go-licenses/v2/tests/modules/cli02
MainModule string
// Detailed metadata of all the module dependencies.
// Does not include the main module.
Modules []Module
}

// List dependencies from module metadata in a go binary.
// Modules with replace directives are returned as the replaced module instead.
//
// Prerequisites:
// * The go binary must be built with go modules without any further modifications.
// * The command must run with working directory same as to build the analyzed
// go binary, because we need the exact go modules info used to build it.
//
// Here, I am using [1] as a short term solution. It runs [4] go version -m and parses
// output. This is preferred over [2], because [2] is an alternative implemention
// for go version -m, and I expect better long term compatibility for go version -m.
//
// The parsing command output hack is still unfavorable in the long term. As
// dicussed in [3], golang community will move go version parsing into an individual
// module in golang.org/x. We can use that module instead after it is built.
//
// References of similar implementations or dicussions:
// 1. https://github.com/uw-labs/lichen/blob/be9752894a5958f6ba7be9e05dc370b7a73b58db/internal/module/extract.go#L16
// 2. https://github.com/mitchellh/golicense/blob/8c09a94a11ac73299a72a68a7b41e3a737119f91/module/module.go#L27
// 3. https://github.com/golang/go/issues/39301
// 4. https://golang.org/pkg/cmd/go/internal/version/
func ExtractBinaryMetadata(path string) (*BinaryMetadata, error) {
buildInfo, err := listModulesInBinary(path)
if err != nil {
return nil, err
}
mods, err := joinModulesMetadata(buildInfo.Deps)
if err != nil {
return nil, err
}
return &BinaryMetadata{
MainModule: buildInfo.Main.Path,
Modules: mods,
}, nil
}

func listModulesInBinary(path string) (buildinfo *debug.BuildInfo, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("listModulesInGoBinary(path=%q): %w", path, err)
}
}()
// TODO(Bobgy): replace with x/mod equivalent from https://github.com/golang/go/issues/39301
// when it is available.
return version(path)
}

// joinModulesMetadata inner joins local go modules metadata with module ref
// extracted from the binary.
// The local go modules metadata is taken from calling `go list -m -json all`.
// Only those appeared in refs will be returned.
// An error is reported when we cannot find go module metadata for some refs,
// or when there's a version mismatch. These errors usually indicate your current
// working directory does not match exactly where the go binary is built.
func joinModulesMetadata(refs []*debug.Module) (modules []Module, err error) {
// Note, there was an attempt to use golang.org/x/tools/go/packages for
// loading modules instead, but it fails for modules like golang.org/x/sys.
// These modules only contains sub-packages, but no source code, so it
// throws an error when using packages.Load.
// More context: https://github.com/google/go-licenses/pull/71#issuecomment-890342154
localModulesDict, err := ListModules()
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

for _, ref := range refs {
localModule, ok := localModulesDict[ref.Path]
if !ok {
return nil, fmt.Errorf("Cannot find %v in current dir's go modules. Are you running this tool from the working dir to build the binary you are analyzing?", ref.Path)
}
if localModule.Dir == "" {
return nil, fmt.Errorf("Module %v's local directory is empty. Did you run go mod download?", ref.Path)
}
if localModule.Version != ref.Version {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This causes cause problems if the current module context doesn't match the same version as the binary. I think we'll need to rely on go list -m (or some library equivalent if available) instead.

To test this I used a modified cli02 binary using golang.org/x/tools@v0.1.3, with a go-licenses@v2 using golang.org/x/tools@v0.1.4 (diff)

$ go test ./...
--- FAIL: TestListModulesInGoBinary (0.28s)
    --- FAIL: TestListModulesInGoBinary/../tests/modules/cli02 (0.13s)
        go_binary_test.go:81: Found golang.org/x/tools v0.1.3 in go binary, but v0.1.4 is downloaded in go modules. Are you running this tool from the working dir to build the binary you are analyzing?
FAIL
$ go list -json -m golang.org/x/tools@v0.1.3  # Even though last command failed, module is downloaded locally
{
 "Path": "golang.org/x/tools",
 "Version": "v0.1.3",
 "Time": "2021-06-09T21:40:20Z",
 "Dir": "/usr/local/google/home/wlynch/pkg/mod/golang.org/x/tools@v0.1.3",
 "GoMod": "/usr/local/google/home/wlynch/pkg/mod/cache/download/golang.org/x/tools/@v/v0.1.3.mod",
 "GoVersion": "1.17"
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, I didn't know we could use go list -json -m <module@version> to also download the module and get metadata at the same time. And I just tried, it's not restricted by the current module dir. I can run the command anywhere to get the modules.
I think we are now very close to lift the restriction of go module dir, if we just go list -json -m <the-entire-list-of-modules> to get all the modules.

There's one remaining challenge to me, the main module to build a binary is usually not versioned. How can we know what it is if outside of the go working dir?

This causes cause problems if the current module context doesn't match the same version as the binary.

For clarification, it was expected behavior in my design. How do we know where the main module is and whether it is the correct version? If we are in a working dir and found go modules version has a mismatch, a more serious problem is that most likely the main module's version is incorrect

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the main module to build a binary is usually not versioned. How can we know what it is if outside of the go working dir?

I think you can pull this out from go version, as long as the binary was built from a released module version. e.g.

# Install from github.com/google/go-licenses@latest
$ go version -m go-licenses
go-licenses: devel +b7a85e0003
        path    github.com/google/go-licenses
        mod     github.com/google/go-licenses   v0.0.0-20210816172045-3099c18c36e1 h1:ZK63Yns/0Y8hE5y50WuSsfFWNPmpYDQ9tzh/J2vWV8c=
# Install from master
$ go version -m go-licenses
go-licenses: devel +b7a85e0003
        path    github.com/google/go-licenses
        mod     github.com/google/go-licenses   (devel)

(+b7a85e0003 corresponds to the go tool version, not the module version, so we can't infer the source commit from there)

I don't know how we can infer version information from devel sources. 😞 The answer might just be we can't for now and just throw an error for the time being telling people they need to go install @version if they want it to work.

If we are in a working dir and found go modules version has a mismatch, a more serious problem is that most likely the main module's version is incorrect

Assuming main module == binary module here, I think this is expected. The binary module is going to be different from the local working directory module, unless the local working directory module is the module that produces the binary (but if that were the case, my expectation is you would run go-licenses on the source package itself rather than the binary). That's why I don't think we can join the modules here, not only because of version mismatches, but also different replacements.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The binary module is going to be different from the local working directory module, unless the local working directory module is the module that produces the binary (but if that were the case, my expectation is you would run go-licenses on the source package itself rather than the binary)

My use-case is exactly running go-licenses in the same working directory that produces the binary, that's a prerequisite for running the tool.
The reason I chose to run the tool on the binary instead of source code is because some go module dependencies (test / tool dependencies) are trimmed when compiling to the binary, so we can check license only for the modules included in the binary.

When you said running go-licenses on the source package itself, I assume you are talking about using packages.Load like the same logic in go-licenses v1. I think both solutions work, if you have strong opinions, I can try to use packages.Load instead.

I personally feel that it's better to rely on the modules metainfo from built binary, because it's better for single source of truth -- the go compiler checks for all dependencies, compile to a binary and record the modules metainfo in the binary.
On the contrary, if we use packages.Load to get all the modules from a package, the go compiler will collect all dependencies once, and packages.Load will get all the modules for the second time. How do we guarantee the two are giving the same results? I think there are potential risks for:

  • version skew -- when go compiler has a different version from packages.Load
  • build tag / config skew -- go compiler uses different build tags / flags than what packages.Load is using
    If we read the modules meta info directly from the binary, these risks are removed, because only the go compiler decides what modules are pulled in.

I believe we are making a trade off here.

Option 1 - the tool only runs in the same workdir used to build the binary.
Option 2 - the tool only works for binaries built by go install module@version.

Considering the major user scenario should be module authors adding license info to binaries built by themselves, I think option 1 is better.

After some experimentation using the new tool in github.com/kubeflow/pipelines, my typical licensing management workflow is like the following:

  1. keep a copy of generated license csv file in the repo
  2. in presubmit test, besides running go unit tests, also run go-licenses csv and verify the license csv is up-to-date. When there's an error, the PR author should check licenses for updated dependencies and update the licenses csv.
  3. when releasing, go-licenses save the notices to the container image containing the binary

In all the scenarios, the person/CICD tool running go-licenses should be using it in exactly the same workdir as where the binary is built.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the additional context! I was under the assumption you wanted to include additional binary tools as part of the license output, or to generate a license summary from a standalone unrelated binary. IIUC, you want to use the binary metadata as a shortcut to generate a minimal license summary that only includes dependencies that would be included in binaries that you are distributing.

The reason I chose to run the tool on the binary instead of source code is because some go module dependencies (test / tool dependencies) are trimmed when compiling to the binary, so we can check license only for the modules included in the binary.

Test dependencies are already excluded unless Test: true is included in packages.Config (which is currently not enabled for go-licenses). Related: #62

Tool dependencies (assuming this is following https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module) would be treated like any other dependency and only be present if they are included transitively.

I think this would cover most cases to generate a minimal license summary, but if you have any examples in your project that don't fit well let me know! Would happy to dive deeper here.

version skew -- when go compiler has a different version from packages.Load

I'm not particularly worried about go compiler vs packages.Load version skew. Go has a pretty good track record when it comes to backwards compatibility. packages.Load is used by other tools in golang.org/x/tools, so I would imagine/hope any breaking changes / incompatibilities would be noticed prior to major Go releases.

What I am more worried about is version skew between the code and the binary, since the binary could have been built from a different version than what is currently present in the repo / go.mod. I'd rather lean on the code as the source of truth as much as possible, since that's what go.mod is referring to.

build tag / config skew -- go compiler uses different build tags / flags than what packages.Load is using

Although go-licenses does not directly accept build flags on its own CLI, we do support build flags via GOFLAGS.
If we wanted, we could pass through flags via packages.Config.

Considering the major user scenario should be module authors adding license info to binaries built by themselves, I think option 1 is better.

Agreed - I also prefer Option 1, but would prefer to use the source code as the source of truth rather than the binary.

FWIW - projects I'm involved in follows an identical pattern to what you're describing in your release workflow using the existing tool.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @wlynch, I think the new update will be what you want.
As we discussed initially, we can support both getting dependencies from the binary or from code + import path.

I've included another implementation to get all the dependencies and their modules using packages.Visit.

FWIW - projects I'm involved in follows an identical pattern to what you're describing in your release workflow using the existing tool.

Glad to know, that gives me more confidence of the UX.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-P0 idea:

By the way, after thinking a bit more about the use-case for analyzing completely unknown go binaries.
I think it can be a good additional feature.
As discussed in #71 (comment), the current limitation is that we do not know the exact version of the main module. However, the limitations seem to be a good trade off.

e.g. consider this UX:

I use go-licenses to analyze random binaries taken from containers, go-licenses can get exact version of all the dependencies and get HEAD version of the main repo. The result is slightly inaccurate for the main repo, so we show a warning message that we do not know version of the main repo. In this case, we can just check HEAD version.
Despite the limitation, the overall result is good-enough, I have a very good idea of which licenses are used in the binary and only in rare cases will there be a mismatch. From what I heard, when a project changes license, most of the cases, it's switching from a permissive license to a more restrictive license, so analyzing HEAD version of the main repo is unlikely to give false negatives. False positives are easy to deal with, because people can just check them manually.

return nil, fmt.Errorf("Found %v %v in go binary, but %v is downloaded in go modules. Are you running this tool from the working dir to build the binary you are analyzing?", ref.Path, ref.Version, localModule.Version)
}
modules = append(modules, localModule)
}
return modules, nil
}
100 changes: 100 additions & 0 deletions v2/gocli/go_binary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gocli_test

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"testing"

"github.com/google/go-licenses/v2/gocli"
"github.com/stretchr/testify/assert"
)

func TestListModulesInGoBinary(t *testing.T) {
var tests = []struct {
workdir string
mainModule string
modules []string
}{
{
workdir: "../tests/modules/hello01",
mainModule: "github.com/google/go-licenses/v2/tests/modules/hello01",
modules: []string{},
},
{
workdir: "../tests/modules/cli02",
mainModule: "github.com/google/go-licenses/v2/tests/modules/cli02",
modules: []string{
"github.com/fsnotify/fsnotify@v1.4.9",
"github.com/hashicorp/hcl@v1.0.0",
"github.com/magiconair/properties@v1.8.5",
"github.com/mitchellh/go-homedir@v1.1.0",
"github.com/mitchellh/mapstructure@v1.4.1",
"github.com/pelletier/go-toml@v1.9.3",
"github.com/spf13/afero@v1.6.0",
"github.com/spf13/cast@v1.3.1",
"github.com/spf13/cobra@v1.1.3",
"github.com/spf13/jwalterweatherman@v1.1.0",
"github.com/spf13/pflag@v1.0.5",
"github.com/spf13/viper@v1.8.0",
"github.com/subosito/gotenv@v1.2.0",
"golang.org/x/sys@v0.0.0-20210510120138-977fb7262007",
"golang.org/x/text@v0.3.5",
"gopkg.in/ini.v1@v1.62.0",
"gopkg.in/yaml.v2@v2.4.0",
},
},
}
originalWorkDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for _, tc := range tests {
t.Run(tc.workdir, func(t *testing.T) {
os.Chdir(filepath.Join(originalWorkDir, tc.workdir))
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
// This outputs the built binary as name "main".
binaryName := path.Join(tempDir, "main")
cmd := exec.Command("go", "build", "-o", binaryName)
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
_, err = cmd.Output()
// defer remove before checking error, because the file
// may be created even when there's an error.
defer os.Remove(binaryName)
if err != nil {
t.Fatalf("Failed to build binary: %v", err)
}
metadata, err := gocli.ExtractBinaryMetadata(binaryName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, tc.mainModule, metadata.MainModule)
modulesActual := make([]string, 0)
for _, module := range metadata.Modules {
assert.NotEmpty(t, module.Path)
assert.NotEmpty(t, module.Version)
modulesActual = append(modulesActual, fmt.Sprintf("%s@%s", module.Path, module.Version))
}
assert.Equal(t, tc.modules, modulesActual)
})
}
}
85 changes: 85 additions & 0 deletions v2/gocli/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gocli

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os/exec"

"golang.org/x/tools/go/packages"
)

// List go modules with metadata in workdir using go CLI list command.
// Modules with replace directive are returned as the replaced module instead.
func ListModules() (map[string]Module, error) {
out, err := exec.Command("go", "list", "-m", "-json", "all").Output()
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("Failed to list go modules: %w", err)
}
// reference: https://github.com/golang/go/issues/27655#issuecomment-420993215
modules := make([]Module, 0)

dec := json.NewDecoder(bytes.NewReader(out))
for {
var tmp packages.Module
if err := dec.Decode(&tmp); err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("Failed to read go list output: %w", err)
}
// Example of a module with replace directive: k8s.io/kubernetes => k8s.io/kubernetes v1.11.1
// {
// "Path": "k8s.io/kubernetes",
// "Version": "v0.17.9",
// "Replace": {
// "Path": "k8s.io/kubernetes",
// "Version": "v1.11.1",
// "Time": "2018-07-17T04:20:29Z",
// "Dir": "/home/gongyuan_kubeflow_org/go/pkg/mod/k8s.io/kubernetes@v1.11.1",
// "GoMod": "/home/gongyuan_kubeflow_org/go/pkg/mod/cache/download/k8s.io/kubernetes/@v/v1.11.1.mod"
// },
// "Dir": "/home/gongyuan_kubeflow_org/go/pkg/mod/k8s.io/kubernetes@v1.11.1",
// "GoMod": "/home/gongyuan_kubeflow_org/go/pkg/mod/cache/download/k8s.io/kubernetes/@v/v1.11.1.mod"
// }
// handle replace directives
// Note, we specifically want to replace version field.
// Haven't confirmed, but we may also need to override the
// entire struct when using replace directive with local folders.
mod := tmp
if mod.Replace != nil {
mod = *mod.Replace
}
modules = append(modules, Module{
Path: mod.Path,
Version: mod.Version,
Time: mod.Time,
Main: mod.Main,
Indirect: mod.Indirect,
Dir: mod.Dir,
GoMod: mod.GoMod,
GoVersion: mod.GoVersion,
})
}

dict := make(map[string]Module)
for i := range modules {
dict[modules[i].Path] = modules[i]
}
return dict, nil
}
Loading