Hive Hive
Sign in

fix(cli): handle SwifterPM registry symlinks and xcframework module maps

GitHub issue · Closed

Metadata
Source
tuist/tuist #11470
Updated
Jul 5, 2026
Domains
Generated projects
Details

Related reports:

What changed

This updates SwifterPM registry download materialization so symlink mode keeps each package root as a real workspace-local directory under .build/registry/downloads. The root directory’s top-level entries are symlinks into SwifterPM’s cache, so local installs still avoid a recursive copy while Xcode keeps seeing the same package root shape that Swift Package Manager exposes.

This also updates the static .xcframework module map mapper. When a static .xcframework slice provides module.modulemap, Tuist now exposes a derived headers directory containing the copied headers and the selected module map, instead of pointing dependents back at the original slice headers directory.

Why

SwifterPM should stay compatible with the workspace layout that Swift Package Manager expects, and only change the parts that make dependency restoration more efficient. Replacing the whole registry package root with a symlink was too visible to Xcode’s dependency scanner. Keeping the root real but symlinking the contents preserves the efficient local cache behavior without changing the path model that generated projects rely on.

Root cause

For registry packages with transitive C or C++ headers, Xcode canonicalizes a symlinked package root during dependency scanning. That can make the compiler reason from SwifterPM’s cache location instead of the workspace registry download location, which breaks header paths that are supposed to be rooted under .build/registry/downloads.

For CLibsql.xcframework, the slice already ships a module.modulemap. Tuist generated another module map and also exposed the original headers directory, so clang dependency scanning could see two definitions of CLibsql.

Approach

Registry package downloads now use a dedicated replacement path. Copy mode still copies the full directory. Symlink mode creates a real destination directory and symlinks each top-level source entry into it. Automatic mode remains environment-sensitive through the existing cached-directory materialization policy, so continuous integration keeps using copies and local restores keep using symlinked content.

For static .xcframework module maps, the mapper copies all non-module-map header files from the selected slice headers directory into Tuist’s derived headers directory. Header search paths then point to that derived directory, where Tuist’s chosen module map is the only module map visible to downstream targets.

Impact

Local warm restores keep sharing package contents through SwifterPM’s cache, including registry packages. Generated Xcode projects now see registry download roots as regular directories, which matches Swift Package Manager’s workspace shape more closely. Continuous integration remains copy-based in automatic mode.

Validation

I validated this by running:

  • swift test --replace-scm-with-registry --filter SupportTests from swifterpm/
  • swift test --replace-scm-with-registry --filter RestoreTests from swifterpm/
  • xcodebuild test -workspace Tuist.xcworkspace -scheme Tuist-Workspace -only-testing TuistKitTests/StaticXCFrameworkModuleMapGraphMapperTests CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""
  • git diff --check

I also validated the Firebase Firestore reproduction with the locally built tuist binary:

  • TUIST_USE_SWIFTERPM=1 FIREBASE_SOURCE_FIRESTORE=1 ... tuist install
  • TUIST_USE_SWIFTERPM=1 FIREBASE_SOURCE_FIRESTORE=1 ... tuist generate
  • xcodebuild -workspace FirebaseFirestoreSymlinkRepro.xcworkspace -scheme ReproApp -destination "platform=iOS Simulator,name=iPhone 17" -derivedDataPath DerivedData build

The registry download shape after install was:

  • registry_root_symlinks=0
  • registry_root_directories=12
  • top_level_content_symlinks=307

How to test locally

In a project that uses registry packages with transitive C or C++ headers, enable SwifterPM and run tuist install, tuist generate, and an Xcode build in local automatic mode. The generated project should build while registry package roots under .build/registry/downloads remain real directories whose top-level contents point at the SwifterPM cache.

Flights

Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.

New Flights are paused Configure model inference, GitHub, and a sandbox provider to start another Flight. Existing results remain available below.
No Flights yet

Start a Flight and preserve its objective, outcome, and session here.

Comments
T
tuist[bot] Jun 24, 2026

🛠️ Tuist Run Report 🛠️

Tests 🧪
Scheme Status Cache hit rate Tests Skipped Ran Commit
TuistAcceptanceTests 82 % 194 0 194 460b67627
TuistUnitTests 83 % 3045 4 3041 460b67627
Flaky Tests ⚠️
  • TuistAcceptanceTests: 1 flaky test (View all)
  • TuistUnitTests: 2 flaky tests (View all)
Test case Module Suite
app_with_plugins() TuistKitAcceptanceTests PluginAcceptanceTests
parseTestWithCustomLabelXCResult() TuistXCResultServiceTests XCResultServiceTests
parseTestXCResult() TuistXCResultServiceTests XCResultServiceTests
Builds 🔨
Scheme Status Duration Commit
TuistAcceptanceTests 1m 28s 460b67627
TuistUnitTests 3m 7s 460b67627