diff --git a/CHANGELOG.md b/CHANGELOG.md index e59d225189..7ecdbfa9f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,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. * (pypi) An extra argument to add the interpreter lib dir to `LDFLAGS` when building wheels from `sdist`. diff --git a/MODULE.bazel b/MODULE.bazel index 3d7c3042a5..dc2193cec2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -174,16 +174,83 @@ use_repo( "build_bazel_bazel_self", ) -# EXPERIMENTAL: This is experimental and may be removed without notice -uv = use_extension( +# TODO @aignas 2025-01-27: should this be moved to `//python/extensions:uv.bzl` or should +# it stay as it is? I think I may prefer to move it. +uv = use_extension("//python/uv:uv.bzl", "uv") + +# Here is how we can define platforms for the `uv` binaries - this will affect +# all of the downstream callers because we are using the extension without +# `dev_dependency = True`. +uv.default( + base_url = "https://github.com/astral-sh/uv/releases/download", + manifest_filename = "dist-manifest.json", + version = "0.6.3", +) +uv.default( + compatible_with = [ + "@platforms//os:macos", + "@platforms//cpu:aarch64", + ], + platform = "aarch64-apple-darwin", +) +uv.default( + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:aarch64", + ], + platform = "aarch64-unknown-linux-gnu", +) +uv.default( + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:ppc", + ], + platform = "powerpc64-unknown-linux-gnu", +) +uv.default( + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:ppc64le", + ], + platform = "powerpc64le-unknown-linux-gnu", +) +uv.default( + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:s390x", + ], + platform = "s390x-unknown-linux-gnu", +) +uv.default( + compatible_with = [ + "@platforms//os:macos", + "@platforms//cpu:x86_64", + ], + platform = "x86_64-apple-darwin", +) +uv.default( + compatible_with = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], + platform = "x86_64-pc-windows-msvc", +) +uv.default( + compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + platform = "x86_64-unknown-linux-gnu", +) +use_repo(uv, "uv") + +register_toolchains("@uv//:all") + +uv_dev = use_extension( "//python/uv:uv.bzl", "uv", dev_dependency = True, ) -uv.toolchain(uv_version = "0.4.25") -use_repo(uv, "uv_toolchains") - -register_toolchains( - "@uv_toolchains//:all", - dev_dependency = True, +uv_dev.configure( + version = "0.6.2", ) diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index eaed078d63..69e384e42b 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -101,12 +101,15 @@ 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 -uv = use_extension("@rules_python//python/uv:uv.bzl", "uv") -uv.toolchain(uv_version = "0.4.25") -use_repo(uv, "uv_toolchains") - -register_toolchains("@uv_toolchains//:all") +# EXPERIMENTAL: This is experimental and may be changed or removed without notice +uv = use_extension( + "@rules_python//python/uv:uv.bzl", + "uv", + # Use `dev_dependency` so that the toolchains are not defined pulled when your + # module is used elsewhere. + dev_dependency = True, +) +uv.configure(version = "0.6.2") # This extension allows a user to create modifications to how rules_python # creates different wheel repositories. Different attributes allow the user diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel index 006c856d02..acf2a9c1f7 100644 --- a/python/uv/private/BUILD.bazel +++ b/python/uv/private/BUILD.bazel @@ -47,20 +47,19 @@ bzl_library( name = "uv_bzl", srcs = ["uv.bzl"], visibility = ["//python/uv:__subpackages__"], - deps = [":uv_repositories_bzl"], -) - -bzl_library( - name = "uv_repositories_bzl", - srcs = ["uv_repositories.bzl"], - visibility = ["//python/uv:__subpackages__"], deps = [ ":toolchain_types_bzl", + ":uv_repository_bzl", ":uv_toolchains_repo_bzl", - ":versions_bzl", ], ) +bzl_library( + name = "uv_repository_bzl", + srcs = ["uv_repository.bzl"], + visibility = ["//python/uv:__subpackages__"], +) + bzl_library( name = "uv_toolchain_bzl", srcs = ["uv_toolchain.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/toolchains_hub.bzl b/python/uv/private/toolchains_hub.bzl new file mode 100644 index 0000000000..b39d84f0c2 --- /dev/null +++ b/python/uv/private/toolchains_hub.bzl @@ -0,0 +1,65 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A macro used from the uv_toolchain hub repo.""" + +load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") + +def toolchains_hub( + *, + name, + toolchains, + implementations, + target_compatible_with, + target_settings): + """Define the toolchains so that the lexicographical order registration is deterministic. + + TODO @aignas 2025-03-09: see if this can be reused in the python toolchains. + + Args: + name: The prefix to all of the targets, which goes after a numeric prefix. + toolchains: The toolchain names for the targets defined by this macro. + The earlier occurring items take precedence over the later items if + they match the target platform and target settings. + implementations: The name to label mapping. + target_compatible_with: The name to target_compatible_with list mapping. + target_settings: The name to target_settings list mapping. + """ + if len(toolchains) != len(implementations): + fail("Each name must have an implementation") + + # We are defining the toolchains so that the order of toolchain matching is + # the same as the order of the toolchains, because: + # * the toolchains are matched by target settings and target_compatible_with + # * the first toolchain satisfying the above wins + # + # this means we need to register the toolchains prefixed with a number of + # format 00xy, where x and y are some digits and the leading zeros to + # ensure lexicographical sorting. + # + # Add 1 so that there is always a leading zero + prefix_len = len(str(len(toolchains))) + 1 + prefix = "0" * (prefix_len - 1) + + for i, toolchain in enumerate(toolchains): + # prefix with a prefix and then truncate the string. + number_prefix = "{}{}".format(prefix, i)[-prefix_len:] + + native.toolchain( + name = "{}_{}_{}".format(number_prefix, name, toolchain), + target_compatible_with = target_compatible_with.get(toolchain, []), + target_settings = target_settings.get(toolchain, []), + toolchain = implementations[toolchain], + toolchain_type = UV_TOOLCHAIN_TYPE, + ) diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 886e7fe748..55a05be032 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -18,36 +18,480 @@ EXPERIMENTAL: This is experimental and may be removed without notice A module extension for working with uv. """ -load(":uv_repositories.bzl", "uv_repositories") +load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") +load(":uv_repository.bzl", "uv_repository") +load(":uv_toolchains_repo.bzl", "uv_toolchains_repo") _DOC = """\ A module extension for working with uv. + +Basic usage: +```starlark +uv = use_extension( + "@rules_python//python/uv:uv.bzl", + "uv", + # Use `dev_dependency` so that the toolchains are not defined pulled when + # your module is used elsewhere. + dev_dependency = True, +) +uv.configure(version = "0.5.24") +``` + +Since this is only for locking the requirements files, it should be always +marked as a `dev_dependency`. """ -uv_toolchain = tag_class( - doc = "Configure uv toolchain for lock file generation.", - attrs = { - "uv_version": attr.string(doc = "Explicit version of uv.", mandatory = True), +_DEFAULT_ATTRS = { + "base_url": attr.string( + doc = """\ +Base URL to download metadata about the binaries and the binaries themselves. +""", + ), + "compatible_with": attr.label_list( + doc = """\ +The compatible with constraint values for toolchain resolution. +""", + ), + "manifest_filename": attr.string( + doc = """\ +The distribution manifest filename to use for the metadata fetching from GH. The +defaults for this are set in `rules_python` MODULE.bazel file that one can override +for a specific version. +""", + default = "dist-manifest.json", + ), + "platform": attr.string( + doc = """\ +The platform string used in the UV repository to denote the platform triple. +""", + ), + "target_settings": attr.label_list( + doc = """\ +The `target_settings` to add to platform definitions that then get used in `toolchain` +definitions. +""", + ), + "version": attr.string( + doc = """\ +The version of uv to configure the sources for. If this is not specified it will be the +last version used in the module or the default version set by `rules_python`. +""", + ), +} + +default = tag_class( + doc = """\ +Set the uv configuration defaults. +""", + attrs = _DEFAULT_ATTRS, +) + +configure = tag_class( + doc = """\ +Build the `uv` toolchain configuration by appending the provided configuration. +The information is appended to the version configuration that is specified by +{attr}`version` attribute, or if the version is unspecified, the version of the +last {obj}`uv.configure` call in the current module, or the version from the +defaults is used. + +Complex configuration example: +```starlark +# Configure the base_url for the default version. +uv.configure(base_url = "my_mirror") + +# Add an extra platform that can be used with your version. +uv.configure( + platform = "extra-platform", + target_settings = ["//my_config_setting_label"], + compatible_with = ["@platforms//os:exotic"], +) + +# Add an extra platform that can be used with your version. +uv.configure( + platform = "patched-binary", + target_settings = ["//my_super_config_setting"], + urls = ["https://example.zip"], + sha256 = "deadbeef", +) +``` +""", + attrs = _DEFAULT_ATTRS | { + "sha256": attr.string( + doc = "The sha256 of the downloaded artifact if the {attr}`urls` is specified.", + ), + "urls": attr.string_list( + doc = """\ +The urls to download the binary from. If this is used, {attr}`base_url` and +{attr}`manifest_name` are ignored for the given version. + +::::note +If the `urls` are specified, they need to be specified for all of the platforms +for a particular version. +:::: +""", + ), }, ) -def _uv_toolchain_extension(module_ctx): +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 not platform: + if compatible_with or target_settings or urls: + fail("`platform` name must be specified when specifying `compatible_with`, `target_settings` or `urls`") + elif compatible_with or target_settings: + if not override and config.get("platforms", {}).get(platform): + return + + config["platforms"][platform] = struct( + name = platform.replace("-", "_").lower(), + compatible_with = compatible_with, + target_settings = target_settings, + ) + elif urls: + if not override and config.get("urls", {}).get(platform): + return + + config.setdefault("urls", {})[platform] = struct( + sha256 = sha256, + urls = urls, + ) + else: + config["platforms"].pop(platform) + +def process_modules( + module_ctx, + hub_name = "uv", + uv_repository = uv_repository, + toolchain_type = str(UV_TOOLCHAIN_TYPE), + hub_repo = uv_toolchains_repo): + """Parse the modules to get the config for 'uv' toolchains. + + Args: + module_ctx: the context. + hub_name: the name of the hub repository. + uv_repository: the rule to create a uv_repository override. + toolchain_type: the toolchain type to use here. + hub_repo: the hub repo factory function to use. + + Returns: + the result of the hub_repo. Mainly used for tests. + """ + + # default values to apply for version specific config + defaults = { + "base_url": "", + "manifest_filename": "", + "platforms": { + # The structure is as follows: + # "platform_name": struct( + # compatible_with = [], + # target_settings = [], + # ), + # + # NOTE: urls and sha256 cannot be set in defaults + }, + "version": "", + } for mod in module_ctx.modules: - 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, + if not (mod.is_root or mod.name == "rules_python"): + continue + + for tag in mod.tags.default: + _configure( + defaults, + version = tag.version, + base_url = tag.base_url, + manifest_filename = tag.manifest_filename, + platform = tag.platform, + compatible_with = tag.compatible_with, + target_settings = tag.target_settings, + override = mod.is_root, + ) + + for key in [ + "version", + "manifest_filename", + "platforms", + ]: + if not defaults.get(key, None): + fail("defaults need to be set for '{}'".format(key)) + + # resolved per-version configuration. The shape is something like: + # versions = { + # "1.0.0": { + # "base_url": "", + # "manifest_filename": "", + # "platforms": { + # "platform_name": struct( + # compatible_with = [], + # target_settings = [], + # urls = [], # can be unset + # sha256 = "", # can be unset + # ), + # }, + # }, + # } + versions = {} + for mod in module_ctx.modules: + if not (mod.is_root or mod.name == "rules_python"): + continue + + # last_version is the last version used in the MODULE.bazel or the default + last_version = None + for tag in mod.tags.configure: + last_version = tag.version or last_version or defaults["version"] + specific_config = versions.setdefault( + last_version, + { + "base_url": defaults["base_url"], + "manifest_filename": defaults["manifest_filename"], + # shallow copy is enough as the values are structs and will + # be replaced on modification + "platforms": dict(defaults["platforms"]), + }, + ) + + _configure( + specific_config, + base_url = tag.base_url, + manifest_filename = tag.manifest_filename, + platform = tag.platform, + compatible_with = tag.compatible_with, + target_settings = tag.target_settings, + sha256 = tag.sha256, + urls = tag.urls, + override = mod.is_root, ) + if not versions: + return hub_repo( + name = hub_name, + toolchain_type = toolchain_type, + toolchain_names = ["none"], + toolchain_implementations = { + # NOTE @aignas 2025-02-24: the label to the toolchain can be anything + "none": str(Label("//python:none")), + }, + toolchain_compatible_with = { + "none": ["@platforms//:incompatible"], + }, + toolchain_target_settings = {}, + ) + + toolchain_names = [] + toolchain_implementations = {} + toolchain_compatible_with_by_toolchain = {} + toolchain_target_settings = {} + for version, config in versions.items(): + platforms = config["platforms"] + + # Use the manually specified urls + urls = { + platform: src + for platform, src in config.get("urls", {}).items() + if src.urls + } + + # Or fallback to fetching them from GH manifest file + # Example file: https://github.com/astral-sh/uv/releases/download/0.6.3/dist-manifest.json + if not urls: + urls = _get_tool_urls_from_dist_manifest( + module_ctx, + base_url = "{base_url}/{version}".format( + version = version, + base_url = config["base_url"], + ), + manifest_filename = config["manifest_filename"], + platforms = sorted(platforms), + ) + + for platform_name, platform in platforms.items(): + if platform_name not in urls: + continue + + toolchain_name = "{}_{}".format(version.replace(".", "_"), platform_name.lower().replace("-", "_")) + uv_repository_name = "{}_{}".format(hub_name, toolchain_name) + uv_repository( + name = uv_repository_name, + version = version, + platform = platform_name, + urls = urls[platform_name].urls, + sha256 = urls[platform_name].sha256, + ) + + toolchain_names.append(toolchain_name) + toolchain_implementations[toolchain_name] = "@{}//:uv_toolchain".format(uv_repository_name) + toolchain_compatible_with_by_toolchain[toolchain_name] = [ + str(label) + for label in platform.compatible_with + ] + if platform.target_settings: + toolchain_target_settings[toolchain_name] = [ + str(label) + for label in platform.target_settings + ] + + return hub_repo( + name = hub_name, + toolchain_type = toolchain_type, + toolchain_names = toolchain_names, + toolchain_implementations = toolchain_implementations, + toolchain_compatible_with = toolchain_compatible_with_by_toolchain, + toolchain_target_settings = toolchain_target_settings, + ) + +def _uv_toolchain_extension(module_ctx): + process_modules( + module_ctx, + hub_name = "uv", + ) + +def _overlap(first_collection, second_collection): + for x in first_collection: + if x in second_collection: + return True + + return False + +def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename, platforms): + """Download the results about remote tool sources. + + This relies on the tools using the cargo packaging to infer the actual + sha256 values for each binary. + + Example manifest url: https://github.com/astral-sh/uv/releases/download/0.6.5/dist-manifest.json + + The example format is as below + + dist_version "0.28.0" + announcement_tag "0.6.5" + announcement_tag_is_implicit false + announcement_is_prerelease false + announcement_title "0.6.5" + announcement_changelog "text" + announcement_github_body "MD text" + releases [ + { + app_name "uv" + app_version "0.6.5" + env + install_dir_env_var "UV_INSTALL_DIR" + unmanaged_dir_env_var "UV_UNMANAGED_INSTALL" + disable_update_env_var "UV_DISABLE_UPDATE" + no_modify_path_env_var "UV_NO_MODIFY_PATH" + github_base_url_env_var "UV_INSTALLER_GITHUB_BASE_URL" + ghe_base_url_env_var "UV_INSTALLER_GHE_BASE_URL" + display_name "uv" + display true + artifacts [ + "source.tar.gz" + "source.tar.gz.sha256" + "uv-installer.sh" + "uv-installer.ps1" + "sha256.sum" + "uv-aarch64-apple-darwin.tar.gz" + "uv-aarch64-apple-darwin.tar.gz.sha256" + "... + ] + artifacts + uv-aarch64-apple-darwin.tar.gz + name "uv-aarch64-apple-darwin.tar.gz" + kind "executable-zip" + target_triples [ + "aarch64-apple-darwin" + assets [ + { + id "uv-aarch64-apple-darwin-exe-uv" + name "uv" + path "uv" + kind "executable" + }, + { + id "uv-aarch64-apple-darwin-exe-uvx" + name "uvx" + path "uvx" + kind "executable" + } + ] + checksum "uv-aarch64-apple-darwin.tar.gz.sha256" + uv-aarch64-apple-darwin.tar.gz.sha256 + name "uv-aarch64-apple-darwin.tar.gz.sha256" + kind "checksum" + target_triples [ + "aarch64-apple-darwin" + ] + """ + dist_manifest = module_ctx.path(manifest_filename) + result = module_ctx.download( + base_url + "/" + manifest_filename, + output = dist_manifest, + ) + if not result.success: + fail(result) + dist_manifest = json.decode(module_ctx.read(dist_manifest)) + + artifacts = dist_manifest["artifacts"] + tool_sources = {} + downloads = {} + for fname, artifact in artifacts.items(): + if artifact.get("kind") != "executable-zip": + continue + + checksum = artifacts[artifact["checksum"]] + if not _overlap(checksum["target_triples"], platforms): + # we are not interested in this platform, so skip + continue + + checksum_fname = checksum["name"] + checksum_path = module_ctx.path(checksum_fname) + downloads[checksum_path] = struct( + download = module_ctx.download( + "{}/{}".format(base_url, checksum_fname), + output = checksum_path, + block = False, + ), + archive_fname = fname, + platforms = checksum["target_triples"], + ) + + for checksum_path, download in downloads.items(): + result = download.download.wait() + if not result.success: + fail(result) + + archive_fname = download.archive_fname + + sha256, _, checksummed_fname = module_ctx.read(checksum_path).partition(" ") + checksummed_fname = checksummed_fname.strip(" *\n") + if archive_fname != checksummed_fname: + fail("The checksum is for a different file, expected '{}' but got '{}'".format( + archive_fname, + checksummed_fname, + )) + + for platform in download.platforms: + tool_sources[platform] = struct( + urls = ["{}/{}".format(base_url, archive_fname)], + sha256 = sha256, + ) + + return tool_sources + uv = module_extension( doc = _DOC, implementation = _uv_toolchain_extension, - tag_classes = {"toolchain": uv_toolchain}, + tag_classes = { + "configure": configure, + "default": default, + }, ) diff --git a/python/uv/private/uv_repositories.bzl b/python/uv/private/uv_repositories.bzl deleted file mode 100644 index 24fb9c2447..0000000000 --- a/python/uv/private/uv_repositories.bzl +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright 2024 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -EXPERIMENTAL: This is experimental and may be removed without notice - -Create repositories for uv toolchain dependencies -""" - -load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") -load(":uv_toolchains_repo.bzl", "uv_toolchains_repo") -load(":versions.bzl", "UV_PLATFORMS", "UV_TOOL_VERSIONS") - -UV_BUILD_TMPL = """\ -# Generated by repositories.bzl -load("@rules_python//python/uv:uv_toolchain.bzl", "uv_toolchain") - -uv_toolchain( - name = "uv_toolchain", - uv = "{binary}", - version = "{version}", -) -""" - -def _uv_repo_impl(repository_ctx): - platform = repository_ctx.attr.platform - uv_version = repository_ctx.attr.uv_version - - is_windows = "windows" in platform - - suffix = ".zip" if is_windows else ".tar.gz" - filename = "uv-{platform}{suffix}".format( - platform = platform, - suffix = suffix, - ) - url = "https://github.com/astral-sh/uv/releases/download/{version}/{filename}".format( - version = uv_version, - filename = filename, - ) - if filename.endswith(".tar.gz"): - strip_prefix = filename[:-len(".tar.gz")] - else: - strip_prefix = "" - - repository_ctx.download_and_extract( - url = url, - sha256 = UV_TOOL_VERSIONS[repository_ctx.attr.uv_version][repository_ctx.attr.platform].sha256, - stripPrefix = strip_prefix, - ) - - binary = "uv.exe" if is_windows else "uv" - repository_ctx.file( - "BUILD.bazel", - UV_BUILD_TMPL.format( - binary = binary, - version = uv_version, - ), - ) - -uv_repository = repository_rule( - _uv_repo_impl, - doc = "Fetch external tools needed for uv toolchain", - attrs = { - "platform": attr.string(mandatory = True, values = UV_PLATFORMS.keys()), - "uv_version": attr.string(mandatory = True, values = UV_TOOL_VERSIONS.keys()), - }, -) - -def uv_repositories(name = "uv_toolchains", uv_version = None, register_toolchains = True): - """Convenience macro which does typical toolchain setup - - Skip this macro if you need more control over the toolchain setup. - - Args: - name: {type}`str` The name of the toolchains repo. - uv_version: The uv toolchain version to download. - register_toolchains: If true, repositories will be generated to produce and register `uv_toolchain` targets. - """ - if not uv_version: - fail("uv_version is required") - - toolchain_names = [] - toolchain_labels_by_toolchain = {} - toolchain_compatible_with_by_toolchain = {} - - for platform in UV_PLATFORMS.keys(): - uv_repository_name = UV_PLATFORMS[platform].default_repo_name - - uv_repository( - name = uv_repository_name, - uv_version = uv_version, - platform = platform, - ) - - toolchain_name = uv_repository_name + "_toolchain" - toolchain_names.append(toolchain_name) - toolchain_labels_by_toolchain[toolchain_name] = "@{}//:uv_toolchain".format(uv_repository_name) - toolchain_compatible_with_by_toolchain[toolchain_name] = UV_PLATFORMS[platform].compatible_with - - uv_toolchains_repo( - name = name, - toolchain_type = str(UV_TOOLCHAIN_TYPE), - toolchain_names = toolchain_names, - toolchain_labels = toolchain_labels_by_toolchain, - toolchain_compatible_with = toolchain_compatible_with_by_toolchain, - ) - - if register_toolchains: - native.register_toolchains("@{}/:all".format(name)) diff --git a/python/uv/private/uv_repository.bzl b/python/uv/private/uv_repository.bzl new file mode 100644 index 0000000000..ba7d2a766c --- /dev/null +++ b/python/uv/private/uv_repository.bzl @@ -0,0 +1,74 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +EXPERIMENTAL: This is experimental and may be removed without notice + +Create repositories for uv toolchain dependencies +""" + +UV_BUILD_TMPL = """\ +# Generated by repositories.bzl +load("@rules_python//python/uv:uv_toolchain.bzl", "uv_toolchain") + +uv_toolchain( + name = "uv_toolchain", + uv = "{binary}", + version = "{version}", +) +""" + +def _uv_repo_impl(repository_ctx): + platform = repository_ctx.attr.platform + + is_windows = "windows" in platform + _, _, filename = repository_ctx.attr.urls[0].rpartition("/") + if filename.endswith(".tar.gz"): + strip_prefix = filename[:-len(".tar.gz")] + else: + strip_prefix = "" + + result = repository_ctx.download_and_extract( + url = repository_ctx.attr.urls, + sha256 = repository_ctx.attr.sha256, + stripPrefix = strip_prefix, + ) + + binary = "uv.exe" if is_windows else "uv" + repository_ctx.file( + "BUILD.bazel", + UV_BUILD_TMPL.format( + binary = binary, + version = repository_ctx.attr.version, + ), + ) + + return { + "name": repository_ctx.attr.name, + "platform": repository_ctx.attr.platform, + "sha256": result.sha256, + "urls": repository_ctx.attr.urls, + "version": repository_ctx.attr.version, + } + +uv_repository = repository_rule( + _uv_repo_impl, + doc = "Fetch external tools needed for uv toolchain", + attrs = { + "platform": attr.string(mandatory = True), + "sha256": attr.string(mandatory = False), + "urls": attr.string_list(mandatory = True), + "version": attr.string(mandatory = True), + }, +) diff --git a/python/uv/private/uv_toolchain.bzl b/python/uv/private/uv_toolchain.bzl index 3b51f5f533..b740fc304d 100644 --- a/python/uv/private/uv_toolchain.bzl +++ b/python/uv/private/uv_toolchain.bzl @@ -30,6 +30,8 @@ def _uv_toolchain_impl(ctx): uv_toolchain_info = UvToolchainInfo( uv = uv, version = ctx.attr.version, + # Exposed for testing/debugging + label = ctx.label, ) # Export all the providers inside our ToolchainInfo diff --git a/python/uv/private/uv_toolchain_info.bzl b/python/uv/private/uv_toolchain_info.bzl index 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 9a8858f1b0..7e11e0adb6 100644 --- a/python/uv/private/uv_toolchains_repo.bzl +++ b/python/uv/private/uv_toolchains_repo.bzl @@ -16,37 +16,44 @@ load("//python/private:text_util.bzl", "render") -_TOOLCHAIN_TEMPLATE = """ -toolchain( - name = "{name}", - target_compatible_with = {compatible_with}, - toolchain = "{toolchain_label}", - toolchain_type = "{toolchain_type}", -) -""" +_TEMPLATE = """\ +load("@rules_python//python/uv/private:toolchains_hub.bzl", "toolchains_hub") -def _toolchains_repo_impl(repository_ctx): - build_content = "" - for toolchain_name in repository_ctx.attr.toolchain_names: - toolchain_label = repository_ctx.attr.toolchain_labels[toolchain_name] - toolchain_compatible_with = repository_ctx.attr.toolchain_compatible_with[toolchain_name] +{} +""" - build_content += _TOOLCHAIN_TEMPLATE.format( - name = toolchain_name, - toolchain_type = repository_ctx.attr.toolchain_type, - toolchain_label = toolchain_label, - compatible_with = render.list(toolchain_compatible_with), - ) +def _non_empty(d): + return {k: v for k, v in d.items() if v} - repository_ctx.file("BUILD.bazel", build_content) +def _toolchains_repo_impl(repository_ctx): + contents = _TEMPLATE.format( + render.call( + "toolchains_hub", + name = repr("uv_toolchain"), + toolchains = render.list(repository_ctx.attr.toolchain_names), + implementations = render.dict( + repository_ctx.attr.toolchain_implementations, + ), + target_compatible_with = render.dict( + repository_ctx.attr.toolchain_compatible_with, + value_repr = render.list, + ), + target_settings = render.dict( + _non_empty(repository_ctx.attr.toolchain_target_settings), + value_repr = render.list, + ), + ), + ) + repository_ctx.file("BUILD.bazel", contents) uv_toolchains_repo = repository_rule( _toolchains_repo_impl, doc = "Generates a toolchain hub repository", attrs = { "toolchain_compatible_with": attr.string_list_dict(doc = "A list of platform constraints for this toolchain, keyed by toolchain name.", mandatory = True), - "toolchain_labels": attr.string_dict(doc = "The name of the toolchain implementation target, keyed by toolchain name.", mandatory = True), + "toolchain_implementations": attr.string_dict(doc = "The name of the toolchain implementation target, keyed by toolchain name.", mandatory = True), "toolchain_names": attr.string_list(doc = "List of toolchain names", mandatory = True), + "toolchain_target_settings": attr.string_list_dict(doc = "A list of target_settings constraints for this toolchain, keyed by toolchain name.", mandatory = True), "toolchain_type": attr.string(doc = "The toolchain type of the toolchains", mandatory = True), }, ) diff --git a/python/uv/private/versions.bzl b/python/uv/private/versions.bzl deleted file mode 100644 index 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", - ), - }, -} 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/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..bf0deefa88 --- /dev/null +++ b/tests/uv/uv/uv_tests.bzl @@ -0,0 +1,592 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "subjects") +load("//python/uv:uv_toolchain_info.bzl", "UvToolchainInfo") +load("//python/uv/private:uv.bzl", "process_modules") # buildifier: disable=bzl-visibility +load("//python/uv/private:uv_toolchain.bzl", "uv_toolchain") # buildifier: disable=bzl-visibility + +_tests = [] + +def _mock_mctx(*modules, download = None, read = None): + # Here we construct a fake minimal manifest file that we use to mock what would + # be otherwise read from GH files + manifest_files = { + "different.json": { + x: { + "checksum": x + ".sha256", + "kind": "executable-zip", + } + for x in ["linux", "osx"] + } | { + x + ".sha256": { + "name": x + ".sha256", + "target_triples": [x], + } + for x in ["linux", "osx"] + }, + "manifest.json": { + x: { + "checksum": x + ".sha256", + "kind": "executable-zip", + } + for x in ["linux", "os", "osx", "something_extra"] + } | { + x + ".sha256": { + "name": x + ".sha256", + "target_triples": [x], + } + for x in ["linux", "os", "osx", "something_extra"] + }, + } + + fake_fs = { + "linux.sha256": "deadbeef linux", + "os.sha256": "deadbeef os", + "osx.sha256": "deadb00f osx", + } | { + fname: json.encode({"artifacts": contents}) + for fname, contents in manifest_files.items() + } + + return struct( + path = str, + download = download or (lambda *_, **__: struct( + success = True, + wait = lambda: struct( + success = True, + ), + )), + read = read or (lambda x: fake_fs[x]), + modules = [ + struct( + name = modules[0].name, + tags = modules[0].tags, + is_root = modules[0].is_root, + ), + ] + [ + struct( + name = mod.name, + tags = mod.tags, + is_root = False, + ) + for mod in modules[1:] + ], + ) + +def _mod(*, name = None, default = [], configure = [], is_root = True): + return struct( + name = name, # module_name + tags = struct( + default = default, + configure = configure, + ), + is_root = is_root, + ) + +def _process_modules(env, **kwargs): + result = process_modules(hub_repo = struct, **kwargs) + + return env.expect.that_struct( + struct( + names = result.toolchain_names, + implementations = result.toolchain_implementations, + compatible_with = result.toolchain_compatible_with, + target_settings = result.toolchain_target_settings, + ), + attrs = dict( + names = subjects.collection, + implementations = subjects.dict, + compatible_with = subjects.dict, + target_settings = subjects.dict, + ), + ) + +def _default( + base_url = None, + compatible_with = None, + manifest_filename = None, + platform = None, + target_settings = None, + version = None, + **kwargs): + return struct( + base_url = base_url, + compatible_with = [] + (compatible_with or []), # ensure that the type is correct + manifest_filename = manifest_filename, + platform = platform, + target_settings = [] + (target_settings or []), # ensure that the type is correct + version = version, + **kwargs + ) + +def _configure(urls = None, sha256 = None, **kwargs): + # We have the same attributes + return _default(sha256 = sha256, urls = urls, **kwargs) + +def _test_only_defaults(env): + uv = _process_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + base_url = "https://example.org", + manifest_filename = "manifest.json", + version = "1.0.0", + platform = "some_name", + compatible_with = ["@platforms//:incompatible"], + ), + ], + ), + ), + ) + + # No defined platform means nothing gets registered + uv.names().contains_exactly([ + "none", + ]) + uv.implementations().contains_exactly({ + "none": str(Label("//python:none")), + }) + uv.compatible_with().contains_exactly({ + "none": ["@platforms//:incompatible"], + }) + uv.target_settings().contains_exactly({}) + +_tests.append(_test_only_defaults) + +def _test_manual_url_spec(env): + calls = [] + uv = _process_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + manifest_filename = "manifest.json", + version = "1.0.0", + ), + _default( + platform = "linux", + compatible_with = ["@platforms//os:linux"], + ), + # This will be ignored because urls are passed for some of + # the binaries. + _default( + platform = "osx", + compatible_with = ["@platforms//os:osx"], + ), + ], + configure = [ + _configure( + platform = "linux", + urls = ["https://example.org/download.zip"], + sha256 = "deadbeef", + ), + ], + ), + read = lambda *args, **kwargs: fail(args, kwargs), + ), + uv_repository = lambda **kwargs: calls.append(kwargs), + ) + + uv.names().contains_exactly([ + "1_0_0_linux", + ]) + uv.implementations().contains_exactly({ + "1_0_0_linux": "@uv_1_0_0_linux//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "1_0_0_linux": ["@platforms//os:linux"], + }) + uv.target_settings().contains_exactly({}) + env.expect.that_collection(calls).contains_exactly([ + { + "name": "uv_1_0_0_linux", + "platform": "linux", + "sha256": "deadbeef", + "urls": ["https://example.org/download.zip"], + "version": "1.0.0", + }, + ]) + +_tests.append(_test_manual_url_spec) + +def _test_defaults(env): + calls = [] + uv = _process_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + base_url = "https://example.org", + manifest_filename = "manifest.json", + version = "1.0.0", + platform = "linux", + compatible_with = ["@platforms//os:linux"], + target_settings = ["//:my_flag"], + ), + ], + configure = [ + _configure(), # use defaults + ], + ), + ), + uv_repository = lambda **kwargs: calls.append(kwargs), + ) + + uv.names().contains_exactly([ + "1_0_0_linux", + ]) + uv.implementations().contains_exactly({ + "1_0_0_linux": "@uv_1_0_0_linux//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "1_0_0_linux": ["@platforms//os:linux"], + }) + uv.target_settings().contains_exactly({ + "1_0_0_linux": ["//:my_flag"], + }) + env.expect.that_collection(calls).contains_exactly([ + { + "name": "uv_1_0_0_linux", + "platform": "linux", + "sha256": "deadbeef", + "urls": ["https://example.org/1.0.0/linux"], + "version": "1.0.0", + }, + ]) + +_tests.append(_test_defaults) + +def _test_default_building(env): + calls = [] + uv = _process_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + base_url = "https://example.org", + manifest_filename = "manifest.json", + version = "1.0.0", + ), + _default( + platform = "linux", + compatible_with = ["@platforms//os:linux"], + target_settings = ["//:my_flag"], + ), + _default( + platform = "osx", + compatible_with = ["@platforms//os:osx"], + ), + ], + configure = [ + _configure(), # use defaults + ], + ), + ), + uv_repository = lambda **kwargs: calls.append(kwargs), + ) + + uv.names().contains_exactly([ + "1_0_0_linux", + "1_0_0_osx", + ]) + uv.implementations().contains_exactly({ + "1_0_0_linux": "@uv_1_0_0_linux//:uv_toolchain", + "1_0_0_osx": "@uv_1_0_0_osx//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "1_0_0_linux": ["@platforms//os:linux"], + "1_0_0_osx": ["@platforms//os:osx"], + }) + uv.target_settings().contains_exactly({ + "1_0_0_linux": ["//:my_flag"], + }) + env.expect.that_collection(calls).contains_exactly([ + { + "name": "uv_1_0_0_linux", + "platform": "linux", + "sha256": "deadbeef", + "urls": ["https://example.org/1.0.0/linux"], + "version": "1.0.0", + }, + { + "name": "uv_1_0_0_osx", + "platform": "osx", + "sha256": "deadb00f", + "urls": ["https://example.org/1.0.0/osx"], + "version": "1.0.0", + }, + ]) + +_tests.append(_test_default_building) + +def _test_complex_configuring(env): + calls = [] + uv = _process_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + base_url = "https://example.org", + manifest_filename = "manifest.json", + version = "1.0.0", + platform = "osx", + compatible_with = ["@platforms//os:os"], + ), + ], + configure = [ + _configure(), # use defaults + _configure( + version = "1.0.1", + ), # use defaults + _configure( + version = "1.0.2", + base_url = "something_different", + manifest_filename = "different.json", + ), # use defaults + _configure( + platform = "osx", + compatible_with = ["@platforms//os:different"], + ), + _configure( + version = "1.0.3", + ), + _configure(platform = "osx"), # remove the default + _configure( + platform = "linux", + compatible_with = ["@platforms//os:linux"], + ), + ], + ), + ), + uv_repository = lambda **kwargs: calls.append(kwargs), + ) + + uv.names().contains_exactly([ + "1_0_0_osx", + "1_0_1_osx", + "1_0_2_osx", + "1_0_3_linux", + ]) + uv.implementations().contains_exactly({ + "1_0_0_osx": "@uv_1_0_0_osx//:uv_toolchain", + "1_0_1_osx": "@uv_1_0_1_osx//:uv_toolchain", + "1_0_2_osx": "@uv_1_0_2_osx//:uv_toolchain", + "1_0_3_linux": "@uv_1_0_3_linux//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "1_0_0_osx": ["@platforms//os:os"], + "1_0_1_osx": ["@platforms//os:os"], + "1_0_2_osx": ["@platforms//os:different"], + "1_0_3_linux": ["@platforms//os:linux"], + }) + uv.target_settings().contains_exactly({}) + env.expect.that_collection(calls).contains_exactly([ + { + "name": "uv_1_0_0_osx", + "platform": "osx", + "sha256": "deadb00f", + "urls": ["https://example.org/1.0.0/osx"], + "version": "1.0.0", + }, + { + "name": "uv_1_0_1_osx", + "platform": "osx", + "sha256": "deadb00f", + "urls": ["https://example.org/1.0.1/osx"], + "version": "1.0.1", + }, + { + "name": "uv_1_0_2_osx", + "platform": "osx", + "sha256": "deadb00f", + "urls": ["something_different/1.0.2/osx"], + "version": "1.0.2", + }, + { + "name": "uv_1_0_3_linux", + "platform": "linux", + "sha256": "deadbeef", + "urls": ["https://example.org/1.0.3/linux"], + "version": "1.0.3", + }, + ]) + +_tests.append(_test_complex_configuring) + +def _test_non_rules_python_non_root_is_ignored(env): + calls = [] + uv = _process_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + base_url = "https://example.org", + manifest_filename = "manifest.json", + version = "1.0.0", + platform = "osx", + compatible_with = ["@platforms//os:os"], + ), + ], + configure = [ + _configure(), # use defaults + ], + ), + _mod( + name = "something", + configure = [ + _configure(version = "6.6.6"), # use defaults whatever they are + ], + ), + ), + uv_repository = lambda **kwargs: calls.append(kwargs), + ) + + uv.names().contains_exactly([ + "1_0_0_osx", + ]) + uv.implementations().contains_exactly({ + "1_0_0_osx": "@uv_1_0_0_osx//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "1_0_0_osx": ["@platforms//os:os"], + }) + uv.target_settings().contains_exactly({}) + env.expect.that_collection(calls).contains_exactly([ + { + "name": "uv_1_0_0_osx", + "platform": "osx", + "sha256": "deadb00f", + "urls": ["https://example.org/1.0.0/osx"], + "version": "1.0.0", + }, + ]) + +_tests.append(_test_non_rules_python_non_root_is_ignored) + +def _test_rules_python_does_not_take_precedence(env): + calls = [] + uv = _process_modules( + env, + module_ctx = _mock_mctx( + _mod( + default = [ + _default( + base_url = "https://example.org", + manifest_filename = "manifest.json", + version = "1.0.0", + platform = "osx", + compatible_with = ["@platforms//os:os"], + ), + ], + configure = [ + _configure(), # use defaults + ], + ), + _mod( + name = "rules_python", + configure = [ + _configure( + version = "1.0.0", + base_url = "https://foobar.org", + platform = "osx", + compatible_with = ["@platforms//os:osx"], + ), + ], + ), + ), + uv_repository = lambda **kwargs: calls.append(kwargs), + ) + + uv.names().contains_exactly([ + "1_0_0_osx", + ]) + uv.implementations().contains_exactly({ + "1_0_0_osx": "@uv_1_0_0_osx//:uv_toolchain", + }) + uv.compatible_with().contains_exactly({ + "1_0_0_osx": ["@platforms//os:os"], + }) + uv.target_settings().contains_exactly({}) + env.expect.that_collection(calls).contains_exactly([ + { + "name": "uv_1_0_0_osx", + "platform": "osx", + "sha256": "deadb00f", + "urls": ["https://example.org/1.0.0/osx"], + "version": "1.0.0", + }, + ]) + +_tests.append(_test_rules_python_does_not_take_precedence) + +_analysis_tests = [] + +def _test_toolchain_precedence(name): + analysis_test( + name = name, + impl = _test_toolchain_precedence_impl, + target = "//python/uv:current_toolchain", + config_settings = { + "//command_line_option:extra_toolchains": [ + str(Label("//tests/uv/uv_toolchains:all")), + ], + "//command_line_option:platforms": str(Label("//tests/support:linux_aarch64")), + }, + ) + +def _test_toolchain_precedence_impl(env, target): + # Check that the forwarded UvToolchainInfo looks vaguely correct. + uv_info = env.expect.that_target(target).provider( + UvToolchainInfo, + factory = lambda v, meta: v, + ) + env.expect.that_str(str(uv_info.label)).contains("//tests/uv/uv:fake_foof") + +_analysis_tests.append(_test_toolchain_precedence) + +def uv_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite( + name = name, + basic_tests = _tests, + tests = _analysis_tests, + ) + + uv_toolchain( + name = "fake_bar", + uv = ":BUILD.bazel", + version = "0.0.1", + ) + + uv_toolchain( + name = "fake_foof", + uv = ":BUILD.bazel", + version = "0.0.1", + ) diff --git a/tests/uv/uv_toolchains/BUILD.bazel b/tests/uv/uv_toolchains/BUILD.bazel new file mode 100644 index 0000000000..4e2a12dcae --- /dev/null +++ b/tests/uv/uv_toolchains/BUILD.bazel @@ -0,0 +1,25 @@ +load("//python/uv/private:toolchains_hub.bzl", "toolchains_hub") # buildifier: disable=bzl-visibility + +toolchains_hub( + name = "uv_unit_test", + implementations = { + "bar": "//tests/uv/uv:fake_bar", + "foo": "//tests/uv/uv:fake_foof", + }, + target_compatible_with = { + "bar": [ + "@platforms//os:linux", + "@platforms//cpu:aarch64", + ], + "foo": [ + "@platforms//os:linux", + "@platforms//cpu:aarch64", + ], + }, + target_settings = {}, + # We expect foo to take precedence over bar + toolchains = [ + "foo", + "bar", + ], +)