From 5b4d063c7b987ee5bf1c4930508a047ac93d3ed9 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 17 Sep 2026 01:31:48 +0300 Subject: [PATCH] fix(cli): support Xcode's JSON project format (project.xcproj) Fixes #8607 --- cli/src/ios/common.ts | 38 +++- cli/src/tasks/migrate-uiscene.ts | 16 +- cli/src/tasks/migrate.ts | 16 +- .../fixtures/xcproj/default/project.xcproj | 171 +++++++++++++++++ .../fixtures/xcproj/per-config/project.xcproj | 173 ++++++++++++++++++ cli/test/ios-project-file.spec.ts | 111 +++++++++++ 6 files changed, 508 insertions(+), 17 deletions(-) create mode 100644 cli/test/fixtures/xcproj/default/project.xcproj create mode 100644 cli/test/fixtures/xcproj/per-config/project.xcproj create mode 100644 cli/test/ios-project-file.spec.ts diff --git a/cli/src/ios/common.ts b/cli/src/ios/common.ts index 779dafd90..4752f4d92 100644 --- a/cli/src/ios/common.ts +++ b/cli/src/ios/common.ts @@ -1,5 +1,5 @@ import { execSync } from 'child_process'; -import { readFile, readFileSync, writeFile } from 'fs-extra'; +import { existsSync, readFile, readFileSync, writeFile, writeFileSync } from 'fs-extra'; import { join, resolve } from 'path'; import c from '../colors'; @@ -105,7 +105,7 @@ export async function editProjectSettingsIOS(config: Config): Promise { const appId = config.app.appId; const appName = config.app.appName.replace(/&/g, '&').replace(//g, '>'); - const pbxPath = `${config.ios.nativeXcodeProjDirAbs}/project.pbxproj`; + const projectPath = getXcodeProjectFile(config); const plistPath = resolve(config.ios.nativeTargetDirAbs, 'Info.plist'); let plistContent = await readFile(plistPath, { encoding: 'utf-8' }); @@ -115,15 +115,41 @@ export async function editProjectSettingsIOS(config: Config): Promise { `CFBundleDisplayName\n ${appName}`, ); - let pbxContent = await readFile(pbxPath, { encoding: 'utf-8' }); - pbxContent = pbxContent.replace(/PRODUCT_BUNDLE_IDENTIFIER = ([^;]+)/g, `PRODUCT_BUNDLE_IDENTIFIER = ${appId}`); + let projectContent = await readFile(projectPath, { encoding: 'utf-8' }); + projectContent = projectPath.endsWith('.xcproj') + ? projectContent.replace(xcprojSettingPattern('PRODUCT_BUNDLE_IDENTIFIER'), `$1${appId}$3`) + : projectContent.replace(/PRODUCT_BUNDLE_IDENTIFIER = ([^;]+)/g, `PRODUCT_BUNDLE_IDENTIFIER = ${appId}`); await writeFile(plistPath, plistContent, { encoding: 'utf-8' }); - await writeFile(pbxPath, pbxContent, { encoding: 'utf-8' }); + await writeFile(projectPath, projectContent, { encoding: 'utf-8' }); +} + +export function getXcodeProjectFile(config: Config): string { + const pbxprojPath = join(config.ios.nativeXcodeProjDirAbs, 'project.pbxproj'); + const xcprojPath = join(config.ios.nativeXcodeProjDirAbs, 'project.xcproj'); + return !existsSync(pbxprojPath) && existsSync(xcprojPath) ? xcprojPath : pbxprojPath; +} + +function xcprojSettingPattern(name: string): RegExp { + return new RegExp(`("${name}(?:\\[[^\\]]*\\])?"\\s*:\\s*")([^"]*)(")`, 'g'); +} + +export function setXcprojDeploymentTarget(projectFile: string, version: string): boolean { + const content = readFileSync(projectFile, 'utf-8'); + const updated = content.replace(xcprojSettingPattern('IPHONEOS_DEPLOYMENT_TARGET'), `$1${version}$3`); + writeFileSync(projectFile, updated, 'utf-8'); + return updated !== content; } export function getMajoriOSVersion(config: Config): string { - const pbx = readFileSync(join(config.ios.nativeXcodeProjDirAbs, 'project.pbxproj'), 'utf-8'); + const projectFile = getXcodeProjectFile(config); + const pbx = readFileSync(projectFile, 'utf-8'); + if (projectFile.endsWith('.xcproj')) { + const majors = [...pbx.matchAll(xcprojSettingPattern('IPHONEOS_DEPLOYMENT_TARGET'))].map((match) => + parseInt(match[2], 10), + ); + return majors.length > 0 ? String(Math.min(...majors)) : ''; + } const searchString = 'IPHONEOS_DEPLOYMENT_TARGET = '; const iosVersion = pbx.substring( pbx.indexOf(searchString) + searchString.length, diff --git a/cli/src/tasks/migrate-uiscene.ts b/cli/src/tasks/migrate-uiscene.ts index 9f6dfa278..8ef320adf 100644 --- a/cli/src/tasks/migrate-uiscene.ts +++ b/cli/src/tasks/migrate-uiscene.ts @@ -3,6 +3,7 @@ import { join, sep } from 'path'; import { runTask } from '../common'; import type { Config } from '../definitions'; +import { getXcodeProjectFile } from '../ios/common'; import { logger } from '../log'; import { deleteFolderRecursive, readdirp } from '../util/fs'; import { addSceneManifestIfNeeded, hasSceneManifest } from '../util/spm'; @@ -63,9 +64,20 @@ export async function migrateToUIScene(config: Config): Promise { }); await runTask('Registering SceneDelegate.swift with the Xcode App target.', async () => { - const pbxprojPath = join(config.ios.nativeXcodeProjDirAbs, 'project.pbxproj'); + const projectFile = getXcodeProjectFile(config); + if (projectFile.endsWith('.xcproj')) { + if (readFileSync(projectFile, 'utf-8').includes('"SceneDelegate.swift"')) { + logger.warn('SceneDelegate.swift is already registered in the App target, skipping.'); + } else { + logger.warn( + 'Could not register SceneDelegate.swift automatically in a project.xcproj project. ' + + 'Add SceneDelegate.swift to the App target in Xcode manually.', + ); + } + return; + } try { - const { added } = addSwiftFileToAppTarget(pbxprojPath, 'App', 'SceneDelegate.swift'); + const { added } = addSwiftFileToAppTarget(projectFile, 'App', 'SceneDelegate.swift'); if (!added) { logger.warn('SceneDelegate.swift is already registered in the App target, skipping.'); } diff --git a/cli/src/tasks/migrate.ts b/cli/src/tasks/migrate.ts index eec9673d5..aefa79fff 100644 --- a/cli/src/tasks/migrate.ts +++ b/cli/src/tasks/migrate.ts @@ -7,7 +7,7 @@ import c from '../colors'; import { getCoreVersion, runTask, checkJDKMajorVersion } from '../common'; import type { Config } from '../definitions'; import { fatal } from '../errors'; -import { getMajoriOSVersion } from '../ios/common'; +import { getMajoriOSVersion, getXcodeProjectFile, setXcprojDeploymentTarget } from '../ios/common'; import { logger, logPrompt, logSuccess } from '../log'; import { deleteFolderRecursive } from '../util/fs'; import { runCommand } from '../util/subprocess'; @@ -152,14 +152,12 @@ export async function migrateCommand(config: Config, noprompt: boolean, packagem const currentiOSVersion = getMajoriOSVersion(config); if (parseInt(currentiOSVersion) < parseInt(iOSVersion)) { // ios template changes - await runTask(`Migrating deployment target to ${iOSVersion}.0.`, () => { - return updateFile( - config, - join(config.ios.nativeXcodeProjDirAbs, 'project.pbxproj'), - 'IPHONEOS_DEPLOYMENT_TARGET = ', - ';', - `${iOSVersion}.0`, - ); + await runTask(`Migrating deployment target to ${iOSVersion}.0.`, async () => { + const projectFile = getXcodeProjectFile(config); + if (projectFile.endsWith('.xcproj')) { + return setXcprojDeploymentTarget(projectFile, `${iOSVersion}.0`); + } + return updateFile(config, projectFile, 'IPHONEOS_DEPLOYMENT_TARGET = ', ';', `${iOSVersion}.0`); }); if ((await config.ios.packageManager) !== 'SPM') { diff --git a/cli/test/fixtures/xcproj/default/project.xcproj b/cli/test/fixtures/xcproj/default/project.xcproj new file mode 100644 index 000000000..7b67ad41d --- /dev/null +++ b/cli/test/fixtures/xcproj/default/project.xcproj @@ -0,0 +1,171 @@ +{ + "build-independent-targets-in-parallel": false, + "default-configuration": "Release", + "configurations": [ + { "name": "Debug", "file": "debug.xcconfig" }, + "Release", + ], + "localizations": { + "development": "en", + "supported": [ + "Base", + ], + }, + "packages": [ + { + "kind": "local", + "path": "CapApp-SPM", + }, + ], + "files": [ + { "path": "/../debug.xcconfig" }, + { + "kind": "group", + "path": "App", + "children": [ + { "path": "SceneDelegate.swift", "target-membership": [ "App/compile-sources" ] }, + { "path": "capacitor.config.json", "encoding": "utf8", "target-membership": [ "App/resources" ] }, + { "path": "AppDelegate.swift", "target-membership": [ "App/compile-sources" ] }, + { + "kind": "variant-group", + "name": "Main.storyboard", + "target-membership": [ + "App/resources", + ], + "children": [ + { + "path": "Base.lproj/Main.storyboard", + }, + ], + }, + { "path": "Assets.xcassets", "target-membership": [ "App/resources" ] }, + { + "kind": "variant-group", + "name": "LaunchScreen.storyboard", + "target-membership": [ + "App/resources", + ], + "children": [ + { + "path": "Base.lproj/LaunchScreen.storyboard", + }, + ], + }, + { "path": "Info.plist" }, + { "path": "config.xml", "target-membership": [ "App/resources" ] }, + { "path": "public", "target-membership": [ "App/resources" ] }, + ], + }, { + "kind": "group", + "name": "Products", + "children": [ + { "path": "/App.app", "id": "504EC3041FED79650016851F", "type": "wrapper.application", "index": false }, + ], + }, + ], + "targets": [ + { + "name": "App", + "id": "504EC3031FED79650016851F", + "product": "Products/App.app", + "product-type": "application", + "last-swift-migration": "11.0", + "legacy-provisioning-style": "automatic", + "specialized-configurations": [ + { "name": "Debug", "file": "debug.xcconfig" }, + ], + "build-phases": [ + "compile-sources", + "frameworks", + "resources", + ], + "package-product-members": [ + { + "package": "CapApp-SPM", + "product-name": "CapApp-SPM", + "build-phase": { "build-phase": "frameworks" }, + }, + ], + "build-settings": { + "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", + "CODE_SIGN_STYLE": "Automatic", + "CURRENT_PROJECT_VERSION": "1", + "INFOPLIST_FILE": "App/Info.plist", + "IPHONEOS_DEPLOYMENT_TARGET": "15.0", + "LD_RUNPATH_SEARCH_PATHS": [ + "$(inherited)", + "@executable_path/Frameworks", + ], + "MARKETING_VERSION": "1.0", + "OTHER_SWIFT_FLAGS[config=Debug]": "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"", + "PRODUCT_BUNDLE_IDENTIFIER": "com.getcapacitor.App", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]": "DEBUG", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Release]": "", + "SWIFT_VERSION": "5.0", + "TARGETED_DEVICE_FAMILY": "1,2", + }, + }, + ], + "build-settings": { + "ALWAYS_SEARCH_USER_PATHS": "NO", + "CLANG_ANALYZER_NONNULL": "YES", + "CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION": "YES_AGGRESSIVE", + "CLANG_CXX_LANGUAGE_STANDARD": "gnu++14", + "CLANG_CXX_LIBRARY": "libc++", + "CLANG_ENABLE_MODULES": "YES", + "CLANG_ENABLE_OBJC_ARC": "YES", + "CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING": "YES", + "CLANG_WARN_BOOL_CONVERSION": "YES", + "CLANG_WARN_COMMA": "YES", + "CLANG_WARN_CONSTANT_CONVERSION": "YES", + "CLANG_WARN_DIRECT_OBJC_ISA_USAGE": "YES_ERROR", + "CLANG_WARN_DOCUMENTATION_COMMENTS": "YES", + "CLANG_WARN_EMPTY_BODY": "YES", + "CLANG_WARN_ENUM_CONVERSION": "YES", + "CLANG_WARN_INFINITE_RECURSION": "YES", + "CLANG_WARN_INT_CONVERSION": "YES", + "CLANG_WARN_NON_LITERAL_NULL_CONVERSION": "YES", + "CLANG_WARN_OBJC_LITERAL_CONVERSION": "YES", + "CLANG_WARN_OBJC_ROOT_CLASS": "YES_ERROR", + "CLANG_WARN_RANGE_LOOP_ANALYSIS": "YES", + "CLANG_WARN_STRICT_PROTOTYPES": "YES", + "CLANG_WARN_SUSPICIOUS_MOVE": "YES", + "CLANG_WARN_UNGUARDED_AVAILABILITY": "YES_AGGRESSIVE", + "CLANG_WARN_UNREACHABLE_CODE": "YES", + "CLANG_WARN__DUPLICATE_METHOD_MATCH": "YES", + "CODE_SIGN_IDENTITY": "iPhone Developer", + "COPY_PHASE_STRIP": "NO", + "DEBUG_INFORMATION_FORMAT[config=Debug]": "dwarf", + "DEBUG_INFORMATION_FORMAT[config=Release]": "dwarf-with-dsym", + "ENABLE_NS_ASSERTIONS[config=Release]": "NO", + "ENABLE_STRICT_OBJC_MSGSEND": "YES", + "ENABLE_TESTABILITY[config=Debug]": "YES", + "GCC_C_LANGUAGE_STANDARD": "gnu11", + "GCC_DYNAMIC_NO_PIC[config=Debug]": "NO", + "GCC_NO_COMMON_BLOCKS": "YES", + "GCC_OPTIMIZATION_LEVEL[config=Debug]": "0", + "GCC_PREPROCESSOR_DEFINITIONS[config=Debug]": [ + "DEBUG=1", + "$(inherited)", + ], + "GCC_WARN_64_TO_32_BIT_CONVERSION": "YES", + "GCC_WARN_ABOUT_RETURN_TYPE": "YES_ERROR", + "GCC_WARN_UNDECLARED_SELECTOR": "YES", + "GCC_WARN_UNINITIALIZED_AUTOS": "YES_AGGRESSIVE", + "GCC_WARN_UNUSED_FUNCTION": "YES", + "GCC_WARN_UNUSED_VARIABLE": "YES", + "IPHONEOS_DEPLOYMENT_TARGET": "15.0", + "MTL_ENABLE_DEBUG_INFO[config=Debug]": "YES", + "MTL_ENABLE_DEBUG_INFO[config=Release]": "NO", + "ONLY_ACTIVE_ARCH[config=Debug]": "YES", + "SDKROOT": "iphoneos", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]": "DEBUG", + "SWIFT_COMPILATION_MODE[config=Release]": "wholemodule", + "SWIFT_OPTIMIZATION_LEVEL[config=Debug]": "-Onone", + "SWIFT_OPTIMIZATION_LEVEL[config=Release]": "-O", + "VALIDATE_PRODUCT[config=Release]": "YES", + }, + "last-upgrade": "9.2", + "last-swift-update": "9.2", +} diff --git a/cli/test/fixtures/xcproj/per-config/project.xcproj b/cli/test/fixtures/xcproj/per-config/project.xcproj new file mode 100644 index 000000000..a30c2cd2a --- /dev/null +++ b/cli/test/fixtures/xcproj/per-config/project.xcproj @@ -0,0 +1,173 @@ +{ + "build-independent-targets-in-parallel": false, + "default-configuration": "Release", + "configurations": [ + { "name": "Debug", "file": "debug.xcconfig" }, + "Release", + ], + "localizations": { + "development": "en", + "supported": [ + "Base", + ], + }, + "packages": [ + { + "kind": "local", + "path": "CapApp-SPM", + }, + ], + "files": [ + { "path": "/../debug.xcconfig" }, + { + "kind": "group", + "path": "App", + "children": [ + { "path": "SceneDelegate.swift", "target-membership": [ "App/compile-sources" ] }, + { "path": "capacitor.config.json", "encoding": "utf8", "target-membership": [ "App/resources" ] }, + { "path": "AppDelegate.swift", "target-membership": [ "App/compile-sources" ] }, + { + "kind": "variant-group", + "name": "Main.storyboard", + "target-membership": [ + "App/resources", + ], + "children": [ + { + "path": "Base.lproj/Main.storyboard", + }, + ], + }, + { "path": "Assets.xcassets", "target-membership": [ "App/resources" ] }, + { + "kind": "variant-group", + "name": "LaunchScreen.storyboard", + "target-membership": [ + "App/resources", + ], + "children": [ + { + "path": "Base.lproj/LaunchScreen.storyboard", + }, + ], + }, + { "path": "Info.plist" }, + { "path": "config.xml", "target-membership": [ "App/resources" ] }, + { "path": "public", "target-membership": [ "App/resources" ] }, + ], + }, { + "kind": "group", + "name": "Products", + "children": [ + { "path": "/App.app", "id": "504EC3041FED79650016851F", "type": "wrapper.application", "index": false }, + ], + }, + ], + "targets": [ + { + "name": "App", + "id": "504EC3031FED79650016851F", + "product": "Products/App.app", + "product-type": "application", + "last-swift-migration": "11.0", + "legacy-provisioning-style": "automatic", + "specialized-configurations": [ + { "name": "Debug", "file": "debug.xcconfig" }, + ], + "build-phases": [ + "compile-sources", + "frameworks", + "resources", + ], + "package-product-members": [ + { + "package": "CapApp-SPM", + "product-name": "CapApp-SPM", + "build-phase": { "build-phase": "frameworks" }, + }, + ], + "build-settings": { + "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", + "CODE_SIGN_STYLE": "Automatic", + "CURRENT_PROJECT_VERSION": "1", + "INFOPLIST_FILE": "App/Info.plist", + "IPHONEOS_DEPLOYMENT_TARGET[config=Debug]": "15.0", + "IPHONEOS_DEPLOYMENT_TARGET[config=Release]": "16.0", + "LD_RUNPATH_SEARCH_PATHS": [ + "$(inherited)", + "@executable_path/Frameworks", + ], + "MARKETING_VERSION": "1.0", + "OTHER_SWIFT_FLAGS[config=Debug]": "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"", + "PRODUCT_BUNDLE_IDENTIFIER": "com.getcapacitor.App", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]": "DEBUG", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Release]": "", + "SWIFT_VERSION": "5.0", + "TARGETED_DEVICE_FAMILY": "1,2", + }, + }, + ], + "build-settings": { + "ALWAYS_SEARCH_USER_PATHS": "NO", + "CLANG_ANALYZER_NONNULL": "YES", + "CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION": "YES_AGGRESSIVE", + "CLANG_CXX_LANGUAGE_STANDARD": "gnu++14", + "CLANG_CXX_LIBRARY": "libc++", + "CLANG_ENABLE_MODULES": "YES", + "CLANG_ENABLE_OBJC_ARC": "YES", + "CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING": "YES", + "CLANG_WARN_BOOL_CONVERSION": "YES", + "CLANG_WARN_COMMA": "YES", + "CLANG_WARN_CONSTANT_CONVERSION": "YES", + "CLANG_WARN_DIRECT_OBJC_ISA_USAGE": "YES_ERROR", + "CLANG_WARN_DOCUMENTATION_COMMENTS": "YES", + "CLANG_WARN_EMPTY_BODY": "YES", + "CLANG_WARN_ENUM_CONVERSION": "YES", + "CLANG_WARN_INFINITE_RECURSION": "YES", + "CLANG_WARN_INT_CONVERSION": "YES", + "CLANG_WARN_NON_LITERAL_NULL_CONVERSION": "YES", + "CLANG_WARN_OBJC_LITERAL_CONVERSION": "YES", + "CLANG_WARN_OBJC_ROOT_CLASS": "YES_ERROR", + "CLANG_WARN_RANGE_LOOP_ANALYSIS": "YES", + "CLANG_WARN_STRICT_PROTOTYPES": "YES", + "CLANG_WARN_SUSPICIOUS_MOVE": "YES", + "CLANG_WARN_UNGUARDED_AVAILABILITY": "YES_AGGRESSIVE", + "CLANG_WARN_UNREACHABLE_CODE": "YES", + "CLANG_WARN__DUPLICATE_METHOD_MATCH": "YES", + "CODE_SIGN_IDENTITY": "iPhone Developer", + "COPY_PHASE_STRIP": "NO", + "DEBUG_INFORMATION_FORMAT[config=Debug]": "dwarf", + "DEBUG_INFORMATION_FORMAT[config=Release]": "dwarf-with-dsym", + "ENABLE_NS_ASSERTIONS[config=Release]": "NO", + "ENABLE_STRICT_OBJC_MSGSEND": "YES", + "ENABLE_TESTABILITY[config=Debug]": "YES", + "GCC_C_LANGUAGE_STANDARD": "gnu11", + "GCC_DYNAMIC_NO_PIC[config=Debug]": "NO", + "GCC_NO_COMMON_BLOCKS": "YES", + "GCC_OPTIMIZATION_LEVEL[config=Debug]": "0", + "GCC_PREPROCESSOR_DEFINITIONS[config=Debug]": [ + "DEBUG=1", + "$(inherited)", + ], + "GCC_WARN_64_TO_32_BIT_CONVERSION": "YES", + "GCC_WARN_ABOUT_RETURN_TYPE": "YES_ERROR", + "GCC_WARN_UNDECLARED_SELECTOR": "YES", + "GCC_WARN_UNINITIALIZED_AUTOS": "YES_AGGRESSIVE", + "GCC_WARN_UNUSED_FUNCTION": "YES", + "GCC_WARN_UNUSED_VARIABLE": "YES", + "IPHONEOS_DEPLOYMENT_TARGET[config=Debug]": "15.0", + "IPHONEOS_DEPLOYMENT_TARGET[config=Release]": "16.0", + "MTL_ENABLE_DEBUG_INFO[config=Debug]": "YES", + "MTL_ENABLE_DEBUG_INFO[config=Release]": "NO", + "ONLY_ACTIVE_ARCH[config=Debug]": "YES", + "SDKROOT": "iphoneos", + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]": "DEBUG", + "SWIFT_COMPILATION_MODE[config=Release]": "wholemodule", + "SWIFT_OPTIMIZATION_LEVEL[config=Debug]": "-Onone", + "SWIFT_OPTIMIZATION_LEVEL[config=Release]": "-O", + "VALIDATE_PRODUCT[config=Release]": "YES", + }, + "last-upgrade": "9.2", + "last-swift-update": "9.2", +} diff --git a/cli/test/ios-project-file.spec.ts b/cli/test/ios-project-file.spec.ts new file mode 100644 index 000000000..12e34a132 --- /dev/null +++ b/cli/test/ios-project-file.spec.ts @@ -0,0 +1,111 @@ +import { copyFileSync, mkdirpSync, readFileSync } from 'fs-extra'; +import { join, resolve } from 'path'; + +import type { Config } from '../src/definitions'; +import { + editProjectSettingsIOS, + getMajoriOSVersion, + getXcodeProjectFile, + setXcprojDeploymentTarget, +} from '../src/ios/common'; + +import { mktmp } from './util'; + +const REPO_ROOT = resolve(__dirname, '..', '..'); +const SHIPPED_TEMPLATE = resolve(REPO_ROOT, 'ios-spm-template/App'); +const XCPROJ = resolve(__dirname, 'fixtures/xcproj/default/project.xcproj'); +const XCPROJ_PER_CONFIG = resolve(__dirname, 'fixtures/xcproj/per-config/project.xcproj'); + +describe('Xcode project file', () => { + let tmpDir: any; + let projectDir: string; + let config: Config; + + beforeEach(async () => { + tmpDir = await mktmp(); + projectDir = join(tmpDir.path, 'App.xcodeproj'); + const targetDir = join(tmpDir.path, 'App'); + mkdirpSync(projectDir); + mkdirpSync(targetDir); + copyFileSync(join(SHIPPED_TEMPLATE, 'App', 'Info.plist'), join(targetDir, 'Info.plist')); + config = { + app: { appId: 'com.example.renamed', appName: 'Renamed' }, + ios: { nativeXcodeProjDirAbs: projectDir, nativeTargetDirAbs: targetDir }, + } as unknown as Config; + }); + + afterEach(() => { + tmpDir.cleanupCallback(); + }); + + const pbxprojPath = () => join(projectDir, 'project.pbxproj'); + const xcprojPath = () => join(projectDir, 'project.xcproj'); + const writePbxproj = () => copyFileSync(join(SHIPPED_TEMPLATE, 'App.xcodeproj', 'project.pbxproj'), pbxprojPath()); + const writeXcproj = (fixture = XCPROJ) => copyFileSync(fixture, xcprojPath()); + + it('uses project.pbxproj when the project has one', () => { + writePbxproj(); + writeXcproj(); + + expect(getXcodeProjectFile(config)).toBe(pbxprojPath()); + }); + + it('uses project.xcproj when the project only has that', () => { + writeXcproj(); + + expect(getXcodeProjectFile(config)).toBe(xcprojPath()); + }); + + it('reads the major deployment target from project.pbxproj', () => { + writePbxproj(); + + expect(getMajoriOSVersion(config)).toBe('15'); + }); + + it('reads the major deployment target from project.xcproj', () => { + writeXcproj(); + + expect(getMajoriOSVersion(config)).toBe('15'); + }); + + it('reads the lowest deployment target when project.xcproj sets one per configuration', () => { + writeXcproj(XCPROJ_PER_CONFIG); + + expect(getMajoriOSVersion(config)).toBe('15'); + }); + + it('sets every deployment target in project.xcproj', () => { + writeXcproj(XCPROJ_PER_CONFIG); + + expect(setXcprojDeploymentTarget(xcprojPath(), '17.0')).toBe(true); + + const content = readFileSync(xcprojPath(), 'utf-8'); + expect(content.match(/"IPHONEOS_DEPLOYMENT_TARGET\[config=(Debug|Release)\]": "17\.0"/g)).toHaveLength(4); + expect(content).not.toMatch(/"IPHONEOS_DEPLOYMENT_TARGET[^"]*": "1[56]\.0"/); + expect(getMajoriOSVersion(config)).toBe('17'); + }); + + it('sets the bundle identifier in project.xcproj and keeps the rest of the file', async () => { + writeXcproj(); + const original = readFileSync(XCPROJ, 'utf-8'); + + await editProjectSettingsIOS(config); + + expect(readFileSync(xcprojPath(), 'utf-8')).toBe( + original.replace( + '"PRODUCT_BUNDLE_IDENTIFIER": "com.getcapacitor.App"', + '"PRODUCT_BUNDLE_IDENTIFIER": "com.example.renamed"', + ), + ); + }); + + it('still sets the bundle identifier in project.pbxproj', async () => { + writePbxproj(); + + await editProjectSettingsIOS(config); + + const content = readFileSync(pbxprojPath(), 'utf-8'); + expect(content).toContain('PRODUCT_BUNDLE_IDENTIFIER = com.example.renamed;'); + expect(content).not.toContain('PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.App;'); + }); +});