blob: bfa5bcfe3af1ff6bf85bdee2f240de17d04314cd [file] [log] [blame]
# Copyright 2023 The Bazel Authors. All rights reserved.
#
# 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.
load("//internal:go_repository.bzl", "go_repository")
load(
":default_gazelle_overrides.bzl",
"DEFAULT_BUILD_EXTRA_ARGS_BY_PATH",
"DEFAULT_BUILD_FILE_GENERATION_BY_PATH",
"DEFAULT_DIRECTIVES_BY_PATH",
)
load(":go_mod.bzl", "deps_from_go_mod", "go_work_from_label", "sums_from_go_mod", "sums_from_go_work")
load(":semver.bzl", "COMPARES_HIGHEST_SENTINEL", "semver")
load(
":utils.bzl",
"drop_nones",
"extension_metadata",
"format_rule_call",
"get_directive_value",
"with_replaced_or_new_fields",
)
visibility("//")
_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, \
but module "{module_name}" requests it.
If you need this override for a Bazel module that will be available in a public \
registry (such as the Bazel Central Registry), please file an issue at \
https://github.com/bazelbuild/bazel-gazelle/issues/new or submit a PR adding \
the required directives to the "default_gazelle_overrides.bzl" file at \
https://github.com/bazelbuild/bazel-gazelle/tree/master/internal/bzlmod/default_gazelle_overrides.bzl.
"""
_GAZELLE_ATTRS = {
"build_file_generation": attr.string(
default = "on",
doc = """One of `"auto"`, `"on"` (default), `"off"`, `"clean"`.
Whether Gazelle should generate build files for the Go module.
Although "auto" is the default globally for build_file_generation,
if a `"gazelle_override"` or `"gazelle_default_attributes"` tag is present
for a Go module, the `"build_file_generation"` attribute will default to "on"
since these tags indicate the presence of `"directives"` or `"build_extra_args"`.
In `"auto"` mode, Gazelle will run if there is no build file in the Go
module's root directory.
In `"clean"` mode, Gazelle will first remove any existing build files.
""",
values = [
"auto",
"off",
"on",
"clean",
],
),
"build_extra_args": attr.string_list(
default = [],
doc = """
A list of additional command line arguments to pass to Gazelle when generating build files.
""",
),
"directives": attr.string_list(
doc = """Gazelle configuration directives to use for this Go module's external repository.
Each directive uses the same format as those that Gazelle
accepts as comments in Bazel source files, with the
directive name followed by optional arguments separated by
whitespace.""",
),
}
def _fail_on_non_root_overrides(module_ctx, module, tag_class):
if module.is_root:
return
# Isolated module extension usages only contain tags from a single module, so we can allow
# overrides. This is a new feature in Bazel 6.3.0, earlier versions do not allow module usages
# to be isolated.
if getattr(module_ctx, "is_isolated", False):
return
if getattr(module.tags, tag_class):
fail(_FORBIDDEN_OVERRIDE_TAG.format(
tag_class = tag_class,
module_name = module.name,
))
def _fail_on_duplicate_overrides(path, module_name, overrides):
if path in overrides:
fail("Multiple overrides defined for Go module path \"{}\" in module \"{}\".".format(path, module_name))
def _fail_on_unmatched_overrides(override_keys, resolutions, override_name):
unmatched_overrides = [path for path in override_keys if path not in resolutions]
if unmatched_overrides:
fail("Some {} did not target a Go module with a matching path: {}".format(
override_name,
", ".join(unmatched_overrides),
))
def _check_directive(directive):
if directive.startswith("gazelle:") and " " in directive and not directive[len("gazelle:"):][0].isspace():
return
fail("Invalid Gazelle directive: \"{}\". Gazelle directives must be of the form \"gazelle:key value\".".format(directive))
def _get_override_or_default(specific_overrides, gazelle_default_attributes, default_path_overrides, path, default_value, attribute_name):
# 1st: Check for user-provided specific overrides. If a specific override is found,
# all of its attributes will be applied (even if left to the tag's default). This is to allow
# users to override the gazelle_default_attributes tag back to the tag's default.
#
# This will also cause "build_file_generation" to default to "on" if a specific override is found.
specific_override = specific_overrides.get(path)
if specific_override and hasattr(specific_override, attribute_name):
return getattr(specific_override, attribute_name)
# 2nd. Check for default attributes provided by the user. This must be done before checking for
# gazelle's defaults path overrides to prevent Gazelle from overriding a user-specified flag.
#
# This will also cause "build_file_generation" to default to "on" if default attributes are found.
global_override_value = getattr(gazelle_default_attributes, attribute_name, None)
if global_override_value:
return global_override_value
# 3rd: Check for default overrides for specific path.
default_path_override = default_path_overrides.get(path)
if default_path_override:
return default_path_override
# 4th. Return the default value if no override was found.
# This will cause "build_file_generation" to default to "auto".
return default_value
def _get_directives(path, gazelle_overrides, gazelle_default_attributes):
return _get_override_or_default(gazelle_overrides, gazelle_default_attributes, DEFAULT_DIRECTIVES_BY_PATH, path, [], "directives")
def _get_build_file_generation(path, gazelle_overrides, gazelle_default_attributes):
# The default value for build_file_generation is "auto" if no override is found, but will default to "on" if an override is found.
return _get_override_or_default(gazelle_overrides, gazelle_default_attributes, DEFAULT_BUILD_FILE_GENERATION_BY_PATH, path, "auto", "build_file_generation")
def _get_build_extra_args(path, gazelle_overrides, gazelle_default_attributes):
return _get_override_or_default(gazelle_overrides, gazelle_default_attributes, DEFAULT_BUILD_EXTRA_ARGS_BY_PATH, path, [], "build_extra_args")
def _get_patches(path, module_overrides):
return _get_override_or_default(module_overrides, struct(), {}, path, [], "patches")
def _get_patch_args(path, module_overrides):
override = _get_override_or_default(module_overrides, struct(), {}, path, None, "patch_strip")
return ["-p{}".format(override)] if override else []
def _repo_name(importpath):
path_segments = importpath.split("/")
segments = reversed(path_segments[0].split(".")) + path_segments[1:]
candidate_name = "_".join(segments).replace("-", "_")
return "".join([c.lower() if c.isalnum() else "_" for c in candidate_name.elems()])
def _is_dev_dependency(module_ctx, tag):
if hasattr(tag, "_is_dev_dependency"):
# Synthetic tags generated from go_deps.from_file have this "hidden" attribute.
return tag._is_dev_dependency
# This function is available in Bazel 6.2.0 and later. This is the same version that has
# module_ctx.extension_metadata, so the return value of this function is not used if it is
# not available.
return module_ctx.is_dev_dependency(tag) if hasattr(module_ctx, "is_dev_dependency") else False
def _intersperse_newlines(tags):
return [tag for p in zip(tags, len(tags) * ["\n"]) for tag in p]
# This function processes the gazelle_default_attributes tag for a given module and returns a struct
# containing the attributes from _GAZELLE_ATTRS that are defined in the tag.
def _process_gazelle_default_attributes(module_ctx):
for module in module_ctx.modules:
_fail_on_non_root_overrides(module_ctx, module, "gazelle_default_attributes")
for module in module_ctx.modules:
tags = module.tags.gazelle_default_attributes
if not tags:
continue
if len(tags) > 1:
fail(
"go_deps.gazelle_default_attributes: only one tag can be specified per module, got:\n",
*[t for p in zip(module.tags.gazelle_default_attributes, len(module.tags.gazelle_default_attributes) * ["\n"]) for t in p]
)
tag = tags[0]
return struct(**{
attr: getattr(tag, attr)
for attr in _GAZELLE_ATTRS.keys()
if hasattr(tag, attr)
})
return None
# This function processes a given override type for a given module, checks for duplicate overrides
# and inserts the override returned from the process_override_func into the overrides dict.
def _process_overrides(module_ctx, module, override_type, overrides, process_override_func, additional_overrides = None):
_fail_on_non_root_overrides(module_ctx, module, override_type)
for override_tag in getattr(module.tags, override_type):
_fail_on_duplicate_overrides(override_tag.path, module.name, overrides)
# Some overrides conflict with other overrides. These can be specified in the
# additional_overrides dict. If the override is in the additional_overrides dict, then fail.
if additional_overrides:
_fail_on_duplicate_overrides(override_tag.path, module.name, additional_overrides)
overrides[override_tag.path] = process_override_func(override_tag)
def _process_gazelle_override(gazelle_override_tag):
for directive in gazelle_override_tag.directives:
_check_directive(directive)
return struct(**{
attr: getattr(gazelle_override_tag, attr)
for attr in _GAZELLE_ATTRS.keys()
if hasattr(gazelle_override_tag, attr)
})
def _process_module_override(module_override_tag):
return struct(
patches = module_override_tag.patches,
patch_strip = module_override_tag.patch_strip,
)
def _process_archive_override(archive_override_tag):
return struct(
urls = archive_override_tag.urls,
sha256 = archive_override_tag.sha256,
strip_prefix = archive_override_tag.strip_prefix,
patches = archive_override_tag.patches,
patch_strip = archive_override_tag.patch_strip,
)
def _go_repository_config_impl(ctx):
repos = []
for name, importpath in sorted(ctx.attr.importpaths.items()):
repos.append(format_rule_call(
"go_repository",
name = name,
importpath = importpath,
module_name = ctx.attr.module_names.get(name),
build_naming_convention = ctx.attr.build_naming_conventions.get(name),
))
ctx.file("WORKSPACE", "\n".join(repos))
ctx.file("BUILD.bazel", "exports_files(['WORKSPACE', 'config.json'])")
ctx.file("go_env.bzl", content = "GO_ENV = " + repr(ctx.attr.go_env))
# For use by @rules_go//go.
ctx.file("config.json", content = json.encode_indent({
"go_env": ctx.attr.go_env,
"dep_files": ctx.attr.dep_files,
}))
_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),
"go_env": attr.string_dict(mandatory = True),
"dep_files": attr.string_list(),
},
)
def check_for_version_conflict(version, previous, module_tag, module_name_to_go_dot_mod_label, conflict_printer):
"""
Check if duplicate modules have different versions, and fail with a useful error message if they do.
Args:
version: The version of the module.
previous: The previous module object.
module_tag: The module tag.
module_name_to_go_dot_mod_label: A dictionary mapping module paths to go.mod labels.
conflict_printer: a printer function to use for printing the error message, generally either print or fail.
"""
if not previous or version == previous.version:
# no previous module, so no possible error OR
# version is the same, skip because we won't error
return
if hasattr(module_tag, "local_path"):
# overrides are not considered for version conflicts
return
# When using go.work, duplicate dependency versions are possible.
# This can cause issues, so we fail with a hopefully actionable error.
current_label = module_tag._parent_label
previous_label = previous.module_tag._parent_label
corrective_measure = """To correct this:
1. ensure that '{}' in all go.mod files is the same version.
2. in the folders where you made changes run: bazel run @rules_go//go -- mod tidy
3. at the workspace root run: bazel run @rules_go//go -- work sync.""".format(module_tag.path)
message = """Multiple versions of {} found:
- {} contains: {}
- {} contains: {}
{}""".format(module_tag.path, current_label, module_tag.version, previous_label, previous.module_tag.version, corrective_measure)
conflict_printer(message)
def _noop(_):
pass
# These repos are shared between the isolated and non-isolated instances of go_deps as they are
# referenced directly by rules (go_proto_library) and would result in linker errors due to duplicate
# packages if they were resolved separately.
# When adding a new Go module to this list, make sure that:
# 1. The corresponding repository is visible to the gazelle module via a use_repo directive.
# 2. All transitive dependencies of the module are also in this list. Avoid adding module that have
# a large number of transitive dependencies.
_SHARED_REPOS = [
"github.com/golang/protobuf",
"google.golang.org/protobuf",
]
def _go_deps_impl(module_ctx):
module_resolutions = {}
sums = {}
replace_map = {}
bazel_deps = {}
gazelle_default_attributes = _process_gazelle_default_attributes(module_ctx)
archive_overrides = {}
gazelle_overrides = {}
module_overrides = {}
root_versions = {}
root_module_direct_deps = {}
root_module_direct_dev_deps = {}
first_module = module_ctx.modules[0]
if first_module.is_root and first_module.name in ["gazelle", "rules_go"]:
root_module_direct_deps["bazel_gazelle_go_repository_config"] = None
outdated_direct_dep_printer = print
go_env = {}
dep_files = []
debug_mode = False
for module in module_ctx.modules:
if len(module.tags.config) > 1:
fail(
"Multiple \"go_deps.config\" tags defined in module \"{}\":\n".format(module.name),
*_intersperse_newlines(module.tags.config)
)
# Parse the go_deps.config tag of the root module only.
for mod_config in module.tags.config:
if not module.is_root:
continue
check_direct_deps = mod_config.check_direct_dependencies
if check_direct_deps == "off":
outdated_direct_dep_printer = _noop
elif check_direct_deps == "warning":
outdated_direct_dep_printer = print
elif check_direct_deps == "error":
outdated_direct_dep_printer = fail
go_env = mod_config.go_env
debug_mode = mod_config.debug_mode
_process_overrides(module_ctx, module, "gazelle_override", gazelle_overrides, _process_gazelle_override)
_process_overrides(module_ctx, module, "module_override", module_overrides, _process_module_override, archive_overrides)
_process_overrides(module_ctx, module, "archive_override", archive_overrides, _process_archive_override, module_overrides)
if len(module.tags.from_file) > 1:
fail(
"Multiple \"go_deps.from_file\" tags defined in module \"{}\": {}".format(
module.name,
", ".join([str(tag.go_mod) for tag in module.tags.from_file]),
),
)
additional_module_tags = []
from_file_tags = []
module_name_to_go_dot_mod_label = {}
for from_file_tag in module.tags.from_file:
if bool(from_file_tag.go_work) == bool(from_file_tag.go_mod):
fail("go_deps.from_file tag must have either go_work or go_mod attribute, but not both.")
if from_file_tag.go_mod:
from_file_tags.append(from_file_tag)
elif from_file_tag.go_work:
go_work = go_work_from_label(module_ctx, from_file_tag.go_work)
# this ensures go.work replacements are considered
additional_module_tags += [
with_replaced_or_new_fields(tag, _is_dev_dependency = False)
for tag in go_work.module_tags
]
for entry, new_sum in sums_from_go_work(module_ctx, from_file_tag.go_work).items():
_safe_insert_sum(sums, entry, new_sum)
replace_map.update(go_work.replace_map)
from_file_tags = from_file_tags + go_work.from_file_tags
else:
fail("Either \"go_mod\" or \"go_work\" must be specified in \"go_deps.from_file\" tags.")
for from_file_tag in from_file_tags:
module_path, module_tags_from_go_mod, go_mod_replace_map, module_name = deps_from_go_mod(module_ctx, from_file_tag.go_mod)
module_name_to_go_dot_mod_label[module_name] = from_file_tag.go_mod
# Collect the relative path of the root module's go.mod file if it lives in the main
# repository.
if module.is_root and not from_file_tag.go_mod.repo_name:
go_mod = "go.mod"
if from_file_tag.go_mod.package:
go_mod = from_file_tag.go_mod.package + "/" + go_mod
dep_files.append(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)
for tag in module_tags_from_go_mod
]
if module.is_root or getattr(module_ctx, "is_isolated", False):
# for the replace_map, first in wins
for mod_path, mod in go_mod_replace_map.items():
if not mod_path in replace_map:
replace_map[mod_path] = mod
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.repo_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:
for entry, new_sum in sums_from_go_mod(module_ctx, from_file_tag.go_mod).items():
_safe_insert_sum(sums, entry, new_sum)
# Load sums from manually specified modules separately.
for module_tag in module.tags.module:
if module_tag.build_naming_convention:
fail("""The "build_naming_convention" attribute is no longer supported for "go_deps.module" tags. Use a "gazelle:go_naming_convention" directive via the "gazelle_override" tag's "directives" attribute instead.""")
if module_tag.build_file_proto_mode:
fail("""The "build_file_proto_mode" attribute is no longer supported for "go_deps.module" tags. Use a "gazelle:proto" directive via the "gazelle_override" tag's "directives" attribute instead.""")
sum_version = _canonicalize_raw_version(module_tag.version)
_safe_insert_sum(sums, (module_tag.path, sum_version), module_tag.sum)
# Parse the go_dep.module tags of all transitive dependencies and apply
# Minimum Version Selection to resolve importpaths to Go module versions
# and sums.
#
# Note: This applies Minimum Version Selection on the resolved
# dependency graphs of all transitive Bazel module dependencies, which
# is not what `go mod` does. But since this algorithm ends up using only
# Go module versions that have been explicitly declared somewhere in the
# full graph, we can assume that at that place all its required
# transitive dependencies have also been declared - we may end up
# resolving them to higher versions, but only compatible ones.
paths = {}
for module_tag in module.tags.module + additional_module_tags:
raw_version = _canonicalize_raw_version(module_tag.version)
# For modules imported from a go.sum, we know which ones are direct
# dependencies and can thus only report implicit version upgrades
# for direct dependencies. For manually specified go_deps.module
# tags, we always report version upgrades unless users override with
# the "indirect" attribute.
if module.is_root and not module_tag.indirect:
root_versions[module_tag.path] = raw_version
if _is_dev_dependency(module_ctx, module_tag):
root_module_direct_dev_deps[_repo_name(module_tag.path)] = None
else:
root_module_direct_deps[_repo_name(module_tag.path)] = None
version = semver.to_comparable(raw_version)
previous = paths.get(module_tag.path)
fail_on_version_conflict = any([x.fail_on_version_conflict for x in module.tags.from_file])
conflict_printer = fail if fail_on_version_conflict else print
check_for_version_conflict(version, previous, module_tag, module_name_to_go_dot_mod_label, conflict_printer)
paths[module_tag.path] = struct(version = version, module_tag = module_tag)
if module_tag.path not in module_resolutions or version > module_resolutions[module_tag.path].version:
to_path = None
local_path = None
if module_tag.path in replace_map:
replacement = replace_map[module_tag.path]
to_path = replacement.to_path
local_path = replacement.local_path
module_resolutions[module_tag.path] = struct(
repo_name = _repo_name(module_tag.path),
version = version,
raw_version = raw_version,
to_path = to_path,
local_path = local_path,
)
_fail_on_unmatched_overrides(archive_overrides.keys(), module_resolutions, "archive_overrides")
_fail_on_unmatched_overrides(gazelle_overrides.keys(), module_resolutions, "gazelle_overrides")
_fail_on_unmatched_overrides(module_overrides.keys(), module_resolutions, "module_overrides")
# All `replace` directives are applied after version resolution.
# We can simply do this by checking the replace paths' existence
# in the module resolutions and swapping out the entry.
for path, replace in replace_map.items():
if path in module_resolutions:
# If the replace directive specified a version then we only
# apply it if the versions match.
if replace.from_version:
comparable_from_version = semver.to_comparable(replace.from_version)
if module_resolutions[path].version != comparable_from_version:
continue
new_version = semver.to_comparable(replace.version)
module_resolutions[path] = with_replaced_or_new_fields(
module_resolutions[path],
replace = replace.to_path,
version = new_version,
raw_version = replace.version,
)
if path in root_versions:
if replace != replace.to_path:
# If the root module replaces a Go module with a completely different one, do
# not ever report an implicit version upgrade.
root_versions.pop(path)
else:
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 archive_overrides or path in gazelle_overrides or path in module_overrides or path in replace_map:
# TODO: Consider adding a warning here. Users should patch the bazel_dep instead.
continue
bazel_dep_is_older = path in module_resolutions and bazel_dep.version < module_resolutions[path].version
# Version mismatches between the Go module and the bazel_dep can confuse Go tooling. If the bazel_dep version
# is lower, it won't be used, which can result in unexpected builds and should thus always be reported, even for
# indirect deps. Explicitly overridden modules are not reported as this requires manual action.
if (path in module_resolutions and
bazel_dep.version != module_resolutions[path].version and
bazel_dep.version != _HIGHEST_VERSION_SENTINEL and
(bazel_dep_is_older or path in root_versions)):
bazel_dep_name = bazel_dep.module_name
bazel_dep_version = bazel_dep.raw_version
go_module_version = module_resolutions[path].raw_version
if bazel_dep_is_older:
remediation = [
"""
Either ensure that you have
bazel_dep(module_name = "{bazel_dep_name}", version = "{go_module_version}")
in your MODULE.bazel file or downgrade the Go module version via
bazel run""".format(
bazel_dep_name = bazel_dep_name,
go_module_version = go_module_version,
),
Label("@io_bazel_rules_go//go"),
"-- get {path}@v{bazel_dep_version}\n\n".format(
path = path,
bazel_dep_version = bazel_dep_version,
),
]
else:
remediation = [
"""
Update the Go module version via
bazel run""",
Label("@io_bazel_rules_go//go"),
"-- get {path}@v{bazel_dep_version}\n\n".format(
path = path,
bazel_dep_version = bazel_dep_version,
),
]
outdated_direct_dep_printer("""
Mismatch between versions requested for Go module {module}:
bazel_dep version (MODULE.bazel): {bazel_dep_version} (as "{bazel_dep_name}")
Go module version (go.mod): {go_module_version}
""".format(
module = path,
bazel_dep_name = bazel_dep_name,
bazel_dep_version = bazel_dep_version,
go_module_version = go_module_version,
), *remediation)
# Only use the Bazel module if it is at least as high as the required Go module version.
if bazel_dep_is_older:
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
recommended_updates = []
for path, root_version in root_versions.items():
resolved_version = module_resolutions[path].version
# Do not report version mismatches for overridden Bazel modules.
if resolved_version != _HIGHEST_VERSION_SENTINEL and semver.to_comparable(root_version) < resolved_version:
recommended_updates.append((path, root_version, module_resolutions[path].raw_version))
if recommended_updates:
outdated_direct_dep_printer(
"The following Go modules were required by the root module at the given versions, but were implicitly updated to higher versions due to transitive dependencies:\n",
*(
[
"\n {path}: v{root_version} -> v{resolved_version}".format(
path = path,
root_version = root_version,
resolved_version = resolved_version,
)
for path, root_version, resolved_version in recommended_updates
] + ["\n\nUpdate the root module's dependencies to match the resolved versions via:\n\n bazel run", Label("@io_bazel_rules_go//go"), "-- get " + " ".join([
"{path}@v{resolved_version}".format(path = path, resolved_version = resolved_version)
for path, _, resolved_version in recommended_updates
] + ["\n\n"])]
)
)
repos_processed = {}
for path, module in module_resolutions.items():
if hasattr(module, "module_name") or (getattr(module_ctx, "is_isolated", False) and path in _SHARED_REPOS):
# Do not create a go_repository for a Go module provided by a bazel_dep or one shared with the non-isolated
# instance of go_deps.
root_module_direct_deps.pop(_repo_name(path), None)
root_module_direct_dev_deps.pop(_repo_name(path), None)
continue
if module.repo_name in repos_processed:
fail("Go module {prev_path} and {path} will resolve to the same Bazel repo name: {name}. While Go allows modules to only differ in case, this isn't supported in Gazelle (yet). Please ensure you only use one of these modules in your go.mod(s)".format(
prev_path = repos_processed[module.repo_name],
path = path,
name = module.repo_name,
))
repos_processed[module.repo_name] = path
go_repository_args = {
"name": module.repo_name,
# Compared to the name attribute, the content of this attribute does not go through repo
# mapping.
"internal_only_do_not_use_apparent_name": module.repo_name,
"importpath": path,
"build_directives": _get_directives(path, gazelle_overrides, gazelle_default_attributes),
"build_file_generation": _get_build_file_generation(path, gazelle_overrides, gazelle_default_attributes),
"build_extra_args": _get_build_extra_args(path, gazelle_overrides, gazelle_default_attributes),
"patches": _get_patches(path, module_overrides),
"patch_args": _get_patch_args(path, module_overrides),
"debug_mode": debug_mode,
}
archive_override = archive_overrides.get(path)
if archive_override:
go_repository_args.update({
"urls": archive_override.urls,
"strip_prefix": archive_override.strip_prefix,
"sha256": archive_override.sha256,
"patches": _get_patches(path, archive_overrides),
"patch_args": _get_patch_args(path, archive_overrides),
})
elif module.local_path:
go_repository_args.update({
# the version is now meaningless
"version": None,
"local_path": module.local_path,
})
else:
repo_args = {
"replace": getattr(module, "replace", None),
"version": "v" + module.raw_version,
}
sum = _get_sum_from_module(path, module, sums)
if sum:
repo_args["sum"] = sum
go_repository_args.update(repo_args)
go_repository(**go_repository_args)
# Create a synthetic WORKSPACE file that lists all Go repositories created
# above and contains all the information required by Gazelle's -repo_config
# to generate BUILD files for external Go modules. This skips the need to
# run generate_repo_config. Only "importpath" and "build_naming_convention"
# are relevant.
_go_repository_config(
name = "bazel_gazelle_go_repository_config",
importpaths = {
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, gazelle_default_attributes),
"go_naming_convention",
)
for path, module in module_resolutions.items()
}),
go_env = go_env,
dep_files = dep_files,
)
return extension_metadata(
module_ctx,
root_module_direct_deps = root_module_direct_deps.keys(),
# If a Go module appears as both a dev and a non-dev dependency, it has to be imported as a
# non-dev dependency.
root_module_direct_dev_deps = {
repo_name: None
for repo_name in root_module_direct_dev_deps.keys()
if repo_name not in root_module_direct_deps
}.keys(),
reproducible = True,
)
def _get_sum_from_module(path, module, sums):
entry = (path, module.raw_version)
if hasattr(module, "replace"):
entry = (module.replace, module.raw_version)
if entry not in sums:
if module.raw_version == COMPARES_HIGHEST_SENTINEL:
# replacement have no sums, so we can skip this
return None
elif module.local_path == None:
# When updating a dependency, its sum may not be in go.sum and we can't hard fail here
# since we need Bazel to tidy the module
print("No sum for {}@{} found, run bazel run".format(path, module.raw_version), Label("@io_bazel_rules_go//go"), "-- mod tidy to generate it")
return None
return sums[entry]
def _safe_insert_sum(sums, entry, new_sum):
if entry in sums and new_sum != sums[entry]:
fail("Multiple mismatching sums for {}@{} found: {} vs {}".format(entry[0], entry[1], new_sum, sums[entry]))
sums[entry] = new_sum
def _canonicalize_raw_version(raw_version):
if raw_version.startswith("v"):
return raw_version[1:]
return raw_version
_config_tag = tag_class(
doc = """
Configures the general behavior of the go_deps extension.
Only the root module's config tag is used.
""",
attrs = {
"check_direct_dependencies": attr.string(
doc = """
The way in which warnings about version mismatches for direct dependencies and Go modules that are
also Bazel modules are reported.
""",
values = ["off", "warning", "error"],
),
"go_env": attr.string_dict(
doc = "The environment variables to use when fetching Go dependencies or running the `@rules_go//go` tool.",
),
"debug_mode": attr.bool(doc = "Whether or not to print stdout and stderr messages from gazelle", default = False),
},
)
_from_file_tag = tag_class(
doc = """
Imports Go module dependencies from either a go.mod file or a go.work file.
All direct and indirect dependencies of the specified module will be imported, but only direct dependencies should
be imported into the scope of the using module via `use_repo` calls. Use `bazel mod tidy` to update these calls
automatically.
""",
attrs = {
"go_mod": attr.label(mandatory = False),
"go_work": attr.label(mandatory = False),
"fail_on_version_conflict": attr.bool(
default = True,
doc = "Fail if duplicate modules have different versions",
),
},
)
_module_tag = tag_class(
doc = """Declare a single Go module dependency. Prefer using `from_file` instead.""",
attrs = {
"path": attr.string(
doc = """The module path.""",
mandatory = True,
),
"version": attr.string(mandatory = True),
"sum": attr.string(),
"indirect": attr.bool(
doc = """Whether this Go module is an indirect dependency.""",
default = False,
),
"build_naming_convention": attr.string(doc = """Removed, do not use""", default = ""),
"build_file_proto_mode": attr.string(doc = """Removed, do not use""", default = ""),
"local_path": attr.string(
doc = """For when a module is replaced by one residing in a local directory path """,
mandatory = False,
),
},
)
_archive_override_tag = tag_class(
attrs = {
"path": attr.string(
doc = """The Go module path for the repository to be overridden.
This module path must be defined by other tags in this
extension within this Bazel module.""",
mandatory = True,
),
"urls": attr.string_list(
doc = """A list of HTTP(S) URLs where an archive containing the project can be
downloaded. Bazel will attempt to download from the first URL; the others
are mirrors.""",
),
"strip_prefix": attr.string(
doc = """If the repository is downloaded via HTTP (`urls` is set), this is a
directory prefix to strip. See [`http_archive.strip_prefix`].""",
),
"sha256": attr.string(
doc = """If the repository is downloaded via HTTP (`urls` is set), this is the
SHA-256 sum of the downloaded archive. When set, Bazel will verify the archive
against this sum before extracting it.""",
),
"patches": attr.label_list(
doc = "A list of patches to apply to the repository *after* gazelle runs.",
),
"patch_strip": attr.int(
default = 0,
doc = "The number of leading path segments to be stripped from the file name in the patches.",
),
},
doc = "Override the default source location on a given Go module in this extension.",
)
_gazelle_override_tag = tag_class(
attrs = {
"path": attr.string(
doc = """The Go module path for the repository to be overridden.
This module path must be defined by other tags in this
extension within this Bazel module.""",
mandatory = True,
),
} | _GAZELLE_ATTRS,
doc = "Override Gazelle's behavior on a given Go module defined by other tags in this extension.",
)
_gazelle_default_attributes_tag = tag_class(
attrs = _GAZELLE_ATTRS,
doc = "Override Gazelle's default attribute values for all modules in this extension.",
)
_module_override_tag = tag_class(
attrs = {
"path": attr.string(
doc = """The Go module path for the repository to be overridden.
This module path must be defined by other tags in this
extension within this Bazel module.""",
mandatory = True,
),
"patches": attr.label_list(
doc = "A list of patches to apply to the repository *after* gazelle runs.",
),
"patch_strip": attr.int(
default = 0,
doc = "The number of leading path segments to be stripped from the file name in the patches.",
),
},
doc = "Apply patches to a given Go module defined by other tags in this extension.",
)
go_deps = module_extension(
_go_deps_impl,
tag_classes = {
"archive_override": _archive_override_tag,
"config": _config_tag,
"from_file": _from_file_tag,
"gazelle_override": _gazelle_override_tag,
"gazelle_default_attributes": _gazelle_default_attributes_tag,
"module": _module_tag,
"module_override": _module_override_tag,
},
)