What changed
- Updates
TuistCacheEE so cached graph mutation keeps external bundle targets as source dependencies when they are reachable from a source target that cannot embed external resource bundles, such as an internal framework.
- Keeps the existing behavior for app-like targets: external resource bundles can still be replaced by cached bundle artifacts when the source root is able to embed them.
- Adds regression coverage for both cases in
CacheGraphMutatorTests.
Why
A customer reported that tuist cache generated internal dynamic frameworks with SPM resource bundles in their Copy Bundle Resources phase. That diverged from source generation, where those external bundles are embedded at the app level instead of copied into every internal framework that transitively depends on the package.
The duplicated bundles caused framework products, and ultimately the app bundle, to grow substantially.
Root cause
CacheGraphMutator treated cached external bundle artifacts like other replaceable artifacts whenever they were present in the precompiled artifacts map. When an internal framework was generated from source while one of its transitive dependencies was cached, the external SPM resource bundle could be substituted with a cached bundle artifact and then appear as a resource dependency of the source framework.
Framework targets should not embed those external resource bundles. They need the external bundle target to remain in the graph so resource bundle traversal can continue to the app-like target that is responsible for embedding it.
E2E repro
I created a temporary fixture at /private/tmp/tuist-cache-resource-repro with this shape:
App app target depends on Account
Account dynamic framework depends on Theme
Theme dynamic framework depends on external SPM product ResourcesLib
ResourcesLib is a static library product with processed resources, which creates ResourcesPackage_ResourcesLib.bundle
Using the mise-installed CLI:
/Users/marekfort/.local/share/mise/installs/tuist/latest/bin/tuist version
# 4.199.2
env XDG_CACHE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-installed-cache \
XDG_STATE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-installed-state \
/Users/marekfort/.local/share/mise/installs/tuist/latest/bin/tuist install \
--path /private/tmp/tuist-cache-resource-repro
env XDG_CACHE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-installed-cache \
XDG_STATE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-installed-state \
/Users/marekfort/.local/share/mise/installs/tuist/latest/bin/tuist cache warm \
--path /private/tmp/tuist-cache-resource-repro Theme
env XDG_CACHE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-installed-cache \
XDG_STATE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-installed-state \
/Users/marekfort/.local/share/mise/installs/tuist/latest/bin/tuist cache warm \
--path /private/tmp/tuist-cache-resource-repro --generate-only Account
The installed CLI reproduced the issue:
rg -n "ResourcesPackage_ResourcesLib.bundle in Resources" \
/private/tmp/tuist-cache-resource-repro/CacheResourceRepro.xcodeproj/project.pbxproj
# 171: ResourcesPackage_ResourcesLib.bundle in Resources
xcodebuild build \
-project /private/tmp/tuist-cache-resource-repro/CacheResourceRepro.xcodeproj \
-scheme Account \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath /private/tmp/tuist-cache-resource-repro/derived-installed \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \
-quiet
find /private/tmp/tuist-cache-resource-repro/derived-installed/Build/Products \
-name 'ResourcesPackage_ResourcesLib.bundle' -print
# /private/tmp/tuist-cache-resource-repro/derived-installed/Build/Products/Debug-iphonesimulator/Account.framework/ResourcesPackage_ResourcesLib.bundle
As a source-generation baseline, this command produced an empty Account resources phase:
env XDG_CACHE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-installed-cache \
XDG_STATE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-installed-state \
/Users/marekfort/.local/share/mise/installs/tuist/latest/bin/tuist generate run \
--path /private/tmp/tuist-cache-resource-repro \
--no-open \
--no-binary-cache \
Account
E2E validation with this fix
I built the patched CLI locally:
xcodebuild build \
-workspace Tuist.xcworkspace \
-scheme tuist \
-destination platform=macOS,arch=arm64 \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \
-quiet
Then reran the same fixture with separate cache/state directories:
PATCHED_TUIST=/Users/marekfort/Library/Developer/Xcode/DerivedData/Tuist-eypjkqhhtpkiigepfjigsqzyxzmw/Build/Products/Debug/tuist
env XDG_CACHE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-fixed-cache \
XDG_STATE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-fixed-state \
"$PATCHED_TUIST" install --path /private/tmp/tuist-cache-resource-repro
env XDG_CACHE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-fixed-cache \
XDG_STATE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-fixed-state \
"$PATCHED_TUIST" cache warm --path /private/tmp/tuist-cache-resource-repro Theme
env XDG_CACHE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-fixed-cache \
XDG_STATE_HOME=/private/tmp/tuist-cache-resource-repro/xdg-fixed-state \
"$PATCHED_TUIST" cache warm --path /private/tmp/tuist-cache-resource-repro --generate-only Account
The fixed generated project no longer embeds the external bundle into Account:
rg -n "ResourcesPackage_ResourcesLib.bundle in Resources|ResourcesPackage_ResourcesLib.bundle|Theme.xcframework|ResourcesLib.xcframework" \
/private/tmp/tuist-cache-resource-repro/CacheResourceRepro.xcodeproj/project.pbxproj
# Shows Theme.xcframework and ResourcesLib.xcframework references, but no ResourcesPackage_ResourcesLib.bundle.
The fixed product also builds without the duplicated bundle:
xcodebuild build \
-project /private/tmp/tuist-cache-resource-repro/CacheResourceRepro.xcodeproj \
-scheme Account \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath /private/tmp/tuist-cache-resource-repro/derived-fixed \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \
-quiet
find /private/tmp/tuist-cache-resource-repro/derived-fixed/Build/Products \
-name 'ResourcesPackage_ResourcesLib.bundle' -print
# No output
Additional validation
env TUIST_EE=1 tuist generate tuist TuistCacheEE TuistCacheEETests ProjectDescription --no-open
xcodebuild test \
-workspace Tuist.xcworkspace \
-scheme TuistCacheEEUnitTests \
-destination platform=macOS,arch=arm64 \
-only-testing TuistCacheEETests/CacheGraphMutatorTests \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \
-quiet
git diff --check
git -C cli/TuistCacheEE diff --check
Impact
Internal source frameworks generated during cache workflows should no longer receive transitive external SPM resource bundles in their copy resources phase. App-like source targets still receive the bundle dependency so runtime resource lookup keeps working.