From f0582cb1a4e4f869e45a090e7345494a0a5b0186 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Fri, 2 Oct 2026 15:54:47 +0200 Subject: [PATCH] feat(ios): let App_Resources/Catalyst/build.xcconfig override the iOS one A Catalyst build only read App_Resources/iOS/build.xcconfig, and passes the deployment target on the xcodebuild command line, so no xcconfig could give Catalyst a different one. The Catalyst file is now merged first, and the settings passed on the command line read it before the iOS one. Co-Authored-By: Claude Opus 5.5 --- lib/definitions/ios.d.ts | 1 + lib/services/ios-project-service.ts | 21 +++++++ lib/services/ios/xcodebuild-args-service.ts | 67 ++++++++++++--------- lib/services/project-changes-service.ts | 5 ++ 4 files changed, 66 insertions(+), 28 deletions(-) diff --git a/lib/definitions/ios.d.ts b/lib/definitions/ios.d.ts index 7940fc7e33..0b6dc63106 100644 --- a/lib/definitions/ios.d.ts +++ b/lib/definitions/ios.d.ts @@ -123,6 +123,7 @@ declare global { getXcodeProjectArgs( platformData: IPlatformData, projectData: IProjectData, + catalyst?: boolean, ): string[]; } diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 7a9ad87933..3125621e26 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -1864,6 +1864,27 @@ export class IOSProjectService // mergeFiles keeps whichever value is already present, so the app's // xcconfig is merged before any plugin's to make it authoritative: a // plugin must not be able to dictate a setting the app has chosen. + // A Mac Catalyst build merges App_Resources/Catalyst/build.xcconfig first, + // so it overrides the iOS one it otherwise shares. + if ( + this.$mobileHelper.isCatalystPlatform( + this.$options.platformOverride ?? "", + ) + ) { + const catalystXcconfigPath = path.join( + projectData.appResourcesDirectoryPath, + this.$devicePlatformsConstants.Catalyst, + BUILD_XCCONFIG_FILE_NAME, + ); + if (this.$fs.exists(catalystXcconfigPath)) { + for (const pluginsXcconfigFilePath of pluginsXcconfigFilePaths) { + await this.$xcconfigService.mergeFiles( + catalystXcconfigPath, + pluginsXcconfigFilePath, + ); + } + } + } const appResourcesXcconfigPath = path.join( projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, diff --git a/lib/services/ios/xcodebuild-args-service.ts b/lib/services/ios/xcodebuild-args-service.ts index 103f061e62..ae22d93375 100644 --- a/lib/services/ios/xcodebuild-args-service.ts +++ b/lib/services/ios/xcodebuild-args-service.ts @@ -56,7 +56,7 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { .concat( // the deployment target is re-added below, clamped to what Catalyst supports this - .getXcodeProjectArgs(platformData, projectData) + .getXcodeProjectArgs(platformData, projectData, true) .filter((arg) => !arg.startsWith("IPHONEOS_DEPLOYMENT_TARGET=")), ) .concat(this.getCatalystDeploymentTargetArgs(projectData)) @@ -192,6 +192,7 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { public getXcodeProjectArgs( platformData: IPlatformData, projectData: IProjectData, + catalyst = false, ): string[] { const xcworkspacePath = path.join( platformData.projectRoot, @@ -226,11 +227,13 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { ); } - const BUILD_SETTINGS_FILE_PATH = path.join( - projectData.appResourcesDirectoryPath, - platformData.normalizedPlatformName, - constants.BUILD_XCCONFIG_FILE_NAME, - ); + const readBuildSetting = (propertyName: string) => + this.readBuildSetting( + projectData, + platformData.normalizedPlatformName, + propertyName, + catalyst, + ); // Only include explicit properties from build.xcconfig // Note: we could include entire file via -xcconfig flag @@ -249,26 +252,17 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { // ref: https://forums.swift.org/t/xcode-26-unable-to-find-module-dependency/80516 const explicitModulesProperty = "SWIFT_ENABLE_EXPLICIT_MODULES"; const explicitModulesValue = - this.$xcconfigService.readPropertyValue( - BUILD_SETTINGS_FILE_PATH, - explicitModulesProperty, - ) || "NO"; + readBuildSetting(explicitModulesProperty) || "NO"; extraArgs.push(`${explicitModulesProperty}=${explicitModulesValue}`); const deployTargetProperty = "IPHONEOS_DEPLOYMENT_TARGET"; - const deployTargetVersion = this.$xcconfigService.readPropertyValue( - BUILD_SETTINGS_FILE_PATH, - deployTargetProperty, - ); + const deployTargetVersion = readBuildSetting(deployTargetProperty); if (deployTargetVersion) { extraArgs.push(`${deployTargetProperty}=${deployTargetVersion}`); } const swiftUIBootProperty = "NS_SWIFTUI_BOOT"; - const swiftUIBootValue = this.$xcconfigService.readPropertyValue( - BUILD_SETTINGS_FILE_PATH, - swiftUIBootProperty, - ); + const swiftUIBootValue = readBuildSetting(swiftUIBootProperty); if (swiftUIBootValue) { extraArgs.push(`${swiftUIBootProperty}=${swiftUIBootValue}`); } @@ -277,10 +271,7 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { // development team when building for a device. Pass DEVELOPMENT_TEAM as a // command-line build setting so it applies to SPM package targets too. const developmentTeamProperty = "DEVELOPMENT_TEAM"; - const developmentTeamValue = this.$xcconfigService.readPropertyValue( - BUILD_SETTINGS_FILE_PATH, - developmentTeamProperty, - ); + const developmentTeamValue = readBuildSetting(developmentTeamProperty); if (developmentTeamValue) { extraArgs.push(`${developmentTeamProperty}=${developmentTeamValue}`); } @@ -296,6 +287,28 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { return ["-project", xcodeprojPath, ...extraArgs]; } + /** On Mac Catalyst, App_Resources/Catalyst/build.xcconfig overrides the iOS one. */ + private readBuildSetting( + projectData: IProjectData, + platformName: string, + propertyName: string, + catalyst: boolean, + ): string { + const readFrom = (resourcesPlatformName: string) => + this.$xcconfigService.readPropertyValue( + path.join( + projectData.appResourcesDirectoryPath, + resourcesPlatformName, + constants.BUILD_XCCONFIG_FILE_NAME, + ), + propertyName, + ); + return ( + (catalyst && readFrom(this.$devicePlatformsConstants.Catalyst)) || + readFrom(platformName) + ); + } + private getBuildLoggingArgs(): string[] { return this.$logger.getLevel() === "INFO" ? ["-quiet"] : []; } @@ -306,16 +319,14 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { * rather than failing — the iOS build keeps whatever the app has chosen. * `MACCATALYST_DEPLOYMENT_TARGET` is passed alongside because the runtime's * metadata generator reads it and older runtimes crash when it is unset. + * App_Resources/Catalyst/build.xcconfig can set a higher target than iOS. */ private getCatalystDeploymentTargetArgs(projectData: IProjectData): string[] { - const buildSettingsFilePath = path.join( - projectData.appResourcesDirectoryPath, + const projectDeploymentTarget = this.readBuildSetting( + projectData, this.$devicePlatformsConstants.iOS, - constants.BUILD_XCCONFIG_FILE_NAME, - ); - const projectDeploymentTarget = this.$xcconfigService.readPropertyValue( - buildSettingsFilePath, "IPHONEOS_DEPLOYMENT_TARGET", + true, ); const minimum = XcodebuildArgsService.MIN_CATALYST_DEPLOYMENT_TARGET; let deploymentTarget = projectDeploymentTarget; diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index c8b11e2624..9504dd10b6 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -170,6 +170,11 @@ export class ProjectChangesService implements IProjectChangesService { path.join(platformResourcesDir, platformData.configurationFileName), path.join(platformResourcesDir, "LaunchScreen.storyboard"), path.join(platformResourcesDir, BUILD_XCCONFIG_FILE_NAME), + path.join( + projectData.appResourcesDirectoryPath, + this.$devicePlatformsConstants.Catalyst, + BUILD_XCCONFIG_FILE_NAME + ), ]); } else { this._changesInfo.configChanged = this.filesChanged([