Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .bazelignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ tests/integration/compile_pip_requirements/bazel-compile_pip_requirements
tests/integration/local_toolchains/bazel-local_toolchains
tests/integration/py_cc_toolchain_registered/bazel-py_cc_toolchain_registered
tests/integration/toolchain_target_settings/bazel-module_under_test
tests/integration/uv_lock/bazel-uv_lock
1 change: 1 addition & 0 deletions .bazelrc.deleted_packages
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ common --deleted_packages=tests/integration/pip_parse/empty
common --deleted_packages=tests/integration/pip_parse_isolated
common --deleted_packages=tests/integration/py_cc_toolchain_registered
common --deleted_packages=tests/integration/toolchain_target_settings
common --deleted_packages=tests/integration/uv_lock
common --deleted_packages=tests/modules/another_module
common --deleted_packages=tests/modules/other
common --deleted_packages=tests/modules/other/nspkg_delta
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ END_UNRELEASED_TEMPLATE
* (pypi) `package_metadata` support, fixes
[#2054](https://github.com/bazel-contrib/rules_python/issues/2054).
* (coverage) Add support for python 3.14 and bump `coverage.py` to 7.10.7.
* (uv) allow user overwrite the build environment using `--action_env` to allow
setting authentication for the index URL.
([#3045](https://github.com/bazel-contrib/rules_python/issues/3405))

{#v2-0-2}
## [2.0.2] - 2026-05-14
Expand Down
21 changes: 21 additions & 0 deletions docs/pypi/download.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,27 @@ into whatever HTTP(S) request it performs against `example.com`.

See the [Credential Helper Spec][cred-helper-spec] for more details.

### Using a credential helper with the lock rule
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I'm +1 on changing the bazel run //foo.lock target to run uv directly so that it uses the user's permissions. For casual use, its more convenient than having to setup the mount pair etc

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but if you want a diff_test here, then everything runs as an action, so we would then need to mount the credential helper inside. Maybe the docs should be updated to reflect that better?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah doh, //requirements.run already works the way I described.

Actually, re-reading the uv docs you linked to, since uv has its own credential store, is what needs to be mounted in the sandbox the uv credential store? And then uv just has to be told where that is.


The same credential helper pattern can be used with the {obj}`@rules_python//python/uv:lock.bzl`
`lock` macro. Instead of embedding credentials in `UV_EXTRA_INDEX_URL`, set
`RULES_PYTHON_UV_CREDENTIAL_HELPER` to point to a credential helper script
and use `--sandbox_add_mount_pair` to mount it into the sandbox, alternatively
use the upstream document to set the credential helper up:

```
# .bazelrc
build --sandbox_add_mount_pair=%workspace%/path/to/cred_helper.sh=/cred_helper.sh
build --action_env=RULES_PYTHON_UV_CREDENTIAL_HELPER=/cred_helper.sh
```

The credential helper script follows the same format as the Bazel credential
helper described above — it receives `get` (and optionally the URL) as an
argument and outputs JSON with the `Authorization` header.

This ensures that credentials are never exposed through environment variables.
The script itself is mounted into the sandbox from the host filesystem.

[rfc7617]: https://datatracker.ietf.org/doc/html/rfc7617
[cred-helper-design]: https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md
[cred-helper-spec]: https://github.com/EngFlow/credential-helper-spec/blob/main/spec.md
54 changes: 29 additions & 25 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ babel==2.18.0 \
--hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \
--hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35
# via sphinx
certifi==2026.2.25 \
--hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \
--hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7
certifi==2026.5.20 \
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
# via requests
charset-normalizer==3.4.7 \
--hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \
Expand Down Expand Up @@ -183,9 +183,9 @@ docutils==0.22.4 ; python_full_version >= '3.11' \
# myst-parser
# sphinx
# sphinx-rtd-theme
idna==3.11 \
--hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \
--hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902
idna==3.16 \
--hash=sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5 \
--hash=sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d
# via requests
imagesize==1.5.0 ; python_full_version < '3.10' \
--hash=sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899 \
Expand Down Expand Up @@ -216,9 +216,9 @@ markdown-it-py==3.0.0 ; python_full_version < '3.11' \
# via
# mdit-py-plugins
# myst-parser
markdown-it-py==4.0.0 ; python_full_version >= '3.11' \
--hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \
--hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3
markdown-it-py==4.2.0 ; python_full_version >= '3.11' \
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
# via
# mdit-py-plugins
# myst-parser
Expand Down Expand Up @@ -319,9 +319,9 @@ mdit-py-plugins==0.4.2 ; python_full_version < '3.10' \
--hash=sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636 \
--hash=sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5
# via myst-parser
mdit-py-plugins==0.5.0 ; python_full_version >= '3.10' \
--hash=sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f \
--hash=sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6
mdit-py-plugins==0.6.1 ; python_full_version >= '3.10' \
--hash=sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d \
--hash=sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0
# via myst-parser
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
Expand All @@ -335,13 +335,13 @@ myst-parser==4.0.1 ; python_full_version == '3.10.*' \
--hash=sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4 \
--hash=sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d
# via rules-python-docs (docs/pyproject.toml)
myst-parser==5.0.0 ; python_full_version >= '3.11' \
--hash=sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211 \
--hash=sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a
myst-parser==5.1.0 ; python_full_version >= '3.11' \
--hash=sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a \
--hash=sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02
# via rules-python-docs (docs/pyproject.toml)
packaging==26.1 \
--hash=sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f \
--hash=sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de
packaging==26.2 \
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
# via
# readthedocs-sphinx-ext
# sphinx
Expand Down Expand Up @@ -442,19 +442,19 @@ requests==2.32.5 ; python_full_version < '3.10' \
# via
# readthedocs-sphinx-ext
# sphinx
requests==2.33.1 ; python_full_version >= '3.10' \
--hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \
--hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a
requests==2.34.2 ; python_full_version >= '3.10' \
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
# via
# readthedocs-sphinx-ext
# sphinx
roman-numerals==4.1.0 ; python_full_version >= '3.11' \
--hash=sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2 \
--hash=sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7
# via sphinx
snowballstemmer==3.0.1 \
--hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \
--hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895
snowballstemmer==3.1.0 \
--hash=sha256:17e6d1da216aa07db6dad37139ea70cf13c4b2e9a096f6e64a9648fc657d3154 \
--hash=sha256:fd9e34526b23340cd23ffea6c9f9760974ecc2c2ac9e1d81401443ccdb2a801f
# via sphinx
sphinx==7.4.7 ; python_full_version < '3.10' \
--hash=sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe \
Expand Down Expand Up @@ -594,10 +594,14 @@ typing-extensions==4.15.0 \
# rules-python-docs (docs/pyproject.toml)
# astroid
# sphinx-autodoc2
urllib3==2.6.3 \
urllib3==2.6.3 ; python_full_version < '3.10' \
--hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \
--hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4
# via requests
urllib3==2.7.0 ; python_full_version >= '3.10' \
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
# via requests
zipp==3.23.1 ; python_full_version < '3.10' \
--hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \
--hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110
Expand Down
7 changes: 4 additions & 3 deletions examples/wheel/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("//examples/wheel/private:wheel_utils.bzl", "directory_writer", "make_variable_tags")
load("//python:packaging.bzl", "py_package", "py_wheel")
load("//python:pip.bzl", "compile_pip_requirements")
load("//python:py_library.bzl", "py_library")
load("//python:py_test.bzl", "py_test")
load("//python:versions.bzl", "gen_python_config_settings")
load("//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
load("//python/uv:lock.bzl", "lock")

package(default_visibility = ["//visibility:public"])

Expand Down Expand Up @@ -473,9 +473,10 @@ py_test(

# Test wheel publishing

compile_pip_requirements(
lock(
name = "requirements_server",
src = "requirements_server.in",
srcs = ["requirements_server.in"],
out = "requirements_server.txt",
)

py_test(
Expand Down
16 changes: 5 additions & 11 deletions examples/wheel/requirements_server.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# This file was autogenerated by uv via the following command:
# bazel run //examples/wheel:requirements_server.update
#
importlib-resources==7.1.0 \
--hash=sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708 \
--hash=sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1
Expand All @@ -12,13 +8,11 @@ packaging==26.1 \
--hash=sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f \
--hash=sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de
# via pypiserver
pypiserver==2.4.1 \
--hash=sha256:156540f87ecfd6db06ae2c16e25ae5afe4fda6f510bd1c34e46fbb0c491bcd9e \
--hash=sha256:45f116d0bff6aafcaed002cfad48a6832e62a82393e3a9b447d5c41a0e310fff
# via -r examples/wheel/requirements_server.in

# The following packages are considered to be unsafe in a requirements file:
pip==24.0 \
--hash=sha256:ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc \
--hash=sha256:ea9bd1a847e8c5774a5777bb398c19e80bcd4e2aa16a4b301b718fe6f593aba2
# via pypiserver
pypiserver==2.4.1 \
--hash=sha256:156540f87ecfd6db06ae2c16e25ae5afe4fda6f510bd1c34e46fbb0c491bcd9e \
--hash=sha256:45f116d0bff6aafcaed002cfad48a6832e62a82393e3a9b447d5c41a0e310fff
# via -r examples/wheel/requirements_server.in
35 changes: 35 additions & 0 deletions python/uv/private/lock.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ def _lock_impl(ctx):
uv,
python_files,
],
# User reported being unable to add `--action_env` and get it to work.
# Without this flag.
#
# Ref: https://app.slack.com/client/TA4K1KQ87/CA306CEV6
use_default_shell_env = True,
progress_message = "Creating a requirements.txt with uv: %{label}",
env = ctx.attr.env,
)
Expand Down Expand Up @@ -394,6 +399,36 @@ def lock(
All of the targets have `manual` tags as locking results cannot be cached.
:::

To authenticate with a private index without embedding credentials in
environment variables, use {obj}`uv <UV_CREDENTIAL_HELPER>`'s credential
helper support. Set `UV_CREDENTIAL_HELPER` via the `env` attribute or
`--action_env` and mount the credential helper script into the sandbox
using `--sandbox_add_mount_pair`. The credential helper script follows
the same protocol as the Bazel credential helper — it is called as
`<path> get [url]` and must return JSON containing an `Authorization`
header.

:::{tip}
`uv` itself can serve as the credential helper. First log in with
``uv auth login <url>``, then set ``UV_CREDENTIAL_HELPER`` to a wrapper
script that delegates to ``uv --preview-features auth-helper auth helper
--protocol=bazel``. The wrapper can be as simple as:

```bash
#!/usr/bin/env bash
exec uv --preview-features auth-helper auth helper --protocol=bazel "$@"
```

This avoids writing a separate credential helper script and keeps all
credential management in `uv`.
:::

:::{seealso}
See {gh-path}`docs/pypi/download.md` for an example script and configuration.
See the [upstream uv documentation](https://github.com/astral-sh/uv/commit/634b03f972330183295adae438ec90e76105593e)
for details on ``uv auth helper``.
:::

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
Expand Down
7 changes: 7 additions & 0 deletions tests/integration/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ test_suite(
tests = [
"bzlmod_lockfile_test_bazel_9.1.0",
"local_toolchains_test_bazel_self",
"uv_lock_test_bazel_self",
],
)

Expand Down Expand Up @@ -111,6 +112,12 @@ rules_python_integration_test(
py_main = "toolchain_target_settings_test.py",
)

rules_python_integration_test(
name = "uv_lock_test",
py_deps = ["@pypiserver//pypiserver"],
py_main = "uv_lock_test.py",
)

py_library(
name = "runner_lib",
srcs = ["runner.py"],
Expand Down
7 changes: 5 additions & 2 deletions tests/integration/integration_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ load(
)
load("//python:py_test.bzl", "py_test")

def _test_runner(*, name, bazel_version, py_main, bzlmod):
def _test_runner(*, name, bazel_version, py_main, bzlmod, py_deps):
if py_main:
test_runner = "{}_bazel_{}_py_runner".format(name, bazel_version)
py_test(
name = test_runner,
srcs = [py_main],
main = py_main,
deps = [":runner_lib"],
deps = [":runner_lib"] + py_deps,
# Hide from ... patterns; should only be run as part
# of the bazel integration test
tags = ["manual"],
Expand All @@ -46,6 +46,7 @@ def rules_python_integration_test(
bzlmod = True,
tags = None,
py_main = None,
py_deps = None,
bazel_versions = None,
**kwargs):
"""Runs a bazel-in-bazel integration test.
Expand All @@ -60,6 +61,7 @@ def rules_python_integration_test(
py_main: Optional `.py` file to run tests using. When specified, a
python based test runner is used, and this source file is the main
entry point and responsible for executing tests.
py_deps: Optional test runner deps to use for setup.
bazel_versions: `list[str] | None`, the bazel versions to test. I
not specified, defaults to all configured bazel versions.
**kwargs: Passed to the upstream `bazel_integration_tests` rule.
Expand Down Expand Up @@ -91,6 +93,7 @@ def rules_python_integration_test(
name = name,
bazel_version = bazel_version,
py_main = py_main,
py_deps = py_deps or [],
bzlmod = bzlmod,
)
bazel_integration_test(
Expand Down
6 changes: 5 additions & 1 deletion tests/integration/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,17 @@ def setUp(self):
"RUNFILES_DIR": os.environ["TEST_SRCDIR"],
}

def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult:
def run_bazel(
self, *args: str, check: bool = True, input: str | None = None
) -> ExecuteResult:
"""Run a bazel invocation.

Args:
*args: The args to pass to bazel; the leading `bazel` command is
added automatically
check: True if the execution must succeed, False if failure
should raise an error.
input: Optional stdin data to pass to the process.
Returns:
An `ExecuteResult` from running Bazel
"""
Expand All @@ -111,6 +114,7 @@ def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult:
cwd=cwd,
env=env,
check=False,
input=input,
)
exec_result = ExecuteResult(args, env, cwd, proc_result)
if check and exec_result.exit_code:
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/uv_lock/.bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build --enable_runfiles
common --experimental_isolated_extension_usages

try-import %workspace%/user.bazelrc
1 change: 1 addition & 0 deletions tests/integration/uv_lock/.bazelversion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9.1.0
22 changes: 22 additions & 0 deletions tests/integration/uv_lock/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
load("@rules_python//python/uv:lock.bzl", "lock")
load(":uv_runner.bzl", "uv_runner")

lock(
name = "requirements",
srcs = ["requirements.in"],
out = "requirements.txt",
tags = ["no-remote-exec"],
)

uv_runner(
name = "uv",
tags = ["manual"],
)

diff_test(
name = "requirements_diff_test",
timeout = "short",
file1 = ":requirements",
file2 = ":requirements_expected.txt",
)
Loading