|
| 1 | +# Unique wheel file names with a `wheel_build_tag` hook |
| 2 | + |
| 3 | +- Author: Christian Heimes |
| 4 | +- Created: 2026-04-16 |
| 5 | +- Status: Open |
| 6 | +- GitHub issue: [#1059](https://github.com/python-wheel-build/fromager/issues/1059) |
| 7 | + |
| 8 | +## What |
| 9 | + |
| 10 | +This enhancement proposes a new configurable hook, `build_tag_hook`, for |
| 11 | +injecting custom suffixes into the |
| 12 | +[wheel build tag](https://packaging.python.org/en/latest/specifications/binary-distribution-format/), |
| 13 | +producing unique wheel file names that encode platform, accelerator stack, |
| 14 | +and dependency ABI information. |
| 15 | + |
| 16 | +## Why |
| 17 | + |
| 18 | +Fromager-built wheels are platform-specific and may depend on: |
| 19 | + |
| 20 | +- **OS / distribution version**, e.g. Fedora 43, RHEL 9.6, RHEL 10.1 |
| 21 | +- **AI accelerator stack**, e.g. CUDA 13.1 vs CUDA 12.9, ROCm 7.1 |
| 22 | +- **Torch ABI**, which is unstable across versions; a wheel compiled for |
| 23 | + Torch 2.10.0 may have a different ABI than one compiled for Torch 2.11.0 |
| 24 | + |
| 25 | +Currently, wheel filenames carry none of this information, making it |
| 26 | +difficult to invalidate caches, distinguish builds for different stacks, or |
| 27 | +replace outdated wheels with correctly-targeted rebuilds. |
| 28 | + |
| 29 | +## Goals |
| 30 | + |
| 31 | +- introduce a `build_tag_hook` option in the `wheels` section of the global |
| 32 | + settings file |
| 33 | +- allow the hook to contribute ordered suffix segments to the wheel build tag |
| 34 | +- produce unique, deterministic wheel file names that reflect the build |
| 35 | + environment |
| 36 | + |
| 37 | +## Non-goals |
| 38 | + |
| 39 | +- Filtering or selecting wheels by build tag at install time. `pip install` |
| 40 | + and `uv pip install` only use the build tag for sorting, not for filtering. |
| 41 | +- Sharing wheels across indexes. While unique file names enable this in |
| 42 | + principle, the mechanics of cross-index sharing are out of scope. |
| 43 | +- Accessing wheel content, the build environment, or ELF dependency info from |
| 44 | + within the hook. The hook must work identically whether a wheel is freshly |
| 45 | + built or retrieved from cache. |
| 46 | +- Validation of annotations such as "depends on libtorch". A validation |
| 47 | + system for `build_wheel` may be added in the future. |
| 48 | +- Package override hook. It would complicate the design and there is no |
| 49 | + compelling use-case for package-specific tags. |
| 50 | + |
| 51 | +## How |
| 52 | + |
| 53 | +### Wheel spec background |
| 54 | + |
| 55 | +The wheel filename format is: |
| 56 | + |
| 57 | +``` |
| 58 | +{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl |
| 59 | +``` |
| 60 | + |
| 61 | +The [build tag](https://packaging.python.org/en/latest/specifications/binary-distribution-format/) |
| 62 | +is optional, must start with a digit, and must not contain `-`. Fromager |
| 63 | +already fills the numeric part from the variant + package changelog and sets |
| 64 | +the string suffix to `""`. This proposal extends that suffix with |
| 65 | +hook-provided segments (e.g. `_el9.6_rocm7.1_torch2.10.0`). |
| 66 | + |
| 67 | +### Configuration |
| 68 | + |
| 69 | +The hook is configured in the global settings file under a new `wheels` |
| 70 | +section. The callable is specified as a dotted import path using Pydantic's |
| 71 | +`ImportString` type for validation and loading. |
| 72 | + |
| 73 | +```yaml |
| 74 | +wheels: |
| 75 | + build_tag_hook: "mypackage.hooks:build_tag_hook" |
| 76 | +``` |
| 77 | +
|
| 78 | +When `build_tag_hook` is not set, no suffix is appended to the build tag. |
| 79 | + |
| 80 | +### Hook signature |
| 81 | + |
| 82 | +```python |
| 83 | +def build_tag_hook( |
| 84 | + *, |
| 85 | + ctx: context.WorkContext, |
| 86 | + req: Requirement, |
| 87 | + version: Version, |
| 88 | + wheel_tags: frozenset[Tag], |
| 89 | +) -> typing.Sequence[str]: |
| 90 | + ... |
| 91 | +``` |
| 92 | + |
| 93 | +The hook returns `typing.Sequence[str]`, a sequence of suffix segments |
| 94 | +(e.g. `["el9.6", "rocm7.1", "torch2.10.0"]`). The segments are joined |
| 95 | +with `_` and appended to the existing build tag. |
| 96 | + |
| 97 | +Each segment must only contain alphanumeric ASCII characters or dot |
| 98 | +(`[a-zA-Z0-9.]`). When the hook returns any other character or raises an |
| 99 | +exception, the build fails. |
| 100 | + |
| 101 | +### Example hook |
| 102 | + |
| 103 | +```{literalinclude} wheel_tag_example_hook.py |
| 104 | +:start-at: example_hook |
| 105 | +``` |
| 106 | + |
| 107 | +### Hook scope |
| 108 | + |
| 109 | +The hook can access `ctx` (variant, package settings, annotations), |
| 110 | +`wheel_tags` (to distinguish purelib vs platlib), and standard library |
| 111 | +APIs like `os.environ` and `platform.freedesktop_os_release()`. |
| 112 | + |
| 113 | +The hook **cannot** access wheel content, the build environment, or ELF |
| 114 | +dependency info. These are unavailable when wheels come from cache, and the |
| 115 | +hook must produce the same result regardless of source. |
| 116 | + |
| 117 | +### Examples |
| 118 | + |
| 119 | +| Wheel | Build tag | OS | Stack | |
| 120 | +| -- | -- | -- | -- | |
| 121 | +| `flash_attn-2.8.3-8_el9.6_rocm7.1_torch2.10.0-cp312-cp312-linux_x86_64.whl` | `8_el9.6_rocm7.1_torch2.10.0` | RHEL 9.6 | ROCm | |
| 122 | +| `torch-2.10.0-7_el9.6_rocm7.1-cp312-cp312-linux_x86_64.whl` | `7_el9.6_rocm7.1` | RHEL 9.6 | ROCm | |
| 123 | +| `torch-2.9.1-8_fc43_cuda13.0-cp312-cp312-linux_x86_64.whl` | `8_fc43_cuda13.0` | Fedora 43 | CUDA | |
| 124 | +| `pillow-12.2.0-2_el9.6-cp312-cp312-linux_x86_64.whl` | `2_el9.6` | RHEL 9.6 | any | |
| 125 | +| `fromager-0.79.0-2-py3-none-any.whl` | `2` (no suffix) | any | any | |
| 126 | + |
| 127 | +Pure-python wheels (`py3-none-any`) receive no suffix, while platlib wheels |
| 128 | +get progressively more specific tags based on their dependencies. |
| 129 | + |
| 130 | +## Limitations |
| 131 | + |
| 132 | +A single index still cannot contain both CUDA and ROCm builds of the same |
| 133 | +package. `pip` and `uv` only use the build tag for sorting, not filtering. |
| 134 | +The upcoming [Wheel.Next](https://wheelnext.dev/) initiative |
| 135 | +([PEP 817](https://peps.python.org/pep-0817/) / |
| 136 | +[PEP 825](https://peps.python.org/pep-0825/)) aims to address this with |
| 137 | +wheel variants. Hook logic for accelerator selection may be reusable when |
| 138 | +that standard lands. |
0 commit comments