What changed
- Keep the cache-warm preload graph normalized with
FrameworkSearchPathsGraphMapper.
- Preserve the same normalized hashing graph before
TargetsToCacheBinariesGraphMapper runs in generate/build flows that were not already preserving it.
- Add unit coverage for the mapper ordering so cache replacement receives stable hashes in warm, generate, and build paths.
- Add an acceptance test that warms a framework wrapping precompiled dependencies, runs
generate --cache-profile all-possible, and verifies the warmed binary is actually reused and linked.
Why
A cache-warmed target could be stored under a different key than the one requested later by tuist generate --cache-profile all-possible.
The concrete failure mode was a dynamic framework wrapping external binary/framework dependencies. tuist cache warm <target> hashed a graph where generated framework search paths had already been injected into targetSettings, while the unfocused generate --cache-profile all-possible path computed cache keys from a graph that had not preserved that same normalized state before replacing targets with cache binaries. The target’s sources, resources, dependencies, project settings, and other hash components matched, but targetSettings diverged, so generate missed the warmed artifact.
The first implementation tried to remove framework-search-path normalization from cache warm. That aligned the all-possible reproduction, but it broke existing default/focused cache reuse paths that depend on warm hashing the normalized graph. The final fix keeps warm behavior intact and instead makes generate/build preserve the same normalized hashing graph before binary replacement.
Regression source
This regression was introduced by #11054 (de9b946c217), which moved framework search path setup into FrameworkSearchPathsGraphMapper.
That change made generated framework search paths part of the mapped graph earlier in some flows. Cache warm included those settings in its target-settings hash, while unfocused generate/build cache replacement could request binaries using a hash computed before that same normalization had been preserved.
Approach
The mapper factory now inserts CacheHashingGraphMapper before TargetsToCacheBinariesGraphMapper for binary-cache generate/build flows that do not already preserve the hashing graph through focused targets. The hashing mapper runs the same normalization mappers used by warm, including framework search paths, so both warm and consume paths derive cache keys from the same graph.
This avoids reintroducing the prior default/focused cache regression because FrameworkSearchPathsGraphMapper remains in the cache-warm preload chain. Build-time framework search paths are still generated where the produced Xcode project needs them.
Impact
Targets warmed with tuist cache warm <target> should now be reusable by tuist generate --cache-profile all-possible and by build/generate cache replacement paths when their logical inputs are unchanged. The change is limited to cache hashing graph preservation and mapper ordering; it does not change artifact production or upload logic.
Validation
- Reproduced the mismatch with a local fixture using
4.201.0-rc.5: warm and generate produced different hashes for the wrapper target.
- Verified the locally built CLI reuses the warmed wrapper binary: generate reports
Using cache binaries for the following targets: Library, and the generated project links Library.xcframework.
- Ran targeted cache acceptance coverage:
generated_macos_tool_with_cached_nested_header_xcframework()
generated_ios_app_with_cached_xctest_support()
generate_reuses_warmed_framework_wrapping_precompiled_dependencies()
- Ran focused mapper unit tests:
TuistKitTests/CacheGraphMapperFactoryTests
- Ran lint:
- Confirmed CI is green on the PR after rerunning a transient acceptance shard that timed out downloading the prebuilt test bundle.