repomatic.release.binary module

Binary build targets and verification utilities.

Defines the Nuitka compilation targets for all supported platforms and provides native binary verification: architecture and minimum-OS floors are parsed straight from the executables’ ELF, Mach-O and PE headers, so no external tool is needed on runners or inside build containers.

repomatic.release.binary.BINARY_ASSET_SUFFIXES = ('.bin', '.exe')

File extensions identifying compiled binaries among release assets.

The one definition of “a compiled release asset”: scan-virustotal uploads this set, the release workflow downloads it (--pattern flags in _release-engine.yaml), docs/binaries.md lists it, and the dev-release asset globs derive from it.

repomatic.release.binary.PYTHON_DIST_SUFFIXES = ('.tar.gz', '.whl')

File extensions identifying Python distributions among release assets.

The counterpart of BINARY_ASSET_SUFFIXES, and the same kind of single definition: pack_binary_assets() excludes this set from the binary upload list (create-release already attached those), and the dev-release asset globs add it on top of the compiled binaries.

repomatic.release.binary.MACHO_MAGIC_64: Final[int] = 4277009103

Magic of a 64-bit Mach-O header, in the file’s own (little) endianness.

repomatic.release.binary.MACHO_FAT_MAGICS: Final[frozenset[int]] = frozenset({3405691582, 3405691583})

Big-endian magics of universal (fat) Mach-O containers, 32- and 64-bit.

class repomatic.release.binary.BinaryFormat(label, extension, floor_label)[source]

Bases: Enum

Executable container format a compiled binary uses.

Each member owns what varies by format: the name messages print, the file extension its executables take, what a target’s floor measures, and the header parser read_info() dispatches to. The floor label doubles as the enforceability flag, since a format recording no measurable floor has nothing to label.

Note

Members carry no magic bytes of their own: detect() needs to read them in a fixed order (a fat Mach-O and a PE both fail an equality test on the first four bytes), so the probe stays one method rather than a table.

ELF = ('elf', 'bin', 'glibc')
MACHO = ('macho', 'bin', 'macOS')
PE = ('pe', 'exe', None)
label

Lowercase format name, as printed in verification messages.

extension

File extension executables of this format take.

floor_label

What a target’s floor measures for this format, or None.

None marks a format whose headers record nothing a scan can check, so BuildTarget.enforced_floor reports no floor to enforce. Only PE is in that case: its version headers are nominal, and the Windows floor is CPython’s own support policy, not a linker artifact.

read_info(path)[source]

Parse a binary’s headers into its machine set and measured floor.

The machine element type follows the format (ELF machine names, Mach-O CPU type integers, the PE machine id or None when unparsable), matching what ARCH_MACHINES records as the expectation. The floor is the measured requirement verify_binary_floor() compares against the declared one: always None on PE, whose headers record nothing enforceable.

Parameters:

path (Path) – Path to the binary file.

Return type:

tuple[frozenset[object], str | None]

Returns:

(machines, floor) as parsed from the headers.

classmethod detect(path)[source]

Identify a file’s executable format from its magic bytes.

Parameters:

path (Path) – Path to the file to probe.

Return type:

BinaryFormat | None

Returns:

The matching format, or None for anything unrecognized.

repomatic.release.binary.PLATFORM_FORMATS: Final[dict[Platform | Group, BinaryFormat]] = {Group(id='linux', name='Linux distributions'): BinaryFormat.ELF, Platform(id='macos', name='macOS'): BinaryFormat.MACHO, Platform(id='windows', name='Windows'): BinaryFormat.PE}

Executable format each build platform compiles to.

Read through BuildTarget.binary_format, which is how every caller reaches it.

repomatic.release.binary.MACHINE_IDS: Final[dict[tuple[BinaryFormat, Architecture], str | int]] = {(BinaryFormat.ELF, Architecture(id='aarch64', name='ARM64 (AArch64)')): 'EM_AARCH64', (BinaryFormat.ELF, Architecture(id='x86_64', name='x86-64 (AMD64)')): 'EM_X86_64', (BinaryFormat.MACHO, Architecture(id='aarch64', name='ARM64 (AArch64)')): 16777228, (BinaryFormat.MACHO, Architecture(id='x86_64', name='x86-64 (AMD64)')): 16777223, (BinaryFormat.PE, Architecture(id='aarch64', name='ARM64 (AArch64)')): 43620, (BinaryFormat.PE, Architecture(id='x86_64', name='x86-64 (AMD64)')): 34404}

Machine identifier a binary’s header carries, per format and architecture.

One table rather than three, keyed the way PlatformKey keys the tool registry: the value is a pyelftools machine name on ELF and a raw header integer on Mach-O and PE, so a caller compares it against whatever the matching parser returns.

class repomatic.release.binary.BuildTarget(id, runner, platform, arch, floor, container=None)[source]

Bases: object

One Nuitka compile target: a runner image, a platform and an architecture.

Carries the metadata every consumer branches on, and the methods that interpret it, so a caller asks the target what its binaries must look like instead of re-deriving it from a platform name.

Platform and architecture are Extra Platforms traits rather than free strings, which is the same vocabulary PlatformKey keys the downloaded-binary registry on. as_matrix_entry() renders both back to their ids for the workflow matrix.

Every field is irreducible: anything a format decides (the file extension, what the floor measures, which header field to read) lives on BinaryFormat instead.

id: str

Short target identifier, chosen for user-friendliness.

It names the published release asset, so it must stay stable: download URLs, docs/install.md and the binaries.csv catalog all match on it.

Note

It is deliberately not derived from platform and arch, even though all six targets currently read {platform}-{short arch}. The asset names froze on the short x64 and arm64 spellings while those fields carry the canonical extra-platforms ids, and a future target splitting an existing pair (a musl Linux, say) would need a name the derivation cannot produce. tests/test_binary.py pins the pairing.

runner: str

Runner image, as named in GitHub-hosted runners.

Named for what it holds, not for the os matrix key it renders to: ubuntu-26.04-arm is an image, and platform is the operating system.

Hint

One compile job per target, each on one of the six runners the test matrix already covers, so a published binary is built on an image the suite is validated against. The targets are exactly KNOWN_RUNNERS, not a separate selection: an image is added here by widening the test axes, never on its own.

platform: Platform | Group

Operating system the binary runs on.

arch: Architecture

CPU architecture the binary is compiled for.

floor: str

Oldest runtime the binary is built to run on.

What the version counts is the format’s business, named by BinaryFormat.floor_label: a glibc symbol version on ELF, a macOS deployment target on Mach-O, a Windows release on PE. The first two are measured out of the compiled files by verify_binary_floor(); the third is documentation, since PE version headers record nothing to measure.

On macOS the release workflow also exports it as MACOSX_DEPLOYMENT_TARGET at compile time. Without it, compiled objects and processed dylibs inherit the build runner’s own macOS version.

container: str | None = None

OCI image the Linux compile and self-test jobs run in.

Passed to the container: key of the release workflow. Compiling inside manylinux_2_28 caps the toolchain at glibc 2.28, so binaries stop inheriting the floor of whatever glibc the current runner image ships.

Declared per target rather than derived from platform, which would assume every Linux target is a manylinux one. Absent on macOS and Windows: GitHub Actions containers do not exist for those runners.

property binary_format: BinaryFormat

Executable format the compiler emits for this target.

property extension: str

File extension of the compiled binary.

property expected_machine: str | int

Machine identifier this target’s binaries carry in their header.

property enforced_floor: str | None

floor, or None when the format records nothing to measure.

as_matrix_entry()[source]

Flat, JSON-safe mapping of this target, for GitHub matrix inclusion.

Renders platform and arch back to their extra-platforms ids, the spelling the workflow expressions (matrix.platform_id) and tests.yaml’s runner check compare against, and runner back to the os key runs-on: reads. A target with no container omits the key, so an entry carries only what applies to it.

Return type:

dict[str, str]

repomatic.release.binary.NUITKA_BUILD_TARGETS: Final[dict[str, BuildTarget]] = {'linux-arm64': BuildTarget(id='linux-arm64', runner='ubuntu-26.04-arm', platform=Group(id='linux', name='Linux distributions'), arch=Architecture(id='aarch64', name='ARM64 (AArch64)'), floor='2.28', container='quay.io/pypa/manylinux_2_28_aarch64@sha256:e7035406e58d96b7407246af1f6514a3cbd753a0025b42b9adfbeadd3b29ba80'), 'linux-x64': BuildTarget(id='linux-x64', runner='ubuntu-26.04', platform=Group(id='linux', name='Linux distributions'), arch=Architecture(id='x86_64', name='x86-64 (AMD64)'), floor='2.28', container='quay.io/pypa/manylinux_2_28_x86_64@sha256:fdb9a9c223b215604dc7b6f7e8fff4b39bfea5fbaa7777a2e5544a60dfa437f8'), 'macos-arm64': BuildTarget(id='macos-arm64', runner='macos-26', platform=Platform(id='macos', name='macOS'), arch=Architecture(id='aarch64', name='ARM64 (AArch64)'), floor='11.0', container=None), 'macos-x64': BuildTarget(id='macos-x64', runner='macos-26-intel', platform=Platform(id='macos', name='macOS'), arch=Architecture(id='x86_64', name='x86-64 (AMD64)'), floor='10.15', container=None), 'windows-arm64': BuildTarget(id='windows-arm64', runner='windows-11-arm', platform=Platform(id='windows', name='Windows'), arch=Architecture(id='aarch64', name='ARM64 (AArch64)'), floor='11', container=None), 'windows-x64': BuildTarget(id='windows-x64', runner='windows-2025', platform=Platform(id='windows', name='Windows'), arch=Architecture(id='x86_64', name='x86-64 (AMD64)'), floor='10', container=None)}

GitHub-hosted runner matrix for Nuitka builds, keyed by target name.

The roster is closed: every entry compiles on a runner the test matrix already covers, and the key doubles as the compiled binary’s published identifier. See BuildTarget for what each field means and which of them are frozen.

repomatic.release.binary.FLAT_BUILD_TARGETS: Final[list[dict[str, str]]] = [{'arch': 'aarch64', 'container': 'quay.io/pypa/manylinux_2_28_aarch64@sha256:e7035406e58d96b7407246af1f6514a3cbd753a0025b42b9adfbeadd3b29ba80', 'extension': 'bin', 'floor': '2.28', 'os': 'ubuntu-26.04-arm', 'platform_id': 'linux', 'target': 'linux-arm64'}, {'arch': 'x86_64', 'container': 'quay.io/pypa/manylinux_2_28_x86_64@sha256:fdb9a9c223b215604dc7b6f7e8fff4b39bfea5fbaa7777a2e5544a60dfa437f8', 'extension': 'bin', 'floor': '2.28', 'os': 'ubuntu-26.04', 'platform_id': 'linux', 'target': 'linux-x64'}, {'arch': 'aarch64', 'extension': 'bin', 'floor': '11.0', 'os': 'macos-26', 'platform_id': 'macos', 'target': 'macos-arm64'}, {'arch': 'x86_64', 'extension': 'bin', 'floor': '10.15', 'os': 'macos-26-intel', 'platform_id': 'macos', 'target': 'macos-x64'}, {'arch': 'aarch64', 'extension': 'exe', 'floor': '11', 'os': 'windows-11-arm', 'platform_id': 'windows', 'target': 'windows-arm64'}, {'arch': 'x86_64', 'extension': 'exe', 'floor': '10', 'os': 'windows-2025', 'platform_id': 'windows', 'target': 'windows-x64'}]

List of build targets in a flat format, suitable for matrix inclusion.

repomatic.release.binary.binary_name(package, target, version=None)[source]

Compose a compiled binary’s release-asset filename.

The one definition of the naming convention: {package}-{version}-{target}.{ext} for the versioned upload, and with no version the stable alias ({package}-{target}.{ext}) backing the releases/latest/download URLs. The extension comes from NUITKA_BUILD_TARGETS.

Return type:

str

repomatic.release.binary.versionless_alias(filename, version)[source]

Map a versioned binary filename to its stable alias, or None.

Strips the -{version}- segment (papaya-1.2.3-linux-arm64.bin becomes papaya-linux-arm64.bin). Returns None for filenames that carry no such segment or are not compiled binaries, so callers can filter and map in one pass.

Return type:

str | None

repomatic.release.binary.binary_filename_re(package)[source]

Match a package binary filename, versioned or versionless.

Captures target and ext, both alternations derived from NUITKA_BUILD_TARGETS so a new build target extends the pattern without anyone editing a regex. The release freeze rewrites both spellings onto the versioned form through this; tests/test_platform_keys.py pins the pattern against every target.

Return type:

Pattern[str]

repomatic.release.binary.pack_binary_assets(dist_dir, version)[source]

Pack a release’s upload list, materializing the versionless aliases.

Mirrors what the release engine’s upload step needs: every file in dist_dir except the Python distributions (create-release already uploaded those), plus a byte-identical versionless alias copied beside each versioned binary so the stable releases/latest/download URLs always resolve. Aliases share their sibling’s digest, which is what lets artifact attestations verify them unchanged and the binaries catalog collapse them (see binaries_page._binary_assets).

Idempotent: re-running overwrites the same aliases with the same bytes.

Parameters:
  • dist_dir (Path) – Directory holding the compiled binaries and attestation bundles downloaded from the build jobs.

  • version (str) – The release version whose binaries earn aliases.

Return type:

list[Path]

Returns:

Sorted paths to upload, aliases included.

repomatic.release.binary.BINARY_AFFECTING_PATHS: Final[tuple[str, ...]] = ('.github/workflows/_release-engine.yaml', '.github/workflows/release.yaml', 'pyproject.toml', 'tests/', 'uv.lock')

Path prefixes that always affect compiled binaries, regardless of the project.

Project-specific source directories (derived from [project.scripts] in pyproject.toml) are added dynamically by binary_affecting_paths.

The release workflow entries cover both layouts: upstream keeps the _release-engine.yaml lane (which defines the Nuitka compile and binary self-test jobs) in-repo, while downstream repos call the engine cross-repo from their generated release.yaml, so a pin bump there rightly triggers a rebuild.

repomatic.release.binary.SKIP_BINARY_BUILD_BRANCHES: Final[frozenset[str]] = frozenset({'format-images', 'format-json', 'format-markdown', 'format-shell', 'sync-gitignore', 'sync-mailmap', 'update-dep-graph'})

Autofix branches whose changes cannot affect compiled binaries.

Members are PR branch names produced by autofix jobs that touch only repository housekeeping (.mailmap, .gitignore, JSON, Markdown, images, shell scripts, dependency graph). The binary output is unchanged, so skip_binary_build returns True when the PR head branch matches a member, saving an expensive Nuitka compilation.

Note

This set is intentionally disjoint from repomatic.git_ops.VERSION_BUMP_BRANCHES: version-bump branches do change binaries (they rewrite the version string baked into the build), so they belong to a different policy.

repomatic.release.binary.LC_VERSION_MIN_MACOSX: Final[int] = 36

Mach-O load command carrying the minimum macOS version (pre-10.14 SDKs).

repomatic.release.binary.LC_BUILD_VERSION: Final[int] = 50

Mach-O load command carrying the platform and minimum OS (10.14+ SDKs).

repomatic.release.binary.MACHO_PLATFORM_MACOS: Final[int] = 1

platform field value naming macOS inside an LC_BUILD_VERSION command.

repomatic.release.binary.verify_binary_arch(target, binary_path)[source]

Verify that a binary matches the expected architecture for a target.

Parses the executable’s own headers, so it needs no external tool and behaves identically on runner VMs and inside build containers.

Parameters:
  • target (str) – Build target (e.g., ‘linux-arm64’, ‘macos-x64’).

  • binary_path (Path) – Path to the binary file.

Raises:
  • ValueError – If target is unknown.

  • ValueError – If binary format or architecture does not match.

Return type:

None

repomatic.release.binary.verify_binary_floor(target, binary_path, dist_dirs=())[source]

Verify the binary and its dist tree stay within the target’s OS floor.

Scans the onefile binary itself plus every native library of the given Nuitka dist directories (whose content the onefile payload repacks), and compares each file’s measured requirement to the target’s declared floor:

What the floor counts comes from the format, per BinaryFormat.floor_label:

  • ELF: the highest GLIBC_x.y version requirement of each file. A higher requirement means a compiled object picked up symbols newer than the build container provides for, and the binary would die at load time on the distributions the floor promises.

  • Mach-O: the minos of each file, against the deployment target the build exports as MACOSX_DEPLOYMENT_TARGET.

  • PE: nothing. Its version headers are nominal, so BuildTarget.enforced_floor reports no floor and this returns early; the Windows floor is CPython’s own support policy, tracked in the docs.

Parameters:
  • target (str) – Build target (e.g., ‘linux-arm64’, ‘macos-x64’).

  • binary_path (Path) – Path to the binary file.

  • dist_dirs (Iterable[Path]) – Nuitka dist directories to include in the scan.

Raises:
  • ValueError – If target is unknown.

  • ValueError – If any scanned file exceeds the declared floor.

Return type:

None