Skip to content

Commit 5f499dc

Browse files
feat: extend GoModResolver to support any valid Go *.mod file (#268)
1 parent 8c96ee2 commit 5f499dc

3 files changed

Lines changed: 194 additions & 9 deletions

File tree

Makefile

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ PROJECT ?= license-eye
2020
VERSION ?= latest
2121
INSTALL_DIR ?= /usr/local/bin
2222
OUT_DIR = bin
23-
ARCH := $(shell uname)
24-
OSNAME := $(if $(findstring Darwin,$(ARCH)),darwin,linux)
23+
UNAME := $(shell uname)
24+
OSNAME := $(if $(findstring Darwin,$(UNAME)),darwin,linux)
2525

2626
GO := GO111MODULE=on go
2727
GO_PATH = $(shell $(GO) env GOPATH)
@@ -30,12 +30,13 @@ GO_TEST = $(GO) test
3030
GO_LINT = $(GO_PATH)/bin/golangci-lint
3131
GO_BUILD_LDFLAGS = -X github.com/apache/skywalking-eyes/commands.version=$(VERSION)
3232
GOOS ?= $(shell $(GO) env GOOS)
33+
GO_TEST_IMAGE := $(shell sed -n '/^FROM/{ s/FROM \([^ ]*\) AS.*/\1/p; q; }' Dockerfile)
3334

3435
PLANTUML_VERSION = 1.2021.9
3536

3637
PLATFORMS := windows linux darwin
3738
os = $(word 1, $@)
38-
ARCH = amd64
39+
ARCH ?= $(shell $(GO) env GOARCH)
3940

4041
RELEASE_BIN = skywalking-$(PROJECT)-$(VERSION)-bin
4142
RELEASE_SRC = skywalking-$(PROJECT)-$(VERSION)-src
@@ -63,6 +64,14 @@ test: clean
6364
$(GO_TEST) ./... -coverprofile=coverage.txt -covermode=atomic
6465
@>&2 echo "Great, all tests passed."
6566

67+
.PHONY: test-docker
68+
test-docker:
69+
docker run --rm \
70+
-v $(shell pwd):/license-eye \
71+
-w /license-eye \
72+
$(GO_TEST_IMAGE) \
73+
sh -c "apk add --no-cache git make maven && make test"
74+
6675
windows: PROJECT_SUFFIX=.exe
6776

6877
.PHONY: $(PLATFORMS)

pkg/deps/golang.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"os/exec"
2828
"path/filepath"
2929
"regexp"
30+
"strings"
3031

3132
"github.com/apache/skywalking-eyes/pkg/license"
3233
"github.com/apache/skywalking-eyes/pkg/logger"
@@ -38,27 +39,59 @@ type GoModResolver struct {
3839
Resolver
3940
}
4041

42+
const (
43+
goModFileName = "go.mod"
44+
)
45+
46+
var (
47+
goModuleDirective = regexp.MustCompile(`(?m)^\s*module\s+\S`)
48+
possibleLicenseFileName = regexp.MustCompile(`(?i)^(LICENSE|LICENCE|COPYING)(\.txt)?$`)
49+
)
50+
4151
func (resolver *GoModResolver) CanResolve(file string) bool {
4252
base := filepath.Base(file)
4353
logger.Log.Debugln("Base name:", base)
44-
return base == "go.mod"
54+
return strings.HasSuffix(base, ".mod")
55+
}
56+
57+
func validateGoModFile(file string) error {
58+
content, err := os.ReadFile(file)
59+
if err != nil {
60+
return err
61+
}
62+
if !goModuleDirective.Match(content) {
63+
return fmt.Errorf("%v is not a valid Go module file", file)
64+
}
65+
return nil
4566
}
4667

4768
// Resolve resolves licenses of all dependencies declared in the go.mod file.
4869
func (resolver *GoModResolver) Resolve(goModFile string, config *ConfigDeps, report *Report) error {
70+
if err := validateGoModFile(goModFile); err != nil {
71+
return err
72+
}
73+
4974
if err := os.Chdir(filepath.Dir(goModFile)); err != nil {
5075
return err
5176
}
5277

53-
goModDownload := exec.Command("go", "mod", "download")
78+
base := filepath.Base(goModFile)
79+
downloadArgs := []string{"mod", "download"}
80+
jsonArgs := []string{"mod", "download", "-json"}
81+
if base != goModFileName {
82+
downloadArgs = append(downloadArgs, "-modfile", base)
83+
jsonArgs = append(jsonArgs, "-modfile", base)
84+
}
85+
86+
goModDownload := exec.Command("go", downloadArgs...)
5487
logger.Log.Debugf("Run command: %v, please wait", goModDownload.String())
5588
goModDownload.Stdout = os.Stdout
5689
goModDownload.Stderr = os.Stderr
5790
if err := goModDownload.Run(); err != nil {
5891
return err
5992
}
6093

61-
output, err := exec.Command("go", "mod", "download", "-json").Output()
94+
output, err := exec.Command("go", jsonArgs...).Output()
6295
if err != nil {
6396
return err
6497
}
@@ -110,8 +143,6 @@ func (resolver *GoModResolver) ResolvePackages(modules []*packages.Module, confi
110143
return nil
111144
}
112145

113-
var possibleLicenseFileName = regexp.MustCompile(`(?i)^LICENSE|LICENCE(\.txt)?|COPYING(\.txt)?$`)
114-
115146
func (resolver *GoModResolver) ResolvePackageLicense(config *ConfigDeps, module *packages.Module, report *Report) error {
116147
dir := module.Dir
117148

@@ -122,7 +153,7 @@ func (resolver *GoModResolver) ResolvePackageLicense(config *ConfigDeps, module
122153
return err
123154
}
124155
for _, info := range files {
125-
if !possibleLicenseFileName.MatchString(info.Name()) {
156+
if info.IsDir() || !possibleLicenseFileName.MatchString(info.Name()) {
126157
continue
127158
}
128159
licenseFilePath := filepath.Join(dir, info.Name())
@@ -135,6 +166,7 @@ func (resolver *GoModResolver) ResolvePackageLicense(config *ConfigDeps, module
135166
return err
136167
}
137168

169+
logger.Log.Debugf("\t- Found license: %v", identifier)
138170
report.Resolve(&Result{
139171
Dependency: module.Path,
140172
LicenseFilePath: licenseFilePath,

pkg/deps/golang_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package deps_test
19+
20+
import (
21+
"io"
22+
"os"
23+
"path/filepath"
24+
"runtime"
25+
"testing"
26+
27+
"golang.org/x/tools/go/packages"
28+
29+
"github.com/apache/skywalking-eyes/pkg/deps"
30+
"github.com/apache/skywalking-eyes/pkg/logger"
31+
)
32+
33+
func TestMain(m *testing.M) {
34+
logger.Log.SetOutput(io.Discard)
35+
os.Exit(m.Run())
36+
}
37+
38+
const (
39+
validGoMod = `module example.com/foo
40+
41+
go 1.21
42+
`
43+
noModuleDirective = "go 1.21\n"
44+
spdxApache20 = "Apache-2.0"
45+
)
46+
47+
func TestCanResolveGoMod(t *testing.T) {
48+
resolver := new(deps.GoModResolver)
49+
dir := t.TempDir()
50+
51+
tests := []struct {
52+
name string
53+
filename string
54+
want bool
55+
}{
56+
{"go.mod", "go.mod", true},
57+
{"go.tool.mod", "go.tool.mod", true},
58+
{"custom.mod", "custom.mod", true},
59+
{"non-.mod extension", "Cargo.toml", false},
60+
{"no extension", "go", false},
61+
}
62+
63+
for _, tt := range tests {
64+
t.Run(tt.name, func(t *testing.T) {
65+
path := writeTempFile(t, dir, tt.filename, validGoMod)
66+
if got := resolver.CanResolve(path); got != tt.want {
67+
t.Errorf("CanResolve(%q) = %v, want %v", tt.filename, got, tt.want)
68+
}
69+
})
70+
}
71+
}
72+
73+
func TestResolveGoModInvalidFile(t *testing.T) {
74+
resolver := new(deps.GoModResolver)
75+
config := &deps.ConfigDeps{Threshold: 75}
76+
77+
tests := []struct {
78+
name string
79+
content string
80+
}{
81+
{"missing module directive", noModuleDirective},
82+
{"non-Go content", "worker_processes auto;\n"},
83+
}
84+
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
dir := t.TempDir()
88+
path := writeTempFile(t, dir, "go.mod", tt.content)
89+
var report deps.Report
90+
if err := resolver.Resolve(path, config, &report); err == nil {
91+
t.Errorf("Resolve should return an error for: %v", tt.name)
92+
}
93+
})
94+
}
95+
}
96+
97+
func TestResolvePackageLicense(t *testing.T) {
98+
resolver := new(deps.GoModResolver)
99+
config := &deps.ConfigDeps{Threshold: 75}
100+
101+
_, thisFile, _, ok := runtime.Caller(0)
102+
if !ok {
103+
t.Fatal("runtime.Caller failed")
104+
}
105+
apacheLicense, err := os.ReadFile(filepath.Join(filepath.Dir(thisFile), "..", "..", "LICENSE"))
106+
if err != nil {
107+
t.Fatalf("failed to read LICENSE fixture: %v", err)
108+
}
109+
110+
t.Run("license found in module dir", func(t *testing.T) {
111+
dir := t.TempDir()
112+
writeTempFile(t, dir, "LICENSE", string(apacheLicense))
113+
114+
module := &packages.Module{Path: "example.com/foo", Version: "v1.0.0", Dir: dir}
115+
var report deps.Report
116+
if err := resolver.ResolvePackageLicense(config, module, &report); err != nil {
117+
t.Fatalf("unexpected error: %v", err)
118+
}
119+
if len(report.Resolved) != 1 {
120+
t.Fatalf("expected 1 resolved, got %d", len(report.Resolved))
121+
}
122+
if report.Resolved[0].LicenseSpdxID != spdxApache20 {
123+
t.Errorf("expected %v, got %v", spdxApache20, report.Resolved[0].LicenseSpdxID)
124+
}
125+
})
126+
127+
t.Run("no license found", func(t *testing.T) {
128+
dir := t.TempDir()
129+
module := &packages.Module{Path: "example.com/foo", Version: "v1.0.0", Dir: dir}
130+
var report deps.Report
131+
if err := resolver.ResolvePackageLicense(config, module, &report); err == nil {
132+
t.Error("expected error when no license file present")
133+
}
134+
})
135+
}
136+
137+
func writeTempFile(t *testing.T, dir, name, content string) string {
138+
t.Helper()
139+
path := filepath.Join(dir, name)
140+
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
141+
t.Fatalf("failed to write %v: %v", name, err)
142+
}
143+
return path
144+
}

0 commit comments

Comments
 (0)