From 76a0c9f8a8b48e823724e53808878ea7cbca5c9e Mon Sep 17 00:00:00 2001 From: Robin Genz Date: Thu, 19 Feb 2026 12:15:00 +0100 Subject: [PATCH 1/7] feat(cli): support SPM package traits in generated Package.swift --- cli/src/declarations.ts | 12 ++++++++++++ cli/src/util/spm.ts | 9 +++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 6612eec77..c66c1560e 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -507,6 +507,18 @@ export interface CapacitorConfig { */ provisioningProfile?: string; }; + + spm?: { + /** + * Define package traits for SPM plugin dependencies. + * + * The key is the plugin ID (e.g. `@capacitor-firebase/analytics`) + * and the value is an array of trait names. + * + * @since 8.1.0 + */ + packageTraits?: { [pluginId: string]: string[] }; + }; }; server?: { diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index 30e51cf1a..d353e655a 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -98,8 +98,11 @@ export async function removeCocoapodsFiles(config: Config): Promise { export async function generatePackageText(config: Config, plugins: Plugin[]): Promise { const iosPlatformVersion = await getCapacitorPackageVersion(config, config.ios.name); const iosVersion = getMajoriOSVersion(config); + const packageTraits = config.app.extConfig.ios?.spm?.packageTraits ?? {}; + const hasTraits = Object.keys(packageTraits).length > 0; + const swiftToolsVersion = hasTraits ? '6.1' : '5.9'; - let packageSwiftText = `// swift-tools-version: 5.9 + let packageSwiftText = `// swift-tools-version: ${swiftToolsVersion} import PackageDescription // DO NOT MODIFY THIS FILE - managed by Capacitor CLI commands @@ -119,7 +122,9 @@ let package = Package( packageSwiftText += `,\n .package(name: "${plugin.name}", path: "../../capacitor-cordova-ios-plugins/sources/${plugin.name}")`; } else { const relPath = relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath); - packageSwiftText += `,\n .package(name: "${plugin.ios?.name}", path: "${relPath}")`; + const traits = packageTraits[plugin.id]; + const traitsSuffix = traits?.length ? `, traits: [${traits.map((t) => `"${t}"`).join(', ')}]` : ''; + packageSwiftText += `,\n .package(name: "${plugin.ios?.name}", path: "${relPath}"${traitsSuffix})`; } } From 0b87ab904bdfb414f74d644e899118aeedc7da26 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 23 Feb 2026 11:16:42 +0000 Subject: [PATCH 2/7] docs(cli): update capacitor version for package traits --- cli/src/declarations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index c66c1560e..1f634d789 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -515,7 +515,7 @@ export interface CapacitorConfig { * The key is the plugin ID (e.g. `@capacitor-firebase/analytics`) * and the value is an array of trait names. * - * @since 8.1.0 + * @since 8.2.0 */ packageTraits?: { [pluginId: string]: string[] }; }; From 88e347ae24ec11b62690be2963bcac8f11a3fdeb Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 23 Feb 2026 11:26:30 +0000 Subject: [PATCH 3/7] fix(cli): Allow passing .defaults to SPM traits --- cli/src/util/spm.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index d353e655a..ffe0e1158 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -123,7 +123,12 @@ let package = Package( } else { const relPath = relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath); const traits = packageTraits[plugin.id]; - const traitsSuffix = traits?.length ? `, traits: [${traits.map((t) => `"${t}"`).join(', ')}]` : ''; + const traitsSuffix = traits?.length + ? `, traits: [${traits.map((t) => { + // Any trait is written with quotes, with the exception of .defaults + return /^\.?defaults?$/i.test(t) ? ".defaults" : `"${t}"`; + }).join(", ")}]` + : ""; packageSwiftText += `,\n .package(name: "${plugin.ios?.name}", path: "${relPath}"${traitsSuffix})`; } } From 8c18f201263f2a37a29ed18d16da32aab89df586 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 23 Feb 2026 11:41:51 +0000 Subject: [PATCH 4/7] docs(cli): Minor clarifications regarding default traits --- cli/src/declarations.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 1f634d789..41987e932 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -515,6 +515,9 @@ export interface CapacitorConfig { * The key is the plugin ID (e.g. `@capacitor-firebase/analytics`) * and the value is an array of trait names. * + * Packages can have default traits. If you use this property, and + * want to preserve the defaults, include ".defaults" in the array. + * * @since 8.2.0 */ packageTraits?: { [pluginId: string]: string[] }; From fc063fb8b7c6af50a13a3b3101d533057a38ccd3 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 23 Feb 2026 12:12:01 +0000 Subject: [PATCH 5/7] chore: fix lint issues --- cli/src/util/spm.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index ffe0e1158..2bf8aea94 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -124,11 +124,13 @@ let package = Package( const relPath = relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath); const traits = packageTraits[plugin.id]; const traitsSuffix = traits?.length - ? `, traits: [${traits.map((t) => { - // Any trait is written with quotes, with the exception of .defaults - return /^\.?defaults?$/i.test(t) ? ".defaults" : `"${t}"`; - }).join(", ")}]` - : ""; + ? `, traits: [${traits + .map((t) => { + // Any trait is written with quotes, with the exception of .defaults + return /^\.?defaults?$/i.test(t) ? '.defaults' : `"${t}"`; + }) + .join(', ')}]` + : ''; packageSwiftText += `,\n .package(name: "${plugin.ios?.name}", path: "${relPath}"${traitsSuffix})`; } } From 2707c86ec1f062c1092abb382173a258452c427d Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 23 Mar 2026 10:44:28 +0000 Subject: [PATCH 6/7] chore(cli): Make packageTraits experimental As discussed in PR - https://github.com/ionic-team/capacitor/pull/8351#issuecomment-3990698223 --- cli/src/declarations.ts | 35 +++++++++++++++++++---------------- cli/src/util/spm.ts | 3 +-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 6435f18f2..6c8645406 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -507,21 +507,6 @@ export interface CapacitorConfig { */ provisioningProfile?: string; }; - - spm?: { - /** - * Define package traits for SPM plugin dependencies. - * - * The key is the plugin ID (e.g. `@capacitor-firebase/analytics`) - * and the value is an array of trait names. - * - * Packages can have default traits. If you use this property, and - * want to preserve the defaults, include ".defaults" in the array. - * - * @since 8.2.0 - */ - packageTraits?: { [pluginId: string]: string[] }; - }; }; experimental?: { @@ -551,11 +536,29 @@ export interface CapacitorConfig { * * This setting may graduate to `ios.spm.swiftToolsVersion` in a future major release. * - * @since 8.2.0 + * @since 8.3.0 * @default '5.9' * @example '6.1' */ swiftToolsVersion?: string; + + /** + * Define package traits for SPM plugin dependencies. + * + * This requires explicitly setting experimental.ios.spm.swiftToolsVersion + * to '6.1' or higher. + * + * The key is the plugin ID (e.g. `@capacitor-firebase/analytics`) + * and the value is an array of trait names. + * + * Packages can have default traits. If you use this property, and + * want to preserve the defaults, include ".defaults" in the array. + * + * This setting may graduate to `ios.spm.packageTraits` in a future major release. + * + * @since 8.3.0 + */ + packageTraits?: { [pluginId: string]: string[] }; }; }; }; diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index 233a52f98..20d340c50 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -98,8 +98,7 @@ export async function removeCocoapodsFiles(config: Config): Promise { export async function generatePackageText(config: Config, plugins: Plugin[]): Promise { const iosPlatformVersion = await getCapacitorPackageVersion(config, config.ios.name); const iosVersion = getMajoriOSVersion(config); - const packageTraits = config.app.extConfig.ios?.spm?.packageTraits ?? {}; - const hasTraits = Object.keys(packageTraits).length > 0; + const packageTraits = config.app.extConfig.experimental?.ios?.spm?.packageTraits ?? {}; const swiftToolsVersion = config.app.extConfig.experimental?.ios?.spm?.swiftToolsVersion ?? '5.9'; let packageSwiftText = `// swift-tools-version: ${swiftToolsVersion} From ee17172b0dba88808868672d795af939ea422ecf Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 23 Mar 2026 11:19:27 +0000 Subject: [PATCH 7/7] fix(cli): Error for packageTraits without swiftToolsVersion Without this cap sync would pass and we'd get a generic error in Xcode: Missing package product 'CapApp-SPM' This way it's easier to understand what the actual problem is. --- cli/src/ios/common.ts | 3 ++- cli/src/util/spm.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cli/src/ios/common.ts b/cli/src/ios/common.ts index 7ac2831e0..779dafd90 100644 --- a/cli/src/ios/common.ts +++ b/cli/src/ios/common.ts @@ -11,7 +11,7 @@ import type { Config } from '../definitions'; import { logger } from '../log'; import { PluginType, getPluginPlatform } from '../plugin'; import type { Plugin } from '../plugin'; -import { checkSwiftToolsVersion } from '../util/spm'; +import { checkPackageTraitsRequirements, checkSwiftToolsVersion } from '../util/spm'; import { isInstalled, runCommand } from '../util/subprocess'; export async function checkIOSPackage(config: Config): Promise { @@ -38,6 +38,7 @@ export async function getCommonChecks(config: Config): Promise if (swiftToolsVersion) { checks.push(() => checkSwiftToolsVersion(config, swiftToolsVersion)); } + checks.push(() => checkPackageTraitsRequirements(config)); } return checks; } diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index 20d340c50..3e2e51529 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -216,6 +216,37 @@ export async function checkSwiftToolsVersion(config: Config, version: string | u return null; } +export async function checkPackageTraitsRequirements(config: Config): Promise { + const packageTraits = config.app.extConfig.experimental?.ios?.spm?.packageTraits; + const swiftToolsVersion = config.app.extConfig.experimental?.ios?.spm?.swiftToolsVersion; + + const hasPackageTraits = packageTraits && Object.keys(packageTraits).some((key) => packageTraits[key]?.length > 0); + + if (!hasPackageTraits) { + return null; + } + + if (!swiftToolsVersion) { + return ( + `Package traits require an explicit Swift tools version of 6.1 or higher.\n` + + `Set experimental.ios.spm.swiftToolsVersion to '6.1' or higher in your Capacitor configuration.` + ); + } + + const versionParts = swiftToolsVersion.split('.').map((part) => parseInt(part, 10)); + const major = versionParts[0] || 0; + const minor = versionParts[1] || 0; + + if (major < 6 || (major === 6 && minor < 1)) { + return ( + `Package traits require Swift tools version 6.1 or higher, but "${swiftToolsVersion}" was specified.\n` + + `Update experimental.ios.spm.swiftToolsVersion to '6.1' or higher in your Capacitor configuration.` + ); + } + + return null; +} + // Private Functions async function pluginsWithPackageSwift(plugins: Plugin[]): Promise {