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

Let bazel_deps replace Go deps #1526

Merged
merged 4 commits into from
Jun 2, 2023
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
/tests/bcr/bazel-out
/tests/bcr/bazel-testlogs
.DS_STORE
/.ijwb/
.ijwb/
14 changes: 13 additions & 1 deletion cmd/gazelle/fix-update.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,22 @@ func (ucr *updateConfigurer) CheckFlags(fs *flag.FlagSet, c *config.Config) erro
GoPrefix: imp,
})
}

moduleToApparentName, err := module.ExtractModuleToApparentNameMapping(c.RepoRoot)
if err != nil {
return err
}

for _, r := range c.Repos {
if r.Kind() == "go_repository" {
var name string
if apparentName := moduleToApparentName(r.AttrString("module_name")); apparentName != "" {
name = apparentName
} else {
name = r.Name()
}
uc.repos = append(uc.repos, repo.Repo{
Name: r.Name(),
Name: name,
GoPrefix: r.AttrString("importpath"),
})
}
Expand Down
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ type Config struct {
// extensions as well. Values in here may be populated by command line
// arguments, directives in build files, or other mechanisms.
Exts map[string]interface{}

// Whether Gazelle is loaded as a Bzlmod 'bazel_dep'.
Bzlmod bool
}

// MappedKind describes a replacement to use for a built-in kind.
Expand Down Expand Up @@ -192,6 +195,7 @@ type CommonConfigurer struct {
repoRoot, buildFileNames, readBuildFilesDir, writeBuildFilesDir string
indexLibraries, strict bool
langCsv string
bzlmod bool
}

func (cc *CommonConfigurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *Config) {
Expand All @@ -202,6 +206,7 @@ func (cc *CommonConfigurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *Confi
fs.StringVar(&cc.readBuildFilesDir, "experimental_read_build_files_dir", "", "path to a directory where build files should be read from (instead of -repo_root)")
fs.StringVar(&cc.writeBuildFilesDir, "experimental_write_build_files_dir", "", "path to a directory where build files should be written to (instead of -repo_root)")
fs.StringVar(&cc.langCsv, "lang", "", "if non-empty, process only these languages (e.g. \"go,proto\")")
fs.BoolVar(&cc.bzlmod, "bzlmod", false, "for internal usage only")
}

func (cc *CommonConfigurer) CheckFlags(fs *flag.FlagSet, c *Config) error {
Expand Down Expand Up @@ -244,6 +249,7 @@ func (cc *CommonConfigurer) CheckFlags(fs *flag.FlagSet, c *Config) error {
if len(cc.langCsv) > 0 {
c.Langs = strings.Split(cc.langCsv, ",")
}
c.Bzlmod = cc.bzlmod
return nil
}

Expand Down
2 changes: 2 additions & 0 deletions def.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ def _gazelle_runner_impl(ctx):
args.extend(["-go_prefix", ctx.attr.prefix])
if ctx.attr.build_tags:
args.extend(["-build_tags", ",".join(ctx.attr.build_tags)])
if IS_BZLMOD_ENABLED:
args.append("-bzlmod")
args.extend([ctx.expand_location(arg, ctx.attr.data) for arg in ctx.attr.extra_args])

for key in ctx.attr.env:
Expand Down
63 changes: 53 additions & 10 deletions internal/bzlmod/go_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ load(

visibility("//")

# These Go modules are imported as Bazel modules via bazel_dep, not as
# go_repository.
_IGNORED_MODULE_PATHS = [
"github.com/bazelbuild/bazel-gazelle",
"github.com/bazelbuild/rules_go",
]
_HIGHEST_VERSION_SENTINEL = semver.to_comparable("999999.999999.999999")

_FORBIDDEN_OVERRIDE_TAG = """\
Using the "go_deps.{tag_class}" tag in a non-root Bazel module is forbidden, \
Expand Down Expand Up @@ -106,6 +101,7 @@ def _go_repository_config_impl(ctx):
"go_repository",
name = name,
importpath = importpath,
module_name = ctx.attr.module_names.get(name),
build_naming_convention = ctx.attr.build_naming_conventions.get(name),
))

Expand All @@ -116,6 +112,7 @@ _go_repository_config = repository_rule(
implementation = _go_repository_config_impl,
attrs = {
"importpaths": attr.string_dict(mandatory = True),
"module_names": attr.string_dict(mandatory = True),
"build_naming_conventions": attr.string_dict(mandatory = True),
},
)
Expand All @@ -127,6 +124,7 @@ def _go_deps_impl(module_ctx):
module_resolutions = {}
sums = {}
replace_map = {}
bazel_deps = {}

gazelle_overrides = {}
module_overrides = {}
Expand Down Expand Up @@ -183,7 +181,7 @@ def _go_deps_impl(module_ctx):
)
additional_module_tags = []
for from_file_tag in module.tags.from_file:
module_tags_from_go_mod, go_mod_replace_map = deps_from_go_mod(module_ctx, from_file_tag.go_mod)
module_path, module_tags_from_go_mod, go_mod_replace_map = deps_from_go_mod(module_ctx, from_file_tag.go_mod)
is_dev_dependency = _is_dev_dependency(module_ctx, from_file_tag)
additional_module_tags += [
with_replaced_or_new_fields(tag, _is_dev_dependency = is_dev_dependency)
Expand All @@ -192,6 +190,21 @@ def _go_deps_impl(module_ctx):

if module.is_root:
replace_map.update(go_mod_replace_map)
else:
# Register this Bazel module as providing the specified Go module. It participates
# in version resolution using its registry version, which uses a relaxed variant of
# semver that can however still be compared to strict semvers.
# An empty version string signals an override, which is assumed to be newer than any
# other version.
raw_version = _canonicalize_raw_version(module.version)
version = semver.to_comparable(raw_version, relaxed = True) if raw_version else _HIGHEST_VERSION_SENTINEL
if module_path not in bazel_deps or version > bazel_deps[module_path].version:
bazel_deps[module_path] = struct(
module_name = module.name,
repo_name = "@" + from_file_tag.go_mod.workspace_name,
version = version,
raw_version = raw_version,
)

# Load all sums from transitively resolved `go.sum` files that have modules.
if len(module_tags_from_go_mod) > 0:
Expand Down Expand Up @@ -222,7 +235,7 @@ def _go_deps_impl(module_ctx):
for module_tag in module.tags.module + additional_module_tags:
if module_tag.path in paths:
fail("Duplicate Go module path \"{}\" in module \"{}\".".format(module_tag.path, module.name))
if module_tag.path in _IGNORED_MODULE_PATHS:
if module_tag.path in bazel_deps:
continue
paths[module_tag.path] = None
raw_version = _canonicalize_raw_version(module_tag.version)
Expand All @@ -242,7 +255,6 @@ def _go_deps_impl(module_ctx):
version = semver.to_comparable(raw_version)
if module_tag.path not in module_resolutions or version > module_resolutions[module_tag.path].version:
module_resolutions[module_tag.path] = struct(
module = module.name,
repo_name = _repo_name(module_tag.path),
version = version,
raw_version = raw_version,
Expand Down Expand Up @@ -274,7 +286,28 @@ def _go_deps_impl(module_ctx):
# not ever report an implicit version upgrade.
root_versions.pop(path)
else:
root_versions[path] = new_version
root_versions[path] = replace.version

for path, bazel_dep in bazel_deps.items():
# We can't apply overrides to Bazel dependencies and thus fall back to using the Go module.
if path in gazelle_overrides or path in module_overrides or path in replace_map:
continue

# Only use the Bazel module if it is at least as high as the required Go module version.
if path in module_resolutions and bazel_dep.version < module_resolutions[path].version:
outdated_direct_dep_printer(
"Go module \"{path}\" is provided by Bazel module \"{bazel_module}\" in version {bazel_dep_version}, but requested at higher version {go_version} via Go requirements. Consider adding or updating an appropriate \"bazel_dep\" to ensure that the Bazel module is used to provide the Go module.".format(
path = path,
bazel_module = bazel_dep.module_name,
bazel_dep_version = bazel_dep.raw_version,
go_version = module_resolutions[path].raw_version,
),
)
continue

# TODO: We should update root_versions if the bazel_dep is a direct dependency of the root
# module. However, we currently don't have a way to determine that.
module_resolutions[path] = bazel_dep

for path, root_version in root_versions.items():
if semver.to_comparable(root_version) < module_resolutions[path].version:
Expand All @@ -287,6 +320,12 @@ def _go_deps_impl(module_ctx):
)

for path, module in module_resolutions.items():
if hasattr(module, "module_name"):
# Do not create a go_repository for a Go module provided by a bazel_dep.
root_module_direct_deps.pop(_repo_name(path), default = None)
root_module_direct_dev_deps.pop(_repo_name(path), default = None)
continue

go_repository(
name = module.repo_name,
importpath = path,
Expand All @@ -310,6 +349,10 @@ def _go_deps_impl(module_ctx):
module.repo_name: path
for path, module in module_resolutions.items()
},
module_names = {
info.repo_name: info.module_name
for path, info in bazel_deps.items()
},
build_naming_conventions = drop_nones({
module.repo_name: get_directive_value(
_get_directives(path, gazelle_overrides),
Expand Down
6 changes: 3 additions & 3 deletions internal/bzlmod/go_mod.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ def deps_from_go_mod(module_ctx, go_mod_label):
go_mod_label: a Label for a `go.mod` file.

Returns:
A list of structs is returned. Each struct represents a `require` statement
from the go.mod file.
a tuple (Go module path, deps, replace map), where deps is a list of structs representing
`require` statements from the go.mod file.
"""
_check_go_mod_name(go_mod_label.name)

Expand All @@ -33,7 +33,7 @@ def deps_from_go_mod(module_ctx, go_mod_label):
indirect = require.indirect,
))

return deps, go_mod.replace_map
return go_mod.module, deps, go_mod.replace_map

def parse_go_mod(content, path):
# See https://go.dev/ref/mod#go-mod-file.
Expand Down
29 changes: 19 additions & 10 deletions internal/bzlmod/semver.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ _COMPARES_LOWEST_SENTINEL = ""
# Compares higher than any valid non-numeric identifier (containing only [A-Za-z0-9-]).
_COMPARES_HIGHEST_SENTINEL = "{"

def _identifier_to_comparable(ident):
def _identifier_to_comparable(ident, *, numeric_only):
if not ident:
fail("Identifiers in semantic version strings must not be empty")
if ident.isdigit():
Expand All @@ -20,36 +20,45 @@ def _identifier_to_comparable(ident):
# 11.4.3:
# "Numeric identifiers always have lower precedence than non-numeric identifiers."
return (_COMPARES_LOWEST_SENTINEL, int(ident))
elif numeric_only:
fail("Expected a numeric identifier, got: " + ident)
else:
# 11.4.2:
# "Identifiers with letters or hyphens are compared lexically in ASCII sort order."
return (ident,)

def _semver_to_comparable(v):
def _semver_to_comparable(v, *, relaxed = False):
"""
Parses a string representation of a semver version into an opaque comparable object.

Args:
v: The string representation of the version.
relaxed: If true, the release version string is allowed to have an arbitrary number of
dot-separated components, each of which is allowed to contain the same set of characters
as a pre-release segment. This is the version string format used by Bazel modules.
"""

# Strip build metadata as it is not relevant for comparisons.
v, _, _ = v.partition("+")

major_minor_patch_str, _, prerelease_str = v.partition("-")
release_str, _, prerelease_str = v.partition("-")
if prerelease_str:
# 11.4.4:
# "A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding
# identifiers are equal."
prerelease = tuple([_identifier_to_comparable(ident) for ident in prerelease_str.split(".")])
prerelease = [_identifier_to_comparable(ident, numeric_only = False) for ident in prerelease_str.split(".")]
else:
# 11.3:
# "When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version."
prerelease = ((_COMPARES_HIGHEST_SENTINEL,),)
prerelease = [(_COMPARES_HIGHEST_SENTINEL,)]

release = release_str.split(".")
if not relaxed and len(release) != 3:
fail("Semantic version strings must have exactly three dot-separated components, got: " + v)

major_str, minor_str, patch_str = major_minor_patch_str.split(".")
return (
int(major_str),
int(minor_str),
int(patch_str),
prerelease,
tuple([_identifier_to_comparable(s, numeric_only = not relaxed) for s in release]),
tuple(prerelease),
)

semver = struct(
Expand Down
4 changes: 3 additions & 1 deletion internal/go_repository.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

load("//internal:common.bzl", "env_execute", "executable_extension")
load("//internal:common.bzl", "IS_BZLMOD_ENABLED", "env_execute", "executable_extension")
load("//internal:go_repository_cache.bzl", "read_cache_env")
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "patch", "read_user_netrc", "use_netrc")

Expand Down Expand Up @@ -304,6 +304,8 @@ def _go_repository_impl(ctx):
cmd.extend(["-proto", ctx.attr.build_file_proto_mode])
if ctx.attr.build_naming_convention:
cmd.extend(["-go_naming_convention", ctx.attr.build_naming_convention])
if IS_BZLMOD_ENABLED:
cmd.append("-bzlmod")
cmd.extend(ctx.attr.build_extra_args)
cmd.append(ctx.path(""))
ctx.report_progress("running Gazelle")
Expand Down
14 changes: 8 additions & 6 deletions language/go/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,14 @@ func ResolveGo(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, im
// These have names that don't following conventions and they're
// typeically declared with http_archive, not go_repository, so Gazelle
// won't recognize them.
if pathtools.HasPrefix(imp, "github.com/bazelbuild/rules_go") {
pkg := pathtools.TrimPrefix(imp, "github.com/bazelbuild/rules_go")
return label.New("io_bazel_rules_go", pkg, "go_default_library"), nil
} else if pathtools.HasPrefix(imp, "github.com/bazelbuild/bazel-gazelle") {
pkg := pathtools.TrimPrefix(imp, "github.com/bazelbuild/bazel-gazelle")
return label.New("bazel_gazelle", pkg, "go_default_library"), nil
if !c.Bzlmod {
if pathtools.HasPrefix(imp, "github.com/bazelbuild/rules_go") {
pkg := pathtools.TrimPrefix(imp, "github.com/bazelbuild/rules_go")
return label.New("io_bazel_rules_go", pkg, "go_default_library"), nil
} else if pathtools.HasPrefix(imp, "github.com/bazelbuild/bazel-gazelle") {
pkg := pathtools.TrimPrefix(imp, "github.com/bazelbuild/bazel-gazelle")
return label.New("bazel_gazelle", pkg, "go_default_library"), nil
}
}

if !c.IndexLibraries {
Expand Down
9 changes: 8 additions & 1 deletion tests/bcr/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ local_path_override(
path = "../..",
)

bazel_dep(name = "rules_go", version = "0.39.1")
bazel_dep(name = "rules_go", version = "0.39.1", repo_name = "my_rules_go")

# This bazel_dep provides the Go dependency github.com/cloudflare/circl, which requires custom
# patches beyond what Gazelle can generate.
bazel_dep(name = "circl", version = "1.3.3")

go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")

Expand Down Expand Up @@ -64,9 +68,12 @@ use_repo(
"com_github_bmatcuk_doublestar_v4",
"com_github_datadog_sketches_go",
"com_github_envoyproxy_protoc_gen_validate",
"com_github_fmeum_dep_on_gazelle",
"com_github_google_safetext",
"com_github_stretchr_testify",
"org_golang_x_sys",
# It is not necessary to list transitive dependencies here, only direct ones.
# "in_gopkg_yaml_v3",
# Only used for testing.
"bazel_gazelle_go_repository_config",
)
10 changes: 10 additions & 0 deletions tests/bcr/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ replace github.com/bmatcuk/doublestar/v4 => github.com/bmatcuk/doublestar v1.3.4

require (
github.com/DataDog/sketches-go v1.4.1
github.com/bazelbuild/rules_go v0.39.1
github.com/bmatcuk/doublestar/v4 v4.0.0
github.com/cloudflare/circl v1.3.3
github.com/envoyproxy/protoc-gen-validate v1.0.1
github.com/fmeum/dep_on_gazelle v1.0.0
github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2
golang.org/x/sys v0.8.0
)

require (
github.com/bazelbuild/bazel-gazelle v0.30.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
)
Loading