Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config_settings): add extra settings for toolchain platforms #1743

Closed
wants to merge 13 commits into from
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ A brief description of the categories of changes:
* (coverage) Add support for python 3.12 and bump `coverage.py` to
7.4.1.

* (toolchain) The variants of `is_python` `config_setting` including the
target os and cpu values have been added for better ergonomics for matching
both, the python version and the os/cpu value in a single select statement.
They are added mainly for internal use and the API might change at any time
as needed by the internal repository rules.

## 0.29.0 - 2024-01-22

[0.29.0]: https://github.com/bazelbuild/rules_python/releases/tag/0.29.0
Expand Down
5 changes: 4 additions & 1 deletion python/config_settings/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ filegroup(

construct_config_settings(
name = "construct_config_settings",
python_versions = TOOL_VERSIONS.keys(),
python_versions = {
version: metadata["sha256"].keys()
for version, metadata in TOOL_VERSIONS.items()
},
)
176 changes: 129 additions & 47 deletions python/config_settings/config_settings.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def construct_config_settings(name, python_versions):
Args:
name: str, unused; only specified to satisfy buildifier lint checks
and allow programatic modification of the target.
python_versions: list of all (x.y.z) Python versions supported by rules_python.
python_versions: a dict of all (x.y.z) Python versions with supported
platforms as a list for each version. This should be in the same
format as in //python:versions.bzl#TOOL_VERSIONS.
"""

# Maps e.g. "3.8" -> ["3.8.1", "3.8.2", etc]
Expand All @@ -51,55 +53,58 @@ def construct_config_settings(name, python_versions):
)

for minor_version, micro_versions in minor_to_micro_versions.items():
_config_settings_for_minor_version(
minor_version = minor_version,
micro_versions = {
v: [
_parse_platform(plat)
for plat in python_versions[v]
]
for v in micro_versions
},
)

def _parse_platform(plat):
cpu, _, os = plat.partition("-")
if "linux" in os:
os = "linux"
elif "darwin" in os:
os = "osx"
elif "windows" in os:
os = "windows"
else:
fail("unknown os: {}".format(os))

return (os, cpu)

def _config_settings_for_minor_version(*, minor_version, micro_versions):
"""Constructs a set of configs for all Python versions.
"""
matches_minor_version_names = {}
constraint_values_by_suffix = {}

for micro_version, plats in micro_versions.items():
for suffix, constraint_values in _constraint_values(plats).items():
matches_micro_version = _micro_version_condition(
micro_version = micro_version,
suffix = suffix,
constraint_values = constraint_values,
)
matches_minor_version_names.setdefault(suffix, []).append(matches_micro_version)
constraint_values_by_suffix[suffix] = constraint_values

for suffix, constraint_values in constraint_values_by_suffix.items():
# This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8
# It's private because matching the concept of e.g. "3.8" value is done
# using the `is_python_X.Y` config setting group, which is aware of the
# minor versions that could match instead.
equals_minor_version_name = "_python_version_flag_equals_" + minor_version
equals_minor_version = "_python_version_flag_equals_" + minor_version + suffix
native.config_setting(
name = equals_minor_version_name,
name = equals_minor_version,
flag_values = {":python_version": minor_version},
constraint_values = constraint_values,
)
matches_minor_version_names = [equals_minor_version_name]

default_micro_version = MINOR_MAPPING[minor_version]

for micro_version in micro_versions:
is_micro_version_name = "is_python_" + micro_version
if default_micro_version != micro_version:
native.config_setting(
name = is_micro_version_name,
flag_values = {":python_version": micro_version},
visibility = ["//visibility:public"],
)
matches_minor_version_names.append(is_micro_version_name)
continue

# Ensure that is_python_3.9.8 is matched if python_version is set
# to 3.9 if MINOR_MAPPING points to 3.9.8
equals_micro_name = "_python_version_flag_equals_" + micro_version
native.config_setting(
name = equals_micro_name,
flag_values = {":python_version": micro_version},
)

# An alias pointing to an underscore-prefixed config_setting_group
# is used because config_setting_group creates
# `is_{minor}_N` targets, which are easily confused with the
# `is_{minor}.{micro}` (dot) targets.
selects.config_setting_group(
name = "_" + is_micro_version_name,
match_any = [
equals_micro_name,
equals_minor_version_name,
],
)
native.alias(
name = is_micro_version_name,
actual = "_" + is_micro_version_name,
visibility = ["//visibility:public"],
)
matches_minor_version_names.append(equals_micro_name)
matches_minor_version_names[suffix].append(equals_minor_version)

# This is prefixed with an underscore to prevent confusion due to how
# config_setting_group is implemented and how our micro-version targets
Expand All @@ -108,12 +113,89 @@ def construct_config_settings(name, python_versions):
# Meanwhile, the micro-version tarets are named "is_python_3.10.1" --
# just a single dot vs underscore character difference.
selects.config_setting_group(
name = "_is_python_" + minor_version,
match_any = matches_minor_version_names,
name = "_is_python_" + minor_version + suffix,
match_any = matches_minor_version_names[suffix],
)

native.alias(
name = "is_python_" + minor_version,
actual = "_is_python_" + minor_version,
name = "is_python_" + minor_version + suffix,
actual = "_is_python_" + minor_version + suffix,
visibility = ["//visibility:public"],
)

def _constraint_values(plats):
"""For a list of (os, cpu) tuples get all possible platform names and constraint values.
"""
ret = {
# This is the no platform constraint values version
"": [],
}
os_cpus = []
for (os, cpu) in plats:
# Normalize the cpu names to the ones present in the `@platforms//cpu`
if cpu == "ppc64le":
cpu = "ppc"

if (os, None) not in os_cpus:
# Add only the OS constraint value
os_cpus.append((os, None))
if (None, cpu) not in os_cpus:
# Add only the CPU constraint value
os_cpus.append((None, cpu))

# Add both OS and CPU constraint values
os_cpus.append((os, cpu))

for (os, cpu) in os_cpus:
constraint_values = []
parts = [""]
if os:
constraint_values.append("@platforms//os:{}".format(os))
parts.append(os)
if cpu:
constraint_values.append("@platforms//cpu:{}".format(cpu))
parts.append(cpu)

ret["_".join(parts)] = constraint_values

return ret

def _micro_version_condition(*, micro_version, suffix, constraint_values):
minor_version, _, _ = micro_version.rpartition(".")
is_micro_version_name = "is_python_" + micro_version + suffix
if MINOR_MAPPING[minor_version] != micro_version:
native.config_setting(
name = is_micro_version_name,
flag_values = {":python_version": micro_version},
constraint_values = constraint_values,
visibility = ["//visibility:public"],
)
return is_micro_version_name

# Ensure that is_python_3.9.8 is matched if python_version is set
# to 3.9 if MINOR_MAPPING points to 3.9.8
equals_micro_name = "_python_version_flag_equals_" + micro_version + suffix
equals_minor_name = "_python_version_flag_equals_" + minor_version + suffix
native.config_setting(
name = equals_micro_name,
flag_values = {":python_version": micro_version},
constraint_values = constraint_values,
)

# An alias pointing to an underscore-prefixed config_setting_group
# is used because config_setting_group creates
# `is_{minor}_N` targets, which are easily confused with the
# `is_{minor}.{micro}` (dot) targets.
selects.config_setting_group(
name = "_" + is_micro_version_name,
match_any = [
equals_micro_name,
equals_minor_name,
],
)
native.alias(
name = is_micro_version_name,
actual = "_" + is_micro_version_name,
visibility = ["//visibility:public"],
)
return equals_micro_name
78 changes: 19 additions & 59 deletions python/pip_install/private/generate_whl_library_build_bazel.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -141,85 +141,48 @@ def _render_list_and_select(deps, deps_by_platform, tmpl):
return "{} + {}".format(deps, deps_by_platform)

def _render_config_settings(dependencies_by_platform):
py_version_by_os_arch = {}
plats = []
for p in dependencies_by_platform:
# p can be one of the following formats:
# * @platforms//os:{value}
# * @platforms//cpu:{value}
# * @//python/config_settings:is_python_3.{minor_version}
# * @//python/config_settings:is_python_3.{minor_version}_{os}_{cpu}
# * @//python/config_settings:is_python_3.{minor_version}_{os}
# * @//python/config_settings:is_python_3.{minor_version}_{cpu}
# * {os}_{cpu}
# * cp3{minor_version}_{os}_{cpu}
if p.startswith("@"):
continue

abi, _, tail = p.partition("_")
if not abi.startswith("cp"):
tail = p
abi = ""
os, _, arch = tail.partition("_")
os = "" if os == "anyos" else os
arch = "" if arch == "anyarch" else arch
os, _, arch = p.partition("_")
os = "" if os == "any" else os
arch = "" if arch == "any" else arch

py_version_by_os_arch.setdefault((os, arch), []).append(abi)
plats.append((os, arch))

if not py_version_by_os_arch:
return None, None
if not plats:
return None

loads = []
additional_content = []
for (os, arch), abis in py_version_by_os_arch.items():
constraint_values = []
if os:
constraint_values.append("@platforms//os:{}".format(os))
if arch:
constraint_values.append("@platforms//cpu:{}".format(arch))

os_arch = (os or "anyos") + "_" + (arch or "anyarch")
for (os, arch) in plats:
constraint_values = [
"@platforms//os:{}".format(os),
"@platforms//cpu:{}".format(arch),
]

additional_content.append(
"""\
config_setting(
name = "is_{name}",
constraint_values = {values},
visibility = ["//visibility:private"],
)""".format(
name = os_arch,
name = "{}_{}".format(os, arch),
values = render.indent(render.list(sorted([str(Label(c)) for c in constraint_values]))).strip(),
),
)

if abis == [""]:
if not os or not arch:
fail("BUG: both os and arch should be set in this case")
continue

for abi in abis:
if not loads:
loads.append("""load("@bazel_skylib//lib:selects.bzl", "selects")""")
minor_version = int(abi[len("cp3"):])
setting = "@@{rules_python}//python/config_settings:is_python_3.{version}".format(
rules_python = str(Label("//:BUILD.bazel")).partition("//")[0].strip("@"),
version = minor_version,
)
settings = [
":is_" + os_arch,
setting,
]

plat = "{}_{}".format(abi, os_arch)

additional_content.append(
"""\
selects.config_setting_group(
name = "{name}",
match_all = {values},
visibility = ["//visibility:private"],
)""".format(
name = _plat_label(plat).lstrip(":"),
values = render.indent(render.list(sorted(settings))).strip(),
),
)

return loads, "\n\n".join(additional_content)
return "\n\n".join(additional_content)

def generate_whl_library_build_bazel(
*,
Expand Down Expand Up @@ -328,11 +291,8 @@ def generate_whl_library_build_bazel(
"""load("@bazel_skylib//rules:copy_file.bzl", "copy_file")""",
]

loads_, config_settings_content = _render_config_settings(dependencies_by_platform)
config_settings_content = _render_config_settings(dependencies_by_platform)
if config_settings_content:
for line in loads_:
if line not in loads:
loads.append(line)
additional_content.append(config_settings_content)

lib_dependencies = _render_list_and_select(
Expand Down
15 changes: 8 additions & 7 deletions python/pip_install/tools/wheel_installer/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,17 @@ def __str__(self) -> str:
else:
return f"{self.os}_{self.arch}"

prefix = f"@//python/config_settings:is_python_3.{self.minor_version}"
if self.arch is None and self.os is None:
return f"@//python/config_settings:is_python_3.{self.minor_version}"
return prefix

if self.arch is None:
return f"cp3{self.minor_version}_{self.os}_anyarch"

if self.os is None:
return f"cp3{self.minor_version}_anyos_{self.arch}"
suffix = [prefix]
if self.os:
suffix.append(str(self.os))
if self.arch:
suffix.append(str(self.arch))

return f"cp3{self.minor_version}_{self.os}_{self.arch}"
return "_".join(suffix)

@classmethod
def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]:
Expand Down
15 changes: 12 additions & 3 deletions python/pip_install/tools/wheel_installer/wheel_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,18 @@ def test_can_get_version_select(self):
self.assertEqual(
{
"@//python/config_settings:is_python_3.7": ["baz"],
"cp37_linux_anyarch": ["baz", "posix_dep"],
"cp38_linux_anyarch": ["posix_dep", "posix_dep_with_version"],
"cp39_linux_anyarch": ["posix_dep", "posix_dep_with_version"],
"@//python/config_settings:is_python_3.7_linux": [
"baz",
"posix_dep",
],
"@//python/config_settings:is_python_3.8_linux": [
"posix_dep",
"posix_dep_with_version",
],
"@//python/config_settings:is_python_3.9_linux": [
"posix_dep",
"posix_dep_with_version",
],
},
got.deps_select,
)
Expand Down
Loading
Loading