Published on January 3rd, 2014 | by Luke Queenan1
Using pkgbuild and productbuild on OS X 10.7+
I recently had to upgrade our build process on OS X from using the outdated Package Maker to pkgbuild and productbuild tools. My first stop when using any new tools on a unix platform is to check the man pages – which weren’t too helpful for creating a stand alone package. I believe this is partly due to the fact that Apple is pushing their App Store model and moving people away from individually distributing applications.
The end goal here was total automation in the build process from start to finish, which is something we didn’t have the with existing Package Maker tool. Some of the steps in this process are only done once, others are done every time the application is built. These are the steps I’ll be going over:
- Generating a plist for each application bundle
- Building each pkg using the previously generated plist
- Generating a generic Distribution.xml for the final installer using the pkgs
- Editing the Distribution.xml so that you can modify:
- installer icon
- installer options
- install location
- display license files
- Building the installer with multiple pkg files
- Signing the installer
A quick Google search didn’t turn up too much info either, except for this really informative stack overflow post. I encountered a few obstacles when trying to follow these steps, so I thought I would combine the information I found there with the additional steps I had to apply and figure out. The structure of my final product was almost identical to the example used in the stack overflow post – two build projects, a main application and a helper application. I will also show you how to add a generic folder to the installer, in this case an ‘examples’ folder.
I’ve broken the steps into two sections, the first for the commands that only need to be done once, and another for the commands that need to happen every build. This is a bit confusing as these once off steps need to happen in-between the scriptable steps during the first build, but I’ll try and make this ordering as clear as possible.
These steps only need to happen once, or need to be redone when there is a change in the packages. I’ll make a note of the correct ordering in the next section.
So the first step is to create a .plist file for each of the .app bundles that you have. This is where I had the most trouble – all the commands I tried either resulted in an obscure error message or an incomplete plist file. After sifting through numerous links on Google, I came across a thread where someone suggested moving the .app bundle into its own separate folder, and running the command on the folder instead. Doing this resulted in a full plist file. (If anyone knows why this is required, please comment below.)
pkgbuild --analyze --root ./app 'My App.plist' pkgbuild --analyze --root ./helper 'Helper.plist'
You can now edit the required fields in the plist files. The man page (man pkgbuild) explains what the options mean under the COMPONENT PROPERTY LIST heading. In this case, I had to change the ‘BundleIsRelocatable’ and ‘RootRelativeBundlePath’ keys. The final file should look something like the following. Again, all of this should be generated for you by the previous command – if it wasn’t, then something is wrong with the .app bundle, such as a missing Info.plist.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <array> <dict> <key>BundleHasStrictIdentifier</key> <true/> <key>BundleIsRelocatable</key> <false/> <key>BundleIsVersionChecked</key> <true/> <key>BundleOverwriteAction</key> <string>upgrade</string> <key>RootRelativeBundlePath</key> <string>My App.app</string> </dict> </array> </plist>
The second step is to generate a “Distribution.xml” file for the final product. To do this, we use productbuild synthesize and package flags to generate a basic distribution file.
1 2 3 4 5
productbuild --synthesize \ --package 'My App.pkg' --package 'Helper.pkg' \ --package 'examples.pkg' \ Distribution.xml
This file controls how the installer acts and appears to the end user. Here’s a link (PDF) to the documentation on the Apple website. You can add images and license file pages to the installer. Here are the changes I made to add our license agreement and background image to the installer, as well as allowing the application to be installed in the root directory.
1 2 3 4
<title>My App</title> <background file="installerImage.png" mime-type="image/png" alignment="right" scaling="none" /> <license file="license.txt"/> <domains enable_localSystem="true"/>
These are the steps that you can automate in your makefile provided you’ve done the initial build with the previous steps as well. These commands should all take place in the directory where your .app bundles are located.
Begin by copying all the resource files required by the end installer into a ‘resources’ folder. If you have any scripts that need to be run before or after the installation, copy them to a folder as well. These have to be named either ‘preinstall’ or ‘postinstall’, remember to omit the ‘.sh’. Also copy the plists and Distribution.xml files over.
1 2 3 4 5 6 7 8 9
mkdir resources cp $(SRC)/installerImage.png /resources cp $(SRC)/license.txt /resources mkdir helper_scripts cp $(SRC)/postinstall /helper_scripts cp $(SRC)/*.plist . cp $(SRC)/Distribution.xml .
Now that we have the resource files, fix the permissions on the bundles so that everything is readable after the install.
1 2 3
chmod -R a+xr 'My App.app' chmod -R a+xr 'Helper.app' chmod -R a+xr examples
Once the permissions are set correctly, we move each of the .app bundles into their own folders.
cp -Rfp 'My App.app' app/'My App.app' cp -Rfp 'Helper.app' helper/'Helper.app'
During your first run through of these steps, you will need to generate the individual pkg plist files before continuing. This is the first step in the non-scriptable section. Now that we have the plist files, we can go ahead and create the individual packages. If you have pre or post scripts that need to be run, specify the folder that contains them here using the –script command, as seen with the Helper.pkg.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
pkgbuild --root ./app \ --component-plist 'My App.plist' \ --identifier "com.company.app.pkg" \ --version "2.1" \ --install-location "/Applications/Company/My App/" \ 'My App.pkg' pkgbuild --root ./helper \ --component-plist 'Helper.plist' \ --scripts ./helper_scripts --identifier "com.company.helper.pkg" \ --version "1.0" \ --install-location "/Applications/Company/Helper/" \ 'Helper.pkg' pkgbuild --root ./examples \ --identifier "com.company.app.pkg" \ --version "1.0" \ --install-location "/Applications/Company/My App/" \ 'examples.pkg'
Once you have .pkgs for each application, if this is your first run through of the steps, you will need to generate and modify the Distribution.xml file here. This is the second step in the non-scriptable section. Using the Distribution.xml that we generated earlier, execute the following to create and sign the installer.
1 2 3 4 5 6 7 8 9
productbuild --distribution Distribution.xml \ --resources ./resources \ 'pre sign app.pkg' security unlock-keychain -p pleasecodesign Company.keychain productsign --sign "Company" \ --keychain Company.keychain \ "pre sign app.pkg" "My App.pkg"
That’s all there is to it! At least for the applications I had to port over. Since I had to figure some of this our through trial and error and obscure posts online, I’m sure there are things that could be improved or missing. Let me know if you have any suggestions or improvements.