docs(uv): document uv.lock support in locking and download docs (#4354)

`rules_python` supports `uv.lock` in addition to `requirements.txt`,
leaving documentation that states only `requirements.txt` is supported
outdated.

Update the locking and download guides and `lock` rule docstrings to
document both `uv.lock` generation via `uv lock` and `requirements.txt`
generation via `uv pip compile`.
diff --git a/docs/pypi/download.md b/docs/pypi/download.md
index b9904d4..cc8b789 100644
--- a/docs/pypi/download.md
+++ b/docs/pypi/download.md
@@ -19,6 +19,8 @@
     hub_name = "my_deps",
     python_version = "3.13",
     requirements_lock = "//:requirements_lock_3_11.txt",
+    # Or use a uv.lock file:
+    # uv_lock = "//:uv.lock",
 )
 
 use_repo(pip, "my_deps")
diff --git a/docs/pypi/lock.md b/docs/pypi/lock.md
index c723659..78c88c5 100644
--- a/docs/pypi/lock.md
+++ b/docs/pypi/lock.md
@@ -4,7 +4,8 @@
 # Lock
 
 :::{note}
-Currently `rules_python` only supports `requirements.txt` format.
+`rules_python` supports `requirements.txt` and `uv.lock` (bzlmod only)
+formats.
 
 #{gh-issue}`2787` tracks `pylock.toml` support.
 :::
@@ -86,13 +87,13 @@
 
 #### `[tool.uv]` settings from pyproject.toml
 
-When a `pyproject.toml` file is among the {attr}`lock.srcs`, the
+When a `pyproject.toml` file is among the {arg}`lock.srcs`, the
 {obj}`lock` rule auto-detects the project directory and passes
-`--project <dir>` to `uv pip compile`. This causes `uv` to read
+`--project <dir>` to `uv`. This causes `uv` to read
 `[tool.uv]` settings from that `pyproject.toml`, such as
 `no-build-isolation`, `exclude-dependencies`, and workspace members.
 
-If multiple `pyproject.toml` files are in {attr}`lock.srcs`, the one
+If multiple `pyproject.toml` files are in {arg}`lock.srcs`, the one
 with the shortest directory path is selected (this heuristic works for
 typical uv workspace layouts where the root configuration is at the
 shortest path).
@@ -112,11 +113,48 @@
 :::{warning}
 **Known limitations of auto-detection**
 
-1. **Workspace heuristic** — the shortest-path selection may incorrectly assume the upper-most
-   workspace `pyproject.toml` is the correct one. For monorepos with multiple independent
-   sub-projects, you must set `project` explicitly for each {obj}`lock` target.
-1. **No test target** — unlike {obj}`compile_pip_requirements`, no test target is auto-created; see
-   the {obj}`lock` docs for how to add one manually using `diff_test` from `bazel_skylib`.
+1. **Workspace heuristic** — the shortest-path selection may incorrectly assume
+   the upper-most workspace `pyproject.toml` is the correct one. For monorepos
+   with multiple independent sub-projects, you must set `project` explicitly
+   for each {obj}`lock` target.
+1. **No test target** — unlike {obj}`compile_pip_requirements`, no test target
+   is auto-created; see the {obj}`lock` docs for how to add one manually using
+   `diff_test` from `bazel_skylib`.
 :::
 
+## uv.lock (bzlmod only)
+
+The {obj}`lock` rule also supports generating cross-platform `uv.lock` files
+using `uv lock` when {arg}`lock.out` ends with `.lock` (such as `uv.lock`).
+
+### Example usage
+
+In `BUILD.bazel`:
+
+```starlark
+load("@rules_python//python/uv:lock.bzl", "lock")
+
+lock(
+    name = "uv_lock",
+    srcs = ["pyproject.toml"],
+    out = "uv.lock",
+)
+```
+
+Run `bazel run //:uv_lock.update` to generate or update `uv.lock`, then pass it
+to {obj}`pip.parse` in `MODULE.bazel` via {attr}`pip.parse.uv_lock`:
+
+```starlark
+pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
+pip.parse(
+    hub_name = "my_deps",
+    python_version = "3.13",
+    uv_lock = "//:uv.lock",
+)
+use_repo(pip, "my_deps")
+```
+
+`[tool.uv]` settings auto-detection and the {arg}`lock.project` attribute work
+the same way as described in the `uv pip compile` section above.
+
 For more documentation see {obj}`lock`.
diff --git a/python/uv/lock.bzl b/python/uv/lock.bzl
index 8fece73..13610aa 100644
--- a/python/uv/lock.bzl
+++ b/python/uv/lock.bzl
@@ -22,13 +22,14 @@
 - Supports transitions out of the box.
 
 Note, this does not provide a test target like {obj}`compile_pip_requirements` does.
-The `uv pip compile` command is not hermetic and thus a test based on it would most likely be flaky:
+The `uv lock` and `uv pip compile` commands are not hermetic and thus a test
+based on them would most likely be flaky:
 - It may require auth injected into it, so most likely it requires a local tag added so that the bazel action runs without sandboxing.
 - It requires network access.
 
 Given those points, a test target should be an explicit and properly documented target and not a hidden implicit target.
 If, you would like to add a test target that always does the locking automatically to ensure that the
-`requirements.txt` file is up-to-date, add something similar to:
+lock file is up-to-date, add something similar to:
 
 ```starlark
 load("@bazel_skylib//rules:native_binary.bzl", "native_test")
@@ -37,6 +38,7 @@
 lock(
     name = "requirements",
     srcs = ["pyproject.toml"],
+    out = "uv.lock",
 )
 
 native_test(
@@ -47,8 +49,8 @@
 
 ### `[tool.uv]` settings support
 
-When a `pyproject.toml` file is included in {attr}`lock.srcs`, the
-`--project` flag is automatically passed to `uv pip compile` using the
+When a `pyproject.toml` file is included in {arg}`lock.srcs`, the
+`--project` flag is automatically passed to `uv` using the
 directory of the shortest-path `pyproject.toml`. This causes `uv` to
 read `[tool.uv]` settings such as `no-build-isolation`,
 `exclude-dependencies`, and `[tool.uv.workspace]` from that file.
@@ -63,6 +65,7 @@
         "pyproject.toml",
         "requirements.in",
     ],
+    out = "requirements_lock.txt",
     project = "subproject",
 )
 ```
diff --git a/python/uv/private/lock.bzl b/python/uv/private/lock.bzl
index a75c54d..c0ad8bf 100644
--- a/python/uv/private/lock.bzl
+++ b/python/uv/private/lock.bzl
@@ -387,7 +387,7 @@
     ),
     "project": attr.string(
         doc = """\
-Overrides the `--project` directory passed to `uv pip compile`.
+Overrides the `--project` directory passed to `uv`.
 If not set, the project directory is auto-detected: when
 `pyproject.toml` files are in {obj}`lock.srcs`, the one with the
 shortest directory path is selected. This makes `uv` read
@@ -629,12 +629,12 @@
     """Pin the requirements based on the src files.
 
     This macro creates the following targets:
-    - `name`: the target that creates the requirements.txt file in a build
-      action. This target will have `no-cache` and `requires-network` added
-      to its tags.
+    - `name`: the target that creates the lock file (`uv.lock` or
+      `requirements.txt`) in a build action. This target will have `no-cache`
+      and `requires-network` added to its tags.
     - `name.run`: a runnable target that can be used to pass extra parameters
       to the same command that would be run in the `name` action. This will
-      update the source copy of the requirements file. You can customize the
+      update the source copy of the lock file. You can customize the
       args via the command line, but it requires being able to run `uv` (and
       possibly `python`) directly on your host. This target uses the `uv`
       cache and inherits its cache settings from the environment, including
@@ -642,9 +642,10 @@
       data or `--no-cache` to disable caching. Build actions, including those
       used by `name.update`, disable the `uv` cache.
     - `name.update`: a target that can be run to update the source-tree version
-      of the requirements lock file. The output can be fed to the
-      {obj}`pip.parse` bzlmod extension tag class. Note, you can use
-      `native_test` to wrap this target to make a test. You can't customize the
+      of the lock file. The output can be fed to the {obj}`pip.parse` bzlmod
+      extension tag class via {attr}`pip.parse.uv_lock` or
+      {attr}`pip.parse.requirements_lock`. Note, you can use `native_test` to
+      wrap this target to make a test. You can't customize the
       args via command line, but you can use RBE to generate requirements
       (offload execution and run for different platforms). Note, that for RBE
       to be usable, one needs to ensure that the nodes running the action have
@@ -662,8 +663,12 @@
     Args:
         name: {type}`str` The prefix of all targets created by this macro.
         srcs: {type}`list[Label]` The sources that will be used. Add all of the
-            files that would be passed as srcs to the `uv pip compile` command.
-        out: {type}`str` The output file relative to the package.
+            files that would be passed as srcs to `uv pip compile`, or the
+            `pyproject.toml` files when generating `uv.lock`.
+        out: {type}`str` The output file relative to the package. If the file
+            name ends with `.lock` (e.g. `uv.lock`), `uv lock` is used to
+            generate a `uv.lock` file; otherwise `uv pip compile` is used to
+            generate a `requirements.txt`-formatted file.
         args: {type}`list[str]` The list of args to pass to uv. Note, these are
             written into the runnable `name.run` target.
         env: {type}`dict[str, str]` the environment variables to set. Note, this
@@ -683,7 +688,7 @@
             are compiling the requirements file for using it as a constraints
             file. Defaults to `False`.
         project: {type}`str | None` overrides the `--project` directory
-            passed to `uv pip compile`. By default the project directory
+            passed to `uv`. By default the project directory
             is auto-detected: when {obj}`lock.srcs` contains
             `pyproject.toml` files, the one with the shortest directory
             path is selected. This causes `uv` to read `[tool.uv]`