Bug
npx cap sync ios generates a Package.swift that cannot resolve when the Xcode project's IPHONEOS_DEPLOYMENT_TARGET is 18 or higher. A valid, current deployment target silently produces a broken build.
Environment
@capacitor/cli 8.5.0
- SPM-based iOS project (
ios/App/CapApp-SPM), no CocoaPods
- Discovered on a cloud macOS CI runner (Codemagic); reproducible from the generated file alone
What happens
IPHONEOS_DEPLOYMENT_TARGET = 18.0 in project.pbxproj produces:
// swift-tools-version: 5.9
import PackageDescription
// DO NOT MODIFY THIS FILE - managed by Capacitor CLI commands
let package = Package(
name: "CapApp-SPM",
platforms: [.iOS(.v18)],
and the build fails with:
Package.swift:7:23: error: 'v18' is unavailable
note: 'v18' was introduced in PackageDescription 6.0
Cause
In dist/util/spm.js, generatePackageText:
- line 89 reads the major from the deployment target —
const iosVersion = getMajoriOSVersion(config)
- line 99 writes it verbatim —
platforms: [.iOS(.v${iosVersion})],
- line 92 takes the tools version from
config.app.extConfig.experimental?.ios?.spm?.swiftToolsVersion, defaulting to '5.9'
- line 93 writes that header —
let packageSwiftText = `// swift-tools-version: ${swiftToolsVersion} ``
.v18 requires PackageDescription 6.0, so with the default tools version the generated manifest contradicts its own header. The two values are produced independently and never checked against each other.
There is a workaround — setting experimental.ios.spm.swiftToolsVersion to "6.0" — but it is undocumented in this context and not discoverable from the error, which points at a generated file the header tells you not to modify.
Suggested fix
Either derive a minimum tools version from the deployment target (.v18 ⇒ at least 6.0), or emit a warning at sync time when the target exceeds what the selected tools version can express. Failing at sync, where the inputs are known, is far cheaper than failing at resolve time in a generated file.
Separable second issue: Windows path separators
dist/util/spm.js line 112 builds plugin paths with path.relative():
const relPath = (0, path_1.relative)(config.ios.nativeXcodeProjDirAbs, plugin.rootPath);
packageSwiftText += `,\n .package(name: "${plugin.id}", path: "${relPath}")`;
Run on Windows, that emits host-native separators:
.package(name: "CapacitorApp", path: "..\..\..\node_modules\@capacitor\app"),
A backslash is not a path separator on macOS, which is the only platform that consumes this file. Since a Capacitor project is commonly developed on Windows and built on a Mac or macOS CI, the committed manifest is wrong for its consumer. Package.swift should emit POSIX separators regardless of host — e.g. relPath.split(path.sep).join('/').
(We work around it by regenerating on the macOS runner before the build, and by gitignoring the file so the Windows-generated copy never reaches CI. Both are workarounds for output that could just be host-independent.)
Bug
npx cap sync iosgenerates aPackage.swiftthat cannot resolve when the Xcode project'sIPHONEOS_DEPLOYMENT_TARGETis 18 or higher. A valid, current deployment target silently produces a broken build.Environment
@capacitor/cli8.5.0ios/App/CapApp-SPM), no CocoaPodsWhat happens
IPHONEOS_DEPLOYMENT_TARGET = 18.0inproject.pbxprojproduces:and the build fails with:
Cause
In
dist/util/spm.js,generatePackageText:const iosVersion = getMajoriOSVersion(config)platforms: [.iOS(.v${iosVersion})],config.app.extConfig.experimental?.ios?.spm?.swiftToolsVersion, defaulting to'5.9'let packageSwiftText = `// swift-tools-version: ${swiftToolsVersion}``.v18requires PackageDescription 6.0, so with the default tools version the generated manifest contradicts its own header. The two values are produced independently and never checked against each other.There is a workaround — setting
experimental.ios.spm.swiftToolsVersionto"6.0"— but it is undocumented in this context and not discoverable from the error, which points at a generated file the header tells you not to modify.Suggested fix
Either derive a minimum tools version from the deployment target (
.v18⇒ at least 6.0), or emit a warning at sync time when the target exceeds what the selected tools version can express. Failing at sync, where the inputs are known, is far cheaper than failing at resolve time in a generated file.Separable second issue: Windows path separators
dist/util/spm.jsline 112 builds plugin paths withpath.relative():Run on Windows, that emits host-native separators:
A backslash is not a path separator on macOS, which is the only platform that consumes this file. Since a Capacitor project is commonly developed on Windows and built on a Mac or macOS CI, the committed manifest is wrong for its consumer.
Package.swiftshould emit POSIX separators regardless of host — e.g.relPath.split(path.sep).join('/').(We work around it by regenerating on the macOS runner before the build, and by gitignoring the file so the Windows-generated copy never reaches CI. Both are workarounds for output that could just be host-independent.)