Unique wheel file names with a wheel_build_tag hook
Author: Christian Heimes
Created: 2026-04-16
Status: Open
GitHub issue: #1059
What
This enhancement proposes a new configurable hook, build_tag_hook, for
injecting custom suffixes into the
wheel build tag,
producing unique wheel file names that encode platform, accelerator stack,
and dependency ABI information.
Why
Fromager-built wheels are platform-specific and may depend on:
OS / distribution version, e.g. Fedora 43, RHEL 9.6, RHEL 10.1
AI accelerator stack, e.g. CUDA 13.1 vs CUDA 12.9, ROCm 7.1
Torch ABI, which is unstable across versions; a wheel compiled for Torch 2.10.0 may have a different ABI than one compiled for Torch 2.11.0
Currently, wheel filenames carry none of this information, making it difficult to invalidate caches, distinguish builds for different stacks, or replace outdated wheels with correctly-targeted rebuilds.
Goals
introduce a
build_tag_hookoption in thewheelssection of the global settings fileallow the hook to contribute ordered suffix segments to the wheel build tag
produce unique, deterministic wheel file names that reflect the build environment
Non-goals
Filtering or selecting wheels by build tag at install time.
pip installanduv pip installonly use the build tag for sorting, not for filtering.Sharing wheels across indexes. While unique file names enable this in principle, the mechanics of cross-index sharing are out of scope.
Accessing wheel content, the build environment, or ELF dependency info from within the hook. The hook must work identically whether a wheel is freshly built or retrieved from cache.
Validation of annotations such as “depends on libtorch”. A validation system for
build_wheelmay be added in the future.Package override hook. It would complicate the design and there is no compelling use-case for package-specific tags.
How
Wheel spec background
The wheel filename format is:
{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
The build tag
is optional, must start with a digit, and must not contain -. Fromager
already fills the numeric part from the variant + package changelog and sets
the string suffix to "". This proposal extends that suffix with
hook-provided segments (e.g. _el9.6_rocm7.1_torch2.10.0).
Configuration
The hook is configured in the global settings file under a new wheels
section. The callable is specified as a dotted import path using Pydantic’s
ImportString type for validation and loading.
wheels:
build_tag_hook: "mypackage.hooks:build_tag_hook"
When build_tag_hook is not set, no suffix is appended to the build tag.
Hook signature
def build_tag_hook(
*,
ctx: context.WorkContext,
req: Requirement,
version: Version,
wheel_tags: frozenset[Tag],
) -> typing.Sequence[str]:
...
The hook returns typing.Sequence[str], a sequence of suffix segments
(e.g. ["el9.6", "rocm7.1", "torch2.10.0"]). The segments are joined
with _ and appended to the existing build tag.
Each segment must only contain alphanumeric ASCII characters or dot
([a-zA-Z0-9.]). When the hook returns any other character or raises an
exception, the build fails.
Example hook
def example_hook(
*,
ctx: context.WorkContext,
req: Requirement,
version: Version,
wheel_tags: frozenset[Tag],
) -> typing.Sequence[str]:
result: list[str] = []
platlib = any(tag.platform != "any" for tag in wheel_tags)
if platlib:
# fc43, el9.6, ...
result.append(get_distro_tag())
pbi = ctx.package_build_info(req)
# example how to use annotations and ctx.variant for custom flags
if pbi.annotations.get("example.accelerator-specific") == "true":
# cpu, cuda13.0, ...
if ctx.variant.startswith("cpu"):
result.append("cpu")
elif ctx.variant.startswith("cuda"):
cv = Version(os.environ["CUDA_VERSION"])
result.append(f"cuda{cv.major}.{cv.minor}")
else:
raise NotImplementedError(ctx.variant)
return result
@functools.cache
def get_distro_tag() -> str:
info = platform.freedesktop_os_release()
ids = [info["ID"]] # always defined
if "ID_LIKE" in info: # ids in precedence order
ids.extend(info["ID_LIKE"].split())
version_id = info.get("VERSION_ID", "")
for ident in ids:
if ident == "rhel": # RHEL and CentOS
return f"el{version_id}"
elif ident == "fedora":
return f"fc{version_id}"
# other distros
return f"{ids[0]}{version_id}".replace("_", "").replace("-", "")
Hook scope
The hook can access ctx (variant, package settings, annotations),
wheel_tags (to distinguish purelib vs platlib), and standard library
APIs like os.environ and platform.freedesktop_os_release().
The hook cannot access wheel content, the build environment, or ELF dependency info. These are unavailable when wheels come from cache, and the hook must produce the same result regardless of source.
Examples
Wheel |
Build tag |
OS |
Stack |
|---|---|---|---|
|
|
RHEL 9.6 |
ROCm |
|
|
RHEL 9.6 |
ROCm |
|
|
Fedora 43 |
CUDA |
|
|
RHEL 9.6 |
any |
|
|
any |
any |
Pure-python wheels (py3-none-any) receive no suffix, while platlib wheels
get progressively more specific tags based on their dependencies.
Limitations
A single index still cannot contain both CUDA and ROCm builds of the same
package. pip and uv only use the build tag for sorting, not filtering.
The upcoming Wheel.Next initiative
(PEP 817 /
PEP 825) aims to address this with
wheel variants. Hook logic for accelerator selection may be reusable when
that standard lands.