Skip to content

Commit ac02537

Browse files
committed
refactor(uv): create a reusable macro for using uv for locking reqs
Also: - Fix the lock file rule to take into account the currently existing lockfiles. - Use a transition to allow for a smart selection of the python version to generate the lock file for.
1 parent 04e2f32 commit ac02537

File tree

2 files changed

+190
-66
lines changed

2 files changed

+190
-66
lines changed

docs/BUILD.bazel

Lines changed: 6 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313
# limitations under the License.
1414

1515
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
16-
load("@bazel_skylib//rules:write_file.bzl", "write_file")
1716
load("@dev_pip//:requirements.bzl", "requirement")
18-
load("//python:py_binary.bzl", "py_binary")
1917
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
2018
load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility
19+
load("//python/uv/private:pin.bzl", "pin") # buildifier: disable=bzl-visibility
2120
load("//sphinxdocs:readthedocs.bzl", "readthedocs_install")
2221
load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
2322
load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardoc", "sphinx_stardocs")
@@ -140,71 +139,12 @@ sphinx_build_binary(
140139
],
141140
)
142141

143-
_REQUIREMENTS_TARGET_COMPATIBLE_WITH = select({
144-
"@platforms//os:linux": [],
145-
"@platforms//os:macos": [],
146-
"@platforms//os:windows": [],
147-
"//conditions:default": ["@platforms//:incompatible"],
148-
}) if BZLMOD_ENABLED else ["@platforms//:incompatible"]
149-
150-
# Run bazel run //docs:requirements.update
151-
genrule(
152-
name = "requirements",
142+
# Run bazel run //docs:requirements_update
143+
pin(
144+
name = "requirements_update",
153145
srcs = ["pyproject.toml"],
154-
outs = ["_requirements.txt"],
155-
cmd = "$(UV_BIN) pip compile " + " ".join([
156-
"--custom-compile-command='bazel run //docs:requirements.update'",
157-
"--generate-hashes",
158-
"--universal",
159-
"--emit-index-url",
160-
"--no-strip-extras",
161-
"--no-build",
162-
"--python=$(PYTHON3)",
163-
"$<",
164-
"--output-file=$@",
165-
# Always try upgrading
166-
"--upgrade",
167-
]),
168-
tags = [
169-
"local",
170-
"manual",
171-
"no-cache",
172-
],
173-
target_compatible_with = _REQUIREMENTS_TARGET_COMPATIBLE_WITH,
174-
toolchains = [
175-
"//python/uv:current_toolchain",
176-
"//python:current_py_toolchain",
177-
],
178-
)
179-
180-
# Write a script that can be used for updating the in-tree version of the
181-
# requirements file
182-
write_file(
183-
name = "gen_update_requirements",
184-
out = "requirements.update.py",
185-
content = [
186-
"from os import environ",
187-
"from pathlib import Path",
188-
"from sys import stderr",
189-
"",
190-
'src = Path(environ["REQUIREMENTS_FILE"])',
191-
'dst = Path(environ["BUILD_WORKSPACE_DIRECTORY"]) / "docs" / "requirements.txt"',
192-
'print(f"Writing requirements contents from {src} to {dst}", file=stderr)',
193-
"dst.write_text(src.read_text())",
194-
'print("Success!", file=stderr)',
195-
],
196-
target_compatible_with = _REQUIREMENTS_TARGET_COMPATIBLE_WITH,
197-
)
198-
199-
py_binary(
200-
name = "requirements.update",
201-
srcs = ["requirements.update.py"],
202-
data = [":requirements"],
203-
env = {
204-
"REQUIREMENTS_FILE": "$(location :requirements)",
205-
},
206-
tags = ["manual"],
207-
target_compatible_with = _REQUIREMENTS_TARGET_COMPATIBLE_WITH,
146+
out = "requirements.txt",
147+
upgrade = True,
208148
)
209149

210150
licenses(["notice"]) # Apache 2.0

python/uv/private/pin.bzl

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""A simple macro to pin the requirements.
16+
"""
17+
18+
load("@bazel_skylib//rules:write_file.bzl", "write_file")
19+
load("//python:py_binary.bzl", "py_binary")
20+
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
21+
22+
_REQUIREMENTS_TARGET_COMPATIBLE_WITH = select({
23+
"@platforms//os:linux": [],
24+
"@platforms//os:macos": [],
25+
"//conditions:default": ["@platforms//:incompatible"],
26+
}) if BZLMOD_ENABLED else ["@platforms//:incompatible"]
27+
28+
def pin(*, name, srcs, out, upgrade = False, universal = True, python_version = None):
29+
"""Pin the requirements based on the src files.
30+
31+
Args:
32+
name: The name of the target to run for updating the requirements.
33+
srcs: The srcs to use as inputs.
34+
out: The output file.
35+
upgrade: Tell `uv` to always upgrade the dependencies instead of
36+
keeping them as they are.
37+
universal: Tell `uv` to generate a universal lock file.
38+
python_version: Tell `rules_python` to use a particular version.
39+
Defaults to the default py toolchain.
40+
41+
Differences with the current pip-compile rule:
42+
- This is implemented in shell and uv.
43+
- This does not error out if the output file does not exist yet.
44+
- Supports transitions out of the box.
45+
"""
46+
pkg = native.package_name()
47+
_out = "_" + out
48+
49+
args = [
50+
"--custom-compile-command='bazel run //{}:{}'".format(pkg, name),
51+
"--generate-hashes",
52+
"--emit-index-url",
53+
"--no-strip-extras",
54+
"--python=$(PYTHON3)",
55+
] + [
56+
"$(location {})".format(src)
57+
for src in srcs
58+
] + [
59+
"--output-file=$(location {})".format(_out),
60+
]
61+
if upgrade:
62+
args.append("--upgrade")
63+
if universal:
64+
args.append("--universal")
65+
cmd = "$(UV_BIN) pip compile " + " ".join(args)
66+
67+
# Check if the output file already exists, if yes, first copy it to the
68+
# output file location in order to make `uv` not change the requirements if
69+
# we are just running the command.
70+
if native.glob([out]):
71+
cmd = "cp -v $(location {}) $@; {}".format(out, cmd)
72+
srcs.append(out)
73+
74+
native.genrule(
75+
name = name + ".uv.out",
76+
srcs = srcs,
77+
outs = [_out],
78+
cmd_bash = cmd,
79+
tags = [
80+
"local",
81+
"manual",
82+
"no-cache",
83+
],
84+
target_compatible_with = _REQUIREMENTS_TARGET_COMPATIBLE_WITH,
85+
toolchains = [
86+
Label("//python/uv:current_toolchain"),
87+
Label("//python:current_py_toolchain"),
88+
],
89+
)
90+
if python_version:
91+
transitioned_name = "{}.uv.out.{}".format(name, python_version)
92+
_versioned(
93+
name = transitioned_name,
94+
src = _out,
95+
python_version = python_version,
96+
tags = ["manual"],
97+
)
98+
_out = transitioned_name
99+
100+
# Write a script that can be used for updating the in-tree version of the
101+
# requirements file
102+
write_file(
103+
name = name + ".gen",
104+
out = name + ".gen.py",
105+
content = [
106+
"from os import environ",
107+
"from pathlib import Path",
108+
"from sys import stderr",
109+
"",
110+
'src = Path(environ["REQUIREMENTS_FILE"])',
111+
'dst = Path(environ["BUILD_WORKSPACE_DIRECTORY"]) / "{}" / "{}"'.format(pkg, out),
112+
'print(f"Writing requirements contents\\n from {src.absolute()}\\n to {dst.absolute()}", file=stderr)',
113+
"dst.write_text(src.read_text())",
114+
'print("Success!", file=stderr)',
115+
],
116+
target_compatible_with = _REQUIREMENTS_TARGET_COMPATIBLE_WITH,
117+
)
118+
119+
py_binary(
120+
name = name,
121+
srcs = [name + ".gen.py"],
122+
main = name + ".gen.py",
123+
data = [_out],
124+
env = {
125+
"REQUIREMENTS_FILE": "$(location {})".format(_out),
126+
},
127+
tags = ["manual"],
128+
target_compatible_with = _REQUIREMENTS_TARGET_COMPATIBLE_WITH,
129+
)
130+
131+
def _transition_python_version_impl(_, attr):
132+
return {"//python/config_settings:python_version": str(attr.python_version)}
133+
134+
_transition_python_version = transition(
135+
implementation = _transition_python_version_impl,
136+
inputs = [],
137+
outputs = ["//python/config_settings:python_version"],
138+
)
139+
140+
def _impl(ctx):
141+
target = ctx.attr.src
142+
143+
default_info = target[0][DefaultInfo]
144+
files = default_info.files
145+
original_executable = default_info.files_to_run.executable
146+
runfiles = default_info.default_runfiles
147+
148+
new_executable = ctx.actions.declare_file(ctx.attr.name)
149+
150+
ctx.actions.symlink(
151+
output = new_executable,
152+
target_file = original_executable,
153+
is_executable = True,
154+
)
155+
156+
files = depset(direct = [new_executable], transitive = [files])
157+
runfiles = runfiles.merge(ctx.runfiles([new_executable]))
158+
159+
return [
160+
DefaultInfo(
161+
files = files,
162+
runfiles = runfiles,
163+
executable = new_executable,
164+
),
165+
]
166+
167+
_versioned = rule(
168+
implementation = _impl,
169+
attrs = {
170+
"python_version": attr.string(
171+
mandatory = True,
172+
),
173+
"src": attr.label(
174+
allow_single_file = True,
175+
executable = False,
176+
mandatory = True,
177+
cfg = _transition_python_version,
178+
),
179+
"_allowlist_function_transition": attr.label(
180+
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
181+
),
182+
},
183+
executable = True,
184+
)

0 commit comments

Comments
 (0)