Hive Hive
Sign in

tuist cache fails with header '<Module>-Swift.h' not found for pure-Swift SPM products promoted to .framework under single-arch builds

GitHub issue · Closed

Metadata
Source
tuist/tuist #10967
Updated
Jun 24, 2026
Domains
Generated projects
Details

What happened?

When all three of these conditions hold:

  1. A pure-Swift SPM package (no .h headers, no @objc interop) is included as an external dependency.
  2. It’s promoted to .framework via PackageSettings.productTypes (e.g. productTypes: ["Logging": .framework]).
  3. The build is constrained to a single architecture via the ARCHS or EXCLUDED_ARCHS build setting (any variant: ARCHS = arm64, EXCLUDED_ARCHS = x86_64, EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64, or excluding the other arch).

…then tuist cache <consumer-target> fails because Xcode auto-generates a module.modulemap for the SPM-promoted framework that references <Module>-Swift.h. Under Xcode 26’s tightened explicit-modules dependency scanner, the scan against that modulemap runs before Swift has emitted the bridging header, so Clang aborts with header not found.

Multi-arch builds happen to work — Xcode’s build-phase scheduling produces -Swift.h early enough that the dep scanner finds it. ONLY_ACTIVE_ARCH=YES also works (xcodebuild treats it differently for generic destinations). Only an explicit ARCHS=/EXCLUDED_ARCHS= triggers the failure.

Expected behavior: tuist cache should succeed regardless of arch settings. Pure-Swift SPM products promoted to .framework should have a modulemap that does not depend on a build-phase ordering quirk to be valid.

How do we reproduce it?

A standalone 4-file minimal project (no App target, no UI, no in-tree fixture modification needed).

Files

Zipped: tuist-cache-bug-repro.zip

Tuist.swift:

import ProjectDescription
let tuist = Tuist(project: .tuist())

Tuist/Package.swift:

// swift-tools-version: 5.9
@preconcurrency import PackageDescription
#if TUIST
import ProjectDescription
let packageSettings = PackageSettings(
// Promote the pure-Swift `Logging` SPM product to a dynamic framework.
productTypes: ["Logging": .framework],
// Single-arch simulator build — the bug trigger.
baseSettings: .settings(
base: ["EXCLUDED_ARCHS[sdk=iphonesimulator*]": "x86_64"]
)
)
#endif
let package = Package(
name: "Repro",
dependencies: [
.package(url: "https://github.com/apple/swift-log", from: "1.0.0"),
]
)

Project.swift:

import ProjectDescription
let project = Project(
name: "Repro",
targets: [
.target(
name: "Consumer",
destinations: .iOS,
product: .framework,
bundleId: "dev.tuist.Consumer",
sources: ["Sources/**"],
dependencies: [
.external(name: "Logging"),
]
),
]
)

Sources/Consumer.swift:

import Logging
public enum Consumer {
public static func makeLogger() -> Logger {
Logger(label: "dev.tuist.repro")
}
}

Commands

tuist install
tuist cache Consumer --configuration Debug

Error log

[2026-05-27T16:16:18Z] [debug] [Noora] Error alert: Error
- Message: The command '/usr/bin/xcrun xcodebuild build -scheme Binaries-Cache-iOS -workspace /private/tmp/tuist-cache-bug-repro/Repro.xcworkspace -destination generic/platform=iOS Simulator SKIP_INSTALL=NO DEBUG_INFORMATION_FORMAT=dwarf STRIP_INSTALLED_PRODUCT=YES SWIFT_SERIALIZE_DEBUGGING_OPTIONS=NO ONLY_ACTIVE_ARCH=NO CODE_SIGN_IDENTITY= CODE_SIGN_ENTITLEMENTS= CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO COMPILER_INDEX_STORE_ENABLE=NO -configuration Debug SYMROOT=/private/var/folders/mm/1g4_22v926g2_xv6cyd3l5tc0000gn/T/CacheWarm-BD458357-FC5E-4C13-A61F-44232EF8F223/derived-data/Build/Products -resultBundlePath /private/var/folders/mm/1g4_22v926g2_xv6cyd3l5tc0000gn/T/CacheWarm-BD458357-FC5E-4C13-A61F-44232EF8F223/derived-data/509B42B7-ADE0-4778-AAE8-DB24DE283CD6 -derivedDataPath /private/var/folders/mm/1g4_22v926g2_xv6cyd3l5tc0000gn/T/CacheWarm-BD458357-FC5E-4C13-A61F-44232EF8F223/derived-data' terminated with the code 65:
error: clang dependency scanning failure: /private/var/folders/mm/1g4_22v926g2_xv6cyd3l5tc0000gn/T/CacheWarm-BD458357-FC5E-4C13-A61F-44232EF8F223/derived-data/Build/Products/Debug-iphonesimulator/Logging.framework/Modules/module.modulemap:2:10: error: header 'Logging-Swift.h' not found
Logging-efdab6b4.input:1:1: fatal error: could not build module 'Logging'
/tmp/tuist-cache-bug-repro/Sources/Consumer.swift:1:8: error: unable to resolve module dependency: 'Logging'
import Logging
^
/tmp/tuist-cache-bug-repro/Sources/Consumer.swift:1:8: note: a dependency of main module 'Consumer'
import Logging
^
error: clang dependency scanning failure: /private/var/folders/mm/1g4_22v926g2_xv6cyd3l5tc0000gn/T/CacheWarm-BD458357-FC5E-4C13-A61F-44232EF8F223/derived-data/Build/Products/Debug-iphonesimulator/Logging.framework/Modules/module.modulemap:2:10: error: header 'Logging-Swift.h' not found
Logging-fa1b7f76.input:1:1: fatal error: could not build module 'Logging'
/tmp/tuist-cache-bug-repro/Sources/Consumer.swift:1:8: error: unable to resolve module dependency: 'Logging'
import Logging
^
/tmp/tuist-cache-bug-repro/Sources/Consumer.swift:1:8: note: a dependency of main module 'Consumer'
import Logging
^
** BUILD FAILED **

macOS version

15.5 (24F74)

Tuist version

4.195.7

Xcode version

26.5 (Build 17F42) locally. Also reproduces on Xcode 26.3 .

Comments
H
HeEAaD May 27, 2026

Root cause analysis by Claude:

The <Module>-Swift.h line in the failing modulemap is not written by Tuist — grep -r '-Swift\.h' cli/Sources returns zero hits. It’s emitted by Xcode’s own modulemap auto-generator, which kicks in when a framework target has DEFINES_MODULE=YES, Swift sources, and no umbrella header file.

Tuist enables that auto-generation for pure-Swift SPM-promoted-to-framework targets. The relevant code is cli/Sources/TuistLoader/SwiftPackageManager/PackageInfoMapper.swift:1437-1452:

if let moduleMap {
switch moduleMap {
case .directory, .header, .custom:
settingsDictionary["DEFINES_MODULE"] = "NO" // ← suppresses Xcode auto-gen
// …plus OTHER_CFLAGS += -fmodule-name=<Module>
case .none:
break // ← leaves DEFINES_MODULE=YES → Xcode auto-gens
}
}

For a pure-Swift SPM target (no .h files anywhere under publicHeadersPath), SwiftPackageManagerModuleMapGenerator.generate() returns .none — see cli/Sources/TuistLoader/SwiftPackageManager/SwiftPackageManagerModuleMapGenerator.swift:134-149. The .none branch in PackageInfoMapper does nothing, so DEFINES_MODULE stays at Xcode’s framework default (YES), no MODULEMAP_FILE is set, and Xcode synthesises a modulemap with header "<Module>-Swift.h".

That synthesised modulemap is only valid if -Swift.h is on disk when the explicit-modules dependency scanner reads it. With multi-arch simulator builds (or ONLY_ACTIVE_ARCH=YES), the build-phase scheduling happens to produce -Swift.h before the scan. With ARCHS=/EXCLUDED_ARCHS= constraining to a single arch, the scheduling shifts and the scan beats the Swift compile.

The Xcode build-ordering quirk under explicit modules isn’t Tuist’s bug, but the trigger condition — leaving Xcode in charge of modulemap synthesis for pure-Swift SPM frameworks — is.

P
pepicrft May 27, 2026

Opened a PR for the modulemap side of this: https://github.com/tuist/tuist/pull/10971

The fix is intentionally scoped to Swift-only SwiftPM products promoted to frameworks. Those targets did not get a Tuist-provided modulemap, so Xcode could synthesize one that referenced <Module>-Swift.h. With explicit module scanning, that synthesized modulemap can be scanned before Swift emits the compatibility header, which explains the header '<Module>-Swift.h' not found failure.

I looked at the architecture angle too, especially projects where a package dependency excludes x86_64 for the simulator. There is a second concern there: cache warming builds with ONLY_ACTIVE_ARCH=NO, so it can try to produce simulator slices that the original graph cannot actually build.

I do not think we should fix that by propagating architecture exclusions into the generated project or cache consumption graph. If we do that, supported architectures become inconsistent across the graph depending on which dependency introduced the constraint and whether cache is enabled. It also means a cached binary can silently narrow the consumer target’s architecture support, which is hard to reason about.

So the current PR leaves architecture settings unchanged. That means binaries in the cache can still be partial architecture artifacts when the source graph itself only supports a partial simulator architecture set. We should treat that as a separate design problem: either make cache warming respect the exact build destination/architecture intentionally, or encode architecture support in the cache artifact metadata and selection logic so consumers never receive an incompatible binary. I would avoid solving it by mutating the consumer side of the graph.

H
HeEAaD May 27, 2026

Validated against 4.195.9 + Xcode 26.5 using the original 4-file repro: ✅ the header 'Logging-Swift.h' not found failure is gone. tuist cache Consumer --configuration Debug succeeds end-to-end without the EXCLUDED_ARCHS trigger (2 targets stored: Consumer, Logging). Thanks @pepicrft 🙌

One secondary issue surfaced when I re-tested the trigger combination from the OP: with productTypes: ["Logging": .framework] and baseSettings.base["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "x86_64" still set, caching a first-party .framework consumer of Logging now fails with:

…/LoggingConsumer/Sources/Consumer.swift:4:40: cannot find type 'Logger' in scope
[!] 'Consumer' is missing a dependency on 'Logging' because dependency scan of Swift module 'Consumer' discovered a dependency on 'Logging'

Different root cause — PackageSettings.baseSettings only applies to SPM products, so Logging.framework is built arm64-only while the first-party Consumer.framework still builds multi-arch. Consumer’s x86_64 SwiftCompile then can’t resolve Logging because no x86_64 slice exists.

Workaround: mirror the same EXCLUDED_ARCHS[sdk=iphonesimulator*] value into the consumer project’s settings so first-party targets also build arm64-only on simulator. For example, in Project.swift:

let project = Project(
name: "Repro",
settings: .settings(
base: ["EXCLUDED_ARCHS[sdk=iphonesimulator*]": "x86_64"]
),
targets: [ ... ]
)

With that single addition, the cache succeeds end-to-end (2 targets stored: Consumer, Logging).

Distinct from the original modulemap issue and arguably a settings-inheritance question rather than a bug, but flagging here in case anyone hits it after upgrading. Happy to open a separate issue if it warrants its own thread.

TA
tuist-atlas[bot] May 28, 2026

The fix for Swift-only package framework modulemaps is now available in 4.195.9. Please update to this version to resolve the header '<Module>-Swift.h' not found error when caching pure-Swift SPM products promoted to .framework under single-arch builds.

TA
tuist-atlas[bot] Jun 4, 2026

The fix for the missing -Swift.h header in pure-Swift SPM frameworks is now available in 4.195.15. Update to this version to resolve the cache failures.