I’ve created a demo project for this. Please let me know if there is a workaround fix for this issue
Hive
Xcode 27: external dependency compiled for iOS 13.0 despite its iOS 15.0 minimum deployment target
GitHub issue · Open
What happened?
When generating and building our iOS project with Tuist 4.202.5 and Xcode 27 beta 3, Tuist produces a target configuration that compiles for iOS 13.0.
However, a transitive framework dependency, JOSESwift, has a minimum deployment target of iOS 15.0. This causes the Xcode build to fail because the consuming target is compiled for an older deployment target.
The dependency chain is:
Trainline App → ThreeDSecureProviders → checkout-3ds-sdk-ios → JOSESwift
This appears to be related to Xcode 27’s requirement that the minimum iOS deployment target is iOS 15.0, which is not being reflected in the generated project or dependency configuration.
How do we reproduce it?
Use this sample: DemoProject.zip
- run
.reproduce.sh
Error log
compiling for iOS 13.0, but module 'JOSESwift' has a minimum deployment target of iOS 15.0:
/Users/<user>/Library/Developer/Xcode/DerivedData/Trainline-.../Build/Products/Debug-iphonesimulator/JOSESwift.framework/Modules/JOSESwift.swiftmodule/arm64-apple-ios-simulator.swiftmodule
macOS version
26.5.2
Tuist version
4.202.5
Xcode version
27 beta 3
Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.
Start a Flight and preserve its objective, outcome, and session here.
We hit the same class of failure moving to Xcode 27. Here’s the manifest-level workaround that worked for us and survives regeneration.
The important part: PackageSettings.baseSettings alone is not enough. That only sets the project-level value on the generated dependency projects — SwiftPM re-asserts each package’s own minimum at the target build configuration, which overrides it. You have to floor the affected targets explicitly as well:
// Tuist/Package.swift
#if TUIST
import ProjectDescription
// Xcode 27 rejects any IPHONEOS_DEPLOYMENT_TARGET below 15.0. Generated SPM dependency
// targets that declare (or default to) iOS 12/13 fail the build. `baseSettings` sets the
// project-level floor; SPM re-asserts each package's own minimum at the target level, so
// every affected target has to be floored there too.
private let targetsBelowiOS15: [String] = [
"SomeDependencyTarget",
"AnotherDependencyTarget",
// ...
]
let packageSettings = PackageSettings(
baseSettings: .settings(base: ["IPHONEOS_DEPLOYMENT_TARGET": "15.0"]),
targetSettings: Dictionary(
uniqueKeysWithValues: targetsBelowiOS15.map { name in
(name, ["IPHONEOS_DEPLOYMENT_TARGET": "15.0"] as SettingsDictionary)
}
)
)
#endif
To collect the target names, run tuist install && tuist generate, then grep the generated dependency projects:
grep -rho 'IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*' \
Tuist/.build/tuist-derived/*/*.xcodeproj/project.pbxproj | sort -u
Anything below 15 needs to go in the list. This got our project building green under Xcode 27, and because it lives in the manifest it holds across tuist generate — unlike a post-generation pbxproj patch, which also defeats binary caching.
The downside is that targetSettings is keyed by exact target name with no globbing, so the list is hand-maintained: adding a dependency that declares a sub-15 minimum breaks the build again. At least it fails loudly with a clear message.
Hi @diegodossantos95, unfortunately, that workaround doesn’t work for my project or the demo project. I suspect the issue is around using a static framework 🤔 When I collect the list of targets, it already says each target uses 15.0, so adding the targetSettings doesn’t update it.
find Tuist/.build/tuist-derived -name project.pbxproj -path '*.xcodeproj/*' -print0 \
| xargs -0 grep -H 'IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*' \
| sed -E 's|.*/([^/]+)\.xcodeproj/project\.pbxproj:[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = ([0-9.]+);|\1: \2|' \
| sort -u
Adyen: 15.0
AdyenNetworking: 15.0
JOSESwift: 15.0
swiftui-introspect: 15.0
I was able to reproduce this with Xcode 27.0 (build 27A5228h) and Tuist 4.200.5 using the demo project.
Root cause
The Checkout3DS.xcframework has MinimumOSVersion: 13.0 in its Info.plist, and its .swiftinterface embeds -target arm64-apple-ios13.0-simulator in the swift-module-flags. The swiftinterface imports JOSESwift (line 10 of the interface file).
When Xcode 27 compiles the swiftinterface via SwiftExplicitDependencyCompileModuleFromInterface, it uses the binary framework’s own deployment target (iOS 13.0) rather than the consuming target’s deployment target. JOSESwift is correctly compiled by Tuist with an iOS 15.0 deployment target (the oldestVersions(for:) function in PackageInfoMapper returns iOS 15.0 for Swift 6.4). Since 13.0 < 15.0, the compilation fails.
The Checkout3DS binary was built with -enable-library-evolution, so its original deployment target should not limit consumers to iOS 13.0. The framework is deployment-target agnostic at the binary level.
What changed
Xcode 27 enforces deployment target compatibility during explicit module compilation of binary framework swiftinterfaces more strictly than previous Xcode versions, which did not flag this mismatch.
Reproduction output
[x] Checkout3DS.framework/Modules/Checkout3DS.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface:11:8:
compiling for iOS 13.0, but module 'JOSESwift' has a minimum deployment target of iOS 15.0
Failing build command: SwiftExplicitDependencyCompileModuleFromInterface arm64
Suggestion
The ideal fix is on Apple’s side: SwiftExplicitDependencyCompileModuleFromInterface should use the consuming target’s deployment target, not the binary framework’s original build target. Since the framework uses library evolution, the embedded -target arm64-apple-ios13.0-simulator is informational and should not constrain downstream consumers.
As a Tuist-side workaround, we could bump the MinimumOSVersion in binary xcframework Info.plist files during tuist install or tuist generate to at least the minimum deployment target supported by the active Xcode version (iOS 15.0 for Xcode 27). This is a project-side workaround though, and the underlying Xcode behavior should be addressed.