Skip to content

Commit 7a37d5d

Browse files
committed
feat: uv lock rule instead of genrule
This change implements the uv pip compile as a rule. In order to also make things easier to debug we provide a runnable rule that has the same arguments and updates the source tree output file automatically. The main design is to have a regular lock rule and then it returns a custom provider that has all of the recipe ingredients to construct an executable rule. The execution depends on having bash or powershell, however the powershell script is not yet complete and requires some help from the community. Work towards #1975. Address all of the comments
1 parent 175fe4c commit 7a37d5d

25 files changed

+1116
-106
lines changed

docs/BUILD.bazel

+6-2
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,12 @@ lock(
176176
name = "requirements",
177177
srcs = ["pyproject.toml"],
178178
out = "requirements.txt",
179-
upgrade = True,
180-
visibility = ["//private:__pkg__"],
179+
args = [
180+
"--emit-index-url",
181+
"--universal",
182+
"--upgrade",
183+
],
184+
visibility = ["//:__subpackages__"],
181185
)
182186

183187
# Temporary compatibility aliases for some other projects depending on the old

examples/BUILD.bazel

+3
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@ lock(
2121
name = "bzlmod_requirements_3_9",
2222
srcs = ["bzlmod/requirements.in"],
2323
out = "bzlmod/requirements_lock_3_9.txt",
24+
args = [
25+
"--emit-index-url",
26+
],
2427
python_version = "3.9.19",
2528
)

examples/bzlmod/requirements_lock_3_9.txt

+6-11
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ chardet==4.0.0 \
2626
colorama==0.4.6 \
2727
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
2828
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
29-
# via
30-
# -r examples/bzlmod/requirements.in
31-
# pylint
32-
# sphinx
29+
# via -r examples/bzlmod/requirements.in
3330
dill==0.3.6 \
3431
--hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \
3532
--hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373
@@ -46,7 +43,7 @@ imagesize==1.4.1 \
4643
--hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
4744
--hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
4845
# via sphinx
49-
importlib-metadata==8.4.0 ; python_version < '3.10' \
46+
importlib-metadata==8.4.0 \
5047
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
5148
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
5249
# via sphinx
@@ -265,9 +262,7 @@ s3cmd==2.1.0 \
265262
setuptools==65.6.3 \
266263
--hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \
267264
--hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75
268-
# via
269-
# babel
270-
# yamllint
265+
# via yamllint
271266
six==1.16.0 \
272267
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
273268
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
@@ -316,15 +311,15 @@ tabulate==0.9.0 \
316311
--hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \
317312
--hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f
318313
# via -r examples/bzlmod/requirements.in
319-
tomli==2.0.1 ; python_version < '3.11' \
314+
tomli==2.0.1 \
320315
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
321316
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
322317
# via pylint
323318
tomlkit==0.11.6 \
324319
--hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \
325320
--hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73
326321
# via pylint
327-
typing-extensions==4.12.2 ; python_version < '3.10' \
322+
typing-extensions==4.12.2 \
328323
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
329324
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
330325
# via
@@ -480,7 +475,7 @@ yamllint==1.28.0 \
480475
--hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \
481476
--hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b
482477
# via -r examples/bzlmod/requirements.in
483-
zipp==3.20.0 ; python_version < '3.10' \
478+
zipp==3.20.0 \
484479
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
485480
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
486481
# via importlib-metadata

private/BUILD.bazel

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ multirun(
1515
] + [
1616
"//docs:requirements.update",
1717
],
18+
tags = ["manual"],
1819
)
1920

2021
# NOTE: The requirements for the pip dependencies may sometimes break the build
@@ -24,4 +25,5 @@ multirun(
2425
alias(
2526
name = "whl_library_requirements.update",
2627
actual = "//tools/private/update_deps:update_pip_deps",
28+
tags = ["manual"],
2729
)

python/private/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ bzl_library(
361361
name = "py_exec_tools_toolchain_bzl",
362362
srcs = ["py_exec_tools_toolchain.bzl"],
363363
deps = [
364+
":common_bzl",
364365
":py_exec_tools_info_bzl",
365366
":sentinel_bzl",
366367
":toolchain_types_bzl",

python/private/py_exec_tools_toolchain.bzl

+68-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
load("@bazel_skylib//lib:paths.bzl", "paths")
1818
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
19+
load(":common.bzl", "runfiles_root_path")
1920
load(":py_exec_tools_info.bzl", "PyExecToolsInfo")
2021
load(":sentinel.bzl", "SentinelInfo")
2122
load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")
@@ -82,24 +83,86 @@ See {obj}`PyExecToolsInfo.exec_interpreter` for further docs.
8283
},
8384
)
8485

86+
def relative_path(from_, to):
87+
"""Compute a relative path from one path to another.
88+
89+
Args:
90+
from_: {type}`str` the starting directory. Note that it should be
91+
a directory because relative-symlinks are relative to the
92+
directory the symlink resides in.
93+
to: {type}`str` the path that `from_` wants to point to
94+
95+
Returns:
96+
{type}`str` a relative path
97+
"""
98+
from_parts = from_.split("/")
99+
to_parts = to.split("/")
100+
101+
# Strip common leading parts from both paths
102+
n = min(len(from_parts), len(to_parts))
103+
for _ in range(n):
104+
if from_parts[0] == to_parts[0]:
105+
from_parts.pop(0)
106+
to_parts.pop(0)
107+
else:
108+
break
109+
110+
# Impossible to compute a relative path without knowing what ".." is
111+
if from_parts and from_parts[0] == "..":
112+
fail("cannot compute relative path from '%s' to '%s'", from_, to)
113+
114+
parts = ([".."] * len(from_parts)) + to_parts
115+
return paths.join(*parts)
116+
85117
def _current_interpreter_executable_impl(ctx):
86118
toolchain = ctx.toolchains[TARGET_TOOLCHAIN_TYPE]
87119
runtime = toolchain.py3_runtime
120+
direct = []
88121

89122
# NOTE: We name the output filename after the underlying file name
90123
# because of things like pyenv: they use $0 to determine what to
91124
# re-exec. If it's not a recognized name, then they fail.
92125
if runtime.interpreter:
93-
executable = ctx.actions.declare_file(runtime.interpreter.basename)
94-
ctx.actions.symlink(output = executable, target_file = runtime.interpreter, is_executable = True)
126+
# Even though ctx.actions.symlink() could be used, we bump into the issue
127+
# with RBE where bazel is making a copy to the file instead of symlinking
128+
# to the hermetic toolchain repository. This means that we need to employ
129+
# a similar strategy to how the `py_executable` venv is created where the
130+
# file in the `runfiles` is a dangling symlink into the hermetic toolchain
131+
# repository. This smells like a bug in RBE, but I would not be surprised
132+
# if it is not one.
133+
134+
# Create a dangling symlink in `bin/python3` to the real interpreter
135+
# in the hermetic toolchain.
136+
interpreter_basename = runtime.interpreter.basename
137+
executable = ctx.actions.declare_symlink("bin/" + interpreter_basename)
138+
direct.append(executable)
139+
interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path)
140+
target_path = relative_path(
141+
# dirname is necessary because a relative symlink is relative to
142+
# the directory the symlink resides within.
143+
from_ = paths.dirname(runfiles_root_path(ctx, executable.short_path)),
144+
to = interpreter_actual_path,
145+
)
146+
ctx.actions.symlink(output = executable, target_path = target_path)
147+
148+
# Create a dangling symlink into the runfiles and use that as the
149+
# entry point.
150+
interpreter_actual_path = runfiles_root_path(ctx, executable.short_path)
151+
executable = ctx.actions.declare_symlink(interpreter_basename)
152+
target_path = interpreter_basename + ".runfiles/" + interpreter_actual_path
153+
ctx.actions.symlink(output = executable, target_path = target_path)
95154
else:
96-
executable = ctx.actions.declare_symlink(paths.basename(runtime.interpreter_path))
97-
ctx.actions.symlink(output = executable, target_path = runtime.interpreter_path)
155+
interpreter_basename = paths.basename(runtime.interpreter.interpreter_path)
156+
executable = ctx.actions.declare_symlink(interpreter_basename)
157+
direct.append(executable)
158+
target_path = runtime.interpreter_path
159+
ctx.actions.symlink(output = executable, target_path = target_path)
160+
98161
return [
99162
toolchain,
100163
DefaultInfo(
101164
executable = executable,
102-
runfiles = ctx.runfiles([executable], transitive_files = runtime.files),
165+
runfiles = ctx.runfiles(direct, transitive_files = runtime.files),
103166
),
104167
]
105168

python/uv/lock.bzl

+26
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,32 @@
1414

1515
"""The `uv` locking rule.
1616
17+
Differences with the legacy {obj}`compile_pip_requirements` rule:
18+
- This is implemented as a rule that performs locking in a build action.
19+
- Additionally one can use the runnable target.
20+
- Uses `uv`.
21+
- This does not error out if the output file does not exist yet.
22+
- Supports transitions out of the box.
23+
24+
Note, this does not provide a `test` target, if you would like to add a test
25+
target that always does the locking automatically to ensure that the
26+
`requirements.txt` file is up-to-date, add something similar to:
27+
28+
```starlark
29+
load("@bazel_skylib//rules:native_binary.bzl", "native_test")
30+
load("@rules_python//python/uv:lock.bzl", "lock")
31+
32+
lock(
33+
name = "requirements",
34+
srcs = ["pyproject.toml"],
35+
)
36+
37+
native_test(
38+
name = "requirements_test",
39+
src = "requirements.update",
40+
)
41+
```
42+
1743
EXPERIMENTAL: This is experimental and may be removed without notice
1844
"""
1945

python/uv/private/BUILD.bazel

+24-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
# limitations under the License.
1414

1515
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
16+
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
17+
18+
exports_files(
19+
srcs = [
20+
"lock_copier.py",
21+
],
22+
# only because this is used from a macro to template
23+
visibility = ["//visibility:public"],
24+
)
1625

1726
filegroup(
1827
name = "distribution",
@@ -31,9 +40,13 @@ bzl_library(
3140
srcs = ["lock.bzl"],
3241
visibility = ["//python/uv:__subpackages__"],
3342
deps = [
43+
":toolchain_types_bzl",
3444
"//python:py_binary_bzl",
3545
"//python/private:bzlmod_enabled_bzl",
36-
"@bazel_skylib//rules:write_file",
46+
"//python/private:full_version_bzl",
47+
"//python/private:toolchain_types_bzl",
48+
"@bazel_skylib//lib:shell",
49+
"@pythons_hub//:versions_bzl",
3750
],
3851
)
3952

@@ -81,3 +94,13 @@ bzl_library(
8194
"//python/private:text_util_bzl",
8295
],
8396
)
97+
98+
filegroup(
99+
name = "lock_template",
100+
srcs = select({
101+
"@platforms//os:windows": ["lock.bat"],
102+
"//conditions:default": ["lock.sh"],
103+
}),
104+
target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"],
105+
visibility = ["//visibility:public"],
106+
)

python/uv/private/lock.bat

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
if defined BUILD_WORKSPACE_DIRECTORY (
2+
set "out=%BUILD_WORKSPACE_DIRECTORY%\{{src_out}}"
3+
) else (
4+
exit /b 1
5+
)
6+
7+
"{{args}}" --output-file "%out%" %*

0 commit comments

Comments
 (0)