External binaryTarget dependency update fails to compile without Xcode Clean

Steps to reproduce:

1. Run tuist init

2. In Tuist/Package.swift, add:

.package(url: "https://github.com/getsentry/sentry-cocoa", exact: "9.16.1")

3. In Project.swift, add the dependency to the main target:

dependencies: [.external(name: "Sentry")]

4. Run tuist install

5. Run tuist generate

6. In Xcode, add the following code:

import Sentry

// Add this somewhere:
SentrySDK.start { options in }

7. In Xcode press Cmd+BBuilds OK :white_check_mark:

8. Downgrade the version in Tuist/Package.swift to exact: "8.58.3"

9. Run tuist install

10. Run tuist generate

11. In Xcode press Cmd+BBuild Fails :cross_mark:

BUT: If I do an Xcode Clean at this point, it builds fine.

Expected behavior:

After running tuist install and tuist generate with the updated version, Xcode should build successfully without requiring a manual clean.

Environment:

Tuist version: 4.196.0

Thanks!

Hey!

Can you provide a reproducible sample and provide us with the exact Xcode versions? I wasn’t able to reproduce the issue on our side based on this description.

I was able to reproduce the issue deterministically now, and the short version is: this is an Xcode bug, not a Tuist one. It reproduces with plain SwiftPM and no Tuist involved.

Reproduction

On Xcode 26.5 + Tuist 4.196.0, the failure is reliable once you go back and forth between the two versions:

clean build @ Sentry 8.58.3                    → ✅switch → 9.16.1, tuist install + generate, build → ✅switch → 8.58.3, tuist install + generate, build → ❌ missing required module 'CommonCrypto'

The failing step is always the downgrade, and only after at least two switches. A single switch, or starting clean on 9.16.1, doesn’t trigger it, which lines up with your observation that it worked at first and then started happening after toggling a few times.

What’s actually going on

It’s Xcode’s explicit modules incremental build reusing a stale precompiled module from the module cache:

  • Sentry 9.16.1’s Sentry.swiftinterface does import CommonCrypto. 8.58.3’s does not (their module.modulemap differs too).
  • After you downgrade, the explicit module map Xcode hands to the Swift frontend correctly contains no CommonCrypto entry, but it still points Sentry at a precompiled Sentry-*.swiftmodule left over from the 9.16.1 build that still records a requirement on CommonCrypto (and SwiftUI). The frontend can’t satisfy it, so you get missing required module 'CommonCrypto'.
  • Because the binary xcframework is vendored at a version-independent path, the framework search path never changes across versions, so Xcode’s scanner doesn’t re-derive the module and the stale one sticks around until you Clean.

Why it’s not Tuist

I built a tiny vanilla SwiftPM package with the same Sentry binary dependency and no Tuist at all, then ran the exact same back-and-forth via xcodebuild. It fails identically (missing required modules: 'CommonCrypto', 'SwiftUI'). Native SwiftPM vendors binary artifacts at the same kind of stable path, so it hits the same cache staleness. I’d recommend filing this with Apple as an Xcode explicit-modules / binary-target incremental-build bug.

Workarounds

All three of these resolve it, verified:

  1. Xcode Clean (what you’re doing today).
  2. You don’t actually need a full Clean. Wiping just the module cache is enough:
rm -rf <DerivedData>/Build/Intermediates.noindex/SwiftExplicitPrecompiledModulesrm -rf <DerivedData>/ModuleCache.noindex
  1. The cleanest one for CI: disable explicit modules by setting SWIFT_ENABLE_EXPLICIT_MODULES=NO. With that, the full back-and-forth sequence builds end to end with no clean. You can set it as a build setting in your Project.swift on the affected target(s). The tradeoff is you lose the explicit-modules build-performance improvements, but it sidesteps the cache bug.

Hope that unblocks your CI. Happy to help wire up option 3 in the project if useful.

note: answer generated by Claude