# 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"], )