From 93091f3c7131584d8820012a04ea8317ff95b825 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Thu, 29 Jan 2026 16:03:15 +0100 Subject: [PATCH] feat(cli): Add packageManager to iOS config --- cli/src/config.ts | 33 +++++++++++++++++++++------------ cli/src/definitions.ts | 2 ++ cli/src/index.ts | 15 ++++++++++++--- cli/src/ios/build.ts | 5 +---- cli/src/ios/common.ts | 11 +++++++++++ cli/src/ios/doctor.ts | 4 ++-- cli/src/ios/open.ts | 3 +-- cli/src/ios/run.ts | 5 ++--- cli/src/ios/update.ts | 25 ++++++++++--------------- cli/src/tasks/add.ts | 8 ++++---- cli/src/tasks/migrate-spm.ts | 14 ++++++++------ cli/src/tasks/migrate.ts | 3 +-- cli/src/tasks/sync.ts | 4 ++-- cli/src/tasks/update.ts | 27 ++++++++++++++++++++++++--- cli/src/util/spm.ts | 31 +++++++++++++++---------------- 15 files changed, 116 insertions(+), 74 deletions(-) diff --git a/cli/src/config.ts b/cli/src/config.ts index 216a15abee..79eb115c4b 100644 --- a/cli/src/config.ts +++ b/cli/src/config.ts @@ -1,5 +1,5 @@ import Debug from 'debug'; -import { pathExists, readFile, readJSON, writeFile, writeJSON } from 'fs-extra'; +import { existsSync, pathExists, readFile, readJSON, writeFile, writeJSON } from 'fs-extra'; import { dirname, extname, join, relative, resolve } from 'path'; import c from './colors'; @@ -22,7 +22,7 @@ import { formatJSObject } from './util/js'; import { findNXMonorepoRoot, isNXMonorepo } from './util/monorepotools'; import { requireTS, resolveNode } from './util/node'; import { lazy } from './util/promise'; -import { getCommandOutput } from './util/subprocess'; +import { getCommandOutput, isInstalled } from './util/subprocess'; const debug = Debug('capacitor:config'); @@ -273,7 +273,8 @@ async function loadIOSConfig(rootDir: string, extConfig: ExternalConfig): Promis const nativeXcodeProjDir = `${nativeProjectDir}/App.xcodeproj`; const nativeXcodeProjDirAbs = resolve(platformDirAbs, nativeXcodeProjDir); const nativeXcodeWorkspaceDirAbs = lazy(() => determineXcodeWorkspaceDirAbs(nativeProjectDirAbs)); - const podPath = lazy(() => determineGemfileOrCocoapodPath(rootDir, platformDirAbs, nativeProjectDirAbs)); + const podPath = lazy(() => determineCocoapodPath()); + const packageManager = lazy(() => determinePackageManager(rootDir, platformDirAbs, nativeProjectDirAbs)); const webDirAbs = lazy(() => determineIOSWebDirAbs(nativeProjectDirAbs, nativeTargetDirAbs, nativeXcodeProjDirAbs)); const cordovaPluginsDir = 'capacitor-cordova-ios-plugins'; const buildOptions = { @@ -301,6 +302,7 @@ async function loadIOSConfig(rootDir: string, extConfig: ExternalConfig): Promis webDir: lazy(async () => relative(platformDirAbs, await webDirAbs)), webDirAbs, podPath, + packageManager, buildOptions, }; } @@ -415,13 +417,20 @@ async function determineAndroidStudioPath(os: OS): Promise { return ''; } -async function determineGemfileOrCocoapodPath( +async function determineCocoapodPath(): Promise { + if (process.env.CAPACITOR_COCOAPODS_PATH) { + return process.env.CAPACITOR_COCOAPODS_PATH; + } + return 'pod'; +} + +async function determinePackageManager( rootDir: string, platformDir: any, nativeProjectDirAbs: string, -): Promise { - if (process.env.CAPACITOR_COCOAPODS_PATH) { - return process.env.CAPACITOR_COCOAPODS_PATH; +): Promise<'Cocoapods' | 'bundler' | 'SPM'> { + if (existsSync(resolve(nativeProjectDirAbs, 'CapApp-SPM'))) { + return 'SPM'; } let gemfilePath = ''; @@ -450,17 +459,17 @@ async function determineGemfileOrCocoapodPath( try { const gemfileText = (await readFile(gemfilePath)).toString(); if (!gemfileText) { - return 'pod'; + return 'Cocoapods'; } const cocoapodsInGemfile = new RegExp(/gem\s+['"]cocoapods/).test(gemfileText); - if (cocoapodsInGemfile) { - return 'bundle exec pod'; + if (cocoapodsInGemfile && (await isInstalled('bundle'))) { + return 'bundler'; } else { - return 'pod'; + return 'Cocoapods'; } } catch { - return 'pod'; + return 'Cocoapods'; } } diff --git a/cli/src/definitions.ts b/cli/src/definitions.ts index 893a9d0300..f60b937ca5 100644 --- a/cli/src/definitions.ts +++ b/cli/src/definitions.ts @@ -3,6 +3,7 @@ import type { CapacitorConfig, PluginsConfig } from './declarations'; type DeepReadonly = { readonly [P in keyof T]: DeepReadonly }; export type ExternalConfig = DeepReadonly; +export type Writable = T extends object ? { -readonly [K in keyof T]: Writable } : T; export const enum OS { Unknown = 'unknown', @@ -116,6 +117,7 @@ export interface IOSConfig extends PlatformConfig { readonly cordovaPluginsDirAbs: string; readonly minVersion: string; readonly podPath: Promise; + readonly packageManager: Promise<'Cocoapods' | 'bundler' | 'SPM'>; readonly scheme: string; readonly webDir: Promise; readonly webDirAbs: Promise; diff --git a/cli/src/index.ts b/cli/src/index.ts index 999fb0c1af..5d52c15e41 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -3,7 +3,7 @@ import { resolve } from 'path'; import c from './colors'; import { loadConfig } from './config'; -import type { Config } from './definitions'; +import type { Config, Writable } from './definitions'; import { fatal, isFatal } from './errors'; import { receive } from './ipc'; import { logger, output } from './log'; @@ -11,8 +11,6 @@ import { telemetryAction } from './telemetry'; import { wrapAction } from './util/cli'; import { emoji as _e } from './util/emoji'; -type Writable = T extends object ? { -readonly [K in keyof T]: Writable } : T; - process.on('unhandledRejection', (error) => { console.error(c.failure('[fatal]'), error); }); @@ -29,6 +27,16 @@ export async function run(): Promise { } } +async function getPackageManager(config: Config, packageManager: any): Promise { + if (packageManager === 'cocoapods') { + if ((await config.ios.packageManager) === 'bundler') { + return 'bundler'; + } + return 'Cocoapods'; + } + return 'SPM'; +} + export function runProgram(config: Config): void { program.version(config.cli.package.version); @@ -313,6 +321,7 @@ export function runProgram(config: Config): void { const { addCommand } = await import('./tasks/add'); const configWritable: Writable = config as Writable; + configWritable.ios.packageManager = getPackageManager(config, packagemanager?.toLowerCase()); if (packagemanager?.toLowerCase() === 'CocoaPods'.toLowerCase()) { configWritable.cli.assets.ios.platformTemplateArchive = 'ios-pods-template.tar.gz'; configWritable.cli.assets.ios.platformTemplateArchiveAbs = resolve( diff --git a/cli/src/ios/build.ts b/cli/src/ios/build.ts index 982e30c337..83485f913a 100644 --- a/cli/src/ios/build.ts +++ b/cli/src/ios/build.ts @@ -6,18 +6,15 @@ import { runTask } from '../common'; import { XcodeExportMethod, type Config } from '../definitions'; import { logSuccess } from '../log'; import { type BuildCommandOptions } from '../tasks/build'; -import { checkPackageManager } from '../util/spm'; import { runCommand } from '../util/subprocess'; export async function buildiOS(config: Config, buildOptions: BuildCommandOptions): Promise { const theScheme = buildOptions.scheme ?? 'App'; - const packageManager = await checkPackageManager(config); - let typeOfBuild: string; let projectName: string; - if (packageManager == 'Cocoapods') { + if ((await config.ios.packageManager) !== 'SPM') { typeOfBuild = '-workspace'; projectName = basename(await config.ios.nativeXcodeWorkspaceDirAbs); } else { diff --git a/cli/src/ios/common.ts b/cli/src/ios/common.ts index 71bd344990..312c13dad2 100644 --- a/cli/src/ios/common.ts +++ b/cli/src/ios/common.ts @@ -4,6 +4,7 @@ import { join, resolve } from 'path'; import c from '../colors'; import { checkCapacitorPlatform } from '../common'; +import type { CheckFunction } from '../common'; import { getIncompatibleCordovaPlugins } from '../cordova'; import { OS } from '../definitions'; import type { Config } from '../definitions'; @@ -25,6 +26,16 @@ function execBundler() { } } +export async function getCommonChecks(config: Config): Promise { + const checks: CheckFunction[] = []; + if ((await config.ios.packageManager) === 'bundler') { + checks.push(() => checkBundler(config)); + } else if ((await config.ios.packageManager) === 'Cocoapods') { + checks.push(() => checkCocoaPods(config)); + } + return checks; +} + export async function checkBundler(config: Config): Promise { if (config.cli.os === OS.Mac) { let bundlerResult = execBundler(); diff --git a/cli/src/ios/doctor.ts b/cli/src/ios/doctor.ts index 77d9b8b175..87bb3911f5 100644 --- a/cli/src/ios/doctor.ts +++ b/cli/src/ios/doctor.ts @@ -4,7 +4,7 @@ import { fatal } from '../errors'; import { logSuccess } from '../log'; import { isInstalled } from '../util/subprocess'; -import { checkBundler, checkCocoaPods } from './common'; +import { getCommonChecks } from './common'; export async function doctorIOS(config: Config): Promise { // DOCTOR ideas for iOS: @@ -19,7 +19,7 @@ export async function doctorIOS(config: Config): Promise { // check online datebase of common errors // check if www folder is empty (index.html does not exist) try { - await check([() => checkBundler(config) || checkCocoaPods(config), () => checkWebDir(config), checkXcode]); + await check([() => checkWebDir(config), checkXcode, ...(await getCommonChecks(config))]); logSuccess('iOS looking great! 👌'); } catch (e: any) { fatal(e.stack ?? e); diff --git a/cli/src/ios/open.ts b/cli/src/ios/open.ts index ea115ca0f2..3652b9eeac 100644 --- a/cli/src/ios/open.ts +++ b/cli/src/ios/open.ts @@ -2,10 +2,9 @@ import open from 'open'; import { wait } from '../common'; import type { Config } from '../definitions'; -import { checkPackageManager } from '../util/spm'; export async function openIOS(config: Config): Promise { - if ((await checkPackageManager(config)) == 'SPM') { + if ((await config.ios.packageManager) == 'SPM') { await open(config.ios.nativeXcodeProjDirAbs, { wait: false }); } else { await open(await config.ios.nativeXcodeWorkspaceDirAbs, { wait: false }); diff --git a/cli/src/ios/run.ts b/cli/src/ios/run.ts index 13a031f395..d893f8cf1e 100644 --- a/cli/src/ios/run.ts +++ b/cli/src/ios/run.ts @@ -6,7 +6,6 @@ import { promptForPlatformTarget, runTask } from '../common'; import type { Config } from '../definitions'; import type { RunCommandOptions } from '../tasks/run'; import { runNativeRun, getPlatformTargets } from '../util/native-run'; -import { checkPackageManager } from '../util/spm'; import { runCommand } from '../util/subprocess'; const debug = Debug('capacitor:ios:run'); @@ -33,12 +32,12 @@ export async function runIOS( const derivedDataPath = resolve(config.ios.platformDirAbs, 'DerivedData', target.id); - const packageManager = await checkPackageManager(config); + const packageManager = await config.ios.packageManager; let typeOfBuild: string; let projectName: string; - if (packageManager == 'Cocoapods') { + if (packageManager !== 'SPM') { typeOfBuild = '-workspace'; projectName = basename(await config.ios.nativeXcodeWorkspaceDirAbs); } else { diff --git a/cli/src/ios/update.ts b/cli/src/ios/update.ts index e5aaea6c4e..1003098ca1 100644 --- a/cli/src/ios/update.ts +++ b/cli/src/ios/update.ts @@ -21,7 +21,7 @@ import { copy as copyTask } from '../tasks/copy'; import { convertToUnixPath } from '../util/fs'; import { generateIOSPackageJSON } from '../util/iosplugin'; import { resolveNode } from '../util/node'; -import { checkPackageManager, generatePackageFile, checkPluginsForPackageSwift } from '../util/spm'; +import { generatePackageFile, checkPluginsForPackageSwift } from '../util/spm'; import { runCommand, isInstalled } from '../util/subprocess'; import { extractTemplate } from '../util/template'; @@ -51,7 +51,7 @@ async function updatePluginFiles(config: Config, plugins: Plugin[], deployment: } await handleCordovaPluginsJS(cordovaPlugins, config, platform); await checkPluginDependencies(plugins, platform, config.app.extConfig.cordova?.failOnUninstalledPlugins); - if ((await checkPackageManager(config)) === 'SPM') { + if ((await config.ios.packageManager) === 'SPM') { await generateCordovaPackageFiles(cordovaPlugins, config); const validSPMPackages = await checkPluginsForPackageSwift(config, plugins); @@ -131,20 +131,15 @@ async function updatePodfile(config: Config, plugins: Plugin[], deployment: bool await writeFile(podfilePath, podfileContent, { encoding: 'utf-8' }); const podPath = await config.ios.podPath; - const useBundler = podPath.startsWith('bundle') && (await isInstalled('bundle')); - const podCommandExists = await isInstalled('pod'); - if (useBundler || podCommandExists) { - if (useBundler) { - await runCommand('bundle', ['exec', 'pod', 'install', ...(deployment ? ['--deployment'] : [])], { - cwd: config.ios.nativeProjectDirAbs, - }); - } else { - await runCommand(podPath, ['install', ...(deployment ? ['--deployment'] : [])], { - cwd: config.ios.nativeProjectDirAbs, - }); - } + const useBundler = (await config.ios.packageManager) === 'bundler'; + if (useBundler) { + await runCommand('bundle', ['exec', 'pod', 'install', ...(deployment ? ['--deployment'] : [])], { + cwd: config.ios.nativeProjectDirAbs, + }); } else { - logger.warn('Skipping pod install because CocoaPods is not installed'); + await runCommand(podPath, ['install', ...(deployment ? ['--deployment'] : [])], { + cwd: config.ios.nativeProjectDirAbs, + }); } const isXcodebuildAvailable = await isInstalled('xcodebuild'); diff --git a/cli/src/tasks/add.ts b/cli/src/tasks/add.ts index f849132bf4..f45b0c363c 100644 --- a/cli/src/tasks/add.ts +++ b/cli/src/tasks/add.ts @@ -22,7 +22,7 @@ import type { CheckFunction } from '../common'; import type { Config } from '../definitions'; import { fatal, isFatal } from '../errors'; import { addIOS } from '../ios/add'; -import { editProjectSettingsIOS, checkBundler, checkCocoaPods, checkIOSPackage } from '../ios/common'; +import { editProjectSettingsIOS, checkIOSPackage, getCommonChecks } from '../ios/common'; import { logger, logSuccess, output } from '../log'; import { sync } from './sync'; @@ -75,7 +75,7 @@ export async function addCommand(config: Config, selectedPlatformName: string): } try { - await check([() => checkPackage(), () => checkAppConfig(config), ...addChecks(config, platformName)]); + await check([() => checkPackage(), () => checkAppConfig(config), ...(await getAddChecks(config, platformName))]); await doAdd(config, platformName); await editPlatforms(config, platformName); @@ -110,9 +110,9 @@ function printNextSteps(platformName: string) { ); } -function addChecks(config: Config, platformName: string): CheckFunction[] { +async function getAddChecks(config: Config, platformName: string): Promise { if (platformName === config.ios.name) { - return [() => checkIOSPackage(config), () => checkBundler(config) || checkCocoaPods(config)]; + return [() => checkIOSPackage(config), ...(await getCommonChecks(config))]; } else if (platformName === config.android.name) { return [() => checkAndroidPackage(config)]; } else if (platformName === config.web.name) { diff --git a/cli/src/tasks/migrate-spm.ts b/cli/src/tasks/migrate-spm.ts index f59a069a2b..a99f5cb591 100644 --- a/cli/src/tasks/migrate-spm.ts +++ b/cli/src/tasks/migrate-spm.ts @@ -1,8 +1,9 @@ -import type { Config } from '../definitions'; +import { check } from '../common'; +import type { Config, Writable } from '../definitions'; import { fatal } from '../errors'; +import { getCommonChecks } from '../ios/common'; import { logger } from '../log'; import { - checkPackageManager, extractSPMPackageDirectory, removeCocoapodsFiles, runCocoapodsDeintegrate, @@ -12,16 +13,17 @@ import { import { update } from './update'; export async function migrateToSPM(config: Config): Promise { - if ((await checkPackageManager(config)) == 'SPM') { + if ((await config.ios.packageManager) == 'SPM') { fatal('Capacitor project is already using SPM, exiting.'); } - + await check(await getCommonChecks(config)); await extractSPMPackageDirectory(config); await runCocoapodsDeintegrate(config); await removeCocoapodsFiles(config); await addInfoPlistDebugIfNeeded(config); - await update(config, 'ios', true); - + const configWritable: Writable = config as Writable; + configWritable.ios.packageManager = Promise.resolve('SPM'); + await update(configWritable as Config, 'ios', false); logger.info( 'To complete migration follow the manual steps at https://capacitorjs.com/docs/ios/spm#using-our-migration-tool', ); diff --git a/cli/src/tasks/migrate.ts b/cli/src/tasks/migrate.ts index 8794c704ab..b2c419c080 100644 --- a/cli/src/tasks/migrate.ts +++ b/cli/src/tasks/migrate.ts @@ -10,7 +10,6 @@ import { fatal } from '../errors'; import { getMajoriOSVersion } from '../ios/common'; import { logger, logPrompt, logSuccess } from '../log'; import { deleteFolderRecursive } from '../util/fs'; -import { checkPackageManager } from '../util/spm'; import { runCommand } from '../util/subprocess'; import { extractTemplate } from '../util/template'; @@ -161,7 +160,7 @@ export async function migrateCommand(config: Config, noprompt: boolean, packagem ); }); - if ((await checkPackageManager(config)) === 'Cocoapods') { + if ((await config.ios.packageManager) !== 'SPM') { // Update Podfile await runTask(`Migrating Podfile to ${iOSVersion}.0.`, () => { return updateFile( diff --git a/cli/src/tasks/sync.ts b/cli/src/tasks/sync.ts index 98660e8b8c..e76ecaac87 100644 --- a/cli/src/tasks/sync.ts +++ b/cli/src/tasks/sync.ts @@ -5,7 +5,7 @@ import { logger } from '../log'; import { allSerial } from '../util/promise'; import { copy, copyCommand } from './copy'; -import { update, updateChecks, updateCommand } from './update'; +import { addUpdateChecks, update, updateCommand } from './update'; /** * Sync is a copy and an update in one. @@ -27,7 +27,7 @@ export async function syncCommand( const then = +new Date(); const platforms = await selectPlatforms(config, selectedPlatformName); try { - await check([() => checkPackage(), () => checkWebDir(config), ...updateChecks(config, platforms)]); + await check([() => checkPackage(), () => checkWebDir(config), ...(await addUpdateChecks(config, platforms))]); await allSerial(platforms.map((platformName) => () => sync(config, platformName, deployment, inline))); const now = +new Date(); const diff = (now - then) / 1000; diff --git a/cli/src/tasks/update.ts b/cli/src/tasks/update.ts index a2b6cbe0d6..362a20aa70 100644 --- a/cli/src/tasks/update.ts +++ b/cli/src/tasks/update.ts @@ -13,7 +13,7 @@ import { import type { CheckFunction } from '../common'; import type { Config } from '../definitions'; import { fatal, isFatal } from '../errors'; -import { checkBundler, checkCocoaPods } from '../ios/common'; +import { checkBundler, checkCocoaPods, getCommonChecks } from '../ios/common'; import { updateIOS } from '../ios/update'; import { logger } from '../log'; import { allSerial } from '../util/promise'; @@ -30,8 +30,7 @@ export async function updateCommand(config: Config, selectedPlatformName: string const then = +new Date(); const platforms = await selectPlatforms(config, selectedPlatformName); try { - await check([() => checkPackage(), ...updateChecks(config, platforms)]); - + await check([() => checkPackage(), ...(await addUpdateChecks(config, platforms))]); await allSerial(platforms.map((platformName) => async () => await update(config, platformName, deployment))); const now = +new Date(); const diff = (now - then) / 1000; @@ -46,6 +45,28 @@ export async function updateCommand(config: Config, selectedPlatformName: string } } +export async function addUpdateChecks(config: Config, platforms: string[]): Promise { + let checks: CheckFunction[] = []; + for (const platformName of platforms) { + if (platformName === config.ios.name) { + checks = await getCommonChecks(config); + } else if (platformName === config.android.name) { + continue; + } else if (platformName === config.web.name) { + continue; + } else { + throw `Platform ${platformName} is not valid.`; + } + } + return checks; +} + +/** + * @deprecated use addUpdateChecks + * @param config + * @param platforms + * @returns + */ export function updateChecks(config: Config, platforms: string[]): CheckFunction[] { const checks: CheckFunction[] = []; for (const platformName of platforms) { diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index b5dc1a8afc..30e51cf1a4 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -12,13 +12,18 @@ import { getMajoriOSVersion } from '../ios/common'; import { logger } from '../log'; import type { Plugin } from '../plugin'; import { getPluginType, PluginType } from '../plugin'; -import { runCommand, isInstalled } from '../util/subprocess'; +import { runCommand } from '../util/subprocess'; export interface SwiftPlugin { name: string; path: string; } +/** + * @deprecated use config.ios.packageManager + * @param config + * @returns 'Cocoapods' | 'SPM' + */ export async function checkPackageManager(config: Config): Promise<'Cocoapods' | 'SPM'> { const iosDirectory = config.ios.nativeProjectDirAbs; if (existsSync(resolve(iosDirectory, 'CapApp-SPM'))) { @@ -144,25 +149,19 @@ let package = Package( export async function runCocoapodsDeintegrate(config: Config): Promise { const podPath = await config.ios.podPath; const projectFileName = config.ios.nativeXcodeProjDirAbs; - const useBundler = podPath.startsWith('bundle') && (await isInstalled('bundle')); - const podCommandExists = await isInstalled('pod'); - - if (useBundler) logger.info('Found bundler, using it to run CocoaPods.'); + const useBundler = (await config.ios.packageManager) === 'bundler'; logger.info('Running pod deintegrate on project ' + projectFileName); - if (useBundler || podCommandExists) { - if (useBundler) { - await runCommand('bundle', ['exec', 'pod', 'deintegrate', projectFileName], { - cwd: config.ios.nativeProjectDirAbs, - }); - } else { - await runCommand(podPath, ['deintegrate', projectFileName], { - cwd: config.ios.nativeProjectDirAbs, - }); - } + if (useBundler) { + logger.info('Found bundler, using it to run CocoaPods.'); + await runCommand('bundle', ['exec', 'pod', 'deintegrate', projectFileName], { + cwd: config.ios.nativeProjectDirAbs, + }); } else { - logger.warn('Skipping pod deintegrate because CocoaPods is not installed - migration will be incomplete'); + await runCommand(podPath, ['deintegrate', projectFileName], { + cwd: config.ios.nativeProjectDirAbs, + }); } }