1# Copyright 2023 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"""Rules to verify and update pip-compile locked requirements.txt""" 16 17load("//python:defs.bzl", _py_binary = "py_binary", _py_test = "py_test") 18load("//python/pip_install:repositories.bzl", "requirement") 19 20def compile_pip_requirements( 21 name, 22 extra_args = [], 23 extra_deps = [], 24 generate_hashes = True, 25 py_binary = _py_binary, 26 py_test = _py_test, 27 requirements_in = None, 28 requirements_txt = None, 29 requirements_darwin = None, 30 requirements_linux = None, 31 requirements_windows = None, 32 visibility = ["//visibility:private"], 33 tags = None, 34 **kwargs): 35 """Generates targets for managing pip dependencies with pip-compile. 36 37 By default this rules generates a filegroup named "[name]" which can be included in the data 38 of some other compile_pip_requirements rule that references these requirements 39 (e.g. with `-r ../other/requirements.txt`). 40 41 It also generates two targets for running pip-compile: 42 43 - validate with `bazel test [name]_test` 44 - update with `bazel run [name].update` 45 46 If you are using a version control system, the requirements.txt generated by this rule should 47 be checked into it to ensure that all developers/users have the same dependency versions. 48 49 Args: 50 name: base name for generated targets, typically "requirements". 51 extra_args: passed to pip-compile. 52 extra_deps: extra dependencies passed to pip-compile. 53 generate_hashes: whether to put hashes in the requirements_txt file. 54 py_binary: the py_binary rule to be used. 55 py_test: the py_test rule to be used. 56 requirements_in: file expressing desired dependencies. 57 requirements_txt: result of "compiling" the requirements.in file. 58 requirements_linux: File of linux specific resolve output to check validate if requirement.in has changes. 59 requirements_darwin: File of darwin specific resolve output to check validate if requirement.in has changes. 60 requirements_windows: File of windows specific resolve output to check validate if requirement.in has changes. 61 tags: tagging attribute common to all build rules, passed to both the _test and .update rules. 62 visibility: passed to both the _test and .update rules. 63 **kwargs: other bazel attributes passed to the "_test" rule. 64 """ 65 requirements_in = name + ".in" if requirements_in == None else requirements_in 66 requirements_txt = name + ".txt" if requirements_txt == None else requirements_txt 67 68 # "Default" target produced by this macro 69 # Allow a compile_pip_requirements rule to include another one in the data 70 # for a requirements file that does `-r ../other/requirements.txt` 71 native.filegroup( 72 name = name, 73 srcs = kwargs.pop("data", []) + [requirements_txt], 74 visibility = visibility, 75 ) 76 77 data = [name, requirements_in, requirements_txt] + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None] 78 79 # Use the Label constructor so this is expanded in the context of the file 80 # where it appears, which is to say, in @rules_python 81 pip_compile = Label("//python/pip_install/tools/dependency_resolver:dependency_resolver.py") 82 83 loc = "$(rlocationpath {})" 84 85 args = [ 86 loc.format(requirements_in), 87 loc.format(requirements_txt), 88 # String None is a placeholder for argv ordering. 89 loc.format(requirements_linux) if requirements_linux else "None", 90 loc.format(requirements_darwin) if requirements_darwin else "None", 91 loc.format(requirements_windows) if requirements_windows else "None", 92 "//%s:%s.update" % (native.package_name(), name), 93 ] + (["--generate-hashes"] if generate_hashes else []) + extra_args 94 95 deps = [ 96 requirement("build"), 97 requirement("click"), 98 requirement("colorama"), 99 requirement("pep517"), 100 requirement("pip"), 101 requirement("pip_tools"), 102 requirement("setuptools"), 103 requirement("tomli"), 104 requirement("importlib_metadata"), 105 requirement("zipp"), 106 requirement("more_itertools"), 107 Label("//python/runfiles:runfiles"), 108 ] + extra_deps 109 110 tags = tags or [] 111 tags.append("requires-network") 112 tags.append("no-remote-exec") 113 tags.append("no-sandbox") 114 attrs = { 115 "args": args, 116 "data": data, 117 "deps": deps, 118 "main": pip_compile, 119 "srcs": [pip_compile], 120 "tags": tags, 121 "visibility": visibility, 122 } 123 124 # cheap way to detect the bazel version 125 _bazel_version_4_or_greater = "propeller_optimize" in dir(native) 126 127 # Bazel 4.0 added the "env" attribute to py_test/py_binary 128 if _bazel_version_4_or_greater: 129 attrs["env"] = kwargs.pop("env", {}) 130 131 py_binary( 132 name = name + ".update", 133 **attrs 134 ) 135 136 timeout = kwargs.pop("timeout", "short") 137 138 py_test( 139 name = name + "_test", 140 timeout = timeout, 141 # kwargs could contain test-specific attributes like size or timeout 142 **dict(attrs, **kwargs) 143 ) 144