From 4f6a433bbe7501f56538f90975c24f4235325b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 2 Mar 2026 14:54:06 +0100 Subject: [PATCH 01/17] Boilerplate for 9998 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 111 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 peps/pep-9998.rst diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst new file mode 100644 index 00000000000..ccd7165d2c1 --- /dev/null +++ b/peps/pep-9998.rst @@ -0,0 +1,111 @@ +PEP: 9998 +Title: Wheel Variants: Building +Author: Jonathan Dekhtiar , + Michał Górny , + Konstantin Schütze , + Ralf Gommers , + Andrey Talman , + Charlie Marsh , + Michael Sarahan , + Eli Uriegas , + Barry Warsaw , + Donald Stufft , + Andy R. Terrel +Discussions-To: Pending +Status: Draft +Type: Standards Track +Topic: Packaging +Requires: 825, 9999 +Created: 02-Mar-2026 +Post-History: Pending + +Abstract +======== + + +Motivation +========== + + +Specification +============= + +Definitions +----------- + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", +"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this +document are to be interpreted as described in :rfc:`2119`. + + +Rationale +========= + + +Backwards Compatibility +======================= + + +Security Implications +===================== + + +How to Teach This +================= + + +Reference Implementation +======================== + +The `variantlib `__ project +provides a reference implementation of low-level library functions for +this PEP. The `pep_817_wheel_variants +`__ monorepo +includes a demo ports of a few build systems. + + +Rejected Ideas +============== + + + +Open Issues +=========== + + +Acknowledgements +================ + +This work would not have been possible without the contributions and +feedback of many people in the Python packaging community. In +particular, we would like to credit the following individuals for their +help in shaping this PEP (in alphabetical order): + +Alban Desmaison, Bradley Dice, Chris Gottbrath, Dmitry Rogozhkin, +Emma Smith, Geoffrey Thomas, Henry Schreiner, Jeff Daily, Jeremy Tanner, +Jithun Nair, Keith Kraus, Leo Fang, Mike McCarty, Nikita Shulga, +Paul Ganssle, Philip Hyunsu Cho, Robert Maynard, Vyas Ramasubramani, +and Zanie Blue. + + +Change History +============== + +- xx-yyy-2026 + + - Initial version, split from :pep:`817` draft. + + +Appendices +========== + +:: + + - :ref:`pep9998-variant-json-schema` + + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. From 924865da1697132a0ac86ee1cc3f8865c4078523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 2 Mar 2026 17:05:33 +0100 Subject: [PATCH 02/17] Some spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 117 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index ccd7165d2c1..2a4f0c864f9 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -26,6 +26,11 @@ Abstract Motivation ========== +:pep:`825` and :pep:`9999` introduced variant wheels while focusing on +the file format and installation process. This PEP follows up on that by +defining a consistent data format and behavior for building variant +wheels. + Specification ============= @@ -38,6 +43,118 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", document are to be interpreted as described in :rfc:`2119`. +Building variant wheels +----------------------- + +Tools MAY support building variant wheels. When building variant wheels, +they MUST ensure that the produced wheels conform to :pep:`825` and +:pep:`9999`. In particular, tools MUST ensure that: + +- variant metadata conforms to the schema +- variant label matches the only key in the ``variants`` dictionary +- there are entries in ``default-priorities.namespace`` and + ``providers`` dictionaries for all namespaces found in variant + properties +- when ``providers.{namespace}.static-properties`` specifies more than + one feature name, there are entries for all feature names in + ``default-priorities.feature.{namespace}`` + +Additionally, tools MAY verify that: + +- all specified variant properties are valid according to the respective + providers (using `extensions to provider plugin API`_) + + +Build backend support +--------------------- + +Build backends MAY support building variant wheels. However, :pep:`517` +and :pep:`660` hooks MUST NOT build variant wheels, unless they are +explicitly requested by the user. + +For consistency, it is RECOMMENDED that build backends implement the +following interface for building variant wheels: + +- variant metadata seed is stored in ``pyproject.toml`` file, as + described in `pyproject.toml integration`_ +- the available variants are described in ``variant.variants`` table + in said file +- the ``config_settings`` dictionary of wheel building hooks accepts a + ``variant-label`` key that enables building the specified variant from + ``variant.variants`` dictionary + + +pyproject.toml integration +-------------------------- + +This specification extends the ``pyproject.toml`` file originally +defined in :pep:`518`. Said file MAY contain a top-level ``variant`` +table that contains variant metadata for the project, as defined by +:pep:`9999` or a subsequent specification, converted into the respective +TOML types. When building variant wheels, tools SHOULD use the +information from this file to seed the variant metadata in the built +variant wheels. More specifically: + +- the ``default-priorities`` dictionary is used to seed + ``default-priorities`` in the wheels +- the ``providers`` dictionary is used to seed ``providers`` in the + wheels and to query providers at build time, if necessary +- the ``variants`` dictionary MAY be used to provide a mapping from + variant labels to variant properties, to facilitate requesting a build + of a specific variant + +Tools MAY provide additional methods of providing the necessary variant +metadata. + + +Example +''''''' + +.. code:: toml + + [variant] + $schema = + + [variant.default-priorities] + # prefer CPU features over BLAS/LAPACK variants + namespace = ["x86_64", "aarch64", "blas_lapack"] + + # prefer aarch64 version and x86_64 level features over other features + # (specific CPU extensions like "sse4.1") + feature.aarch64 = ["version"] + feature.x86_64 = ["level"] + + # prefer x86-64-v3 and then older (even if CPU is newer) + property.x86_64.level = ["v3", "v2", "v1"] + + [variant.providers.aarch64] + # example using different package based on Python version + requires = [ + "provider-variant-aarch64 >=0.0.1; python_version >= '3.12'", + "legacy-provider-variant-aarch64 >=0.0.1; python_version < '3.12'", + ] + # use only on aarch64/arm machines + enable-if = "platform_machine == 'aarch64' or 'arm' in platform_machine" + plugin-api = "provider_variant_aarch64.plugin:AArch64Plugin" + + [variant.providers.x86_64] + requires = ["provider-variant-x86-64 >=0.0.1"] + # use only on x86_64 machines + enable-if = "platform_machine == 'x86_64' or platform_machine == 'AMD64'" + plugin-api = "provider_variant_x86_64.plugin:X8664Plugin" + + [variant.providers.blas_lapack] + # plugin-api inferred from requires + requires = ["blas-lapack-variant-provider"] + # plugin used only when building package, properties will be inlined + # into variant.json + install-time = false + + +Extensions to provider plugin API +--------------------------------- + + Rationale ========= From ab3a901d9efd58169536cecd005f010f8cb10953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 2 Mar 2026 17:35:01 +0100 Subject: [PATCH 03/17] Finish the TOML example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 75 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 2a4f0c864f9..fe3d882b11f 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -101,7 +101,7 @@ variant wheels. More specifically: wheels and to query providers at build time, if necessary - the ``variants`` dictionary MAY be used to provide a mapping from variant labels to variant properties, to facilitate requesting a build - of a specific variant + of a specific variant (``null`` variant does not need to be specified) Tools MAY provide additional methods of providing the necessary variant metadata. @@ -113,42 +113,71 @@ Example .. code:: toml [variant] - $schema = + "$schema" = "https://variants-schema.wheelnext.dev/peps/9999/v0.2.0.json" [variant.default-priorities] - # prefer CPU features over BLAS/LAPACK variants + # REQUIRED: specifies that x86_64 CPU properties are more important than + # aarch64 CPU properties (both are mutually exclusive, so the exact order + # does not matter), and both are more important than specific BLAS/LAPACK + # library: namespace = ["x86_64", "aarch64", "blas_lapack"] - # prefer aarch64 version and x86_64 level features over other features - # (specific CPU extensions like "sse4.1") - feature.aarch64 = ["version"] - feature.x86_64 = ["level"] + # OPTIONAL: makes "library" the most important feature in "blas_lapack" + # namespace + feature.blas_lapack = ["library"] - # prefer x86-64-v3 and then older (even if CPU is newer) - property.x86_64.level = ["v3", "v2", "v1"] + # OPTIONAL: makes ["mkl", "openblas"] the most important values of + # "blas_lapack :: library" feature + property.blas_lapack.library = ["mkl", "openblas"] + + # REQUIRED: variant providers + # --------------------------- + # (keys must matchdefault-priorities.namespace) [variant.providers.aarch64] - # example using different package based on Python version + # Specifies provider plugin package. REQUIRED since there is no + # "static-properties" -- plugin will be queried at install time. requires = [ - "provider-variant-aarch64 >=0.0.1; python_version >= '3.12'", - "legacy-provider-variant-aarch64 >=0.0.1; python_version < '3.12'", + "provider-variant-aarch64 >=0.0.1", ] - # use only on aarch64/arm machines - enable-if = "platform_machine == 'aarch64' or 'arm' in platform_machine" + # Overrides plugin API endpoint. OPTIONAL; without it, the API + # endpoint "provider_variant_aarch64" would be used. plugin-api = "provider_variant_aarch64.plugin:AArch64Plugin" + [variant.providers.blas_lapack] + # Specifies compatible properties. No install-time querying is + # necessary. + static-properties.library = ["accelerate", "openblas", "mkl"] + # OPTIONAL: unused since "static-properties" are provided. + requires = ["blas-lapack-variant-provider"] + [variant.providers.x86_64] - requires = ["provider-variant-x86-64 >=0.0.1"] - # use only on x86_64 machines - enable-if = "platform_machine == 'x86_64' or platform_machine == 'AMD64'" + # Specifies provider plugin package. REQUIRED since there is no + # "static-properties" -- plugin will be queried at install time. + requires = [ + "provider-variant-x86-64 >=0.0.1; python_version >= '3.12'", + "legacy-provider-variant-x86-64 >=0.0.1; python_version < '3.12'", + ] + # Overrides plugin API endpoint. REQUIRED since "requires" may + # evaluate to two different packages depending on Python version. plugin-api = "provider_variant_x86_64.plugin:X8664Plugin" - [variant.providers.blas_lapack] - # plugin-api inferred from requires - requires = ["blas-lapack-variant-provider"] - # plugin used only when building package, properties will be inlined - # into variant.json - install-time = false + # RECOMMENDED: variants that can be built + # --------------------------------------- + + # "x8664v3_openblas" label corresponds to: + # - blas_lapack :: library :: openblas + # - x86_64 :: level :: v3 + [variant.variants.x8664_v3_openblas] + blas_lapack.library = ["openblas"] + x86_64.level = ["v3"] + + # "x8664v4_mkl" label corresponds to: + # - blas_lapack :: library :: mkl + # - x86_64 :: level :: v4 + [variant.variants.x8664_v4_mkl] + blas_lapack.library = ["mkl"] + x86_64.level = ["v4"] Extensions to provider plugin API From 1144526daf66cb2d64da29d9a8401179938dd0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 2 Mar 2026 18:12:51 +0100 Subject: [PATCH 04/17] Plugin interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index fe3d882b11f..22d3e1704d3 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -183,6 +183,64 @@ Example Extensions to provider plugin API --------------------------------- +The plugin interface specified in :pep:`9999` is extended by adding the +following REQUIRED function: + +- ``get_all_config() -> list[VariantFeatureConfigType]`` that returns a + list of variant feature names and their values that are valid + according to the provider plugin. The ordering of the lists is + insignificant. A particular plugin version MUST always return the same + lists (modulo ordering), irrespective of any runtime conditions. + +The value returned by ``get_supported_configs()`` MUST be a subset of +the feature names and values returned by ``get_all_configs()`` (modulo +ordering). + + +Example implementation +'''''''''''''''''''''' + +.. code:: python + + from dataclasses import dataclass + + + @dataclass + class VariantFeatureConfig: + name: str + values: list[str] + multi_value: bool + + + # internal -- provided for illustrative purpose + _MAX_VERSION = 4 + _ALL_GPUS = ["narf", "poit", "zort"] + + + class MyPlugin: + @staticmethod + def get_supported_configs() -> list[VariantFeatureConfig]: + ... # same as in PEP 9999 + + @staticmethod + def get_all_configs() -> list[VariantFeatureConfig]: + return [ + VariantFeatureConfig( + name="min_version", + # [1, 2, 3, 4] (the order does not matter) + values=[str(x) for x in range(1, _MAX_VERSION + 1)], + # single-valued, since there is always one minimum + multi_value=False, + ), + VariantFeatureConfig( + name="gpu", + # [narf, poit, zort] + values=_ALL_GPUS, + # multi-valued, since a package can target multiple GPUs + multi_value=True, + ), + ] + Rationale ========= From 81706457e1e9e964054cfa3b0185b6ae28d76758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 2 Mar 2026 18:16:28 +0100 Subject: [PATCH 05/17] Add prefaces to examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 22d3e1704d3..289bf267eca 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -110,6 +110,10 @@ metadata. Example ''''''' +The ``pyproject.toml`` file corresponding to the package with variant +metadata equivalent to the one provided in the example in :pep:`9999` +could look like the following: + .. code:: toml [variant] @@ -200,6 +204,9 @@ ordering). Example implementation '''''''''''''''''''''' +The example plugin implementation from :pep:`9999` can be extended in +the following way: + .. code:: python from dataclasses import dataclass From 36057c3039f1455077ec7e023e13b4d11ce2366c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 2 Mar 2026 18:44:12 +0100 Subject: [PATCH 06/17] Onwards! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 289bf267eca..b14968c79b3 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -82,6 +82,11 @@ following interface for building variant wheels: - the ``config_settings`` dictionary of wheel building hooks accepts a ``variant-label`` key that enables building the specified variant from ``variant.variants`` dictionary +- the ``get_requires_for_build_wheel()`` hook of :pep:`517`, + ``get_requires_for_build_editable()`` hook of :pep:`660`, and hooks of + similar purpose include the necessary provider plugin packages for the + variant being built, and the build backend uses the version installed + in the build environment as a result pyproject.toml integration @@ -256,10 +261,28 @@ Rationale Backwards Compatibility ======================= +As noted in :pep:`825`, variant wheels are incompatible with the +existing tooling. To avoid disrupting existing workflows, this PEP +requires (in `build backend support`_) that build backends do not build +variant wheels by default. + +The introduction of a new ``variant`` table (in `pyproject.toml +integration`_) follows from :pep:`518` specifying that such tables are +"reserved for future use by other PEPs". Nevertheless, it is possible +that overly strict tools will reject such ``pyproject.toml`` files as +unsupported, even if the use of wheel variants is not relevant to their +use. + Security Implications ===================== +There are no new security implications beyond what is already noted in +:pep:`9999`. In particular, the use of provider plugins during build +time is not seen as an additional risk, given that the dependencies are +provider by the user directly (for example, in ``pyproject.toml``) and +they are treated equivalently to other build dependencies. + How to Teach This ================= From b87281bc731cea25be3abd7217cebdb8bb2d4320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 2 Mar 2026 20:58:16 +0100 Subject: [PATCH 07/17] Draft the `build-requires` idea MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 118 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 9 deletions(-) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index b14968c79b3..58282f6848b 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -95,10 +95,10 @@ pyproject.toml integration This specification extends the ``pyproject.toml`` file originally defined in :pep:`518`. Said file MAY contain a top-level ``variant`` table that contains variant metadata for the project, as defined by -:pep:`9999` or a subsequent specification, converted into the respective -TOML types. When building variant wheels, tools SHOULD use the -information from this file to seed the variant metadata in the built -variant wheels. More specifically: +:pep:`9999` or a subsequent specification, with the extensions listed +in this PEP, converted into the respective TOML types. When building +variant wheels, tools SHOULD use the information from this file to seed +the variant metadata in the built variant wheels. More specifically: - the ``default-priorities`` dictionary is used to seed ``default-priorities`` in the wheels @@ -112,6 +112,24 @@ Tools MAY provide additional methods of providing the necessary variant metadata. +Extension to provider metadata +'''''''''''''''''''''''''''''' + +The provider information dictionaries used as values in the +``providers`` dictionary MAY contain the following key: + +- ``build-requires: list[str]``: A list of one or more + :ref:`dependency specifiers `, same as + ``requires`` but used only to populate ``static-properties`` at build + time, as explained in `populating static properties from provider + plugins`_. + +If ``build-requires`` is specified, both ``requires`` and +``static-properties`` MUST NOT be present. In the resulting variant +wheel, the ``build-requires`` and ``plugin-api`` keys MUST be removed, +and ``static-properites`` MUST be added instead. + + Example ''''''' @@ -154,11 +172,8 @@ could look like the following: plugin-api = "provider_variant_aarch64.plugin:AArch64Plugin" [variant.providers.blas_lapack] - # Specifies compatible properties. No install-time querying is - # necessary. - static-properties.library = ["accelerate", "openblas", "mkl"] - # OPTIONAL: unused since "static-properties" are provided. - requires = ["blas-lapack-variant-provider"] + # REQUIRED: used to populate "static-properties". + build-requires = ["blas-lapack-variant-provider"] [variant.providers.x86_64] # Specifies provider plugin package. REQUIRED since there is no @@ -192,6 +207,10 @@ could look like the following: Extensions to provider plugin API --------------------------------- +Provider plugins MAY be used at build time, if the provider metadata +specifies either ``requires`` or ``build-requires`` key, per the +`extension to provider metadata`_ section. + The plugin interface specified in :pep:`9999` is extended by adding the following REQUIRED function: @@ -205,6 +224,13 @@ The value returned by ``get_supported_configs()`` MUST be a subset of the feature names and values returned by ``get_all_configs()`` (modulo ordering). +Additionally, the following OPTIONAL attribute is added: + +- ``is_aot_plugin: bool = False``; if it set to ``True``, then all valid + feature names and values are always supported. In that case, the + ``get_supported_configs()`` function MUST always return the same value + as ``get_all_configs()`` (modulo ordering). + Example implementation '''''''''''''''''''''' @@ -254,6 +280,80 @@ the following way: ] +Example AoT plugin implementation +''''''''''''''''''''''''''''''''' + +.. code:: python + + from dataclasses import dataclass + + + @dataclass + class _VariantFeatureConfig: + name: str + values: list[str] + multi_value: bool + + + is_aot_plugin = True + + + def get_supported_configs() -> list[_VariantFeatureConfig]: + return [ + VariantFeatureConfig( + name="library", + values=["accelerate", "openblas", "mkl"], + multi_value=False, + ), + ] + + def get_all_configs() -> list[_VariantFeatureConfig]: + return get_supported_configs() + + +Populating static properties from provider plugins +-------------------------------------------------- + +If a variant provider metadata used at build time contains the +``build-requires`` key (as specified in the `extension to provider +metadata`_ section), the specified provider plugin is used to populate +the ``static-properties`` dictionary while building the variant wheel. + +The packages to install and the plugin interface to use are determined +as specified in :pep:`9999`, except that: + +- the ``build-requires`` key is used in place of ``requires`` key +- all providers are assumed to be enabled and trusted + +In particular, the ``plugin-api`` key MAY be specified to override the +plugin API endpoint. + +When populating static properties, the tool MUST: + +1. Ensure that the loaded plugin has ``is_aot_plugin == True`` + attribute. It is invalid to use a plugin without that attribute in + ``build-requires``. + +2. Call ``get_supported_configs()`` to obtain the ordered lists of + supported feature name and their values. + +3. Verify that the properties the wheel is built with are compatible per + ``get_supported_configs()``. It is invalid to build a variant wheel + with properties that are not permitted by ``static-properties``. + +4. Remove ``build-requires`` and ``plugin-api`` keys from the provider + metadata. + +5. Add ``static-properties`` key to the provider metadata, populating it + using the lists obtained from ``get_supported_configs()``. + +6. If ``get_supported_configs()`` returned more than one feature name, + ensure that all feature names are present in + ``default-priorities.feature.{namespace}`` list. Any feature names + missing should be appended in the same order as returned by + ``get_supported_configs()``. + + Rationale ========= From 3221449f1a325ff90efebb14de26f6af20599c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 3 Mar 2026 11:12:57 +0100 Subject: [PATCH 08/17] List changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 58282f6848b..29fdc4f20cd 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -428,6 +428,15 @@ Change History - xx-yyy-2026 - Initial version, split from :pep:`817` draft. + - A specific build backend interface for building variant wheels was + proposed. + - The ``variants`` dictionary is now permitted in ``pyproject.toml`` + metadata, and it can be used to define available variants to build. + - A new ``build-requires`` key was introduced in provider metadata. + It can only be used ``pyproject.toml`` and it is used to populate + ``static-properties`` from a provider plugin at build time. It + replaced the previous combination of ``install-time = false`` with + non-empty ``requires``. Appendices From 4931341bf06d900a808e2bacc1343afaeccd9ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 3 Mar 2026 16:11:36 +0100 Subject: [PATCH 09/17] Rationale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 29fdc4f20cd..4ea4d2485c1 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -357,6 +357,36 @@ When populating static properties, the tool MUST: Rationale ========= +This PEP considers a variety of possible tools that may emit variant +wheels, including build backends, build frontends and tools dedicated to +converting regular wheels into variant wheels. A standardized interfaces +for project tree metadata and build backends are proposed to offer +consistent user experience, but are by no means mandatory. The standard +``pyproject.toml`` file is used to avoid spreading the project +configuration across multiple files, and the same schema as for variant +metadata is used. + +When using build backends, using ``get_requires*()`` hooks for exposing +provider plugin dependencies is recommended, to reuse the existing logic +for creating isolated environments and installing packages. This implies +that provider plugin packages are treated as regular build dependencies. + +The provider plugin API is extended to enable verifying whether the +properties specified for a variant are correct. To this purpose, a +``get_all_configs()`` function is introduced that is symmetric to +``get_supported_configs()`` introduced in :pep:`9999`. However, the +build tools are not required to perform this validation, as it requires +installing and querying provider plugins. + +Furthermore, this PEP permits populating ``static-properties`` in +provider information using provider plugins. This needs both to be +supported by the plugin in question, and explicitly enabled in the +package, through using ``build-requires`` in place of ``requires``. This +can be used to ensure that multiple packages use the same properties for +the same features, such as BLAS/LAPACK library names. The process is +handled entirely at build time, and the resulting wheel is fully +compliant with :pep:`9999`. + Backwards Compatibility ======================= From 8c3165a4cc83508344d1253dd19a413eb443f731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 3 Mar 2026 18:55:55 +0100 Subject: [PATCH 10/17] Rationalize redundancy of all/supported getters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 4ea4d2485c1..f12e1527da9 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -387,6 +387,12 @@ the same features, such as BLAS/LAPACK library names. The process is handled entirely at build time, and the resulting wheel is fully compliant with :pep:`9999`. +Technically, the presence of both ``get_supported_configs()`` and +``get_all_configs()`` for AoT plugins is redundant: both must return the +same values, except that the latter may return them in any order. +However, the specification requires both to keep a single interface for +all provider plugins. + Backwards Compatibility ======================= From 1072aced72eec52a62d47815dfd92f9227beb9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 3 Mar 2026 19:01:06 +0100 Subject: [PATCH 11/17] HtTT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index f12e1527da9..278f2f02291 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -423,6 +423,12 @@ they are treated equivalently to other build dependencies. How to Teach This ================= +This PEP is primarily oriented towards package maintainers. The primary +source of information will be the documentation of build backends and +other tools that support building variant wheels. The documentation +present on ``packaging.python.org`` will also be updated for the +recommended build backend interface. + Reference Implementation ======================== From 38e7a2182e1a09de56fc62f4539c73421d85ef0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 9 Mar 2026 14:12:51 +0100 Subject: [PATCH 12/17] Update the install-time plugin example not to use class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 278f2f02291..808a23d6a53 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -255,29 +255,27 @@ the following way: _ALL_GPUS = ["narf", "poit", "zort"] - class MyPlugin: - @staticmethod - def get_supported_configs() -> list[VariantFeatureConfig]: - ... # same as in PEP 9999 - - @staticmethod - def get_all_configs() -> list[VariantFeatureConfig]: - return [ - VariantFeatureConfig( - name="min_version", - # [1, 2, 3, 4] (the order does not matter) - values=[str(x) for x in range(1, _MAX_VERSION + 1)], - # single-valued, since there is always one minimum - multi_value=False, - ), - VariantFeatureConfig( - name="gpu", - # [narf, poit, zort] - values=_ALL_GPUS, - # multi-valued, since a package can target multiple GPUs - multi_value=True, - ), - ] + def get_supported_configs() -> list[VariantFeatureConfig]: + ... # same as in PEP 9999 + + + def get_all_configs() -> list[VariantFeatureConfig]: + return [ + VariantFeatureConfig( + name="min_version", + # [1, 2, 3, 4] (the order does not matter) + values=[str(x) for x in range(1, _MAX_VERSION + 1)], + # single-valued, since there is always one minimum + multi_value=False, + ), + VariantFeatureConfig( + name="gpu", + # [narf, poit, zort] + values=_ALL_GPUS, + # multi-valued, since a package can target multiple GPUs + multi_value=True, + ), + ] Example AoT plugin implementation @@ -307,6 +305,7 @@ Example AoT plugin implementation ), ] + def get_all_configs() -> list[_VariantFeatureConfig]: return get_supported_configs() From 3871a8460afa555e7e3dca582f3058b36cdd03f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 9 Mar 2026 14:14:41 +0100 Subject: [PATCH 13/17] =?UTF-8?q?`is=5Faot=5Fplugin`=20=E2=86=92=20`all=5F?= =?UTF-8?q?properties=5Fcompatible`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 808a23d6a53..cb262106bdb 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -226,10 +226,10 @@ ordering). Additionally, the following OPTIONAL attribute is added: -- ``is_aot_plugin: bool = False``; if it set to ``True``, then all valid - feature names and values are always supported. In that case, the - ``get_supported_configs()`` function MUST always return the same value - as ``get_all_configs()`` (modulo ordering). +- ``all_properties_compatible: bool = False``; if it set to ``True``, + then all valid feature names and values are always supported. In that + case, the ``get_supported_configs()`` function MUST always return the + same value as ``get_all_configs()`` (modulo ordering). Example implementation @@ -293,7 +293,7 @@ Example AoT plugin implementation multi_value: bool - is_aot_plugin = True + all_properties_compatible = True def get_supported_configs() -> list[_VariantFeatureConfig]: @@ -329,9 +329,9 @@ plugin API endpoint. When populating static properties, the tool MUST: -1. Ensure that the loaded plugin has ``is_aot_plugin == True`` - attribute. It is invalid to use a plugin without that attribute in - ``build-requires``. +1. Ensure that the loaded plugin has ``all_properties_compatible == + True`` attribute. It is invalid to use a plugin without that + attribute in ``build-requires``. 2. Call ``get_supported_configs()`` to obtain the ordered lists of supported feature name and their values. From 68e4f4a59b1041728de063c0bbac31c529c8e737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 9 Mar 2026 14:15:59 +0100 Subject: [PATCH 14/17] Remove remaining "AoT"s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index cb262106bdb..7d0744356df 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -278,8 +278,8 @@ the following way: ] -Example AoT plugin implementation -''''''''''''''''''''''''''''''''' +Example implementation of a plugin suitable for build-time use +'''''''''''''''''''''''''''''''''----------------------------- .. code:: python @@ -387,10 +387,10 @@ handled entirely at build time, and the resulting wheel is fully compliant with :pep:`9999`. Technically, the presence of both ``get_supported_configs()`` and -``get_all_configs()`` for AoT plugins is redundant: both must return the -same values, except that the latter may return them in any order. -However, the specification requires both to keep a single interface for -all provider plugins. +``get_all_configs()`` when all properties are compatible is redundant: +both must return the same values, except that the latter may return them +in any order. However, the specification requires both to keep a single +interface for all provider plugins. Backwards Compatibility From 831e4e8a17db4d6021ceb31544f4e0c55ff6d03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 9 Mar 2026 14:16:36 +0100 Subject: [PATCH 15/17] =?UTF-8?q?supported=20=E2=86=92=20compatible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 7d0744356df..74cac71c4da 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -227,7 +227,7 @@ ordering). Additionally, the following OPTIONAL attribute is added: - ``all_properties_compatible: bool = False``; if it set to ``True``, - then all valid feature names and values are always supported. In that + then all valid feature names and values are always compatible. In that case, the ``get_supported_configs()`` function MUST always return the same value as ``get_all_configs()`` (modulo ordering). From 44c29844639b2fed41b5d910cd6f4bd60f80330e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 12 Mar 2026 15:43:31 +0100 Subject: [PATCH 16/17] Address some of the review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index 74cac71c4da..e8bc84d97e2 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -192,14 +192,14 @@ could look like the following: # "x8664v3_openblas" label corresponds to: # - blas_lapack :: library :: openblas # - x86_64 :: level :: v3 - [variant.variants.x8664_v3_openblas] + [variant.variants.x8664v3_openblas] blas_lapack.library = ["openblas"] x86_64.level = ["v3"] # "x8664v4_mkl" label corresponds to: # - blas_lapack :: library :: mkl # - x86_64 :: level :: v4 - [variant.variants.x8664_v4_mkl] + [variant.variants.x8664v4_mkl] blas_lapack.library = ["mkl"] x86_64.level = ["v4"] @@ -214,7 +214,7 @@ specifies either ``requires`` or ``build-requires`` key, per the The plugin interface specified in :pep:`9999` is extended by adding the following REQUIRED function: -- ``get_all_config() -> list[VariantFeatureConfigType]`` that returns a +- ``get_all_configs() -> list[VariantFeatureConfigType]`` that returns a list of variant feature names and their values that are valid according to the provider plugin. The ordering of the lists is insignificant. A particular plugin version MUST always return the same @@ -403,10 +403,8 @@ variant wheels by default. The introduction of a new ``variant`` table (in `pyproject.toml integration`_) follows from :pep:`518` specifying that such tables are -"reserved for future use by other PEPs". Nevertheless, it is possible -that overly strict tools will reject such ``pyproject.toml`` files as -unsupported, even if the use of wheel variants is not relevant to their -use. +"reserved for future use by other PEPs". Therefore, introducing the +``[variant]`` table is backwards compatible. Security Implications From d7e1603e5a7dff40a4aa2efe698fb661f7986b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 12 Mar 2026 16:05:37 +0100 Subject: [PATCH 17/17] Update labels in example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- peps/pep-9998.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/peps/pep-9998.rst b/peps/pep-9998.rst index e8bc84d97e2..0c6f22532b7 100644 --- a/peps/pep-9998.rst +++ b/peps/pep-9998.rst @@ -189,17 +189,17 @@ could look like the following: # RECOMMENDED: variants that can be built # --------------------------------------- - # "x8664v3_openblas" label corresponds to: + # "x86_64_v3_openblas" label corresponds to: # - blas_lapack :: library :: openblas # - x86_64 :: level :: v3 - [variant.variants.x8664v3_openblas] + [variant.variants.x86_64_v3_openblas] blas_lapack.library = ["openblas"] x86_64.level = ["v3"] - # "x8664v4_mkl" label corresponds to: + # "x86_64_v4_mkl" label corresponds to: # - blas_lapack :: library :: mkl # - x86_64 :: level :: v4 - [variant.variants.x8664v4_mkl] + [variant.variants.x86_64_v4_mkl] blas_lapack.library = ["mkl"] x86_64.level = ["v4"]