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__":