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_hook option in the wheels section of the global settings file

  • allow 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 install and uv pip install only 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_wheel may 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

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

torch-2.10.0-7_el9.6_rocm7.1-cp312-cp312-linux_x86_64.whl

7_el9.6_rocm7.1

RHEL 9.6

ROCm

torch-2.9.1-8_fc43_cuda13.0-cp312-cp312-linux_x86_64.whl

8_fc43_cuda13.0

Fedora 43

CUDA

pillow-12.2.0-2_el9.6-cp312-cp312-linux_x86_64.whl

2_el9.6

RHEL 9.6

any

fromager-0.79.0-2-py3-none-any.whl

2 (no suffix)

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.