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