Nuitka compilation¶
The release engine compiles every selected [project.scripts] entry point into a self-contained, one-file executable with Nuitka, for each supported platform and architecture. This page is the canonical home for how those builds work and how fast they are; the surfaces it drives are documented on their own pages:
Page |
Covers |
|---|---|
Download URLs and release attestation checks. |
|
The per-release executable catalog, VirusTotal scans, minimum OS floors, and development builds. |
|
The |
|
The pinned Nuitka version and the |
|
The |
Build targets¶
One compile job per target, always on the latest runner image each OS offers for the architecture:
Target |
Runner |
Architecture |
Extension |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Linux targets compile inside digest-pinned manylinux_2_28 containers and macOS targets pin MACOSX_DEPLOYMENT_TARGET, so every binary carries a stable, measured OS floor instead of inheriting whatever the runner image ships. The floors, and what they open execution to, are documented in Minimum OS requirements; repomatic.binary.verify_binary_floor() fails the build when a linked floor exceeds the declared one.
Build cadence¶
Compiling the full fleet on every push is the expensive habit this design retired: a release push triggers ~96 jobs against an account-wide cap of 40 concurrent Linux runners and 5 macOS ones, and queue time, not compile time, dominated release latency. Three cadences share the work:
Ordinary pushes compile only the
[tool.repomatic] nuitka.dev-targetscanary subset (default["linux-arm64"], the fastest and cheapest builder; set[]to disable dev builds entirely). The binary self-tests in place inside the compile job and refreshes the rolling dev pre-release. With a warm cache this is a single ~3-minute job.The weekly schedule (Monday 00:43 UTC, batched with the other weekly maintenance jobs) rebuilds every target: it refreshes the dev pre-release with a complete binary set, keeps each target’s compile cache warm (GitHub evicts cache entries unused for 7 days), and surfaces platform-specific breakage before release day.
Release commits and manual
workflow_dispatchruns build the full fleet. Release commits additionally run the standalonetest-binariesjobs, which re-validate each artifact on a pristine VM through the same upload/download round-trip a user’s download takes.
Scheduled and dispatched runs own a run-scoped concurrency group, so a later push can neither cancel them nor be cancelled by them. Targets listed in nuitka.unstable-targets build with continue-on-error, and every compile job carries a 45-minute execution timeout so a hung build frees its runner slot instead of squatting it for GitHub’s 360-minute default.
Compile caching¶
Most of what Nuitka compiles is the standard library and dependencies, which barely change between builds, so the C-object caches are persisted across runs with actions/cache on canary and weekly builds. Release commits build cold, on purpose: see the supply-chain posture below. NUITKA_CACHE_DIR relocates every Nuitka cache (C objects, downloads, demoted-module bytecode) under the workspace, one cache entry per target, keyed per commit with a prefix restore-keys fallback: every caching run restores the latest previous snapshot and saves a fresh one, and the compiler caches hash compiler, flags and sources themselves, so a stale snapshot degrades to misses, never to wrong objects. A guard step resets any cache past 2 GiB, because Nuitka’s clcache never prunes itself.
Each platform caches through a different mechanism:
Target |
Object cache |
Provisioning |
|---|---|---|
|
Nuitka’s internal clcache |
Built into Nuitka, on by default for MSVC. |
|
none |
ccache is disabled on macOS with |
|
ccache |
Installed from EPEL inside the |
ccache needs two non-default settings to hit across runner VMs: compiler_check = content (the default hashes the compiler’s mtime and size, which VMs do not keep stable) and sloppiness = time_macros. They are exported as CCACHE_* environment variables and written into ccache.conf inside the persisted cache directory, so whichever channel reaches the compiler wrapper carries the same values.
Measured on 7.9.0.dev0 (Nuitka 4.1.3, ~470-495 C files per binary), cold versus warm-cache execution time of the compile job:
Target |
Cold |
Warm |
Warm hit rate |
|---|---|---|---|
|
9.4 min |
3.0 min |
470/470 |
|
11.9 min |
3.4 min |
470/470 |
|
17.4 min |
6.0 min |
495/495 |
|
15.5 min |
6.5 min |
494/495 |
|
10.5 min |
not cached |
0/470 across VMs |
|
11.0 min |
not cached |
0/470 across VMs |
The residual warm time is the part caching cannot touch: Nuitka’s Python-level compilation, linking, onefile payload compression, and the self-test. The one cache-missing module on warm Windows builds is the version module the pre-bake step stamps per build.
macOS is the measured exception: its ccache (the 4.2.1 x86-64 build Nuitka auto-downloads, under Rosetta on arm) never hits across runner VMs, and the cause is undiagnosed. A cache written 15 minutes earlier by a sibling VM on the same commit missed 470/470 with the CCACHE_* environment variables exported, and missed 470/470 again with the same settings in ccache.conf inside the restored cache directory, so something ccache hashes (not its configuration) differs per VM. Since a within-run ccache never re-hits either, macOS ccache carried zero benefit, and it is now disabled outright (--disable-ccache), which also removes an unchecksummed binary download from the pipeline. Capturing Nuitka’s CCACHE_LOGFILE from a scheduled run is the next probe before reconsidering. The cost is bounded, since macOS only builds weekly and at releases.
To inspect a cache, the compile job logs Nuitka’s own hit/miss summary (Cached C files (using ccache) with result …) plus a per-subdirectory size report. To reset one, delete its entries: gh cache list --key nuitka- then gh cache delete {key}; the next build re-seeds it.
Supply-chain posture¶
The compile job resolves every input through a pinned, cooldown-gated channel: the CLI and its dependencies from the hash-pinned uv.lock under the workflow-wide UV_EXCLUDE_NEWER, Nuitka itself as an exact registry pin walked forward under minimum-release-age, actions as SHA pins, and the Linux ccache from EPEL, a GPG-verified distro archive that the cooldown doctrine deliberately leaves to the distro’s own staging.
The compile cache is the one input no cooldown concept covers, and it gets a structural control instead. GitHub scopes cache writes by ref, so fork pull requests can never write entries the engine restores; the residual writer set is code already running in main-ref workflows, which is why every action there is SHA-pinned and cooled. Against the remaining sliver (a cache entry is a tar archive extracted into the workspace, and a poisoned compiler object would flow silently into a signed, published, immutable binary), release commits neither restore nor save the cache: every published artifact compiles from source on a fresh VM, and cache contents can influence only throwaway canary and weekly builds. macOS additionally runs with ccache disabled, removing the one unchecksummed download (Nuitka’s ccache-4.2.1.zip from nuitka.net, fetched with no hash at the call site) from the pipeline.
Detection layers back the prevention: artifact attestation binds every binary to the exact run that built it, verify-binary checks architecture and OS floors, the self-test executes the binary, and every release is submitted to VirusTotal. None of these would catch a competent backdoor on their own; the cold-release rule is what keeps them from having to.
Link-time optimization¶
Nuitka’s --lto=auto (the default, and what repomatic runs) disables LTO outright above 250 compiled modules, and these builds sit far past that threshold, so no LTO runs and no flag is set in [tool.nuitka]. Forcing --lto=yes would slow every build for a marginal runtime gain on a CLI whose cost is cold-start, and on MSVC it would compile objects with /GL, which no object cache can store. Leave it alone.
Upstream workarounds¶
Three Nuitka issues shape the integration; each workaround names its ticket and lives next to the code it patches:
Nuitka#3879: a
__main__.pyentry point must be compiled as its package directory plus--python-flag=-m, or the binary silently exits.repomatic.metadata.Metadata.nuitka_matrixcomputes the flag per entry point; the same property documents why Nuitka 4.1’s--main-entry-pointflag cannot replace the positional form yet.Nuitka#3909: Nuitka does not read
[tool.nuitka]natively, so the tool runner translates the section into CLI flags at build time.Nuitka#3994: a symlink inside an
--include-data-dirtree ships dangling (and crashes macOS codesigning), so the tool runner stages a symlink-free copy of any such directory before invoking Nuitka. Upstream confirmed the diagnosis; a real fix lands no earlier than Nuitka 4.3, and the staging shim stays until then.
Troubleshooting¶
A build fails outright: the job uploads Nuitka’s
nuitka-crash-report.xmlas a run artifact before the failure gate trips.verify-binaryfails: a toolchain, runner-image or container update raised the binary’s actual glibc or macOS floor above the declared one. That gate exists precisely to stop the floor from drifting silently; fix the environment rather than the declaration.A release shipped without some binaries: expected behavior, not an incident. Publishing is deliberately not held hostage by a failing build cell, and immutable releases make the gap permanent for that version; the next release carries the missing target. See the release workflow documentation.
A cache behaves suspiciously: caches self-invalidate on compiler and source changes and auto-reset past 2 GiB, so poisoning is unlikely; deleting the target’s
nuitka-*cache entries forces a clean rebuild.