Posted on September 28, 2010 at 5:42pm EDT. More.

File references relative to DERIVED_FILE_DIR in Xcode

Let’s say you have a Mac or iOS app which stores its data in an SQLite database. It looks for this database file on launch; if missing, the app copies a template file from its bundle. How do you configure your Xcode project to store that template file in the app bundle?

Let’s also say you have a source file of SQL commands, checked into source control, that you use to generate the database file. You could manually generate the database file and add it to your project, but you want a clean build process, with no steps to remember between checkout and build.

The right way to accomplish this is by adding a Run Script Build Phase to your target, so that when you build, Xcode creates the database file for you.

Xcode lets you specify the Input Files the script depends on and the Output Files it generates, so that it can examine modification dates and determine if the script needs to be run at all. When you add a new Output File to the list, the default value is $(DERIVED_FILE_DIR)/myfile, which seems to be a pretty clear endorsement of that location as the place to store generated files.

So let’s say you write the script and save the database file to $(DERIVED_FILE_DIR)/Template.db. You add a reference to this file to your Xcode project (navigating to a DerivedSources folder somewhere within the Intermediate Build Files directory). When you build, Xcode generates your file (Run Script phase) then copies it into your app’s bundle (Copy Bundle Resources phase). Hooray!

Then you change the build configuration from Debug to Release, and it breaks. Why?

The problem is the file references. The DERIVED_FILE_DIR location depends on the current target and build configuration. When you switched from Debug to Release, the script dutifully generated a file in the correct location, but the file references are looking in the wrong place. The Debug location is hardcoded into the path.

Select any of the files and hit Cmd-I, and you’ll see a Path Type popup menu that tells Xcode how to resolve that file’s path. Here are all the options:

At this point, you could give up and modify your script to write the file directly into the app bundle. But that would be so dirty!

It turns out you are not limited to only those options. Additional source trees you define in Xcode Preferences will appear in that popup menu. Here’s what to do:

  1. If you haven’t already, organize all the derived files into a DerivedSources group, and set the Path Type of each to be Relative to Enclosing Group. If done properly, the Path: field should contain only the file name. (If that’s not the case, click Choose… to select the file. If the file doesn’t exist, do a build to create it.)
  2. Open Xcode Preferences and select Source Trees.
  3. Add a new source tree, with Setting Name as DERIVED_FILE_DIR, Display Name as Derived Files, and Path as $(DERIVED_FILE_DIR).
  4. Click OK.
  5. In the project window, select the DerivedSources group and hit Cmd-I.
  6. Change Path Type to Relative to Derived Files.
  7. Unfortunately, the Xcode GUI is not able to resolve DERIVED_FILE_DIR properly; the Full Path will be missing the active target’s name. As a result, the path is invalid, and Xcode will fill in the Path field trying to find it’s way to a DerivedSources folder. We don’t want that, we want Path: to be None. I know of no way to do that within Xcode, so we’ll have to edit the project file. Take a deep breath.
  8. Close Xcode.
  9. Use a text editor to open the project.pbxproj file inside your Xcode project bundle.
  10. Find the DerivedSources section. It will contain a few key-value pairs: isa, children, name, path, and sourceTree. Delete the path line and save the file.
  11. Reopen your project file.

The group will appear in red, indicating that the Xcode GUI can’t find them, but when you perform a build Xcode will look in the right places and use the correct files.

I figured this out by experimenting on my own, so it’s possible that these instructions won’t work for you, or that there’s a better way to do this. If that’s the case, let me know, and I’ll update this post.