xref: /aosp_15_r20/external/bazelbuild-rules_python/python/private/pypi/multi_pip_parse.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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 pip_parse implementation for version aware toolchains in WORKSPACE."""
16
17load("//python/private:text_util.bzl", "render")
18load(":pip_repository.bzl", pip_parse = "pip_repository")
19
20def _multi_pip_parse_impl(rctx):
21    rules_python = rctx.attr._rules_python_workspace.workspace_name
22    load_statements = []
23    install_deps_calls = []
24    process_requirements_calls = []
25    for python_version, pypi_repository in rctx.attr.pip_parses.items():
26        sanitized_python_version = python_version.replace(".", "_")
27        load_statement = """\
28load(
29    "@{pypi_repository}//:requirements.bzl",
30    _{sanitized_python_version}_install_deps = "install_deps",
31    _{sanitized_python_version}_all_requirements = "all_requirements",
32)""".format(
33            pypi_repository = pypi_repository,
34            sanitized_python_version = sanitized_python_version,
35        )
36        load_statements.append(load_statement)
37        process_requirements_call = """\
38_process_requirements(
39    pkg_labels = _{sanitized_python_version}_all_requirements,
40    python_version = "{python_version}",
41    repo_prefix = "{pypi_repository}_",
42)""".format(
43            pypi_repository = pypi_repository,
44            python_version = python_version,
45            sanitized_python_version = sanitized_python_version,
46        )
47        process_requirements_calls.append(process_requirements_call)
48        install_deps_call = """    _{sanitized_python_version}_install_deps(**whl_library_kwargs)""".format(
49            sanitized_python_version = sanitized_python_version,
50        )
51        install_deps_calls.append(install_deps_call)
52
53    # NOTE @aignas 2023-10-31: I am not sure it is possible to render aliases
54    # for all of the packages using the `render_pkg_aliases` function because
55    # we need to know what the list of packages for each version is and then
56    # we would be creating directories for each.
57    macro_tmpl = "@%s_{}//:{}" % rctx.attr.name
58
59    requirements_bzl = """\
60# Generated by python/pip.bzl
61
62load("@{rules_python}//python:pip.bzl", "whl_library_alias", "pip_utils")
63{load_statements}
64
65_wheel_names = []
66_version_map = dict()
67def _process_requirements(pkg_labels, python_version, repo_prefix):
68    for pkg_label in pkg_labels:
69        wheel_name = Label(pkg_label).package
70        if not wheel_name:
71            # We are dealing with the cases where we don't have aliases.
72            workspace_name = Label(pkg_label).workspace_name
73            wheel_name = workspace_name[len(repo_prefix):]
74
75        _wheel_names.append(wheel_name)
76        if not wheel_name in _version_map:
77            _version_map[wheel_name] = dict()
78        _version_map[wheel_name][python_version] = repo_prefix
79
80{process_requirements_calls}
81
82def requirement(name):
83    return "{macro_tmpl}".format(pip_utils.normalize_name(name), "pkg")
84
85def whl_requirement(name):
86    return "{macro_tmpl}".format(pip_utils.normalize_name(name), "whl")
87
88def data_requirement(name):
89    return "{macro_tmpl}".format(pip_utils.normalize_name(name), "data")
90
91def dist_info_requirement(name):
92    return "{macro_tmpl}".format(pip_utils.normalize_name(name), "dist_info")
93
94def install_deps(**whl_library_kwargs):
95{install_deps_calls}
96    for wheel_name in _wheel_names:
97        whl_library_alias(
98            name = "{name}_" + wheel_name,
99            wheel_name = wheel_name,
100            default_version = "{default_version}",
101            minor_mapping = {minor_mapping},
102            version_map = _version_map[wheel_name],
103        )
104""".format(
105        name = rctx.attr.name,
106        install_deps_calls = "\n".join(install_deps_calls),
107        load_statements = "\n".join(load_statements),
108        macro_tmpl = macro_tmpl,
109        process_requirements_calls = "\n".join(process_requirements_calls),
110        rules_python = rules_python,
111        default_version = rctx.attr.default_version,
112        minor_mapping = render.indent(render.dict(rctx.attr.minor_mapping)).lstrip(),
113    )
114    rctx.file("requirements.bzl", requirements_bzl)
115    rctx.file("BUILD.bazel", "exports_files(['requirements.bzl'])")
116
117_multi_pip_parse = repository_rule(
118    _multi_pip_parse_impl,
119    attrs = {
120        "default_version": attr.string(),
121        "minor_mapping": attr.string_dict(),
122        "pip_parses": attr.string_dict(),
123        "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
124    },
125)
126
127def multi_pip_parse(name, default_version, python_versions, python_interpreter_target, requirements_lock, minor_mapping, **kwargs):
128    """NOT INTENDED FOR DIRECT USE!
129
130    This is intended to be used by the multi_pip_parse implementation in the template of the
131    multi_toolchain_aliases repository rule.
132
133    Args:
134        name: the name of the multi_pip_parse repository.
135        default_version: {type}`str` the default Python version.
136        python_versions: {type}`list[str]` all Python toolchain versions currently registered.
137        python_interpreter_target: {type}`dict[str, Label]` a dictionary which keys are Python versions and values are resolved host interpreters.
138        requirements_lock: {type}`dict[str, Label]` a dictionary which keys are Python versions and values are locked requirements files.
139        minor_mapping: {type}`dict[str, str]` mapping between `X.Y` to `X.Y.Z` format.
140        **kwargs: extra arguments passed to all wrapped pip_parse.
141
142    Returns:
143        The internal implementation of multi_pip_parse repository rule.
144    """
145    pip_parses = {}
146    for python_version in python_versions:
147        if not python_version in python_interpreter_target:
148            fail("Missing python_interpreter_target for Python version %s in '%s'" % (python_version, name))
149        if not python_version in requirements_lock:
150            fail("Missing requirements_lock for Python version %s in '%s'" % (python_version, name))
151
152        pip_parse_name = name + "_" + python_version.replace(".", "_")
153        pip_parse(
154            name = pip_parse_name,
155            python_interpreter_target = python_interpreter_target[python_version],
156            requirements_lock = requirements_lock[python_version],
157            **kwargs
158        )
159        pip_parses[python_version] = pip_parse_name
160
161    return _multi_pip_parse(
162        name = name,
163        default_version = default_version,
164        pip_parses = pip_parses,
165        minor_mapping = minor_mapping,
166    )
167