Adds Module Cache support for static and dynamic library products, building on the XCTest and Swift Testing support framework cache work from #11281.
Related PRs
What changed
- Static and dynamic library targets are now treated as cacheable binary products in hashing, EE binary target selection, and warm generation.
- Cache warm now creates library XCFrameworks with
xcodebuild -create-xcframework -library, and passes -headers when the source target declares public headers.
- XCFramework
Info.plist decoding now preserves HeadersPath, and graph traversal exposes cached library Swift module directories and public header directories to generated targets.
- Added a complex acceptance fixture that combines static libraries, dynamic libraries, static frameworks, dynamic frameworks, Swift imports, and a public C header smoke build.
- Updated the Module Cache guide and changelog to document library support, including Swift modules and public C/Objective-C headers.
Why it changed
.xcframework artifacts support both frameworks and libraries, but the module cache flow only selected framework products for binary caching. That meant projects using static or dynamic library targets still had to rebuild those modules from source, even when their graph could otherwise be restored from cached binaries.
The missing piece was not just selecting libraries. Generated projects also need the metadata required to compile against a cached library artifact. Swift libraries need their .swiftmodule directories in the include paths, and C/Objective-C libraries need public headers from the XCFramework slice.
Root cause
The cacheable product lists and EE binary scheme selection were framework-centric. The warm step always emitted -framework arguments when creating XCFrameworks, so library products were never produced as cache artifacts. On restore, graph traversal only propagated public headers and Swift include paths for direct .library dependencies, not for library-based .xcframework dependencies.
Solution rationale
The implementation uses Xcode’s native XCFramework library support instead of introducing a custom artifact format or wrapping libraries as frameworks. For library products, cache warm now emits -library arguments and includes -headers when public headers exist. On restore, Tuist reads the standard HeadersPath metadata that Xcode writes into XCFramework Info.plist files and maps it back into header search paths.
This keeps the artifact format aligned with Xcode, preserves the existing framework cache behavior, and lets mixed framework/library graphs use the same binary cache pipeline.
User and developer impact
Projects with static or dynamic library targets can now reuse module cache binaries across local and CI builds. Mixed graphs can cache reusable library modules without forcing those modules to become frameworks. Test bundles remain excluded from binary caching, but framework and library targets that tests depend on can be cached.
How to test locally
TUIST_EE=1 TUIST_ENABLE_CACHING=1 tuist install --path .
TUIST_EE=1 TUIST_ENABLE_CACHING=1 tuist generate run --no-open --cache-profile none --path . tuist ProjectDescription TuistCoreTests TuistCacheTests TuistCacheEETests TuistCacheEEAcceptanceTests
TUIST_EE=1 TUIST_ENABLE_CACHING=1 tuist generate run --no-open --cache-profile none --path . tuist ProjectDescription TuistCacheEETests
xcodebuild test -workspace Tuist.xcworkspace -scheme TuistUnitTests '-only-testing:TuistCoreTests/GraphTraverserTests/test_librariesPublicHeadersFolders_includesLibraryXCFrameworkHeaders' '-only-testing:TuistCoreTests/GraphTraverserTests/test_librariesSwiftIncludePaths_includesLibraryXCFrameworkSwiftModules' CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""
xcodebuild test -workspace Tuist.xcworkspace -scheme TuistUnitTests '-only-testing:TuistCacheEETests/GenerateCacheableSchemesGraphMapperTests/test_map_when_cacheable_schemes_are_generated' '-only-testing:TuistCacheEETests/TargetsToCacheBinariesGraphMapperTests/test_map_when_library_binaries_are_fetched_successfully' CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""
xcodebuild test -workspace Tuist.xcworkspace -scheme TuistCacheEEAcceptanceTests '-only-testing:TuistCacheEEAcceptanceTests/TuistCacheEEAcceptanceTests/generated_macos_tool_with_cached_libraries_and_frameworks()' CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""
git diff --check