I recently was working on a Mac app to flash an Arduino using the open source project avrdude in my Mac app, and got it working as expected. I then looked at submitting the app to the Mac App Store. I had expected that the avrdude binary needed to be signed and placed in the correct location in the app bundle. However, it didn’t work out that way. Turns out that there are some very specific steps and concepts that need to be considered when including a command line tool in a Mac App.
I had access to the avrdude source code, but didn’t want (or need) to add the code to Xcode for building since it built fine using make. Things are a lot easier if you are creating the command line tool in Xcode from scratch, but most command line utilities don’t have Xcode project files. I compiled the command line tool avrdude and had Xcode copy to the resource folder of the application bundle. avrdude was signed, but it couldn’t access the serial ports (shown in Console as sandboxd error). This was all expected. I created an entitlement in Xcode, and signed the binary using that entitlement. Here is the contents of the entitlement file that later caused much heartburn. If you arrived here via Google and skimmed to this part, don’t just copy and paste this. You have been warned.
<?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"> <dict> <key>com.apple.security.app-sandbox</key> <true/> <key>com.apple.security.inherit</key> <true/> <key>com.apple.security.device.serial</key> <true/> </dict> </plist>
I then signed the existing binary using this entitlement:
codesign --force --entitlements avrdude.entitlements -s "Mac Developer: Timothy Perfitt (Y4UE4LMSUA)" avrdude
It worked great! The command line tool was correctly signed, I could turn on sandboxing for the main macOS app, and it worked fine. I tried to submit the app to the App Store and that is where the wheels came off.
Whenever I submitted the app to the Mac App Store with Xcode, Xcode would fail, complaining about the command line tool not having an Info.plist (“Couldn’t find platform family in Info.plist CFBundleSupportedPlatforms or Mach-O LC_VERSION_MIN for avrdude):
An Info.plist is what defines different properties about the app and is in the app bundle. However, a command line binary doesn’t have a bundle with an Info.plist in it. From the error message, it looks like the App Store needs to know the minimum OS version for the binary and needs that info inside the binary itself. Using Google searches turned up some pretty scary articles about injecting information in Mach-O headers.
I knew that you could add in an Info.plist to a command line tool and have done so in the past; however, that was for a tool whose source code was already in Xcode and the compiler settings could be tweaked (“CREATE_INFOPLIST_SECTION_IN_BINARY=YES”). The tool I was using was outside of Xcode but I could recompile it. I found that you could tell GCC to inject an Info.plist into a binary during compilation by including a CC variable:
./configure CC=”gcc -sectcreate,__TEXT,__info_plist,/path/to/avrdude-info.plist”
The Info.plist contained both an LSMinimumSystemVersion and CFBundleSupportedPlatforms:
<?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"> <dict> <key>LSMinimumSystemVersion</key> <string>10.14</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleSupportedPlatforms</key> <string>MacOSX</string> <key>CFBundleIdentifier</key> <string>com.twocanoes.MacDeployStick</string> </dict> </plist>
After recompiling, the binary now contained the plist but I was getting the same error when submitting to App Store.
I am starting to suspect that code signing likes it when I cry.
— timo (@tperfitt) January 6, 2019
I then turned to looking at the second part of the error message: Adding LC_VERSION_MIN to the Mach-O headers. I didn’t want to change the binary headers, as I had the source code. I did find some info that mmacosx-version-min as a GCC flag inserts the LC_VERSION_MIN in the Mach-O header, so I played around with providing the GCC flag in configure. This seemed to work:
./configure CC="gcc -mmacosx-version-min=10.12"
Using otool -l avrdude showed that the Mach-O header now had “LC_VERSION_MIN_MACOSX”:
Load command 9 cmd LC_VERSION_MIN_MACOSX cmdsize 16 version 10.12 sdk 10.14
Success! I signed the app using the entitlement I had before and it now validated for submission to the App Store. Done, right? Nope. During testing, the command line tool continually crashed with an “Illegal Instruction: 4” both in the app and when I ran the tool outside the app on the command line. Turning off code signing (or not signing the app) make the issue go way, but code signing is required for submitting to the Mac App Store.
After much searching, it seemed to be related to the entitlements:
The reason I asked this is that, once you add the sandbox inheritance entitlement (com.apple.security.inherit) to a command line tool, the only way to run that tool is by sublaunching it from your sandboxed app. You can’t run it from Terminal because in that context it has no sandbox to inherit.
Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware
It didn’t exactly apply to me, since I was getting the same whether it was launched in the app or from the command line. Then I found this:
If you embed a executable within a sandboxed app it must have the
com.apple.security.app-sandbox
andcom.apple.security.inherit
entitlements, and only those entitlements.Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware
So, the extra rule for the serial port in the entitlements file for the command line tool was causing the app to crash. I removed the serial port entitlement line from the command line entitlements file, re-signed it, and then added the serial port entitlement to the macOS app. The command line tool would then inherit the entitlement when launched from the main app, and work as expected.
Long and short of it:
- The command line tool must have a Mach-O load command for LC_VERSION_MIN_MACOSX. It can be set using the GCC flag “-mmacosx-version-min=10.12” (change 10.12 to what makes sense).
- Command line tools must be signed with an entitlement that has exactly 2 rules: sandbox and inherit. It can be set with the codesign command. All other rules are inherited from the main app and should be set there.
- Pretty sure that the command line tool must be in the MacOS folder or a perhaps a folder named “Helpers”. I put mine in the Executables folder in a Copy Files build phase:
- Quinn “The Eskimo!” rocks!
The battle was long and hard-fought, but I have emerged victorious over the rebel factions who refer to themselves as “code signing” and “entitlements”. The war is not over, but tonight I sleep knowing my app uploads.
— timo (@tperfitt) January 6, 2019