From 47aea4efbd189502d52c431a3acbb41527ad3ad3 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 12 Oct 2026 21:43:24 -0700 Subject: [PATCH 1/3] feat(packaging): support Python toolchain make variables in py_wheel Setting version-specific wheel tags (`python_tag`, `abi`, `platform`) and `python_requires` metadata in `py_wheel` previously required custom `select()` expressions or `TemplateVariableInfo` rules. Additionally, `platform` and `python_requires` did not expand make variables. Populate `TemplateVariableInfo` on `py_runtime_pair` with Python version, implementation, and wheel tag variables (such as `PY_WHEEL_PYTHON_TAG`, `PY_WHEEL_ABI_TAG`, and `PY_MAJOR_DOT_MINOR`), and forward them through `current_py_toolchain`. Attach the target Python toolchain to `py_wheel` so these variables are available by default, and expand make variables in `platform` and `python_requires`. --- docs/howto/build-a-wheel.md | 85 +++++++++++++- news/py-wheel-makevars.added.md | 3 + python/current_py_toolchain.bzl | 24 +++- python/packaging.bzl | 3 +- python/private/BUILD.bazel | 2 + python/private/py_runtime_pair_rule.bzl | 97 ++++++++++++++-- python/private/py_wheel.bzl | 61 ++++++++-- python/py_runtime_pair.bzl | 15 +-- .../py_runtime_pair/py_runtime_pair_tests.bzl | 54 +++++++++ tests/py_wheel/py_wheel_tests.bzl | 107 ++++++++++++++++++ 10 files changed, 416 insertions(+), 35 deletions(-) create mode 100644 news/py-wheel-makevars.added.md diff --git a/docs/howto/build-a-wheel.md b/docs/howto/build-a-wheel.md index 0248ff802b..d93155f9c4 100644 --- a/docs/howto/build-a-wheel.md +++ b/docs/howto/build-a-wheel.md @@ -84,4 +84,87 @@ It's highly recommended to disable this behavior by setting a flag in your ``` # .bazelrc build --incompatible_default_to_explicit_init_py=true -``` \ No newline at end of file +``` + +## Setting wheel filename tags from the Python version + +To automatically generate the wheel filename and `Requires-Python` metadata for +the active Python version, use make variable syntax in `abi`, `platform`, +`python_requires`, `python_tag`, and `version`: + +```starlark +# BUILD.bazel +load("@rules_python//python:packaging.bzl", "py_wheel") + +py_wheel( + name = "my_project_wheel", + abi = "$(PY_WHEEL_ABI_TAG)", + distribution = "my-project", + python_requires = ">=$(PY_MAJOR_DOT_MINOR)", + python_tag = "$(PY_WHEEL_PYTHON_TAG)", + version = "0.1.0", + deps = [":my_project_lib"], +) +``` + +When built with CPython 3.12, this produces +`my_project-0.1.0-cp312-cp312-any.whl` and sets `Requires-Python: >=3.12` in the +wheel `METADATA`. With freethreaded CPython 3.13, `$(PY_WHEEL_ABI_TAG)` expands +to `cp313t`. + +### Available Python version variables + +The Python toolchain provides the following make variables by default: + +| Variable | Description | Example | +| :--- | :--- | :--- | +| `$(PY_WHEEL_PYTHON_TAG)` | Wheel Python tag | `cp312` | +| `$(PY_WHEEL_ABI_TAG)` | Wheel ABI tag | `cp312`, `cp313t` | +| `$(PY_WHEEL_IMPLEMENTATION_TAG)` | Wheel implementation prefix | `cp` | +| `$(PY_MAJOR_DOT_MINOR)` | Dotted `major.minor` version | `3.12` | +| `$(PY_MAJOR_MINOR_NODOT)` | Dotless `majorminor` version | `312` | +| `$(PY_MAJOR)` | Major version | `3` | +| `$(PY_MINOR)` | Minor version | `12` | +| `$(PY_ABI_FLAGS)` | Runtime ABI flags (`sys.abiflags`) | `""`, `"t"` | +| `$(PY_IMPLEMENTATION_NAME)` | Implementation name | `cpython` | + +See {rule}`py_runtime_pair` for the full list of variables. + +### Adding custom make variables + +To define custom make variables (for example, to compute the package version or +platform tag in Starlark), write a rule that returns +`platform_common.TemplateVariableInfo` and pass it in the `toolchains` attribute +of {obj}`py_wheel`: + +```starlark +# wheel_vars.bzl +def _wheel_vars_impl(ctx): + return [platform_common.TemplateVariableInfo({ + "MY_VERSION": "1.2.3", + })] + +wheel_vars = rule( + implementation = _wheel_vars_impl, +) +``` + +```starlark +# BUILD.bazel +load("@rules_python//python:packaging.bzl", "py_wheel") +load(":wheel_vars.bzl", "wheel_vars") + +wheel_vars( + name = "vars", +) + +py_wheel( + name = "my_project_wheel", + abi = "$(PY_WHEEL_ABI_TAG)", + distribution = "my-project", + python_tag = "$(PY_WHEEL_PYTHON_TAG)", + toolchains = [":vars"], + version = "$(MY_VERSION)", + deps = [":my_project_lib"], +) +``` diff --git a/news/py-wheel-makevars.added.md b/news/py-wheel-makevars.added.md new file mode 100644 index 0000000000..2ae604c58d --- /dev/null +++ b/news/py-wheel-makevars.added.md @@ -0,0 +1,3 @@ +(packaging) {obj}`py_wheel` now expands Python toolchain make variables by +default and in `platform` and `python_requires`. {obj}`py_runtime_pair` and +{obj}`current_py_toolchain` now provide version and wheel tag make variables. diff --git a/python/current_py_toolchain.bzl b/python/current_py_toolchain.bzl index 4fb9eb9eed..0e7ab684af 100644 --- a/python/current_py_toolchain.bzl +++ b/python/current_py_toolchain.bzl @@ -41,10 +41,15 @@ def _current_py_toolchain_impl(ctx): vars["PYTHON2"] = toolchain.py2_runtime.interpreter_path vars["PYTHON2_ROOTPATH"] = toolchain.py2_runtime.interpreter_path + if getattr(toolchain, "template_vars", None): + template_vars = toolchain.template_vars + else: + template_vars = platform_common.TemplateVariableInfo(vars) + files = depset(direct, transitive = transitive) return [ toolchain, - platform_common.TemplateVariableInfo(vars), + template_vars, DefaultInfo( runfiles = ctx.runfiles(transitive_files = files), files = files, @@ -53,14 +58,21 @@ def _current_py_toolchain_impl(ctx): current_py_toolchain = rule( doc = """ - This rule exists so that the current python toolchain can be used in the `toolchains` attribute of - other rules, such as genrule. It allows exposing a python toolchain after toolchain resolution has - happened, to a rule which expects a concrete implementation of a toolchain, rather than a + This rule exists so that the current python toolchain can be used in the + `toolchains` attribute of other rules, such as genrule. It allows exposing a + python toolchain after toolchain resolution has happened, to a rule which + expects a concrete implementation of a toolchain, rather than a toolchain_type which could be resolved to that toolchain. :::{versionchanged} 1.4.0 - From now on, we also expose `$(PYTHON2_ROOTPATH)` and `$(PYTHON3_ROOTPATH)` which are runfiles - locations equivalents of `$(PYTHON2)` and `$(PYTHON3) respectively. + From now on, we also expose `$(PYTHON2_ROOTPATH)` and `$(PYTHON3_ROOTPATH)` + which are runfiles locations equivalents of `$(PYTHON2)` and `$(PYTHON3)` + respectively. + ::: + + :::{versionchanged} VERSION_NEXT_FEATURE + Exposes make variables from the underlying toolchain's `template_vars` + (see {rule}`py_runtime_pair`). ::: """, implementation = _current_py_toolchain_impl, diff --git a/python/packaging.bzl b/python/packaging.bzl index fb333ca72f..1ec1d380df 100644 --- a/python/packaging.bzl +++ b/python/packaging.bzl @@ -97,7 +97,8 @@ def py_wheel( Wheels are Python distribution format defined in {pep}`PEP 427`. This macro packages a set of targets into a single wheel. - It wraps the [py_wheel rule](#py_wheel_rule). + It wraps the [py_wheel rule](#py_wheel_rule). See + {doc}`/howto/build-a-wheel` for a usage guide. Currently only pure-python wheels are supported. diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index d28add4edf..ca506d85cb 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -671,6 +671,7 @@ bzl_library( ":common_labels", ":reexports", "//python:py_runtime_info", + "//python/private/pypi:python_tag", "@bazel_skylib//rules:common_settings", ], ) @@ -730,6 +731,7 @@ bzl_library( ":py_package", ":rule_builders", ":stamp_impl", + ":toolchain_types", ":transition_labels", ":version", ], diff --git a/python/private/py_runtime_pair_rule.bzl b/python/private/py_runtime_pair_rule.bzl index c6c4c34d19..452d6d6af0 100644 --- a/python/private/py_runtime_pair_rule.bzl +++ b/python/private/py_runtime_pair_rule.bzl @@ -16,9 +16,58 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") +load("//python/private/pypi:python_tag.bzl", "python_tag") load(":common_labels.bzl", "labels") load(":reexports.bzl", "BuiltinPyRuntimeInfo") +def _get_template_vars(py2_runtime, py3_runtime): + vars = {} + if py3_runtime: + if py3_runtime.interpreter: + vars["PYTHON3"] = py3_runtime.interpreter.path + vars["PYTHON3_ROOTPATH"] = py3_runtime.interpreter.short_path + elif py3_runtime.interpreter_path: + vars["PYTHON3"] = py3_runtime.interpreter_path + vars["PYTHON3_ROOTPATH"] = py3_runtime.interpreter_path + + abi_flags = py3_runtime.abi_flags + vars["PY_ABI_FLAGS"] = abi_flags + + impl_name = py3_runtime.implementation_name + impl_tag = None + if impl_name: + impl_tag = python_tag(impl_name) + vars["PY_IMPLEMENTATION_NAME"] = impl_name + vars["PY_WHEEL_IMPLEMENTATION_TAG"] = impl_tag + + version_info = py3_runtime.interpreter_version_info + if ( + version_info and + version_info.major != None and + version_info.minor != None + ): + major = str(version_info.major) + minor = str(version_info.minor) + major_minor_nodot = "{}{}".format(major, minor) + vars["PY_MAJOR"] = major + vars["PY_MINOR"] = minor + vars["PY_MAJOR_DOT_MINOR"] = "{}.{}".format(major, minor) + vars["PY_MAJOR_MINOR_NODOT"] = major_minor_nodot + if impl_tag: + py_tag = "{}{}".format(impl_tag, major_minor_nodot) + vars["PY_WHEEL_PYTHON_TAG"] = py_tag + vars["PY_WHEEL_ABI_TAG"] = "{}{}".format(py_tag, abi_flags) + + if py2_runtime: + if py2_runtime.interpreter: + vars["PYTHON2"] = py2_runtime.interpreter.path + vars["PYTHON2_ROOTPATH"] = py2_runtime.interpreter.short_path + elif py2_runtime.interpreter_path: + vars["PYTHON2"] = py2_runtime.interpreter_path + vars["PYTHON2_ROOTPATH"] = py2_runtime.interpreter_path + + return platform_common.TemplateVariableInfo(vars) + def _py_runtime_pair_impl(ctx): if ctx.attr.py2_runtime != None: py2_runtime = _get_py_runtime_info(ctx.attr.py2_runtime) @@ -44,11 +93,17 @@ def _py_runtime_pair_impl(ctx): if ctx.attr._visible_for_testing[BuildSettingInfo].value: extra_kwargs["toolchain_label"] = ctx.label - return [platform_common.ToolchainInfo( - py2_runtime = py2_runtime, - py3_runtime = py3_runtime, - **extra_kwargs - )] + template_vars = _get_template_vars(py2_runtime, py3_runtime) + + return [ + platform_common.ToolchainInfo( + py2_runtime = py2_runtime, + py3_runtime = py3_runtime, + template_vars = template_vars, + **extra_kwargs + ), + template_vars, + ] def _get_py_runtime_info(target): # Prior to Bazel 7, the builtin PyRuntimeInfo object must be used because @@ -70,7 +125,7 @@ py_runtime_pair = rule( "py2_runtime": attr.label( providers = [[PyRuntimeInfo]] + _MaybeBuiltinPyRuntimeInfo, cfg = "target", - doc = """\ + doc = """ The runtime to use for Python 2 targets. Must have `python_version` set to `PY2`. """, @@ -78,7 +133,7 @@ The runtime to use for Python 2 targets. Must have `python_version` set to "py3_runtime": attr.label( providers = [[PyRuntimeInfo]] + _MaybeBuiltinPyRuntimeInfo, cfg = "target", - doc = """\ + doc = """ The runtime to use for Python 3 targets. Must have `python_version` set to `PY3`. """, @@ -88,7 +143,7 @@ The runtime to use for Python 3 targets. Must have `python_version` set to ), }, fragments = ["py"], - doc = """\ + doc = """ A toolchain rule for Python. This wraps up to two Python runtimes, one for Python 2 and one for Python 3. @@ -103,8 +158,28 @@ This rule returns a {obj}`ToolchainInfo` provider with fields: * `py2_runtime`: {type}`PyRuntimeInfo | None`, runtime information for a Python 2 runtime. -* `py3_runtime`: {type}`PyRuntimeInfo | None`. runtime information for a +* `py3_runtime`: {type}`PyRuntimeInfo | None`, runtime information for a Python 3 runtime. +* `template_vars`: {type}`TemplateVariableInfo`, make variables exposed by this + toolchain. + +It also returns a `TemplateVariableInfo` provider with these make variables +(when the corresponding runtime values are set): + +* `$(PYTHON2)`: Python 2 interpreter path. +* `$(PYTHON2_ROOTPATH)`: Python 2 interpreter runfiles path. +* `$(PYTHON3)`: Python 3 interpreter path. +* `$(PYTHON3_ROOTPATH)`: Python 3 interpreter runfiles path. +* `$(PY_ABI_FLAGS)`: Runtime ABI flags (`sys.abiflags`), e.g. `""` or `"t"`. +* `$(PY_IMPLEMENTATION_NAME)`: Implementation name (`sys.implementation.name`), + e.g. `cpython`. +* `$(PY_MAJOR)`: Major version, e.g. `3`. +* `$(PY_MINOR)`: Minor version, e.g. `12`. +* `$(PY_MAJOR_DOT_MINOR)`: Dotted major and minor version, e.g. `3.12`. +* `$(PY_MAJOR_MINOR_NODOT)`: Dotless major and minor version, e.g. `312`. +* `$(PY_WHEEL_ABI_TAG)`: Wheel ABI tag, e.g. `cp312` or `cp313t`. +* `$(PY_WHEEL_IMPLEMENTATION_TAG)`: Wheel implementation tag, e.g. `cp`. +* `$(PY_WHEEL_PYTHON_TAG)`: Wheel Python tag, e.g. `cp312`. Example usage: @@ -145,5 +220,9 @@ toolchain( register_toolchains("//my_pkg:my_toolchain") ``` + +:::{versionchanged} VERSION_NEXT_FEATURE +Provides `TemplateVariableInfo` and `ToolchainInfo.template_vars`. +::: """, ) diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index c28aefff48..c44623f2f7 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -19,6 +19,7 @@ load(":py_info.bzl", "PyInfo") load(":py_package.bzl", "py_package_lib") load(":rule_builders.bzl", "ruleb") load(":stamp_impl.bzl", "is_stamping_enabled") +load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") load(":transition_labels.bzl", "TRANSITION_LABELS") load(":version.bzl", "version") @@ -36,7 +37,17 @@ PyWheelInfo = provider( _distribution_attrs = { "abi": attr.string( default = "none", - doc = "Python ABI tag. 'none' for pure-Python wheels.", + doc = """ +Python ABI tag. 'none' for pure-Python wheels. + +Supports make variables, e.g. `abi = "$(PY_WHEEL_ABI_TAG)"`. See +{rule}`py_runtime_pair` for variables provided by the Python toolchain. + +:::{versionchanged} VERSION_NEXT_FEATURE +Python toolchain make variables (such as `$(PY_WHEEL_ABI_TAG)`) are available by +default. +::: +""", ), "compress": attr.bool( default = True, @@ -59,7 +70,7 @@ For the available keys, see =3.5,<3.7'" - ), + doc = """ +Python versions required by this distribution, e.g. '>=3.5,<3.7'. + +Supports make variables, e.g. `python_requires = "==$(PY_MAJOR_DOT_MINOR).*"`. +See {rule}`py_runtime_pair` for variables provided by the Python toolchain. + +:::{versionchanged} VERSION_NEXT_FEATURE +Supports make variables. +::: +""", default = "", ), "strip_path_prefixes": attr.string_list( @@ -360,6 +394,8 @@ def _input_file_to_arg(input_file): def _py_wheel_impl(ctx): abi = _replace_make_variables(ctx.attr.abi, ctx) + platform = _replace_make_variables(ctx.attr.platform, ctx) + python_requires = _replace_make_variables(ctx.attr.python_requires, ctx) python_tag = _replace_make_variables(ctx.attr.python_tag, ctx) version_str = _replace_make_variables(ctx.attr.version, ctx) @@ -368,7 +404,7 @@ def _py_wheel_impl(ctx): version.normalize(version_str), _escape_filename_segment(python_tag), _escape_filename_segment(abi), - _escape_filename_segment(ctx.attr.platform), + _escape_filename_segment(platform), ] outfile = ctx.actions.declare_file("-".join(filename_segments) + ".whl") @@ -402,7 +438,7 @@ def _py_wheel_impl(ctx): args.add("--version", version_str) args.add("--python_tag", python_tag) args.add("--abi", abi) - args.add("--platform", ctx.attr.platform) + args.add("--platform", platform) args.add("--out", outfile) args.add("--name_file", name_file) args.add_all(ctx.attr.strip_path_prefixes, format_each = "--strip_path_prefix=%s") @@ -450,8 +486,8 @@ def _py_wheel_impl(ctx): for c in ctx.attr.classifiers: metadata_contents.append("Classifier: %s" % c) - if ctx.attr.python_requires: - metadata_contents.append("Requires-Python: %s" % ctx.attr.python_requires) + if python_requires: + metadata_contents.append("Requires-Python: %s" % python_requires) if ctx.attr.requires and ctx.attr.requires_file: fail("`requires` and `requires_file` are mutually exclusive. Please update {}".format(ctx.label)) @@ -640,7 +676,7 @@ def _transition_wheel_impl(settings, attr): py_wheel = ruleb.Rule( implementation = py_wheel_lib.implementation, - doc = """\ + doc = """ Internal rule used by the [py_wheel macro](#py_wheel). These intentionally have the same name to avoid sharp edges with Bazel macros. @@ -653,4 +689,7 @@ in the way they expect. inputs = TRANSITION_LABELS, outputs = TRANSITION_LABELS, ), + toolchains = [ + ruleb.ToolchainType(TARGET_TOOLCHAIN_TYPE, mandatory = False), + ], ).build() diff --git a/python/py_runtime_pair.bzl b/python/py_runtime_pair.bzl index f44c722d54..497baf929d 100644 --- a/python/py_runtime_pair.bzl +++ b/python/py_runtime_pair.bzl @@ -23,20 +23,21 @@ def py_runtime_pair(name, py2_runtime = None, py3_runtime = None, **attrs): This is a macro around the underlying {rule}`py_runtime_pair` rule. - This used to wrap up to two Python runtimes, one for Python 2 and one for Python 3. - However, Python 2 is no longer supported, so it now only wraps a single Python 3 - runtime. + This used to wrap up to two Python runtimes, one for Python 2 and one for + Python 3. However, Python 2 is no longer supported, so it now only wraps a + single Python 3 runtime. - Usually the wrapped runtimes are declared using the `py_runtime` rule, but any - rule returning a `PyRuntimeInfo` provider may be used. + Usually the wrapped runtimes are declared using the `py_runtime` rule, but + any rule returning a `PyRuntimeInfo` provider may be used. - This rule returns a `platform_common.ToolchainInfo` provider with the following - schema: + This rule returns a `platform_common.ToolchainInfo` provider with the + following schema: ```python platform_common.ToolchainInfo( py2_runtime = None, py3_runtime = , + template_vars = , ) ``` diff --git a/tests/py_runtime_pair/py_runtime_pair_tests.bzl b/tests/py_runtime_pair/py_runtime_pair_tests.bzl index f8656977e0..eeb847f265 100644 --- a/tests/py_runtime_pair/py_runtime_pair_tests.bzl +++ b/tests/py_runtime_pair/py_runtime_pair_tests.bzl @@ -144,6 +144,60 @@ def _test_py_runtime_pair_and_binary_impl(env, target): _tests.append(_test_py_runtime_pair_and_binary) +def _template_var_info_factory(value, meta): + return subjects.struct( + value, + meta = meta, + attrs = { + "variables": subjects.dict, + }, + ) + +def _test_template_variables(name): + rt_util.helper_target( + py_runtime, + name = name + "_runtime", + abi_flags = "t", + implementation_name = "cpython", + interpreter_path = "/fake_interpreter", + interpreter_version_info = { + "major": "3", + "minor": "13", + }, + python_version = "PY3", + ) + rt_util.helper_target( + py_runtime_pair, + name = name + "_subject", + py3_runtime = name + "_runtime", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_template_variables_impl, + ) + +def _test_template_variables_impl(env, target): + template_vars = env.expect.that_target(target).provider( + platform_common.TemplateVariableInfo, + factory = _template_var_info_factory, + ) + template_vars.variables().contains_exactly({ + "PYTHON3": "/fake_interpreter", + "PYTHON3_ROOTPATH": "/fake_interpreter", + "PY_ABI_FLAGS": "t", + "PY_IMPLEMENTATION_NAME": "cpython", + "PY_MAJOR": "3", + "PY_MAJOR_DOT_MINOR": "3.13", + "PY_MAJOR_MINOR_NODOT": "313", + "PY_MINOR": "13", + "PY_WHEEL_ABI_TAG": "cp313t", + "PY_WHEEL_IMPLEMENTATION_TAG": "cp", + "PY_WHEEL_PYTHON_TAG": "cp313", + }) + +_tests.append(_test_template_variables) + def py_runtime_pair_test_suite(name): test_suite( name = name, diff --git a/tests/py_wheel/py_wheel_tests.bzl b/tests/py_wheel/py_wheel_tests.bzl index f39397bb40..92fb73f6c2 100644 --- a/tests/py_wheel/py_wheel_tests.bzl +++ b/tests/py_wheel/py_wheel_tests.bzl @@ -206,6 +206,113 @@ def _test_config_settings_impl(env, target): _tests.append(_test_config_settings) +def _test_default_make_variables(name): + rt_util.helper_target( + py_wheel, + name = name + "_subject", + abi = "$(PY_WHEEL_ABI_TAG)", + config_settings = { + labels.PY_FREETHREADED: "yes", + labels.PYTHON_VERSION: "3.13", + }, + distribution = "mydist_" + name, + platform = "linux_$(PY_IMPLEMENTATION_NAME)_$(PY_ABI_FLAGS)", + python_requires = "==$(PY_MAJOR_DOT_MINOR).*", + python_tag = ( + "$(PY_WHEEL_PYTHON_TAG)." + + "$(PY_WHEEL_IMPLEMENTATION_TAG)$(PY_MAJOR)$(PY_MINOR)." + + "py$(PY_MAJOR_MINOR_NODOT)" + ), + version = "1.0.0", + ) + analysis_test( + name = name, + impl = _test_default_make_variables_impl, + target = name + "_subject", + ) + +def _test_default_make_variables_impl(env, target): + env.expect.that_target(target).default_outputs().contains_exactly([ + "{package}/mydist_{test_name}-1.0.0-" + + "cp313.cp313.py313-cp313t-linux_cpython_t.whl", + ]) + action = env.expect.that_target(target).action_named("PyWheel") + action.contains_at_least_args([ + "--version", + "1.0.0", + "--python_tag", + "cp313.cp313.py313", + "--abi", + "cp313t", + "--platform", + "linux_cpython_t", + ]) + metadata_action = env.expect.that_target(target).action_generating( + "{package}/{name}.metadata.txt", + ) + metadata_action.content().split("\n").contains( + "Requires-Python: ==3.13.*", + ) + +_tests.append(_test_default_make_variables) + +def _make_vars_impl(ctx): + return [platform_common.TemplateVariableInfo(ctx.attr.vars)] + +_make_vars = rule( + implementation = _make_vars_impl, + attrs = { + "vars": attr.string_dict(), + }, +) + +def _test_custom_make_variables(name): + rt_util.helper_target( + _make_vars, + name = name + "_vars", + vars = { + "PLATFORM": "manylinux2014_x86_64", + "VERSION": "1.2.3", + }, + ) + rt_util.helper_target( + py_wheel, + name = name + "_subject", + abi = "$(PY_WHEEL_ABI_TAG)", + config_settings = { + labels.PYTHON_VERSION: "3.12", + }, + distribution = "mydist_" + name, + platform = "$(PLATFORM)", + python_tag = "$(PY_WHEEL_PYTHON_TAG)", + toolchains = [name + "_vars"], + version = "$(VERSION)", + ) + analysis_test( + name = name, + impl = _test_custom_make_variables_impl, + target = name + "_subject", + ) + +def _test_custom_make_variables_impl(env, target): + env.expect.that_target(target).default_outputs().contains_exactly([ + "{package}/mydist_{test_name}-1.2.3-cp312-cp312-" + + "manylinux2014_x86_64.whl", + ]) + action = env.expect.that_target(target).action_named("PyWheel") + action.contains_at_least_args([ + "--version", + "1.2.3", + "--python_tag", + "cp312", + "--abi", + "cp312", + "--platform", + "manylinux2014_x86_64", + ]) + +_tests.append(_test_custom_make_variables) + def py_wheel_test_suite(name): test_suite( name = name, From 738d74586532234727b323e30f59014d6556a113 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 12 Oct 2026 22:03:35 -0700 Subject: [PATCH 2/3] refactor: guard rules_python fields on BuiltinPyRuntimeInfo In Bazel 7 and WORKSPACE builds, `py_runtime_pair` may receive a legacy `BuiltinPyRuntimeInfo` provider, which does not define `abi_flags`, `implementation_name`, or `interpreter_version_info`. Check `hasattr(py3_runtime, "abi_flags")` before reading those fields when populating `TemplateVariableInfo`. --- python/private/py_runtime_pair_rule.bzl | 58 +++++++++++++------------ 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/python/private/py_runtime_pair_rule.bzl b/python/private/py_runtime_pair_rule.bzl index 452d6d6af0..4a32bdc371 100644 --- a/python/private/py_runtime_pair_rule.bzl +++ b/python/private/py_runtime_pair_rule.bzl @@ -30,33 +30,37 @@ def _get_template_vars(py2_runtime, py3_runtime): vars["PYTHON3"] = py3_runtime.interpreter_path vars["PYTHON3_ROOTPATH"] = py3_runtime.interpreter_path - abi_flags = py3_runtime.abi_flags - vars["PY_ABI_FLAGS"] = abi_flags - - impl_name = py3_runtime.implementation_name - impl_tag = None - if impl_name: - impl_tag = python_tag(impl_name) - vars["PY_IMPLEMENTATION_NAME"] = impl_name - vars["PY_WHEEL_IMPLEMENTATION_TAG"] = impl_tag - - version_info = py3_runtime.interpreter_version_info - if ( - version_info and - version_info.major != None and - version_info.minor != None - ): - major = str(version_info.major) - minor = str(version_info.minor) - major_minor_nodot = "{}{}".format(major, minor) - vars["PY_MAJOR"] = major - vars["PY_MINOR"] = minor - vars["PY_MAJOR_DOT_MINOR"] = "{}.{}".format(major, minor) - vars["PY_MAJOR_MINOR_NODOT"] = major_minor_nodot - if impl_tag: - py_tag = "{}{}".format(impl_tag, major_minor_nodot) - vars["PY_WHEEL_PYTHON_TAG"] = py_tag - vars["PY_WHEEL_ABI_TAG"] = "{}{}".format(py_tag, abi_flags) + # BuiltinPyRuntimeInfo (used in Bazel 7) lacks rules_python-specific + # fields like abi_flags, implementation_name, and + # interpreter_version_info. + if hasattr(py3_runtime, "abi_flags"): + abi_flags = py3_runtime.abi_flags + vars["PY_ABI_FLAGS"] = abi_flags + + impl_name = py3_runtime.implementation_name + impl_tag = None + if impl_name: + impl_tag = python_tag(impl_name) + vars["PY_IMPLEMENTATION_NAME"] = impl_name + vars["PY_WHEEL_IMPLEMENTATION_TAG"] = impl_tag + + version_info = py3_runtime.interpreter_version_info + if ( + version_info and + version_info.major != None and + version_info.minor != None + ): + major = str(version_info.major) + minor = str(version_info.minor) + major_minor_nodot = "{}{}".format(major, minor) + vars["PY_MAJOR"] = major + vars["PY_MINOR"] = minor + vars["PY_MAJOR_DOT_MINOR"] = "{}.{}".format(major, minor) + vars["PY_MAJOR_MINOR_NODOT"] = major_minor_nodot + if impl_tag: + py_tag = "{}{}".format(impl_tag, major_minor_nodot) + vars["PY_WHEEL_PYTHON_TAG"] = py_tag + vars["PY_WHEEL_ABI_TAG"] = "{}{}".format(py_tag, abi_flags) if py2_runtime: if py2_runtime.interpreter: From 78ee4ef02d2d70d00d4b7f88a6a21bc066913bcf Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 13 Oct 2026 00:00:33 -0700 Subject: [PATCH 3/3] refactor: remove py2 path from _get_template_vars Python 2 is no longer supported by `py_runtime_pair`, so `py3_runtime` is always used. Remove the `py2_runtime` branch and make `py3_runtime` handling non-conditional in `_get_template_vars`. --- python/private/py_runtime_pair_rule.bzl | 91 +++++++++++-------------- 1 file changed, 40 insertions(+), 51 deletions(-) diff --git a/python/private/py_runtime_pair_rule.bzl b/python/private/py_runtime_pair_rule.bzl index 4a32bdc371..c339e0bd67 100644 --- a/python/private/py_runtime_pair_rule.bzl +++ b/python/private/py_runtime_pair_rule.bzl @@ -20,55 +20,46 @@ load("//python/private/pypi:python_tag.bzl", "python_tag") load(":common_labels.bzl", "labels") load(":reexports.bzl", "BuiltinPyRuntimeInfo") -def _get_template_vars(py2_runtime, py3_runtime): +def _get_template_vars(py3_runtime): vars = {} - if py3_runtime: - if py3_runtime.interpreter: - vars["PYTHON3"] = py3_runtime.interpreter.path - vars["PYTHON3_ROOTPATH"] = py3_runtime.interpreter.short_path - elif py3_runtime.interpreter_path: - vars["PYTHON3"] = py3_runtime.interpreter_path - vars["PYTHON3_ROOTPATH"] = py3_runtime.interpreter_path - - # BuiltinPyRuntimeInfo (used in Bazel 7) lacks rules_python-specific - # fields like abi_flags, implementation_name, and - # interpreter_version_info. - if hasattr(py3_runtime, "abi_flags"): - abi_flags = py3_runtime.abi_flags - vars["PY_ABI_FLAGS"] = abi_flags - - impl_name = py3_runtime.implementation_name - impl_tag = None - if impl_name: - impl_tag = python_tag(impl_name) - vars["PY_IMPLEMENTATION_NAME"] = impl_name - vars["PY_WHEEL_IMPLEMENTATION_TAG"] = impl_tag - - version_info = py3_runtime.interpreter_version_info - if ( - version_info and - version_info.major != None and - version_info.minor != None - ): - major = str(version_info.major) - minor = str(version_info.minor) - major_minor_nodot = "{}{}".format(major, minor) - vars["PY_MAJOR"] = major - vars["PY_MINOR"] = minor - vars["PY_MAJOR_DOT_MINOR"] = "{}.{}".format(major, minor) - vars["PY_MAJOR_MINOR_NODOT"] = major_minor_nodot - if impl_tag: - py_tag = "{}{}".format(impl_tag, major_minor_nodot) - vars["PY_WHEEL_PYTHON_TAG"] = py_tag - vars["PY_WHEEL_ABI_TAG"] = "{}{}".format(py_tag, abi_flags) - - if py2_runtime: - if py2_runtime.interpreter: - vars["PYTHON2"] = py2_runtime.interpreter.path - vars["PYTHON2_ROOTPATH"] = py2_runtime.interpreter.short_path - elif py2_runtime.interpreter_path: - vars["PYTHON2"] = py2_runtime.interpreter_path - vars["PYTHON2_ROOTPATH"] = py2_runtime.interpreter_path + if py3_runtime.interpreter: + vars["PYTHON3"] = py3_runtime.interpreter.path + vars["PYTHON3_ROOTPATH"] = py3_runtime.interpreter.short_path + elif py3_runtime.interpreter_path: + vars["PYTHON3"] = py3_runtime.interpreter_path + vars["PYTHON3_ROOTPATH"] = py3_runtime.interpreter_path + + # BuiltinPyRuntimeInfo (used in Bazel 7) lacks rules_python-specific + # fields like abi_flags, implementation_name, and + # interpreter_version_info. + if hasattr(py3_runtime, "abi_flags"): + abi_flags = py3_runtime.abi_flags + vars["PY_ABI_FLAGS"] = abi_flags + + impl_name = py3_runtime.implementation_name + impl_tag = None + if impl_name: + impl_tag = python_tag(impl_name) + vars["PY_IMPLEMENTATION_NAME"] = impl_name + vars["PY_WHEEL_IMPLEMENTATION_TAG"] = impl_tag + + version_info = py3_runtime.interpreter_version_info + if ( + version_info and + version_info.major != None and + version_info.minor != None + ): + major = str(version_info.major) + minor = str(version_info.minor) + major_minor_nodot = "{}{}".format(major, minor) + vars["PY_MAJOR"] = major + vars["PY_MINOR"] = minor + vars["PY_MAJOR_DOT_MINOR"] = "{}.{}".format(major, minor) + vars["PY_MAJOR_MINOR_NODOT"] = major_minor_nodot + if impl_tag: + py_tag = "{}{}".format(impl_tag, major_minor_nodot) + vars["PY_WHEEL_PYTHON_TAG"] = py_tag + vars["PY_WHEEL_ABI_TAG"] = "{}{}".format(py_tag, abi_flags) return platform_common.TemplateVariableInfo(vars) @@ -97,7 +88,7 @@ def _py_runtime_pair_impl(ctx): if ctx.attr._visible_for_testing[BuildSettingInfo].value: extra_kwargs["toolchain_label"] = ctx.label - template_vars = _get_template_vars(py2_runtime, py3_runtime) + template_vars = _get_template_vars(py3_runtime) return [ platform_common.ToolchainInfo( @@ -170,8 +161,6 @@ This rule returns a {obj}`ToolchainInfo` provider with fields: It also returns a `TemplateVariableInfo` provider with these make variables (when the corresponding runtime values are set): -* `$(PYTHON2)`: Python 2 interpreter path. -* `$(PYTHON2_ROOTPATH)`: Python 2 interpreter runfiles path. * `$(PYTHON3)`: Python 3 interpreter path. * `$(PYTHON3_ROOTPATH)`: Python 3 interpreter runfiles path. * `$(PY_ABI_FLAGS)`: Runtime ABI flags (`sys.abiflags`), e.g. `""` or `"t"`.