ionic-team / ionic-team/capacitor
[Bug]: `cap sync ios` doesn't support Xcode 27.2's JSON project format (`project.xcproj`)
- 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.htmlnpm 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
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