Search code examples
macoscocoaxcode7mac-app-store

Xcode app archive validation fails for Mac App Store distribution due to lack of LC_VERSION_MIN in a Mach-O header


I have a Swift project which calls a command line utility using NSTask.

For portability, I have included that command line utility in the bundled resources of the app.

The utility is quite complex - it's a Ruby interpreted application, with a bunch of gems, some of which have native extensions. The gems are all installed with Bundler into a standard vendor/ directory within the utility.

I have successfully archived, validated, distributed, and run the app on a second Mac which does not have that CLI utility (or Xcode) installed through the Developer ID ('outside the Mac App Store') workflow several times.

However, the archive failed to validate for Mac App Store distribution with this error:

2016-08-28 15:26:41 +0000 [MT] Presenting: Error Domain=DVTFoundationNSBundleAdditionsErrorDomain Code=1 "Couldn't find platform family in Info.plist CFBundleSupportedPlatforms or Mach-O LC_VERSION_MIN for AbstractMemory.o" UserInfo={NSLocalizedDescription=Couldn't find platform family in Info.plist CFBundleSupportedPlatforms or Mach-O LC_VERSION_MIN for AbstractMemory.o}

So... why would app archive validation fail in the MAS workflow, but not the Developer ID workflow, with such an error?


Potentially interesting points:

  • The app uses 1 temporary exception entitlement to let it read and write a particular folder inside the home folder without having to nag the user for access.
  • The app does not use any MAS specific capabilities / entitlements.
  • AbstractMemory.o is part of the native extension for the FFI ruby gem, which is used by the CLI utility.
  • I've tried validating with Debug Information Format set to DWARF, and also to DWARF + dSYM, but the same problem happens.
  • No .dSYM files are listed explicitly in the 'Copy Bundle Resources' settings (though they might be hiding in the folder or gem native extensions of the CLI executable).
  • Xcode seems to latch on to certain bits of the native extensions and tries to treat them in special ways. The following bits are hoisted to the top level of the 'Binary and Entitlements' list in the validate dialog, just underneath the main .app artifact, while other bits (like plain text files in the gem dependency folders) are not:
    • Executables (e.g. iconv, xmlcatalog)
    • .o files (e.g. AbstractMemory.o)
    • .bundle files (e.g. ffi_c.bundle, nokogiri.bundle)
    • .dylib files (e.g. libcapi.dylib, libcharset.dylib)
    • .a files (e.g. libcharset.a)
  • In the 'Binary and Entitlements' list, the main .app is shown as having the expected number of entitlements (the ones I added), but the executables, .o, .a, .bundle, and .dylib files are shown as having 0 entitlements.

Solution

  • TLDR

    The dependency project had originally been compiled with a really old version of Xcode and OS X, so a number of Mach-O executables inside it did not have the LC_VERSION_MIN_MACOSX load command in their headers. Basically I had to find a way to squeeze that load command in.

    The only surefire and clean way to do that is to recompile all the native code in the dependency project using the Xcode 7 build tools. If the dependency is open source you could do it yourself. If not you'll have to plead with your software vendor.

    If you need Yosemite Deployment Target compatibility, you could risk it and use Xcode 7.1 GM on a Yosemite build box. But I recommend that you recompile the code with Xcode 7.1+ on an El Capitan build box, to ensure your binary dependency plays nice with El Capitan things like SIP.

    Details

    In my case, the dependency project was open source, so I was able to:

    1. Track down its source code on GitHub.
    2. Fork it.
    3. Build the project without making any changes on a version of OS X that matched, as far as possible, the version that the 3rd party devs originally used on their build box to compile the dependency.
    4. Look at the build log when (if) it fails.
    5. Make all the necessary changes to get the build green. You have now reached parity with the original build box that was used.
    6. Now update the build box OS X version to either Yosemite + Xcode 7.1 GM (if you must have Yosemite compatibility) or El Capitan + Xcode 7.1+.
    7. Rebuild, fix, yada yada.
    8. When it's green again, grab the newly compiled binary artifact.
    9. Find a Mach-O executable inside the bundle.
    10. Run otool -l [path to executable].
    11. In the big list of Mach-O header load commands, you should now see LC_VERSION_MIN_MACOSX of 10.10 or 10.11 (depending on what you used).
    12. Your binary artefact should be good to go.

    I was able to set up multiple OS X build VMs without going mad by using Travis CI's Mac build fleet.

    Remaining non-showstopper problems

    "Include symbols for debugging" had to be unticked for archive validation or export. This is because the symbolicator could not deal with the third party object code. I guess this is because it was built for release, and so the object code did not come with any accompanying debug symbols.

    If you cannot recompile the code

    If you are not able to recompile the code I've heard rumours of dirty workarounds that can stuff Mach-O load commands into a pre-existing binary. This suffers from the following problems:

    • This approach is very fragile because it relies on there being sufficient blank bits left in the header for your new load command. Unsurprisingly I've heard that modifying the value of an existing load command tends to be more reliable than inserting a new one.
    • The binary must not be signed already. Gatekeeper will instantly reject a binary which has been tampered with in any way after it was signed.
    • Mach-O header rewriting is not a trivial task, so it tends to be done with the assistance of third party tools. If you are not used to working at the binary + hex editor level, it is very hard to verify what these tools have done, so you will be putting an awful lot of trust in them to (a) do it correctly and (b) not do anything nefarious.

    Extra discussion

    I cross-posted this question to the Apple Developer forums at https://forums.developer.apple.com/message/175427 to see if it would get answered any faster by a more specialist community. It wasn't, but you may find additional discussion of the problem through that link.