ionic-team / ionic-team/capacitor

[Bug]: `cap sync ios` doesn't support Xcode 27.2's JSON project format (`project.xcproj`)

Open
#8,607 0 comments 0 reactions 0 assignees View on GitHub
triage
Dominant language
TypeScript
Stars
16.7k
Forks
1.3k
Avg merge
3d 15h
Merged PRs (30d)
10

Description

### Capacitor Version

```
💊 Capacitor Doctor 💊

Latest Dependencies:

@capacitor/cli: 8.5.2
@capacitor/core: 8.5.2
@capacitor/android: 8.5.2
@capacitor/ios: 8.5.2

Installed Dependencies:

@capacitor/android: not installed
@capacitor/core: 8.5.2
@capacitor/cli: 8.5.2
@capacitor/ios: 8.5.2

[success] iOS looking great! 👌
```

### Other API Details

```
Xcode 27.2 beta (27B5019j), also reproduced building with Xcode 27.0
macOS 27.0
Node 24.15.0
Swift Package Manager (the default for new Capacitor 8 apps)
```

### Platforms Affected

- [x] iOS
- [ ] Android
- [ ] Web

### Current Behavior

Xcode 27.2 beta adds a JSON-based project format. [Its release notes](https://developer.apple.com/documentation/xcode-release-notes/xcode-27_2-release-notes) say it's "more readable, merge-friendly, and easier for coding agents to edit", and projects in this format also open in Xcode 27.0. Converting a project replaces `App.xcodeproj/project.pbxproj` with `App.xcodeproj/project.xcproj`.

The CLI still reads `project.pbxproj` directly, so after converting:

- `npx cap sync ios` logs `Unable to write to …/CapApp-SPM/Package.swift` with `ENOENT: no such file or directory, open '…/App.xcodeproj/project.pbxproj'`, but **exits with code 0**.
- `CapApp-SPM/Package.swift` is never rewritten, so plugins you add or remove never reach iOS.
- After removing a plugin, Xcode then fails with `Could not resolve package dependencies`, because `Package.swift` still points at the removed package.
- `npx cap doctor` still reports "iOS looking great".

Xcode 27.0 and 27.2 both build the converted project fine, so only the CLI is affected.

### Expected Behavior

The CLI works with both `project.pbxproj` and `project.xcproj`. At minimum, it should fail with a non-zero exit code and a clear message instead of reporting success.

### Project Reproduction

With a blank app, no plugins of mine involved:

```sh
mkdir blank-cap && cd blank-cap && mkdir www && echo '

hi

' > www/index.html
npm init -y
npm install @capacitor/core@8.5.2 @capacitor/ios@8.5.2 @capacitor/app@8.1.1
npm install -D @capacitor/cli@8.5.2
echo '{ "appId": "com.example.blank", "appName": "blank", "webDir": "www" }' > capacitor.config.json
npx cap add ios
npx cap sync ios # works

# Convert to the new format (Xcode 27.2 beta); the same option is in Xcode's File inspector
xcodebuild -convert-project "Xcode Project" -project ios/App/App.xcodeproj

npm uninstall @capacitor/app
npx cap sync ios # logs the ENOENT error above, exits 0
grep CapacitorApp ios/App/CapApp-SPM/Package.swift # the removed plugin is still listed
xcodebuild -project ios/App/App.xcodeproj -scheme App -destination 'generic/platform=iOS Simulator' build
# error: Could not resolve package dependencies
```

### Additional Information

Places on `main` that read `project.pbxproj`:

| File | What it does | Effect with `project.xcproj` |
| --- | --- | --- |
| `cli/src/ios/common.ts`, `getMajoriOSVersion()` | Reads `IPHONEOS_DEPLOYMENT_TARGET` | Throws. Called from `util/spm.ts` (this bug) and `ios/update.ts` |
| `cli/src/ios/common.ts`, `editProjectSettingsIOS()` | Sets `PRODUCT_BUNDLE_IDENTIFIER` on `cap add ios` | Would throw (new projects are still `.pbxproj` for now) |
| `cli/src/tasks/migrate.ts` | Bumps the deployment target | Throws |
| `cli/src/tasks/migrate-uiscene.ts` + `cli/src/util/xcode.ts` | Adds `SceneDelegate.swift` through the `xcode` package, which only parses `.pbxproj` | Throws |
| `cli/src/config.ts`, `determineIOSWebDirAbs()` | Checks where `public` lives | Error is caught, no effect |

The new file keeps the same build settings as JSON keys, for example:

```json5
"IPHONEOS_DEPLOYMENT_TARGET": "15.0",
"PRODUCT_BUNDLE_IDENTIFIER": "com.example.blank",
```

It allows trailing commas, so `JSON.parse` can't read it as-is. For the deployment target and bundle ID, falling back to `project.xcproj` when `project.pbxproj` is missing, with regexes that accept both `KEY = value;` and `"KEY": "value"`, would cover `cap sync`, `cap add` and `cap migrate`. The UIScene migration needs its own change, since the `xcode` package can't edit the new file.

Contributor guide

Open the contributing guide

Research direction

Start with the reproduction using Xcode 27.0 or 27.2, then read cli/src/util/spm.ts and cli/src/ios/common.ts, especially getMajoriOSVersion(). Review the other project.pbxproj callers listed in cli/src/ios/common.ts, cli/src/tasks/migrate.ts, cli/src/tasks/migrate-uiscene.ts, cli/src/util/xcode.ts, and cli/src/config.ts. Done means converted projects sync correctly, or unsupported cases fail with a clear non-zero result.

Written by the indexing model from the issue text.

Assessment

Tech stack
ios, typescript
Domain
cli, mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.