Corrects the ARCore caching fix from #11506, which traded one build failure for another. Reproduced and guarded by a new acceptance test.
What changed
StaticXCFrameworkModuleMapGraphMapper now distinguishes nested ARCore-style static Objective-C xcframeworks from flat ones and consumes nested ones through the xcframework’s own module map:
- Nested (module map + headers in a
Headers/<ModuleName>/ subdirectory): -fmodule-map-file points at the xcframework’s original module map, and the Headers root (parent of the <ModuleName>/ subdir) is the consumer’s HEADER_SEARCH_PATHS. No derived copy or umbrella rewrite is generated.
- Flat (headers directly next to the module map): unchanged — derived module map + prefix-stripped umbrella, as before.
A new acceptance test (generated_macos_tool_with_cached_nested_header_xcframework) and a nested unit test were added. Existing flat/framework/relocation unit tests are untouched.
Why / root cause
ARCore’s static xcframeworks (verified against ARCoreGARSession 1.54.0) lay their public API out as:
ARCoreGARSession.xcframework/ios-arm64/Headers/ARCoreGARSession/
module.modulemap # module ARCoreGARSession { umbrella header "…"; module * { export * } }
ARCoreGARSession.h # #import <ARCoreGARSession/GARAnchor.h> … (framework-prefixed)
GARAnchor.h # #import <ARCoreGARSession/GARTrackingState.h> (prefixed sibling import)
…
When such an xcframework is reached behind a dynamic framework through the binary cache, it routes through this mapper (it is never linked directly, so Xcode’s ProcessXCFramework never extracts its headers to $(BUILT_PRODUCTS_DIR)/include).
- rc.2: the mapper put only the
Headers/<Module>/ subdir on the search path, so the headers’ own <Module/Sibling.h> cross-imports could not resolve → 'ARCoreGARSession/GARTrackingState.h' file not found → could not build Objective-C module 'ARCoreGARSession'.
- #11506 (rc.3): added the xcframework’s
Headers root to the search path. The imports resolved, but the original module map (sitting next to the headers, with module * { export * } wildcard submodules) became reachable alongside the derived copy referenced via -fmodule-map-file → the module was defined twice → import of shadowed module 'GARAnchor' → could not build Objective-C module 'ARCoreGeospatial'.
Both are the same flat-vs-nested split the mapper never modeled. For nested layouts the umbrella already imports with the <Module/...> prefix, so the original module map + Headers root resolves everything with the module defined exactly once — no derived copy, no rewrite, no second module map.
Why this approach over the obvious alternative
A unified “copy the whole module-map directory into a hermetic derived tree” also works and was validated, but it changes HEADER_SEARCH_PATHS/-fmodule-map-file/side-effects for every static-objc library xcframework — large churn across ~9 existing unit tests and the cross-project path-relocation logic they cover. Branching on layout keeps the proven flat path (and its tests) byte-for-byte unchanged and touches only the nested case.
Validation
Built a faithful two-module fixture — NestedObjC / NestedObjCKit (static .a Objective-C xcframeworks with nested, prefix-self-importing headers, where NestedObjCKit imports NestedObjC, mirroring ARCore Geospatial → GARSession), linked by a dynamic Library that is cached and consumed by Tool. Run with real tuist binaries:
| tuist |
result |
| 4.200.5 |
❌ 'NestedObjC/TrackingState.h' file not found |
| 4.201.0-rc.3 (#11506) |
❌ import of shadowed module 'Anchor' → can’t build NestedObjCKit |
| this PR |
✅ BUILD SUCCEEDED |
StaticXCFrameworkModuleMapGraphMapperTests: 18/18 pass (new nested test + all existing).
- End-to-end with a
tuist built from this branch: tuist cache Library → tuist generate Tool → xcodebuild build Tool succeeds.
Impact / follow-up
Unblocks customers caching ARCore (and similarly-shaped static ObjC xcframeworks with nested prefix-self-importing headers). 4.201.0-rc.3 ships the broken (#11506) fix and should not be used — this needs to be backported to releases/4.201.x and a fresh RC (rc.4) cut.
How to test locally
cd examples/xcode/generated_macos_tool_with_cached_nested_header_xcframework
tuist cache Library
tuist generate Tool --no-open
xcodebuild build -workspace NestedHeaderXCFramework.xcworkspace -scheme Tool -destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""
(NestedObjCSources/build.sh regenerates the prebuilt xcframeworks.)
🤖 Generated with Claude Code