Hive Hive
Sign in

fix(cli): avoid embedding external resource bundles in cached frameworks

GitHub issue · Closed

Metadata
Source
tuist/tuist #11257
Updated
Jun 24, 2026
Domains
Cache
Details

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.

Comments
T
tuist[bot] Jun 12, 2026

🛠️ Tuist Run Report 🛠️

Tests 🧪
Scheme Status Cache hit rate Tests Skipped Ran Commit
TuistAcceptanceTests 0 % 100 0 100 178350c62
TuistUnitTests 0 % 2996 5 2991 178350c62
Failed Tests ❌
Flaky Tests ⚠️
  • TuistUnitTests: 3 flaky tests (View all)
Test case Module Suite
parseTestStatuses_returnsPassingModuleNames() TuistXCResultServiceTests XCResultServiceTests
parseTestStatuses_returnsCorrectStatuses() TuistXCResultServiceTests XCResultServiceTests
parseTestXCResult() TuistXCResultServiceTests XCResultServiceTests
Builds 🔨
Scheme Status Duration Commit
TuistAcceptanceTests 6m 52s 178350c62
TuistUnitTests 5m 13s 178350c62
TA
tuist-atlas[bot] Jun 20, 2026

The fix to avoid embedding external resource bundles in cached frameworks is now available in 4.201.0-canary.18. Update to this version to get it.

TA
tuist-atlas[bot] Jun 21, 2026

A fix to avoid embedding external resource bundles in cached frameworks is now available in 4.201.0-canary.19. This prevents internal source frameworks from incorrectly receiving transitive external SPM resource bundles in their copy resources phase. Update to 4.201.0-canary.19 to get this fix.