Skip to content

refactor!(toolchain): remove uname dep in the repository_rule stage #2406

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

Merged
merged 16 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,37 @@ Unreleased changes template.

{#v0-0-0-changed}
### Changed
* Nothing yet.

**Breaking**:
* (toolchains) stop exposing config settings in python toolchain alias repos.
Please consider depending on the flags defined in
`//python/config_setting/...` and the `@platforms` package instead.
* (toolchains) consumers who were depending on the `MACOS_NAME` and the `arch`
attribute in the `PLATFORMS` list, please update your code to respect the new
values. The values now correspond to the values available in the
`@platforms//` package constraint values.
* (toolchains) `host_platform` and `interpreter` constants are no longer created
in the `toolchain` generated alias `.bzl` files. If you need to access the
host interpreter during the `repository_rule` evaluation, please use the
`@python_{version}_host//:python` targets created by
{bzl:obj}`python_register_toolchains` and
{bzl:obj}`python_register_multi_toolchains` macros or the {bzl:obj}`python`
bzlmod extension.

Other changes:
* Nothing yet

{#v0-0-0-fixed}
### Fixed
* Nothing yet.
* (toolchains) stop depending on `uname` to get the value of the host platform.

{#v0-0-0-added}
### Added
* Nothing yet.
* (toolchains) allow users to select which variant of the support host toolchain
they would like to use through
`RULES_PYTHON_REPO_TOOLCHAIN_{VERSION}_{OS}_{ARCH}` env variable setting. For
example, this allows one to use `freethreaded` python interpreter in the
`repository_rule` to build a wheel from `sdist`.

{#v0-0-0-removed}
### Removed
Expand Down
2 changes: 1 addition & 1 deletion WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ load("@rules_python_gazelle_plugin//:deps.bzl", _py_gazelle_deps = "gazelle_deps
_py_gazelle_deps()

# This interpreter is used for various rules_python dev-time tools
load("@python//3.11.9:defs.bzl", "interpreter")
interpreter = "@python_3_11_9_host//:python"

#####################
# Install twine for our own runfiles wheel publishing.
Expand Down
6 changes: 5 additions & 1 deletion python/private/python_register_toolchains.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ def python_register_toolchains(
platform = platform,
))

host_toolchain(name = name + "_host")
host_toolchain(
name = name + "_host",
platforms = loaded_platforms,
python_version = python_version,
)

toolchain_aliases(
name = name,
Expand Down
74 changes: 74 additions & 0 deletions python/private/toolchain_aliases.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright 2024 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.

"""Create toolchain alias targets."""

load("@rules_python//python:versions.bzl", "PLATFORMS")

def toolchain_aliases(*, name, platforms, visibility = None, native = native):
"""Cretae toolchain aliases for the python toolchains.

Args:
name: {type}`str` The name of the current repository.
platforms: {type}`platforms` The list of platforms that are supported
for the current toolchain repository.
visibility: {type}`list[Target] | None` The visibility of the aliases.
native: The native struct used in the macro, useful for testing.
"""
for platform in PLATFORMS.keys():
if platform not in platforms:
continue

native.config_setting(
name = platform,
flag_values = PLATFORMS[platform].flag_values,
constraint_values = PLATFORMS[platform].compatible_with,
visibility = ["//visibility:private"],
)

prefix = name
for name in [
"files",
"includes",
"libpython",
"py3_runtime",
"python_headers",
"python_runtimes",
]:
native.alias(
name = name,
actual = select({
":" + platform: "@{}_{}//:{}".format(prefix, platform, name)
for platform in platforms
}),
visibility = visibility,
)

native.alias(
name = "python3",
actual = select({
":" + platform: "@{}_{}//:{}".format(prefix, platform, "python.exe" if "windows" in platform else "bin/python3")
for platform in platforms
}),
visibility = visibility,
)
native.alias(
name = "pip",
actual = select({
":" + platform: "@{}_{}//:python_runtimes".format(prefix, platform)
for platform in platforms
if "windows" not in platform
}),
visibility = visibility,
)
143 changes: 59 additions & 84 deletions python/private/toolchains_repo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ platform-specific repositories.

load(
"//python:versions.bzl",
"LINUX_NAME",
"MACOS_NAME",
"PLATFORMS",
"WINDOWS_NAME",
)
Expand Down Expand Up @@ -126,43 +124,26 @@ toolchains_repo = repository_rule(
)

def _toolchain_aliases_impl(rctx):
logger = repo_utils.logger(rctx)
(os_name, arch) = _get_host_os_arch(rctx, logger)

host_platform = _get_host_platform(os_name, arch)

is_windows = (os_name == WINDOWS_NAME)
python3_binary_path = "python.exe" if is_windows else "bin/python3"

# Base BUILD file for this repository.
build_contents = """\
# Generated by python/private/toolchains_repo.bzl
load("@rules_python//python/private:toolchain_aliases.bzl", "toolchain_aliases")

package(default_visibility = ["//visibility:public"])
load("@rules_python//python:versions.bzl", "gen_python_config_settings")
gen_python_config_settings()

exports_files(["defs.bzl"])

PLATFORMS = [
{loaded_platforms}
]
alias(name = "files", actual = select({{":" + item: "@{py_repository}_" + item + "//:files" for item in PLATFORMS}}))
alias(name = "includes", actual = select({{":" + item: "@{py_repository}_" + item + "//:includes" for item in PLATFORMS}}))
alias(name = "libpython", actual = select({{":" + item: "@{py_repository}_" + item + "//:libpython" for item in PLATFORMS}}))
alias(name = "py3_runtime", actual = select({{":" + item: "@{py_repository}_" + item + "//:py3_runtime" for item in PLATFORMS}}))
alias(name = "python_headers", actual = select({{":" + item: "@{py_repository}_" + item + "//:python_headers" for item in PLATFORMS}}))
alias(name = "python_runtimes", actual = select({{":" + item: "@{py_repository}_" + item + "//:python_runtimes" for item in PLATFORMS}}))
alias(name = "python3", actual = select({{":" + item: "@{py_repository}_" + item + "//:" + ("python.exe" if "windows" in item else "bin/python3") for item in PLATFORMS}}))
toolchain_aliases(
name = "{py_repository}",
platforms = PLATFORMS,
)
""".format(
py_repository = rctx.attr.user_repository_name,
loaded_platforms = "\n".join([" \"{}\",".format(p) for p in rctx.attr.platforms]),
)
if not is_windows:
build_contents += """\
alias(name = "pip", actual = select({{":" + item: "@{py_repository}_" + item + "//:python_runtimes" for item in PLATFORMS if "windows" not in item}}))
""".format(
py_repository = rctx.attr.user_repository_name,
host_platform = host_platform,
)
rctx.file("BUILD.bazel", build_contents)

# Expose a Starlark file so rules can know what host platform we used and where to find an interpreter
Expand All @@ -181,9 +162,6 @@ load(
)
load("{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements")

host_platform = "{host_platform}"
interpreter = "@{py_repository}_{host_platform}//:{python3_binary_path}"

def py_binary(name, **kwargs):
return _py_binary(
name = name,
Expand Down Expand Up @@ -214,10 +192,7 @@ def compile_pip_requirements(name, **kwargs):
)

""".format(
host_platform = host_platform,
py_repository = rctx.attr.user_repository_name,
python_version = rctx.attr.python_version,
python3_binary_path = python3_binary_path,
rules_python = get_repository_name(rctx.attr._rules_python_workspace),
))

Expand All @@ -243,15 +218,21 @@ actions.""",
)

def _host_toolchain_impl(rctx):
logger = repo_utils.logger(rctx)
rctx.file("BUILD.bazel", """\
# Generated by python/private/toolchains_repo.bzl

exports_files(["python"], visibility = ["//visibility:public"])
""")

(os_name, arch) = _get_host_os_arch(rctx, logger)
host_platform = _get_host_platform(os_name, arch)
os_name = repo_utils.get_platforms_os_name(rctx)
host_platform = _get_host_platform(
rctx = rctx,
logger = repo_utils.logger(rctx),
python_version = rctx.attr.python_version,
os_name = os_name,
cpu_name = repo_utils.get_platforms_cpu_name(rctx),
platforms = rctx.attr.platforms,
)
repo = "@@{py_repository}_{host_platform}".format(
py_repository = rctx.attr.name[:-len("_host")],
host_platform = host_platform,
Expand Down Expand Up @@ -320,6 +301,8 @@ toolchain_aliases repo because referencing the `python` interpreter target from
this repo causes an eager fetch of the toolchain for the host platform.
""",
attrs = {
"platforms": attr.string_list(mandatory = True),
"python_version": attr.string(mandatory = True),
"_rule_name": attr.string(default = "host_toolchain"),
"_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
},
Expand All @@ -336,16 +319,12 @@ def _multi_toolchain_aliases_impl(rctx):
load(
"@{repository_name}//:defs.bzl",
_compile_pip_requirements = "compile_pip_requirements",
_host_platform = "host_platform",
_interpreter = "interpreter",
_py_binary = "py_binary",
_py_console_script_binary = "py_console_script_binary",
_py_test = "py_test",
)

compile_pip_requirements = _compile_pip_requirements
host_platform = _host_platform
interpreter = _interpreter
py_binary = _py_binary
py_console_script_binary = _py_console_script_binary
py_test = _py_test
Expand Down Expand Up @@ -388,57 +367,53 @@ multi_toolchain_aliases = repository_rule(
def sanitize_platform_name(platform):
return platform.replace("-", "_")

def _get_host_platform(os_name, arch):
def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platforms):
"""Gets the host platform.

Args:
os_name: the host OS name.
arch: the host arch.
rctx: {type}`repository_ctx`.
logger: {type}`struct`.
python_version: {type}`string`.
os_name: {type}`str` the host OS name.
cpu_name: {type}`str` the host CPU name.
platforms: {type}`list[str]` the list of loaded platforms.
Returns:
The host platform.
"""
host_platform = None
for platform, meta in PLATFORMS.items():
if "freethreaded" in platform:
continue

if meta.os_name == os_name and meta.arch == arch:
host_platform = platform
if not host_platform:
fail("No platform declared for host OS {} on arch {}".format(os_name, arch))
return host_platform
candidates = []
for platform in platforms:
meta = PLATFORMS[platform]

def _get_host_os_arch(rctx, logger):
"""Infer the host OS name and arch from a repository context.
if meta.os_name == os_name and meta.arch == cpu_name:
candidates.append(platform)

Args:
rctx: Bazel's repository_ctx.
logger: Logger to use for operations.
if len(candidates) == 1:
return candidates[0]

Returns:
A tuple with the host OS name and arch.
"""
os_name = rctx.os.name

# We assume the arch for Windows is always x86_64.
if "windows" in os_name.lower():
arch = "x86_64"

# Normalize the os_name. E.g. os_name could be "OS windows server 2019".
os_name = WINDOWS_NAME
else:
# This is not ideal, but bazel doesn't directly expose arch.
arch = repo_utils.execute_unchecked(
rctx,
op = "GetUname",
arguments = [repo_utils.which_checked(rctx, "uname"), "-m"],
logger = logger,
).stdout.strip()

# Normalize the os_name.
if "mac" in os_name.lower():
os_name = MACOS_NAME
elif "linux" in os_name.lower():
os_name = LINUX_NAME

return (os_name, arch)
if candidates:
env_var = "RULES_PYTHON_REPO_TOOLCHAIN_{}_{}_{}".format(
python_version.replace(".", "_"),
os_name.upper(),
cpu_name.upper(),
)
preference = repo_utils.getenv(rctx, env_var)
if preference == None:
logger.info("Consider using '{}' to select from one of the platforms: {}".format(
env_var,
candidates,
))
elif preference not in candidates:
logger.fail("Please choose a preferred interpreter out of the following platforms: {}".format(candidates))
return None
else:
candidates = [preference]

if candidates:
return candidates[0]

logger.fail("Could not find a compatible 'host' python for '{os_name}', '{cpu_name}' from the loaded platforms: {platforms}".format(
os_name = os_name,
cpu_name = cpu_name,
platforms = platforms,
))
return None
Loading