| # 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, |
| }, |
| ) |