From 07a3b0841c5d1498835d1153d88197bd4dba748c Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Wed, 8 Jun 2022 11:14:47 +0200 Subject: [PATCH] Prototype of bzlmod-powered transitive dependencies Modules can use the `go_deps` module extension to define Go dependencies. Only Go module dependencies are supported for now (`go_deps.module`). HTTP and VCS Go repositories can be implemented for the root module only using the new bazel_module.is_root field added in https://github.com/bazelbuild/bazel/pull/15792 has been merged. They should follow the semantics of archive_override/git_override. --- .bazelci/presubmit.yml | 18 +- .bazelignore | 1 + .gitignore | 13 +- BUILD.bazel | 7 + MODULE.bazel | 244 +++++++++++++++++++++++++ extensions.bzl | 3 + internal/BUILD.bazel | 1 + internal/bzlmod/BUILD.bazel | 40 ++++ internal/bzlmod/go_deps.bzl | 144 +++++++++++++++ internal/bzlmod/non_module_deps.bzl | 33 ++++ internal/bzlmod/semver.bzl | 53 ++++++ internal/go_repository_tools_srcs.bzl | 1 + internal/list_repository_tools_srcs.go | 2 +- tests/bcr/.bazelrc | 1 + tests/bcr/.bazelversion | 1 + tests/bcr/BUILD.bazel | 14 ++ tests/bcr/MODULE.bazel | 39 ++++ tests/bcr/WORKSPACE | 0 tests/bcr/mvs_test.go | 19 ++ tests/bzlmod/BUILD.bazel | 3 + tests/bzlmod/semver_test.bzl | 61 +++++++ 21 files changed, 691 insertions(+), 7 deletions(-) create mode 100644 .bazelignore create mode 100644 MODULE.bazel create mode 100644 extensions.bzl create mode 100644 internal/bzlmod/BUILD.bazel create mode 100644 internal/bzlmod/go_deps.bzl create mode 100644 internal/bzlmod/non_module_deps.bzl create mode 100644 internal/bzlmod/semver.bzl create mode 100644 tests/bcr/.bazelrc create mode 100644 tests/bcr/.bazelversion create mode 100644 tests/bcr/BUILD.bazel create mode 100644 tests/bcr/MODULE.bazel create mode 100644 tests/bcr/WORKSPACE create mode 100644 tests/bcr/mvs_test.go create mode 100644 tests/bzlmod/BUILD.bazel create mode 100644 tests/bzlmod/semver_test.bzl diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index d5f8472a5..c2c082dab 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -1,6 +1,7 @@ --- -platforms: +tasks: ubuntu1804: + platform: ubuntu1804 build_targets: - "..." run_targets: @@ -9,17 +10,32 @@ platforms: - "//:gazelle_ci" test_targets: - "..." + ubuntu1804_bcr_tests: + name: BCR test module + platform: ubuntu1804 + # Includes https://github.com/bazelbuild/bazel/commit/4e439689db73a12b9c8a1f9f1a5f8023e09d40b0, + # which will eventually be released in Bazel 5.3.0. + bazel_version: 4e439689db73a12b9c8a1f9f1a5f8023e09d40b0 + working_directory: tests/bcr + build_targets: + - "//..." + - "//:gazelle" + test_targets: + - "//..." ubuntu1604: + platform: ubuntu1604 build_targets: - "..." test_targets: - "..." macos: + platform: macos build_targets: - "..." test_targets: - "..." windows: + platform: windows build_targets: - "--" - "..." diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 000000000..23758d5ac --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +tests/bcr diff --git a/.gitignore b/.gitignore index 5fd2c8a7e..d9854487f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ -bazel-bazel-gazelle -bazel-bin -bazel-genfiles -bazel-out -bazel-testlogs +/bazel-bazel-gazelle +/bazel-bin +/bazel-out +/bazel-testlogs +/tests/bcr/bazel-bcr +/tests/bcr/bazel-bin +/tests/bcr/bazel-out +/tests/bcr/bazel-testlogs diff --git a/BUILD.bazel b/BUILD.bazel index 8eec8b56c..72b0e518b 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -109,3 +109,10 @@ bzl_library( "@bazel_tools//tools/build_defs/repo:git.bzl", ], ) + +bzl_library( + name = "extensions", + srcs = ["extensions.bzl"], + visibility = ["//visibility:public"], + deps = ["//internal/bzlmod:go_deps"], +) diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 000000000..05e12c3d1 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,244 @@ +module( + name = "gazelle", + version = "0.26.0", +) + +print("WARNING: The bazel_gazelle Bazel module is still highly experimental and subject to change at any time. Only use it to try out bzlmod for now.") + +bazel_dep(name = "bazel_skylib", version = "1.2.0") +bazel_dep(name = "rules_go", version = "0.33.0", repo_name = "io_bazel_rules_go") + +go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk") + +# Known to exist since it is instantiated by rules_go itself. +use_repo(go_sdk, "go_default_sdk") + +non_module_deps = use_extension("//internal/bzlmod:non_module_deps.bzl", "non_module_deps") +use_repo( + non_module_deps, + "bazel_gazelle_go_repository_cache", + "bazel_gazelle_go_repository_config", + "bazel_gazelle_go_repository_tools", +) + +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.module( + importpath = "honnef.co/go/tools", + sum = "h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=", + version = "v0.0.0-20190523083050-ea95bdfd59fc", +) +go_deps.module( + importpath = "github.com/bazelbuild/buildtools", + sum = "h1:ox8T5nCkvLflRgKZ6uzzdvLDN3gbNZBgLBqA5Xgn7QA=", + version = "v0.0.0-20220323134444-a9f46b2bb3de", + build_naming_convention = "go_default_library", +) +go_deps.module( + importpath = "github.com/bazelbuild/rules_go", + sum = "h1:8/MDe6zCE7nYKXhlbamM1izUgJjw2ce7M5kd4QKF/3Y=", + version = "v0.31.0", +) +go_deps.module( + importpath = "github.com/bmatcuk/doublestar/v4", + sum = "h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA=", + version = "v4.0.2", +) +go_deps.module( + importpath = "github.com/BurntSushi/toml", + sum = "h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=", + version = "v0.3.1", +) +go_deps.module( + importpath = "github.com/census-instrumentation/opencensus-proto", + sum = "h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=", + version = "v0.2.1", +) +go_deps.module( + importpath = "github.com/chzyer/logex", + sum = "h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=", + version = "v1.1.10", +) +go_deps.module( + importpath = "github.com/chzyer/readline", + sum = "h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=", + version = "v0.0.0-20180603132655-2972be24d48e", +) +go_deps.module( + importpath = "github.com/chzyer/test", + sum = "h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=", + version = "v0.0.0-20180213035817-a1ea475d72b1", +) +go_deps.module( + importpath = "github.com/client9/misspell", + sum = "h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=", + version = "v0.3.4", +) +go_deps.module( + importpath = "github.com/davecgh/go-spew", + sum = "h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=", + version = "v1.1.1", +) +go_deps.module( + importpath = "github.com/envoyproxy/go-control-plane", + sum = "h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=", + version = "v0.9.1-0.20191026205805-5f8ba28d4473", +) +go_deps.module( + importpath = "github.com/envoyproxy/protoc-gen-validate", + sum = "h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=", + version = "v0.1.0", +) +go_deps.module( + importpath = "github.com/fsnotify/fsnotify", + sum = "h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=", + version = "v1.5.1", +) +go_deps.module( + importpath = "github.com/golang/glog", + sum = "h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=", + version = "v0.0.0-20160126235308-23def4e6c14b", +) +go_deps.module( + importpath = "github.com/golang/mock", + sum = "h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=", + version = "v1.1.1", +) +go_deps.module( + importpath = "github.com/golang/protobuf", + sum = "h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=", + version = "v1.4.3", +) +go_deps.module( + importpath = "github.com/google/go-cmp", + sum = "h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=", + version = "v0.5.7", +) +go_deps.module( + importpath = "github.com/pelletier/go-toml", + sum = "h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=", + version = "v1.9.4", +) +go_deps.module( + importpath = "github.com/pmezard/go-difflib", + sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=", + version = "v1.0.0", +) +go_deps.module( + importpath = "github.com/prometheus/client_model", + sum = "h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=", + version = "v0.0.0-20190812154241-14fe0d1b01d4", +) +go_deps.module( + importpath = "github.com/yuin/goldmark", + sum = "h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ=", + version = "v1.4.0", +) +go_deps.module( + importpath = "cloud.google.com/go", + sum = "h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=", + version = "v0.26.0", +) +go_deps.module( + importpath = "gopkg.in/check.v1", + sum = "h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=", + version = "v0.0.0-20161208181325-20d25e280405", +) +go_deps.module( + importpath = "gopkg.in/yaml.v2", + sum = "h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=", + version = "v2.2.2", +) +go_deps.module( + importpath = "go.starlark.net", + sum = "h1:xwwDQW5We85NaTk2APgoN9202w/l0DVGp+GZMfsrh7s=", + version = "v0.0.0-20210223155950-e043a3d3c984", +) +go_deps.module( + importpath = "google.golang.org/appengine", + sum = "h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=", + version = "v1.4.0", +) +go_deps.module( + importpath = "google.golang.org/genproto", + sum = "h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=", + version = "v0.0.0-20200526211855-cb27e3aa2013", +) +go_deps.module( + importpath = "google.golang.org/grpc", + sum = "h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=", + version = "v1.27.0", +) +go_deps.module( + importpath = "google.golang.org/protobuf", + sum = "h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=", + version = "v1.25.0", +) +go_deps.module( + importpath = "golang.org/x/crypto", + sum = "h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=", + version = "v0.0.0-20191011191535-87dc89f01550", +) +go_deps.module( + importpath = "golang.org/x/exp", + sum = "h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=", + version = "v0.0.0-20190121172915-509febef88a4", +) +go_deps.module( + importpath = "golang.org/x/lint", + sum = "h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=", + version = "v0.0.0-20190313153728-d0100b6bd8b3", +) +go_deps.module( + importpath = "golang.org/x/mod", + sum = "h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=", + version = "v0.6.0-dev.0.20220106191415-9b9b3d81d5e3", +) +go_deps.module( + importpath = "golang.org/x/net", + sum = "h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=", + version = "v0.0.0-20210805182204-aaa1db679c0d", +) +go_deps.module( + importpath = "golang.org/x/oauth2", + sum = "h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=", + version = "v0.0.0-20180821212333-d2e6202438be", +) +go_deps.module( + importpath = "golang.org/x/sync", + sum = "h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=", + version = "v0.0.0-20210220032951-036812b2e83c", +) +go_deps.module( + importpath = "golang.org/x/sys", + sum = "h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=", + version = "v0.0.0-20220319134239-a9b59b0215f8", +) +go_deps.module( + importpath = "golang.org/x/text", + sum = "h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=", + version = "v0.3.6", +) +go_deps.module( + importpath = "golang.org/x/tools", + sum = "h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=", + version = "v0.1.10", +) +go_deps.module( + importpath = "golang.org/x/xerrors", + sum = "h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=", + version = "v0.0.0-20200804184101-5ec99f83aff1", +) +use_repo( + go_deps, + "com_github_bazelbuild_buildtools", + "com_github_bmatcuk_doublestar_v4", + "com_github_fsnotify_fsnotify", + "com_github_google_go_cmp", + "com_github_pelletier_go_toml", + "com_github_pmezard_go_difflib", + "org_golang_x_mod", + "org_golang_x_sync", + "org_golang_x_tools", + # Used internally by the go_deps module extension. + "_bazel_gazelle_go_repository_directives", +) diff --git a/extensions.bzl b/extensions.bzl new file mode 100644 index 000000000..4ffd63d12 --- /dev/null +++ b/extensions.bzl @@ -0,0 +1,3 @@ +load("//internal/bzlmod:go_deps.bzl", _go_deps = "go_deps") + +go_deps = _go_deps diff --git a/internal/BUILD.bazel b/internal/BUILD.bazel index 9ed1675e2..4e687f4d6 100644 --- a/internal/BUILD.bazel +++ b/internal/BUILD.bazel @@ -50,6 +50,7 @@ filegroup( "overlay_repository.bzl", "repository_docs.bzl", "repository_rules_test_errors.patch", + "//internal/bzlmod:all_files", "//internal/gazellebinarytest:all_files", "//internal/generationtest:all_files", "//internal/language:all_files", diff --git a/internal/bzlmod/BUILD.bazel b/internal/bzlmod/BUILD.bazel new file mode 100644 index 000000000..487f6c038 --- /dev/null +++ b/internal/bzlmod/BUILD.bazel @@ -0,0 +1,40 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +filegroup( + name = "all_files", + testonly = True, + srcs = [ + "BUILD.bazel", + "go_deps.bzl", + "non_module_deps.bzl", + "semver.bzl", + ], + visibility = ["//visibility:public"], +) + +bzl_library( + name = "go_deps", + srcs = ["go_deps.bzl"], + visibility = ["//:__subpackages__"], + deps = [ + ":semver", + "//internal:go_repository", + ], +) + +bzl_library( + name = "non_module_deps", + srcs = ["non_module_deps.bzl"], + visibility = ["//:__subpackages__"], + deps = [ + "//internal:go_repository_cache", + "//internal:go_repository_config", + "//internal:go_repository_tools", + ], +) + +bzl_library( + name = "semver", + srcs = ["semver.bzl"], + visibility = ["//:__subpackages__"], +) diff --git a/internal/bzlmod/go_deps.bzl b/internal/bzlmod/go_deps.bzl new file mode 100644 index 000000000..f881c380a --- /dev/null +++ b/internal/bzlmod/go_deps.bzl @@ -0,0 +1,144 @@ +load("//internal:go_repository.bzl", "go_repository") +load(":semver.bzl", "semver") + +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 _go_repository_directives_impl(ctx): + directives = [ + "# gazelle:repository go_repository name={name} {directives}".format( + name = name, + directives = " ".join(directives), + ) + for name, directives in ctx.attr.directives.items() + ] + 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 + + # 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. + importpaths = {} + for module_tag in module.tags.module: + if module_tag.importpath in importpaths: + fail("Duplicate importpath '%s' in module '%s'" % (module_tag.importpath, module.name)) + importpaths[module_tag.importpath] = None + raw_version = module_tag.version + if raw_version.startswith("v"): + raw_version = raw_version[1:] + if getattr(module, "is_root", False): + root_versions[module_tag.importpath] = raw_version + version = semver.to_comparable(raw_version) + current_resolution = module_resolutions.get(module_tag.importpath, default = None) + if not current_resolution or version > current_resolution.version: + module_resolutions[module_tag.importpath] = struct( + module = module.name, + repo_name = _repo_name(module_tag.importpath), + version = version, + raw_version = raw_version, + sum = module_tag.sum, + build_naming_convention = module_tag.build_naming_convention, + ) + is_root_module = False + + for importpath, root_version in root_versions.items(): + if semver.to_comparable(root_version) < module_resolutions[importpath].version: + outdated_direct_dep_printer( + "For Go module '{importpath}', the root module requires module version v{root_version}, but got v{resolved_version} in the resolved dependency graph.".format( + importpath = importpath, + root_version = root_version, + resolved_version = module_resolutions[importpath].raw_version, + ) + ) + + [ + go_repository( + name = module.repo_name, + importpath = importpath, + sum = module.sum, + version = "v" + module.raw_version, + build_naming_convention = module.build_naming_convention, + ) + for importpath, 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=" + importpath, + "build_naming_convention=" + module.build_naming_convention, + ] + for importpath, module in module_resolutions.items() + } + _go_repository_directives( + name = "_bazel_gazelle_go_repository_directives", + directives = directives, + ) + +_config_tag = tag_class( + attrs = { + "check_direct_dependencies": attr.string( + values = ["off", "warning", "error"], + ), + }, +) + +_module_tag = tag_class( + attrs = { + "importpath": attr.string(mandatory = True), + "version": attr.string(mandatory = True), + "sum": attr.string(), + "build_naming_convention": attr.string(default = "import_alias"), + }, +) + +go_deps = module_extension( + _go_deps_impl, + tag_classes = { + "config": _config_tag, + "module": _module_tag, + }, +) diff --git a/internal/bzlmod/non_module_deps.bzl b/internal/bzlmod/non_module_deps.bzl new file mode 100644 index 000000000..808dc207a --- /dev/null +++ b/internal/bzlmod/non_module_deps.bzl @@ -0,0 +1,33 @@ +load( + "//internal:go_repository_cache.bzl", + "go_repository_cache", +) +load( + "//internal:go_repository_tools.bzl", + "go_repository_tools", +) +load( + "//internal:go_repository_config.bzl", + "go_repository_config", +) + +def _non_module_deps_impl(module_ctx): + go_repository_cache( + name = "bazel_gazelle_go_repository_cache", + # Always provided by rules_go. + go_sdk_name = "go_default_sdk", + go_env = {}, + ) + go_repository_tools( + name = "bazel_gazelle_go_repository_tools", + go_cache = Label("@bazel_gazelle_go_repository_cache//:go.env"), + ) + go_repository_config( + name = "bazel_gazelle_go_repository_config", + # Generated by the go_dep module extension. + config = Label("@_bazel_gazelle_go_repository_directives//:WORKSPACE"), + ) + +non_module_deps = module_extension( + _non_module_deps_impl, +) diff --git a/internal/bzlmod/semver.bzl b/internal/bzlmod/semver.bzl new file mode 100644 index 000000000..3ebc8e264 --- /dev/null +++ b/internal/bzlmod/semver.bzl @@ -0,0 +1,53 @@ +# Compares lower than any non-numeric identifier. +_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): + if not ident: + fail("Identifiers in semantic version strings must not be empty") + if ident.isdigit(): + if ident[0] == "0" and ident != "0": + fail("Numeric identifiers in semantic version strings must not include leading zeroes") + + # 11.4.1: + # "Identifiers consisting of only digits are compared numerically." + # 11.4.3: + # "Numeric identifiers always have lower precedence than non-numeric identifiers." + return (_COMPARES_LOWEST_SENTINEL, int(ident)) + else: + # 11.4.2: + # "Identifiers with letters or hyphens are compared lexically in ASCII sort order." + return (ident,) + +def _semver_to_comparable(v): + """ + Parses a string representation of a semver version into an opaque comparable object. + """ + + # Strip build metadata as it is not relevant for comparisons. + v, _, _ = v.partition("+") + + major_minor_patch_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(".")]) + 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,),) + + major_str, minor_str, patch_str = major_minor_patch_str.split(".") + return ( + int(major_str), + int(minor_str), + int(patch_str), + prerelease, + ) + +semver = struct( + to_comparable = _semver_to_comparable, +) diff --git a/internal/go_repository_tools_srcs.bzl b/internal/go_repository_tools_srcs.bzl index b68f07ecc..60a5f890f 100644 --- a/internal/go_repository_tools_srcs.bzl +++ b/internal/go_repository_tools_srcs.bzl @@ -30,6 +30,7 @@ GO_REPOSITORY_TOOLS_SRCS = [ Label("//flag:BUILD.bazel"), Label("//flag:flag.go"), Label("//internal:BUILD.bazel"), + Label("//internal/bzlmod:BUILD.bazel"), Label("//internal/gazellebinarytest:BUILD.bazel"), Label("//internal/gazellebinarytest:xlang.go"), Label("//internal/generationtest:BUILD.bazel"), diff --git a/internal/list_repository_tools_srcs.go b/internal/list_repository_tools_srcs.go index 3c6a74cd5..02052ea74 100644 --- a/internal/list_repository_tools_srcs.go +++ b/internal/list_repository_tools_srcs.go @@ -68,7 +68,7 @@ func main() { } base := filepath.Base(path) - if base == "docs" || base == "vendor" || base == "third_party" || base == "testdata" { + if base == "bcr_tests" || base == "docs" || base == "vendor" || base == "third_party" || base == "testdata" { return filepath.SkipDir } if !info.IsDir() && diff --git a/tests/bcr/.bazelrc b/tests/bcr/.bazelrc new file mode 100644 index 000000000..b3a24e860 --- /dev/null +++ b/tests/bcr/.bazelrc @@ -0,0 +1 @@ +common --experimental_enable_bzlmod diff --git a/tests/bcr/.bazelversion b/tests/bcr/.bazelversion new file mode 100644 index 000000000..7f72eb937 --- /dev/null +++ b/tests/bcr/.bazelversion @@ -0,0 +1 @@ +4e439689db73a12b9c8a1f9f1a5f8023e09d40b0 diff --git a/tests/bcr/BUILD.bazel b/tests/bcr/BUILD.bazel new file mode 100644 index 000000000..aca6a4b18 --- /dev/null +++ b/tests/bcr/BUILD.bazel @@ -0,0 +1,14 @@ +load("@rules_go//go:def.bzl", "go_test") +load("@gazelle//:def.bzl", "gazelle") + +# gazelle:prefix github.com/bazel-gazelle/tests/bcr +gazelle(name = "gazelle") + +go_test( + name = "mvs_test", + srcs = ["mvs_test.go"], + deps = [ + "@com_github_pelletier_go_toml//:go-toml", + "@com_github_stretchr_testify//require", + ] +) diff --git a/tests/bcr/MODULE.bazel b/tests/bcr/MODULE.bazel new file mode 100644 index 000000000..4cdcc1c6a --- /dev/null +++ b/tests/bcr/MODULE.bazel @@ -0,0 +1,39 @@ +module( + name = "gazelle_bcr_tests", +) + +bazel_dep(name = "gazelle", version = "") +local_path_override( + module_name = "gazelle", + path = "../..", +) + +bazel_dep(name = "rules_go", version = "0.33.0") + +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") + +# Using an older version than a transitive dependency (in this case, gazelle itself) emits a warning for the root +# module, but still fetches the most recent version due to Minimum Version Selection (MVS). +go_deps.module( + importpath = "github.com/pelletier/go-toml", + sum = "h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=", + version = "v1.7.0", +) +go_deps.module( + importpath = "github.com/stretchr/testify", + sum = "h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=", + version = "v1.8.0", +) +# Transitive dependencies have to be listed here explicitly. +go_deps.module( + importpath = "gopkg.in/yaml.v3", + sum = "h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=", + version = "v3.0.1", +) +use_repo( + go_deps, + "com_github_pelletier_go_toml", + "com_github_stretchr_testify", + # It is not necessary to list transitive dependencies here, only direct ones. + # "in_gopkg_yaml_v3", +) diff --git a/tests/bcr/WORKSPACE b/tests/bcr/WORKSPACE new file mode 100644 index 000000000..e69de29bb diff --git a/tests/bcr/mvs_test.go b/tests/bcr/mvs_test.go new file mode 100644 index 000000000..73d0367ff --- /dev/null +++ b/tests/bcr/mvs_test.go @@ -0,0 +1,19 @@ +package main + +import ( + "testing" + + toml "github.com/pelletier/go-toml" + "github.com/stretchr/testify/require" +) + +func TestMvs(t *testing.T) { + // Depend on functionality added in go-toml 1.8.1, which is a dependency of the 'gazelle' module, but strictly newer + // than the version that the root module depends on, to verify that MVS is used to resolve dependencies. + // Don't try this at home, it's not good style. + // https://github.com/pelletier/go-toml/commit/bcacc71a18be4b36b52baf6d10a95513f94dc7b2 + tree, err := toml.Load(`[modules] +deps = ["gazelle", "rules_go"]`) + require.NoError(t, err) + require.Equal(t, []string{"gazelle", "rules_go"}, tree.GetArray("modules.deps")) +} diff --git a/tests/bzlmod/BUILD.bazel b/tests/bzlmod/BUILD.bazel new file mode 100644 index 000000000..54e720b68 --- /dev/null +++ b/tests/bzlmod/BUILD.bazel @@ -0,0 +1,3 @@ +load(":semver_test.bzl", "semver_test_suite") + +semver_test_suite(name = "semver_test") diff --git a/tests/bzlmod/semver_test.bzl b/tests/bzlmod/semver_test.bzl new file mode 100644 index 000000000..f0c1ac55b --- /dev/null +++ b/tests/bzlmod/semver_test.bzl @@ -0,0 +1,61 @@ +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//internal/bzlmod:semver.bzl", "semver") + +_SORTED_TEST_VERSIONS = [ + "1.0.0-0.3.7", + "1.0.0-alpha", + "1.0.0-alpha+001", + "1.0.0-alpha.1", + "1.0.0-alpha.beta", + "1.0.0-beta+exp.sha.5114f85", + "1.0.0-beta", + "1.0.0-beta.2", + "1.0.0-beta.11", + "1.0.0-rc.1", + "1.0.0-x.7.z.92", + "1.0.0-x-y-z.–", + "1.0.0+21AF26D3—-117B344092BD", + "1.0.0+20130313144700", + "1.0.0", + "2.0.0", + "2.1.0", + "2.1.1", +] + +_SCRAMBLED_TEST_VERSIONS = [ + "2.1.1", + "2.1.0", + "2.0.0", + "1.0.0+21AF26D3—-117B344092BD", + "1.0.0+20130313144700", + "1.0.0", + "1.0.0-x.7.z.92", + "1.0.0-x-y-z.–", + "1.0.0-rc.1", + "1.0.0-beta.11", + "1.0.0-beta.2", + "1.0.0-beta+exp.sha.5114f85", + "1.0.0-beta", + "1.0.0-alpha.beta", + "1.0.0-alpha.1", + "1.0.0-alpha", + "1.0.0-alpha+001", + "1.0.0-0.3.7", +] + +def _semver_test_impl(ctx): + env = unittest.begin(ctx) + asserts.equals( + env, + _SORTED_TEST_VERSIONS, + sorted(_SCRAMBLED_TEST_VERSIONS, key = semver.to_comparable), + ) + return unittest.end(env) + +semver_test = unittest.make(_semver_test_impl) + +def semver_test_suite(name): + unittest.suite( + name, + semver_test, + )