| load("//internal:go_repository.bzl", "go_repository") |
| load(":go_mod.bzl", "deps_from_go_mod") |
| load(":semver.bzl", "semver") |
| |
| # 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", |
| ] |
| |
| 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()]) |
| |
| _GO_DEPS_HELPER_DEFS_BZL = """load("@bazel_gazelle//internal/bzlmod:go_helper.bzl", "go_helper") |
| |
| def go(import_path): |
| return go_helper(import_path, _GO_DEPS, lambda x: Label(x)) |
| |
| _GO_DEPS = {} |
| """ |
| |
| def _go_deps_helper_impl(ctx): |
| ctx.file("def.bzl", _GO_DEPS_HELPER_DEFS_BZL.format(ctx.attr.go_deps)) |
| ctx.file("WORKSPACE") |
| ctx.file("BUILD.bazel") |
| |
| _go_deps_helper = repository_rule( |
| implementation = _go_deps_helper_impl, |
| attrs = { |
| "go_deps": attr.string_dict(), |
| }, |
| ) |
| |
| def _go_repository_directives_impl(ctx): |
| directives = [ |
| "# gazelle:repository go_repository name={name} {directives}".format( |
| name = name, |
| directives = " ".join(attrs), |
| ) |
| for name, attrs in ctx.attr.directives.items() |
| ] |
| # Needed by the go_deps go macro to reliably transform an import path into a |
| # Bazel label. |
| directives.append("# gazelle:go_naming_convention_external import_alias") |
| ctx.file("WORKSPACE", "\n".join(directives)) |
| ctx.file("BUILD.bazel") |
| |
| _go_repository_directives = repository_rule( |
| implementation = _go_repository_directives_impl, |
| attrs = { |
| "directives": attr.string_list_dict(mandatory = True), |
| }, |
| ) |
| |
| def _noop(s): |
| pass |
| |
| def _go_deps_impl(module_ctx): |
| module_resolutions = {} |
| root_versions = {} |
| |
| outdated_direct_dep_printer = print |
| for module in module_ctx.modules: |
| # Parse the go_dep.config tag of the root module only. |
| for mod_config in module.tags.config: |
| # bazel_module.is_root is only available as of Bazel 5.3.0. |
| if not getattr(module, "is_root", False): |
| 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 |
| |
| additional_module_tags = [] |
| for from_file_tag in module.tags.from_file: |
| additional_module_tags += deps_from_go_mod(module_ctx, from_file_tag.go_mod) |
| |
| # 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: |
| 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: |
| continue |
| paths[module_tag.path] = None |
| raw_version = module_tag.version |
| if raw_version.startswith("v"): |
| raw_version = raw_version[1:] |
| |
| # 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. |
| if getattr(module, "is_root", False) and getattr(module_tag, "direct", True): |
| root_versions[module_tag.path] = raw_version |
| 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, |
| sum = module_tag.sum, |
| build_naming_convention = module_tag.build_naming_convention, |
| build_file_proto_mode = module_tag.build_file_proto_mode, |
| ) |
| |
| for path, root_version in root_versions.items(): |
| if semver.to_comparable(root_version) < module_resolutions[path].version: |
| outdated_direct_dep_printer( |
| "For Go module '{path}', the root module requires module version v{root_version}, but got v{resolved_version} in the resolved dependency graph.".format( |
| path = path, |
| root_version = root_version, |
| resolved_version = module_resolutions[path].raw_version, |
| ), |
| ) |
| |
| [ |
| go_repository( |
| name = module.repo_name, |
| importpath = path, |
| sum = module.sum, |
| version = "v" + module.raw_version, |
| build_naming_convention = module.build_naming_convention, |
| build_file_proto_mode = module.build_file_proto_mode, |
| ) |
| for path, module in module_resolutions.items() |
| ] |
| |
| # With transitive dependencies, Gazelle would no longer just have to pass a |
| # single top-level WORKSPACE/MODULE.bazel file, but those of all modules |
| # that use the go_dep tag. Instead, emit a synthetic WORKSPACE file with |
| # Gazelle directives for all of those modules here. |
| directives = { |
| module.repo_name: [ |
| "importpath=" + path, |
| "build_naming_convention=" + module.build_naming_convention, |
| "build_file_proto_mode=" + module_tag.build_file_proto_mode, |
| ] |
| for path, module in module_resolutions.items() |
| } |
| _go_repository_directives( |
| name = "bazel_gazelle_go_repository_directives", |
| directives = directives, |
| ) |
| |
| _go_deps_helper( |
| name = "go_deps", |
| go_deps = { |
| path: module.repo_name |
| for path, module in module_resolutions.items() |
| }, |
| ) |
| |
| _config_tag = tag_class( |
| attrs = { |
| "check_direct_dependencies": attr.string( |
| values = ["off", "warning", "error"], |
| ), |
| }, |
| ) |
| |
| _from_file_tag = tag_class( |
| attrs = { |
| "go_mod": attr.label(mandatory = True), |
| }, |
| ) |
| |
| _module_tag = tag_class( |
| attrs = { |
| "path": attr.string(mandatory = True), |
| "version": attr.string(mandatory = True), |
| "sum": attr.string(), |
| "build_naming_convention": attr.string( |
| default = "import_alias", |
| values = [ |
| "go_default_library", |
| "import", |
| "import_alias", |
| ], |
| ), |
| "build_file_proto_mode": attr.string( |
| default = "default", |
| values = [ |
| "default", |
| "disable", |
| "disable_global", |
| "legacy", |
| "package", |
| ], |
| ), |
| }, |
| ) |
| |
| go_deps = module_extension( |
| _go_deps_impl, |
| tag_classes = { |
| "config": _config_tag, |
| "from_file": _from_file_tag, |
| "module": _module_tag, |
| }, |
| ) |