| # Copyright 2025 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. |
| |
| """This module is for implementing PEP508 compliant METADATA deps parsing. |
| """ |
| |
| load("//python/private:normalize_name.bzl", "normalize_name") |
| load(":pep508_env.bzl", "env") |
| load(":pep508_evaluate.bzl", "evaluate") |
| load(":pep508_platform.bzl", "platform", "platform_from_str") |
| load(":pep508_requirement.bzl", "requirement") |
| |
| _ALL_OS_VALUES = [ |
| "windows", |
| "osx", |
| "linux", |
| ] |
| _ALL_ARCH_VALUES = [ |
| "aarch64", |
| "ppc64", |
| "ppc64le", |
| "s390x", |
| "x86_32", |
| "x86_64", |
| ] |
| |
| def deps(name, *, requires_dist, platforms = [], extras = [], host_python_version = None): |
| """Parse the RequiresDist from wheel METADATA |
| |
| Args: |
| name: {type}`str` the name of the wheel. |
| requires_dist: {type}`list[str]` the list of RequiresDist lines from the |
| METADATA file. |
| extras: {type}`list[str]` the requested extras to generate targets for. |
| platforms: {type}`list[str]` the list of target platform strings. |
| host_python_version: {type}`str` the host python version. |
| |
| Returns: |
| A struct with attributes: |
| * deps: {type}`list[str]` dependencies to include unconditionally. |
| * deps_select: {type}`dict[str, list[str]]` dependencies to include on particular |
| subset of target platforms. |
| """ |
| reqs = sorted( |
| [requirement(r) for r in requires_dist], |
| key = lambda x: "{}:{}:".format(x.name, sorted(x.extras), x.marker), |
| ) |
| deps = {} |
| deps_select = {} |
| name = normalize_name(name) |
| want_extras = _resolve_extras(name, reqs, extras) |
| |
| # drop self edges |
| reqs = [r for r in reqs if r.name != name] |
| |
| platforms = [ |
| platform_from_str(p, python_version = host_python_version) |
| for p in platforms |
| ] or [ |
| platform_from_str("", python_version = host_python_version), |
| ] |
| |
| abis = sorted({p.abi: True for p in platforms if p.abi}) |
| if host_python_version and len(abis) > 1: |
| _, _, minor_version = host_python_version.partition(".") |
| minor_version, _, _ = minor_version.partition(".") |
| default_abi = "cp3" + minor_version |
| elif len(abis) > 1: |
| fail( |
| "all python versions need to be specified explicitly, got: {}".format(platforms), |
| ) |
| else: |
| default_abi = None |
| |
| for req in reqs: |
| _add_req( |
| deps, |
| deps_select, |
| req, |
| extras = want_extras, |
| platforms = platforms, |
| default_abi = default_abi, |
| ) |
| |
| return struct( |
| deps = sorted(deps), |
| deps_select = { |
| _platform_str(p): sorted(deps) |
| for p, deps in deps_select.items() |
| }, |
| ) |
| |
| def _platform_str(self): |
| if self.abi == None: |
| if not self.os and not self.arch: |
| return "//conditions:default" |
| elif not self.arch: |
| return "@platforms//os:{}".format(self.os) |
| else: |
| return "{}_{}".format(self.os, self.arch) |
| |
| minor_version = self.abi[3:] |
| if self.arch == None and self.os == None: |
| return str(Label("//python/config_settings:is_python_3.{}".format(minor_version))) |
| |
| return "cp3{}_{}_{}".format( |
| minor_version, |
| self.os or "anyos", |
| self.arch or "anyarch", |
| ) |
| |
| def _platform_specializations(self, cpu_values = _ALL_ARCH_VALUES, os_values = _ALL_OS_VALUES): |
| """Return the platform itself and all its unambiguous specializations. |
| |
| For more info about specializations see |
| https://bazel.build/docs/configurable-attributes |
| """ |
| specializations = [] |
| specializations.append(self) |
| if self.arch == None: |
| specializations.extend([ |
| platform(os = self.os, arch = arch, abi = self.abi) |
| for arch in cpu_values |
| ]) |
| if self.os == None: |
| specializations.extend([ |
| platform(os = os, arch = self.arch, abi = self.abi) |
| for os in os_values |
| ]) |
| if self.os == None and self.arch == None: |
| specializations.extend([ |
| platform(os = os, arch = arch, abi = self.abi) |
| for os in os_values |
| for arch in cpu_values |
| ]) |
| return specializations |
| |
| def _add(deps, deps_select, dep, platform): |
| dep = normalize_name(dep) |
| |
| if platform == None: |
| deps[dep] = True |
| |
| # If the dep is in the platform-specific list, remove it from the select. |
| pop_keys = [] |
| for p, _deps in deps_select.items(): |
| if dep not in _deps: |
| continue |
| |
| _deps.pop(dep) |
| if not _deps: |
| pop_keys.append(p) |
| |
| for p in pop_keys: |
| deps_select.pop(p) |
| return |
| |
| if dep in deps: |
| # If the dep is already in the main dependency list, no need to add it in the |
| # platform-specific dependency list. |
| return |
| |
| # Add the platform-specific branch |
| deps_select.setdefault(platform, {}) |
| |
| # Add the dep to specializations of the given platform if they |
| # exist in the select statement. |
| for p in _platform_specializations(platform): |
| if p not in deps_select: |
| continue |
| |
| deps_select[p][dep] = True |
| |
| if len(deps_select[platform]) == 1: |
| # We are adding a new item to the select and we need to ensure that |
| # existing dependencies from less specialized platforms are propagated |
| # to the newly added dependency set. |
| for p, _deps in deps_select.items(): |
| # Check if the existing platform overlaps with the given platform |
| if p == platform or platform not in _platform_specializations(p): |
| continue |
| |
| deps_select[platform].update(_deps) |
| |
| def _maybe_add_common_dep(deps, deps_select, platforms, dep): |
| abis = sorted({p.abi: True for p in platforms if p.abi}) |
| if len(abis) < 2: |
| return |
| |
| platforms = [platform()] + [ |
| platform(abi = abi) |
| for abi in abis |
| ] |
| |
| # If the dep is targeting all target python versions, lets add it to |
| # the common dependency list to simplify the select statements. |
| for p in platforms: |
| if p not in deps_select: |
| return |
| |
| if dep not in deps_select[p]: |
| return |
| |
| # All of the python version-specific branches have the dep, so lets add |
| # it to the common deps. |
| deps[dep] = True |
| for p in platforms: |
| deps_select[p].pop(dep) |
| if not deps_select[p]: |
| deps_select.pop(p) |
| |
| def _resolve_extras(self_name, reqs, extras): |
| """Resolve extras which are due to depending on self[some_other_extra]. |
| |
| Some packages may have cyclic dependencies resulting from extras being used, one example is |
| `etils`, where we have one set of extras as aliases for other extras |
| and we have an extra called 'all' that includes all other extras. |
| |
| Example: github.com/google/etils/blob/a0b71032095db14acf6b33516bca6d885fe09e35/pyproject.toml#L32. |
| |
| When the `requirements.txt` is generated by `pip-tools`, then it is likely that |
| this step is not needed, but for other `requirements.txt` files this may be useful. |
| |
| NOTE @aignas 2023-12-08: the extra resolution is not platform dependent, |
| but in order for it to become platform dependent we would have to have |
| separate targets for each extra in extras. |
| """ |
| |
| # Resolve any extra extras due to self-edges, empty string means no |
| # extras The empty string in the set is just a way to make the handling |
| # of no extras and a single extra easier and having a set of {"", "foo"} |
| # is equivalent to having {"foo"}. |
| extras = extras or [""] |
| |
| self_reqs = [] |
| for req in reqs: |
| if req.name != self_name: |
| continue |
| |
| if req.marker == None: |
| # I am pretty sure we cannot reach this code as it does not |
| # make sense to specify packages in this way, but since it is |
| # easy to handle, lets do it. |
| # |
| # TODO @aignas 2023-12-08: add a test |
| extras = extras + req.extras |
| else: |
| # process these in a separate loop |
| self_reqs.append(req) |
| |
| # A double loop is not strictly optimal, but always correct without recursion |
| for req in self_reqs: |
| if [True for extra in extras if evaluate(req.marker, env = {"extra": extra})]: |
| extras = extras + req.extras |
| else: |
| continue |
| |
| # Iterate through all packages to ensure that we include all of the extras from previously |
| # visited packages. |
| for req_ in self_reqs: |
| if [True for extra in extras if evaluate(req.marker, env = {"extra": extra})]: |
| extras = extras + req_.extras |
| |
| # Poor mans set |
| return sorted({x: None for x in extras}) |
| |
| def _add_req(deps, deps_select, req, *, extras, platforms, default_abi = None): |
| if not req.marker: |
| _add(deps, deps_select, req.name, None) |
| return |
| |
| # NOTE @aignas 2023-12-08: in order to have reasonable select statements |
| # we do have to have some parsing of the markers, so it begs the question |
| # if packaging should be reimplemented in Starlark to have the best solution |
| # for now we will implement it in Python and see what the best parsing result |
| # can be before making this decision. |
| match_os = len([ |
| tag |
| for tag in [ |
| "os_name", |
| "sys_platform", |
| "platform_system", |
| ] |
| if tag in req.marker |
| ]) > 0 |
| match_arch = "platform_machine" in req.marker |
| match_version = "version" in req.marker |
| |
| if not (match_os or match_arch or match_version): |
| if [ |
| True |
| for extra in extras |
| for p in platforms |
| if evaluate( |
| req.marker, |
| env = env( |
| target_platform = p, |
| extra = extra, |
| ), |
| ) |
| ]: |
| _add(deps, deps_select, req.name, None) |
| return |
| |
| for plat in platforms: |
| if not [ |
| True |
| for extra in extras |
| if evaluate( |
| req.marker, |
| env = env( |
| target_platform = plat, |
| extra = extra, |
| ), |
| ) |
| ]: |
| continue |
| |
| if match_arch and default_abi: |
| _add(deps, deps_select, req.name, plat) |
| if plat.abi == default_abi: |
| _add(deps, deps_select, req.name, platform(os = plat.os, arch = plat.arch)) |
| elif match_arch: |
| _add(deps, deps_select, req.name, platform(os = plat.os, arch = plat.arch)) |
| elif match_os and default_abi: |
| _add(deps, deps_select, req.name, platform(os = plat.os, abi = plat.abi)) |
| if plat.abi == default_abi: |
| _add(deps, deps_select, req.name, platform(os = plat.os)) |
| elif match_os: |
| _add(deps, deps_select, req.name, platform(os = plat.os)) |
| elif match_version and default_abi: |
| _add(deps, deps_select, req.name, platform(abi = plat.abi)) |
| if plat.abi == default_abi: |
| _add(deps, deps_select, req.name, platform()) |
| elif match_version: |
| _add(deps, deps_select, req.name, None) |
| else: |
| fail("BUG: {} support is not implemented".format(req.marker)) |
| |
| _maybe_add_common_dep(deps, deps_select, platforms, req.name) |