Support Xcode 27 in Tuist-generated projects (#7208)

* ci: conditionally bump SDK deployment targets for Xcode 27 SDK

Add [sdk=*27*] conditional deployment-target overrides to RevenueCat.xcodeproj
so Carthage from-source builds compile at the Xcode-27-required floors
(iOS 15 / macOS 12 / tvOS 15 / watchOS 8) while older Xcode keeps building at
iOS 13. Lets integrators get the right floor automatically based on their Xcode.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: bump watchOS Xcode 27 SDK override to 9.0

Xcode 27 requires a watchOS deployment target of at least 9.0 (and drops the
deprecated armv7k arch at that floor), so raise the [sdk=watch*27*] conditional
override from 8.0 to 9.0.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: add Carthage installation test job on Xcode 27

Add installation-tests-carthage-xcode-27 and extract the shared steps into a
reusable run-carthage-installation-tests command. Declare conditional
[sdk=*27*] deployment targets on the Carthage consumer app to match the SDK
project so the from-source build works on Xcode 27.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: unify Carthage installation tests into a parameterized job

Replace the duplicated installation-tests-carthage and
installation-tests-carthage-xcode-27 jobs (and the shared command) with a
single parameterized job invoked twice via a `name` override, and teach the
on-demand generator to emit the parameterized invocation for the Xcode 27
variant.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: don't require Xcode 27 Carthage job for releases

Xcode 27 is still beta, so the installation-tests-carthage-xcode-27 job still
runs on release branches but no longer blocks the all-tests-succeeded gate.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: run automatic Carthage installation tests on Xcode 26

Xcode 26 still builds RevenueCat.xcodeproj at its declared deployment
targets (its SDK floors are iOS 12 / macOS 10.13 / tvOS 12 / watchOS 4 /
visionOS 1), so the from-source build needs no overrides there. Move the
automatic installation-tests-carthage job from Xcode 16 to Xcode 26, which
is a current App-Store-valid toolchain.

The Xcode-27-only floor bump (iOS 15 / macOS 12 / tvOS 15 / watchOS 9)
remains covered by the [sdk=*27*] overrides, and the
installation-tests-carthage-xcode-27 variant stays available as a
manual-only on-demand job instead of running automatically in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: run Carthage installation tests on iOS 26 simulator

fastlane scan was defaulting to the pre-installed iOS 18.6 simulator, whose
networking hangs under Xcode 26.5's CoreSimulator, causing testCanFetchCustomerInfo
to deterministically time out. Pin the device to iPhone 17 to force an iOS 26.x runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: only pin Carthage test simulator on Xcode 26

Scope the iPhone 17 device override to Xcode 26 (whose CI image otherwise defaults
scan to a stale iOS 18 simulator with broken networking); newer Xcodes already
auto-select a matching runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: support Xcode 27 in Tuist-generated projects

Add SDK-conditional [sdk=*27*] deployment-target overrides to Tuist
manifests so the generated workspace builds on Xcode 27 (which enforces
iOS 15 / tvOS 15 / watchOS 9 / macOS 12 minimums) while leaving the base
deployment targets untouched for every other SDK. No environment flag,
mirroring the RevenueCat.xcodeproj mechanism.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: cover remaining sub-floor Tuist projects for Xcode 27

Apply the Xcode 27 SDK-conditional deployment-target overrides to the
remaining workspace projects with sub-floor minimums: APITesters,
BinarySizeTest, PurchaseTester and XCFrameworkInstallationTests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: drop version from Xcode deployment-target override naming

Rename addingXcode27DeploymentTargetOverrides to
addingXcodeDeploymentTargetOverrides (and related identifiers/file) so the
helper can accumulate future Xcode SDK floors without renaming.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: derive dependency lists from a single source in Package.swift

Write the wrapped test dependency names once and derive both productTypes
and the deployment-target targetSettings from it, instead of repeating the
list in two places.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: clarify productType grouping and trim override doc comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: scope iOS deployment floor to macOS SDK for Mac Catalyst

Mac Catalyst builds against the macOS SDK but derive their minimum from the
iOS deployment target (ignoring MACOSX_DEPLOYMENT_TARGET), so the Xcode 27 iOS
floor must also be scoped to [sdk=macosx27*] for the Catalyst build validation.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antonio PallaresandCursor authored and GitHub committed 2026-07-21 12:19:44 +02:00
1 parent 3a8178a34c
commit 454440d5b9
10 files changed
+135 -27

No files matched your search

+1 -1
View File
@@ -92,7 +92,7 @@ let project = Project(
],
settings: .framework
),
],
].addingXcodeDeploymentTargetOverrides(),
schemes: [
.scheme(
name: "PurchaseTester",
+1 -1
View File
@@ -136,7 +136,7 @@ let project = Project(
],
metadata: .metadata(tags: ["APITester"])
)
],
].addingXcodeDeploymentTargetOverrides(),
schemes: [
.scheme(
name: "All API Tests",
+1 -1
View File
@@ -148,7 +148,7 @@ let project = Project(
defaultSettings: .essential
)
)
],
].addingXcodeDeploymentTargetOverrides(),
schemes: [
.scheme(
name: "BinarySizeTest",
+1 -1
View File
@@ -73,7 +73,7 @@ let project = Project(
]
)
],
].addingXcodeDeploymentTargetOverrides(),
schemes: [
.scheme(
name: "RevenueCat",
+1 -1
View File
@@ -296,7 +296,7 @@ let project = Project(
metadata: .metadata(tags: ["RevenueCatTests"])
)
],
].addingXcodeDeploymentTargetOverrides(),
schemes: [
.scheme(
+1 -1
View File
@@ -24,7 +24,7 @@ let project = Project(
base: ([:] as SettingsDictionary).appendingTuistSwiftConditions()
)
)
],
].addingXcodeDeploymentTargetOverrides(),
schemes: [
.scheme(
name: "RevenueCatUI",
+1 -1
View File
@@ -37,7 +37,7 @@ let project = Project(
.target(name: "RulesEngineInternal")
]
)
],
].addingXcodeDeploymentTargetOverrides(),
schemes: [
.scheme(
name: "RulesEngineInternal",
@@ -74,7 +74,7 @@ let project = Project(
],
settings: .appTarget
)
],
].addingXcodeDeploymentTargetOverrides(),
schemes: [
.scheme(
name: "XCFrameworkInstallationTests",
+42 -19
View File
@@ -13,26 +13,49 @@ if !includeTestDependencies {
#if TUIST
import ProjectDescription
// Test dependencies that must be built as dynamic frameworks to fix
// "Attempted to report a test failure to XCTest while no test case was running"
// (see https://github.com/Quick/Nimble/issues/1101). These also declare deployment minimums
// below the floors newer Xcode SDKs enforce, so they receive the overrides below.
let testFrameworkDependencies = [
"Nimble", "NimbleObjectiveC",
"CwlPreconditionTesting", "CwlPosixPreconditionTesting",
"CwlCatchException", "CwlMachBadInstructionHandler",
"SnapshotTesting", "OHHTTPStubs", "OHHTTPStubsSwift"
]
// Our own frameworks and their binary dependencies, built as dynamic frameworks (the default for
// several of these would otherwise be `.staticFramework`).
var productTypes: [String: ProjectDescription.Product] = [
"RevenueCat": .framework,
"RevenueCatUI": .framework,
"Purchases": .framework,
"GoogleMobileAds": .framework
]
testFrameworkDependencies.forEach { productTypes[$0] = .framework }
// SDK-conditional deployment-target overrides, mirroring `Target+DeploymentTargetOverrides.swift`.
// Applied per target (target level), which overrides the deployment target Tuist derives from each
// package's platforms only when building against a newer Xcode SDK. `CwlCatchExceptionSupport` is a
// transitive Cwl target that is built (so it needs the override) but isn't forced to a framework.
let xcodeDeploymentTargetOverrides: SettingsDictionary = [
"IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos27*]": "15.0",
"IPHONEOS_DEPLOYMENT_TARGET[sdk=iphonesimulator27*]": "15.0",
// Mac Catalyst derives its minimum from the iOS deployment target under the macOS SDK.
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx27*]": "15.0",
"TVOS_DEPLOYMENT_TARGET[sdk=appletvos27*]": "15.0",
"TVOS_DEPLOYMENT_TARGET[sdk=appletvsimulator27*]": "15.0",
"WATCHOS_DEPLOYMENT_TARGET[sdk=watchos27*]": "9.0",
"WATCHOS_DEPLOYMENT_TARGET[sdk=watchsimulator27*]": "9.0",
"MACOSX_DEPLOYMENT_TARGET[sdk=macosx27*]": "12.0"
]
let deploymentOverrideTargets = testFrameworkDependencies + ["CwlCatchExceptionSupport"]
let packageSettings = PackageSettings(
productTypes: [
// Nimble and its dependencies must be dynamic frameworks
// to fix "Attempted to report a test failure to XCTest while no test case was running"
// See: https://github.com/Quick/Nimble/issues/1101
"Nimble": .framework,
"NimbleObjectiveC": .framework,
"CwlPreconditionTesting": .framework,
"CwlPosixPreconditionTesting": .framework,
"CwlCatchException": .framework,
"CwlMachBadInstructionHandler": .framework,
// Other frameworks
"SnapshotTesting": .framework, // default is .staticFramework,
"RevenueCat": .framework,
"RevenueCatUI": .framework,
"Purchases": .framework,
"GoogleMobileAds": .framework,
"OHHTTPStubs": .framework,
"OHHTTPStubsSwift": .framework
]
productTypes: productTypes,
targetSettings: deploymentOverrideTargets.reduce(into: [:]) { result, target in
result[target] = .settings(base: xcodeDeploymentTargetOverrides)
}
)
#endif
@@ -0,0 +1,85 @@
//
// Target+DeploymentTargetOverrides.swift
//
// Created by Antonio Pallares.
//
import ProjectDescription
extension Target {
/// Returns a copy of the target with SDK-conditional deployment-target overrides.
///
/// Newer Xcode SDKs raise the minimum deployment target they'll build (e.g. Xcode 27 enforces
/// iOS 15 / tvOS 15 / watchOS 9 / macOS 12). Instead of raising the SDK's true minimums everywhere
/// (which would drop support for older OSes on every Xcode), this adds `[sdk=*]`-conditional
/// overrides that only take effect when building against those newer SDKs. The base deployment
/// target — and every other SDK — is left untouched, so a single generated workspace keeps
/// building across Xcode versions with no environment flag. Add future SDK floors here.
///
/// It is safe to apply to every target: only platforms whose deployment target is *below* a floor
/// are raised, so targets already at or above the floor are returned unchanged and no platform is
/// ever lowered.
public func addingXcodeDeploymentTargetOverrides() -> Target {
guard let deploymentTargets else { return self }
var overrides: SettingsDictionary = [:]
func raiseToFloor(_ current: String?, floor: String, deviceSDK: String, simulatorSDK: String, buildSetting: String) {
guard let current, current.rc_isVersionBelow(floor) else { return }
overrides["\(buildSetting)[sdk=\(deviceSDK)27*]"] = .string(floor)
overrides["\(buildSetting)[sdk=\(simulatorSDK)27*]"] = .string(floor)
}
// Xcode 27 floors.
raiseToFloor(deploymentTargets.iOS, floor: "15.0",
deviceSDK: "iphoneos", simulatorSDK: "iphonesimulator",
buildSetting: "IPHONEOS_DEPLOYMENT_TARGET")
raiseToFloor(deploymentTargets.tvOS, floor: "15.0",
deviceSDK: "appletvos", simulatorSDK: "appletvsimulator",
buildSetting: "TVOS_DEPLOYMENT_TARGET")
raiseToFloor(deploymentTargets.watchOS, floor: "9.0",
deviceSDK: "watchos", simulatorSDK: "watchsimulator",
buildSetting: "WATCHOS_DEPLOYMENT_TARGET")
if let iOS = deploymentTargets.iOS, iOS.rc_isVersionBelow("15.0") {
overrides["IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx27*]"] = "15.0"
}
// macOS exposes a single SDK name (no separate simulator), matching the committed .xcodeproj.
if let macOS = deploymentTargets.macOS, macOS.rc_isVersionBelow("12.0") {
overrides["MACOSX_DEPLOYMENT_TARGET[sdk=macosx27*]"] = "12.0"
}
guard !overrides.isEmpty else { return self }
var target = self
var settings = target.settings ?? .settings()
settings.base = settings.base.merging(overrides)
target.settings = settings
return target
}
}
extension [Target] {
/// Applies ``Target/addingXcodeDeploymentTargetOverrides()`` to every target in the array.
public func addingXcodeDeploymentTargetOverrides() -> [Target] {
map { $0.addingXcodeDeploymentTargetOverrides() }
}
}
private extension String {
/// Compares dotted version strings (e.g. "13.0" < "15.0") numerically, component by component.
func rc_isVersionBelow(_ other: String) -> Bool {
let lhs = split(separator: ".").map { Int($0) ?? 0 }
let rhs = other.split(separator: ".").map { Int($0) ?? 0 }
for index in 0..<Swift.max(lhs.count, rhs.count) {
let left = index < lhs.count ? lhs[index] : 0
let right = index < rhs.count ? rhs[index] : 0
if left != right { return left < right }
}
return false
}
}