feat(uv): parse the dist-manifest.json to not hardcode sha256 in rules_python (#2578)
Finalize the `uv` extension interface employing a builder pattern so
that the users can specify the exact version that needs to be
registered.
This also moves the registration of the actual toolchain to
`rules_python`
itself and ensures that an incompatible noop toolchain is registered if
nothing is configured. This ensures that the
`register_toolchains("@uv//:all")`
never fails.
If the `url/sha256` values are not specified, this is falling back to
using the `dist-manifest.json` on the GH releases page so that
we can get the expected `sha256` value of each available file and
download all of the usable archives. This means that `rules_python` no
longer needs to be updated for `uv` version bumps.
The remaining bits for closing the ticket:
- [ ] Finalize the `lock` interface.
- [ ] Add the locking target to the `pip.parse` hub repo if
`pyproject.toml`
is passed in.
- [ ] Add a rule/target for `venv` creation.
Work towards #1975.diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7ae4bf..413442e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -80,6 +80,11 @@
* {obj}`//python/bin:python`: convenience target for directly running an
interpreter. {obj}`--//python/bin:python_src` can be used to specify a
binary whose interpreter to use.
+* (uv) Now the extension can be fully configured via `bzlmod` APIs without the
+ need to patch `rules_python`. The documentation has been added to `rules_python`
+ docs but usage of the extension may result in your setup breaking without any
+ notice. What is more, the URLs and SHA256 values will be retrieved from the
+ GitHub releases page metadata published by the `uv` project.
* (pypi) An extra argument to add the interpreter lib dir to `LDFLAGS` when
building wheels from `sdist`.
diff --git a/MODULE.bazel b/MODULE.bazel
index 3d7c304..dc2193c 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -174,16 +174,83 @@
"build_bazel_bazel_self",
)
-# EXPERIMENTAL: This is experimental and may be removed without notice
-uv = use_extension(
+# TODO @aignas 2025-01-27: should this be moved to `//python/extensions:uv.bzl` or should
+# it stay as it is? I think I may prefer to move it.
+uv = use_extension("//python/uv:uv.bzl", "uv")
+
+# Here is how we can define platforms for the `uv` binaries - this will affect
+# all of the downstream callers because we are using the extension without
+# `dev_dependency = True`.
+uv.default(
+ base_url = "https://github.com/astral-sh/uv/releases/download",
+ manifest_filename = "dist-manifest.json",
+ version = "0.6.3",
+)
+uv.default(
+ compatible_with = [
+ "@platforms//os:macos",
+ "@platforms//cpu:aarch64",
+ ],
+ platform = "aarch64-apple-darwin",
+)
+uv.default(
+ compatible_with = [
+ "@platforms//os:linux",
+ "@platforms//cpu:aarch64",
+ ],
+ platform = "aarch64-unknown-linux-gnu",
+)
+uv.default(
+ compatible_with = [
+ "@platforms//os:linux",
+ "@platforms//cpu:ppc",
+ ],
+ platform = "powerpc64-unknown-linux-gnu",
+)
+uv.default(
+ compatible_with = [
+ "@platforms//os:linux",
+ "@platforms//cpu:ppc64le",
+ ],
+ platform = "powerpc64le-unknown-linux-gnu",
+)
+uv.default(
+ compatible_with = [
+ "@platforms//os:linux",
+ "@platforms//cpu:s390x",
+ ],
+ platform = "s390x-unknown-linux-gnu",
+)
+uv.default(
+ compatible_with = [
+ "@platforms//os:macos",
+ "@platforms//cpu:x86_64",
+ ],
+ platform = "x86_64-apple-darwin",
+)
+uv.default(
+ compatible_with = [
+ "@platforms//os:windows",
+ "@platforms//cpu:x86_64",
+ ],
+ platform = "x86_64-pc-windows-msvc",
+)
+uv.default(
+ compatible_with = [
+ "@platforms//os:linux",
+ "@platforms//cpu:x86_64",
+ ],
+ platform = "x86_64-unknown-linux-gnu",
+)
+use_repo(uv, "uv")
+
+register_toolchains("@uv//:all")
+
+uv_dev = use_extension(
"//python/uv:uv.bzl",
"uv",
dev_dependency = True,
)
-uv.toolchain(uv_version = "0.4.25")
-use_repo(uv, "uv_toolchains")
-
-register_toolchains(
- "@uv_toolchains//:all",
- dev_dependency = True,
+uv_dev.configure(
+ version = "0.6.2",
)
diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel
index eaed078..69e384e 100644
--- a/examples/bzlmod/MODULE.bazel
+++ b/examples/bzlmod/MODULE.bazel
@@ -101,12 +101,15 @@
# rules based on the `python_version` arg values.
use_repo(python, "python_3_10", "python_3_9", "python_versions", "pythons_hub")
-# EXPERIMENTAL: This is experimental and may be removed without notice
-uv = use_extension("@rules_python//python/uv:uv.bzl", "uv")
-uv.toolchain(uv_version = "0.4.25")
-use_repo(uv, "uv_toolchains")
-
-register_toolchains("@uv_toolchains//:all")
+# EXPERIMENTAL: This is experimental and may be changed or removed without notice
+uv = use_extension(
+ "@rules_python//python/uv:uv.bzl",
+ "uv",
+ # Use `dev_dependency` so that the toolchains are not defined pulled when your
+ # module is used elsewhere.
+ dev_dependency = True,
+)
+uv.configure(version = "0.6.2")
# This extension allows a user to create modifications to how rules_python
# creates different wheel repositories. Different attributes allow the user
diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel
index 006c856..acf2a9c 100644
--- a/python/uv/private/BUILD.bazel
+++ b/python/uv/private/BUILD.bazel
@@ -47,18 +47,17 @@
name = "uv_bzl",
srcs = ["uv.bzl"],
visibility = ["//python/uv:__subpackages__"],
- deps = [":uv_repositories_bzl"],
+ deps = [
+ ":toolchain_types_bzl",
+ ":uv_repository_bzl",
+ ":uv_toolchains_repo_bzl",
+ ],
)
bzl_library(
- name = "uv_repositories_bzl",
- srcs = ["uv_repositories.bzl"],
+ name = "uv_repository_bzl",
+ srcs = ["uv_repository.bzl"],
visibility = ["//python/uv:__subpackages__"],
- deps = [
- ":toolchain_types_bzl",
- ":uv_toolchains_repo_bzl",
- ":versions_bzl",
- ],
)
bzl_library(
@@ -82,9 +81,3 @@
"//python/private:text_util_bzl",
],
)
-
-bzl_library(
- name = "versions_bzl",
- srcs = ["versions.bzl"],
- visibility = ["//python/uv:__subpackages__"],
-)
diff --git a/python/uv/private/lock.bzl b/python/uv/private/lock.bzl
index e0491b2..9378f18 100644
--- a/python/uv/private/lock.bzl
+++ b/python/uv/private/lock.bzl
@@ -30,9 +30,11 @@
"""Pin the requirements based on the src files.
Differences with the current {obj}`compile_pip_requirements` rule:
- - This is implemented in shell and uv.
+ - This is implemented in shell and `uv`.
- This does not error out if the output file does not exist yet.
- Supports transitions out of the box.
+ - The execution of the lock file generation is happening inside of a build
+ action in a `genrule`.
Args:
name: The name of the target to run for updating the requirements.
@@ -41,8 +43,8 @@
upgrade: Tell `uv` to always upgrade the dependencies instead of
keeping them as they are.
universal: Tell `uv` to generate a universal lock file.
- args: Extra args to pass to `uv`.
- **kwargs: Extra kwargs passed to the {obj}`py_binary` rule.
+ args: Extra args to pass to the rule.
+ **kwargs: Extra kwargs passed to the binary rule.
"""
pkg = native.package_name()
update_target = name + ".update"
diff --git a/python/uv/private/toolchains_hub.bzl b/python/uv/private/toolchains_hub.bzl
new file mode 100644
index 0000000..b39d84f
--- /dev/null
+++ b/python/uv/private/toolchains_hub.bzl
@@ -0,0 +1,65 @@
+# Copyright 2025 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.
+
+"""A macro used from the uv_toolchain hub repo."""
+
+load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE")
+
+def toolchains_hub(
+ *,
+ name,
+ toolchains,
+ implementations,
+ target_compatible_with,
+ target_settings):
+ """Define the toolchains so that the lexicographical order registration is deterministic.
+
+ TODO @aignas 2025-03-09: see if this can be reused in the python toolchains.
+
+ Args:
+ name: The prefix to all of the targets, which goes after a numeric prefix.
+ toolchains: The toolchain names for the targets defined by this macro.
+ The earlier occurring items take precedence over the later items if
+ they match the target platform and target settings.
+ implementations: The name to label mapping.
+ target_compatible_with: The name to target_compatible_with list mapping.
+ target_settings: The name to target_settings list mapping.
+ """
+ if len(toolchains) != len(implementations):
+ fail("Each name must have an implementation")
+
+ # We are defining the toolchains so that the order of toolchain matching is
+ # the same as the order of the toolchains, because:
+ # * the toolchains are matched by target settings and target_compatible_with
+ # * the first toolchain satisfying the above wins
+ #
+ # this means we need to register the toolchains prefixed with a number of
+ # format 00xy, where x and y are some digits and the leading zeros to
+ # ensure lexicographical sorting.
+ #
+ # Add 1 so that there is always a leading zero
+ prefix_len = len(str(len(toolchains))) + 1
+ prefix = "0" * (prefix_len - 1)
+
+ for i, toolchain in enumerate(toolchains):
+ # prefix with a prefix and then truncate the string.
+ number_prefix = "{}{}".format(prefix, i)[-prefix_len:]
+
+ native.toolchain(
+ name = "{}_{}_{}".format(number_prefix, name, toolchain),
+ target_compatible_with = target_compatible_with.get(toolchain, []),
+ target_settings = target_settings.get(toolchain, []),
+ toolchain = implementations[toolchain],
+ toolchain_type = UV_TOOLCHAIN_TYPE,
+ )
diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl
index 886e7fe..55a05be 100644
--- a/python/uv/private/uv.bzl
+++ b/python/uv/private/uv.bzl
@@ -18,36 +18,480 @@
A module extension for working with uv.
"""
-load(":uv_repositories.bzl", "uv_repositories")
+load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE")
+load(":uv_repository.bzl", "uv_repository")
+load(":uv_toolchains_repo.bzl", "uv_toolchains_repo")
_DOC = """\
A module extension for working with uv.
+
+Basic usage:
+```starlark
+uv = use_extension(
+ "@rules_python//python/uv:uv.bzl",
+ "uv",
+ # Use `dev_dependency` so that the toolchains are not defined pulled when
+ # your module is used elsewhere.
+ dev_dependency = True,
+)
+uv.configure(version = "0.5.24")
+```
+
+Since this is only for locking the requirements files, it should be always
+marked as a `dev_dependency`.
"""
-uv_toolchain = tag_class(
- doc = "Configure uv toolchain for lock file generation.",
- attrs = {
- "uv_version": attr.string(doc = "Explicit version of uv.", mandatory = True),
+_DEFAULT_ATTRS = {
+ "base_url": attr.string(
+ doc = """\
+Base URL to download metadata about the binaries and the binaries themselves.
+""",
+ ),
+ "compatible_with": attr.label_list(
+ doc = """\
+The compatible with constraint values for toolchain resolution.
+""",
+ ),
+ "manifest_filename": attr.string(
+ doc = """\
+The distribution manifest filename to use for the metadata fetching from GH. The
+defaults for this are set in `rules_python` MODULE.bazel file that one can override
+for a specific version.
+""",
+ default = "dist-manifest.json",
+ ),
+ "platform": attr.string(
+ doc = """\
+The platform string used in the UV repository to denote the platform triple.
+""",
+ ),
+ "target_settings": attr.label_list(
+ doc = """\
+The `target_settings` to add to platform definitions that then get used in `toolchain`
+definitions.
+""",
+ ),
+ "version": attr.string(
+ doc = """\
+The version of uv to configure the sources for. If this is not specified it will be the
+last version used in the module or the default version set by `rules_python`.
+""",
+ ),
+}
+
+default = tag_class(
+ doc = """\
+Set the uv configuration defaults.
+""",
+ attrs = _DEFAULT_ATTRS,
+)
+
+configure = tag_class(
+ doc = """\
+Build the `uv` toolchain configuration by appending the provided configuration.
+The information is appended to the version configuration that is specified by
+{attr}`version` attribute, or if the version is unspecified, the version of the
+last {obj}`uv.configure` call in the current module, or the version from the
+defaults is used.
+
+Complex configuration example:
+```starlark
+# Configure the base_url for the default version.
+uv.configure(base_url = "my_mirror")
+
+# Add an extra platform that can be used with your version.
+uv.configure(
+ platform = "extra-platform",
+ target_settings = ["//my_config_setting_label"],
+ compatible_with = ["@platforms//os:exotic"],
+)
+
+# Add an extra platform that can be used with your version.
+uv.configure(
+ platform = "patched-binary",
+ target_settings = ["//my_super_config_setting"],
+ urls = ["https://example.zip"],
+ sha256 = "deadbeef",
+)
+```
+""",
+ attrs = _DEFAULT_ATTRS | {
+ "sha256": attr.string(
+ doc = "The sha256 of the downloaded artifact if the {attr}`urls` is specified.",
+ ),
+ "urls": attr.string_list(
+ doc = """\
+The urls to download the binary from. If this is used, {attr}`base_url` and
+{attr}`manifest_name` are ignored for the given version.
+
+::::note
+If the `urls` are specified, they need to be specified for all of the platforms
+for a particular version.
+::::
+""",
+ ),
},
)
-def _uv_toolchain_extension(module_ctx):
- for mod in module_ctx.modules:
- for toolchain in mod.tags.toolchain:
- if not mod.is_root:
- fail(
- "Only the root module may configure the uv toolchain.",
- "This prevents conflicting registrations with any other modules.",
- "NOTE: We may wish to enforce a policy where toolchain configuration is only allowed in the root module, or in rules_python. See https://github.com/bazelbuild/bazel/discussions/22024",
- )
+def _configure(config, *, platform, compatible_with, target_settings, urls = [], sha256 = "", override = False, **values):
+ """Set the value in the config if the value is provided"""
+ for key, value in values.items():
+ if not value:
+ continue
- uv_repositories(
- uv_version = toolchain.uv_version,
- register_toolchains = False,
+ if not override and config.get(key):
+ continue
+
+ config[key] = value
+
+ config.setdefault("platforms", {})
+ if not platform:
+ if compatible_with or target_settings or urls:
+ fail("`platform` name must be specified when specifying `compatible_with`, `target_settings` or `urls`")
+ elif compatible_with or target_settings:
+ if not override and config.get("platforms", {}).get(platform):
+ return
+
+ config["platforms"][platform] = struct(
+ name = platform.replace("-", "_").lower(),
+ compatible_with = compatible_with,
+ target_settings = target_settings,
+ )
+ elif urls:
+ if not override and config.get("urls", {}).get(platform):
+ return
+
+ config.setdefault("urls", {})[platform] = struct(
+ sha256 = sha256,
+ urls = urls,
+ )
+ else:
+ config["platforms"].pop(platform)
+
+def process_modules(
+ module_ctx,
+ hub_name = "uv",
+ uv_repository = uv_repository,
+ toolchain_type = str(UV_TOOLCHAIN_TYPE),
+ hub_repo = uv_toolchains_repo):
+ """Parse the modules to get the config for 'uv' toolchains.
+
+ Args:
+ module_ctx: the context.
+ hub_name: the name of the hub repository.
+ uv_repository: the rule to create a uv_repository override.
+ toolchain_type: the toolchain type to use here.
+ hub_repo: the hub repo factory function to use.
+
+ Returns:
+ the result of the hub_repo. Mainly used for tests.
+ """
+
+ # default values to apply for version specific config
+ defaults = {
+ "base_url": "",
+ "manifest_filename": "",
+ "platforms": {
+ # The structure is as follows:
+ # "platform_name": struct(
+ # compatible_with = [],
+ # target_settings = [],
+ # ),
+ #
+ # NOTE: urls and sha256 cannot be set in defaults
+ },
+ "version": "",
+ }
+ for mod in module_ctx.modules:
+ if not (mod.is_root or mod.name == "rules_python"):
+ continue
+
+ for tag in mod.tags.default:
+ _configure(
+ defaults,
+ version = tag.version,
+ base_url = tag.base_url,
+ manifest_filename = tag.manifest_filename,
+ platform = tag.platform,
+ compatible_with = tag.compatible_with,
+ target_settings = tag.target_settings,
+ override = mod.is_root,
)
+ for key in [
+ "version",
+ "manifest_filename",
+ "platforms",
+ ]:
+ if not defaults.get(key, None):
+ fail("defaults need to be set for '{}'".format(key))
+
+ # resolved per-version configuration. The shape is something like:
+ # versions = {
+ # "1.0.0": {
+ # "base_url": "",
+ # "manifest_filename": "",
+ # "platforms": {
+ # "platform_name": struct(
+ # compatible_with = [],
+ # target_settings = [],
+ # urls = [], # can be unset
+ # sha256 = "", # can be unset
+ # ),
+ # },
+ # },
+ # }
+ versions = {}
+ for mod in module_ctx.modules:
+ if not (mod.is_root or mod.name == "rules_python"):
+ continue
+
+ # last_version is the last version used in the MODULE.bazel or the default
+ last_version = None
+ for tag in mod.tags.configure:
+ last_version = tag.version or last_version or defaults["version"]
+ specific_config = versions.setdefault(
+ last_version,
+ {
+ "base_url": defaults["base_url"],
+ "manifest_filename": defaults["manifest_filename"],
+ # shallow copy is enough as the values are structs and will
+ # be replaced on modification
+ "platforms": dict(defaults["platforms"]),
+ },
+ )
+
+ _configure(
+ specific_config,
+ base_url = tag.base_url,
+ manifest_filename = tag.manifest_filename,
+ platform = tag.platform,
+ compatible_with = tag.compatible_with,
+ target_settings = tag.target_settings,
+ sha256 = tag.sha256,
+ urls = tag.urls,
+ override = mod.is_root,
+ )
+
+ if not versions:
+ return hub_repo(
+ name = hub_name,
+ toolchain_type = toolchain_type,
+ toolchain_names = ["none"],
+ toolchain_implementations = {
+ # NOTE @aignas 2025-02-24: the label to the toolchain can be anything
+ "none": str(Label("//python:none")),
+ },
+ toolchain_compatible_with = {
+ "none": ["@platforms//:incompatible"],
+ },
+ toolchain_target_settings = {},
+ )
+
+ toolchain_names = []
+ toolchain_implementations = {}
+ toolchain_compatible_with_by_toolchain = {}
+ toolchain_target_settings = {}
+ for version, config in versions.items():
+ platforms = config["platforms"]
+
+ # Use the manually specified urls
+ urls = {
+ platform: src
+ for platform, src in config.get("urls", {}).items()
+ if src.urls
+ }
+
+ # Or fallback to fetching them from GH manifest file
+ # Example file: https://github.com/astral-sh/uv/releases/download/0.6.3/dist-manifest.json
+ if not urls:
+ urls = _get_tool_urls_from_dist_manifest(
+ module_ctx,
+ base_url = "{base_url}/{version}".format(
+ version = version,
+ base_url = config["base_url"],
+ ),
+ manifest_filename = config["manifest_filename"],
+ platforms = sorted(platforms),
+ )
+
+ for platform_name, platform in platforms.items():
+ if platform_name not in urls:
+ continue
+
+ toolchain_name = "{}_{}".format(version.replace(".", "_"), platform_name.lower().replace("-", "_"))
+ uv_repository_name = "{}_{}".format(hub_name, toolchain_name)
+ uv_repository(
+ name = uv_repository_name,
+ version = version,
+ platform = platform_name,
+ urls = urls[platform_name].urls,
+ sha256 = urls[platform_name].sha256,
+ )
+
+ toolchain_names.append(toolchain_name)
+ toolchain_implementations[toolchain_name] = "@{}//:uv_toolchain".format(uv_repository_name)
+ toolchain_compatible_with_by_toolchain[toolchain_name] = [
+ str(label)
+ for label in platform.compatible_with
+ ]
+ if platform.target_settings:
+ toolchain_target_settings[toolchain_name] = [
+ str(label)
+ for label in platform.target_settings
+ ]
+
+ return hub_repo(
+ name = hub_name,
+ toolchain_type = toolchain_type,
+ toolchain_names = toolchain_names,
+ toolchain_implementations = toolchain_implementations,
+ toolchain_compatible_with = toolchain_compatible_with_by_toolchain,
+ toolchain_target_settings = toolchain_target_settings,
+ )
+
+def _uv_toolchain_extension(module_ctx):
+ process_modules(
+ module_ctx,
+ hub_name = "uv",
+ )
+
+def _overlap(first_collection, second_collection):
+ for x in first_collection:
+ if x in second_collection:
+ return True
+
+ return False
+
+def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename, platforms):
+ """Download the results about remote tool sources.
+
+ This relies on the tools using the cargo packaging to infer the actual
+ sha256 values for each binary.
+
+ Example manifest url: https://github.com/astral-sh/uv/releases/download/0.6.5/dist-manifest.json
+
+ The example format is as below
+
+ dist_version "0.28.0"
+ announcement_tag "0.6.5"
+ announcement_tag_is_implicit false
+ announcement_is_prerelease false
+ announcement_title "0.6.5"
+ announcement_changelog "text"
+ announcement_github_body "MD text"
+ releases [
+ {
+ app_name "uv"
+ app_version "0.6.5"
+ env
+ install_dir_env_var "UV_INSTALL_DIR"
+ unmanaged_dir_env_var "UV_UNMANAGED_INSTALL"
+ disable_update_env_var "UV_DISABLE_UPDATE"
+ no_modify_path_env_var "UV_NO_MODIFY_PATH"
+ github_base_url_env_var "UV_INSTALLER_GITHUB_BASE_URL"
+ ghe_base_url_env_var "UV_INSTALLER_GHE_BASE_URL"
+ display_name "uv"
+ display true
+ artifacts [
+ "source.tar.gz"
+ "source.tar.gz.sha256"
+ "uv-installer.sh"
+ "uv-installer.ps1"
+ "sha256.sum"
+ "uv-aarch64-apple-darwin.tar.gz"
+ "uv-aarch64-apple-darwin.tar.gz.sha256"
+ "...
+ ]
+ artifacts
+ uv-aarch64-apple-darwin.tar.gz
+ name "uv-aarch64-apple-darwin.tar.gz"
+ kind "executable-zip"
+ target_triples [
+ "aarch64-apple-darwin"
+ assets [
+ {
+ id "uv-aarch64-apple-darwin-exe-uv"
+ name "uv"
+ path "uv"
+ kind "executable"
+ },
+ {
+ id "uv-aarch64-apple-darwin-exe-uvx"
+ name "uvx"
+ path "uvx"
+ kind "executable"
+ }
+ ]
+ checksum "uv-aarch64-apple-darwin.tar.gz.sha256"
+ uv-aarch64-apple-darwin.tar.gz.sha256
+ name "uv-aarch64-apple-darwin.tar.gz.sha256"
+ kind "checksum"
+ target_triples [
+ "aarch64-apple-darwin"
+ ]
+ """
+ dist_manifest = module_ctx.path(manifest_filename)
+ result = module_ctx.download(
+ base_url + "/" + manifest_filename,
+ output = dist_manifest,
+ )
+ if not result.success:
+ fail(result)
+ dist_manifest = json.decode(module_ctx.read(dist_manifest))
+
+ artifacts = dist_manifest["artifacts"]
+ tool_sources = {}
+ downloads = {}
+ for fname, artifact in artifacts.items():
+ if artifact.get("kind") != "executable-zip":
+ continue
+
+ checksum = artifacts[artifact["checksum"]]
+ if not _overlap(checksum["target_triples"], platforms):
+ # we are not interested in this platform, so skip
+ continue
+
+ checksum_fname = checksum["name"]
+ checksum_path = module_ctx.path(checksum_fname)
+ downloads[checksum_path] = struct(
+ download = module_ctx.download(
+ "{}/{}".format(base_url, checksum_fname),
+ output = checksum_path,
+ block = False,
+ ),
+ archive_fname = fname,
+ platforms = checksum["target_triples"],
+ )
+
+ for checksum_path, download in downloads.items():
+ result = download.download.wait()
+ if not result.success:
+ fail(result)
+
+ archive_fname = download.archive_fname
+
+ sha256, _, checksummed_fname = module_ctx.read(checksum_path).partition(" ")
+ checksummed_fname = checksummed_fname.strip(" *\n")
+ if archive_fname != checksummed_fname:
+ fail("The checksum is for a different file, expected '{}' but got '{}'".format(
+ archive_fname,
+ checksummed_fname,
+ ))
+
+ for platform in download.platforms:
+ tool_sources[platform] = struct(
+ urls = ["{}/{}".format(base_url, archive_fname)],
+ sha256 = sha256,
+ )
+
+ return tool_sources
+
uv = module_extension(
doc = _DOC,
implementation = _uv_toolchain_extension,
- tag_classes = {"toolchain": uv_toolchain},
+ tag_classes = {
+ "configure": configure,
+ "default": default,
+ },
)
diff --git a/python/uv/private/uv_repositories.bzl b/python/uv/private/uv_repositories.bzl
deleted file mode 100644
index 24fb9c2..0000000
--- a/python/uv/private/uv_repositories.bzl
+++ /dev/null
@@ -1,120 +0,0 @@
-# Copyright 2024 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.
-
-"""
-EXPERIMENTAL: This is experimental and may be removed without notice
-
-Create repositories for uv toolchain dependencies
-"""
-
-load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE")
-load(":uv_toolchains_repo.bzl", "uv_toolchains_repo")
-load(":versions.bzl", "UV_PLATFORMS", "UV_TOOL_VERSIONS")
-
-UV_BUILD_TMPL = """\
-# Generated by repositories.bzl
-load("@rules_python//python/uv:uv_toolchain.bzl", "uv_toolchain")
-
-uv_toolchain(
- name = "uv_toolchain",
- uv = "{binary}",
- version = "{version}",
-)
-"""
-
-def _uv_repo_impl(repository_ctx):
- platform = repository_ctx.attr.platform
- uv_version = repository_ctx.attr.uv_version
-
- is_windows = "windows" in platform
-
- suffix = ".zip" if is_windows else ".tar.gz"
- filename = "uv-{platform}{suffix}".format(
- platform = platform,
- suffix = suffix,
- )
- url = "https://github.com/astral-sh/uv/releases/download/{version}/{filename}".format(
- version = uv_version,
- filename = filename,
- )
- if filename.endswith(".tar.gz"):
- strip_prefix = filename[:-len(".tar.gz")]
- else:
- strip_prefix = ""
-
- repository_ctx.download_and_extract(
- url = url,
- sha256 = UV_TOOL_VERSIONS[repository_ctx.attr.uv_version][repository_ctx.attr.platform].sha256,
- stripPrefix = strip_prefix,
- )
-
- binary = "uv.exe" if is_windows else "uv"
- repository_ctx.file(
- "BUILD.bazel",
- UV_BUILD_TMPL.format(
- binary = binary,
- version = uv_version,
- ),
- )
-
-uv_repository = repository_rule(
- _uv_repo_impl,
- doc = "Fetch external tools needed for uv toolchain",
- attrs = {
- "platform": attr.string(mandatory = True, values = UV_PLATFORMS.keys()),
- "uv_version": attr.string(mandatory = True, values = UV_TOOL_VERSIONS.keys()),
- },
-)
-
-def uv_repositories(name = "uv_toolchains", uv_version = None, register_toolchains = True):
- """Convenience macro which does typical toolchain setup
-
- Skip this macro if you need more control over the toolchain setup.
-
- Args:
- name: {type}`str` The name of the toolchains repo.
- uv_version: The uv toolchain version to download.
- register_toolchains: If true, repositories will be generated to produce and register `uv_toolchain` targets.
- """
- if not uv_version:
- fail("uv_version is required")
-
- toolchain_names = []
- toolchain_labels_by_toolchain = {}
- toolchain_compatible_with_by_toolchain = {}
-
- for platform in UV_PLATFORMS.keys():
- uv_repository_name = UV_PLATFORMS[platform].default_repo_name
-
- uv_repository(
- name = uv_repository_name,
- uv_version = uv_version,
- platform = platform,
- )
-
- toolchain_name = uv_repository_name + "_toolchain"
- toolchain_names.append(toolchain_name)
- toolchain_labels_by_toolchain[toolchain_name] = "@{}//:uv_toolchain".format(uv_repository_name)
- toolchain_compatible_with_by_toolchain[toolchain_name] = UV_PLATFORMS[platform].compatible_with
-
- uv_toolchains_repo(
- name = name,
- toolchain_type = str(UV_TOOLCHAIN_TYPE),
- toolchain_names = toolchain_names,
- toolchain_labels = toolchain_labels_by_toolchain,
- toolchain_compatible_with = toolchain_compatible_with_by_toolchain,
- )
-
- if register_toolchains:
- native.register_toolchains("@{}/:all".format(name))
diff --git a/python/uv/private/uv_repository.bzl b/python/uv/private/uv_repository.bzl
new file mode 100644
index 0000000..ba7d2a7
--- /dev/null
+++ b/python/uv/private/uv_repository.bzl
@@ -0,0 +1,74 @@
+# Copyright 2024 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.
+
+"""
+EXPERIMENTAL: This is experimental and may be removed without notice
+
+Create repositories for uv toolchain dependencies
+"""
+
+UV_BUILD_TMPL = """\
+# Generated by repositories.bzl
+load("@rules_python//python/uv:uv_toolchain.bzl", "uv_toolchain")
+
+uv_toolchain(
+ name = "uv_toolchain",
+ uv = "{binary}",
+ version = "{version}",
+)
+"""
+
+def _uv_repo_impl(repository_ctx):
+ platform = repository_ctx.attr.platform
+
+ is_windows = "windows" in platform
+ _, _, filename = repository_ctx.attr.urls[0].rpartition("/")
+ if filename.endswith(".tar.gz"):
+ strip_prefix = filename[:-len(".tar.gz")]
+ else:
+ strip_prefix = ""
+
+ result = repository_ctx.download_and_extract(
+ url = repository_ctx.attr.urls,
+ sha256 = repository_ctx.attr.sha256,
+ stripPrefix = strip_prefix,
+ )
+
+ binary = "uv.exe" if is_windows else "uv"
+ repository_ctx.file(
+ "BUILD.bazel",
+ UV_BUILD_TMPL.format(
+ binary = binary,
+ version = repository_ctx.attr.version,
+ ),
+ )
+
+ return {
+ "name": repository_ctx.attr.name,
+ "platform": repository_ctx.attr.platform,
+ "sha256": result.sha256,
+ "urls": repository_ctx.attr.urls,
+ "version": repository_ctx.attr.version,
+ }
+
+uv_repository = repository_rule(
+ _uv_repo_impl,
+ doc = "Fetch external tools needed for uv toolchain",
+ attrs = {
+ "platform": attr.string(mandatory = True),
+ "sha256": attr.string(mandatory = False),
+ "urls": attr.string_list(mandatory = True),
+ "version": attr.string(mandatory = True),
+ },
+)
diff --git a/python/uv/private/uv_toolchain.bzl b/python/uv/private/uv_toolchain.bzl
index 3b51f5f..b740fc3 100644
--- a/python/uv/private/uv_toolchain.bzl
+++ b/python/uv/private/uv_toolchain.bzl
@@ -30,6 +30,8 @@
uv_toolchain_info = UvToolchainInfo(
uv = uv,
version = ctx.attr.version,
+ # Exposed for testing/debugging
+ label = ctx.label,
)
# Export all the providers inside our ToolchainInfo
diff --git a/python/uv/private/uv_toolchain_info.bzl b/python/uv/private/uv_toolchain_info.bzl
index ac1ef31..5d70766 100644
--- a/python/uv/private/uv_toolchain_info.bzl
+++ b/python/uv/private/uv_toolchain_info.bzl
@@ -17,6 +17,11 @@
UvToolchainInfo = provider(
doc = "Information about how to invoke the uv executable.",
fields = {
+ "label": """
+:type: Label
+
+The uv toolchain implementation label returned by the toolchain.
+""",
"uv": """
:type: Target
diff --git a/python/uv/private/uv_toolchains_repo.bzl b/python/uv/private/uv_toolchains_repo.bzl
index 9a8858f..7e11e0a 100644
--- a/python/uv/private/uv_toolchains_repo.bzl
+++ b/python/uv/private/uv_toolchains_repo.bzl
@@ -16,37 +16,44 @@
load("//python/private:text_util.bzl", "render")
-_TOOLCHAIN_TEMPLATE = """
-toolchain(
- name = "{name}",
- target_compatible_with = {compatible_with},
- toolchain = "{toolchain_label}",
- toolchain_type = "{toolchain_type}",
-)
+_TEMPLATE = """\
+load("@rules_python//python/uv/private:toolchains_hub.bzl", "toolchains_hub")
+
+{}
"""
+def _non_empty(d):
+ return {k: v for k, v in d.items() if v}
+
def _toolchains_repo_impl(repository_ctx):
- build_content = ""
- for toolchain_name in repository_ctx.attr.toolchain_names:
- toolchain_label = repository_ctx.attr.toolchain_labels[toolchain_name]
- toolchain_compatible_with = repository_ctx.attr.toolchain_compatible_with[toolchain_name]
-
- build_content += _TOOLCHAIN_TEMPLATE.format(
- name = toolchain_name,
- toolchain_type = repository_ctx.attr.toolchain_type,
- toolchain_label = toolchain_label,
- compatible_with = render.list(toolchain_compatible_with),
- )
-
- repository_ctx.file("BUILD.bazel", build_content)
+ contents = _TEMPLATE.format(
+ render.call(
+ "toolchains_hub",
+ name = repr("uv_toolchain"),
+ toolchains = render.list(repository_ctx.attr.toolchain_names),
+ implementations = render.dict(
+ repository_ctx.attr.toolchain_implementations,
+ ),
+ target_compatible_with = render.dict(
+ repository_ctx.attr.toolchain_compatible_with,
+ value_repr = render.list,
+ ),
+ target_settings = render.dict(
+ _non_empty(repository_ctx.attr.toolchain_target_settings),
+ value_repr = render.list,
+ ),
+ ),
+ )
+ repository_ctx.file("BUILD.bazel", contents)
uv_toolchains_repo = repository_rule(
_toolchains_repo_impl,
doc = "Generates a toolchain hub repository",
attrs = {
"toolchain_compatible_with": attr.string_list_dict(doc = "A list of platform constraints for this toolchain, keyed by toolchain name.", mandatory = True),
- "toolchain_labels": attr.string_dict(doc = "The name of the toolchain implementation target, keyed by toolchain name.", mandatory = True),
+ "toolchain_implementations": attr.string_dict(doc = "The name of the toolchain implementation target, keyed by toolchain name.", mandatory = True),
"toolchain_names": attr.string_list(doc = "List of toolchain names", mandatory = True),
+ "toolchain_target_settings": attr.string_list_dict(doc = "A list of target_settings constraints for this toolchain, keyed by toolchain name.", mandatory = True),
"toolchain_type": attr.string(doc = "The toolchain type of the toolchains", mandatory = True),
},
)
diff --git a/python/uv/private/versions.bzl b/python/uv/private/versions.bzl
deleted file mode 100644
index 1d68302..0000000
--- a/python/uv/private/versions.bzl
+++ /dev/null
@@ -1,94 +0,0 @@
-# Copyright 2024 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.
-
-"""Version and integrity information for downloaded artifacts"""
-
-UV_PLATFORMS = {
- "aarch64-apple-darwin": struct(
- default_repo_name = "uv_darwin_aarch64",
- compatible_with = [
- "@platforms//os:macos",
- "@platforms//cpu:aarch64",
- ],
- ),
- "aarch64-unknown-linux-gnu": struct(
- default_repo_name = "uv_linux_aarch64",
- compatible_with = [
- "@platforms//os:linux",
- "@platforms//cpu:aarch64",
- ],
- ),
- "powerpc64le-unknown-linux-gnu": struct(
- default_repo_name = "uv_linux_ppc",
- compatible_with = [
- "@platforms//os:linux",
- "@platforms//cpu:ppc",
- ],
- ),
- "s390x-unknown-linux-gnu": struct(
- default_repo_name = "uv_linux_s390x",
- compatible_with = [
- "@platforms//os:linux",
- "@platforms//cpu:s390x",
- ],
- ),
- "x86_64-apple-darwin": struct(
- default_repo_name = "uv_darwin_x86_64",
- compatible_with = [
- "@platforms//os:macos",
- "@platforms//cpu:x86_64",
- ],
- ),
- "x86_64-pc-windows-msvc": struct(
- default_repo_name = "uv_windows_x86_64",
- compatible_with = [
- "@platforms//os:windows",
- "@platforms//cpu:x86_64",
- ],
- ),
- "x86_64-unknown-linux-gnu": struct(
- default_repo_name = "uv_linux_x86_64",
- compatible_with = [
- "@platforms//os:linux",
- "@platforms//cpu:x86_64",
- ],
- ),
-}
-
-# From: https://github.com/astral-sh/uv/releases
-UV_TOOL_VERSIONS = {
- "0.4.25": {
- "aarch64-apple-darwin": struct(
- sha256 = "bb2ff4348114ef220ca52e44d5086640c4a1a18f797a5f1ab6f8559fc37b1230",
- ),
- "aarch64-unknown-linux-gnu": struct(
- sha256 = "4485852eb8013530c4275cd222c0056ce123f92742321f012610f1b241463f39",
- ),
- "powerpc64le-unknown-linux-gnu": struct(
- sha256 = "32421c61e8d497243171b28c7efd74f039251256ae9e57ce4a457fdd7d045e24",
- ),
- "s390x-unknown-linux-gnu": struct(
- sha256 = "9afa342d87256f5178a592d3eeb44ece8a93e9359db37e31be1b092226338469",
- ),
- "x86_64-apple-darwin": struct(
- sha256 = "f0ec1f79f4791294382bff242691c6502e95853acef080ae3f7c367a8e1beb6f",
- ),
- "x86_64-pc-windows-msvc": struct(
- sha256 = "c5c7fa084ae4e8ac9e3b0b6c4c7b61e9355eb0c86801c4c7728c0cb142701f38",
- ),
- "x86_64-unknown-linux-gnu": struct(
- sha256 = "6cb6eaf711cd7ce5fb1efaa539c5906374c762af547707a2041c9f6fd207769a",
- ),
- },
-}
diff --git a/tests/uv/BUILD.bazel b/tests/uv/BUILD.bazel
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/uv/BUILD.bazel
diff --git a/tests/uv/uv/BUILD.bazel b/tests/uv/uv/BUILD.bazel
new file mode 100644
index 0000000..e1535ab
--- /dev/null
+++ b/tests/uv/uv/BUILD.bazel
@@ -0,0 +1,17 @@
+# Copyright 2024 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(":uv_tests.bzl", "uv_test_suite")
+
+uv_test_suite(name = "uv_tests")
diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl
new file mode 100644
index 0000000..bf0deef
--- /dev/null
+++ b/tests/uv/uv/uv_tests.bzl
@@ -0,0 +1,592 @@
+# Copyright 2024 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("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("@rules_testing//lib:truth.bzl", "subjects")
+load("//python/uv:uv_toolchain_info.bzl", "UvToolchainInfo")
+load("//python/uv/private:uv.bzl", "process_modules") # buildifier: disable=bzl-visibility
+load("//python/uv/private:uv_toolchain.bzl", "uv_toolchain") # buildifier: disable=bzl-visibility
+
+_tests = []
+
+def _mock_mctx(*modules, download = None, read = None):
+ # Here we construct a fake minimal manifest file that we use to mock what would
+ # be otherwise read from GH files
+ manifest_files = {
+ "different.json": {
+ x: {
+ "checksum": x + ".sha256",
+ "kind": "executable-zip",
+ }
+ for x in ["linux", "osx"]
+ } | {
+ x + ".sha256": {
+ "name": x + ".sha256",
+ "target_triples": [x],
+ }
+ for x in ["linux", "osx"]
+ },
+ "manifest.json": {
+ x: {
+ "checksum": x + ".sha256",
+ "kind": "executable-zip",
+ }
+ for x in ["linux", "os", "osx", "something_extra"]
+ } | {
+ x + ".sha256": {
+ "name": x + ".sha256",
+ "target_triples": [x],
+ }
+ for x in ["linux", "os", "osx", "something_extra"]
+ },
+ }
+
+ fake_fs = {
+ "linux.sha256": "deadbeef linux",
+ "os.sha256": "deadbeef os",
+ "osx.sha256": "deadb00f osx",
+ } | {
+ fname: json.encode({"artifacts": contents})
+ for fname, contents in manifest_files.items()
+ }
+
+ return struct(
+ path = str,
+ download = download or (lambda *_, **__: struct(
+ success = True,
+ wait = lambda: struct(
+ success = True,
+ ),
+ )),
+ read = read or (lambda x: fake_fs[x]),
+ modules = [
+ struct(
+ name = modules[0].name,
+ tags = modules[0].tags,
+ is_root = modules[0].is_root,
+ ),
+ ] + [
+ struct(
+ name = mod.name,
+ tags = mod.tags,
+ is_root = False,
+ )
+ for mod in modules[1:]
+ ],
+ )
+
+def _mod(*, name = None, default = [], configure = [], is_root = True):
+ return struct(
+ name = name, # module_name
+ tags = struct(
+ default = default,
+ configure = configure,
+ ),
+ is_root = is_root,
+ )
+
+def _process_modules(env, **kwargs):
+ result = process_modules(hub_repo = struct, **kwargs)
+
+ return env.expect.that_struct(
+ struct(
+ names = result.toolchain_names,
+ implementations = result.toolchain_implementations,
+ compatible_with = result.toolchain_compatible_with,
+ target_settings = result.toolchain_target_settings,
+ ),
+ attrs = dict(
+ names = subjects.collection,
+ implementations = subjects.dict,
+ compatible_with = subjects.dict,
+ target_settings = subjects.dict,
+ ),
+ )
+
+def _default(
+ base_url = None,
+ compatible_with = None,
+ manifest_filename = None,
+ platform = None,
+ target_settings = None,
+ version = None,
+ **kwargs):
+ return struct(
+ base_url = base_url,
+ compatible_with = [] + (compatible_with or []), # ensure that the type is correct
+ manifest_filename = manifest_filename,
+ platform = platform,
+ target_settings = [] + (target_settings or []), # ensure that the type is correct
+ version = version,
+ **kwargs
+ )
+
+def _configure(urls = None, sha256 = None, **kwargs):
+ # We have the same attributes
+ return _default(sha256 = sha256, urls = urls, **kwargs)
+
+def _test_only_defaults(env):
+ uv = _process_modules(
+ env,
+ module_ctx = _mock_mctx(
+ _mod(
+ default = [
+ _default(
+ base_url = "https://example.org",
+ manifest_filename = "manifest.json",
+ version = "1.0.0",
+ platform = "some_name",
+ compatible_with = ["@platforms//:incompatible"],
+ ),
+ ],
+ ),
+ ),
+ )
+
+ # No defined platform means nothing gets registered
+ uv.names().contains_exactly([
+ "none",
+ ])
+ uv.implementations().contains_exactly({
+ "none": str(Label("//python:none")),
+ })
+ uv.compatible_with().contains_exactly({
+ "none": ["@platforms//:incompatible"],
+ })
+ uv.target_settings().contains_exactly({})
+
+_tests.append(_test_only_defaults)
+
+def _test_manual_url_spec(env):
+ calls = []
+ uv = _process_modules(
+ env,
+ module_ctx = _mock_mctx(
+ _mod(
+ default = [
+ _default(
+ manifest_filename = "manifest.json",
+ version = "1.0.0",
+ ),
+ _default(
+ platform = "linux",
+ compatible_with = ["@platforms//os:linux"],
+ ),
+ # This will be ignored because urls are passed for some of
+ # the binaries.
+ _default(
+ platform = "osx",
+ compatible_with = ["@platforms//os:osx"],
+ ),
+ ],
+ configure = [
+ _configure(
+ platform = "linux",
+ urls = ["https://example.org/download.zip"],
+ sha256 = "deadbeef",
+ ),
+ ],
+ ),
+ read = lambda *args, **kwargs: fail(args, kwargs),
+ ),
+ uv_repository = lambda **kwargs: calls.append(kwargs),
+ )
+
+ uv.names().contains_exactly([
+ "1_0_0_linux",
+ ])
+ uv.implementations().contains_exactly({
+ "1_0_0_linux": "@uv_1_0_0_linux//:uv_toolchain",
+ })
+ uv.compatible_with().contains_exactly({
+ "1_0_0_linux": ["@platforms//os:linux"],
+ })
+ uv.target_settings().contains_exactly({})
+ env.expect.that_collection(calls).contains_exactly([
+ {
+ "name": "uv_1_0_0_linux",
+ "platform": "linux",
+ "sha256": "deadbeef",
+ "urls": ["https://example.org/download.zip"],
+ "version": "1.0.0",
+ },
+ ])
+
+_tests.append(_test_manual_url_spec)
+
+def _test_defaults(env):
+ calls = []
+ uv = _process_modules(
+ env,
+ module_ctx = _mock_mctx(
+ _mod(
+ default = [
+ _default(
+ base_url = "https://example.org",
+ manifest_filename = "manifest.json",
+ version = "1.0.0",
+ platform = "linux",
+ compatible_with = ["@platforms//os:linux"],
+ target_settings = ["//:my_flag"],
+ ),
+ ],
+ configure = [
+ _configure(), # use defaults
+ ],
+ ),
+ ),
+ uv_repository = lambda **kwargs: calls.append(kwargs),
+ )
+
+ uv.names().contains_exactly([
+ "1_0_0_linux",
+ ])
+ uv.implementations().contains_exactly({
+ "1_0_0_linux": "@uv_1_0_0_linux//:uv_toolchain",
+ })
+ uv.compatible_with().contains_exactly({
+ "1_0_0_linux": ["@platforms//os:linux"],
+ })
+ uv.target_settings().contains_exactly({
+ "1_0_0_linux": ["//:my_flag"],
+ })
+ env.expect.that_collection(calls).contains_exactly([
+ {
+ "name": "uv_1_0_0_linux",
+ "platform": "linux",
+ "sha256": "deadbeef",
+ "urls": ["https://example.org/1.0.0/linux"],
+ "version": "1.0.0",
+ },
+ ])
+
+_tests.append(_test_defaults)
+
+def _test_default_building(env):
+ calls = []
+ uv = _process_modules(
+ env,
+ module_ctx = _mock_mctx(
+ _mod(
+ default = [
+ _default(
+ base_url = "https://example.org",
+ manifest_filename = "manifest.json",
+ version = "1.0.0",
+ ),
+ _default(
+ platform = "linux",
+ compatible_with = ["@platforms//os:linux"],
+ target_settings = ["//:my_flag"],
+ ),
+ _default(
+ platform = "osx",
+ compatible_with = ["@platforms//os:osx"],
+ ),
+ ],
+ configure = [
+ _configure(), # use defaults
+ ],
+ ),
+ ),
+ uv_repository = lambda **kwargs: calls.append(kwargs),
+ )
+
+ uv.names().contains_exactly([
+ "1_0_0_linux",
+ "1_0_0_osx",
+ ])
+ uv.implementations().contains_exactly({
+ "1_0_0_linux": "@uv_1_0_0_linux//:uv_toolchain",
+ "1_0_0_osx": "@uv_1_0_0_osx//:uv_toolchain",
+ })
+ uv.compatible_with().contains_exactly({
+ "1_0_0_linux": ["@platforms//os:linux"],
+ "1_0_0_osx": ["@platforms//os:osx"],
+ })
+ uv.target_settings().contains_exactly({
+ "1_0_0_linux": ["//:my_flag"],
+ })
+ env.expect.that_collection(calls).contains_exactly([
+ {
+ "name": "uv_1_0_0_linux",
+ "platform": "linux",
+ "sha256": "deadbeef",
+ "urls": ["https://example.org/1.0.0/linux"],
+ "version": "1.0.0",
+ },
+ {
+ "name": "uv_1_0_0_osx",
+ "platform": "osx",
+ "sha256": "deadb00f",
+ "urls": ["https://example.org/1.0.0/osx"],
+ "version": "1.0.0",
+ },
+ ])
+
+_tests.append(_test_default_building)
+
+def _test_complex_configuring(env):
+ calls = []
+ uv = _process_modules(
+ env,
+ module_ctx = _mock_mctx(
+ _mod(
+ default = [
+ _default(
+ base_url = "https://example.org",
+ manifest_filename = "manifest.json",
+ version = "1.0.0",
+ platform = "osx",
+ compatible_with = ["@platforms//os:os"],
+ ),
+ ],
+ configure = [
+ _configure(), # use defaults
+ _configure(
+ version = "1.0.1",
+ ), # use defaults
+ _configure(
+ version = "1.0.2",
+ base_url = "something_different",
+ manifest_filename = "different.json",
+ ), # use defaults
+ _configure(
+ platform = "osx",
+ compatible_with = ["@platforms//os:different"],
+ ),
+ _configure(
+ version = "1.0.3",
+ ),
+ _configure(platform = "osx"), # remove the default
+ _configure(
+ platform = "linux",
+ compatible_with = ["@platforms//os:linux"],
+ ),
+ ],
+ ),
+ ),
+ uv_repository = lambda **kwargs: calls.append(kwargs),
+ )
+
+ uv.names().contains_exactly([
+ "1_0_0_osx",
+ "1_0_1_osx",
+ "1_0_2_osx",
+ "1_0_3_linux",
+ ])
+ uv.implementations().contains_exactly({
+ "1_0_0_osx": "@uv_1_0_0_osx//:uv_toolchain",
+ "1_0_1_osx": "@uv_1_0_1_osx//:uv_toolchain",
+ "1_0_2_osx": "@uv_1_0_2_osx//:uv_toolchain",
+ "1_0_3_linux": "@uv_1_0_3_linux//:uv_toolchain",
+ })
+ uv.compatible_with().contains_exactly({
+ "1_0_0_osx": ["@platforms//os:os"],
+ "1_0_1_osx": ["@platforms//os:os"],
+ "1_0_2_osx": ["@platforms//os:different"],
+ "1_0_3_linux": ["@platforms//os:linux"],
+ })
+ uv.target_settings().contains_exactly({})
+ env.expect.that_collection(calls).contains_exactly([
+ {
+ "name": "uv_1_0_0_osx",
+ "platform": "osx",
+ "sha256": "deadb00f",
+ "urls": ["https://example.org/1.0.0/osx"],
+ "version": "1.0.0",
+ },
+ {
+ "name": "uv_1_0_1_osx",
+ "platform": "osx",
+ "sha256": "deadb00f",
+ "urls": ["https://example.org/1.0.1/osx"],
+ "version": "1.0.1",
+ },
+ {
+ "name": "uv_1_0_2_osx",
+ "platform": "osx",
+ "sha256": "deadb00f",
+ "urls": ["something_different/1.0.2/osx"],
+ "version": "1.0.2",
+ },
+ {
+ "name": "uv_1_0_3_linux",
+ "platform": "linux",
+ "sha256": "deadbeef",
+ "urls": ["https://example.org/1.0.3/linux"],
+ "version": "1.0.3",
+ },
+ ])
+
+_tests.append(_test_complex_configuring)
+
+def _test_non_rules_python_non_root_is_ignored(env):
+ calls = []
+ uv = _process_modules(
+ env,
+ module_ctx = _mock_mctx(
+ _mod(
+ default = [
+ _default(
+ base_url = "https://example.org",
+ manifest_filename = "manifest.json",
+ version = "1.0.0",
+ platform = "osx",
+ compatible_with = ["@platforms//os:os"],
+ ),
+ ],
+ configure = [
+ _configure(), # use defaults
+ ],
+ ),
+ _mod(
+ name = "something",
+ configure = [
+ _configure(version = "6.6.6"), # use defaults whatever they are
+ ],
+ ),
+ ),
+ uv_repository = lambda **kwargs: calls.append(kwargs),
+ )
+
+ uv.names().contains_exactly([
+ "1_0_0_osx",
+ ])
+ uv.implementations().contains_exactly({
+ "1_0_0_osx": "@uv_1_0_0_osx//:uv_toolchain",
+ })
+ uv.compatible_with().contains_exactly({
+ "1_0_0_osx": ["@platforms//os:os"],
+ })
+ uv.target_settings().contains_exactly({})
+ env.expect.that_collection(calls).contains_exactly([
+ {
+ "name": "uv_1_0_0_osx",
+ "platform": "osx",
+ "sha256": "deadb00f",
+ "urls": ["https://example.org/1.0.0/osx"],
+ "version": "1.0.0",
+ },
+ ])
+
+_tests.append(_test_non_rules_python_non_root_is_ignored)
+
+def _test_rules_python_does_not_take_precedence(env):
+ calls = []
+ uv = _process_modules(
+ env,
+ module_ctx = _mock_mctx(
+ _mod(
+ default = [
+ _default(
+ base_url = "https://example.org",
+ manifest_filename = "manifest.json",
+ version = "1.0.0",
+ platform = "osx",
+ compatible_with = ["@platforms//os:os"],
+ ),
+ ],
+ configure = [
+ _configure(), # use defaults
+ ],
+ ),
+ _mod(
+ name = "rules_python",
+ configure = [
+ _configure(
+ version = "1.0.0",
+ base_url = "https://foobar.org",
+ platform = "osx",
+ compatible_with = ["@platforms//os:osx"],
+ ),
+ ],
+ ),
+ ),
+ uv_repository = lambda **kwargs: calls.append(kwargs),
+ )
+
+ uv.names().contains_exactly([
+ "1_0_0_osx",
+ ])
+ uv.implementations().contains_exactly({
+ "1_0_0_osx": "@uv_1_0_0_osx//:uv_toolchain",
+ })
+ uv.compatible_with().contains_exactly({
+ "1_0_0_osx": ["@platforms//os:os"],
+ })
+ uv.target_settings().contains_exactly({})
+ env.expect.that_collection(calls).contains_exactly([
+ {
+ "name": "uv_1_0_0_osx",
+ "platform": "osx",
+ "sha256": "deadb00f",
+ "urls": ["https://example.org/1.0.0/osx"],
+ "version": "1.0.0",
+ },
+ ])
+
+_tests.append(_test_rules_python_does_not_take_precedence)
+
+_analysis_tests = []
+
+def _test_toolchain_precedence(name):
+ analysis_test(
+ name = name,
+ impl = _test_toolchain_precedence_impl,
+ target = "//python/uv:current_toolchain",
+ config_settings = {
+ "//command_line_option:extra_toolchains": [
+ str(Label("//tests/uv/uv_toolchains:all")),
+ ],
+ "//command_line_option:platforms": str(Label("//tests/support:linux_aarch64")),
+ },
+ )
+
+def _test_toolchain_precedence_impl(env, target):
+ # Check that the forwarded UvToolchainInfo looks vaguely correct.
+ uv_info = env.expect.that_target(target).provider(
+ UvToolchainInfo,
+ factory = lambda v, meta: v,
+ )
+ env.expect.that_str(str(uv_info.label)).contains("//tests/uv/uv:fake_foof")
+
+_analysis_tests.append(_test_toolchain_precedence)
+
+def uv_test_suite(name):
+ """Create the test suite.
+
+ Args:
+ name: the name of the test suite
+ """
+ test_suite(
+ name = name,
+ basic_tests = _tests,
+ tests = _analysis_tests,
+ )
+
+ uv_toolchain(
+ name = "fake_bar",
+ uv = ":BUILD.bazel",
+ version = "0.0.1",
+ )
+
+ uv_toolchain(
+ name = "fake_foof",
+ uv = ":BUILD.bazel",
+ version = "0.0.1",
+ )
diff --git a/tests/uv/uv_toolchains/BUILD.bazel b/tests/uv/uv_toolchains/BUILD.bazel
new file mode 100644
index 0000000..4e2a12d
--- /dev/null
+++ b/tests/uv/uv_toolchains/BUILD.bazel
@@ -0,0 +1,25 @@
+load("//python/uv/private:toolchains_hub.bzl", "toolchains_hub") # buildifier: disable=bzl-visibility
+
+toolchains_hub(
+ name = "uv_unit_test",
+ implementations = {
+ "bar": "//tests/uv/uv:fake_bar",
+ "foo": "//tests/uv/uv:fake_foof",
+ },
+ target_compatible_with = {
+ "bar": [
+ "@platforms//os:linux",
+ "@platforms//cpu:aarch64",
+ ],
+ "foo": [
+ "@platforms//os:linux",
+ "@platforms//cpu:aarch64",
+ ],
+ },
+ target_settings = {},
+ # We expect foo to take precedence over bar
+ toolchains = [
+ "foo",
+ "bar",
+ ],
+)