blob: f0a66a1a93e4adf984fc97fa18327129a57f7aa9 [file] [log] [blame]
# Copyright 2024 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A simple macro to lock the requirements.
"""
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("//python:py_binary.bzl", "py_binary")
load("//python/config_settings:transition.bzl", transition_py_binary = "py_binary")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
visibility(["//..."])
_REQUIREMENTS_TARGET_COMPATIBLE_WITH = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"]
def lock(*, name, srcs, out, upgrade = False, universal = True, python_version = None):
"""Pin the requirements based on the src files.
Args:
name: The name of the target to run for updating the requirements.
srcs: The srcs to use as inputs.
out: The output file.
upgrade: Tell `uv` to always upgrade the dependencies instead of
keeping them as they are.
universal: Tell `uv` to generate a universal lock file.
python_version: Tell `rules_python` to use a particular version.
Defaults to the default py toolchain.
Differences with the current pip-compile rule:
- This is implemented in shell and uv.
- This does not error out if the output file does not exist yet.
- Supports transitions out of the box.
"""
pkg = native.package_name()
update_target = name + ".update"
args = [
"--custom-compile-command='bazel run //{}:{}'".format(pkg, update_target),
"--generate-hashes",
"--emit-index-url",
"--no-strip-extras",
"--python=$(PYTHON3)",
] + [
"$(location {})".format(src)
for src in srcs
]
if upgrade:
args.append("--upgrade")
if universal:
args.append("--universal")
args.append("--output-file=$@")
cmd = "$(UV_BIN) pip compile " + " ".join(args)
# Check if the output file already exists, if yes, first copy it to the
# output file location in order to make `uv` not change the requirements if
# we are just running the command.
if native.glob([out]):
cmd = "cp -v $(location {}) $@; {}".format(out, cmd)
srcs.append(out)
native.genrule(
name = name,
srcs = srcs,
outs = [out + ".new"],
cmd_bash = cmd,
tags = [
"local",
"manual",
"no-cache",
],
target_compatible_with = _REQUIREMENTS_TARGET_COMPATIBLE_WITH,
toolchains = [
Label("//python/uv:current_toolchain"),
Label("//python:current_py_toolchain"),
],
)
if python_version:
py_binary_rule = lambda *args, **kwargs: transition_py_binary(python_version = python_version, *args, **kwargs)
else:
py_binary_rule = py_binary
# Write a script that can be used for updating the in-tree version of the
# requirements file
write_file(
name = name + ".update_gen",
out = update_target + ".py",
content = [
"from os import environ",
"from pathlib import Path",
"from sys import stderr",
"",
'src = Path(environ["REQUIREMENTS_FILE"])',
'assert src.exists(), f"the {src} file does not exist"',
'dst = Path(environ["BUILD_WORKSPACE_DIRECTORY"]) / "{}" / "{}"'.format(pkg, out),
'print(f"Writing requirements contents\\n from {src.absolute()}\\n to {dst.absolute()}", file=stderr)',
"dst.write_text(src.read_text())",
'print("Success!", file=stderr)',
],
)
py_binary_rule(
name = update_target,
srcs = [update_target + ".py"],
main = update_target + ".py",
data = [name],
env = {
"REQUIREMENTS_FILE": "$(rootpath {})".format(name),
},
tags = ["manual"],
)