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

Resolve dependencies from both host go.mod and module's go.mod #225

Merged
merged 9 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions test/go_repo_transitive_deps/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
subinclude("///shell//build_defs:shell", "//test/build_defs:e2e")

please_repo_e2e_test(
name = "go_repo_transitive_deps_test",
plz_command = "plz test",
repo = "test_repo",
)
14 changes: 14 additions & 0 deletions test/go_repo_transitive_deps/test_repo/.plzconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Parse]
BuildFileName = BUILD_FILE
BuildFileName = BUILD

[Plugin "go"]
Target = //plugins:go
PleaseGoTool = <pleasegotool>
GoTool = <gotool>
FeatureFlags = go_get
Stdlib = //third_party/go:std
Modfile = //third_party/go:modfile

[FeatureFlags]
ExcludeGoRules = true
7 changes: 7 additions & 0 deletions test/go_repo_transitive_deps/test_repo/BUILD_FILE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
subinclude("///go//build_defs:go")

go_test(
name = "go_repo_transitive_deps_test",
srcs = ["go_repo_transitive_deps_test.go"],
deps = ["//third_party/go:testify"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package test_repo

// Testify has a dependency on github.com/pmezard/go-difflib which we DON'T
// have in our go.mod file but testify does.
// The test ensures that go_please generate considers both the host and the
// module go.mod and correctly generates the dependencies for the subrepo.

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestAssertImportable(t *testing.T) {
assert.True(t, true)
}
4 changes: 4 additions & 0 deletions test/go_repo_transitive_deps/test_repo/plugins/BUILD_FILE
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
plugin_repo(
name = "go",
revision = "master",
)
42 changes: 42 additions & 0 deletions test/go_repo_transitive_deps/test_repo/third_party/go/BUILD_FILE
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
subinclude("///go//build_defs:go")

filegroup(
name = "modfile",
srcs = ["go.mod"],
)

go_stdlib(
name = "std",
)

go_repo(
name = "testify",
licences = ["MIT"],
module = "github.com/stretchr/testify",
install = ["..."],
version = "v1.7.0",
)

go_repo(
licences = ["MIT"],
module = "github.com/stretchr/objx",
version = "v0.5.0",
)

go_repo(
licences = ["BSD-3-Clause"],
module = "github.com/pmezard/go-difflib",
version = "v1.0.0",
)

go_repo(
licences = ["ISC"],
module = "github.com/davecgh/go-spew",
version = "v1.1.1",
)

go_repo(
licences = ["MIT"],
module = "gopkg.in/yaml.v3",
version = "v3.0.0-20210107192922-496545a6307b",
)
10 changes: 10 additions & 0 deletions test/go_repo_transitive_deps/test_repo/third_party/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module gorepotransitivedeps

go 1.21

require (
// For this test to work this go.mod should only ever list "testify" as a dependency.
// The point of the test is to ensure that the go.mod of "testify" (or any third-party)
// itself is used as part of dependency resolving.
github.com/stretchr/testify v1.8.4
)
1 change: 1 addition & 0 deletions tools/please_go/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ genrule(
"//tools/please_go/embed:srcs",
"//tools/please_go/filter:srcs",
"//tools/please_go/generate:srcs",
"//tools/please_go/generate/gomoddeps:srcs",
"//tools/please_go/goget:srcs",
"//tools/please_go/install:srcs",
"//tools/please_go/install/exec:srcs",
Expand Down
5 changes: 5 additions & 0 deletions tools/please_go/ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
Version 1.10.0
-------------
* Inspect both the host repo and the module's go.mod when resolving dependencies in the
`generate` command.

Version 1.9.2
-------------
* Generate correct paths for subdir imports within a module.
Expand Down
2 changes: 1 addition & 1 deletion tools/please_go/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.9.2
1.10.0
4 changes: 2 additions & 2 deletions tools/please_go/generate/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ go_library(
visibility = ["//tools/..."],
deps = [
"//third_party/go:buildtools",
"//third_party/go:mod",
"//tools/please_go/generate/gomoddeps",
],
)

Expand All @@ -23,4 +23,4 @@ go_test(
"//third_party/go:testify",
":generate",
],
)
)
50 changes: 13 additions & 37 deletions tools/please_go/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import (
"io/fs"
"log"
"os"
"path"
"path/filepath"
"strings"

"golang.org/x/mod/modfile"

bazelbuild "github.com/bazelbuild/buildtools/build"
bazeledit "github.com/bazelbuild/buildtools/edit"

"github.com/please-build/go-rules/tools/please_go/generate/gomoddeps"
)

type Generate struct {
Expand All @@ -22,17 +23,16 @@ type Generate struct {
srcRoot string
subrepo string
buildContext build.Context
modFile string
hostModFile string
buildFileNames []string
moduleDeps []string
pluginTarget string
replace map[string]string
knownImportTargets map[string]string // cache these so we don't end up looping over all the modules for every import
thirdPartyFolder string
install []string
}

func New(srcRoot, thirdPartyFolder, modFile, module, version, subrepo string, buildFileNames, moduleDeps, install []string, buildTags []string) *Generate {
func New(srcRoot, thirdPartyFolder, hostModFile, module, version, subrepo string, buildFileNames, moduleDeps, install []string, buildTags []string) *Generate {
moduleArg := module
if version != "" {
moduleArg += "@" + version
Expand All @@ -46,7 +46,7 @@ func New(srcRoot, thirdPartyFolder, modFile, module, version, subrepo string, bu
buildContext: ctxt,
buildFileNames: buildFileNames,
moduleDeps: moduleDeps,
modFile: modFile,
hostModFile: hostModFile,
knownImportTargets: map[string]string{},
thirdPartyFolder: thirdPartyFolder,
install: install,
Expand All @@ -59,9 +59,13 @@ func New(srcRoot, thirdPartyFolder, modFile, module, version, subrepo string, bu
// Generate generates a new Please project at the src root. It will walk through the directory tree generating new BUILD
// files. This is primarily intended to generate a please subrepo for third party code.
func (g *Generate) Generate() error {
if err := g.readGoMod(g.modFile); err != nil {
return fmt.Errorf("failed to read go.mod: %w", err)
deps, replacements, err := gomoddeps.GetCombinedDepsAndReplacements(g.hostModFile, path.Join(g.srcRoot, "go.mod"))
if err != nil {
return err
}
g.moduleDeps = append(deps, g.moduleName)
g.replace = replacements

if err := g.writeConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
Expand Down Expand Up @@ -195,34 +199,6 @@ func (g *Generate) writeInstallFilegroup() error {
return saveBuildFile(buildFile)
}

// readGoMod reads the module dependencies
func (g *Generate) readGoMod(path string) error {
if path == "" {
path = filepath.Join(g.srcRoot, "go.mod")
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
modFile, err := modfile.ParseLax(path, data, nil)
if err != nil {
return err
}

// TODO we could probably validate these are known modules
for _, req := range modFile.Require {
g.moduleDeps = append(g.moduleDeps, req.Mod.Path)
}

g.moduleDeps = append(g.moduleDeps, g.moduleName)

g.replace = make(map[string]string, len(modFile.Replace))
for _, replace := range modFile.Replace {
g.replace[replace.Old.Path] = replace.New.Path
}
return nil
}

func (g *Generate) writeConfig() error {
file, err := os.Create(filepath.Join(g.srcRoot, ".plzconfig"))
if err != nil {
Expand Down Expand Up @@ -464,7 +440,7 @@ func (g *Generate) depTarget(importPath string) string {
return target
}

if replacement, ok := g.replace[importPath]; ok {
if replacement, ok := g.replace[importPath]; ok && replacement != importPath {
target := g.depTarget(replacement)
g.knownImportTargets[importPath] = target
return target
Expand Down
30 changes: 30 additions & 0 deletions tools/please_go/generate/gomoddeps/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
subinclude("///go//build_defs:go")

filegroup(
name = "srcs",
srcs = glob(["*.go"], exclude=["*_test.go"]),
visibility = ["//tools/please_go:bootstrap"],
)

go_library(
name = "gomoddeps",
srcs = [
":srcs",
],
visibility = ["//tools/please_go/generate/..."],
deps = [
"//third_party/go:mod",
],
)

go_test(
name = "gomoddeps_test",
srcs = glob(["*_test.go"]),
data = [
"//tools/please_go/generate/gomoddeps/test_data:test_go_mod_files",
],
deps = [
"//third_party/go:testify",
":gomoddeps",
],
)
84 changes: 84 additions & 0 deletions tools/please_go/generate/gomoddeps/gomoddeps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Package gomoddeps parses dependencies and replacements from the host and module go.mod files.
package gomoddeps

import (
"errors"
"fmt"
"io/fs"
"os"

"golang.org/x/mod/modfile"
)

// GetCombinedDepsAndReplacements returns dependencies and replacements after inspecting both
// the host and the module go.mod files.
// Module's replacement are only returned if there is no host go.mod file.
func GetCombinedDepsAndReplacements(hostGoModPath, moduleGoModPath string) ([]string, map[string]string, error) {
var err error

hostDeps := []string{}
hostReplacements := map[string]string{}
if hostGoModPath != "" {
hostDeps, hostReplacements, err = getDepsAndReplacements(hostGoModPath, false)
if err != nil {
return nil, nil, fmt.Errorf("failed to read host repo go.mod %q: %w", hostGoModPath, err)
}
}

var moduleDeps []string
var moduleReplacements = map[string]string{}
useLaxParsingForModule := true
if hostGoModPath == "" {
// If we're only considering the module then we want to extract the replacement's as well (lax mode
// doesn't parse them).
useLaxParsingForModule = false
}
moduleDeps, moduleReplacements, err = getDepsAndReplacements(moduleGoModPath, useLaxParsingForModule)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return hostDeps, hostReplacements, nil
}
return nil, nil, fmt.Errorf("failed to read module go.mod %q: %w", moduleGoModPath, err)
}

var replacements map[string]string
if hostGoModPath == "" {
replacements = moduleReplacements
} else {
replacements = hostReplacements
}

return append(hostDeps, moduleDeps...), replacements, nil
}

// getDepsAndReplacements parses the go.mod file and returns all the dependencies
// and replacement directives from it.
func getDepsAndReplacements(goModPath string, useLaxParsing bool) ([]string, map[string]string, error) {
data, err := os.ReadFile(goModPath)
if err != nil {
return nil, nil, err
}

var modFile *modfile.File
if useLaxParsing {
modFile, err = modfile.ParseLax(goModPath, data, nil)
} else {
modFile, err = modfile.Parse(goModPath, data, nil)
}
if err != nil {
return nil, nil, err
}

moduleDeps := make([]string, 0, len(modFile.Require))
// TODO we could probably validate these are known modules
for _, req := range modFile.Require {
moduleDeps = append(moduleDeps, req.Mod.Path)
}

replacements := make(map[string]string, len(modFile.Replace))
for _, replace := range modFile.Replace {
replacements[replace.Old.Path] = replace.New.Path
}

return moduleDeps, replacements, nil
}
Loading
Loading