Update: the swing breaks down into two layers (~5 MB external + ~3 MB first-party)
Pulling the module-cache targets for three generations of the app (via the dashboard data) pins down exactly what moves. The whole swing is the main Mach-O; assets/resources are byte-identical. A cache-hit (remote) target is linked as a prebuilt static framework; a miss target is compiled from source and dead-stripped, so it links smaller. Bundle size therefore tracks how many targets were cache-warm at tuist generate time:
| Coverage | Install | What’s prebuilt |
|---|---|---|
0 cached (--no-binary-cache) |
19.66 MB | nothing (all source) |
| 135 cached | 24.85 MB | external deps only |
| 145 cached | 27.88 MB | external + first-party |
~5 MB layer (19.66 → 24.85): the 135 third-party deps (ArgumentParser, SwiftSyntax, Nuke, …). These are stable (only change on dep bumps), so they’re almost always warm and contribute a near-constant +5 MB. They only go cold on a cache invalidation/regression.
~3 MB layer (24.85 → 27.88): exactly 10 first-party Tuist* modules — TuistCore, TuistServer, TuistAuthentication, TuistAutomation, TuistPreviews, TuistOnboarding, TuistProfile, TuistErrorHandling, TuistMenuBar, TuistXCActivityLog. This is the intermittent part. Their content hash changes on nearly every main commit (e.g. TuistCore’s buildable_folders subhash differed between the two generations, giving a different cache_hash and a miss vs remote lookup), so whether a build links the prebuilt binary depends purely on whether the cache-warm already ran for that exact hash.
Why caching inflates the binary
The inflation is not library-evolution overhead. BUILD_LIBRARY_FOR_DISTRIBUTION is only set for ProjectDescription/ProjectAutomation in Module.swift; TuistCore and the rest fall into the default branch with it off. The residual difference is inherent cross-module optimization loss: compiled with the app, a module is inlined/specialized across the boundary and the remainder dead-stripped; as a prebuilt static framework it’s compiled in isolation and linked fuller. That part is only recoverable by building from source, not by post-hoc dead-stripping (the static framework already goes through the app’s -dead_strip).
Implication for the fix
tuist generate --cache-profile only-externalstops caching the volatile first-party modules. It removes the ±3 MB flap and keeps third-party build speed, and those first-party hashes barely hit the cache anyway (they churn every commit).--no-binary-cacheon the measured/shipped build yields the true ~19.66 MB size, deterministic but slower.
Net: ~5 MB is stable external-cache inflation, ~3 MB is the first-party-cache flap. Both are “prebuilt framework links larger than source,” not a real code regression, which is why unrelated PRs get blamed.