Fixes ARCore (and similar Google SDK xcframeworks) failing to build when consumed from the binary cache.
What changed
StaticXCFrameworkModuleMapGraphMapper now also adds each matching slice’s Headers root to a consuming target’s HEADER_SEARCH_PATHS, in addition to the module map’s own directory. The root is computed as xcframework.path/<library.identifier>/<library.headersPath> and filtered by target.supportedPlatforms, mirroring the existing FRAMEWORK_SEARCH_PATHS logic in the same mapper and GraphTraverser’s header-root computation.
A nested-header test fixture (ARCore-shaped) was added.
Why / root cause
A target that depends on a cached static Objective-C xcframework reached behind a dynamic xcframework (e.g. Google’s ARCore) failed with:
ARCoreGARSession.xcframework/ios-arm64/Headers/ARCoreGARSession/GARAnchor.h:21:9:
'ARCoreGARSession/GARTrackingState.h' file not found
could not build Objective-C module 'ARCoreGARSession'
The mapper only put the module map’s own directory on HEADER_SEARCH_PATHS. Combined with the umbrella-header rewrite (<ARCoreGARSession/X.h> → <X.h>), that resolves the umbrella’s direct imports. But ARCore’s real headers re-import each other with the framework prefix (#import <ARCoreGARSession/GARTrackingState.h>), which only resolves when the xcframework’s Headers root is also on the search path.
That root used to be supplied by GraphTraverser.librariesPublicHeadersFolders until #11472 excluded the headers of any module-map-bearing xcframework (to fix redefinition of module 'X'). That exclusion is correct for directly-linked xcframeworks — Xcode’s ProcessXCFramework extracts the SDK-matching slice’s headers (and module map) into $(BUILT_PRODUCTS_DIR)/include, which is on the search path. But a static xcframework reached behind a dynamic one is never linked/processed by the consumer, so the include/ copy never exists and the headers became unreachable.
So #11472 traded the rc.1 “redefinition” error for a “missing header” error in the cached static-behind-dynamic path. This PR restores the missing root for that path only.
Why this solution
- Nested layout (ARCore): the module map lives in a subdirectory, so adding the
Headers root does not auto-discover a second module map → no redefinition reintroduced, and the prefixed sibling imports now resolve.
- Flat layout (GoogleMaps): the root equals the module-map directory and is deduplicated → no behavior change.
- Directly-linked xcframeworks are untouched (#11472’s
GraphTraverser exclusion still applies). This only adds back what the cached static-behind-dynamic path lost.
Alternatives considered: copying the full Headers tree into tuist-derived/XCFrameworks/<name>/Headers to make the derived module self-contained — heavier (copies every header) and a larger change for the same effect.
Impact
Unblocks updating Tuist CLI for projects that cache Google ARCore (and similarly-shaped static ObjC xcframeworks with nested, prefix-self-importing headers). Reported by a customer stuck on 4.135.1; regression window is ≥ 4.195.0.
Should be backported to 4.201.x (same as #11472 → #11482).
How to test locally
tuist generate TuistKit TuistKitTests --no-open
xcodebuild test -workspace Tuist.xcworkspace -scheme Tuist-Workspace -only-testing TuistKitTests/StaticXCFrameworkModuleMapGraphMapperTests CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""
The new test test_map_when_static_xcframework_library_with_nested_module_headers_linked_via_dynamic_xcframework fails without the mapper change (asserts both the module-map dir and the Headers root on HEADER_SEARCH_PATHS) and passes with it. Verified the full suite is 18/18 green.
🤖 Generated with Claude Code