Skip to content

Commit 3119c93

Browse files
Merge pull request #1066 from tiran/proposal-wheel-build-tag-hook
[proposal] wheel_build_tag hook for unique wheel file names
2 parents fa05ad2 + 9de0f8b commit 3119c93

3 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import functools
2+
import os
3+
import platform
4+
import typing
5+
6+
from packaging.requirements import Requirement
7+
from packaging.tags import Tag
8+
from packaging.version import Version
9+
10+
from fromager import context
11+
12+
13+
def example_hook(
14+
*,
15+
ctx: context.WorkContext,
16+
req: Requirement,
17+
version: Version,
18+
wheel_tags: frozenset[Tag],
19+
) -> typing.Sequence[str]:
20+
result: list[str] = []
21+
platlib = any(tag.platform != "any" for tag in wheel_tags)
22+
if platlib:
23+
# fc43, el9.6, ...
24+
result.append(get_distro_tag())
25+
pbi = ctx.package_build_info(req)
26+
27+
# example how to use annotations and ctx.variant for custom flags
28+
if pbi.annotations.get("example.accelerator-specific") == "true":
29+
# cpu, cuda13.0, ...
30+
if ctx.variant.startswith("cpu"):
31+
result.append("cpu")
32+
elif ctx.variant.startswith("cuda"):
33+
cv = Version(os.environ["CUDA_VERSION"])
34+
result.append(f"cuda{cv.major}.{cv.minor}")
35+
else:
36+
raise NotImplementedError(ctx.variant)
37+
return result
38+
39+
40+
@functools.cache
41+
def get_distro_tag() -> str:
42+
info = platform.freedesktop_os_release()
43+
ids = [info["ID"]] # always defined
44+
if "ID_LIKE" in info: # ids in precedence order
45+
ids.extend(info["ID_LIKE"].split())
46+
version_id = info.get("VERSION_ID", "")
47+
for ident in ids:
48+
if ident == "rhel": # RHEL and CentOS
49+
return f"el{version_id}"
50+
elif ident == "fedora":
51+
return f"fc{version_id}"
52+
# other distros
53+
return f"{ids[0]}{version_id}".replace("_", "").replace("-", "")

docs/spelling_wordlist.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ json
3636
keyring
3737
lexicographically
3838
libcurl
39+
libtorch
3940
linter
4041
linters
4142
localhost
@@ -67,6 +68,7 @@ setuptools
6768
statelessly
6869
stderr
6970
stdin
71+
stdlib
7072
stdout
7173
subcommand
7274
subcommands

0 commit comments

Comments
 (0)