feat(uv): add @uv//:uv and //python/uv/bin:uv targets to run uv (#4371) Users could not run the Bazel-configured `uv` binary directly because `//python/uv:current_toolchain` had restricted visibility and omitted the underlying `uv` binary from its runfiles and executable symlink flag. Make `//python/uv:current_toolchain` public, mark its symlink executable while preserving any file extension (such as `.exe` on Windows), include the underlying `uv` binary in its runfiles, and expose the `$(UV)` Make variable alongside `$(UV_BIN)`. Expose convenient public aliases at `@uv//:uv` and `//python/uv/bin:uv` so users can invoke `uv` directly via `bazel run`.
diff --git a/docs/api/rules_python/python/uv/bin/index.md b/docs/api/rules_python/python/uv/bin/index.md new file mode 100644 index 0000000..cb3a914 --- /dev/null +++ b/docs/api/rules_python/python/uv/bin/index.md
@@ -0,0 +1,24 @@ +:::{default-domain} bzl +::: +:::{bzl:currentfile} //python/uv/bin:BUILD.bazel +::: + +# //python/uv/bin + +Executable alias for the configured `uv` binary. This target can be run +directly with `bazel run` or passed to tools such as +[`bazel_env.bzl`](https://github.com/buildbuddy-io/bazel_env.bzl) (or via +{obj}`//python/uv:current_toolchain` and `$(UV)`). + +::::{bzl:target} uv + +Alias for {obj}`//python/uv:current_toolchain` to directly run the configured +`uv` binary: + +```sh +bazel run @rules_python//python/uv/bin:uv -- --help +``` + +:::{versionadded} VERSION_NEXT_FEATURE +::: +::::
diff --git a/docs/api/rules_python/python/uv/index.md b/docs/api/rules_python/python/uv/index.md new file mode 100644 index 0000000..6523689 --- /dev/null +++ b/docs/api/rules_python/python/uv/index.md
@@ -0,0 +1,45 @@ +:::{default-domain} bzl +::: +:::{bzl:currentfile} //python/uv:BUILD.bazel +::: + +# //python/uv + +::::{bzl:target} current_toolchain + +Exposes the resolved `uv` toolchain for the target/execution platform as an +executable target and template variable provider (`$(UV)` and `$(UV_BIN)`). +Shorter aliases for this target are also available at +{obj}`//python/uv/bin:uv` and `@uv//:uv`. + +Can be run directly with `bazel run`: + +```sh +bazel run @rules_python//python/uv:current_toolchain -- --help +``` + +Or integrated with +[`bazel_env.bzl`](https://github.com/buildbuddy-io/bazel_env.bzl) to expose +`uv` on your local `PATH`: + +```bzl +bazel_env( + name = "bazel_env", + toolchains = { + "uv": "@rules_python//python/uv:current_toolchain", + }, + tools = { + "uv": "$(UV)", + }, +) +``` + +:::{versionadded} VERSION_NEXT_FEATURE +::: +:::: + +:::{bzl:target} uv_toolchain_type + +Toolchain type identifier for the `uv` toolchain, implemented by +{obj}`uv_toolchain`. +:::
diff --git a/docs/pypi/lock.md b/docs/pypi/lock.md index 727795c..51b5b12 100644 --- a/docs/pypi/lock.md +++ b/docs/pypi/lock.md
@@ -94,6 +94,9 @@ Without `version`, the default version specified by `rules_python` is used. See {obj}`uv.configure` for using a mirror, explicit URLs or extra platforms. +To run the configured `uv` binary directly, run +`bazel run @rules_python//python/uv/bin:uv -- <args>` (or add +`use_repo(uv, "uv")` and run `bazel run @uv//:uv -- <args>`). (uv-workspace-setup)= ##### WORKSPACE
diff --git a/news/uv-bin-target.added.md b/news/uv-bin-target.added.md new file mode 100644 index 0000000..c3142dc --- /dev/null +++ b/news/uv-bin-target.added.md
@@ -0,0 +1,6 @@ +(uv) Added `@uv//:uv`, {obj}`//python/uv/bin:uv`, and public visibility on +{obj}`//python/uv:current_toolchain` (along with the `$(UV)` make variable) to +run the configured `uv` binary directly (e.g. +`bazel run @rules_python//python/uv/bin:uv -- --help` or +`bazel run @uv -- --help`) +([#4371](https://github.com/bazel-contrib/rules_python/pull/4371)).
diff --git a/python/uv/BUILD.bazel b/python/uv/BUILD.bazel index 9551051..e202465 100644 --- a/python/uv/BUILD.bazel +++ b/python/uv/BUILD.bazel
@@ -32,11 +32,7 @@ # Marked manual so that `bazel test //...` passes # even if no toolchain is registered. tags = ["manual"], - # EXPERIMENTAL: Visibility is restricted to allow for changes. - visibility = [ - "//:__subpackages__", - "@rules_python//examples:__subpackages__", - ], + visibility = ["//visibility:public"], ) bzl_library(
diff --git a/python/uv/bin/BUILD.bazel b/python/uv/bin/BUILD.bazel new file mode 100644 index 0000000..e357c70 --- /dev/null +++ b/python/uv/bin/BUILD.bazel
@@ -0,0 +1,14 @@ +load("//python/private:distribution_filegroup.bzl", "distribution_filegroup") + +package(default_visibility = ["//:__subpackages__"]) + +distribution_filegroup( + name = "distribution", +) + +alias( + name = "uv", + actual = "//python/uv:current_toolchain", + tags = ["manual"], + visibility = ["//visibility:public"], +)
diff --git a/python/uv/private/current_toolchain.bzl b/python/uv/private/current_toolchain.bzl index 91a25cb..43b3a30 100644 --- a/python/uv/private/current_toolchain.bzl +++ b/python/uv/private/current_toolchain.bzl
@@ -32,16 +32,29 @@ original_uv_executable = toolchain_info.uv_toolchain_info.uv[DefaultInfo].files_to_run.executable # Use `uv` as the name of the binary to make the help message well formatted - symlink_uv_executable = ctx.actions.declare_file("current_toolchain/uv".format(original_uv_executable.basename)) - ctx.actions.symlink(output = symlink_uv_executable, target_file = original_uv_executable) + if original_uv_executable.extension: + ext = "." + original_uv_executable.extension + else: + ext = "" + symlink_uv_executable = ctx.actions.declare_file( + "{}/uv{}".format(ctx.label.name, ext), + ) + ctx.actions.symlink( + output = symlink_uv_executable, + target_file = original_uv_executable, + is_executable = True, + ) new_default_info = DefaultInfo( files = depset([symlink_uv_executable]), - runfiles = toolchain_info.default_info.default_runfiles, + runfiles = ctx.runfiles(files = [original_uv_executable]).merge( + toolchain_info.default_info.default_runfiles, + ), executable = symlink_uv_executable, ) template_variable_info = platform_common.TemplateVariableInfo({ + "UV": symlink_uv_executable.path, "UV_BIN": symlink_uv_executable.path, })
diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index 1ed9794..32462c0 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl
@@ -36,8 +36,21 @@ uv.configure(version = "0.13.5") ``` +To run `uv` directly: +```sh +bazel run @rules_python//python/uv/bin:uv -- --help +# Or, if `use_repo(uv, "uv")` is in MODULE.bazel: +bazel run @uv//:uv -- --help +``` + Since this is only for locking the requirements files, it should be always marked as a `dev_dependency`. + +:::{versionchanged} VERSION_NEXT_FEATURE +Added `@uv//:uv` and {obj}`//python/uv/bin:uv` (aliases for +{obj}`//python/uv:current_toolchain`) to run the configured `uv` binary +directly. +::: """ _DEFAULT_ATTRS = {
diff --git a/python/uv/private/uv_register_toolchains.bzl b/python/uv/private/uv_register_toolchains.bzl index 1da3011..558d6d4 100644 --- a/python/uv/private/uv_register_toolchains.bzl +++ b/python/uv/private/uv_register_toolchains.bzl
@@ -139,7 +139,8 @@ This is the WORKSPACE equivalent of the {obj}`uv` module extension. It creates one download repository per `uv` version and platform, and the toolchain hub repository `name` with a `toolchain` for each of them, which - it registers as `@{name}//:all`; {obj}`lock` can then be used. See the + it registers as `@{name}//:all`; {obj}`lock` can then be used, and `uv` can + be run directly via `bazel run @{name}//:uv -- <args>`. See the {ref}`WORKSPACE setup guide <uv-workspace-setup>` for usage examples. Platforms without an archive in a manifest get no toolchain. The
diff --git a/python/uv/private/uv_toolchains_repo.bzl b/python/uv/private/uv_toolchains_repo.bzl index 7e11e0a..2e62ad9 100644 --- a/python/uv/private/uv_toolchains_repo.bzl +++ b/python/uv/private/uv_toolchains_repo.bzl
@@ -19,6 +19,12 @@ _TEMPLATE = """\ load("@rules_python//python/uv/private:toolchains_hub.bzl", "toolchains_hub") +alias( + name = "uv", + actual = "@rules_python//python/uv:current_toolchain", + visibility = ["//visibility:public"], +) + {} """
diff --git a/tests/integration/bzlmod_lockfile/MODULE.bazel.lock b/tests/integration/bzlmod_lockfile/MODULE.bazel.lock index f86bf24..f2849f4 100644 --- a/tests/integration/bzlmod_lockfile/MODULE.bazel.lock +++ b/tests/integration/bzlmod_lockfile/MODULE.bazel.lock
@@ -259,7 +259,7 @@ }, "@@rules_python+//python/uv:uv.bzl%uv": { "general": { - "bzlTransitiveDigest": "a11TI1DxtsKzJJnFDIAhAoVNhub4iqECM+h0qTqOT68=", + "bzlTransitiveDigest": "wQHhpqOkC72inE+2jHKImemj6oOnGwbZY66g+HM79aU=", "usagesDigest": "UgEeJTkNLce2eABymcbLBebdHpF0D8qrT6A6PSESotA=", "recordedInputs": [ "REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
diff --git a/tests/uv/toolchain/BUILD.bazel b/tests/uv/toolchain/BUILD.bazel index 9017396..21f6d2a 100644 --- a/tests/uv/toolchain/BUILD.bazel +++ b/tests/uv/toolchain/BUILD.bazel
@@ -3,14 +3,24 @@ genrule( name = "uv_help", outs = ["uv_help.txt"], - cmd = "$(UV_BIN) --python-fetch manual --help >$@", + cmd = 'test "$(UV)" = "$(UV_BIN)" && $(UV) --python-fetch manual --help >$@', toolchains = ["//python/uv:current_toolchain"], ) py_test( name = "uv_help_test", srcs = ["uv_help_test.py"], - data = [":uv_help"], - env = {"DATA": "$(rlocationpath :uv_help)"}, + data = [ + ":uv_help", + "//python/uv:current_toolchain", + "//python/uv/bin:uv", + "@uv", + ], + env = { + "DATA": "$(rlocationpath :uv_help)", + "UV_BIN": "$(rlocationpath @uv//:uv)", + "UV_CURRENT_TOOLCHAIN": "$(rlocationpath //python/uv:current_toolchain)", + "UV_RULES_PYTHON_UV": "$(rlocationpath //python/uv/bin:uv)", + }, deps = ["//python/runfiles"], )
diff --git a/tests/uv/toolchain/uv_help_test.py b/tests/uv/toolchain/uv_help_test.py index c451527..eed10e7 100755 --- a/tests/uv/toolchain/uv_help_test.py +++ b/tests/uv/toolchain/uv_help_test.py
@@ -1,6 +1,7 @@ #!/usr/bin/env python import os +import subprocess import unittest from pathlib import Path @@ -20,7 +21,27 @@ uv_help = Path(uv_help_path).read_text() - self.assertIn("Usage: uv [OPTIONS] <COMMAND>", uv_help) + self.assertRegex(uv_help, r"Usage: uv(\.exe)? \[OPTIONS\] <COMMAND>") + + def test_uv_bin(self): + root = runfiles.CreateOrRaise().root() + + for env_var in ( + "UV_BIN", + "UV_CURRENT_TOOLCHAIN", + "UV_RULES_PYTHON_UV", + ): + with self.subTest(env_var=env_var): + uv_bin_path = root / os.environ[env_var] + result = subprocess.run( + [uv_bin_path, "--python-fetch", "manual", "--help"], + capture_output=True, + text=True, + check=True, + ) + self.assertRegex( + result.stdout, r"Usage: uv(\.exe)? \[OPTIONS\] <COMMAND>" + ) if __name__ == "__main__":