What tools support editing project.pbxproj files?

Solution 1:

project.pbxproj is an old-style ASCII property list file, too. So you can use /usr/libexec/PlistBuddy to edit it.

Print some User-Defined key's value like this,

# Get the key A83311AA20DA4A80004B8C0E in your project.pbxproj
# LZD_NOTIFICATION_SERVICE_BUNDLE_ID is defined by me,
# Replace key paths with your own.
/usr/libexec/PlistBuddy -c 'print :objects:A83311AA20DA4A80004B8C0E:buildSettings:LZD_NOTIFICATION_SERVICE_BUNDLE_ID' LAAppAdapter.xcodeproj/project.pbxproj

Set its value like this,

/usr/libexec/PlistBuddy -c 'set :objects:A83311AA20DA4A80004B8C0E:buildSettings:LZD_NOTIFICATION_SERVICE_BUNDLE_ID com.dawnsong.notification-service' LAAppAdapter.xcodeproj/project.pbxproj

UPDATE: PlistBuddy will automatically convert project.pbxproj into a xml-format plist file since macOS Catalina or earlier version. Consider move the setting item into xcconfig file instead, please since xcconfig is much smaller and simpler than project.pbxproj and not easy to make mistake when editing with perl script.

Solution 2:

I know this has been answered for a while, but since the original question is about tools supporting the manipulation of .pbxproj files, and many other people may be looking for the same information, here's how I do it. It took me quite a while to figure this out because I was very unfamiliar with Xcode when I started attempting this, so I hope this saves others the hours of grief I had to put in.

You can use the plutil command to transform the .pbxproj file from the legacy .plist format into an XML or JSON format you will be able to manipulate more easily. I'm using JSON. To do so, just run:

plutil -convert json project.pbxproj

This will convert the format of project.pbxproj, but be aware that -contrary to common sense- the output won't be another file with a JSON extention such as project.json. What will happen is that project.pbxproj will be converted to JSON format, but retain it's cryptic .pbxproj extension. So even though the file's format has been changed, Xcode will still pick it up and use it in its new JSON format.

Then you can change project.pbxproj with ease using any JSON manipulation tool of your choosing. I'm using Groovy's JsonSlurper class in a Groovy script.

Note I also explored the XML option, but I found the project.pbxproj file in XML format to be cumbersome to parse. The elements are not properly nested to allow for traversing the tree with ease. It's plagued with:

<key>someKey</key>
<dict>
    <!--More elements which provide configuration for the key above-->
</dict>

So it's positional in nature. You have to look for the key element corresponding to the setting you want to manipulate and then jump to the dict element just after it. Which means you have to mount the children of each XML element into an array, in order to index them.

Solution 3:

Here are 3 open-source tools which implement .pbxproj file editing:

  • https://github.com/CocoaPods/Xcodeproj (Ruby based)
  • https://github.com/apache/cordova-node-xcode (NodeJS based)
  • https://github.com/kronenthaler/mod-pbxproj (Python based)

Personally, I made the best experience with the NodeJS based tool. So far it has covered all our needs reliably.

In the following is listed an example javascript file update-project.js which sets the developer team ID, app entitlements, adds a GoogleService-Info.plist file to the project and checks it as part of the build target. Take it as an inspiration and adapt the scripts and its paths to your needs:

const fs = require('fs')
const xcode = require('xcode')

if (process.argv.length !== 3) {
  console.error("Please pass the development team ID as the first argument")
  process.exit(1)
}
const developmentTeamId = process.argv[2]
const path = 'ios/App/App.xcodeproj/project.pbxproj'
const project = xcode.project(path)

project.parse(error => {
  const targetKey = project.findTargetKey('App')
  const appGroupKey = project.findPBXGroupKey({path: 'App'})
  project.addBuildProperty('CODE_SIGN_ENTITLEMENTS', 'App/App.entitlements')
  project.addBuildProperty('DEVELOPMENT_TEAM', developmentTeamId)
  project.addFile('App.entitlements', appGroupKey)
  project.removeFile('GoogleService-Info.plist', appGroupKey)
  const f = project.addFile('GoogleService-Info.plist', appGroupKey, {target: targetKey})
  f.uuid = project.generateUuid()
  project.addToPbxBuildFileSection(f)
  project.addToPbxResourcesBuildPhase(f)
  fs.writeFileSync(path, project.writeSync())
})

Above script can be executed with

yarn run update-project <arguments...>

given that update-project is registered in package.json:

{
  ...,
  "scripts": {
    ...
    "update-project": "node update-project.js"
  },
  ...
}