diff --git a/peps/pep-0817.rst b/peps/pep-0817.rst index a6b2a9afe35..e73c5f283f2 100644 --- a/peps/pep-0817.rst +++ b/peps/pep-0817.rst @@ -13,7 +13,7 @@ Author: Jonathan Dekhtiar , Andy R. Terrel Discussions-To: https://discuss.python.org/t/pep-817-wheel-variants-beyond-platform-tags/105860 Status: Draft -Type: Standards Track +Type: Informational Topic: Packaging Created: 10-Dec-2025 Post-History: `24-Jan-2026 `__ @@ -24,32 +24,98 @@ Abstract Python's existing wheel packaging format uses :doc:`packaging:specifications/platform-compatibility-tags` to specify a -given wheel's supported environments. These tags are unable to express -modern hardware configurations and their features, such as the -availability of GPU acceleration. The tags fail to provide custom -package variants, such as builds against different dependency ABIs. -These inabilities are particularly challenging for scientific computing, -artificial intelligence (AI), machine learning (ML), and -high-performance computing (HPC) communities. - -This PEP proposes "Wheel Variants", an extension to the -:doc:`packaging:specifications/binary-distribution-format`. This -extension introduces a mechanism for package maintainers to declare -multiple build variants for the same package version, while allowing -installers to automatically select the most appropriate variant based on -system hardware and software characteristics. More specifically, it -proposes: - -- An evolution of the wheel format called **Wheel Variant** that allows - wheels to be distinguished by hardware or software attributes. - -- A **variant provider plugin** interface that allows installers to - dynamically detect platform attributes and select the most suitable - wheel. - -The goal is for the obvious installation commands (``{tool} install -``) to select the most appropriate wheel, and provide the best -user experience. +given wheel's supported environments. These tags cannot express modern +hardware configurations and their features, such as the availability of +GPU acceleration or the instruction sets a CPU supports. Neither can +they express build-time choices that matter at run time, such as which +BLAS implementation a wheel was built against, or which version of a +dependency it matches at the ABI level. These inabilities are +particularly challenging for scientific computing, artificial +intelligence (AI), machine learning (ML), and high-performance computing +(HPC) communities. + +"Wheel Variants" is an extension to the +:doc:`packaging:specifications/binary-distribution-format` that lets a +project publish several wheels for the same version, distinguished by +properties the platform compatibility tags cannot carry, and lets +installers choose between them automatically. It comprises: + +- **Variant wheels**, which declare the variant properties they were + built for alongside the existing platform compatibility tags. + +- **Variant providers**, which determine the properties a system + supports. A provider may establish that by inspecting the system at + install time, or supply it statically where there is nothing to + detect, as when choosing between BLAS implementations. + +- A **static description of an environment**, which a user may supply in + place of any inspection. Building a container image, a native + installer, or a wheel that vendors a particular build of a dependency + calls for a stated target rather than a discovered one. + +The goal is for the obvious installation command (``{tool} install +{package}``) to select the most appropriate wheel, whether that choice +is detected or declared, with no index URLs, package-name suffixes or +per-project install instructions. + + +Scope and Standards Track PEPs +============================== + +This PEP is Informational: it specifies nothing normatively. It serves +as the umbrella for a sequence of four Standards Track PEPs, and it +supplies the motivation, the prior art, and the reasoning behind the +design choices those PEPs make, so that each of them can be read +against a shared context rather than restating it. It aims to give a +concise high-level overview of wheel variants; it does not cover the +complete functionality proposed as part of the standard, nor does it +provide normative guidance on implementing it. + +The four PEPs are: + +- :pep:`PEP 825: Package Format <825>`: changes to the wheel format, the + initial definition of variant metadata, the index-level metadata file, + variant wheel ordering rules, environment markers and ``pylock.toml`` + integration. + +- Providers: how the variant properties a system supports are + determined; binding every variant namespace to the one provider that + governs it (e.g., ``nvidia`` for CUDA capabilities, ``x86_64`` for + CPU instruction-set levels, ``blas_lapack`` for the choice of BLAS + implementation); extending variant metadata with provider + information; introducing providers that supply compatible properties + statically via variant metadata or dynamically via opt-in Python + packages, along with the security considerations for their use; + defining the relevant plugin API; introducing a special provider + concerned with ABI compatibility. + +- Building: integration with build backends and the ``pyproject.toml`` + file; extending variant metadata and the plugin API to support + verifying the validity of variant properties and embedding static + properties via plugins at build time. + +- UX, maintainability, security and governance: the introduction + strategy, and a community maintained repository of trusted variant + providers that can be integrated with installers for opt-out behavior. + +Of these, :pep:`825` has been provisionally accepted; the remaining +three are in preparation, and their scope and details may change. + +Some aspects are deliberately left outside the sequence, to evolve via +tools or further PEPs. A non-exhaustive list: + +- The exact format of a static file describing an environment's variant + properties, +- The list of variant providers that are vendored or re-implemented by + installers, +- The specific opt-in mechanisms and UX for allowing an installer to run + non-vendored variant providers, +- How to instruct build backends to emit variants through the :pep:`517` + mechanism. + +The first of these concerns the encoding only. Installers are expected +to accept a static description of an environment's variant properties; +what is left open is the form that description takes. Motivation @@ -80,11 +146,12 @@ a single wheel of considerable size, or using separate package names (``mypackage-gpu``, ``mypackage-cpu``, etc.). Each of these approaches has significant drawbacks and potential security implications. + The limitations of platform compatibility tags ---------------------------------------------- The current wheel format encodes compatibility through three platform -tags: +compatibility tags: 1. **Python tag**: encoding the minimum Python version and optionally restricting Python distributions (e.g., ``py3`` for any Python 3, @@ -97,7 +164,7 @@ tags: architecture and core system libraries (e.g., ``any`` for any platform, ``manylinux_2_34_x86_64`` for x86-64 Linux system with glibc 2.34 or newer, ``macosx_14_0_arm64`` for arm64 macOS 14.0 - or newer system. + or newer system). These tags are limited to expressing the most fundamental properties of the Python interpreter, operating system and the broad CPU @@ -178,7 +245,7 @@ on users also apply to other packages. PyTorch uses a combination of index URLs per accelerator type and local version segments as accelerator tag (such as ``+cu130``, ``+rocm6.4`` or -``+cpu``) . Users need to first determine the correct index URL for +``+cpu``). Users need to first determine the correct index URL for their system, and add an index specifically for PyTorch. .. code:: bash @@ -189,7 +256,7 @@ Tools need to implement special handling for the way PyTorch uses local version segments. These requirements break the pattern that packages are usually installed with. Problems with installing PyTorch are a very common point of user confusion. To quantify this, on -2025-12-05, 552 out of 8136 (6.8%), of issues on `uv's issue tracker +2025-12-05, 552 out of 8136 (6.8%) of issues on `uv's issue tracker `__ contained the term "torch". **Security Risk:** This approach has unfortunately led to supply @@ -239,7 +306,7 @@ not a goal. **Security Risk:** proliferation of suffixed variant packages leads users to expect these suffixes in other packages, making name squatting much easier. For example, one could create a malicious -``numpy-cuda`` package that users will be lead to believe it's a CUDA +``numpy-cuda`` package that users will be led to believe it's a CUDA variant of NumPy. As of the time of writing, CuPy_ has already registered a total of 55 @@ -277,8 +344,8 @@ plugin-based approach. The central ``jax`` package provides a number of extras that can be used to install additional plugins, e.g. ``jax[cuda12]`` or ``jax[tpu]``. This is far from ideal as ``pip install jax`` (with no extra) leads to a nonfunctional -installation, and consequently dependency chains, a fundamental expected -behavior in the Python ecosystem, are dysfunctional. +installation, and consequently breaks dependency chains, a fundamental +expected behavior in the Python ecosystem. JAX includes 12 extras to cover all use cases - many of which overlap and could be misleading to users if they don't read the @@ -348,7 +415,7 @@ and ABI requirements has led to ecosystem fragmentation: systems struggle to handle non-standard installation requirements. * **Fragility**: The established workarounds are often error-prone, - and in the past they have lead to issues such as downloading incorrect + and in the past they have led to issues such as downloading incorrect artifacts. @@ -516,25 +583,8 @@ The potential for improvement can be summarized as: Architect at OpenTeams -Out-of-scope features ---------------------- - -This PEP presents the minimal scope required to meet modern heterogenous -system needs. It leaves aspects beyond the minimal scope to evolve via -tools or future PEPs. A non-exhaustive list of these aspects include: - -- The format of a static file to select variants deterministically or - include variants in a ``pylock.toml`` file, -- The list of variant providers that are vendored or re-implemented by - installers, -- The specific opt-in mechanisms and UX for allowing an installer to run - non-vendored variant providers, -- How to instruct build backends to emit variants through the :pep:`517` - mechanism. - - -Prior art ---------- +Prior Art +========= This problem is not unique to the Python ecosystem, different groups and ecosystems have come up with various answers to that very problem. This @@ -543,7 +593,7 @@ different approaches taken by various communities. Conda - conda-forge -''''''''''''''''''' +------------------- `Conda `__ is a binary-only package ecosystem that uses aggregated metadata indexes for resolution rather than @@ -609,7 +659,7 @@ libraries, and CUDA driver version. Detection logic is tool-specific Spack / Archspec -'''''''''''''''' +---------------- `archspec `__ is a library for detecting, labeling, and reasoning about CPU microarchitecture variants, @@ -619,7 +669,7 @@ developed for the `Spack `__ package manager. ``skylake``, ``zen2``, ``armv8.1a``) form a `Directed Acyclic Graph (DAG) encoding binary compatibility `__, -which helps at resolve to express that ``packageB`` depends on +which helps the resolver express that ``packageB`` depends on ``packageA``. The ordering is partial because (1) separate ISA families are incomparable, and (2) contemporary designs may have incompatible feature sets—cascadelake and cannonlake are incomparable despite both @@ -645,7 +695,7 @@ when no exact match exists. Gentoo Linux -'''''''''''' +------------ `Gentoo Linux `__ is a source-first distribution with support for extensive package customization. This is primarily @@ -680,2003 +730,1241 @@ are used in conjunction with USE flags. For example, build against. -Overview and rationale -====================== +The design space +================ + +Platform compatibility tags cannot express the characteristics described +above, so something must determine whether a given wheel is suitable for +a given system. The question that shapes the design is where that +knowledge lives, and who is able to add to it. + +The approaches below differ in how much standardization they require, +and in whether that requirement returns each time a new compatibility +axis appears. For two of them - logic shipped per project, and separate +package names - adding an axis requires no standardization at all, which +is why both already exist as workarounds today; their costs fall +elsewhere. + +Where standardization is required, its shape differs sharply. +Standardizing the compatibility axes themselves means a PEP per axis, +and a further one each time hardware or a library introduces an axis not +already covered, followed in every case by an implementation in each +installer. Standardizing an interface means one PEP, once. + +.. list-table:: + :header-rows: 1 + :widths: 18 12 26 22 22 + + * - Approach + - Selects without user input + - Adding a new compatibility axis requires + - Detection logic maintained by + - Third-party code runs at install time + * - Fixed set of axes + - Yes + - A PEP for that axis, then an implementation in every installer + - Installer maintainers + - No + * - Logic shipped per project + - Yes + - Nothing; each project decides + - Every variant-shipping project + - Yes + * - Separate package names + - Unresolved + - Nothing; a new project name + - Unspecified; something must still choose the right name + - No + * - User-declared environments + - No + - A PEP for that axis, but no detection to implement + - Not applicable; values are supplied by the user + - No + * - Variant providers + - Yes + - A provider package release + - Provider maintainers + - No, unless the user opts in + +The table states what each approach costs and where the work lands. The +sections below take each in turn; `The approach taken`_ explains the +choice that follows from them. + + +Approaches considered +--------------------- + +A fixed set of compatibility axes +''''''''''''''''''''''''''''''''' + +The standards process could define compatibility markers for these +characteristics - CUDA runtime version, GPU architecture, CPU +instruction set level, and so on - together with the semantics for +matching them. Wheels would declare their requirements against these +markers, and each installer would implement the detection needed to +evaluate them. The specification would not need to say how a tool +measures any particular marker; that would remain an implementation +matter for each tool. + +This approach has real advantages, and it is the alternative most +frequently raised in discussion. It introduces no new kind of artifact +and no new execution model. The specification stays purely declarative, +and the code performing detection is first-party installer code, so no +new trust surface is created. + +It would not, however, be a single document. The rules governing NVIDIA +CUDA compatibility - driver and runtime versions, real and virtual GPU +architectures, and the forward-compatibility guarantees between them - +have little in common with those governing AMD ROCm or Intel XPU, and +less still with x86-64 instruction set levels or Arm feature flags. Each +needs different expertise to write and different reviewers to assess it +competently, and a single document covering them all would be difficult +to review as a whole. A realistic first round is therefore several +PEPs: plausibly one per accelerator vendor, and at least one more for +CPU instruction sets, which may not remain single as architectures +diverge. + +The recurring cost has the same shape, and makes the specification the +gatekeeper for every compatibility axis. Each new axis requires +agreement across the packaging community followed by an implementation +in each installer, and a newly specified axis is only as useful as its +slowest widely deployed implementation. The axes in demand today already +extend well past GPUs: CPU instruction set levels, BLAS and LAPACK +implementations, OpenMP runtimes, C++ ABIs, and matching against the ABI +of a dependency such as PyTorch_. + +The cost also recurs within an axis, not only across axes. An axis that +has been specified still has to be revised as its subject moves: new +CUDA and ROCm releases, new GPU architectures and new instruction set +extensions each change which values exist and how they compare. That is +a far faster tempo than the existing platform compatibility tags deal +with, where operating systems, C libraries and broad CPU architectures +turn over slowly - and even at that pace, writing a PEP per tag did not +hold up: :pep:`600` replaced that practice for ``manylinux`` in order to +let maintainers adopt new tags sooner and to make better use of limited +volunteer time. Hardware vendors would depend on the standards process +in order to ship optimized wheels, on a timescale measured against their +own release cadence. + +The vendors are also not bound by the standards process. A +specification that writes down how a platform's versions relate to one +another records rules that the packaging community does not own and +cannot hold still: the vendor can redefine them in any hardware or +software release it makes, leaving the specification wrong until it is +amended. The renumbering of macOS versions is a reminder that this is +not hypothetical. + +A narrower form of this proposal is to standardize deliberately few +axes - a small set of community-supported hardware target levels, +refreshed periodically - rather than attempting to cover the space. This +is coherent, and it answers a genuine concern: a design permitting many +axes also permits build matrices that are difficult to test and to +audit. Two considerations weigh against it. The set requiring +maintenance is not limited to hardware, as the list above shows, so the +curation burden is larger than it first appears. And it places the +judgment about which hardware merits support with the packaging +community rather than with the vendors and projects that have the +expertise and the motivation to maintain it. The concern about +unmanageable build matrices is better met by observing that nothing +obliges a project to publish many variants; how many to ship remains +that project's decision. + + +Compatibility logic shipped by each project +''''''''''''''''''''''''''''''''''''''''''' + +Each project shipping variants could provide its own detection code, +which the installer would run to choose among that project's wheels, +with no mechanism shared between projects. This is the current situation +extrapolated rather than a new design: the approach described in `Wheel +variant selection via source distribution`_ already works this way, and +`wheel-stub `__ packages it as +a reusable build backend. Both are confined to source distributions, so +they require arbitrary code execution at install time and leave the +wheel that is actually installed outside anything a lock file records. + +The same detection would then be written many times over. Every project +would present its own behavior, its own failure modes and its own user +interface for the same underlying questions, and there would be no +shared story for auditing any of it. Coordination across projects - +ensuring that NumPy_ and SciPy_ agree on a BLAS implementation, or that +everything in an environment is built against the same CUDA major +version - would have no mechanism at all. + + +Separate package names +'''''''''''''''''''''' + +`Package names as variants`_ describes what projects do today. A more +deliberate version of the same idea is to make the mapping part of +installation: the user requests ``torch``, and the installer resolves +that to a project name such as ``torch_cu128`` appropriate for the +current platform. + +This has the attraction of working within the wheel format as it stands. +It requires no filename changes and no metadata changes, and publishers +retain control over what they publish. The drawbacks described in +`Package names as variants`_ still apply: name squatting becomes easier, +packages installing overlapping files cannot be declared mutually +exclusive, and releases cannot be published synchronously across several +project names. + +Two further problems are specific to the mapping. The code performing it +has to live in the installer, and it is bound to a naming convention +that does not exist. Projects already spell the same distinction +differently: JAX_ publishes ``jax-cuda12-plugin`` and +``jax-cuda13-plugin``, identifying CUDA by major version alone, while +PyTorch_ labels its builds ``cu129`` and ``cu130`` in index URLs and +local version segments, by major and minor. Something must establish +which form an installer resolves to, and that whatever convention is +chosen holds for every project using the mechanism. How that would be +agreed between projects and kept consistent across installers is +unclear, and the name carries meaning that nothing in the metadata +records. More fundamentally, the mapping needs a source of truth: +something must still determine which name is appropriate for the +current system. That is the question this section is about, so this +approach relocates it rather than answering it. + + +A user-declared environment +''''''''''''''''''''''''''' + +Compatibility could be matched against values the user declares rather +than values a tool detects. This shares its vocabulary with the first +alternative above - markers for CUDA runtime version, GPU architecture, +instruction set level and so on, defined by the standards process - but +takes their values from a description of the environment that the user +supplies, by hand or produced by a separate tool, rather than from +detection. No detection code would run anywhere during installation. + +Many installs have nothing to detect. An install that produces an +artifact for use elsewhere - a container image, a native installer, or a +wheel that vendors a particular build of a dependency - is a statement +about the target, not about the machine doing the work, and the machine +doing the work may have no GPU in it at all. The approach also describes +how a substantial group of users already works: HPC sites and +locked-down corporate environments commonly declare the available +toolchain, accelerator and library stack as a matter of policy, using +mechanisms such as those described in `Spack / Archspec`_. And it has +the smallest trust surface of any approach considered, since no code +runs in order to make a compatibility decision. + +It is nonetheless unsuitable as the only mechanism, because it requires +every user to know and state their CUDA runtime version, driver version +and instruction set level. The weight of that objection should not be +overstated. Users already need this knowledge today, where it is +translated into a choice of index URL and a bespoke install command, and +declaring it once to the installer would be an improvement on that - not +least because a declared environment composes with dependency +resolution, whereas an index URL is a property of a single command and +does not, so today a project that merely depends on PyTorch_ can undo +the user's choice. The objection is narrower: the burden remains with +the user, whereas ``{tool} install {package}`` selecting a working wheel +without it is the goal stated in the abstract. + + +Variant providers +''''''''''''''''' + +Compatibility could be determined by named, versioned plugins - variant +providers - each owning one or more axes of the variant space. The +standards process would define the interface such a plugin exposes and +how its results are used, rather than defining the axes themselves. A +new compatibility axis would then be introduced by publishing or +updating a provider rather than by amending a specification. + +This places the detection logic with those who have both the expertise +and the motivation to maintain it. A hardware vendor can ship support +for a new accelerator, and correct the compatibility rules when they change, +on its own release cadence. Installers remain generic, and do not +acquire a standing obligation to understand every accelerator and ABI on +every platform. Because providers are shared between projects, +coordinating a consistent choice across an environment remains possible. +Where nothing needs to be detected, as with a choice between BLAS +implementations, a provider supplies its properties statically and no +code runs at install time at all; this is described in `Static +properties in wheels`_. + +The costs are real and worth stating plainly. The ecosystem acquires a +new kind of artifact, with its own questions of versioning, trust and +governance. The system inspection performed during resolution expands +considerably: installers already probe the platform to evaluate +compatibility tags, but that inspection is bounded and first-party, +whereas a provider's is open-ended and may be third-party. Whether a +user benefits at all depends on the relevant provider being available to +their installer; where it is not vendored, allowlisted or opted into, +its properties are treated as incompatible and the installer falls back +to a generic wheel - a working result, but not the one the hardware +could have had. And because the properties driving selection are held in +metadata rather than in the filename, determining which wheel a given +system will select requires reading that metadata rather than a +directory listing; :pep:`825` discusses the alternatives considered for +this. + + +The approach taken +------------------ + +The design set out further down in `The wheel variants design`_ takes +two of the approaches above rather than one. Variant providers are the +default mechanism, and a user-declared description of the environment is +supported alongside them as a first-class path rather than as a +fallback; `Using static compatibility information`_ describes the form +that description takes. + + +Why providers are the default +''''''''''''''''''''''''''''' + +Providers are the default because they are the only approach that +satisfies three requirements at once: the right wheel is selected +without the user having to supply the answer, a new compatibility axis +does not require a new round of standardization, and the detection logic +is written once and shared rather than once per project. + +The first of those requirements matters to more people than those who do +not know what hardware they have. The answer is perishable, since a +driver upgrade or a move to another machine can invalidate it; +establishing it is work, and work repeated for every environment; and it +is easy to get wrong in a way that surfaces much later, as a crash or as +an unexplained slowdown. + +The other approaches satisfy some of the three but not all. A fixed set +of axes selects automatically and keeps the logic out of individual +projects, but routes every future axis through the standards process. +Per-project logic avoids that, but writes the same detection once per +project and forgoes any coordination between them. A user-declared +environment hands the question back to the user, and separate names do +not answer it at all, since something must still determine which name +applies. + + +Why a declared environment sits alongside +''''''''''''''''''''''''''''''''''''''''' + +The user-declared path is not present to compensate for a weakness in +providers. It answers requirements that detection cannot, whatever one's +view of running code at install time. Installing for a machine other +than the one running the installer - building a container image for +other hardware, populating a wheelhouse, reproducing an environment in +CI - offers nothing to detect, because the target system is absent. +Environments whose policy forbids executing code during installation +need a route that involves none. Deployments that must install +identically across a fleet of machines differing in CPU generation +within one family need the same route for a different reason: detection +would correctly select a different wheel on each, when the point is for +them to match. A user whose hardware is misdetected needs a way to say +so. + +The two approaches fit together rather than merely coexisting, because +they supply the same thing. A provider's output is a set of supported +properties in preference order; a static description declares that set +directly. Selection consumes one form or the other without needing to +know which, so the declared path substitutes at a single point instead +of running as a second mechanism in parallel. That is what makes it +reasonable to offer both without doubling what has to be specified, +implemented and reasoned about. + + +Why variants stay out of the resolver's search +''''''''''''''''''''''''''''''''''''''''''''''' + +One boundary of the design is worth stating explicitly. Variant +selection chooses among the wheels of a package version that the +resolver has already selected; with one optional exception noted below, +it does not make variants part of the resolver's search. This keeps a +substantial change out of dependency +resolution, where the algorithms in use - backtracking, SAT, PubGrub - +would all have to accommodate it, and where the nearest precedent, +extras, is a cautionary one. The cost is that a conflict between the +variants of two different packages cannot be resolved by searching: if +one package is installed against one BLAS implementation and another +publishes only wheels built against a different one, an installer can +backtrack to an older version of the first package, but it cannot +revisit the wheel it chose for the version it had already settled on. It +has to report the conflict rather than work around it. + +Two things limit how often that arises. Packages sharing a provider +receive the same properties in the same preference order, so independent +per-package choices tend to converge without coordination, and a user +who needs a particular combination can declare it. The one exception is +the ``abi_dependency`` provider described in `Package ABI matching`_, +which does require a resolver to take variants into account, and which +is optional for precisely that reason. + + +How much code should run, and when +---------------------------------- + +Choosing providers settles where compatibility logic lives. It does not +settle how much of that logic should run without the user having said +so. Both extremes are ruled out, for the reasons below: a provider is +not run merely because a wheel names it, and the user is not asked about +each one in turn. What remains open is how a provider comes to be +trusted, so that it runs without the user opting in to it. That is the +most contested aspect of the design, and the mechanisms themselves +belong to the PEPs listed in `Scope and Standards Track PEPs`_. + +Both extremes fail. An installer automatically running whatever provider +a wheel's metadata names turns that metadata into a means of executing +code on any machine that merely resolves against the package, before the +user has agreed to install anything. An installer prompting for every +provider produces prompts often enough that they stop being read, and +users grant them wholesale, which arrives at the same place with +additional ceremony. The risk of security fatigue is not a rhetorical +objection to opt-in designs; it is the reason a purely opt-in design +does not by itself resolve the problem. + +It is worth being precise about what changes. System inspection during +resolution is not new, and is already more involved than reading a +version string. Establishing ``manylinux`` compatibility requires the +glibc version, which ``packaging`` can obtain by using ``ctypes`` to +load the C library of the running process and call the function that +reports its version. Establishing ``musllinux`` compatibility requires +parsing the interpreter's ELF headers to locate the dynamic loader, then +running that loader as a subprocess and reading the version out of its +output. Installers do this today either by vendoring that code or by +reimplementing it - pip takes the first route, uv the second - and those +are exactly the two strategies proposed for providers. A provider that +an installer vendors or reimplements is much the same kind of artifact. +What is new is that a provider may instead come from a third party, +named by the package being installed. + +Several mechanisms narrow the gap, each with its own cost. Installers +may vendor or reimplement the most widely used providers, which keeps +the common path free of third-party code but concentrates the work on +installer maintainers. A curated list of vetted providers avoids that +concentration, but constitutes a curation authority that must be +governed. A static description of the environment removes the question +entirely for users who want that, at the cost of requiring them to +produce one. + +This PEP does not attempt to settle that trade-off. The fourth PEP in +the sequence, covering UX, maintainability, security and governance, +takes it up: the opt-in mechanisms, a community-maintained repository of +trusted variant providers that installers can treat as opt-out, and the +model that governs what goes into it. + + +The wheel variants design +========================= Wheel variant glossary ---------------------- -Variant Wheels +Variant wheels Wheels that share the same distribution name, version, build number, and platform compatibility tags, but are distinctly identified by an arbitrary set of variant properties. -Variant Namespace +Variant namespace An identifier used to group related features provided by a single provider (e.g., ``nvidia``, ``x86_64``, ``arm``, etc.). -Variant Feature +Variant feature A specific characteristic (key) within a namespace (e.g., - ``version``, ``avx512_bf16``, etc.) that can have one or more + ``sm_arch``, ``avx512_bf16``, etc.) that can have one or more values. -Variant Property +Variant property A 3-tuple (``namespace :: feature-name :: feature-value``) describing a single specific feature and its value. If a feature has multiple values, each is represented by a separate property. -Variant Label - A string (up to 16 characters) added to the wheel filename to - uniquely identify variants. +Variant label + A string added to the wheel filename to uniquely identify variants. -Null Variant +Null variant A special variant with zero variant properties and the reserved label ``null``. Always considered supported but has the lowest priority among wheel variants, while being preferably chosen over non-variant wheels. -Variant Provider - A provider of supported and valid variant properties for a specific - namespace, usually in the form of a Python package that implements - system detection. - -Install-time Provider - A provider implemented as a plugin that can be queried during wheel - installation. +Variant provider + A provider of valid variant properties for a specific namespace. + Can either be a static list of ordered properties, or a Python + package that dynamically determines the properties that are + compatible with the system. -Ahead-of-Time Provider - A provider that features a static list of supported properties which - is then embedded in the wheel metadata. Such a list can either be - embedded in ``pyproject.toml`` or provided by a plugin queried at - build time. - -Overview --------- +High-level overview +------------------- Wheel variants introduce a more fine-grained specification of built -wheel characteristics beyond what existing wheel tags provide. -Individual wheels carry a human-readable label defined at build time, as -described in `modified wheel filename`_, and are characterizing using -`variant property system`_. The properties are organized into a -hierarchical structure of namespaces, features and feature values. When -evaluating wheels to install, the installer determines whether variant -properties of a given wheel are compatible with the system, and perform -`variant ordering`_ based on the priority of the compatible variant -properties. This is done in addition to determining the compatibility. -The ordering by variant properties takes precedence over ordering by -tags. - -Every variant namespace is governed by a variant provider. There are two -kinds of variant providers: install-time providers and ahead-of-time -(AoT) providers. Install-time providers require plugins that are queried -while installing wheels to determine the set of supported properties and -their preference order. For AoT providers, this data is static and -embedded in the wheel; it can be either provided directly by the -wheel maintainer or queried at wheel build time from an AoT plugin. - -Both kinds of plugins are usually implemented as Python packages which -implement the `provider plugin API`_, but they may also be vendored or -reimplemented by installers to improve user experience, as outlined in -`Providers`_. Plugin packages may be installed in isolated or -non-isolated environments. In particular, all plugins may be returned by -the ``get_requires_for_build_wheel()`` hook of a :pep:`517` backend, and -therefore installed along with other build dependencies. For this -reason, it is important that plugin packages do not narrowly pin -dependencies, as that could prevent different packages from being -installed simultaneously in the same environment. - -Metadata governing variant support is defined in ``pyproject.toml`` -file, and it is copied into ``variant.json`` file in wheels, as explored -in `metadata in source tree and wheels`_. Additionally, `variant -environment markers`_ can be used to define dependencies specific to a -subset of variants. - - -Modified wheel filename ------------------------ - -One of the core requirements of the design is to ensure that installers -predating this PEP will ignore wheel variant files. This makes it -possible to publish both variant wheels and non-variant wheels on a -single index, with installers that do not support variants securely -ignoring the former, and falling back to the latter. - -A variant label component is added to the filename for the twofold -purpose of providing a unique mapping from the filename to a set of -variant properties, and providing a human-readable identification for -the variant. The label is kept short and lowercase to avoid issues with -different filesystems. It is added as a ``-``-separated component at the -end to ensure that the existing filename validation algorithms reject -it: - -- If both the build tag and the variant label are present, the filename - contains too many components. Example: - - .. code-block:: text - - numpy-2.3.2-1-cp313-cp313t-musllinux_1_2_x86_64-x86_64_v3.whl - ^^^^^^^^^^ - -- If only the variant label is present, the Python tag at third position - will be misinterpreted as a build number. Since the build number must - start with a digit and no Python tags at the time start with digits, - the filename is considered invalid. Example: - - .. code-block:: text - - numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64-x86_64_v3.whl - ^^^^^ - -This behavior was confirmed for a number of existing tools: -`auditwheel -`__, -`packaging -`__, -`pdm -`__, -`pip -`__, -`poetry -`__, -and `uv -`__. - - -Variant property system ------------------------ - -Variant properties serve the purpose of expressing the characteristics -of the variant. Unlike platform compatibility tags, they are stored in -the variant metadata and therefore do not affect the wheel filename -length. They follow a hierarchical key-value design, with the key -further broken into a namespace and a feature name. Namespaces are used -to group features defined by a single provider, and to avoid conflicts -should multiple providers define a feature with the same name. This -permits independent governance and evolution of every namespace. - -The keys are restricted to lowercase letters, digits, and underscores. -Uppercase characters are disallowed to avoid different spellings of the -same name. The character set for values is more relaxed, to permit -values resembling versions. - -Variant properties are serialized into a structured 3-tuple format -inspired by Trove Classifiers in :pep:`301`: - -.. code-block:: text - - {namespace} :: {feature_name} :: {feature_value} - -Properties are used both to determine variant wheel compatibility, and -to select the best variant to install. Provider plugins indicate which -variant properties are compatible with the system, and order them by -importance. This ordering can further be altered in variant wheel -metadata. - -Variant features can be declared as allowing multiple values to be -present within a single variant wheel. If that is the case, these values -are matched as a logical OR, i.e. only a single value needs to be -compatible with the system for the wheel to be considered supported. On -the other hand, features are treated as a logical AND, i.e. all of them -need to be compatible. This provides some flexibility in designating -variant compatibility while avoiding having to implement a complete -boolean logic. - -Typically, variant features will be single-value and indicate minimal or -mutually exclusive requirements. The system may indicate multiple -compatible values. For example, if the feature declares a minimum CUDA -runtime version, the provider will indicate compatibility with wheels -requiring a minimum version corresponding to the currently installed -version or older, e.g. for CUDA 12.8, the compatible minimum versions -used in wheels would be, in order of decreasing preference: - -.. code-block:: text - - nvidia :: cuda_version_lower_bound :: 12.8 - nvidia :: cuda_version_lower_bound :: 12.7 - nvidia :: cuda_version_lower_bound :: 12.6 - ... - -Similarly, a wheel could indicate its minimum required CPU version, and -the provider will indicate all the compatible CPU versions. - -Multi-value features are useful for "fat" packages where multiple -incompatible targets are supported by a single package. A typical -example are GPUs. In this case, the wheel declares a number of supported -GPUs, and the provider indicates which GPUs are actually installed -(usually one). The wheel is compatible if there is overlap between the -two lists. - - -Null variant ------------- - -A null variant is a variant wheel with no properties, but distinct -from non-variant wheels in having the ``null`` variant label and variant -metadata. During the transition period, it provides the possibility of -providing a distinct fallback for systems that do not support any of -the variants provided, and for systems that do support variant wheels at -all. - -For example, a package with optional GPU support could publish three -kinds of wheels: - -- Multiple GPU-enabled wheels, each built for a single CUDA version with - a matching set of supported GPUs, and used only when the provider - plugin indicates that the system is compatible. - -- A CPU-only null variant, much smaller than the GPU variants, installed - when the provider plugin indicates that no compatible GPU is - installed. - -- A GPU+CPU non-variant wheel, that will be installed on systems without - an installer supporting variants. - -Publishing a null variant is optional, and makes sense only if distinct -fallbacks provide advantages to the user. If one is published, a wheel -variant-enabled installer will prefer it over the non-variant wheel. If -it is not, it will fall back to the non-variant wheel instead. The -non-variant wheel is also used if variant support is explicitly disabled -by an installer flag. - -The null variant uses a reserved ``null`` label to make it clearly -distinguishable from regular variants. - - -Install-time and Ahead-of-Time providers ----------------------------------------- - -The variant wheel metadata specifies what providers are used for its -properties. Providers serve a twofold purpose: - -a. at install time: determining which variant wheels are compatible with - the user's system, and which of them constitutes the best choice, and - -b. at build time: determining which variant properties are valid for - building a wheel. - -The specification proposes two kinds of providers: install-time -providers and Ahead-of-Time providers. - -Install-time providers are implemented either as Python packages that -need to be installed and run to query them, or vendored or reimplemented -in the tools. They are used when user systems need to be queried to -determine wheel compatibility, for example for variants utilizing GPUs -or requiring CPU instruction sets beyond what platform tags provide. -Installing third-party packages involves security risks highlighted in -the `security implications`_ section, and the proposed mitigations incur -a cost on installer implementations. - -Ahead-of-Time providers are implemented as static metadata embedded in -the wheel. They are used when particular variant properties are always -compatible with the user's system (provided that a wheel using them has -been built successfully). However, the metadata indicates which -properties are preferred. For example, AoT providers can be used to -provide choice between builds against different BLAS / LAPACK providers, -or to provide debug builds of packages. Since they do not require -running code external to the installer, they do not pose the problems -faced by install-time providers, and can be used more liberally. - -AoT providers are permitted to feature plugin packages. If that is the -case, these packages are only used when building wheels, and their -output is used to fill in the static metadata used at install time. -This way, it is easier to use consistent property names and values -across multiple packages. Otherwise, the package maintainer needs to -include the supported properties directly in the ``pyproject.toml`` -file. - -When implemented as Python packages, both kinds of provider plugins -expose roughly the same API. However, an AoT provider must always -consider all valid variant properties supported, and it must always -return the same ordered list of supported properties irrespective of the -user system. All AoT providers can technically be used as install-time -providers, but not the other way around. - - -Plugin stability and versioning -------------------------------- - -As the specification introduces the potential necessity of installing -and running provider packages to install wheels, it is recommended that -these packages remain functioning correctly for the variant wheels -published in the past, including very old package versions. Ideally, no -properties previously supported should ever be removed. - -If a breaking change needs to be performed, it is recommended to either -introduce a new provider package for that, or add a new plugin API -endpoint to the existing package. In both cases, it may be necessary to -preserve the old endpoint in minimal maintenance mode, to ensure that -old wheels can still be installed. The old endpoint can trigger -deprecation warnings in the ``get_all_configs()`` hook that is used when -building packages. - -An alternative approach is to use semantic versioning to cut off -breaking changes. However, this relies on package authors reliably using -caps on dependencies, as otherwise old wheels will start using -incompatible plugin versions. This is already a problem with Python -build backends used today. - -When vendoring or reimplementing plugins, installers need to follow -their current behavior. In particular, they should recognize the -relevant provider versions numbers, and possibly fall back to installing -the external plugin when the package in question is incompatible with -the installer's implementation. - - -Metadata in source tree and wheels ------------------------------------ - -Variants introduce a few new portions of metadata that are stored in the -source tree and in wheels. In the source tree, it is stored in the -``pyproject.toml`` file along with other project properties, benefiting -from the TOML format's readability and strictness. Afterwards, it is -converted into an equivalent JSON structure, and stored as a separate -file in the ``.dist-info`` directory. The existing metadata files are -unchanged to avoid unnecessary incompatibility, and to avoid serializing -into the inconvenient :doc:`Core Metadata -` format. - -The metadata in ``pyproject.toml`` includes: - -- information about variant providers that could be used by the wheels, -- optionally, lists overriding the default property ordering, -- static property lists for Ahead-of-Time providers that do not use - plugins. - -In wheel metadata, the above is amended by static property lists -obtained from the plugins and variant properties for the built wheel. - -When wheels are published on an index, the variant metadata from all -wheels is combined into a single ``{name}-{version}-variants.json`` file -that is used by clients to efficiently obtain the variant metadata -without having to download it from individual wheels separately, or -implement explicit variant metadata support in an API provided by the -package index server. - - -ABI dependency variant provider -------------------------------- - -Some packages provide extension modules exposing an Application Binary -Interface (ABI) that is not compatible across wide ranges of versions. -The packages using this interface need to pin their wheels to the -version used at build time. If ABI changes frequently, the pins are very -narrow and users face problems if they need to install two packages that -may happen to pin to different versions of the same dependency. -Providing variants built against different dependency versions can -increase the chance of a resolver being able to find a dependency version -that is compatible with all the packages being installed. - -Unfortunately, such a variant provider cannot be implemented within the -plugin API defined by the specification. Given that a robust -implementation would need to interface with the dependency resolver, -rather than attempt to extend the API to cover this use case and add -significant complexity as a result, the specification reserves -``abi_dependency`` as a special variant namespace that can be -implemented by installers wishing the provide this feature. - -Given the complexity of the problem, this extension is made entirely -optional. This implies that any packages using it need to provide -non-variant wheels as well. - - -Suggested implementation logic for a packaging tool ---------------------------------------------------------- - -Installing a package from an index -'''''''''''''''''''''''''''''''''' - -.. figure:: pep-0817/conceptual_diagram_installers.png - :target: _images/conceptual_diagram_installers.png - :class: invert-in-dark-mode - :alt: A diagram showing installing a package including variant wheel - building. It is split into three columns: Developer, Installer - and Install-time providers. The diagram starts with Develop - initiating package install. The subsequent steps involve - installer, in order: resolver selects package version; - determine if release has variant wheels. If there are no - variant wheels, jump to installing package and report success. - If the version has variant wheels, check user's variant - preferences. In parallel, download JSON from index, then - extract variant provider configuration. If it uses AoT - providers only, converge to determine optimal variant - immediately. If it requires install-time providers, the - further path depends on whether non-vendored providers are - included. If they are not, query install-time providers - immediately and converge to determine optimal variant. If - non-vendored providers are included, they are installed if not - present in env and then queried. Querying providers involves - an exchange of data with different providers (in the diagram, - "provider 1" and "provider 2" are given as examples), each - filtering and ordering supported configurations for the - current environment. All the variant paths converge on - determining optimal variant, which is following by installing - package and reporting success. - - A conceptual diagram of installing a wheel. - -When asked to install a version of a package from an index, the proposed -tool behavior would be to: - -1. Query the remote index for the desired package. -2. Select an initial match for a package version meeting the version constraints, - as usual (this does not need to take variant metadata into account). -3. Filter available wheels based on Platform Compatibility Tags. -4. Determine if any of the remaining wheels are variant wheels. - If not, proceed as with non-variant wheels. -5. If any wheels feature variant labels, download the index-level - variant metadata file, ``{name}-{version}-variants.json``. If this - file is missing, assume all variant wheels are incompatible and - proceed as with non-variant wheels. -6. Map the variant labels into sets of variant properties using the - index-level variant metadata file. If any of the labels present in - wheel filenames are missing in the file, assume that the respective - wheels are incompatible. -7. Obtain the ordered lists of supported variant properties using - providers specified in the index-level variant metadata file: - - - for the enabled AoT providers, obtain them from static property - data in the index-level variant metadata file. - - for the enabled install-time providers: - - - if the user provided static compatibility information, use that. - - otherwise, if the provider is vendored or reimplemented, query it - in implementation-specific manner. - - otherwise, if the Python provider package is considered secure - (either by the installer or via explicit user opt-in), install it - in an isolated environment, and query it via the plugin API. - - if none of the above applies, do not run the provider and either - consider the variant properties incompatible, or fail the - installation. - - - for the disabled providers (e.g. opt-in providers that were not - enabled by the user, providers excluded via environment markers), - assume that all variant properties in the namespace are - incompatible. - -8. Filter and order variants based on the lists of supported properties, - and select the most preferred variant. If no variant wheel matched, - use the non-variant wheels by their rules. -9. If multiple wheels for a given version share the same variant label, - order them by Platform compatibility tags and build number, and - select the best wheel. - - -Installing a local wheel -'''''''''''''''''''''''' - -When asked to install a local wheel file, the tool's proposed behavior would be -to: - -1. If no variant label is present in the filename, proceed as with - non-variant wheels. -2. Verify the wheel compatibility via Platform compatibility tags. -3. Read variant metadata from ``*.dist-info/variant.json`` inside the - wheel file. -4. Obtain the ordered lists of supported variant properties, as when - `installing a package from an index`_. -5. Verify the wheel compatibility via supported properties. - - -Building a variant wheel -'''''''''''''''''''''''' - -In order to build a variant wheel, the build backend needs to receive a -list of variant properties and a variant label. The recommended way to -do that is to use backend-defined keys in the ``config_settings`` -dictionary passed to the build backend hooks. - -When building a variant wheel, the proposed behavior for the build -backend would be to: - -1. Read variant provider metadata from ``pyproject.toml``. -2. Verify that all namespaces specified in the user-defined variant - properties have a corresponding provider in the metadata. -3. In the ``get_requires_for_build_wheel()`` hook, return variant - provider plugin packages along with other build dependencies. -4. In the ``build_wheel()`` hook, query the provider plugins - ``get_all_configs()`` function to obtain all valid property keys and - values. Use it to verify that the specified properties are correct. -5. Convert the variant metadata from ``pyproject.toml`` to JSON, append - the mapping from variant label to variant properties and write the - result into the wheel's ``*.dist-info/variant.json`` file. -6. Build the wheel as usual, except for including the - ``*.dist-info/variant.json`` and the variant label in the filename. - - -Publishing variant wheels on an index -''''''''''''''''''''''''''''''''''''' - -Variant wheels are uploaded to an index just like regular wheels. -There are two possible approaches to publishing the index-level -``{name}-{version}-variants.json`` file for every package version: -it can either be prepared and uploaded by the user, or it can be -generated automatically by the index. - -The file should not be changed once it is published, as clients may have -already cached it or locked to the existing hash. For this reason, if -the index is responsible for generating the file, it should use some -mechanism to defer publishing it until the release is fully uploaded -(for example, :pep:`694`). - -To generate the ``{name}-{version}-variants.json`` file: - -1. For the first variant wheel for a given package version, copy the - data from its ``*.dist-info/variant.json`` file. -2. For subsequent wheels, merge the data from their - ``*.dist-info/variant.json`` files into the existing data: - - - disjoint keys of ``providers``, ``static-properties`` and - ``variants`` dictionaries are merge together - - common keys of these dictionaries must have exactly the same value - - ``default-priorities.namespace`` list can be replaced if the new - value starts with the old value - - ``default-priorities.feature`` and ``default-priorities.value`` - keys can be added if they were not present in the previous - ``default-priorities.namespace`` value - - other keys must have exactly the same value - - -Example use cases ------------------ - -PyTorch CPU/GPU variants -'''''''''''''''''''''''' - -As of October 2025, `PyTorch -`__ publishes a total of seven -variants for every release: a CPU-only variant, three CUDA variants with -different minimal CUDA runtime versions and supported GPUs, two ROCm -variants and a Linux XPU variant. - -This setup could be improved using GPU/XPU plugins that query the -installed runtime version and installed GPUs/XPUs to filter out the -wheels for which the runtime is unavailable, it is too old or the user's -GPU is not supported, and order the remaining variants by the runtime -version. The CPU-only version is published as a null variant that is -always supported. - -If a GPU runtime is available and supported, the installer automatically -chooses the wheel for the newest runtime supported. Otherwise, it falls -back to the CPU-only variant. In the corner case when multiple -accelerators are available and supported, PyTorch package maintainers -indicate which one takes preference by default. - - -Optimized CPU variants -'''''''''''''''''''''' - -Wheel variants can be used to provide variants requiring specific CPU -extensions, beyond what platform tags currently provide. They can be -particularly helpful when runtime dispatching is impractical, when the -package relies on prebuilt components that use instructions above the -baseline, when availability of instruction sets implies library ABI -changes, or simply to benefit from compiler optimizations such as -auto-vectorization applied across the code base. - -For example, an x86-64 CPU plugin can detect the capabilities for the -installed CPU, mapping them onto the appropriate x86-64 architecture -level and a set of extended instruction sets. Variant wheels indicate -which level and/or instruction sets are required. The installer filters -out variants that do not meet the requirements and select the best -optimized variant. A non-variant wheel can be used to represent the -architecture baseline, if supported. - -Implementation using wheel variants makes it possible to provide -fine-grained indication of instruction sets required, with plugins that -can be updated as frequently as necessary. In particular, it is neither -necessary to cover all available instruction sets from the start, nor to -update the installers whenever the instruction set coverage needs to be -improved. - - -BLAS / LAPACK variants -'''''''''''''''''''''' - -Packages such as NumPy_ and SciPy_ can be built using different BLAS / -LAPACK libraries. Users may wish to choose a specific library for -improved performance on a particular hardware, or based on license -considerations. Furthermore, different libraries may use different -OpenMP implementations, whereas using a consistent implementation across -the stack can avoid degrading performance through spawning too many -threads. - -BLAS / LAPACK variants do not require a plugin at install time, since -all variants built for a particular platform are compatible with it. -Therefore, an ahead-of-time provider (with ``install-time = false``) -that provides a predefined set of BLAS / LAPACK library names can be -used. When the package is installed, normally the default variant is -used, but the user can explicitly select another one. - - -Debug package variants -'''''''''''''''''''''' - -A package may wish to provide a special debug-enabled builds for -debugging or CI purposes, in addition to the regular release build. For -this purpose, an optional ahead-of-time provider can be used -(``install-time = false`` with ``optional = true``), defining a custom -property for the debug builds. Since the provider is disabled by -default, users normally install the non-variant wheel providing the -release build. However, they can easily obtain the debug build by -enabling the optional provider or selecting the variant explicitly. - - -Package ABI matching -'''''''''''''''''''' - -Packages such as vLLM_ -need to be pinned to the PyTorch version they were built against to -preserve Application Binary Interface (ABI) compatibility. This often -results in unnecessarily strict pins in package versions, making it -impossible to find a satisfactory resolution for an environment -involving multiple packages requiring different versions of PyTorch, or -resorting to source builds. Variant wheels can be used to publish -variants of vLLM built against different PyTorch versions, therefore -enabling upstream to easily provide support for multiple versions -simultaneously. - -The optional ``abi_dependency`` extension can be used to build multiple -``vllm`` variants that are pinned to different PyTorch versions, e.g.: - -- ``vllm-0.11.0-...-torch29.wheel`` with - ``abi_dependency :: torch :: 2.9`` -- ``vllm-0.11.0-...-torch28.wheel`` with - ``abi_dependency :: torch :: 2.8`` -- ``vllm-0.11.0-...-torch27.wheel`` with - ``abi_dependency :: torch :: 2.7`` - - -Security implications -===================== - -The proposal introduces a plugin system for querying the system -capabilities in order to determine variant wheel capability. The system -permits specifying additional Python packages providing the plugins -in the package index metadata. Installers and other tools that need -to determine whether a particular wheel is installable, or select -the most preferred variant among multiple variant wheels, may need -to install these packages and execute the code within them while -resolving dependencies or processing wheels. - -This elevates the supply-chain attack potential by introducing two new -points for malicious actors to inject arbitrary code payload: - -1. Publishing a version of a variant provider plugin or one of its - dependencies with malicious code. - -2. Introducing a malicious variant provider plugin in an existing - package metadata. - -While such attacks are already possible at the package dependency level, -it needs to be emphasized that in some scenarios the affected tools are -executed with elevated privileges, e.g. when installing packages for -multi-user systems, while the installed packages are only used with -regular user privileges afterwards. Therefore, variant provider plugins -could introduce a Remote Code Execution vulnerability with elevated -privileges. - -A similar issue already exists in the packaging ecosystem when packages -are installed from source distributions, whereas build backends -and other build dependencies are installed and executed. However, -various tools operating purely on wheels, as well as users using -tool-specific options to disable use of source distributions, -have been relying on the assumption that no code external to the system -will be executed while resolving dependencies, installing a wheel or -otherwise processing it. To uphold this assumption, the proposal -explicitly requires that untrusted provider plugin packages are never -installed without explicit user consent. - -The `Providers`_ section of the specification provides further -suggestions that aim to improve both security and the user experience. -Particularly, it is expected that the most popular provider plugins will -be available out of the box, and a dedicated team of maintainers -(initially including a subset of the PEP authors) will be responsible -for inspecting them for security risks and vetting the plugins as safe -to use. Installers will be able to either use a published allowlist, -vendor specific provider plugin versions, reimplement them or use them -as a Python library at their leisure. - -This will lead to the majority of packages focusing on these specific -plugins, rather than implementing competing solutions. Plugins requiring -explicit opt-in should be rare, and primarily affect expert users. This -is important to make variant usage secure-by-default. Furthermore, the -frequent disruption of workflows incentivises users to blanket-allow all -plugins (security fatigue). - -Furthermore, the specification permits using static configuration as -input to skip running plugins altogether. - - -Specification -============= - -This PEP proposes a set of extensions to the -:ref:`packaging:binary-distribution-format` specification that enable -building additional variants of wheels that can be installed by -variant-aware tools while being ignored by programs that do not -implement this 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`. - - -Extended wheel filename ------------------------ - -The wheel filename template originally defined by :pep:`427` is changed -to: - -.. code:: text - - {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}(-{variant label})?.whl - +++++++++++++++++++ - -Wheels using extensions introduced by this PEP MUST feature the variant -label component. The label MUST adhere to the following rules: - -- Lower case only (to prevent issues with case-sensitive - vs. case-insensitive filesystems) -- Between 1-16 characters -- Using only ``0-9``, ``a-z``, ``.`` or ``_`` ASCII characters - -This is equivalent to the following regular expression: -``^[0-9a-z._]{1,16}$``. - -Every label MUST uniquely correspond to a specific set of variant -properties, which MUST be the same for all wheels using the same label -within a single package version. Variant labels SHOULD be specified at -wheel build time, as human-readable strings. The label ``null`` is -reserved for the null variant and MUST use an empty set of variant -properties. - -Installers that do not implement this specification MUST ignore wheels -with variant label when installing from an index, and fall back to a -wheel without such label if it is available. If no such wheel is -available, the installer SHOULD output an appropriate diagnostic, -in particular warning if it results in selecting an earlier package -version or a clear error if no package version can be installed. - -Examples: - -- Non-variant wheel: - ``numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl`` -- Wheel with variant label: - ``numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64-x86_64_v3.whl`` +wheel characteristics beyond what existing wheel tags provide. Every +variant wheel carries zero or more variant properties. Much like +:doc:`packaging:specifications/platform-compatibility-tags`, variant +properties are used both to determine whether the wheel is compatible +with the system in question and to select the most suitable wheel to +install from multiple compatible wheels. + +Unlike tags, variant properties are not stored in the wheel filename, +but in a dedicated metadata file inside the wheel. To distinguish +between different wheel variants and provide a human-readable +identification, variant wheels carry an additional variant label +component in the filename. This label is specified along with the rest +of variant metadata in the project's source tree (normally in +``pyproject.toml``), and it is processed by the build backend that +afterwards embeds it into the built wheel. Additionally, this +information is copied into a dedicated +``{name}-{version}-variants.json`` file on the index, so that clients +can obtain it without having to fetch the wheels. + +The properties are organized into a hierarchical structure of +namespaces, features and feature values. Every namespace is governed by +a variant provider that is also defined as part of the variant metadata. +The provider serves as the data source used to determine which variant +properties are compatible with the user's system, and to order them from +the most desirable to the least desirable. This information is +afterwards used to filter the available variant wheels into install +candidates, and select the one that is most preferred among them. The +data can either be provided in a static manner, in which case all the +compatible properties are embedded in the metadata, or it can be +dynamically established from a running system. + +Providers that need to establish property compatibility are normally +provisioned as installable Python packages implementing a plugin API. +They may also be vendored or reimplemented by installers to improve user +experience. Variant properties ------------------ -Every variant wheel MUST be described by zero or more variant -properties. A variant wheel with exactly zero properties represents the -null variant. The properties are specified when the variant wheel is -being built, using a mechanism defined by the project's build backend. - -Each variant property is described by a 3-tuple that is serialized into -the following format: +Variant properties can be thought of as a correspondence to and an +extension of +:doc:`packaging:specifications/platform-compatibility-tags`. In this +analogy, variant features are the equivalent of tag types, while their +values are the equivalent of the tags themselves. However, variant +features are not fixed and the wheel can have any number of them. +Furthermore, they are organized into namespaces that are governed +independently. -:: +A variant property encodes a single feature value that the wheel is +compatible with. Conversely, if the wheel is compatible with multiple +values of a given feature, they are represented by multiple properties. - {namespace} :: {feature_name} :: {feature_value} +For example, let's say that a package defined a ``nvidia`` namespace +that permitted the following three features: -The namespace MUST consist only of ``0-9``, ``a-z`` and ``_`` ASCII -characters (``^[a-z0-9_]+$``). It MUST correspond to a single variant -provider. +1. ``nvidia :: cuda_version_lower_bound`` specifying the minimum + supported CUDA runtime version. +2. ``nvidia :: cuda_version_upper_bound`` specifying the maximum + supported CUDA runtime version. +3. ``nvidia :: sm_arch`` specifying a single supported GPU architecture. -The feature name MUST consist only of ``0-9``, ``a-z`` and ``_`` ASCII -characters (``^[a-z0-9_]+$``). It MUST correspond to a valid feature -name defined by the respective variant provider in the namespace. - -The feature value MUST consist only of ``0-9``, ``a-z``, ``_`` and ``.`` -ASCII Characters (``^[a-z0-9_.]+$``). It MUST correspond to a valid -value defined by the respective variant provider for the feature. - -If a feature is marked as "multi-value" by the provider plugin, a single -variant wheel can define multiple properties sharing the same namespace -and feature name. Otherwise, there MUST NOT be more than a single value -corresponding to a single pair of namespace and feature name within a -variant wheel. - -For a variant wheel to be considered compatible with the system, all of -the features defined within it MUST be determined to be compatible. For -a feature to be compatible, at least a single value corresponding to it -MUST be compatible. - -Examples: +This package produced a wheel with the following variant properties: .. code:: text - # all of the following must be supported - x86_64 :: level :: v3 - x86_64 :: avx512_bf16 :: on nvidia :: cuda_version_lower_bound :: 12.8 - # additionally, at least one of the following must be supported nvidia :: sm_arch :: 120_real nvidia :: sm_arch :: 110_real +This would imply the following: -Providers ---------- - -When installing or resolving variant wheels, installers SHOULD query the -variant providers to verify whether a given wheel's properties are -compatible with the system and to select the best variant through -`variant ordering`_. However, they MAY provide an option to omit the -verification and install a specified variant explicitly. - -Providers can be marked as install-time or ahead-of-time. For -install-time providers, installers MUST either query the provider for -variant property compatibility, or use user-provided compatibility -information. Installers MAY vendor or reimplement specific providers. -The format of user-provided information is left implementation-defined. - -For ahead-of-time providers, they MUST use the static metadata embedded -in the wheel instead. - -Providers can be marked as optional. If a provider is marked optional, -then the installer MUST NOT query said provider by default, and instead -assume that its properties are incompatible. It SHOULD provide an option -to enable optional providers. - -Providers can also be made conditional to -:ref:`dependency-specifiers-environment-markers`. If that is the case, -the installer MUST check the markers against the environment to which -wheels are going to be installed. It MUST NOT use any providers whose -markers do not match, and instead assume that their properties are -incompatible. - -All the tools that need to query variant providers and are run in a -security-sensitive context, MUST NOT install or run provider packages, -unless they can determine the particular provider package version to be -trusted. The exact mechanism used to do that is implementation-specific. -However, installers SHOULD ensure that the most commonly used providers -can be securely used without an explicit user opt-in. - -When installing provider packages, tools SHOULD use an isolated virtual -environment. - -Install-time provider packages SHOULD take measures to guard against -supply chain attacks, for example by vendoring all dependencies. - -For a consistent experience between tools, variant wheels SHOULD be -supported by default. Tools MAY provide an option to only use -non-variant wheels. - - -Variant metadata ----------------- +- The wheel can only be installed on a system compatible with the + ``nvidia :: cuda_version_lower_bound :: 12.8`` property, that is + featuring installed CUDA runtime version 12.8 or newer. +- Since there is no ``nvidia :: cuda_version_upper_bound``, that feature + is not taken into consideration and there is no upper bound on CUDA + runtime version. +- The wheel can only be installed on a system compatible with at least + one of the ``nvidia :: sm_arch`` values listed, that is having a GPU + with ``120_real`` or ``110_real`` architecture. -This section describes the metadata format for the providers, variants -and properties of a package and its wheels. The format is used in three -locations, with slight variations: -1. in the source tree, inside the ``pyproject.toml`` file -2. in the built wheel, as a ``*.dist-info/variant.json`` file -3. on the package index, as a ``{name}-{version}-variants.json`` file. - -All three variants metadata files share a common JSON-compatible -structure: - -.. code:: text +An end-to-end example +--------------------- - (root) - | - +- providers - | +- {namespace} - | +- enable-if : str | None = None - | +- install-time : bool = True - | +- optional : bool = False - | +- plugin-api : str | None = None - | +- requires : list[str] = [] - | - +- default-priorities - | +- namespace : list[str] - | +- feature - | +- {namespace} : list[str] = [] - | +- property - | +- {namespace} - | +- {feature} : list[str] = [] - | - +- static-properties - | +- {namespace} - | +- {feature} : list[str] = [] - | - +- variants - +- {variant_label} - +- {namespace} - +- {feature} : list[str] = [] - -The top-level object is a dictionary rooted at a specific point in the -containing file. Its individual keys are sub-dictionaries that are -described in the subsequent sections, along with the requirements for -their presence. The tools MUST ignore unknown keys in the dictionaries -for forwards compatibility of updates to the PEP. However, users -MUST NOT use unsupported keys to avoid potential future conflicts. - -A `JSON schema `__ is included in the Appendix -of this PEP, to ease comprehension and validation of the metadata -format. This schema will be updated with each revision to the variant -metadata specification. The schema is available in -:ref:`0817-variant-json-schema`. - -Ultimately, the variant metadata JSON schema SHOULD be served by -`packaging.python.org `__. - -Provider information -'''''''''''''''''''' - -``providers`` is a dictionary, the keys are namespaces, the values are -dictionaries with provider information. It specifies how to install and -use variant providers. A provider information dictionary MUST be -declared in ``pyproject.toml`` for every variant namespace supported by -the package. It MUST be copied to ``variant.json`` as-is, including -the data for providers that are not used in the particular wheel. - -The use of provider information is described in the `Providers`_ and -`Provider plugin API`_ sections. - -A provider information dictionary MAY contain the following keys: - -- ``enable-if: str``: An :ref:`environment marker - ` defining when the plugin - should be used. - -- ``install-time: bool``: Whether this is an install-time provider. - Defaults to ``true``. ``false`` means that it is an AoT provider - instead. - -- ``optional: bool``: Whether the provider is optional. Defaults - to ``false``. If it is ``true``, the provider is - considered optional. - -- ``plugin-api: str``: The API endpoint for the plugin. If it is - specified, it MUST be an object reference as explained in the `API - endpoint`_ section. If it is missing, the package name from the first - dependency specifier in ``requires`` is used, after replacing all - ``-`` characters with ``_`` in the normalized package name. - -- ``requires: list[str]``: A list of zero or more package - :ref:`dependency specifiers `, that are used to - install the provider plugin. If the dependency specifiers include - environment markers, these are evaluated against the environment where - the plugin is being installed and the requirements for which the - markers evaluate to false are filtered out. In that case, at least - one dependency MUST remain present in every possible environment. - Additionally, if ``plugin-api`` is not specified, the first dependency - present after filtering MUST always evaluate to the same API endpoint. - -All the fields are OPTIONAL, with the following exceptions: - -1. If ``install-time`` is true, the dictionary describes an install-time - provider and the ``requires`` key MUST be present and specify at - least one dependency. - -2. If ``install-time`` is false, it describes an AoT provider and the - ``requires`` key is OPTIONAL. In that case: - - a. If ``requires`` is provided and non-empty, the provider dictionary - MUST reference an AoT provider plugin that will be queried at - build time to fill ``static-properties``. - - b. Otherwise, ``static-properties`` MUST be specified in - ``pyproject.toml``. - - -Default priorities -'''''''''''''''''' - -The ``default-priorities`` dictionary controls the ordering of variants. -The exact algorithm is described in the `Variant ordering`_ section. - -It has a single REQUIRED key: - -- ``namespace: list[str]``: All namespaces used by the wheel variants, - ordered in decreasing priority. This list MUST have the same members - as the keys of the ``providers`` dictionary. - -It MAY have the following OPTIONAL keys: - -- ``feature: dict[str, list[str]]``: A dictionary with namespaces as - keys, and ordered list of corresponding feature names as values. The - values in each list override the default ordering from the provider - output. They are listed from the highest priority to the lowest - priority. Features not present on the list are considered of lower - priority than those present, and their relative priority is defined by - the plugin. - -- ``property: dict[str, dict[str, list[str]]]``: A nested dictionary - with namespaces as first-level keys, feature names as second-level - keys and ordered lists of corresponding property values as - second-level values. The values present in the list override the - default ordering from the provider output. They are listed from the - the highest priority to the lowest priority. Properties not present on - the list are considered of lower priority than these present, and - their relative priority is defined by the plugin output. +The following subsections present a complete example of building a +variant wheel, publishing it on an index and installing it on a system. +It covers the baseline scenario where wheel compatibility needs to be +determined at install time, by inspecting the target system. Other cases +will be discussed in subsequent sections. -Static properties -''''''''''''''''' +Building variant wheels +''''''''''''''''''''''' -The ``static-properties`` dictionary specifies the supported properties -for AoT providers. It is a nested dictionary with namespaces as first -level keys, feature name as second level keys and ordered lists of -feature values as second level values. +The recommended way of integrating variant wheel support in a build +backend is to store variant metadata in the ``pyproject.toml`` file, and +to select the variant being built via a ``config_settings`` parameter. -In ``pyproject.toml`` file, the namespaces present in this dictionary -MUST correspond to all AoT providers without a -plugin (i.e. with ``install-time`` of ``false`` and no or empty -``requires``). When building a wheel, the build backend MUST query the -AoT provider plugins (i.e. these with ``install-time`` being ``false`` -and non-empty ``requires``) to obtain supported properties and embed -them into the dictionary. Therefore, the dictionary in ``variant.json`` -and ``*-variants.json`` MUST contain namespaces for all AoT providers -(i.e. all providers with ``install-time`` being ``false``). +Consider the following example: -Since TOML and JSON dictionaries are unsorted, so are the features in -the ``static-properties`` dictionary. If more than one feature is -specified for a namespace, then the order for all features MUST be -specified in ``default-priorities.feature.{namespace}``. If an AoT -plugin is used to fill ``static-properties``, then the features not -already in the list in ``pyproject.toml`` MUST be appended to it. +.. code-block:: toml -The list of values is ordered from the most preferred to the least -preferred, same as the lists returned by ``get_supported_configs()`` -plugin API call (as defined in `plugin interface`_). The -``default-priorities.property`` dict can be used to override the -property ordering. + [variant] + "$schema" = "..." + [variant.default-priorities] + # specifies preference ordering: nvidia support is more important + # than x86_64 optimizations + namespace = ["nvidia", "x86_64"] -Variants -'''''''' + # == provider definitions == -The ``variants`` dictionary is used in ``variant.json`` to indicate the -variant that the wheel was built for, and in ``*-variants.json`` to -indicate all the wheel variants available. It's a 3-level dictionary -listing all properties per variant label: The first level keys are -variant labels, the second level keys are namespaces, the third level -are feature names, and the third level values are lists of feature -values. + [variant.providers.nvidia] + requires = ["nvidia-variant-provider"] + [variant.providers.x86_64] + requires = ["x86-64-variant-provider"] -``pyproject.toml``: variant project-level data table -'''''''''''''''''''''''''''''''''''''''''''''''''''' + # == buildable variant definitions == -The ``pyproject.toml`` file is the standard project configuration file -as defined in :doc:`packaging:specifications/pyproject-toml`. The -variant metadata MUST be rooted at a top-level table named ``variant``. -It MUST NOT specify the ``variants`` dictionary. It is used by build -backends to build variant wheels. + [variant.variants.cu130] + nvidia.cuda_version_lower_bound = ["13.0"] + nvidia.sm_arch = ["120_virtual", "120_real", "100_real", "90_real"] -Example Structure: + [variant.variants.x86_64_v4] + x86_64.level = ["v4"] -.. code:: toml +This example defines two providers: ``nvidia`` responsible for NVIDIA +CUDA support, and ``x86_64`` responsible for x86_64 CPU optimizations. +These two providers are used to specify two buildable variants: - [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" +- ``cu130`` that corresponds to a package using CUDA 13.0 support +- ``x86_64_v4`` that corresponds to a package using code optimized for + x86-64-v4 CPU - [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" +Per the precedence rules specified in +``variant.default-priorities.namespace``, the ``cu130`` variant +featuring GPU support will be preferred if CUDA runtime and a compatible +GPU are available. Otherwise, the ``x86_64_v4`` variant will be the next +best choice, provided a compatible CPU is available. - [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 +A specific variant would then be built via an invocation such as:: + python -m build -w -Cvariant-label=cu130 -``*.dist-info/variant.json``: the packaged variant metadata file -'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +A build backend could behave in the following way: -The ``variant.json`` file MUST be present in the ``*.dist-info/`` -directory of a built variant wheel. It is serialized into JSON, with the -variant metadata dictionary being the top object. It MUST include all -the variant metadata present in ``pyproject.toml``, copied as indicated -in the individual key sections. In addition to that, it MUST contain: +1. Detect ``variant-label`` in ``config_settings``, and enable variant + wheel builds. +2. Validate the ``variant`` table against the schema. +3. Find the ``cu130`` label in the ``variant.variants`` table, and + determine the corresponding properties. +4. In ``get_requires_for_build_wheel()`` hook, return the plugin + packages needed for the namespaces found in the properties. This will + cause the build frontend to install them in the build environment. +5. Query the installed plugins to verify that the variant properties are + valid. +6. Construct the ``*.dist-info/variant.json`` file from the ``variant`` + table. The ``variants`` subtable is filtered to the entry for the + variant being built, the remaining subtables are copied verbatim. +7. Build the wheel. This could involve processing tool-specific + configuration to apply variant-specific modifications to the build + process. -- a ``$schema`` key whose value is the URL corresponding to the schema - file supplied in the appendix of this PEP. The URL contains the - version of the format, and a new version MUST be added to the appendix - whenever the format changes in the future, +The wheel would be named ``*-cu130.whl``. In it, the variant metadata +file ``*.dist-info/variant.json`` would be similar to the following +example: -- a ``variants`` object listing exactly one variant - the variant - provided by the wheel. +.. code-block:: json5 -The variant.json file corresponding to the wheel built from the example -pyproject.toml file for x86-64-v3 would look like: + { + // == copied verbatim from pyproject.toml == -.. code:: json5 + "$schema": "...", - { - // The schema URL will be replaced with the final URL on packaging.python.org - "$schema": "https://variants-schema.wheelnext.dev/v0.0.3.json", "default-priorities": { - "feature": { - "aarch64": ["version"], - "x86_64": ["level"] - }, - "namespace": ["x86_64", "aarch64", "blas_lapack"], - "property": { - "x86_64": { - "level": ["v3", "v2", "v1"] - } - } + "namespace": ["nvidia", "x86_64"] }, + "providers": { - "aarch64": { - "enable-if": "platform_machine == 'aarch64' or 'arm' in platform_machine", - "plugin-api": "provider_variant_aarch64.plugin:AArch64Plugin", - "requires": [ - "provider-variant-aarch64 >=0.0.1; python_version >= '3.12'", - "legacy-provider-variant-aarch64 >=0.0.1; python_version < '3.12'" - ] - }, - "blas_lapack": { - "install-time": false, - "requires": ["blas-lapack-variant-provider"] - }, - "x86_64": { - "enable-if": "platform_machine == 'x86_64' or platform_machine == 'AMD64'", - "plugin-api": "provider_variant_x86_64.plugin:X8664Plugin", - "requires": ["provider-variant-x86-64 >=0.0.1"] - } + "nvidia": { + "requires": ["nvidia-variant-provider"] + }, + "x86_64": { + "requires": ["x86-64-variant-provider"] + } }, - "static-properties": { - "blas_lapack": { - "provider": ["accelerate", "openblas", "mkl"] - }, - }, - "variants": { - // always a single entry, expressing the variant properties of the wheel - "x8664v3_openblas": { - "blas_lapack": { - "provider": ["openblas"] - }, - "x86_64": { - "level": ["v3"] - } - } - } - } - -``{name}-{version}-variants.json``: the index level variant metadata file -''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -For every package version that includes at least one variant wheel, -there MUST exist a corresponding ``{name}-{version}-variants.json`` -file, hosted and served by the package index. The ``{name}`` and -``{version}`` placeholders correspond to the package name and version, -normalized according to the same rules as wheel files, as found in the -:ref:`packaging:wheel-file-name-spec` of the Binary Distribution Format -specification. The link to this file MUST be present on all index pages -where the variant wheels are linked. It is presented in the same simple -repository format as source distribution and wheel links in the index, -including an (OPTIONAL) hash. - -This file uses the same structure as ``variant.json`` described above, -except that the variants object MUST list all variants available on the -package index for the package version in question. It is RECOMMENDED -that tools enforce the same contents of the ``default-priorities``, -``providers`` and ``static-properties`` sections for all variants listed -in the file, though careful merging is possible, as long as no -conflicting information is introduced, and the resolution results within -a subset of variants do not change. - -The index MAY generate the index level variant metadata file -automatically from uploaded wheel metadata. If that is the case, the -file SHOULD NOT be published until it is final, and once published, it -SHOULD NOT change, as clients MAY cache it. - -If the file is not generated automatically, the index MUST permit -package maintainers to upload it. Once the variant level metadata file -is uploaded, the package maintainers SHOULD NOT upload new variants for -the version in question. - -The ``foo-1.2.3-variants.json`` corresponding to the package with two -wheel variants, one of them listed in the previous example, would look -like: - -.. code:: json5 + // == actual properties of the built variant wheel == - { - // The schema URL will be replaced with the final URL on packaging.python.org - "$schema": "https://variants-schema.wheelnext.dev/v0.0.3.json", - "default-priorities": { - // identical to above - }, - "providers": { - // identical to above - }, - "static-properties": { - // identical to above - }, "variants": { - // all available wheel variants - "x8664v3_openblas": { - "blas_lapack": { - "provider": ["openblas"] - }, - "x86_64": { - "level": ["v3"] - } - }, - "x8664v4_mkl": { - "blas_lapack": { - "provider": ["mkl"] - }, - "x86_64": { - "level": ["v4"] - } - } - } + "cu130": { + "nvidia": { + "cuda_version_lower_bound": ["13.0"], + "sm_arch": ["120_virtual", "120_real", "100_real", "90_real"] + } + } + } } -Variant ordering ----------------- - -To determine which variant wheel to install when multiple wheels are -compatible, variant wheels MUST be ordered by their variant properties. - -For the purpose of ordering, variant properties are grouped into -features, and features into namespaces. The ordering MUST be equivalent -to the following algorithm: - -1. Construct the ordered list of namespaces by copying the value of the - ``default-priorities.namespace`` key. - -2. For every namespace: - - i. Construct the initial ordered list of feature names by copying the - value of the respective ``default-priorities.feature.{namespace}`` - key. - - ii. Obtain the supported feature names from the provider, in order. - For every feature name that is not present in the constructed - list, append it to the end. - - After this step, a list of ordered feature names is available for - every namespace. - -3. For every feature: - - i. Construct the initial ordered list of values by copying the value - of the respective - ``default-priorities.property.{namespace}.{feature_name}`` key. - - ii. Obtain the supported values from the provider, in order. For - every value that is not present in the constructed list, append - it to the end. - - After this step, a list of ordered property values is available for - every feature. +Publishing variant wheels on an index +''''''''''''''''''''''''''''''''''''' -4. For every variant property present in at least one of the compatible - variant wheels, construct a sort key that is a 3-tuple consisting of - its namespace, feature name and feature value indices in the - respective ordered lists. +To enable resolvers to select between different variant wheels without +having to fetch the variant metadata straight from wheel files, an +index-level metadata file ``{name}-{version}-variants.json`` needs to +be published alongside these wheels. -5. For every compatible variant wheel, order its properties by their - sort keys, in ascending order. +Initially this could be done via running a dedicated tool on all built +variant wheels, for example by invoking:: -6. To order variant wheels, compare their sorted properties. If the - properties at the first position are different, the variant with the - lower 3-tuple of the respective property is sorted earlier. If they - are the same, compare the properties at the second position, and so - on, until either a tie-breaker is found or the list of properties of - one wheel is exhausted. In the latter case, the variant with more - properties is sorted earlier. + variantlib generate-index-json -d dist/ -After this process, the variant wheels are sorted from the most -preferred to the least preferred. The null variant naturally sorts after -all the other variants, and the non-variant wheel MUST be sorted after -the null variant. Multiple wheels with the same variant set (and -multiple non-variant wheels) MUST then be ordered according to their -platform compatibility tags. +The tool collects the variant metadata from all wheels, ensures that it +is consistent and merges it to create the index-level metadata file. +This file can be then uploaded to the index alongside variant wheels. -Alternatively, the sort algorithm for variant wheels could be described -using the following pseudocode. For simplicity, this code does not -account for non-variant wheels or tags. +Eventually, this manual step will no longer be necessary when uploading +variant wheels to PyPI: PyPI will automatically detect that a variant +wheel is uploaded, read its variant metadata and merge it into its +database. The combined metadata from the database will be published at +the URL corresponding to the index-level metadata file. -.. code:: python +The index-level metadata file for the two ``*-cu130.whl`` and +``*-x86_64_v4.whl`` files could look like: - from typing import Self +.. code-block:: json5 + { + // == copied verbatim from variant.json == - def get_supported_feature_names(namespace: str) -> list[str]: - """Get feature names from plugin's get_supported_configs()""" - ... + "$schema": "...", + "default-priorities": { + "namespace": ["nvidia", "x86_64"] + }, - def get_supported_feature_values(namespace: str, feature_name: str) -> list[str]: - """Get feature values from plugin's get_supported_configs()""" - ... + "providers": { + "nvidia": { + "requires": ["nvidia-variant-provider"] + }, + "x86_64": { + "requires": ["x86-64-variant-provider"] + } + }, + // == merged from different variant wheels == - # default-priorities dict from variant metadata - default_priorities = { - "namespace": [...], # : list[str] - "feature": {...}, # : dict[str, list[str]] - "property": {...}, # : dict[str, dict[str, list[str]]] + "variants": { + "cu130": { + "nvidia": { + "cuda_version_lower_bound": ["13.0"], + "sm_arch": ["120_virtual", "120_real", "100_real", "90_real"] + } + }, + "x86_64_v4": { + "x86_64": { + "level": ["v4"] + } + } + } } - # 1. Construct the ordered list of namespaces. - namespace_order = default_priorities["namespace"] - feature_order = {} - value_order = {} - - for namespace in namespace_order: - # 2. Construct the ordered lists of feature names. - feature_order[namespace] = default_priorities["feature"].get(namespace, []) - for feature_name in get_supported_feature_names(namespace): - if feature_name not in feature_order[namespace]: - feature_order[namespace].append(feature_name) - - value_order[namespace] = {} - for feature_name in feature_order[namespace]: - # 3. Construct the ordered lists of feature values. - value_order[namespace][feature_name] = ( - default_priorities["property"].get(namespace, {}).get(feature_name, []) - ) - for feature_value in get_supported_feature_values(namespace, feature_name): - if feature_value not in value_order[namespace][feature_name]: - value_order[namespace][feature_name].append(feature_value) - - - def property_key(prop: tuple[str, str, str]) -> tuple[int, int, int]: - """Construct a sort key for variant property (akin to step 4.)""" - namespace, feature_name, feature_value = prop - return ( - namespace_order.index(namespace), - feature_order[namespace].index(feature_name), - value_order[namespace][feature_name].index(feature_value), - ) - - - class VariantWheel: - """Example class exposing properties of a variant wheel""" - properties: list[tuple[str, str, str]] - - def __lt__(self: Self, other: Self) -> bool: - """Variant comparison function for sorting (akin to step 6.)""" - for self_prop, other_prop in zip(self.properties, other.properties): - if self_prop != other_prop: - return property_key(self_prop) < property_key(other_prop) - return len(self.properties) > len(other.properties) - - - # A list of variant wheels to sort. - wheels: list[VariantWheel] = [...] - - - for wheel in wheels: - # 5. Order variant wheel properties by their sort keys. - wheel.properties.sort(key=property_key) - # 6. Order variant wheels by comparing their sorted properties - # (see VariantWheel.__lt__()) - wheels.sort() - - -Integration with ``pylock.toml`` --------------------------------- - -The following section is added to the -:doc:`packaging:specifications/pylock-toml`: - -.. code:: rst - - .. _pylock-packages-variants-json: - - ``[packages.variants-json]`` - ---------------------------- - - - **Type**: table - - **Required?**: no; requires that :ref:`pylock-packages-wheels` is used, - mutually-exclusive with :ref:`pylock-packages-vcs`, - :ref:`pylock-packages-directory`, and :ref:`pylock-packages-archive`. - - **Inspiration**: uv_ - - The URL or path to the ``variants.json`` file. - - Only used if the project uses :ref:`wheel variants `. - - .. _pylock-packages-variants-json-url: - - ``packages.variants-json.url`` - '''''''''''''''''''''''''''''' - - See :ref:`pylock-packages-archive-url`. - - .. _pylock-packages-variants-json-path: - - ``packages.variants-json.path`` - ''''''''''''''''''''''''''''''' - - See :ref:`pylock-packages-archive-path`. - - .. _pylock-packages-variants-json-hashes: - - ``packages.variants-json.hashes`` - ''''''''''''''''''''''''''''''''' - - See :ref:`pylock-packages-archive-hashes`. - -If there is a ``[packages.variants-json]`` section, the installer SHOULD -resolve variants to select the best wheel file. - - -Provider plugin API -------------------- - -High level design -''''''''''''''''' - -Every provider plugin MUST operate within a single namespace. This -namespace is used as a unique key for all plugin-related operations. All -the properties defined by the plugin are bound within the plugin's -namespace, and the plugin defines all the valid feature names and values -within that namespace. - -Provider plugin authors SHOULD choose namespaces that can be clearly -associated with the project they represent, and avoid namespaces that -refer to other projects or generic terms that could lead to naming -conflicts in the future. - -All variants published on a single index for a specific package version -MUST use the same provider for a given namespace. Attempting to load -more than one plugin for the same namespace in the same release version -MUST result in a fatal error. While multiple plugins for the same -namespace MAY exist across different packages or release versions (such -as when a plugin is forked due to being unmaintained), they are mutually -exclusive within any single release version. - -To make it easier to discover and install plugins, they SHOULD be -published in the same indexes that the packages using them. In -particular, packages published to PyPI MUST NOT rely on plugins that -need to be installed from other indexes. - -Except for namespaces reserved as part of this PEP, installable Python -packages MUST be provided for plugins. However, as noted in the -`Providers`_ section, these plugins can also be reimplemented by tools -needing them. In the latter case, the resulting reimplementation does -not need to follow the API defined in this section. - -Plugin packages may be run in an isolated environment. They MUST NOT -make decisions based on installed packages. - -A plugin implemented as Python package exposes two kinds of objects at a -specified API endpoint: - -a. attributes that return a specific value after being accessed via: - - .. code:: text - - {API endpoint}.{attribute name} - -b. callables that are called via: - - .. code:: text - - {API endpoint}.{callable name}({arguments}...) - -These can be implemented either as modules, or classes with class -methods or static methods. The specifics are provided in the subsequent -sections. - - -API endpoint -'''''''''''' - -The location of the plugin code is called an "API endpoint", and it is -expressed using the object reference notation following the -:doc:`packaging:specifications/entry-points`: - -.. code:: text - - {import_path}(:{object_path})? - -An API endpoint specification is equivalent to the following Python -pseudocode: - -.. code:: python - - import {import_path} - - if "{object_path}": - plugin = {import_path}.{object_path} - else: - plugin = {import_path} - -API endpoints are used in two contexts: - -a. in the ``plugin-api`` key of variant metadata, either explicitly or - inferred from the package name in the ``requires`` key. This is the - primary method of using the plugin when building and installing - wheels. - -b. as the value of an installed entry point in the ``variant_plugins`` - group. The name of said entry point is insignificant. This is - OPTIONAL but RECOMMENDED, as it permits variant-related utilities to - discover variant plugins installed to the user's environment. - - -Variant feature config class -'''''''''''''''''''''''''''' - -The variant feature config class is used as a return value in plugin API -functions. It defines a single variant feature, along with a list of -possible values. Depending on the context, the order of values MAY be -significant. It is defined using the following protocol: - -.. code:: python - - from abc import abstractmethod - from typing import Protocol - - - class VariantFeatureConfigType(Protocol): - @property - @abstractmethod - def name(self) -> str: - """Feature name""" - raise NotImplementedError - - @property - @abstractmethod - def multi_value(self) -> bool: - """Does this property allow multiple values per variant?""" - raise NotImplementedError - - @property - @abstractmethod - def values(self) -> list[str]: - """List of values, possibly ordered from most preferred to least""" - raise NotImplementedError - -A "variant feature config" MUST provide the following properties or -attributes: - -- ``name: str`` specifying the feature name. - -- ``multi_value: bool`` specifying whether the feature is allowed to - have multiple corresponding values within a single variant wheel. If - it is ``False``, then it is an error to specify multiple values for - the feature. - -- ``values: list[str]`` specifying feature values. In contexts where the - order is significant, the values MUST be ordered from the most - preferred to the least preferred. - -All features are interpreted as being within the plugin's namespace. - - -Plugin interface -'''''''''''''''' - -The plugin interface MUST follow the following protocol: - -.. code:: python - - from abc import abstractmethod - from typing import Protocol - - - class PluginType(Protocol): - # Note: properties are used here for docstring purposes, these - # must be actually implemented as attributes. - - @property - @abstractmethod - def namespace(self) -> str: - """The provider namespace""" - raise NotImplementedError - - @property - def is_aot_plugin(self) -> bool: - """Is this plugin valid for `install-time = false`?""" - return False - - @classmethod - @abstractmethod - def get_all_configs(cls) -> list[VariantFeatureConfigType]: - """Get all valid configs for the plugin""" - raise NotImplementedError - - @classmethod - @abstractmethod - def get_supported_configs(cls) -> list[VariantFeatureConfigType]: - """Get supported configs for the current system""" - raise NotImplementedError - -The plugin interface MUST define the following attributes: - -- ``namespace: str`` specifying the plugin's namespace. - -- ``is_aot_plugin: bool`` indicating whether the plugin is a valid AoT - plugin. If that is the case, ``get_supported_configs()`` MUST always - return the same value as ``get_all_configs()`` (modulo ordering), - which MUST be a fixed list independent of the platform on which the - plugin is running. Defaults to ``False`` if unspecified. - -The plugin interface MUST provide the following functions: - -- ``get_all_config() -> list[VariantFeatureConfigType]`` that returns a - list of "variant feature configs" describing all valid variant - features within the plugin's namespace, along with all their permitted - values. The ordering of the lists is insignificant here. A particular - plugin version MUST always return the same value (modulo ordering), - irrespective of any runtime conditions. - -- ``get_supported_configs() -> list[VariantFeatureConfigType]`` that - returns a list of "variant feature configs" describing the variant - features within the plugin's namespace that are compatible with this - particular system, along with their values that are supported. The - variant feature and value lists MUST be ordered from the most - preferred to the least preferred, as they affect `variant - ordering`_. - -The value returned by ``get_supported_configs()`` MUST be a subset of -the feature names and values returned by ``get_all_configs()`` (modulo -ordering). - -The value returned by ``get_supported_configs()`` MAY be cached -throughout multiple packages in a single install session. - - -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"] - - - def _get_current_version() -> int: - """Returns currently installed runtime version""" - ... # implementation not provided - - - def _is_gpu_available(codename: str) -> bool: - """Is specified GPU installed?""" - ... # implementation not provided - - - class MyPlugin: - namespace = "example" - - # optional, defaults to False - is_aot_plugin = False - - # all valid properties - @staticmethod - def get_all_configs() -> list[VariantFeatureConfig]: - return [ - VariantFeatureConfig( - # example :: gpu -- multi-valued, since the package - # can target multiple GPUs - name="gpu", - # [narf, poit, zort] - values=_ALL_GPUS, - multi_value=True, - ), - VariantFeatureConfig( - # example :: min_version -- single-valued, since - # there is always one minimum - name="min_version", - # [1, 2, 3, 4] (order doesn't matter) - values=[str(x) for x in range(1, _MAX_VERSION + 1)], - multi_value=False, - ), - ] - - # properties compatible with the system - @staticmethod - def get_supported_configs() -> list[VariantFeatureConfig]: - current_version = _get_current_version() - if current_version is None: - # no runtime found, system not supported at all - return [] - - return [ - VariantFeatureConfig( - name="min_version", - # [current, current - 1, ..., 1] - values=[str(x) for x in range(current_version, 0, -1)], - multi_value=False, - ), - VariantFeatureConfig( - name="gpu", - # this may be empty if no GPUs are supported -- - # 'example :: gpu feature' is not supported then; - # but wheels with no GPU-specific code and only - # 'example :: min_version' could still be installed - values=[x for x in _ALL_GPUS if _is_gpu_available(x)], - multi_value=True, - ), - ] - - -Future extensions -''''''''''''''''' - -The future versions of this specification, as well as third-party -extensions MAY introduce additional properties and methods on the plugin -instances. The implementations SHOULD ignore additional attributes. +Querying a provider +''''''''''''''''''' -For best compatibility, all private attributes SHOULD be prefixed with -an underscore (``_``) character to avoid incidental conflicts with -future extensions. +The primary function of a provider is to determine which of the variant +properties are compatible with the user's system, and to order them from +the most preferable to the least preferable. For example, the purpose of +the ``nvidia`` provider used in the example is to determine whether the +user's system features a compatible NVIDIA CUDA runtime and a GPU, while +the purpose of the ``x86_64`` provider is to determine what CPU is used +and whether the instruction sets used in the wheel are supported by it. + +Both these providers rely on install-time checks. It is also possible to +define providers that do not require these checks, and these are +explained in `static properties in wheels`_. + +The most common providers, such as the ``nvidia`` provider in the +example, will likely be vendored or reimplemented by installers, and +they will work in an opt-out manner. + +For providers that are not provided by the installers themselves, the +Python packages listed in the ``requires`` list will be needed to +determine the compatibility and preference order. These packages are +opt-in by default. If the user opts in to using them, they are installed +into an isolated environment and queried via a dedicated API. Otherwise, +they are not used and the properties are assumed to be incompatible. + +Furthermore, it is possible to override the provider queries by `using +static compatibility information`_. + + +Using static compatibility information +'''''''''''''''''''''''''''''''''''''' + +While installing wheels natively and determining property compatibility +at install time is the primary use case for variant wheels, it is +considered equally important for users to be able to provide +compatibility information in a static format. This serves the twofold +goal of being able to avoid plugin invocations and being able to install +the package for another configuration than reported for the system where +the installer is being run. + +While the installers have the final choice of how to implement this, we +are planning to propose a standardized format based on TOML, to make it +suitable both for machine processing and human editing. For example, a +standard tool could query the system using installed variant provider +plugins and output a file resembling the following: + +.. code-block:: toml + + [variant] + "$schema" = "..." + + [[variant.providers]] + # specifies the exact package version used to generate the data + requires = ["nvidia-variant-provider==1.0.0"] + # ordering for the features listed below + feature-order = [ + "cuda_version_lower_bound", + "cuda_version_upper_bound", + "sm_arch", + ] + # properties queried from the plugin, in order of preference + [variant.providers.static-properties] + # CUDA 13.0 is installed, so all minimum versions lower than that + # are acceptable; the wheel built for the newest minimum version + # is preferred + cuda_version_lower_bound = [ + "13.0", "12.20", "12.19", "12.18", # ... + ] + # conversely, maximum versions higher than 13.0 are acceptable + # (the plugin generates a "safe" range); the wheel built for the + # highest maximum version is preferred + cuda_version_upper_bound = [ + "15.20", "15.19", "15.18", # ... + "13.0" + ] + # sm120 compatible GPU is installed; "real" is preferred over + # "virtual" + sm_arch = [ + "120_real", "120_virtual" + ] + [[variant.providers]] + requires = ["x86-64-variant-provider==1.0.0"] + feature-order = [ + "level", + "avx512vl", + "avx512dq", + "avx512cd", + ] + [variant.providers.static-properties] + # x86-64-v4 CPU is compatible with all older versions; higher + # (more optimized) levels are preferred + level = ["v4", "v3", "v2", "v1"] + # explicit instruction sets that are supported by the CPU + avx512vl = ["on"] + avx512dq = ["on"] + avx512cd = ["on"] + # ... + +Such a file could then be passed to an installer to use in place of +properties obtained from plugins. The installer would match the provider +entries based on the ``requires`` key. Depending on the configuration, a +provider not covered by the static file could either result in querying +the plugin, throwing an explicit error or treating the wheel as +incompatible. -Build backends --------------- -As a build backend can't determine whether the frontend supports variant -wheels or not, :pep:`517` and :pep:`660` hooks MUST build non-variant -wheels by default. Build backends MAY provide ways to request variant -builds. This specification does not define any specific configuration. +Installing a package from an index +'''''''''''''''''''''''''''''''''' -When building variant wheels, build backends MUST verify variant -metadata for correctness, and they MUST NOT emit wheels with -nonconformant ``variant.json`` files. They SHOULD also query providers -to determine whether variant properties requested by the user are valid, -though they MAY permit skipping this verification and therefore emitting -variant wheels with potentially unknown properties. +.. figure:: pep-0817/conceptual_diagram_installers.png + :target: _images/conceptual_diagram_installers.png + :class: invert-in-dark-mode + :alt: A diagram showing installing a package including variant wheel + handling. It is split into three columns: Developer, Installer + and Install-time providers. The diagram starts with Developer + initiating package install. The subsequent steps involve + installer, in order: resolver selects package version; + determine if release has variant wheels. If there are no + variant wheels, jump to installing package and report success. + If the version has variant wheels, check user's variant + preferences. In parallel, download JSON from index, then + extract variant provider configuration. If it uses AoT + providers only, converge to determine optimal variant + immediately. If it requires install-time providers, the + further path depends on whether non-vendored providers are + included. If they are not, query install-time providers + immediately and converge to determine optimal variant. If + non-vendored providers are included, they are installed if not + present in env and then queried. Querying providers involves + an exchange of data with different providers (in the diagram, + "provider 1" and "provider 2" are given as examples), each + filtering and ordering supported configurations for the + current environment. All the variant paths converge on + determining optimal variant, which is following by installing + package and reporting success. + A conceptual diagram of installing a wheel. -Variant environment markers +When asked to install a package from an index, the installer would: + +1. Query the remote index and select a version. Obtain the list of files + (distributions) for the selected version. +2. Initially filter wheels based on Platform Compatibility Tags. +3. Determine if any of the remaining wheels are variant wheels (have a + variant label in the filename). Let's say that in this case we found + ``*-cu130.whl`` and ``*-x86_64_v4.whl`` variants. +4. Find the ``{name}-{version}-variants.json`` file corresponding to + the selected version. Fetch the file and parse the variant metadata + in it. +5. Obtain property lists corresponding to the labels in the installable + variant wheels from the metadata file. Construct a set of all + namespaces used in them. +6. Obtain the provider information corresponding to these namespaces + from the metadata file. In this case the set is + ``{"nvidia", "x86_64"}``. +7. Query the providers to determine whether the properties of the + variant wheels on the list are compatible. Discard the variant wheels + that are incompatible. Let's say that both ``*-cu130.whl`` and + ``*-x86_64_v4.whl`` are compatible. +8. If any variant wheels remained, order them primarily based on + ``default-priorities.namespace`` and secondarily based on the data + obtained from the providers, and select the most appropriate variant. + In this case, the ``*-cu130.whl`` variant is selected. +9. If there are multiple wheels with the selected label but different + Platform Compatibility Tags, select the most appropriate of them + based on Platform Compatibility Tags. + + +Static properties in wheels --------------------------- -Four new :ref:`environment markers -` are introduced in -dependency specifications: - -1. ``variant_namespaces`` corresponding to the set of namespaces of all - the variant properties that the wheel variant was built for. -2. ``variant_features`` corresponding to the set of - ``namespace :: feature`` pairs of all the variant properties that the - wheel variant was built for. -3. ``variant_properties`` corresponding to the set of - ``namespace :: feature :: value`` tuples of all the variant - properties that the wheel variant was built for. -4. ``variant_label`` corresponding to the exact variant label that the - wheel was built with. For the non-variant wheel, it is an empty - string. - -The markers evaluating to sets of strings MUST be matched via the ``in`` -or ``not in`` operator, e.g.: - -.. code:: - - # satisfied by any "foo :: * :: *" property - dep1; "foo" in variant_namespaces - # satisfied by any "foo :: bar :: *" property - dep2; "foo :: bar" in variant_features - # satisfied only by "foo :: bar :: baz" property - dep3; "foo :: bar :: baz" in variant_properties - -The ``variant_label`` marker is a plain string: - -.. code:: - - # satisfied by the variant "foobar" - dep4; variant_label == "foobar" - # satisfied by any wheel other other than the null variant - # (including the non-variant wheel) - dep5; variant_label != "null" - # satisfied by the non-variant wheel - dep6; variant_label == "" - -Implementations MUST ignore differences in whitespace while matching the -features and properties. - -Variant marker expressions MUST be evaluated against the variant -properties stored in the wheel being installed, not against the current -output of the provider plugins. If a non-variant wheel was selected or -built, all variant markers evaluate to ``False``. - - -ABI Dependency Variant Namespace (Optional) -------------------------------------------- - -This section describes an **OPTIONAL** extension to the wheel variant -specification. Tools that choose to implement this feature MUST follow -this specification. Tools that do not implement this feature MUST treat -the variants using it as incompatible, and SHOULD inform users when such -wheels are skipped. - -The variant namespace ``abi_dependency`` is reserved for expressing that -different builds of the same version of a package are compatible with -different versions or version ranges of a dependency. This namespace -MUST NOT be used by any variant provider plugin, it MUST NOT be listed -in ``providers`` metadata, and can only appear in a built wheel variant -property. - -Within this namespace, zero or more properties can be used to express -compatible dependency versions. For each property, the feature name MUST -be the :ref:`normalized name ` of the -dependency, whereas the value MUST be a valid release segment of -a public version identifier, as defined by the -:doc:`packaging:specifications/version-specifiers` specification. -It MUST contain up to three version components, that are matched against -the installed version same as the ``=={value}.*`` specifier. Notably, -trailing zeroes match versions with fewer components (e.g. ``2.0`` -matches release ``2`` but not ``2.1``). This also implies that the -property values have different semantics than PEP 440 versions, in -particular ``2``, ``2.0`` and ``2.0.0`` represent different ranges. - -Versions with nonzero epoch are not supported. - -==================================== ================== -Variant Property Matching Rule -==================================== ================== -``abi_dependency :: torch :: 2`` ``torch==2.*`` -``abi_dependency :: torch :: 2.9`` ``torch==2.9.*`` -``abi_dependency :: torch :: 2.8.0`` ``torch==2.8.0.*`` -==================================== ================== - -Multiple variant properties with the same feature name can be used to -indicate wheels compatible with multiple providing package versions, -e.g.: - -.. code:: text +A secondary use case for variant wheels is providing an explicit user +choice between multiple variants that are always supported. Examples +include numerical packages built against different BLAS / LAPACK +implementations, OpenMP implementations, etc. The key difference is that +every variant wheel that was built for a given platform is always +compatible with it, therefore there is no need for querying a provider +at install time. The design proposes using providers with static +properties for that purpose. - abi_dependency :: torch :: 2.8.0 - abi_dependency :: torch :: 2.9.0 +For example, a package supporting multiple BLAS / LAPACK variants could +declare them in the following way in its ``pyproject.toml``: -This means the wheel is compatible with both PyTorch 2.8.0 and 2.9.0. +.. code-block:: toml + [variant] + "$schema" = "..." -How to teach this -================= + [variant.default-priorities] + namespace = ["blas_lapack"] -Python package users --------------------- + [variant.providers.blas_lapack.static-properties] + # in preference order + library = ["openblas", "mkl", "netlib"] -The primary source of information for Python package users should be -installer documentation, supplemented by helpful informational messages -from command-line interface, and tutorials. Users without special needs -should not require any special variant awareness. Advanced users would -specifically need documentation on (provided the installer in question -implements these features): + [variant.variants.openblas] + blas_lapack.library = ["openblas"] -- enabling untrusted provider plugins and the security implications of - that + [variant.variants.mkl] + blas_lapack.library = ["mkl"] -- controlling provider usage, in particular enabling optional providers, - disabling undesirable plugins or disabling variant usage in general + [variant.variants.netlib] + blas_lapack.library = ["netlib"] -- explicitly selecting variants, as well as controlling variant - selection process +In this case there is no provider plugin defined, and properties are +declared statically in the file instead. They are copied into the built +wheel. The installer considers all three ``blas_lapack :: library`` +values supported, with ``openblas`` being most preferred and ``netlib`` +being least preferred. -- configuring variant selection for remote deployment targets, for - example using a static file generated on the target +For example, if wheels for a given platform feature all three variants, +``openblas`` will be selected by default, and the user will be able to +explicitly override the choice to use another variant. If there is no +``openblas`` variant, ``mkl`` will be chosen instead. ``netlib`` will +only be used by default if both other variants are missing. -The installer documentation may also be supplemented by documentation -specific to Python projects, in particular their installation -instructions. +Alternatively, a plugin can be used to provide a static list of values +that is consistent across different packages: -For the transition period, during which some package managers do and -some do not support variant wheels, users need to be aware that certain -features may only be available with certain tools. +.. code-block:: toml + [variant] + "$schema" = "..." -Python package maintainers --------------------------- + [variant.default-priorities] + namespace = ["blas_lapack"] -The primary source of information for maintainers of Python packages -should be build backend documentation, supplemented by tutorials. The -documentation needs to indicate: + [variant.providers.blas_lapack] + build-requires = ["blas-lapack-provider-plugin"] + + [variant.variants.openblas] + blas_lapack.library = ["openblas"] + + [variant.variants.mkl] + blas_lapack.library = ["mkl"] + + [variant.variants.netlib] + blas_lapack.library = ["netlib"] + +This will cause the build backend to query +``blas-lapack-provider-plugin`` for all valid features and their values. +The obtained list will be embedded as ``static-properties`` in the built +wheel, so the installer does not have to query the plugin anymore. + + +Depending on a variant-enabled package +-------------------------------------- + +A project cannot declare a dependency on a particular variant of another +project. There is no way to write ``dependencies = ["torch+cu126"]`` or +anything equivalent; a dependency names a project and a version range, +as it does today. Making ``dependencies = ["torch"]`` reliable is the +goal of this design, rather than allowing build choices to be fixed +across project boundaries. + +Three things argue against permitting it. The first is that naming a +variant couples one project's metadata to another project's build +choices, and those shift over time. A project depending on +``torch+cu124`` would not fail once ``torch`` moved on to ``cu126``; it +would quietly hold its users on the last release that still shipped +``cu124``, where a dependency on ``torch`` would have kept selecting the +highest version. The second is that it would draw variants into the +resolver's search, with the consequences described in `Why variants stay +out of the resolver's search`_. The third is that installers assume +every wheel of a given project version declares the same dependency +metadata, with any differences expressed through environment markers +rather than through separate metadata per wheel; variant-specific +dependencies would break that assumption, and locking across platforms +rests on it. Such a specifier would also offer a way to install a +variant wheel on a system whose user has enabled no providers at all. + +What holds a set of packages together instead is the providers they +share. Where a project and its dependency offer variants governed by the +same provider, the installer makes each selection independently and +arrives at matching values, because the provider reports the same +ordered properties in both cases. + +Not every axis needs to match. A project built without a particular CPU +instruction set runs perfectly well alongside a dependency built with +one; what matters are the axes where combinations are genuinely +incompatible. For those, the recommendation is that a project should not +publish variant builds that fail to work with one of the variants of its +dependency. Exceptions should be rare, and worth stating plainly in the +project's documentation. + +Matching the ABI of a dependency is a related but distinct problem, and +one the design does address; see `Package ABI matching`_ below. -- how to declare variant support in ``pyproject.toml`` -- how to use variant environment markers to specify dependencies +Package ABI matching +-------------------- -- how to build variant wheels +Yet another use case for variant wheels is providing multiple variants +of a package built against different dependency versions, in order to +resolve binary compatibility issues. -- how to publish them and generate the ``*-variants.json`` file on local - indexes +For example, vLLM needs to be installed with the same PyTorch version +that it was built against. While this may not be a major issue for +isolated setups, a more complex environment may end up depending on +multiple packages requiring PyTorch, each of them requiring a specific +PyTorch version. This may require navigating through a complex maze of +interdependencies to find package versions that can all be installed +simultaneously. -The maintainers will also need to peruse provider plugin documentation. -They should also be aware which provider plugins are considered trusted -by commonly used installers, and know the implications of using -untrusted plugins. These materials may also be supplemented by generic -documents explaining publishing variant wheels, along with specific -example use cases. +The design introduces a special ``abi_dependency`` provider that +provides variants per dependency version. For example, vLLM could +publish the following variants: + +- ``vllm-0.27.1-...-torch213.whl`` for PyTorch 2.13.x +- ``vllm-0.27.1-...-torch212.whl`` for PyTorch 2.12.x +- ``vllm-0.27.1-...-torch211.whl`` for PyTorch 2.11.x + +The installer would then select whichever of these matches the PyTorch +version it resolves to. If only ``vllm`` is requested, that is +``torch213``, since nothing constrains PyTorch and the newest version is +chosen. Where something else in the resolution holds PyTorch at an older +version, ``torch212`` or ``torch211`` is selected instead, rather than +vLLM having to be downgraded to a release built against that version. + +``abi_dependency`` is meant to be the only piece of metadata introduced +by the specification that affects dependency resolution rather than only +wheel selection within a version. Choosing among variants in order to +satisfy constraints elsewhere in the dependency graph is ruled out for +variants in general (see `Why variants stay out of the resolver's +search`_), because doing so may be difficult to implement. Support for +``abi_dependency`` is therefore optional for installers: one that does +not implement it treats the variants using it as incompatible, and is +expected to tell the user that those wheels were skipped. + + +Key points +---------- + +The key points in the design we arrived at are: + +1. Package compatibility characteristics are expressed as variant + properties and embedded inside the wheel file, rather than in the + filename. This means that any number of characteristics can be + expressed without increasing the filename length. +2. Variant properties are namespaced and governed by independent + providers. New compatibility axes can be created and the existing + providers can be updated as necessary by the interested parties + without the necessity of going through a PEP process. +3. Providers are ultimately selected by the packages needing them, and + so variant-enabled packages have the final say over which variant + wheels are available and how they are selected. +4. The design provides both for providers that need to determine system + compatibility at runtime, and providers that embed all the necessary + information statically in the wheel itself. It also permits defining + special cases, such as the ``abi_dependency`` provider, that solve + specific problems without adding significant complexity to the + specification. +5. The reference method of implementing providers is through installable + Python packages, which provides the flexibility needed for package + maintainers to extend variant wheel support as necessary, as well as + to facilitate convenient testing. However, we do realize that we + cannot allow arbitrary code execution at install time, so these + plugins are opt-in. +6. At the same time, we recognize the need for good user experience + and the risk of security fatigue from an opt-in design. For this + reason, we propose that commonly used providers are vendored, + reimplemented or otherwise vetted as opt-in in installers, bridging + the gap between security and usability. + + +Security Implications +===================== -For the transition period, package maintainers need to be aware that -they should still publish non-variant wheels for backwards -compatibility. +The full security considerations will be provided in the individual PEPs +listed in `Scope and Standards Track PEPs`_. The most significant are +summarized here. + +The most important concern is that wheel variants introduce a plugin +system for querying the platform capabilities. Tools may install these +packages and execute the code within them during dependency resolution +or wheel processing, risking Remote Code Execution vulnerabilities. In +some cases the affected tools are executed with elevated privileges +(such as when installing packages for multi-user systems). + +To prevent this, the specification will make plugin packages opt-in. +However, to improve user experience and avoid the risk of security +fatigue causing users to blanket enable all plugins, tool maintainers +will be allowed to vendor, reimplement or otherwise trust specific +plugins. A governance model to maintain a repository of trustworthy +plugins will be proposed as well. + +A provider is expected to run in an environment isolated from the one +being modified, so that it does not gain access to the environment whose +contents it is helping to select, and so that the packages it requires +do not enter that environment. This is expressed as a recommendation +rather than a requirement because the environment that is appropriate +differs between tools: a build frontend, for instance, already has an +isolated environment of its own and may reasonably reuse it. + +A second concern is name squatting, which `Package names as variants`_ +describes as a hazard of the current workarounds: projects register many +similarly named packages, and users learn to expect suffixes that an +attacker can then supply. Variant wheels reduce that pressure rather +than repeat it, since every variant of a release lives under one project +name. Nor do variant namespaces amount to a new global namespace to be +claimed. A namespace has meaning only within the variant metadata of the +project that uses it, and that project also names the provider package +implementing it, so choosing a provider is an ordinary dependency +decision, open to the same scrutiny as any other. How a namespace is +bound to the provider that governs it is specified by the Providers PEP. + + +Backwards Compatibility +======================= +Full backwards compatibility considerations are provided in :pep:`825`. +The key point is that variant wheels add an additional variant label +component to the wheel filename, and that tools commonly used to install +wheels at the time of writing reject wheels carrying it. + +That rejection is not incidental. A variant wheel has one filename +component more than a non-variant one, so a tool unaware of variants +reads the Python tag in the position where it expects a build number; +build numbers start with a digit and Python tags do not, so the filename +fails verification. To keep that true in future, :pep:`825` requires that +the Python tag component of a wheel filename must not start with a +digit, which no current tag does. + +The guarantee reaches only as far as filename verification does. A +script that parses wheel filenames loosely, rather than verifying them +in full, may treat a variant wheel as an ordinary one. Conversely, +tooling that audits wheel filenames will report variant wheels as +invalid until it is updated to recognize them. + +What this buys is coexistence. Variant and non-variant wheels are meant +to be published to the same index, under the same project name and for +the same release; neither a separate index nor a separate project name +is needed, and avoiding both is much of the point of this proposal. An +installer that does not understand variants sees the non-variant wheel +and behaves exactly as it does today, while one that does understand +them sees the variant wheels as well and chooses between them. + +One thing can undermine that, and it is arguably the more consequential +compatibility question of the two. :pep:`825` introduces environment +markers that describe variant properties. A project does not have to use +them, but they are what allows a project whose variants differ in their +dependencies - a CUDA build and a CPU build, say - to keep one +dependency list across the entire release instead of diverging per +wheel, and projects such as PyTorch_ are expected to use them for that +reason. Those markers then appear in the metadata of every wheel in the +release, the non-variant wheel included. + +Unlike an unrecognized filename component, an unrecognized marker is not +uniformly survivable, because it changes the dependency specifier +grammar rather than adding a value to an existing field. What installers +do when they see an unrecognized marker varies. pip from 24.1 onwards, +uv and PDM treat the release as having invalid metadata and resolve to +an earlier one, which is inconvenient but safe; earlier versions of pip +fail outright; Poetry discards markers it cannot parse and proceeds, +which makes the dependencies they guard unconditional, so the +dependencies of every variant are installed together - usually more than +the user needs, and in rare cases things that conflict. + + +Adoption and rollout +==================== + +The introduction strategy belongs to the fourth PEP in the series, and +this section sketches only its shape. + +A project adopting variants adds to what it publishes on PyPI rather +than replacing it. Variant wheels go into a release that still carries a +non-variant wheel, so that variant-aware installers choose among the +variants while every other consumer continues to receive the wheel it +would have received anyway. Nothing that works today stops working, and +no user has to do anything. + +Dropping the non-variant wheel is a later and separate decision, and a +much slower one. It only becomes reasonable once variant-aware +installers have reached essentially all of a project's users, which is a +matter of years rather than releases, given how long older installers +stay in service in pinned CI images and long-term-support +distributions. Until then, a release published without a non-variant +wheel is simply uninstallable for part of its audience, and what an +installer does instead is not necessarily graceful: it may attempt a +build from source, which can fail with a hard to understand error. + +The workarounds can go sooner. A project that today distributes +per-accelerator builds through separate index URLs, or under separate +project names, can retire those well before it drops its non-variant +wheel, because reaching the users of a workaround is a much easier +problem: using one is an explicit step taken on the project's own +instructions, and the project can change the instructions. The plain +``{tool} install {package}`` path offers no comparable lever, which is +why the non-variant wheel is the part that has to linger. + +The ``null`` variant does not substitute for a non-variant wheel here. +It is a fallback for installers that understand variants but find no +better match; an installer that does not understand variants cannot +select it either, because it carries a variant label like any other +variant wheel. -Backwards compatibility -======================= -Existing installers MUST NOT accidentally install variant wheels, as -they require additional logic to determine whether a wheel is compatible -with the user's system. This is achieved by `extending wheel filename -<#extended-wheel-filename>`__ through adding a ``-{variant label}`` -component to the end of the filename, effectively causing variant wheels -to be rejected by common installer implementations. For backwards -compatibility, a non-variant wheel can be published in addition to the -variant wheels. It will be the only wheel supported by incompatible -installers, and the least preferred wheel for variant-compatible -installers. - -Aside from this explicit incompatibility, the specification makes -minimal and non-intrusive changes to the binary package format. The -`variant metadata`_ is placed in a separate file in the ``.dist-info`` -directory, which should be preserved by tools that are not concerned -with variants, limiting the necessary changes to updating the filename -validation algorithm (if there is one). - -If the new `variant environment markers`_ are used in wheel -dependencies, these wheels will be incompatible with existing tools. -This is a general problem with the design of environment markers, and -not specific to wheel variants. It is possible to work around this -problem by partially evaluating environment markers at build time, and -removing the markers or dependencies specific to variant wheels from the -non-variant wheel. - -`Build backends`_ produce non-variant wheels to preserve backwards -compatibility with existing frontends. Variant wheels can only be output -on explicit user request. - -By using a separate ``*-variants.json`` `file for shared metadata -<#name-version-variants-json-the-index-level-variant-metadata-file>`__, -it is -possible to use variant wheels on an index that does not specifically -support variant metadata. However, the index MUST permit distributing -wheels that use the extended filename syntax and the JSON file. - - -Reference implementation +Reference Implementation ======================== The `variantlib `__ project -contains a reference implementation of all the protocols and algorithms -introduced in this PEP, as well as a command-line tool to convert -wheels, generate the ``*-variants.json`` index and query plugins. +contains a reference implementation of the current state of wheel +variants specification, as well as a command-line tool to convert +wheels, generate the ``*-variants.json`` file and query plugins. A client for installing variant wheels is implemented in a `uv branch `__. @@ -2689,116 +1977,6 @@ modified versions of some Python packages demonstrating variant wheel uses. -Rejected ideas -============== - -Variants being entirely or transitionally opt-in ------------------------------------------------- - -In discussing the security concerns, proposals were made to make variant -provider usage entirely opt-in, either permanently, or at least -initially to facilitate further testing. While such approaches may -alter who takes responsibility of vetting the provider code, and hence -the maintenance effort or number of packages/maintainers that need to be -trusted, they are not suitable as long-term solutions. - -Most importantly, the opt-in mechanism would lead to far worse user -experience out-of-the-box. For variant-enabled packages, the default -experience would be installing a suboptimal or outright broken variant. -It should be noted that variant-enabled packages may not only be -installed directly, but also as dependencies of other packages. -Therefore, for optimal user experience, all packages that are -variant-enabled or that feature dependencies that are variant-enabled, -would have to document appropriate installer-specific mechanisms for -enabling the respective provider plugins. - -The proliferation of this experience could have two significant -outcomes. The inability to make variants work out of the box for users -could lead to package maintainers refraining from using them, and -instead sticking to the earlier workarounds. What's even worse, it could -also lead to users eventually naively configuring their installers -to enable all variant providers unconditionally, effectively rendering -the provider usage opt-out for a large number of users, enabling all -kinds of supply chain attacks described in the `security implications`_ -section. - -The authors would like to emphasize that security cannot be achieved at -the cost of severely impaired user experience. Instead, the PEP attempts -to strike a balance by introducing a centrally maintained and vetted -pool of trusted providers. - - -An approach without provider plugins ------------------------------------- - -The support for additional variant properties could technically be -implemented without introducing provider plugins, but rather defining -the available properties and their discovery methods as part of the -specification, much like how wheel tags are implemented currently. -However, the existing wheel tag logic already imposes a significant -complexity on packaging tools that need to maintain the logic for -generating supported tags, partially amortized by the data provided by -the Python interpreter itself. - -Every new axis would be imposing even more effort on package manager -maintainers, who would have to maintain an algorithm to determine the -property compatibility. This algorithm could become quite complex, -possibly needing to account for different platforms, hardware versions -and requiring more frequent updates than the one for platform tags. This -would also significantly increase the barrier towards adding new axes -and therefore the risk of lack of feature parity between different -installers, as every new axis will be imposing additional maintenance -cost. - -For comparison, the plugin design essentially democratizes the variant -properties. Provider plugins can be maintained independently by people -having the necessary knowledge and hardware. They can be updated as -frequently as necessary, independently of package managers. The decision -to use a particular provider falls entirely on the maintainer of package -needing it, though they need to take into consideration that using -plugins that are not vetted by the common installers will inconvenience -their users. - - -Resolving variants to separate packages ---------------------------------------- - -An alternative proposal was to publish the variants of the package as -separate projects on the index, along with the main package serving as a -"resolver" directing to other variants via its metadata. For example, a -``torch`` package could indicate the conditions for using ``torch-cpu``, -``torch-cu129``, etc. subpackages. - -Such an approach could possibly feature better backwards compatibility -with existing tools. The changes would be limited to installers, and -even with pre-variant installers the users could explicitly request -installing a specific variant. However, it poses problems at multiple -levels. - -The necessity of creating a new project for every variant will lead to -the proliferation of old projects, such as ``torch-cu123``. While the -use of resolver package will ensure that only the modern variants are -used, users manually installing packages and cross-package dependencies -may accidentally be pinning to old variant projects, or even fall victim -to name squatting. For comparison, the variant wheel proposal scopes -variants to each project version, and ensures that only the project -maintainers can upload them. - -Furthermore, it requires significant changes to the dependency resolver -and package metadata formats. In particular, the dependency resolver -would need to query all "resolver" packages before performing -resolution. It is unclear how to account for such variants while -performing universal resolution. The one-to-one mapping between -dependencies and installed packages would be lost, as a ``torch`` -dependency could effectively be satisfied by ``torch-cu129``. - - -Appendices -========== - -- :ref:`0817-variant-json-schema` - - References ========== @@ -2829,9 +2007,29 @@ Paul Ganssle, Philip Hyunsu Cho, Robert Maynard, Vyas Ramasubramani, and Zanie Blue. -Change history +Change History ============== +- 21-Sep-2026 + + - Change this PEP to an Informational PEP, as an umbrella for + the Standards Track PEPs split off from the first version of + this PEP. + - Added a "The design space" section between the prior art and the + design itself, covering the approaches considered for determining + which variant fits a system, the reasons for choosing variant + providers together with a user-declared environment, and the open + question of how much provider code should run without the user + asking for it. + - Stated that variant selection chooses among the wheels of an + already-selected version rather than taking part in the resolver's + search, and that support for ``abi_dependency``, which is the + exception to that, is optional for installers. + - Clarified that the sequence requires installers to accept a static + description of an environment's variant properties, while leaving + the format of that description open. + + - 18-Mar-2026 - Added high-level outlines of suggested implementation logic per type diff --git a/peps/pep-0817/appendix-variant-metadata-json-schema.rst b/peps/pep-0817/appendix-variant-metadata-json-schema.rst deleted file mode 100644 index 27f3871b867..00000000000 --- a/peps/pep-0817/appendix-variant-metadata-json-schema.rst +++ /dev/null @@ -1,11 +0,0 @@ -:orphan: - -.. _0817-variant-json-schema: - -Appendix: JSON Schema for Variant Metadata -========================================== - -.. literalinclude:: variant_schema.json - :language: json - :linenos: - :name: variant-json-schema diff --git a/peps/pep-0817/conceptual_diagram_installers.png b/peps/pep-0817/conceptual_diagram_installers.png index b6b364eb5f9..357cb9e9f2f 100644 Binary files a/peps/pep-0817/conceptual_diagram_installers.png and b/peps/pep-0817/conceptual_diagram_installers.png differ diff --git a/peps/pep-0817/variant_schema.json b/peps/pep-0817/variant_schema.json deleted file mode 100644 index 010fabaeb96..00000000000 --- a/peps/pep-0817/variant_schema.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://wheelnext.dev/variants.json", - "title": "{name}-{version}-variants.json", - "description": "Combined index metadata for wheel variants", - "type": "object", - "properties": { - "default-priorities": { - "description": "Default provider priorities", - "type": "object", - "properties": { - "namespace": { - "description": "Default namespace priorities", - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z0-9_]+$" - }, - "minItems": 1, - "uniqueItems": true - }, - "feature": { - "description": "Default feature priorities (by namespace)", - "type": "object", - "patternProperties": { - "^[a-z0-9_]+$": { - "description": "Preferred features", - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z0-9_]+$" - }, - "minItems": 0, - "uniqueItems": true - } - }, - "additionalProperties": false, - "uniqueItems": true - }, - "property": { - "description": "Default property priorities (by namespace)", - "type": "object", - "patternProperties": { - "^[a-z0-9_]+$": { - "description": "Default property priorities (by feature) name", - "type": "object", - "patternProperties": { - "^[a-z0-9_]+$": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z0-9_.]+$" - }, - "minItems": 0, - "uniqueItems": true - } - }, - "additionalProperties": false, - "uniqueItems": true - } - }, - "additionalProperties": false, - "uniqueItems": true - } - }, - "additionalProperties": false, - "uniqueItems": true, - "required": [ - "namespace" - ] - }, - "providers": { - "description": "Mapping of namespaces to provider information", - "type": "object", - "patternProperties": { - "^[A-Za-z0-9_]+$": { - "type": "object", - "description": "Provider information", - "properties": { - "plugin-api": { - "description": "Object reference to plugin class", - "type": "string", - "pattern": "^([a-zA-Z0-9._]+ *: *[a-zA-Z0-9._]+)|([a-zA-Z0-9._]+)$" - }, - "enable-if": { - "description": "Environment marker specifying when to enable the plugin", - "type": "string", - "minLength": 1 - }, - "optional": { - "description": "Whether the provider is optional", - "type": "boolean" - }, - "plugin-use": { - "description": "Whether a plugin is used: not at all, at build time or both at build and install time", - "type": "string", - "enum": [ - "none", - "build", - "all" - ] - }, - "requires": { - "description": "Dependency specifiers for how to install the plugin", - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "minItems": 0, - "uniqueItems": true - } - }, - "additionalProperties": false, - "uniqueItems": true, - "required": [ - "requires" - ] - } - }, - "additionalProperties": false, - "uniqueItems": true - }, - "variants": { - "description": "Mapping of variant labels to properties", - "type": "object", - "patternProperties": { - "^[a-z0-9_.]{1,16}$": { - "type": "object", - "description": "Mapping of namespaces in a variant", - "patternProperties": { - "^[a-z0-9_.]+$": { - "patternProperties": { - "^[a-z0-9_.]+$": { - "description": "list of possible values for this variant feature.", - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z0-9_.]+$" - }, - "minItems": 1, - "uniqueItems": true - } - }, - "uniqueItems": true, - "additionalProperties": false - } - }, - "uniqueItems": true, - "additionalProperties": false - } - }, - "additionalProperties": false, - "uniqueItems": true - } - }, - "required": [ - "default-priorities", - "providers", - "variants" - ], - "uniqueItems": true -}