1. Debugging an ARC Mac application on Snow Leopard

    I’m currently developing a Mac application which uses Apple’s relatively new Automatic Reference Counting (ARC) in place of manual memory management or garbage collection. Thus far, I’ve been doing most of my development and testing using Xcode 4.3 running on OS X 10.7, but I wanted to start to do more thorough testing on 10.6. ARC is supported on both versions of OS X (though with a few minor restrictions on 10.6), but as I quickly discovered when I tried building my application with Xcode 4.2 on 10.6, building an ARC application requires linking against the 10.7 SDK, which is only possible if you’re running Xcode on 10.7. So while I can run the app fine on 10.6, not being able to build on 10.6 makes it a little tricky to use Xcode’s debugger to debug the application.

    After a couple days of searching and experimenting, I think I’ve finally come up with a setup that lets me debug my application relatively easily on 10.6 while still using ARC, and I figured I’d document it here. In my particular case, I’m running 10.7 on my main machine, then using VMware Fusion 4 to run an instance of 10.6 in a virtual machine, but the setup explained here should work just as well with two machines connected on a network.

    Workspace setup

    The first step in the process is to get things set up so that you can run your application from within Xcode 4.2 on your 10.6 machine. To access the application that is built on the 10.7 machine, I used standard file sharing in the Finder to connect from one machine to the other (I initially tried using VMware’s folder sharing feature, but it turned out to have some weird quirks and not work correctly a lot of the time).

    I wanted to open my Xcode workspace file in Xcode 4.2, but I figured that directly using the same workspace file that was being used by Xcode 4.3 on the other machine might not be the best idea, in case one saved changes under the other one’s nose, so I created a separate copy of the workspace (still one project file though) and opened that in Xcode 4.2. Later on, when I discovered I didn’t need to still have Xcode 4.3 running on my 10.7 machine (more on that later), this step turned out not to be necessary, but if you do want to have both versions of Xcode running at the same time, it’s probably best to avoid having them confuse each other.

    Since we don’t want to actually build anything in Xcode 4.2, use the “New Scheme” command and create a scheme with its target set to “None”, so no building will take place when using that scheme. Once the scheme is created, edit the scheme, and click on the “Run” action. Select “Other…” from the Executable pop-up menu, then go and select the built application that’s residing on your 10.7 machine. It’ll be hidden away in the Xcode “DerivedData” folder, the default location being ~/Library/Developer/Xcode/DerivedData. Once that’s set up, clicking the “Run” button with that scheme active should run the application from within Xcode.

    Debugging symbols

    You’ll notice when running the app that gdb spits out a whole bunch of messages to the console complaining about not being able to find various .o files. Normally, gdb looks here for debugging symbols that it uses to be able to display variables, stack traces, and other information while debugging. However, the .o files are referenced by absolute path when built, so when that information is stored on the 10.7 machine but being accessed from the 10.6 machine, the paths are different, so gdb is unable to find those files.

    The solution to this is fortunately pretty easy. In Xcode on the 10.7 machine, bring up the build settings for your application target, find the “Debug Information Format” setting, and change it to “DWARF with dSYM File”. This will cause a separate .dSYM file to be generated alongside your application when built, which contains all the debug information in one place and doesn’t depend on absolute paths. When running the app on 10.6, Xcode should be able to pick up the debugging info it needs from the dSYM file.

    Source Code

    We’re now able to debug the application, but if we hit a breakpoint, Xcode shows us a disassembly of our code rather than the actual source code. This is a similar problem to above, where gdb is looking for the source code files using absolute paths, but the paths are now different when running on the 10.6 machine. The best workaround I’ve found for this is the gdb command “set pathname-substitutions”, which basically allows you to tell gdb to treat any file paths starting with a particular prefix as though they started with a different prefix.

    So in our case, you would want to run “set pathname-substitutions /path/to/source/on/10.7/machine /path/to/source/on/10.6/machine”, of course putting in whatever the paths are on your particular setup. Since I didn’t want to have to type this in manually every time we start a new gdb session, I decided to create a .gdbinit file in my home directly and add the command there. gdb will automatically execute commands in that file whenever it starts, so that automates getting the source files to show up.

    Automating the build

    I had things pretty well set up at this point, where I could switch over to Xcode 4.3, edit and build my project, then go back to Xcode 4.2 and debug the newly built application from there. This quickly got tiring though, so I looked for some way to automate this process as well.

    I did this by taking advantage of the “pre-actions” provided by Xcode when setting up a scheme. This lets you specify an arbitrary script that should be run before running your executable each time. To set one up, select “Edit Scheme” from the Product menu, click on the disclosure triangle next to the “Run” action in the scheme editor, then click on the “Pre-actions” item that’s revealed. Click the “+” button at the bottom and select “New Run Script Action” from the pop-up menu.

    The actual building can be done using the xcodebuild command line tool, but of course the tricky part is that we want that to be done on the remote 10.7 machine instead of the 10.6 machine the script is being run from. Fortunately, this can be done pretty easily through ssh, so your script would look something like:

    ssh username@lion-machine-name.local “xcodebuild -workspace ‘/path/to/my.xcworkspace’ -scheme ‘My Scheme’ -configuration ‘Debug’ build”

    You’ll need to have ssh set up so you can connect using a public/private key pair, because otherwise the script will prompt you to enter your password to log in to the remote machine and just hang there. If you need to set this up, just search the web for “ssh keys howto” and you’ll be given plenty of guides on setting ssh up.

    So, now that that’s set up, clicking the “Run” button in Xcode 4.2 will fire off a build on the 10.7 machine, then when it’s done, run the newly built product on the 10.6, all ready for debugging! The only thing I haven’t figured out yet is how to tell when a build error occurs on the remote machine, so you sometimes still do have to switch back over to the Lion machine and build manually in Xcode 4.3 there if you want to see the details of the build error.

    Quirks

    I have run into a couple quirks with this setup still. If I have the project open in both Xcode 4.2 and 4.3 at the same time, sometimes both copies of Xcode got into some weird indexing loop, where one will start indexing, and somehow that will trigger the other copy to reindex files on its end, and so forth in a never ending loop. I’m still not quite sure why this was happening, since each one has its own indexing data stored within each machine’s home folder, but if this happens to you, you’ll probably just need to quit one of the two copies of Xcode.

    Occasionally, when attempting to run the app, I’ll get an error message from dyld saying that it couldn’t find one of the frameworks that’s embeddde inside the app bundle. The framework is indeed there though, so I’m not quite sure why this happens. I assume it must be some odd networking quirk running the app via file sharing. Usually either just trying a few times, rebuilding the app again, or poking at the build folder in the Finder will clear things up. I might investigate putting some “touch” commands in my pre-run script to see if that cuts down on instances of this.