• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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