I was about to start writing a parser for Xcode project files. I spent some time really reading a .pbxproj source (as opposed to just looking at the diffs during code review) and turns out, the format is quite simple (and very interesting, mind you, with a legacy smell of NeXT days). This would have been a fun project, but not a good use of my time and it had probably been done before anyways. Open source, I mean.

It is not clear to me why Xcode has no way to keep the files in a build phase sorted automatically, so as you keep adding files to a build target, you get a mess of a long list in which it is hard to find anything.

Compile Sources build phase of an Xcode target showing files unsorted.

This is especially annoying now for multi-platform targets because the file inspector only allows one to add a file to a target, but not to choose which platform the file is for. For platform specific files, you now have to set the platform filter in the build phase (for example, see the two files in above screenshot that have macOS as the filter), but good luck finding the file you’re looking for, which brings me back to wanting to keep that list sorted alphabetically. And for wanting a parser to automate it.

A quick search in the Package Index and I had me a parser: XcodeProj. And the script to quickly sort the build phase file list is as simple as this:

import PathKit
import XcodeProj

let path = Path("/path/to/project")
let xcodeproj = try XcodeProj(path: path)

let settings = PBXOutputSettings(projBuildPhaseFileOrder: .byFilename)
try xcodeproj.write(path: path, override: true, outputSettings: settings)

But while I was at it, I though I might go ahead and also sort the navigator file list. That’s easy, too, just change the settings to this:

let settings = PBXOutputSettings(
  projNavigatorFileOrder: .byFilenameGroupsFirst,
  projBuildPhaseFileOrder: .byFilename
)

But if I sort file lists, why not sort them all? And keep my Xcode project all sorted?

However, there are a few places that I couldn’t find a built-in way to handle with XcodeProj. One is the list of frameworks in the Link Binary with Libraries phase (raised issue #744 for that) and the package list in the project’s Package Dependencies tab. For both of these, Xcode doesn’t even allow you to manually rearrange the list, which is possible in the target’s Compile Sources build phase.

However, the framework is flexible enough to allow quick workarounds to also sorting those two lists, without having to patch the framework directly. Just adding these two functions in an extension on PBXProj does the trick:

extension PBXProj {

  /// Sorts the list of packages in the project's Package Dependencies tab alphabetically.
  func sortPackageDependencies() {
    if let packages = rootObject?.packages {
      rootObject?.packages = packages.sorted {
        // Without lower-casing, "SomePackage" will come before "a-package".
        if let lhs = $0.name?.lowercased(), let rhs = $1.name?.lowercased() {
          return lhs < rhs
        }
        return true
      }
    }
  }

  /// Sorts the list of packages in a Link Binary with Libraries build phase.
  func sortFrameworksBuildPhases() {
    for phase in frameworksBuildPhases {
      if let files = phase.files {
        phase.files = files.sorted {
          // Without lower-casing, "SomePackage" will come before "a-package".
          if let lhs = $0.product?.productName.lowercased(), let rhs = $1.product?.productName.lowercased() {
            return lhs < rhs
          }
          return true
        }
      }
    }
  }
}

With all of this in place, I added the following command to my home-brew Swiss army knife command line tool (based on Swift Argument Parser and assuming there’s already a MainCommand defined.)

extension MainCommand { 
  struct XcodeCommand: ParsableCommand {
    static var configuration = CommandConfiguration(
      commandName: "xcode",
      abstract: "Process Xcode project files.",
      subcommands: [
        SortCommand.self
      ]
    )
  }
}

extension MainCommand.XcodeCommand {
  struct SortCommand: ParsableCommand {
    static var configuration = CommandConfiguration(
      commandName: "sort",
      abstract: "Sorts file and package references in the project.",
      discussion: """
        - Files in the navigator are sorted by filename, with groups always appearing first.
        - Files in target build phases are sorted by filename.
        - Packages in the project's package dependency list and in target framework build phases lower-case the name for sorting.
        """
    )

    @Argument(help: "The path to the Xcode project to process.")
    var project: String

    func run() throws {
      let path = Path(project)
      let xcodeproj = try XcodeProj(path: path)

      // Let the framework handle what it does.
      let settings = PBXOutputSettings(
        projNavigatorFileOrder: .byFilenameGroupsFirst,
        projBuildPhaseFileOrder: .byFilename
      )
      try xcodeproj.write(path: path, override: true, outputSettings: settings)

      // Now massage the parts the framework doesn't handle according to our needs and save again.
      xcodeproj.pbxproj.sortPackageDependencies()
      xcodeproj.pbxproj.sortFrameworksBuildPhases()
      try xcodeproj.write(path: path, override: true)
    }
  }
}

Note that, for this to work, we need to save the project twice, because of the way the output settings are applied during the saving itself. So we apply built-in sorting first and then save again after applying the two workarounds for sorting package lists.

I can now run the following on any of my Xcode projects and get it all nice and sorted (xind is the name of the above-mentioned Swiss army knife command line tool I maintain):

xind xcode sort ~/Developer/AlexKobachiJP/some-project/SomeProject.xcodeproj

The complete code is available as a gist.