Skip to content

cap sync ios generates an unresolvable Package.swift when IPHONEOS_DEPLOYMENT_TARGET >= 18 (.v18 vs default swift-tools-version 5.9) #8571

Description

@bencoton

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.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions