From 902502ca85f1c92d602b55aa1f07c64a1899f0ba Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:47:26 +0900 Subject: [PATCH 01/46] feat(uv): allow specifying any uv version to use This is 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 it to the `pip.parse` hub repo if `pyproject.toml` is passed in. - [ ] Add a rule/target for `venv` creation. Work towards #1975. --- CHANGELOG.md | 5 + MODULE.bazel | 80 +++++++++++- python/uv/private/BUILD.bazel | 7 -- python/uv/private/lock.bzl | 8 +- python/uv/private/uv.bzl | 173 ++++++++++++++++++++++++-- python/uv/private/uv_repositories.bzl | 67 +++++----- python/uv/private/versions.bzl | 94 -------------- 7 files changed, 281 insertions(+), 153 deletions(-) delete mode 100644 python/uv/private/versions.bzl diff --git a/CHANGELOG.md b/CHANGELOG.md index 1848c1dc59..3c5a78b121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,11 @@ Unreleased changes template. * (rules) deprecation warnings for deprecated symbols have been turned off by default for now and can be enabled with `RULES_PYTHON_DEPRECATION_WARNINGS` env var. +* (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. {#v0-0-0-fixed} ### Fixed diff --git a/MODULE.bazel b/MODULE.bazel index 7034357f61..5105344be3 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -173,14 +173,86 @@ use_repo( "build_bazel_bazel_self", ) -# EXPERIMENTAL: This is experimental and may be removed without notice -uv = use_extension( +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.platform( + name = "aarch64-apple-darwin", + compatible_with = [ + "@platforms//os:macos", + "@platforms//cpu:aarch64", + ], + flag_values = {}, +) +uv.platform( + name = "aarch64-unknown-linux-gnu", + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:aarch64", + ], + flag_values = {}, +) +uv.platform( + name = "powerpc64-unknown-linux-gnu", + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:ppc", + ], + flag_values = {}, +) +uv.platform( + name = "powerpc64le-unknown-linux-gnu", + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:ppc64le", + ], + flag_values = {}, +) +uv.platform( + name = "s390x-unknown-linux-gnu", + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:s390x", + ], + flag_values = {}, +) +uv.platform( + name = "x86_64-apple-darwin", + compatible_with = [ + "@platforms//os:macos", + "@platforms//cpu:x86_64", + ], + flag_values = {}, +) +uv.platform( + name = "x86_64-pc-windows-msvc", + compatible_with = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], + flag_values = {}, +) +uv.platform( + name = "x86_64-unknown-linux-gnu", + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + flag_values = {}, +) + +uv_dev = use_extension( "//python/uv:uv.bzl", "uv", dev_dependency = True, ) -uv.toolchain(uv_version = "0.4.25") -use_repo(uv, "uv_toolchains") +uv_dev.toolchain( + name = "uv_toolchains", + version = "0.5.24", +) +use_repo(uv_dev, "uv_toolchains") register_toolchains( "@uv_toolchains//:all", diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel index 006c856d02..9a69f04a41 100644 --- a/python/uv/private/BUILD.bazel +++ b/python/uv/private/BUILD.bazel @@ -57,7 +57,6 @@ bzl_library( deps = [ ":toolchain_types_bzl", ":uv_toolchains_repo_bzl", - ":versions_bzl", ], ) @@ -82,9 +81,3 @@ bzl_library( "//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 e0491b282c..9378f180db 100644 --- a/python/uv/private/lock.bzl +++ b/python/uv/private/lock.bzl @@ -30,9 +30,11 @@ def lock(*, name, srcs, out, upgrade = False, universal = True, args = [], **kwa """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 @@ def lock(*, name, srcs, out, upgrade = False, universal = True, args = [], **kwa 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/uv.bzl b/python/uv/private/uv.bzl index 886e7fe748..4c0a33f4e7 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -22,32 +22,181 @@ load(":uv_repositories.bzl", "uv_repositories") _DOC = """\ A module extension for working with uv. + +Use it in your own setup by: +```starlark +uv = use_extension( + "@rules_python//python/uv:uv.bzl", + "uv", + dev_dependency = True, +) +uv.toolchain( + name = "uv_toolchains", + version = "0.5.24", +) +use_repo(uv, "uv_toolchains") + +register_toolchains( + "@uv_toolchains//:all", + dev_dependency = True, +) +``` + +Since this is only for locking the requirements files, it should be always +marked as a `dev_dependency`. """ +_DIST_MANIFEST_JSON = "dist-manifest.json" +_DEFAULT_BASE_URL = "https://github.com/astral-sh/uv/releases/download" + +config = tag_class( + doc = "Configure where the binaries are going to be downloaded from.", + attrs = { + "base_url": attr.string( + doc = "Base URL to download metadata about the binaries and the binaries themselves.", + default = _DEFAULT_BASE_URL, + ), + }, +) + +platform = tag_class( + doc = "Configure the available platforms for lock file generation.", + attrs = { + "compatible_with": attr.label_list( + doc = "The compatible with constraint values for toolchain resolution", + ), + "flag_values": attr.label_keyed_string_dict( + doc = "The flag values for toolchain resolution", + ), + "name": attr.string( + doc = "The platform string used in the UV repository to denote the platform triple.", + mandatory = True, + ), + }, +) + uv_toolchain = tag_class( doc = "Configure uv toolchain for lock file generation.", attrs = { - "uv_version": attr.string(doc = "Explicit version of uv.", mandatory = True), + "name": attr.string( + doc = "The name of the toolchain repo", + default = "uv_toolchains", + ), + "version": attr.string( + doc = "Explicit version of uv.", + mandatory = True, + ), }, ) def _uv_toolchain_extension(module_ctx): + config = { + "platforms": {}, + } + for mod in module_ctx.modules: + if not mod.is_root and not mod.name == "rules_python": + # Only rules_python and the root module can configure this. + # + # Ignore any attempts to configure the `uv` toolchain elsewhere + # + # 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 + continue + + # Note, that the first registration will always win, givin priority to + # the root module. + + for platform_attr in mod.tags.platform: + config["platforms"].setdefault(platform_attr.name, struct( + name = platform_attr.name.replace("-", "_").lower(), + compatible_with = platform_attr.compatible_with, + flag_values = platform_attr.flag_values, + )) + + for config_attr in mod.tags.config: + config.setdefault("base_url", config_attr.base_url) + 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", - ) - - uv_repositories( - uv_version = toolchain.uv_version, - register_toolchains = False, + config.setdefault("version", toolchain.version) + config.setdefault("name", toolchain.name) + + if not config["version"]: + return + + config.setdefault("base_url", _DEFAULT_BASE_URL) + config["urls"] = _get_tool_urls_from_dist_manifest( + module_ctx, + base_url = "{base_url}/{version}".format(**config), + ) + uv_repositories( + name = config["name"], + platforms = config["platforms"], + urls = config["urls"], + version = config["version"], + ) + +def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url): + """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. + """ + dist_manifest = module_ctx.path(_DIST_MANIFEST_JSON) + module_ctx.download(base_url + "/" + _DIST_MANIFEST_JSON, output = dist_manifest) + 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"]] + 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 = { + "config": config, + "platform": platform, + "toolchain": uv_toolchain, + }, ) diff --git a/python/uv/private/uv_repositories.bzl b/python/uv/private/uv_repositories.bzl index 24fb9c2447..0ea66d4e79 100644 --- a/python/uv/private/uv_repositories.bzl +++ b/python/uv/private/uv_repositories.bzl @@ -20,7 +20,6 @@ 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 @@ -35,27 +34,17 @@ uv_toolchain( 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, - ) + _, _, filename = repository_ctx.attr.urls[0].rpartition("/") 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, + result = repository_ctx.download_and_extract( + url = repository_ctx.attr.urls, + sha256 = repository_ctx.attr.sha256, stripPrefix = strip_prefix, ) @@ -64,49 +53,64 @@ def _uv_repo_impl(repository_ctx): "BUILD.bazel", UV_BUILD_TMPL.format( binary = binary, - version = uv_version, + 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, values = UV_PLATFORMS.keys()), - "uv_version": attr.string(mandatory = True, values = UV_TOOL_VERSIONS.keys()), + "platform": attr.string(mandatory = True), + "sha256": attr.string(mandatory = False), + "urls": attr.string_list(mandatory = True), + "version": attr.string(mandatory = True), }, ) -def uv_repositories(name = "uv_toolchains", uv_version = None, register_toolchains = True): +def uv_repositories(*, name, version, platforms, urls): """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. + name: The name of the toolchains repo, + version: The uv toolchain version to download. + platforms: The platforms to register uv for. + urls: The urls with sha256 values to register uv for. """ - if not uv_version: - fail("uv_version is required") + if not version: + fail("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 - + for platform_name, platform in platforms.items(): + uv_repository_name = "{}_{}".format(name, platform_name.lower().replace("-", "_")) uv_repository( name = uv_repository_name, - uv_version = uv_version, - platform = platform, + version = version, + platform = platform_name, + urls = urls[platform_name].urls, + sha256 = urls[platform_name].sha256, ) 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 + toolchain_compatible_with_by_toolchain[toolchain_name] = [ + str(label) + for label in platform.compatible_with + ] uv_toolchains_repo( name = name, @@ -115,6 +119,3 @@ def uv_repositories(name = "uv_toolchains", uv_version = None, register_toolchai 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/versions.bzl b/python/uv/private/versions.bzl deleted file mode 100644 index 1d68302c74..0000000000 --- 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", - ), - }, -} From 57377a88b5caf119370286f1b2232640e41e0686 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:54:28 +0900 Subject: [PATCH 02/46] update the example --- examples/bzlmod/MODULE.bazel | 16 +++++++++++++--- python/uv/private/uv.bzl | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index d8535a0115..aa07d6530b 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -105,11 +105,21 @@ python.single_version_platform_override( 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") +uv = use_extension( + "@rules_python//python/uv:uv.bzl", + "uv", + dev_dependency = True, +) +uv.toolchain( + name = "uv_toolchains", + version = "0.5.24", +) use_repo(uv, "uv_toolchains") -register_toolchains("@uv_toolchains//:all") +register_toolchains( + "@uv_toolchains//:all", + dev_dependency = True, +) # 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/uv.bzl b/python/uv/private/uv.bzl index 4c0a33f4e7..82d6f67914 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -106,7 +106,7 @@ def _uv_toolchain_extension(module_ctx): # 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 continue - # Note, that the first registration will always win, givin priority to + # Note, that the first registration will always win, giving priority to # the root module. for platform_attr in mod.tags.platform: From b8fa595b2798f3b462c3889c76014383c67b5723 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:08:00 +0900 Subject: [PATCH 03/46] cleanup --- MODULE.bazel | 8 -------- python/uv/private/uv.bzl | 4 ---- 2 files changed, 12 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 5105344be3..87c26484ec 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -184,7 +184,6 @@ uv.platform( "@platforms//os:macos", "@platforms//cpu:aarch64", ], - flag_values = {}, ) uv.platform( name = "aarch64-unknown-linux-gnu", @@ -192,7 +191,6 @@ uv.platform( "@platforms//os:linux", "@platforms//cpu:aarch64", ], - flag_values = {}, ) uv.platform( name = "powerpc64-unknown-linux-gnu", @@ -200,7 +198,6 @@ uv.platform( "@platforms//os:linux", "@platforms//cpu:ppc", ], - flag_values = {}, ) uv.platform( name = "powerpc64le-unknown-linux-gnu", @@ -208,7 +205,6 @@ uv.platform( "@platforms//os:linux", "@platforms//cpu:ppc64le", ], - flag_values = {}, ) uv.platform( name = "s390x-unknown-linux-gnu", @@ -216,7 +212,6 @@ uv.platform( "@platforms//os:linux", "@platforms//cpu:s390x", ], - flag_values = {}, ) uv.platform( name = "x86_64-apple-darwin", @@ -224,7 +219,6 @@ uv.platform( "@platforms//os:macos", "@platforms//cpu:x86_64", ], - flag_values = {}, ) uv.platform( name = "x86_64-pc-windows-msvc", @@ -232,7 +226,6 @@ uv.platform( "@platforms//os:windows", "@platforms//cpu:x86_64", ], - flag_values = {}, ) uv.platform( name = "x86_64-unknown-linux-gnu", @@ -240,7 +233,6 @@ uv.platform( "@platforms//os:linux", "@platforms//cpu:x86_64", ], - flag_values = {}, ) uv_dev = use_extension( diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 82d6f67914..2f9740eedd 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -65,9 +65,6 @@ platform = tag_class( "compatible_with": attr.label_list( doc = "The compatible with constraint values for toolchain resolution", ), - "flag_values": attr.label_keyed_string_dict( - doc = "The flag values for toolchain resolution", - ), "name": attr.string( doc = "The platform string used in the UV repository to denote the platform triple.", mandatory = True, @@ -113,7 +110,6 @@ def _uv_toolchain_extension(module_ctx): config["platforms"].setdefault(platform_attr.name, struct( name = platform_attr.name.replace("-", "_").lower(), compatible_with = platform_attr.compatible_with, - flag_values = platform_attr.flag_values, )) for config_attr in mod.tags.config: From 8548c69fa9f1baeba686975f28bd6a45522f388d Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 27 Jan 2025 19:14:40 +0900 Subject: [PATCH 04/46] add a note --- MODULE.bazel | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MODULE.bazel b/MODULE.bazel index 87c26484ec..8bb7f774e4 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -173,6 +173,8 @@ use_repo( "build_bazel_bazel_self", ) +# 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 From 069a04cc440ff6e32fe99b5cfc061c8c122f1b97 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:27:18 +0900 Subject: [PATCH 05/46] update changelog --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f870dba2cc..1d35a22fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,11 @@ Unreleased changes template. * {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. {#v0-0-0-removed} ### Removed @@ -83,11 +88,6 @@ Unreleased changes template. * (rules) deprecation warnings for deprecated symbols have been turned off by default for now and can be enabled with `RULES_PYTHON_DEPRECATION_WARNINGS` env var. -* (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) Downgraded versions of packages: `pip` from `24.3.2` to `24.0.0` and `packaging` from `24.2` to `24.0`. From e46d8eb732b7a952c779b70e373eb38c5803f7d7 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:51:54 +0900 Subject: [PATCH 06/46] more cleanup --- MODULE.bazel | 51 +++--- examples/bzlmod/MODULE.bazel | 20 +- python/uv/private/uv.bzl | 222 ++++++++++++++++------- python/uv/private/uv_repositories.bzl | 35 ++-- python/uv/private/uv_toolchains_repo.bzl | 4 + 5 files changed, 215 insertions(+), 117 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index de6af11d04..b15a0abb30 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -181,75 +181,76 @@ 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.platform( - name = "aarch64-apple-darwin", +uv.default( + base_url = "https://github.com/astral-sh/uv/releases/download", + manifest_filename = "dist-manifest.json", + version = "0.6.2", +) +uv.default( compatible_with = [ "@platforms//os:macos", "@platforms//cpu:aarch64", ], + platform = "aarch64-apple-darwin", ) -uv.platform( - name = "aarch64-unknown-linux-gnu", +uv.default( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:aarch64", ], + platform = "aarch64-unknown-linux-gnu", ) -uv.platform( - name = "powerpc64-unknown-linux-gnu", +uv.default( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:ppc", ], + platform = "powerpc64-unknown-linux-gnu", ) -uv.platform( - name = "powerpc64le-unknown-linux-gnu", +uv.default( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:ppc64le", ], + platform = "powerpc64le-unknown-linux-gnu", ) -uv.platform( - name = "s390x-unknown-linux-gnu", +uv.default( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:s390x", ], + platform = "s390x-unknown-linux-gnu", ) -uv.platform( - name = "x86_64-apple-darwin", +uv.default( compatible_with = [ "@platforms//os:macos", "@platforms//cpu:x86_64", ], + platform = "x86_64-apple-darwin", ) -uv.platform( - name = "x86_64-pc-windows-msvc", +uv.default( compatible_with = [ "@platforms//os:windows", "@platforms//cpu:x86_64", ], + platform = "x86_64-pc-windows-msvc", ) -uv.platform( - name = "x86_64-unknown-linux-gnu", +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_dev.toolchain( - name = "uv_toolchains", - version = "0.5.24", -) -use_repo(uv_dev, "uv_toolchains") - -register_toolchains( - "@uv_toolchains//:all", - dev_dependency = True, +uv_dev.append_config( + version = "0.6.2", ) diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index b0d0a5dba4..15af1341e5 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -101,22 +101,20 @@ python.single_version_platform_override( # 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 +# EXPERIMENTAL: This is experimental and may be changed or removed without notice uv = use_extension( "@rules_python//python/uv:uv.bzl", "uv", dev_dependency = True, ) -uv.toolchain( - name = "uv_toolchains", - version = "0.5.24", -) -use_repo(uv, "uv_toolchains") - -register_toolchains( - "@uv_toolchains//:all", - dev_dependency = True, -) +uv.append_config(version = "0.6.2") +# You can also set the defaults for the base_url +# uv.append_config(base_url = "my_mirror") +# uv.append_config( +# platform = "extra-platform", +# target_settings = ["//my_config_setting_label"], +# compatible_with = ["@platforms//os:exotic"], +# ) # 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/uv.bzl b/python/uv/private/uv.bzl index 2f9740eedd..bc32630886 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -18,7 +18,9 @@ EXPERIMENTAL: This is experimental and may be removed without notice A module extension for working with uv. """ +load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") load(":uv_repositories.bzl", "uv_repositories") +load(":uv_toolchains_repo.bzl", "uv_toolchains_repo") _DOC = """\ A module extension for working with uv. @@ -46,102 +48,201 @@ Since this is only for locking the requirements files, it should be always marked as a `dev_dependency`. """ -_DIST_MANIFEST_JSON = "dist-manifest.json" -_DEFAULT_BASE_URL = "https://github.com/astral-sh/uv/releases/download" - -config = tag_class( - doc = "Configure where the binaries are going to be downloaded from.", +default = tag_class( + doc = """\ +Set the uv configuration defaults. +""", attrs = { "base_url": attr.string( doc = "Base URL to download metadata about the binaries and the binaries themselves.", - default = _DEFAULT_BASE_URL, ), - }, -) - -platform = tag_class( - doc = "Configure the available platforms for lock file generation.", - attrs = { "compatible_with": attr.label_list( doc = "The compatible with constraint values for toolchain resolution", ), - "name": attr.string( + "manifest_filename": attr.string( + doc = "The manifest filename to for the metadata fetching.", + default = "dist-manifest.json", + ), + "platform": attr.string( doc = "The platform string used in the UV repository to denote the platform triple.", - mandatory = True, + ), + "target_settings": attr.label_list( + doc = "The `target_settings` to add to platform definitions.", + ), + "version": attr.string( + doc = "The version of uv to use.", ), }, ) -uv_toolchain = tag_class( - doc = "Configure uv toolchain for lock file generation.", +append_config = tag_class( + doc = """\ +Build the UV toolchain configuration appending the last configuration fragment or creating a new. + +A new configuration is created whenever {attr}`version` is passed. +""", attrs = { - "name": attr.string( - doc = "The name of the toolchain repo", - default = "uv_toolchains", + "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 manifest filename to for the metadata fetching.", + 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.", ), "version": attr.string( - doc = "Explicit version of uv.", - mandatory = True, + doc = "The version of uv to use.", ), }, ) -def _uv_toolchain_extension(module_ctx): +def parse_modules(module_ctx): + """Parse the modules to get the config for 'uv' toolchains. + + Args: + module_ctx: the context. + + Returns: + A dictionary for each version of the `uv` to configure. + """ config = { "platforms": {}, } for mod in module_ctx.modules: - if not mod.is_root and not mod.name == "rules_python": - # Only rules_python and the root module can configure this. - # - # Ignore any attempts to configure the `uv` toolchain elsewhere - # - # 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 - continue + for default_attr in mod.tags.default: + if default_attr.version: + config["version"] = default_attr.version - # Note, that the first registration will always win, giving priority to - # the root module. + if default_attr.base_url: + config["base_url"] = default_attr.base_url - for platform_attr in mod.tags.platform: - config["platforms"].setdefault(platform_attr.name, struct( - name = platform_attr.name.replace("-", "_").lower(), - compatible_with = platform_attr.compatible_with, - )) + if default_attr.manifest_filename: + config["manifest_filename"] = default_attr.manifest_filename + + if default_attr.platform and not (default_attr.compatible_with or default_attr.target_settings): + config["platforms"].pop(default_attr.platform) + elif default_attr.platform: + config["platforms"].setdefault( + default_attr.platform, + struct( + name = default_attr.platform.replace("-", "_").lower(), + compatible_with = default_attr.compatible_with, + target_settings = default_attr.target_settings, + ), + ) + elif default_attr.compatible_with or default_attr.target_settings: + fail("TODO: unsupported") + + versions = {} + for mod in module_ctx.modules: + last_version = None + for config_attr in mod.tags.append_config: + last_version = config_attr.version or last_version or config["version"] + specific_config = versions.setdefault(last_version, { + "base_url": config["base_url"], + "manifest_filename": config["manifest_filename"], + "platforms": {k: v for k, v in config["platforms"].items()}, # make a copy + }) + if config_attr.platform and not (config_attr.compatible_with or config_attr.target_settings): + specific_config["platforms"].pop(config_attr.platform) + elif config_attr.platform: + specific_config["platforms"][config_attr.platform] = struct( + name = config_attr.platform.replace("-", "_").lower(), + compatible_with = config_attr.compatible_with, + target_settings = config_attr.target_settings, + ) + elif config_attr.compatible_with or config_attr.target_settings: + fail("TODO: unsupported") + + if config_attr.base_url: + specific_config["base_url"] = config_attr.base_url + + if config_attr.manifest_filename: + config["manifest_filename"] = config_attr.manifest_filename - for config_attr in mod.tags.config: - config.setdefault("base_url", config_attr.base_url) + return versions - for toolchain in mod.tags.toolchain: - config.setdefault("version", toolchain.version) - config.setdefault("name", toolchain.name) +def _uv_toolchain_extension(module_ctx): + uv_versions = parse_modules(module_ctx) - if not config["version"]: + if not uv_versions: + uv_toolchains_repo( + name = "uv", + toolchain_type = str(UV_TOOLCHAIN_TYPE), + toolchain_names = ["none"], + toolchain_labels = { + # 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 = {}, + ) return - config.setdefault("base_url", _DEFAULT_BASE_URL) - config["urls"] = _get_tool_urls_from_dist_manifest( - module_ctx, - base_url = "{base_url}/{version}".format(**config), - ) - uv_repositories( - name = config["name"], - platforms = config["platforms"], - urls = config["urls"], - version = config["version"], + toolchain_names = [] + toolchain_labels_by_toolchain = {} + toolchain_compatible_with_by_toolchain = {} + toolchain_target_settings = {} + + for version, config in uv_versions.items(): + config["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 = config["platforms"] + result = uv_repositories( + name = "uv", + platforms = platforms, + urls = config["urls"], + version = version, + ) + + for name in result.names: + platform = platforms[result.platforms[name]] + + toolchain_names.append(name) + toolchain_labels_by_toolchain[name] = result.labels[name] + toolchain_compatible_with_by_toolchain[name] = [ + str(label) + for label in platform.compatible_with + ] + toolchain_target_settings[name] = [ + str(label) + for label in platform.target_settings + ] + + uv_toolchains_repo( + name = "uv", + toolchain_type = str(UV_TOOLCHAIN_TYPE), + toolchain_names = toolchain_names, + toolchain_labels = toolchain_labels_by_toolchain, + toolchain_compatible_with = toolchain_compatible_with_by_toolchain, + toolchain_target_settings = toolchain_target_settings, ) -def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url): +def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename): """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. """ - dist_manifest = module_ctx.path(_DIST_MANIFEST_JSON) - module_ctx.download(base_url + "/" + _DIST_MANIFEST_JSON, output = dist_manifest) + dist_manifest = module_ctx.path(manifest_filename) + module_ctx.download(base_url + "/" + manifest_filename, output = dist_manifest) dist_manifest = json.decode(module_ctx.read(dist_manifest)) artifacts = dist_manifest["artifacts"] @@ -191,8 +292,7 @@ uv = module_extension( doc = _DOC, implementation = _uv_toolchain_extension, tag_classes = { - "config": config, - "platform": platform, - "toolchain": uv_toolchain, + "append_config": append_config, + "default": default, }, ) diff --git a/python/uv/private/uv_repositories.bzl b/python/uv/private/uv_repositories.bzl index 0ea66d4e79..24f4704e24 100644 --- a/python/uv/private/uv_repositories.bzl +++ b/python/uv/private/uv_repositories.bzl @@ -18,9 +18,6 @@ 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") - UV_BUILD_TMPL = """\ # Generated by repositories.bzl load("@rules_python//python/uv:uv_toolchain.bzl", "uv_toolchain") @@ -86,36 +83,34 @@ def uv_repositories(*, name, version, platforms, urls): version: The uv toolchain version to download. platforms: The platforms to register uv for. urls: The urls with sha256 values to register uv for. + + Returns: + A struct with names, labels and platforms that were created for this invocation. """ if not version: fail("version is required") toolchain_names = [] toolchain_labels_by_toolchain = {} - toolchain_compatible_with_by_toolchain = {} + platform_names_by_toolchain = {} - for platform_name, platform in platforms.items(): - uv_repository_name = "{}_{}".format(name, platform_name.lower().replace("-", "_")) + for platform in platforms: + uv_repository_name = "{}_{}_{}".format(name, version.replace(".", "_"), platform.lower().replace("-", "_")) uv_repository( name = uv_repository_name, version = version, - platform = platform_name, - urls = urls[platform_name].urls, - sha256 = urls[platform_name].sha256, + platform = platform, + urls = urls[platform].urls, + sha256 = urls[platform].sha256, ) 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] = [ - str(label) - for label in 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, + platform_names_by_toolchain[toolchain_name] = platform + + return struct( + names = toolchain_names, + labels = toolchain_labels_by_toolchain, + platforms = platform_names_by_toolchain, ) diff --git a/python/uv/private/uv_toolchains_repo.bzl b/python/uv/private/uv_toolchains_repo.bzl index 9a8858f1b0..7e5f1273e8 100644 --- a/python/uv/private/uv_toolchains_repo.bzl +++ b/python/uv/private/uv_toolchains_repo.bzl @@ -20,6 +20,7 @@ _TOOLCHAIN_TEMPLATE = """ toolchain( name = "{name}", target_compatible_with = {compatible_with}, + target_settings = {target_settings}, toolchain = "{toolchain_label}", toolchain_type = "{toolchain_type}", ) @@ -30,12 +31,14 @@ def _toolchains_repo_impl(repository_ctx): 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] + toolchain_target_settings = repository_ctx.attr.toolchain_target_settings.get(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), + target_settings = render.list(toolchain_target_settings), ) repository_ctx.file("BUILD.bazel", build_content) @@ -47,6 +50,7 @@ uv_toolchains_repo = repository_rule( "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_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), }, ) From 6b675f00528a1b3455f289aa4f629feed42a17b4 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:53:42 +0900 Subject: [PATCH 07/46] add a comment --- examples/bzlmod/MODULE.bazel | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index 15af1341e5..fbcae84a55 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -105,6 +105,8 @@ use_repo(python, "python_3_10", "python_3_9", "python_versions", "pythons_hub") 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.append_config(version = "0.6.2") From 4b709f1e70cc059425176492bcbf1ecc9195e62d Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:07:48 +0900 Subject: [PATCH 08/46] add docs --- MODULE.bazel | 2 +- examples/bzlmod/MODULE.bazel | 9 +------- python/uv/private/uv.bzl | 43 ++++++++++++++++++++++-------------- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index b15a0abb30..d3185c9a62 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -251,6 +251,6 @@ uv_dev = use_extension( "uv", dev_dependency = True, ) -uv_dev.append_config( +uv_dev.configure( version = "0.6.2", ) diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index fbcae84a55..69e384e42b 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -109,14 +109,7 @@ uv = use_extension( # module is used elsewhere. dev_dependency = True, ) -uv.append_config(version = "0.6.2") -# You can also set the defaults for the base_url -# uv.append_config(base_url = "my_mirror") -# uv.append_config( -# platform = "extra-platform", -# target_settings = ["//my_config_setting_label"], -# compatible_with = ["@platforms//os:exotic"], -# ) +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/uv.bzl b/python/uv/private/uv.bzl index bc32630886..9259987695 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -25,23 +25,16 @@ load(":uv_toolchains_repo.bzl", "uv_toolchains_repo") _DOC = """\ A module extension for working with uv. -Use it in your own setup by: +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.toolchain( - name = "uv_toolchains", - version = "0.5.24", -) -use_repo(uv, "uv_toolchains") - -register_toolchains( - "@uv_toolchains//:all", - dev_dependency = True, -) +uv.configure(version = "0.5.24") ``` Since this is only for locking the requirements files, it should be always @@ -75,11 +68,29 @@ Set the uv configuration defaults. }, ) -append_config = tag_class( +configure = tag_class( doc = """\ -Build the UV toolchain configuration appending the last configuration fragment or creating a new. +Build the UV toolchain configuration appending configuration to the last version configuration or starting a new version configuration if {attr}`version` is passed. + +In addition to the very basic configuration pattern outlined above you can customize +the configuration: +```starlark +# Configure the base_url for the specified 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"], +) +``` -A new configuration is created whenever {attr}`version` is passed. +::::tip +The configuration is additive for each version. This means that if you need to set +defaults for all versions, use the {attr}`default` for all of the configuration, +similarly how `rules_python` is doing it itself. +:::: """, attrs = { "base_url": attr.string( @@ -145,7 +156,7 @@ def parse_modules(module_ctx): versions = {} for mod in module_ctx.modules: last_version = None - for config_attr in mod.tags.append_config: + for config_attr in mod.tags.configure: last_version = config_attr.version or last_version or config["version"] specific_config = versions.setdefault(last_version, { "base_url": config["base_url"], @@ -292,7 +303,7 @@ uv = module_extension( doc = _DOC, implementation = _uv_toolchain_extension, tag_classes = { - "append_config": append_config, + "configure": configure, "default": default, }, ) From f29c585eba4c6f4d28d9a6653886de0cd1ee0cc5 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 25 Feb 2025 08:23:10 +0900 Subject: [PATCH 09/46] wip --- python/uv/private/uv.bzl | 57 +++++--- tests/uv/uv/BUILD.bazel | 17 +++ tests/uv/uv/uv_tests.bzl | 300 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 353 insertions(+), 21 deletions(-) create mode 100644 tests/uv/uv/BUILD.bazel create mode 100644 tests/uv/uv/uv_tests.bzl diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 9259987695..87858a8f84 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -115,11 +115,12 @@ similarly how `rules_python` is doing it itself. }, ) -def parse_modules(module_ctx): +def parse_modules(module_ctx, uv_repositories = uv_repositories): """Parse the modules to get the config for 'uv' toolchains. Args: module_ctx: the context. + uv_repositories: the rule to create uv_repositories. Returns: A dictionary for each version of the `uv` to configure. @@ -180,33 +181,30 @@ def parse_modules(module_ctx): if config_attr.manifest_filename: config["manifest_filename"] = config_attr.manifest_filename - return versions - -def _uv_toolchain_extension(module_ctx): - uv_versions = parse_modules(module_ctx) - - if not uv_versions: - uv_toolchains_repo( - name = "uv", - toolchain_type = str(UV_TOOLCHAIN_TYPE), - toolchain_names = ["none"], - toolchain_labels = { + versions = { + v: config + for v, config in versions.items() + if config["platforms"] + } + if not versions: + return struct( + names = ["none"], + labels = { # NOTE @aignas 2025-02-24: the label to the toolchain can be anything "none": str(Label("//python:none")), }, - toolchain_compatible_with = { + compatible_with = { "none": ["@platforms//:incompatible"], }, - toolchain_target_settings = {}, + target_settings = {}, ) - return toolchain_names = [] toolchain_labels_by_toolchain = {} toolchain_compatible_with_by_toolchain = {} toolchain_target_settings = {} - for version, config in uv_versions.items(): + for version, config in versions.items(): config["urls"] = _get_tool_urls_from_dist_manifest( module_ctx, base_url = "{base_url}/{version}".format( @@ -237,13 +235,25 @@ def _uv_toolchain_extension(module_ctx): for label in platform.target_settings ] + return struct( + names = toolchain_names, + labels = toolchain_labels_by_toolchain, + compatible_with = toolchain_compatible_with_by_toolchain, + target_settings = toolchain_target_settings, + ) + +def _uv_toolchain_extension(module_ctx): + toolchain = parse_modules( + module_ctx, + ) + uv_toolchains_repo( name = "uv", toolchain_type = str(UV_TOOLCHAIN_TYPE), - toolchain_names = toolchain_names, - toolchain_labels = toolchain_labels_by_toolchain, - toolchain_compatible_with = toolchain_compatible_with_by_toolchain, - toolchain_target_settings = toolchain_target_settings, + toolchain_names = toolchain.names, + toolchain_labels = toolchain.labels, + toolchain_compatible_with = toolchain.compatible_with, + toolchain_target_settings = toolchain.target_settings, ) def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename): @@ -253,7 +263,12 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename sha256 values for each binary. """ dist_manifest = module_ctx.path(manifest_filename) - module_ctx.download(base_url + "/" + manifest_filename, output = dist_manifest) + 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"] diff --git a/tests/uv/uv/BUILD.bazel b/tests/uv/uv/BUILD.bazel new file mode 100644 index 0000000000..e1535ab5d8 --- /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 0000000000..c91b221e72 --- /dev/null +++ b/tests/uv/uv/uv_tests.bzl @@ -0,0 +1,300 @@ +# 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:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "subjects") +load("//python/uv/private:uv.bzl", "parse_modules") # buildifier: disable=bzl-visibility + +_tests = [] + +def _mock_mctx(*modules, download = None, read = None): + fake_fs = { + "linux.sha256": "deadbeef linux", + "manifest.json": json.encode({ + "artifacts": { + x: { + "checksum": x + ".sha256", + "kind": "executable-zip", + } + for x in ["linux", "os"] + } | { + x + ".sha256": { + "name": x + ".sha256", + "target_triples": [x], + } + for x in ["linux", "os"] + }, + }), + "os.sha256": "deadbeef os", + } + + 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 _parse_modules(env, **kwargs): + return env.expect.that_struct( + parse_modules(**kwargs), + attrs = dict( + names = subjects.collection, + labels = 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(**kwargs): + # We have the same attributes + return _default(**kwargs) + +def _test_simple(env): + uv = _parse_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + base_url = "https://example.org", + manifest_filename = "manifest.json", + version = "1.0.0", + ), + ], + ), + ), + ) + + # No defined platform means nothing gets registered + uv.names().contains_exactly([]) + +_tests.append(_test_simple) + +def _test_defaults(env): + uv = _parse_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.contains_exactly({ + "1.0.0": { + "base_url": "https://example.org", + "manifest_filename": "manifest.json", + "platforms": { + "linux": struct( + name = "linux", + compatible_with = ["@platforms//os:linux"], + target_settings = ["//:my_flag"], + ), + }, + }, + }) + +_tests.append(_test_defaults) + +def _test_default_building(env): + uv = _parse_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.contains_exactly({ + "1.0.0": { + "base_url": "https://example.org", + "manifest_filename": "manifest.json", + "platforms": { + "linux": struct( + name = "linux", + compatible_with = ["@platforms//os:linux"], + target_settings = ["//:my_flag"], + ), + "osx": struct( + name = "osx", + compatible_with = ["@platforms//os:osx"], + target_settings = [], + ), + }, + }, + }) + +_tests.append(_test_default_building) + +def test_complex_configuring(env): + uv = _parse_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + base_url = "https://example.org", + manifest_filename = "manifest.json", + version = "1.0.0", + platform = "os", + 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 = "os", + compatible_with = ["@platforms//os:different"], + ), + _configure( + version = "1.0.3", + ), + _configure(platform = "os"), # remove the default + _configure( + platform = "linux", + compatible_with = ["@platforms//os:linux"], + ), + ], + ), + ), + ) + + uv.contains_exactly({ + "1.0.0": { + "base_url": "https://example.org", + "manifest_filename": "manifest.json", + "platforms": { + "os": struct(compatible_with = ["@platforms//os:os"], name = "os", target_settings = []), + }, + }, + "1.0.1": { + "base_url": "https://example.org", + "manifest_filename": "manifest.json", + "platforms": { + "os": struct(compatible_with = ["@platforms//os:os"], name = "os", target_settings = []), + }, + }, + "1.0.2": { + "base_url": "something_different", + "manifest_filename": "manifest.json", + "platforms": { + "os": struct(compatible_with = ["@platforms//os:different"], name = "os", target_settings = []), + }, + }, + "1.0.3": { + "base_url": "https://example.org", + "manifest_filename": "different.json", + "platforms": { + "linux": struct(compatible_with = ["@platforms//os:linux"], name = "linux", target_settings = []), + }, + }, + }) + +_tests.append(test_complex_configuring) + +def uv_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests) From 79855d2c7a5ba5e412edda873e5226d837874817 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:07:30 +0900 Subject: [PATCH 10/46] wip --- python/uv/private/uv.bzl | 5 +- python/uv/private/uv_repositories.bzl | 6 +- tests/uv/uv/uv_tests.bzl | 117 ++++++++++++-------------- 3 files changed, 59 insertions(+), 69 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 87858a8f84..75d4c82b59 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -115,12 +115,12 @@ similarly how `rules_python` is doing it itself. }, ) -def parse_modules(module_ctx, uv_repositories = uv_repositories): +def parse_modules(module_ctx, uv_repository = None): """Parse the modules to get the config for 'uv' toolchains. Args: module_ctx: the context. - uv_repositories: the rule to create uv_repositories. + uv_repository: the rule to create a uv_repository override. Returns: A dictionary for each version of the `uv` to configure. @@ -219,6 +219,7 @@ def parse_modules(module_ctx, uv_repositories = uv_repositories): platforms = platforms, urls = config["urls"], version = version, + uv_repository = uv_repository, ) for name in result.names: diff --git a/python/uv/private/uv_repositories.bzl b/python/uv/private/uv_repositories.bzl index 24f4704e24..f10c179a8e 100644 --- a/python/uv/private/uv_repositories.bzl +++ b/python/uv/private/uv_repositories.bzl @@ -62,7 +62,7 @@ def _uv_repo_impl(repository_ctx): "version": repository_ctx.attr.version, } -uv_repository = repository_rule( +_uv_repository = repository_rule( _uv_repo_impl, doc = "Fetch external tools needed for uv toolchain", attrs = { @@ -73,7 +73,7 @@ uv_repository = repository_rule( }, ) -def uv_repositories(*, name, version, platforms, urls): +def uv_repositories(*, name, version, platforms, urls, uv_repository = None): """Convenience macro which does typical toolchain setup Skip this macro if you need more control over the toolchain setup. @@ -83,10 +83,12 @@ def uv_repositories(*, name, version, platforms, urls): version: The uv toolchain version to download. platforms: The platforms to register uv for. urls: The urls with sha256 values to register uv for. + uv_repository: The rule function for creating a uv_repository. Returns: A struct with names, labels and platforms that were created for this invocation. """ + uv_repository = uv_repository or _uv_repository if not version: fail("version is required") diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index c91b221e72..8060130588 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -109,7 +109,7 @@ def _configure(**kwargs): # We have the same attributes return _default(**kwargs) -def _test_simple(env): +def _test_only_defaults(env): uv = _parse_modules( env, module_ctx = _mock_mctx( @@ -126,11 +126,21 @@ def _test_simple(env): ) # No defined platform means nothing gets registered - uv.names().contains_exactly([]) + uv.names().contains_exactly([ + "none", + ]) + uv.labels().contains_exactly({ + "none": "@@//python:none", + }) + uv.compatible_with().contains_exactly({ + "none": ["@platforms//:incompatible"], + }) + uv.target_settings().contains_exactly({}) -_tests.append(_test_simple) +_tests.append(_test_only_defaults) def _test_defaults(env): + calls = [] uv = _parse_modules( env, module_ctx = _mock_mctx( @@ -150,25 +160,35 @@ def _test_defaults(env): ], ), ), + uv_repository = lambda **kwargs: calls.append(kwargs), ) - uv.contains_exactly({ - "1.0.0": { - "base_url": "https://example.org", - "manifest_filename": "manifest.json", - "platforms": { - "linux": struct( - name = "linux", - compatible_with = ["@platforms//os:linux"], - target_settings = ["//:my_flag"], - ), - }, - }, + uv.names().contains_exactly([ + "uv_1_0_0_linux_toolchain", + ]) + uv.labels().contains_exactly({ + "uv_1_0_0_linux_toolchain": "@uv_1_0_0_linux//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "uv_1_0_0_linux_toolchain": ["@platforms//os:linux"], }) + uv.target_settings().contains_exactly({ + "uv_1_0_0_linux_toolchain": ["//: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 = _parse_modules( env, module_ctx = _mock_mctx( @@ -194,30 +214,20 @@ def _test_default_building(env): ], ), ), + uv_repository = lambda **kwargs: calls.append(kwargs), ) - uv.contains_exactly({ - "1.0.0": { - "base_url": "https://example.org", - "manifest_filename": "manifest.json", - "platforms": { - "linux": struct( - name = "linux", - compatible_with = ["@platforms//os:linux"], - target_settings = ["//:my_flag"], - ), - "osx": struct( - name = "osx", - compatible_with = ["@platforms//os:osx"], - target_settings = [], - ), - }, - }, - }) + uv.names().contains_exactly([]) + uv.labels().contains_exactly({}) + uv.compatible_with().contains_exactly({}) + uv.target_settings().contains_exactly({}) + env.expect.that_collection(calls).contains_exactly([ + ]) _tests.append(_test_default_building) -def test_complex_configuring(env): +def _test_complex_configuring(env): + calls = [] uv = _parse_modules( env, module_ctx = _mock_mctx( @@ -256,40 +266,17 @@ def test_complex_configuring(env): ], ), ), + uv_repository = lambda **kwargs: calls.append(kwargs), ) - uv.contains_exactly({ - "1.0.0": { - "base_url": "https://example.org", - "manifest_filename": "manifest.json", - "platforms": { - "os": struct(compatible_with = ["@platforms//os:os"], name = "os", target_settings = []), - }, - }, - "1.0.1": { - "base_url": "https://example.org", - "manifest_filename": "manifest.json", - "platforms": { - "os": struct(compatible_with = ["@platforms//os:os"], name = "os", target_settings = []), - }, - }, - "1.0.2": { - "base_url": "something_different", - "manifest_filename": "manifest.json", - "platforms": { - "os": struct(compatible_with = ["@platforms//os:different"], name = "os", target_settings = []), - }, - }, - "1.0.3": { - "base_url": "https://example.org", - "manifest_filename": "different.json", - "platforms": { - "linux": struct(compatible_with = ["@platforms//os:linux"], name = "linux", target_settings = []), - }, - }, - }) + uv.names().contains_exactly([]) + uv.labels().contains_exactly({}) + uv.compatible_with().contains_exactly({}) + uv.target_settings().contains_exactly({}) + env.expect.that_collection(calls).contains_exactly([ + ]) -_tests.append(test_complex_configuring) +_tests.append(_test_complex_configuring) def uv_test_suite(name): """Create the test suite. From 9a7966879de67f3d907dfcad17c71b0f83856efa Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:11:25 +0900 Subject: [PATCH 11/46] wip --- tests/uv/uv/uv_tests.bzl | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index 8060130588..0ae1e3f6c7 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -29,16 +29,17 @@ def _mock_mctx(*modules, download = None, read = None): "checksum": x + ".sha256", "kind": "executable-zip", } - for x in ["linux", "os"] + for x in ["linux", "os", "osx"] } | { x + ".sha256": { "name": x + ".sha256", "target_triples": [x], } - for x in ["linux", "os"] + for x in ["linux", "os", "osx"] }, }), "os.sha256": "deadbeef os", + "osx.sha256": "deadb00f osx", } return struct( @@ -217,11 +218,37 @@ def _test_default_building(env): uv_repository = lambda **kwargs: calls.append(kwargs), ) - uv.names().contains_exactly([]) - uv.labels().contains_exactly({}) - uv.compatible_with().contains_exactly({}) - uv.target_settings().contains_exactly({}) + uv.names().contains_exactly([ + "uv_1_0_0_linux_toolchain", + "uv_1_0_0_osx_toolchain", + ]) + uv.labels().contains_exactly({ + "uv_1_0_0_linux_toolchain": "@uv_1_0_0_linux//:uv_toolchain", + "uv_1_0_0_osx_toolchain": "@uv_1_0_0_osx//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "uv_1_0_0_linux_toolchain": ["@platforms//os:linux"], + "uv_1_0_0_osx_toolchain": ["@platforms//os:osx"], + }) + uv.target_settings().contains_exactly({ + "uv_1_0_0_linux_toolchain": ["//:my_flag"], + "uv_1_0_0_osx_toolchain": [], + }) 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) From 1ac04379805a27c5b73313e90f00d08f3fcac6e5 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:17:27 +0900 Subject: [PATCH 12/46] finish a test --- tests/uv/uv/uv_tests.bzl | 103 +++++++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 19 deletions(-) diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index 0ae1e3f6c7..11730285b4 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -21,25 +21,42 @@ load("//python/uv/private:uv.bzl", "parse_modules") # buildifier: disable=bzl-v _tests = [] def _mock_mctx(*modules, download = None, read = None): + 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"] + } | { + x + ".sha256": { + "name": x + ".sha256", + "target_triples": [x], + } + for x in ["linux", "os", "osx"] + }, + } + fake_fs = { "linux.sha256": "deadbeef linux", - "manifest.json": json.encode({ - "artifacts": { - x: { - "checksum": x + ".sha256", - "kind": "executable-zip", - } - for x in ["linux", "os", "osx"] - } | { - x + ".sha256": { - "name": x + ".sha256", - "target_triples": [x], - } - for x in ["linux", "os", "osx"] - }, - }), "os.sha256": "deadbeef os", "osx.sha256": "deadb00f osx", + } | { + fname: json.encode({"artifacts": contents}) + for fname, contents in manifest_files.items() } return struct( @@ -296,11 +313,59 @@ def _test_complex_configuring(env): uv_repository = lambda **kwargs: calls.append(kwargs), ) - uv.names().contains_exactly([]) - uv.labels().contains_exactly({}) - uv.compatible_with().contains_exactly({}) - uv.target_settings().contains_exactly({}) + uv.names().contains_exactly([ + "uv_1_0_0_os_toolchain", + "uv_1_0_1_os_toolchain", + "uv_1_0_2_os_toolchain", + "uv_1_0_3_linux_toolchain", + ]) + uv.labels().contains_exactly({ + "uv_1_0_0_os_toolchain": "@uv_1_0_0_os//:uv_toolchain", + "uv_1_0_1_os_toolchain": "@uv_1_0_1_os//:uv_toolchain", + "uv_1_0_2_os_toolchain": "@uv_1_0_2_os//:uv_toolchain", + "uv_1_0_3_linux_toolchain": "@uv_1_0_3_linux//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "uv_1_0_0_os_toolchain": ["@platforms//os:os"], + "uv_1_0_1_os_toolchain": ["@platforms//os:os"], + "uv_1_0_2_os_toolchain": ["@platforms//os:different"], + "uv_1_0_3_linux_toolchain": ["@platforms//os:linux"], + }) + uv.target_settings().contains_exactly({ + "uv_1_0_0_os_toolchain": [], + "uv_1_0_1_os_toolchain": [], + "uv_1_0_2_os_toolchain": [], + "uv_1_0_3_linux_toolchain": [], + }) env.expect.that_collection(calls).contains_exactly([ + { + "name": "uv_1_0_0_os", + "platform": "os", + "sha256": "deadbeef", + "urls": ["https://example.org/1.0.0/os"], + "version": "1.0.0", + }, + { + "name": "uv_1_0_1_os", + "platform": "os", + "sha256": "deadbeef", + "urls": ["https://example.org/1.0.1/os"], + "version": "1.0.1", + }, + { + "name": "uv_1_0_2_os", + "platform": "os", + "sha256": "deadbeef", + "urls": ["something_different/1.0.2/os"], + "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) From 76d581cb688e5e96726e97f0ebebcd1d861ae1df Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:36:24 +0900 Subject: [PATCH 13/46] Allow specifying the URL and sha256 values manually --- python/uv/private/uv.bzl | 36 +++++++++---- tests/uv/uv/uv_tests.bzl | 110 ++++++++++++++++++++++++++++----------- 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 75d4c82b59..9b5c3c1a36 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -106,9 +106,15 @@ similarly how `rules_python` is doing it itself. "platform": attr.string( doc = "The platform string used in the UV repository to denote the platform triple.", ), + "sha256": attr.string( + doc = "The sha256 of the downloaded artifact.", + ), "target_settings": attr.label_list( doc = "The `target_settings` to add to platform definitions.", ), + "urls": attr.string_list( + doc = "The urls to download the binary from. If this is used, {attr}`base_url` is ignored.", + ), "version": attr.string( doc = "The version of uv to use.", ), @@ -160,7 +166,7 @@ def parse_modules(module_ctx, uv_repository = None): for config_attr in mod.tags.configure: last_version = config_attr.version or last_version or config["version"] specific_config = versions.setdefault(last_version, { - "base_url": config["base_url"], + "base_url": config.get("base_url", ""), "manifest_filename": config["manifest_filename"], "platforms": {k: v for k, v in config["platforms"].items()}, # make a copy }) @@ -172,6 +178,11 @@ def parse_modules(module_ctx, uv_repository = None): compatible_with = config_attr.compatible_with, target_settings = config_attr.target_settings, ) + if config_attr.urls: + specific_config.setdefault("urls", {})[config_attr.platform] = struct( + sha256 = config_attr.sha256, + urls = config_attr.urls, + ) elif config_attr.compatible_with or config_attr.target_settings: fail("TODO: unsupported") @@ -179,7 +190,7 @@ def parse_modules(module_ctx, uv_repository = None): specific_config["base_url"] = config_attr.base_url if config_attr.manifest_filename: - config["manifest_filename"] = config_attr.manifest_filename + specific_config["manifest_filename"] = config_attr.manifest_filename versions = { v: config @@ -205,19 +216,22 @@ def parse_modules(module_ctx, uv_repository = None): toolchain_target_settings = {} for version, config in versions.items(): - config["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"], - ) + urls = config.get("urls") + 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 = config["platforms"] result = uv_repositories( name = "uv", platforms = platforms, - urls = config["urls"], + urls = urls, version = version, uv_repository = uv_repository, ) diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index 11730285b4..a4f614aa19 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -21,6 +21,8 @@ load("//python/uv/private:uv.bzl", "parse_modules") # buildifier: disable=bzl-v _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: { @@ -123,9 +125,9 @@ def _default( **kwargs ) -def _configure(**kwargs): +def _configure(urls = None, sha256 = None, **kwargs): # We have the same attributes - return _default(**kwargs) + return _default(sha256 = sha256, urls = urls, **kwargs) def _test_only_defaults(env): uv = _parse_modules( @@ -157,6 +159,56 @@ def _test_only_defaults(env): _tests.append(_test_only_defaults) +def _test_manual_url_spec(env): + calls = [] + uv = _parse_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + manifest_filename = "manifest.json", + version = "1.0.0", + ), + ], + configure = [ + _configure( + platform = "linux", + compatible_with = ["@platforms//os: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([ + "uv_1_0_0_linux_toolchain", + ]) + uv.labels().contains_exactly({ + "uv_1_0_0_linux_toolchain": "@uv_1_0_0_linux//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "uv_1_0_0_linux_toolchain": ["@platforms//os:linux"], + }) + uv.target_settings().contains_exactly({ + "uv_1_0_0_linux_toolchain": [], + }) + 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 = _parse_modules( @@ -281,7 +333,7 @@ def _test_complex_configuring(env): base_url = "https://example.org", manifest_filename = "manifest.json", version = "1.0.0", - platform = "os", + platform = "osx", compatible_with = ["@platforms//os:os"], ), ], @@ -296,13 +348,13 @@ def _test_complex_configuring(env): manifest_filename = "different.json", ), # use defaults _configure( - platform = "os", + platform = "osx", compatible_with = ["@platforms//os:different"], ), _configure( version = "1.0.3", ), - _configure(platform = "os"), # remove the default + _configure(platform = "osx"), # remove the default _configure( platform = "linux", compatible_with = ["@platforms//os:linux"], @@ -314,49 +366,49 @@ def _test_complex_configuring(env): ) uv.names().contains_exactly([ - "uv_1_0_0_os_toolchain", - "uv_1_0_1_os_toolchain", - "uv_1_0_2_os_toolchain", + "uv_1_0_0_osx_toolchain", + "uv_1_0_1_osx_toolchain", + "uv_1_0_2_osx_toolchain", "uv_1_0_3_linux_toolchain", ]) uv.labels().contains_exactly({ - "uv_1_0_0_os_toolchain": "@uv_1_0_0_os//:uv_toolchain", - "uv_1_0_1_os_toolchain": "@uv_1_0_1_os//:uv_toolchain", - "uv_1_0_2_os_toolchain": "@uv_1_0_2_os//:uv_toolchain", + "uv_1_0_0_osx_toolchain": "@uv_1_0_0_osx//:uv_toolchain", + "uv_1_0_1_osx_toolchain": "@uv_1_0_1_osx//:uv_toolchain", + "uv_1_0_2_osx_toolchain": "@uv_1_0_2_osx//:uv_toolchain", "uv_1_0_3_linux_toolchain": "@uv_1_0_3_linux//:uv_toolchain", }) uv.compatible_with().contains_exactly({ - "uv_1_0_0_os_toolchain": ["@platforms//os:os"], - "uv_1_0_1_os_toolchain": ["@platforms//os:os"], - "uv_1_0_2_os_toolchain": ["@platforms//os:different"], + "uv_1_0_0_osx_toolchain": ["@platforms//os:os"], + "uv_1_0_1_osx_toolchain": ["@platforms//os:os"], + "uv_1_0_2_osx_toolchain": ["@platforms//os:different"], "uv_1_0_3_linux_toolchain": ["@platforms//os:linux"], }) uv.target_settings().contains_exactly({ - "uv_1_0_0_os_toolchain": [], - "uv_1_0_1_os_toolchain": [], - "uv_1_0_2_os_toolchain": [], + "uv_1_0_0_osx_toolchain": [], + "uv_1_0_1_osx_toolchain": [], + "uv_1_0_2_osx_toolchain": [], "uv_1_0_3_linux_toolchain": [], }) env.expect.that_collection(calls).contains_exactly([ { - "name": "uv_1_0_0_os", - "platform": "os", - "sha256": "deadbeef", - "urls": ["https://example.org/1.0.0/os"], + "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_os", - "platform": "os", - "sha256": "deadbeef", - "urls": ["https://example.org/1.0.1/os"], + "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_os", - "platform": "os", - "sha256": "deadbeef", - "urls": ["something_different/1.0.2/os"], + "name": "uv_1_0_2_osx", + "platform": "osx", + "sha256": "deadb00f", + "urls": ["something_different/1.0.2/osx"], "version": "1.0.2", }, { From 79f3a3dd7725d63dbcab232eee2e84b6fb510e02 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:43:42 +0900 Subject: [PATCH 14/46] docs --- python/uv/private/uv.bzl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 9b5c3c1a36..ab08ebb420 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -84,6 +84,14 @@ uv.configure( 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", +) ``` ::::tip From a1a4072ae7f47898d4ce9d6b748a36c2e65d6759 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:47:49 +0900 Subject: [PATCH 15/46] fixup the tests --- tests/uv/uv/uv_tests.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index a4f614aa19..c3968e6e34 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -150,7 +150,7 @@ def _test_only_defaults(env): "none", ]) uv.labels().contains_exactly({ - "none": "@@//python:none", + "none": str(Label("//python:none")), }) uv.compatible_with().contains_exactly({ "none": ["@platforms//:incompatible"], From a4f2cf898d7ffab558a7cca6c2674b3942005d29 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:50:24 +0900 Subject: [PATCH 16/46] add docs --- python/uv/private/uv.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index ab08ebb420..3b690dd815 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -121,7 +121,7 @@ similarly how `rules_python` is doing it itself. doc = "The `target_settings` to add to platform definitions.", ), "urls": attr.string_list( - doc = "The urls to download the binary from. If this is used, {attr}`base_url` is ignored.", + doc = "The urls to download the binary from. If this is used, {attr}`base_url` is ignored. If the `urls` are specified, they need to be specified for all of the platforms for a particular version.", ), "version": attr.string( doc = "The version of uv to use.", From c4c81057cdac863e2f8aa65edb3b041c100032f8 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:13:28 +0900 Subject: [PATCH 17/46] cleanup the handling of urls and do not download unnecessary sha256 files --- python/uv/private/uv.bzl | 46 +++++++++++++++++++++++++++++----------- tests/uv/uv/uv_tests.bzl | 15 ++++++++++--- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 3b690dd815..e75c900566 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -178,14 +178,15 @@ def parse_modules(module_ctx, uv_repository = None): "manifest_filename": config["manifest_filename"], "platforms": {k: v for k, v in config["platforms"].items()}, # make a copy }) - if config_attr.platform and not (config_attr.compatible_with or config_attr.target_settings): + if config_attr.platform and not (config_attr.compatible_with or config_attr.target_settings or config_attr.urls): specific_config["platforms"].pop(config_attr.platform) elif config_attr.platform: - specific_config["platforms"][config_attr.platform] = struct( - name = config_attr.platform.replace("-", "_").lower(), - compatible_with = config_attr.compatible_with, - target_settings = config_attr.target_settings, - ) + if config_attr.compatible_with or config_attr.target_settings: + specific_config["platforms"][config_attr.platform] = struct( + name = config_attr.platform.replace("-", "_").lower(), + compatible_with = config_attr.compatible_with, + target_settings = config_attr.target_settings, + ) if config_attr.urls: specific_config.setdefault("urls", {})[config_attr.platform] = struct( sha256 = config_attr.sha256, @@ -201,8 +202,8 @@ def parse_modules(module_ctx, uv_repository = None): specific_config["manifest_filename"] = config_attr.manifest_filename versions = { - v: config - for v, config in versions.items() + version: config + for version, config in versions.items() if config["platforms"] } if not versions: @@ -224,7 +225,13 @@ def parse_modules(module_ctx, uv_repository = None): toolchain_target_settings = {} for version, config in versions.items(): - urls = config.get("urls") + platforms = config["platforms"] + + urls = { + platform: src + for platform, src in config.get("urls", {}).items() + if src.urls + } if not urls: urls = _get_tool_urls_from_dist_manifest( module_ctx, @@ -233,12 +240,16 @@ def parse_modules(module_ctx, uv_repository = None): base_url = config["base_url"], ), manifest_filename = config["manifest_filename"], + platforms = sorted(platforms.keys()), ) - platforms = config["platforms"] result = uv_repositories( name = "uv", - platforms = platforms, + platforms = { + platform_name: platform + for platform_name, platform in platforms.items() + if platform_name in urls + }, urls = urls, version = version, uv_repository = uv_repository, @@ -279,7 +290,14 @@ def _uv_toolchain_extension(module_ctx): toolchain_target_settings = toolchain.target_settings, ) -def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename): +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 @@ -302,6 +320,10 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename 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( diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index c3968e6e34..417fde37ae 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -42,13 +42,13 @@ def _mock_mctx(*modules, download = None, read = None): "checksum": x + ".sha256", "kind": "executable-zip", } - for x in ["linux", "os", "osx"] + for x in ["linux", "os", "osx", "something_extra"] } | { x + ".sha256": { "name": x + ".sha256", "target_triples": [x], } - for x in ["linux", "os", "osx"] + for x in ["linux", "os", "osx", "something_extra"] }, } @@ -170,11 +170,20 @@ def _test_manual_url_spec(env): 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", - compatible_with = ["@platforms//os:linux"], urls = ["https://example.org/download.zip"], sha256 = "deadbeef", ), From df80ed8ecbd0293ca62e18ae14bf36f9a8fa1e44 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:21:12 +0900 Subject: [PATCH 18/46] improve docs --- python/uv/private/uv.bzl | 92 +++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index e75c900566..63c975901c 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -41,31 +41,48 @@ Since this is only for locking the requirements files, it should be always marked as a `dev_dependency`. """ +_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. +""", + ), +} + default = tag_class( doc = """\ Set the uv configuration defaults. """, - 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 manifest filename to for the metadata fetching.", - 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.", - ), - "version": attr.string( - doc = "The version of uv to use.", - ), - }, + attrs = _DEFAULT_ATTRS, ) configure = tag_class( @@ -100,31 +117,20 @@ defaults for all versions, use the {attr}`default` for all of the configuration, similarly how `rules_python` is doing it itself. :::: """, - 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 manifest filename to for the metadata fetching.", - default = "dist-manifest.json", - ), - "platform": attr.string( - doc = "The platform string used in the UV repository to denote the platform triple.", - ), + attrs = _DEFAULT_ATTRS | { "sha256": attr.string( - doc = "The sha256 of the downloaded artifact.", - ), - "target_settings": attr.label_list( - doc = "The `target_settings` to add to platform definitions.", + 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` is ignored. If the `urls` are specified, they need to be specified for all of the platforms for a particular version.", - ), - "version": attr.string( - doc = "The version of uv to use.", + 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. +:::: +""", ), }, ) From 832007c662e2cd0127ca46b7177bb7967a0b2470 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:36:39 +0900 Subject: [PATCH 19/46] simplify and reuse code --- python/uv/private/uv.bzl | 110 +++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 63c975901c..560f4d5d04 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -135,6 +135,32 @@ for a particular version. }, ) +def _build_config(config, *, platform, compatible_with, target_settings, urls = [], sha256 = "", **values): + """Set the value in the config if the value is provided""" + for key, value in values.items(): + if not value: + continue + + config[key] = value + + config.setdefault("platforms", {}) + if platform and not (compatible_with or target_settings or urls): + config["platforms"].pop(platform) + elif platform: + if compatible_with or target_settings: + config["platforms"][platform] = struct( + name = platform.replace("-", "_").lower(), + compatible_with = compatible_with, + target_settings = target_settings, + ) + if urls: + config.setdefault("urls", {})[platform] = struct( + sha256 = sha256, + urls = urls, + ) + elif compatible_with or target_settings: + fail("`platform` name must be specified when specifying `compatible_with` or `target_settings`") + def parse_modules(module_ctx, uv_repository = None): """Parse the modules to get the config for 'uv' toolchains. @@ -145,67 +171,47 @@ def parse_modules(module_ctx, uv_repository = None): Returns: A dictionary for each version of the `uv` to configure. """ - config = { - "platforms": {}, - } + config = {} for mod in module_ctx.modules: for default_attr in mod.tags.default: - if default_attr.version: - config["version"] = default_attr.version - - if default_attr.base_url: - config["base_url"] = default_attr.base_url - - if default_attr.manifest_filename: - config["manifest_filename"] = default_attr.manifest_filename - - if default_attr.platform and not (default_attr.compatible_with or default_attr.target_settings): - config["platforms"].pop(default_attr.platform) - elif default_attr.platform: - config["platforms"].setdefault( - default_attr.platform, - struct( - name = default_attr.platform.replace("-", "_").lower(), - compatible_with = default_attr.compatible_with, - target_settings = default_attr.target_settings, - ), - ) - elif default_attr.compatible_with or default_attr.target_settings: - fail("TODO: unsupported") + _build_config( + config, + version = default_attr.version, + base_url = default_attr.base_url, + manifest_filename = default_attr.manifest_filename, + platform = default_attr.platform, + compatible_with = default_attr.compatible_with, + target_settings = default_attr.target_settings, + ) versions = {} for mod in module_ctx.modules: last_version = None for config_attr in mod.tags.configure: last_version = config_attr.version or last_version or config["version"] - specific_config = versions.setdefault(last_version, { - "base_url": config.get("base_url", ""), - "manifest_filename": config["manifest_filename"], - "platforms": {k: v for k, v in config["platforms"].items()}, # make a copy - }) - if config_attr.platform and not (config_attr.compatible_with or config_attr.target_settings or config_attr.urls): - specific_config["platforms"].pop(config_attr.platform) - elif config_attr.platform: - if config_attr.compatible_with or config_attr.target_settings: - specific_config["platforms"][config_attr.platform] = struct( - name = config_attr.platform.replace("-", "_").lower(), - compatible_with = config_attr.compatible_with, - target_settings = config_attr.target_settings, - ) - if config_attr.urls: - specific_config.setdefault("urls", {})[config_attr.platform] = struct( - sha256 = config_attr.sha256, - urls = config_attr.urls, - ) - elif config_attr.compatible_with or config_attr.target_settings: - fail("TODO: unsupported") - - if config_attr.base_url: - specific_config["base_url"] = config_attr.base_url - - if config_attr.manifest_filename: - specific_config["manifest_filename"] = config_attr.manifest_filename + if not last_version: + fail("version must be specified") + + specific_config = versions.setdefault( + last_version, + { + "base_url": config.get("base_url", ""), + "manifest_filename": config["manifest_filename"], + "platforms": dict(config["platforms"]), # copy + }, + ) + + _build_config( + specific_config, + base_url = config_attr.base_url, + manifest_filename = config_attr.manifest_filename, + platform = config_attr.platform, + compatible_with = config_attr.compatible_with, + target_settings = config_attr.target_settings, + sha256 = config_attr.sha256, + urls = config_attr.urls, + ) versions = { version: config From 8076549e7d5130f66ab3fc9d59ca9a3b743ab5dc Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:41:25 +0900 Subject: [PATCH 20/46] more cleanup --- python/uv/private/uv.bzl | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 560f4d5d04..5b6cb1a79c 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -135,7 +135,7 @@ for a particular version. }, ) -def _build_config(config, *, platform, compatible_with, target_settings, urls = [], sha256 = "", **values): +def _configure(config, *, platform, compatible_with, target_settings, urls = [], sha256 = "", **values): """Set the value in the config if the value is provided""" for key, value in values.items(): if not value: @@ -171,12 +171,11 @@ def parse_modules(module_ctx, uv_repository = None): Returns: A dictionary for each version of the `uv` to configure. """ - config = {} - + defaults = {} for mod in module_ctx.modules: for default_attr in mod.tags.default: - _build_config( - config, + _configure( + defaults, version = default_attr.version, base_url = default_attr.base_url, manifest_filename = default_attr.manifest_filename, @@ -189,20 +188,20 @@ def parse_modules(module_ctx, uv_repository = None): for mod in module_ctx.modules: last_version = None for config_attr in mod.tags.configure: - last_version = config_attr.version or last_version or config["version"] + last_version = config_attr.version or last_version or defaults["version"] if not last_version: fail("version must be specified") specific_config = versions.setdefault( last_version, { - "base_url": config.get("base_url", ""), - "manifest_filename": config["manifest_filename"], - "platforms": dict(config["platforms"]), # copy + "base_url": defaults.get("base_url", ""), + "manifest_filename": defaults["manifest_filename"], + "platforms": dict(defaults["platforms"]), # copy }, ) - _build_config( + _configure( specific_config, base_url = config_attr.base_url, manifest_filename = config_attr.manifest_filename, @@ -235,15 +234,18 @@ def parse_modules(module_ctx, uv_repository = None): toolchain_labels_by_toolchain = {} 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, @@ -252,7 +254,7 @@ def parse_modules(module_ctx, uv_repository = None): base_url = config["base_url"], ), manifest_filename = config["manifest_filename"], - platforms = sorted(platforms.keys()), + platforms = sorted(platforms), ) result = uv_repositories( From 0ea9494cc61c4356818408d1bbddc996023b0598 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:41:37 +0900 Subject: [PATCH 21/46] bump the default uv version to 0.6.3 --- MODULE.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MODULE.bazel b/MODULE.bazel index bf0554c631..dc2193cec2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -184,7 +184,7 @@ uv = use_extension("//python/uv:uv.bzl", "uv") uv.default( base_url = "https://github.com/astral-sh/uv/releases/download", manifest_filename = "dist-manifest.json", - version = "0.6.2", + version = "0.6.3", ) uv.default( compatible_with = [ From 9382f6a0ab7e2ad6cc8bd807496c808f7352baac Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:03:12 +0000 Subject: [PATCH 22/46] rename parse_modules to process_modules and create hub repo in it. --- python/uv/private/uv.bzl | 45 +++++++++++++++++++++------------------- tests/uv/uv/uv_tests.bzl | 23 +++++++++++++------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 5b6cb1a79c..25f9334695 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -161,12 +161,20 @@ def _configure(config, *, platform, compatible_with, target_settings, urls = [], elif compatible_with or target_settings: fail("`platform` name must be specified when specifying `compatible_with` or `target_settings`") -def parse_modules(module_ctx, uv_repository = None): +def process_modules( + module_ctx, + hub_name = "uv", + uv_repository = None, + 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: A dictionary for each version of the `uv` to configure. @@ -218,16 +226,18 @@ def parse_modules(module_ctx, uv_repository = None): if config["platforms"] } if not versions: - return struct( - names = ["none"], - labels = { + return hub_repo( + name = hub_name, + toolchain_type = toolchain_type, + toolchain_names = ["none"], + toolchain_labels = { # NOTE @aignas 2025-02-24: the label to the toolchain can be anything "none": str(Label("//python:none")), }, - compatible_with = { + toolchain_compatible_with = { "none": ["@platforms//:incompatible"], }, - target_settings = {}, + toolchain_target_settings = {}, ) toolchain_names = [] @@ -283,27 +293,20 @@ def parse_modules(module_ctx, uv_repository = None): for label in platform.target_settings ] - return struct( - names = toolchain_names, - labels = toolchain_labels_by_toolchain, - compatible_with = toolchain_compatible_with_by_toolchain, - target_settings = toolchain_target_settings, + return hub_repo( + name = hub_name, + toolchain_type = toolchain_type, + toolchain_names = toolchain_names, + toolchain_labels = toolchain_labels_by_toolchain, + toolchain_compatible_with = toolchain_compatible_with_by_toolchain, + toolchain_target_settings = toolchain_target_settings, ) def _uv_toolchain_extension(module_ctx): - toolchain = parse_modules( + toolchain = process_modules( module_ctx, ) - uv_toolchains_repo( - name = "uv", - toolchain_type = str(UV_TOOLCHAIN_TYPE), - toolchain_names = toolchain.names, - toolchain_labels = toolchain.labels, - toolchain_compatible_with = toolchain.compatible_with, - toolchain_target_settings = toolchain.target_settings, - ) - def _overlap(first_collection, second_collection): for x in first_collection: if x in second_collection: diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index 417fde37ae..5581cc280d 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -16,7 +16,7 @@ load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:truth.bzl", "subjects") -load("//python/uv/private:uv.bzl", "parse_modules") # buildifier: disable=bzl-visibility +load("//python/uv/private:uv.bzl", "process_modules") # buildifier: disable=bzl-visibility _tests = [] @@ -96,9 +96,16 @@ def _mod(*, name = None, default = [], configure = [], is_root = True): is_root = is_root, ) -def _parse_modules(env, **kwargs): +def _process_modules(env, **kwargs): + result = process_modules(hub_repo = struct, **kwargs) + return env.expect.that_struct( - parse_modules(**kwargs), + struct( + names = result.toolchain_names, + labels = result.toolchain_labels, + compatible_with = result.toolchain_compatible_with, + target_settings = result.toolchain_target_settings, + ), attrs = dict( names = subjects.collection, labels = subjects.dict, @@ -130,7 +137,7 @@ def _configure(urls = None, sha256 = None, **kwargs): return _default(sha256 = sha256, urls = urls, **kwargs) def _test_only_defaults(env): - uv = _parse_modules( + uv = _process_modules( env, module_ctx = _mock_mctx( _mod( @@ -161,7 +168,7 @@ _tests.append(_test_only_defaults) def _test_manual_url_spec(env): calls = [] - uv = _parse_modules( + uv = _process_modules( env, module_ctx = _mock_mctx( _mod( @@ -220,7 +227,7 @@ _tests.append(_test_manual_url_spec) def _test_defaults(env): calls = [] - uv = _parse_modules( + uv = _process_modules( env, module_ctx = _mock_mctx( _mod( @@ -268,7 +275,7 @@ _tests.append(_test_defaults) def _test_default_building(env): calls = [] - uv = _parse_modules( + uv = _process_modules( env, module_ctx = _mock_mctx( _mod( @@ -333,7 +340,7 @@ _tests.append(_test_default_building) def _test_complex_configuring(env): calls = [] - uv = _parse_modules( + uv = _process_modules( env, module_ctx = _mock_mctx( _mod( From 2f7c38d31e979b83e80f822eb8983cf64e6a3275 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:05:27 +0000 Subject: [PATCH 23/46] update the return value doc --- python/uv/private/uv.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 25f9334695..65d340aaac 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -177,7 +177,7 @@ def process_modules( hub_repo: the hub repo factory function to use. Returns: - A dictionary for each version of the `uv` to configure. + the result of the hub_repo. Mainly used for tests. """ defaults = {} for mod in module_ctx.modules: From 472f5fa5c5f071c6b936f5626e4d1adbfa8a4551 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:14:52 +0000 Subject: [PATCH 24/46] document the builder dicts --- python/uv/private/uv.bzl | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 65d340aaac..a73abb813e 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -179,7 +179,21 @@ def process_modules( Returns: the result of the hub_repo. Mainly used for tests. """ - defaults = {} + # 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: for default_attr in mod.tags.default: _configure( @@ -192,6 +206,21 @@ def process_modules( target_settings = default_attr.target_settings, ) + # 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: last_version = None From 18db7612e65fe5fcdbc268e49bebca7ad4d0f334 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:16:16 +0000 Subject: [PATCH 25/46] ignore calls from non-rules_python non-root modules --- python/uv/private/uv.bzl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index a73abb813e..01d93ad661 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -195,6 +195,9 @@ def process_modules( "version": "", } for mod in module_ctx.modules: + if not (mod.is_root or mod.name == "rules_python"): + continue + for default_attr in mod.tags.default: _configure( defaults, @@ -223,12 +226,14 @@ def process_modules( # } versions = {} for mod in module_ctx.modules: + if not (mod.is_root or mod.name == "rules_python"): + continue + last_version = None for config_attr in mod.tags.configure: last_version = config_attr.version or last_version or defaults["version"] if not last_version: fail("version must be specified") - specific_config = versions.setdefault( last_version, { From 77672e62aee156bae54f0d11366b49123b82dd62 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:19:29 +0000 Subject: [PATCH 26/46] ensure that default keys are set --- python/uv/private/uv.bzl | 10 ++++++++-- tests/uv/uv/uv_tests.bzl | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 01d93ad661..8ba1d3d9ed 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -209,6 +209,14 @@ def process_modules( target_settings = default_attr.target_settings, ) + 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": { @@ -232,8 +240,6 @@ def process_modules( last_version = None for config_attr in mod.tags.configure: last_version = config_attr.version or last_version or defaults["version"] - if not last_version: - fail("version must be specified") specific_config = versions.setdefault( last_version, { diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index 5581cc280d..af97a85615 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -146,6 +146,8 @@ def _test_only_defaults(env): base_url = "https://example.org", manifest_filename = "manifest.json", version = "1.0.0", + platform = "some_name", + compatible_with = ["@platforms//:incompatible"], ), ], ), From e66834c4d3821638be3aefa43b452af484bba5c2 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:25:01 +0000 Subject: [PATCH 27/46] base_url defalut setting --- python/uv/private/uv.bzl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 8ba1d3d9ed..a85de17355 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -243,9 +243,11 @@ def process_modules( specific_config = versions.setdefault( last_version, { - "base_url": defaults.get("base_url", ""), + "base_url": defaults["base_url"], "manifest_filename": defaults["manifest_filename"], - "platforms": dict(defaults["platforms"]), # copy + # shallow copy is enough as the values are structs and will + # be replaced on modification + "platforms": dict(defaults["platforms"]), }, ) From b3dccf73b383f402b35f49a8840053b9f689b3a3 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:25:33 +0000 Subject: [PATCH 28/46] rename '_attr' to 'tag' --- python/uv/private/uv.bzl | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index a85de17355..06ff36486f 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -198,15 +198,15 @@ def process_modules( if not (mod.is_root or mod.name == "rules_python"): continue - for default_attr in mod.tags.default: + for tag in mod.tags.default: _configure( defaults, - version = default_attr.version, - base_url = default_attr.base_url, - manifest_filename = default_attr.manifest_filename, - platform = default_attr.platform, - compatible_with = default_attr.compatible_with, - target_settings = default_attr.target_settings, + 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, ) for key in [ @@ -238,8 +238,8 @@ def process_modules( continue last_version = None - for config_attr in mod.tags.configure: - last_version = config_attr.version or last_version or defaults["version"] + for tag in mod.tags.configure: + last_version = tag.version or last_version or defaults["version"] specific_config = versions.setdefault( last_version, { @@ -253,13 +253,13 @@ def process_modules( _configure( specific_config, - base_url = config_attr.base_url, - manifest_filename = config_attr.manifest_filename, - platform = config_attr.platform, - compatible_with = config_attr.compatible_with, - target_settings = config_attr.target_settings, - sha256 = config_attr.sha256, - urls = config_attr.urls, + 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, ) versions = { From 5deabba400676d3a23d707b39b63ea355417d56b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:27:00 +0000 Subject: [PATCH 29/46] remove guard --- python/uv/private/uv.bzl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 06ff36486f..18c98dfb1c 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -262,11 +262,6 @@ def process_modules( urls = tag.urls, ) - versions = { - version: config - for version, config in versions.items() - if config["platforms"] - } if not versions: return hub_repo( name = hub_name, From 8eff080881fb6a4398c1ab98a13c987a390c2fa8 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:28:31 +0000 Subject: [PATCH 30/46] use a list --- python/uv/private/uv.bzl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 18c98dfb1c..97892d9c13 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -306,11 +306,11 @@ def process_modules( result = uv_repositories( name = "uv", - platforms = { - platform_name: platform - for platform_name, platform in platforms.items() + platforms = [ + platform_name + for platform_name in platforms if platform_name in urls - }, + ], urls = urls, version = version, uv_repository = uv_repository, From ef90b2ffef7de6b894f182935a642c3d3bb19b08 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 02:39:21 +0000 Subject: [PATCH 31/46] document the file format --- python/uv/private/uv.bzl | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 97892d9c13..f49a3f94a1 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -356,6 +356,68 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename 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 bellow + + 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( From 336736633d180c73e144746c92aa38fa781e573b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:09:51 +0000 Subject: [PATCH 32/46] buildifier errors --- python/uv/private/uv.bzl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index f49a3f94a1..94584225b7 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -179,6 +179,7 @@ def process_modules( Returns: the result of the hub_repo. Mainly used for tests. """ + # default values to apply for version specific config defaults = { "base_url": "", @@ -340,8 +341,9 @@ def process_modules( ) def _uv_toolchain_extension(module_ctx): - toolchain = process_modules( + process_modules( module_ctx, + hub_name = "uv", ) def _overlap(first_collection, second_collection): From d07de0b9c9fbce0ca34f0f4d4bf22ba288506cce Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:12:50 +0000 Subject: [PATCH 33/46] rename toolchain_labels --- python/uv/private/uv.bzl | 4 ++-- python/uv/private/uv_toolchains_repo.bzl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 94584225b7..84ee47c017 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -268,7 +268,7 @@ def process_modules( name = hub_name, toolchain_type = toolchain_type, toolchain_names = ["none"], - toolchain_labels = { + toolchain_implementations = { # NOTE @aignas 2025-02-24: the label to the toolchain can be anything "none": str(Label("//python:none")), }, @@ -335,7 +335,7 @@ def process_modules( name = hub_name, toolchain_type = toolchain_type, toolchain_names = toolchain_names, - toolchain_labels = toolchain_labels_by_toolchain, + toolchain_implementations = toolchain_labels_by_toolchain, toolchain_compatible_with = toolchain_compatible_with_by_toolchain, toolchain_target_settings = toolchain_target_settings, ) diff --git a/python/uv/private/uv_toolchains_repo.bzl b/python/uv/private/uv_toolchains_repo.bzl index 7e5f1273e8..0ba52ce7a5 100644 --- a/python/uv/private/uv_toolchains_repo.bzl +++ b/python/uv/private/uv_toolchains_repo.bzl @@ -29,7 +29,7 @@ toolchain( 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_label = repository_ctx.attr.toolchain_implementations[toolchain_name] toolchain_compatible_with = repository_ctx.attr.toolchain_compatible_with[toolchain_name] toolchain_target_settings = repository_ctx.attr.toolchain_target_settings.get(toolchain_name, []) @@ -48,7 +48,7 @@ uv_toolchains_repo = repository_rule( 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), From 479449aaa129066cdbb2e32b70dcd950dde8cf66 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:14:24 +0000 Subject: [PATCH 34/46] document version --- python/uv/private/uv.bzl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 84ee47c017..2ad188cd32 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -73,7 +73,8 @@ definitions. ), "version": attr.string( doc = """\ -The version of uv to configure the sources for. +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`. """, ), } @@ -238,6 +239,7 @@ def process_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"] From 9ed3b7cb170df4a10c26650479219382c9eeb2c0 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 7 Mar 2025 07:23:12 +0000 Subject: [PATCH 35/46] rename toolchain_labels to toolchain_implementations and remove uv_repositories --- python/uv/private/BUILD.bazel | 10 ++-- python/uv/private/uv.bzl | 43 +++++++++-------- ...{uv_repositories.bzl => uv_repository.bzl} | 46 +------------------ tests/uv/uv/uv_tests.bzl | 14 +++--- 4 files changed, 32 insertions(+), 81 deletions(-) rename python/uv/private/{uv_repositories.bzl => uv_repository.bzl} (56%) diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel index 9a69f04a41..38856a195d 100644 --- a/python/uv/private/BUILD.bazel +++ b/python/uv/private/BUILD.bazel @@ -47,17 +47,13 @@ bzl_library( name = "uv_bzl", srcs = ["uv.bzl"], visibility = ["//python/uv:__subpackages__"], - deps = [":uv_repositories_bzl"], + deps = [":uv_repository_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", - ], ) bzl_library( diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 2ad188cd32..11063fa3e1 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -19,7 +19,7 @@ A module extension for working with uv. """ load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") -load(":uv_repositories.bzl", "uv_repositories") +load(":uv_repository.bzl", "uv_repository") load(":uv_toolchains_repo.bzl", "uv_toolchains_repo") _DOC = """\ @@ -165,7 +165,7 @@ def _configure(config, *, platform, compatible_with, target_settings, urls = [], def process_modules( module_ctx, hub_name = "uv", - uv_repository = None, + 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. @@ -281,7 +281,7 @@ def process_modules( ) toolchain_names = [] - toolchain_labels_by_toolchain = {} + toolchain_implementations = {} toolchain_compatible_with_by_toolchain = {} toolchain_target_settings = {} for version, config in versions.items(): @@ -307,28 +307,27 @@ def process_modules( platforms = sorted(platforms), ) - result = uv_repositories( - name = "uv", - platforms = [ - platform_name - for platform_name in platforms - if platform_name in urls - ], - urls = urls, - version = version, - uv_repository = uv_repository, - ) - - for name in result.names: - platform = platforms[result.platforms[name]] + for platform_name, platform in platforms.items(): + if platform_name not in urls: + continue + + uv_repository_name = "{}_{}_{}".format(hub_name, version.replace(".", "_"), platform_name.lower().replace("-", "_")) + uv_repository( + name = uv_repository_name, + version = version, + platform = platform_name, + urls = urls[platform_name].urls, + sha256 = urls[platform_name].sha256, + ) - toolchain_names.append(name) - toolchain_labels_by_toolchain[name] = result.labels[name] - toolchain_compatible_with_by_toolchain[name] = [ + toolchain_name = uv_repository_name + "_toolchain" + 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 ] - toolchain_target_settings[name] = [ + toolchain_target_settings[toolchain_name] = [ str(label) for label in platform.target_settings ] @@ -337,7 +336,7 @@ def process_modules( name = hub_name, toolchain_type = toolchain_type, toolchain_names = toolchain_names, - toolchain_implementations = toolchain_labels_by_toolchain, + toolchain_implementations = toolchain_implementations, toolchain_compatible_with = toolchain_compatible_with_by_toolchain, toolchain_target_settings = toolchain_target_settings, ) diff --git a/python/uv/private/uv_repositories.bzl b/python/uv/private/uv_repository.bzl similarity index 56% rename from python/uv/private/uv_repositories.bzl rename to python/uv/private/uv_repository.bzl index f10c179a8e..ba7d2a766c 100644 --- a/python/uv/private/uv_repositories.bzl +++ b/python/uv/private/uv_repository.bzl @@ -62,7 +62,7 @@ def _uv_repo_impl(repository_ctx): "version": repository_ctx.attr.version, } -_uv_repository = repository_rule( +uv_repository = repository_rule( _uv_repo_impl, doc = "Fetch external tools needed for uv toolchain", attrs = { @@ -72,47 +72,3 @@ _uv_repository = repository_rule( "version": attr.string(mandatory = True), }, ) - -def uv_repositories(*, name, version, platforms, urls, uv_repository = None): - """Convenience macro which does typical toolchain setup - - Skip this macro if you need more control over the toolchain setup. - - Args: - name: The name of the toolchains repo, - version: The uv toolchain version to download. - platforms: The platforms to register uv for. - urls: The urls with sha256 values to register uv for. - uv_repository: The rule function for creating a uv_repository. - - Returns: - A struct with names, labels and platforms that were created for this invocation. - """ - uv_repository = uv_repository or _uv_repository - if not version: - fail("version is required") - - toolchain_names = [] - toolchain_labels_by_toolchain = {} - platform_names_by_toolchain = {} - - for platform in platforms: - uv_repository_name = "{}_{}_{}".format(name, version.replace(".", "_"), platform.lower().replace("-", "_")) - uv_repository( - name = uv_repository_name, - version = version, - platform = platform, - urls = urls[platform].urls, - sha256 = urls[platform].sha256, - ) - - toolchain_name = uv_repository_name + "_toolchain" - toolchain_names.append(toolchain_name) - toolchain_labels_by_toolchain[toolchain_name] = "@{}//:uv_toolchain".format(uv_repository_name) - platform_names_by_toolchain[toolchain_name] = platform - - return struct( - names = toolchain_names, - labels = toolchain_labels_by_toolchain, - platforms = platform_names_by_toolchain, - ) diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index af97a85615..b7aca5312a 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -102,13 +102,13 @@ def _process_modules(env, **kwargs): return env.expect.that_struct( struct( names = result.toolchain_names, - labels = result.toolchain_labels, + implementations = result.toolchain_implementations, compatible_with = result.toolchain_compatible_with, target_settings = result.toolchain_target_settings, ), attrs = dict( names = subjects.collection, - labels = subjects.dict, + implementations = subjects.dict, compatible_with = subjects.dict, target_settings = subjects.dict, ), @@ -158,7 +158,7 @@ def _test_only_defaults(env): uv.names().contains_exactly([ "none", ]) - uv.labels().contains_exactly({ + uv.implementations().contains_exactly({ "none": str(Label("//python:none")), }) uv.compatible_with().contains_exactly({ @@ -206,7 +206,7 @@ def _test_manual_url_spec(env): uv.names().contains_exactly([ "uv_1_0_0_linux_toolchain", ]) - uv.labels().contains_exactly({ + uv.implementations().contains_exactly({ "uv_1_0_0_linux_toolchain": "@uv_1_0_0_linux//:uv_toolchain", }) uv.compatible_with().contains_exactly({ @@ -254,7 +254,7 @@ def _test_defaults(env): uv.names().contains_exactly([ "uv_1_0_0_linux_toolchain", ]) - uv.labels().contains_exactly({ + uv.implementations().contains_exactly({ "uv_1_0_0_linux_toolchain": "@uv_1_0_0_linux//:uv_toolchain", }) uv.compatible_with().contains_exactly({ @@ -309,7 +309,7 @@ def _test_default_building(env): "uv_1_0_0_linux_toolchain", "uv_1_0_0_osx_toolchain", ]) - uv.labels().contains_exactly({ + uv.implementations().contains_exactly({ "uv_1_0_0_linux_toolchain": "@uv_1_0_0_linux//:uv_toolchain", "uv_1_0_0_osx_toolchain": "@uv_1_0_0_osx//:uv_toolchain", }) @@ -389,7 +389,7 @@ def _test_complex_configuring(env): "uv_1_0_2_osx_toolchain", "uv_1_0_3_linux_toolchain", ]) - uv.labels().contains_exactly({ + uv.implementations().contains_exactly({ "uv_1_0_0_osx_toolchain": "@uv_1_0_0_osx//:uv_toolchain", "uv_1_0_1_osx_toolchain": "@uv_1_0_1_osx//:uv_toolchain", "uv_1_0_2_osx_toolchain": "@uv_1_0_2_osx//:uv_toolchain", From 019c6a1e32b7009046601c67a92750005e5c6390 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:07:35 +0900 Subject: [PATCH 36/46] add an analysis test to check lexicographical registration order --- python/uv/private/uv_toolchain.bzl | 1 + python/uv/private/uv_toolchain_info.bzl | 5 +++ python/uv/private/uv_toolchains_repo.bzl | 46 +++++++++---------- python/uv/private/uv_toolchains_repo_def.bzl | 41 +++++++++++++++++ tests/uv/BUILD.bazel | 0 tests/uv/uv/uv_tests.bzl | 47 +++++++++++++++++++- tests/uv/uv_toolchain_info_subject.bzl | 39 ++++++++++++++++ tests/uv/uv_toolchains/BUILD.bazel | 24 ++++++++++ 8 files changed, 178 insertions(+), 25 deletions(-) create mode 100644 python/uv/private/uv_toolchains_repo_def.bzl create mode 100644 tests/uv/BUILD.bazel create mode 100644 tests/uv/uv_toolchain_info_subject.bzl create mode 100644 tests/uv/uv_toolchains/BUILD.bazel diff --git a/python/uv/private/uv_toolchain.bzl b/python/uv/private/uv_toolchain.bzl index 3b51f5f533..d4b45fb9df 100644 --- a/python/uv/private/uv_toolchain.bzl +++ b/python/uv/private/uv_toolchain.bzl @@ -30,6 +30,7 @@ def _uv_toolchain_impl(ctx): uv_toolchain_info = UvToolchainInfo( uv = uv, version = ctx.attr.version, + 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 ac1ef310ea..5d70766e7f 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 0ba52ce7a5..3fb676a425 100644 --- a/python/uv/private/uv_toolchains_repo.bzl +++ b/python/uv/private/uv_toolchains_repo.bzl @@ -16,32 +16,30 @@ load("//python/private:text_util.bzl", "render") -_TOOLCHAIN_TEMPLATE = """ -toolchain( - name = "{name}", - target_compatible_with = {compatible_with}, - target_settings = {target_settings}, - toolchain = "{toolchain_label}", - toolchain_type = "{toolchain_type}", -) -""" - def _toolchains_repo_impl(repository_ctx): - build_content = "" - for toolchain_name in repository_ctx.attr.toolchain_names: - toolchain_label = repository_ctx.attr.toolchain_implementations[toolchain_name] - toolchain_compatible_with = repository_ctx.attr.toolchain_compatible_with[toolchain_name] - toolchain_target_settings = repository_ctx.attr.toolchain_target_settings.get(toolchain_name, []) + repository_ctx.file("BUILD.bazel", """\ +load("@rules_python//python/uv/private:uv_toolchains_repo_def.bzl", "uv_toolchains_repo_def") - 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), - target_settings = render.list(toolchain_target_settings), - ) - - repository_ctx.file("BUILD.bazel", build_content) +uv_toolchains_repo_def( + target_compatible_with = {target_compatible_with}, + implementations = {implementations}, + names = {names}, + target_settings = {target_settings}, +) +""".format( + target_compatible_with = render.dict( + repository_ctx.attr.toolchain_compatible_with, + value_repr = render.list, + ), + names = render.list(repository_ctx.attr.toolchain_names), + implementations = render.dict( + repository_ctx.attr.toolchain_implementations, + ), + target_settings = render.dict( + repository_ctx.attr.toolchain_target_settings, + value_repr = render.list, + ), + )) uv_toolchains_repo = repository_rule( _toolchains_repo_impl, diff --git a/python/uv/private/uv_toolchains_repo_def.bzl b/python/uv/private/uv_toolchains_repo_def.bzl new file mode 100644 index 0000000000..12ea8fb5c4 --- /dev/null +++ b/python/uv/private/uv_toolchains_repo_def.bzl @@ -0,0 +1,41 @@ +# 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.""" + +def uv_toolchains_repo_def(*, name = None, names, implementations, target_compatible_with, target_settings): # @unnamed-macro + """Define the toolchains so that the lexicographical order registration is deterministic. + + Args: + name: Unused. + names: The names for toolchain targets. + 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(names) != len(implementations): + fail("Each name must have an implementation") + + for i, name in enumerate(names): + # poor mans implementation leading 0 + number_prefix = "000{}".format(i) + number_prefix = number_prefix[-3:] + + native.toolchain( + name = "{}_{}".format(number_prefix, name), + target_compatible_with = target_compatible_with.get(name, []), + target_settings = target_settings.get(name, []), + toolchain = implementations[name], + toolchain_type = Label("//python/uv:uv_toolchain_type"), + ) diff --git a/tests/uv/BUILD.bazel b/tests/uv/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index b7aca5312a..f5445bd11e 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -14,9 +14,13 @@ "" +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 +load("//tests/uv:uv_toolchain_info_subject.bzl", "uv_toolchain_info_subject") _tests = [] @@ -440,10 +444,51 @@ def _test_complex_configuring(env): _tests.append(_test_complex_configuring) +_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 = uv_toolchain_info_subject, + ) + env.expect.that_str(str(uv_info.actual.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) + 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_toolchain_info_subject.bzl b/tests/uv/uv_toolchain_info_subject.bzl new file mode 100644 index 0000000000..83e45ff429 --- /dev/null +++ b/tests/uv/uv_toolchain_info_subject.bzl @@ -0,0 +1,39 @@ +# 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. +"""UvToolchainInfo testing subject.""" + +def uv_toolchain_info_subject(info, *, meta): + """Creates a new `CcInfoSubject` for a CcInfo provider instance. + + Args: + info: The CcInfo object. + meta: ExpectMeta object. + + Returns: + A `CcInfoSubject` struct. + """ + + # buildifier: disable=uninitialized + public = struct( + # go/keep-sorted start + actual = info, + # go/keep-sorted end + ) + + # buildifier: @unused + self = struct( + actual = info, + meta = meta, + ) + return public diff --git a/tests/uv/uv_toolchains/BUILD.bazel b/tests/uv/uv_toolchains/BUILD.bazel new file mode 100644 index 0000000000..61e4d04bb0 --- /dev/null +++ b/tests/uv/uv_toolchains/BUILD.bazel @@ -0,0 +1,24 @@ +load("//python/uv/private:uv_toolchains_repo_def.bzl", "uv_toolchains_repo_def") # buildifier: disable=bzl-visibility + +uv_toolchains_repo_def( + implementations = { + "bar": "//tests/uv/uv:fake_bar", + "foo": "//tests/uv/uv:fake_foof", + }, + # We expect bar to not be effective here + names = [ + "foo", + "bar", + ], + target_compatible_with = { + "bar": [ + "@platforms//os:linux", + "@platforms//cpu:aarch64", + ], + "foo": [ + "@platforms//os:linux", + "@platforms//cpu:aarch64", + ], + }, + target_settings = {}, +) From 621b80dc1017c2393f8aa142f87b629396885480 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:15:20 +0900 Subject: [PATCH 37/46] attempt to clarify the documentation wording. --- python/uv/private/uv.bzl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 11063fa3e1..8be36b2edc 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -88,10 +88,12 @@ Set the uv configuration defaults. configure = tag_class( doc = """\ -Build the UV toolchain configuration appending configuration to the last version configuration or starting a new version configuration if {attr}`version` is passed. +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 or the version from the defaults is used. -In addition to the very basic configuration pattern outlined above you can customize -the configuration: +Configuration examples: ```starlark # Configure the base_url for the specified version. uv.configure(base_url = "my_mirror") From 929b3e4db47fb4121ad161085642f37548d5840b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 8 Mar 2025 00:16:25 +0900 Subject: [PATCH 38/46] cleanup docs --- python/uv/private/uv.bzl | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 8be36b2edc..ed2184556f 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -93,9 +93,9 @@ 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 or the version from the defaults is used. -Configuration examples: +Complex configuration example: ```starlark -# Configure the base_url for the specified version. +# 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. @@ -113,12 +113,6 @@ uv.configure( sha256 = "deadbeef", ) ``` - -::::tip -The configuration is additive for each version. This means that if you need to set -defaults for all versions, use the {attr}`default` for all of the configuration, -similarly how `rules_python` is doing it itself. -:::: """, attrs = _DEFAULT_ATTRS | { "sha256": attr.string( From f63d45359e821fd24bb8bd664e2375e6226c307e Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 8 Mar 2025 20:30:26 +0900 Subject: [PATCH 39/46] improve the toolchain hub generation --- python/uv/private/BUILD.bazel | 6 ++- python/uv/private/uv.bzl | 4 +- python/uv/private/uv_toolchains_repo.bzl | 46 +++++++++++--------- python/uv/private/uv_toolchains_repo_def.bzl | 13 +++++- 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel index 38856a195d..acf2a9c1f7 100644 --- a/python/uv/private/BUILD.bazel +++ b/python/uv/private/BUILD.bazel @@ -47,7 +47,11 @@ bzl_library( name = "uv_bzl", srcs = ["uv.bzl"], visibility = ["//python/uv:__subpackages__"], - deps = [":uv_repository_bzl"], + deps = [ + ":toolchain_types_bzl", + ":uv_repository_bzl", + ":uv_toolchains_repo_bzl", + ], ) bzl_library( diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index ed2184556f..ad5e606d84 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -307,7 +307,8 @@ def process_modules( if platform_name not in urls: continue - uv_repository_name = "{}_{}_{}".format(hub_name, version.replace(".", "_"), platform_name.lower().replace("-", "_")) + 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, @@ -316,7 +317,6 @@ def process_modules( sha256 = urls[platform_name].sha256, ) - toolchain_name = uv_repository_name + "_toolchain" toolchain_names.append(toolchain_name) toolchain_implementations[toolchain_name] = "@{}//:uv_toolchain".format(uv_repository_name) toolchain_compatible_with_by_toolchain[toolchain_name] = [ diff --git a/python/uv/private/uv_toolchains_repo.bzl b/python/uv/private/uv_toolchains_repo.bzl index 3fb676a425..156f2f1012 100644 --- a/python/uv/private/uv_toolchains_repo.bzl +++ b/python/uv/private/uv_toolchains_repo.bzl @@ -16,30 +16,34 @@ load("//python/private:text_util.bzl", "render") -def _toolchains_repo_impl(repository_ctx): - repository_ctx.file("BUILD.bazel", """\ +_TEMPLATE = """\ load("@rules_python//python/uv/private:uv_toolchains_repo_def.bzl", "uv_toolchains_repo_def") -uv_toolchains_repo_def( - target_compatible_with = {target_compatible_with}, - implementations = {implementations}, - names = {names}, - target_settings = {target_settings}, -) -""".format( - target_compatible_with = render.dict( - repository_ctx.attr.toolchain_compatible_with, - value_repr = render.list, - ), - names = render.list(repository_ctx.attr.toolchain_names), - implementations = render.dict( - repository_ctx.attr.toolchain_implementations, - ), - target_settings = render.dict( - repository_ctx.attr.toolchain_target_settings, - value_repr = render.list, +{} +""" + +def _non_empty(d): + return {k: v for k, v in d.items() if v} + +def _toolchains_repo_impl(repository_ctx): + contents = _TEMPLATE.format( + render.call( + "uv_toolchains_repo_def", + names = 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, diff --git a/python/uv/private/uv_toolchains_repo_def.bzl b/python/uv/private/uv_toolchains_repo_def.bzl index 12ea8fb5c4..2b1456da09 100644 --- a/python/uv/private/uv_toolchains_repo_def.bzl +++ b/python/uv/private/uv_toolchains_repo_def.bzl @@ -14,7 +14,16 @@ """A macro used from the uv_toolchain hub repo.""" -def uv_toolchains_repo_def(*, name = None, names, implementations, target_compatible_with, target_settings): # @unnamed-macro +load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") + +def uv_toolchains_repo_def( + *, + name = None, + names, + implementations, + target_compatible_with, + target_settings): + # @unnamed-macro """Define the toolchains so that the lexicographical order registration is deterministic. Args: @@ -37,5 +46,5 @@ def uv_toolchains_repo_def(*, name = None, names, implementations, target_compat target_compatible_with = target_compatible_with.get(name, []), target_settings = target_settings.get(name, []), toolchain = implementations[name], - toolchain_type = Label("//python/uv:uv_toolchain_type"), + toolchain_type = UV_TOOLCHAIN_TYPE, ) From f82fc81994d63f2e5ea2e19a75436116940ae45b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 9 Mar 2025 20:52:50 +0900 Subject: [PATCH 40/46] further cleanup --- ...chains_repo_def.bzl => toolchains_hub.bzl} | 9 +-- python/uv/private/uv.bzl | 9 +-- python/uv/private/uv_toolchains_repo.bzl | 4 +- tests/uv/uv/uv_tests.bzl | 64 ++++++++----------- tests/uv/uv_toolchains/BUILD.bazel | 4 +- 5 files changed, 42 insertions(+), 48 deletions(-) rename python/uv/private/{uv_toolchains_repo_def.bzl => toolchains_hub.bzl} (86%) diff --git a/python/uv/private/uv_toolchains_repo_def.bzl b/python/uv/private/toolchains_hub.bzl similarity index 86% rename from python/uv/private/uv_toolchains_repo_def.bzl rename to python/uv/private/toolchains_hub.bzl index 2b1456da09..a6ecc2d005 100644 --- a/python/uv/private/uv_toolchains_repo_def.bzl +++ b/python/uv/private/toolchains_hub.bzl @@ -16,7 +16,7 @@ load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") -def uv_toolchains_repo_def( +def toolchains_hub( *, name = None, names, @@ -36,10 +36,11 @@ def uv_toolchains_repo_def( if len(names) != len(implementations): fail("Each name must have an implementation") - for i, name in enumerate(names): + padding = len(str(len(names))) # get the number of digits + for i, name in sorted(enumerate(names), key = lambda x: -x[0]): # poor mans implementation leading 0 - number_prefix = "000{}".format(i) - number_prefix = number_prefix[-3:] + number_prefix = ("0" * padding) + "{}".format(i) + number_prefix = number_prefix[-padding:] native.toolchain( name = "{}_{}".format(number_prefix, name), diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index ad5e606d84..9cd3392c23 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -323,10 +323,11 @@ def process_modules( str(label) for label in platform.compatible_with ] - toolchain_target_settings[toolchain_name] = [ - str(label) - for label in platform.target_settings - ] + if platform.target_settings: + toolchain_target_settings[toolchain_name] = [ + str(label) + for label in platform.target_settings + ] return hub_repo( name = hub_name, diff --git a/python/uv/private/uv_toolchains_repo.bzl b/python/uv/private/uv_toolchains_repo.bzl index 156f2f1012..8b2374f82a 100644 --- a/python/uv/private/uv_toolchains_repo.bzl +++ b/python/uv/private/uv_toolchains_repo.bzl @@ -17,7 +17,7 @@ load("//python/private:text_util.bzl", "render") _TEMPLATE = """\ -load("@rules_python//python/uv/private:uv_toolchains_repo_def.bzl", "uv_toolchains_repo_def") +load("@rules_python//python/uv/private:toolchains_hub.bzl", "toolchains_hub") {} """ @@ -28,7 +28,7 @@ def _non_empty(d): def _toolchains_repo_impl(repository_ctx): contents = _TEMPLATE.format( render.call( - "uv_toolchains_repo_def", + "toolchains_hub", names = render.list(repository_ctx.attr.toolchain_names), implementations = render.dict( repository_ctx.attr.toolchain_implementations, diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index f5445bd11e..e674043244 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -208,17 +208,15 @@ def _test_manual_url_spec(env): ) uv.names().contains_exactly([ - "uv_1_0_0_linux_toolchain", + "1_0_0_linux", ]) uv.implementations().contains_exactly({ - "uv_1_0_0_linux_toolchain": "@uv_1_0_0_linux//:uv_toolchain", + "1_0_0_linux": "@uv_1_0_0_linux//:uv_toolchain", }) uv.compatible_with().contains_exactly({ - "uv_1_0_0_linux_toolchain": ["@platforms//os:linux"], - }) - uv.target_settings().contains_exactly({ - "uv_1_0_0_linux_toolchain": [], + "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", @@ -256,16 +254,16 @@ def _test_defaults(env): ) uv.names().contains_exactly([ - "uv_1_0_0_linux_toolchain", + "1_0_0_linux", ]) uv.implementations().contains_exactly({ - "uv_1_0_0_linux_toolchain": "@uv_1_0_0_linux//:uv_toolchain", + "1_0_0_linux": "@uv_1_0_0_linux//:uv_toolchain", }) uv.compatible_with().contains_exactly({ - "uv_1_0_0_linux_toolchain": ["@platforms//os:linux"], + "1_0_0_linux": ["@platforms//os:linux"], }) uv.target_settings().contains_exactly({ - "uv_1_0_0_linux_toolchain": ["//:my_flag"], + "1_0_0_linux": ["//:my_flag"], }) env.expect.that_collection(calls).contains_exactly([ { @@ -310,20 +308,19 @@ def _test_default_building(env): ) uv.names().contains_exactly([ - "uv_1_0_0_linux_toolchain", - "uv_1_0_0_osx_toolchain", + "1_0_0_linux", + "1_0_0_osx", ]) uv.implementations().contains_exactly({ - "uv_1_0_0_linux_toolchain": "@uv_1_0_0_linux//:uv_toolchain", - "uv_1_0_0_osx_toolchain": "@uv_1_0_0_osx//:uv_toolchain", + "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({ - "uv_1_0_0_linux_toolchain": ["@platforms//os:linux"], - "uv_1_0_0_osx_toolchain": ["@platforms//os:osx"], + "1_0_0_linux": ["@platforms//os:linux"], + "1_0_0_osx": ["@platforms//os:osx"], }) uv.target_settings().contains_exactly({ - "uv_1_0_0_linux_toolchain": ["//:my_flag"], - "uv_1_0_0_osx_toolchain": [], + "1_0_0_linux": ["//:my_flag"], }) env.expect.that_collection(calls).contains_exactly([ { @@ -388,29 +385,24 @@ def _test_complex_configuring(env): ) uv.names().contains_exactly([ - "uv_1_0_0_osx_toolchain", - "uv_1_0_1_osx_toolchain", - "uv_1_0_2_osx_toolchain", - "uv_1_0_3_linux_toolchain", + "1_0_0_osx", + "1_0_1_osx", + "1_0_2_osx", + "1_0_3_linux", ]) uv.implementations().contains_exactly({ - "uv_1_0_0_osx_toolchain": "@uv_1_0_0_osx//:uv_toolchain", - "uv_1_0_1_osx_toolchain": "@uv_1_0_1_osx//:uv_toolchain", - "uv_1_0_2_osx_toolchain": "@uv_1_0_2_osx//:uv_toolchain", - "uv_1_0_3_linux_toolchain": "@uv_1_0_3_linux//:uv_toolchain", + "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({ - "uv_1_0_0_osx_toolchain": ["@platforms//os:os"], - "uv_1_0_1_osx_toolchain": ["@platforms//os:os"], - "uv_1_0_2_osx_toolchain": ["@platforms//os:different"], - "uv_1_0_3_linux_toolchain": ["@platforms//os:linux"], - }) - uv.target_settings().contains_exactly({ - "uv_1_0_0_osx_toolchain": [], - "uv_1_0_1_osx_toolchain": [], - "uv_1_0_2_osx_toolchain": [], - "uv_1_0_3_linux_toolchain": [], + "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", diff --git a/tests/uv/uv_toolchains/BUILD.bazel b/tests/uv/uv_toolchains/BUILD.bazel index 61e4d04bb0..c2279edacf 100644 --- a/tests/uv/uv_toolchains/BUILD.bazel +++ b/tests/uv/uv_toolchains/BUILD.bazel @@ -1,6 +1,6 @@ -load("//python/uv/private:uv_toolchains_repo_def.bzl", "uv_toolchains_repo_def") # buildifier: disable=bzl-visibility +load("//python/uv/private:toolchains_hub.bzl", "toolchains_hub") # buildifier: disable=bzl-visibility -uv_toolchains_repo_def( +toolchains_hub( implementations = { "bar": "//tests/uv/uv:fake_bar", "foo": "//tests/uv/uv:fake_foof", From e61a2510065f15d24255d3631771bd78287ad51c Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:09:28 +0900 Subject: [PATCH 41/46] add documentation --- python/uv/private/toolchains_hub.bzl | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/python/uv/private/toolchains_hub.bzl b/python/uv/private/toolchains_hub.bzl index a6ecc2d005..11c53ca5ab 100644 --- a/python/uv/private/toolchains_hub.bzl +++ b/python/uv/private/toolchains_hub.bzl @@ -26,6 +26,8 @@ def toolchains_hub( # @unnamed-macro """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: Unused. names: The names for toolchain targets. @@ -36,11 +38,21 @@ def toolchains_hub( if len(names) != len(implementations): fail("Each name must have an implementation") - padding = len(str(len(names))) # get the number of digits + # We are setting the order of the toolchains so that the later coming + # toolchains override the previous definitions using the toolchain + # resolution properties: + # * 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. + prefix_len = len(str(len(names))) + prefix = "0" * (prefix_len - 1) + for i, name in sorted(enumerate(names), key = lambda x: -x[0]): - # poor mans implementation leading 0 - number_prefix = ("0" * padding) + "{}".format(i) - number_prefix = number_prefix[-padding:] + # prefix with a prefix and then truncate the string. + number_prefix = "{}{}".format(prefix, i)[-prefix_len:] native.toolchain( name = "{}_{}".format(number_prefix, name), From d00ee31aa856076f2cdc560fa90b7c410eee169c Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:14:57 +0900 Subject: [PATCH 42/46] fix the toolchains registration and document the algorithm --- python/uv/private/toolchains_hub.bzl | 9 +++++++-- tests/uv/uv_toolchains/BUILD.bazel | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/python/uv/private/toolchains_hub.bzl b/python/uv/private/toolchains_hub.bzl index 11c53ca5ab..f93239cc2e 100644 --- a/python/uv/private/toolchains_hub.bzl +++ b/python/uv/private/toolchains_hub.bzl @@ -30,7 +30,8 @@ def toolchains_hub( Args: name: Unused. - names: The names for toolchain targets. + names: The names for toolchain targets. The later occurring items take + precedence over the previous items if they match the target platform. 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. @@ -50,7 +51,11 @@ def toolchains_hub( prefix_len = len(str(len(names))) prefix = "0" * (prefix_len - 1) - for i, name in sorted(enumerate(names), key = lambda x: -x[0]): + # reverse the names list so that the later items override earlier toolchain + # registrations. + names = [n for _, n in sorted(enumerate(names), key = lambda x: -x[0])] + + for i, name in enumerate(names): # prefix with a prefix and then truncate the string. number_prefix = "{}{}".format(prefix, i)[-prefix_len:] diff --git a/tests/uv/uv_toolchains/BUILD.bazel b/tests/uv/uv_toolchains/BUILD.bazel index c2279edacf..05f3eb9367 100644 --- a/tests/uv/uv_toolchains/BUILD.bazel +++ b/tests/uv/uv_toolchains/BUILD.bazel @@ -5,10 +5,10 @@ toolchains_hub( "bar": "//tests/uv/uv:fake_bar", "foo": "//tests/uv/uv:fake_foof", }, - # We expect bar to not be effective here + # We expect foo to take precedence over bar names = [ - "foo", "bar", + "foo", ], target_compatible_with = { "bar": [ From f2b8ad7c71f6c9afc24020c34d2988e6afdebad6 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:16:16 +0900 Subject: [PATCH 43/46] add a test for non-root modules --- python/uv/private/toolchains_hub.bzl | 34 ++++++++-------- python/uv/private/uv_toolchains_repo.bzl | 3 +- tests/uv/uv/uv_tests.bzl | 51 ++++++++++++++++++++++++ tests/uv/uv_toolchains/BUILD.bazel | 11 ++--- 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/python/uv/private/toolchains_hub.bzl b/python/uv/private/toolchains_hub.bzl index f93239cc2e..a32206d08d 100644 --- a/python/uv/private/toolchains_hub.bzl +++ b/python/uv/private/toolchains_hub.bzl @@ -19,7 +19,7 @@ load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") def toolchains_hub( *, name = None, - names, + toolchains, implementations, target_compatible_with, target_settings): @@ -29,40 +29,38 @@ def toolchains_hub( TODO @aignas 2025-03-09: see if this can be reused in the python toolchains. Args: - name: Unused. - names: The names for toolchain targets. The later occurring items take - precedence over the previous items if they match the target platform. + 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(names) != len(implementations): + if len(toolchains) != len(implementations): fail("Each name must have an implementation") - # We are setting the order of the toolchains so that the later coming - # toolchains override the previous definitions using the toolchain - # resolution properties: + # 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. - prefix_len = len(str(len(names))) + # + # Add 1 so that there is always a leading zero + prefix_len = len(str(len(toolchains))) + 1 prefix = "0" * (prefix_len - 1) - # reverse the names list so that the later items override earlier toolchain - # registrations. - names = [n for _, n in sorted(enumerate(names), key = lambda x: -x[0])] - - for i, name in enumerate(names): + 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), - target_compatible_with = target_compatible_with.get(name, []), - target_settings = target_settings.get(name, []), - toolchain = implementations[name], + 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_toolchains_repo.bzl b/python/uv/private/uv_toolchains_repo.bzl index 8b2374f82a..7e11e0adb6 100644 --- a/python/uv/private/uv_toolchains_repo.bzl +++ b/python/uv/private/uv_toolchains_repo.bzl @@ -29,7 +29,8 @@ def _toolchains_repo_impl(repository_ctx): contents = _TEMPLATE.format( render.call( "toolchains_hub", - names = render.list(repository_ctx.attr.toolchain_names), + name = repr("uv_toolchain"), + toolchains = render.list(repository_ctx.attr.toolchain_names), implementations = render.dict( repository_ctx.attr.toolchain_implementations, ), diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index e674043244..7476d2ebdb 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -436,6 +436,57 @@ def _test_complex_configuring(env): _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) + _analysis_tests = [] def _test_toolchain_precedence(name): diff --git a/tests/uv/uv_toolchains/BUILD.bazel b/tests/uv/uv_toolchains/BUILD.bazel index 05f3eb9367..4e2a12dcae 100644 --- a/tests/uv/uv_toolchains/BUILD.bazel +++ b/tests/uv/uv_toolchains/BUILD.bazel @@ -1,15 +1,11 @@ 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", }, - # We expect foo to take precedence over bar - names = [ - "bar", - "foo", - ], target_compatible_with = { "bar": [ "@platforms//os:linux", @@ -21,4 +17,9 @@ toolchains_hub( ], }, target_settings = {}, + # We expect foo to take precedence over bar + toolchains = [ + "foo", + "bar", + ], ) From c2f76d852274bcc368cfce2fcee349a3976db907 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:32:28 +0900 Subject: [PATCH 44/46] ensure rules_python never overrides settings from the root module --- python/uv/private/uv.bzl | 43 ++++++++++++++++++------------ tests/uv/uv/uv_tests.bzl | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 9cd3392c23..7e7eaeeb2c 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -132,31 +132,40 @@ for a particular version. }, ) -def _configure(config, *, platform, compatible_with, target_settings, urls = [], sha256 = "", **values): +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 + if not override and config.get(key): + continue + config[key] = value config.setdefault("platforms", {}) - if platform and not (compatible_with or target_settings or urls): - config["platforms"].pop(platform) - elif platform: - if compatible_with or target_settings: - config["platforms"][platform] = struct( - name = platform.replace("-", "_").lower(), - compatible_with = compatible_with, - target_settings = target_settings, - ) - if urls: - config.setdefault("urls", {})[platform] = struct( - sha256 = sha256, - urls = urls, - ) + 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: - fail("`platform` name must be specified when specifying `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, @@ -205,6 +214,7 @@ def process_modules( platform = tag.platform, compatible_with = tag.compatible_with, target_settings = tag.target_settings, + override = mod.is_root, ) for key in [ @@ -259,6 +269,7 @@ def process_modules( target_settings = tag.target_settings, sha256 = tag.sha256, urls = tag.urls, + override = mod.is_root, ) if not versions: diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index 7476d2ebdb..53ac43541b 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -487,6 +487,62 @@ def _test_non_rules_python_non_root_is_ignored(env): _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): From 3cd8e4d8100148d16dda412503a05891b293cabf Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 11 Mar 2025 07:23:14 +0000 Subject: [PATCH 45/46] address review comments --- python/uv/private/toolchains_hub.bzl | 3 +- python/uv/private/uv.bzl | 5 ++-- python/uv/private/uv_toolchain.bzl | 1 + tests/uv/uv/uv_tests.bzl | 6 ++-- tests/uv/uv_toolchain_info_subject.bzl | 39 -------------------------- 5 files changed, 7 insertions(+), 47 deletions(-) delete mode 100644 tests/uv/uv_toolchain_info_subject.bzl diff --git a/python/uv/private/toolchains_hub.bzl b/python/uv/private/toolchains_hub.bzl index a32206d08d..b39d84f0c2 100644 --- a/python/uv/private/toolchains_hub.bzl +++ b/python/uv/private/toolchains_hub.bzl @@ -18,12 +18,11 @@ load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") def toolchains_hub( *, - name = None, + name, toolchains, implementations, target_compatible_with, target_settings): - # @unnamed-macro """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. diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 7e7eaeeb2c..55a05be032 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -91,7 +91,8 @@ configure = tag_class( 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 or the version from the defaults is used. +last {obj}`uv.configure` call in the current module, or the version from the +defaults is used. Complex configuration example: ```starlark @@ -370,7 +371,7 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename Example manifest url: https://github.com/astral-sh/uv/releases/download/0.6.5/dist-manifest.json - The example format is as bellow + The example format is as below dist_version "0.28.0" announcement_tag "0.6.5" diff --git a/python/uv/private/uv_toolchain.bzl b/python/uv/private/uv_toolchain.bzl index d4b45fb9df..b740fc304d 100644 --- a/python/uv/private/uv_toolchain.bzl +++ b/python/uv/private/uv_toolchain.bzl @@ -30,6 +30,7 @@ def _uv_toolchain_impl(ctx): uv_toolchain_info = UvToolchainInfo( uv = uv, version = ctx.attr.version, + # Exposed for testing/debugging label = ctx.label, ) diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index 53ac43541b..d784dd8ba6 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -20,7 +20,6 @@ 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 -load("//tests/uv:uv_toolchain_info_subject.bzl", "uv_toolchain_info_subject") _tests = [] @@ -561,10 +560,9 @@ def _test_toolchain_precedence(name): 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 = uv_toolchain_info_subject, + UvToolchainInfo, factory = lambda v, meta: v ) - env.expect.that_str(str(uv_info.actual.label)).contains("//tests/uv/uv:fake_foof") + env.expect.that_str(str(uv_info.label)).contains("//tests/uv/uv:fake_foof") _analysis_tests.append(_test_toolchain_precedence) diff --git a/tests/uv/uv_toolchain_info_subject.bzl b/tests/uv/uv_toolchain_info_subject.bzl deleted file mode 100644 index 83e45ff429..0000000000 --- a/tests/uv/uv_toolchain_info_subject.bzl +++ /dev/null @@ -1,39 +0,0 @@ -# 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. -"""UvToolchainInfo testing subject.""" - -def uv_toolchain_info_subject(info, *, meta): - """Creates a new `CcInfoSubject` for a CcInfo provider instance. - - Args: - info: The CcInfo object. - meta: ExpectMeta object. - - Returns: - A `CcInfoSubject` struct. - """ - - # buildifier: disable=uninitialized - public = struct( - # go/keep-sorted start - actual = info, - # go/keep-sorted end - ) - - # buildifier: @unused - self = struct( - actual = info, - meta = meta, - ) - return public From c2ed9aed652f365ec143d1cc56b15c04e98ea7ed Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 11 Mar 2025 07:23:39 +0000 Subject: [PATCH 46/46] buildifier --- tests/uv/uv/uv_tests.bzl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index d784dd8ba6..bf0deefa88 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -560,7 +560,8 @@ def _test_toolchain_precedence(name): 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 + UvToolchainInfo, + factory = lambda v, meta: v, ) env.expect.that_str(str(uv_info.label)).contains("//tests/uv/uv:fake_foof")