Reproducible Builds on iOS

Dear Apple Developer Forum!

I'm in need of help regarding an issue that has to do with binaries.

I'm building an iOS App that needs a fingerprint of its binaries, exclusively based on the source code written. A "reproducible" build, meaning that when I compile it on my machine and run checksum on it, the output (hash) will be the same, as if another device clones the project, compiles and checksums the values.

The App depends on swift packages which depends on Swift Packages, which I've managed to compile to .o files, convert to .a files (static frameworks) and create xcframeworks, which the App depends on. They work great, once compiled, their checksum value does not change when App is compiled (unless source code of them is changed of course), but the Apps executable (checksummed inside the IPA) changes every time it's compiled. I'm guessing that perhaps the Xcode compiler injects a timestamp or other unique identifier in the binaries?

Is there any way to have "reproducible" builds on iOS (Swift Xcode)?

All input is greatly appreciated,

Thank you very much,

Kind regards Johan.

Answered by DTS Engineer in 821169022

The stable identifier for builds coming from Xcode is the build UUID that is embedded as part of the Mach-O metadata. This is what ties an executable or framework together with its associated dSYM file for things like crash symbolication. So rather than using a checksum (without knowing what you're trying to accomplish), you might be better off by looking for the build UUID. You can get it by running dwarfdump --uuid /Path/To/Binary.

The build UUIDs produced by Xcode should be identical across builds for the following conditions:

  • Exact same source code
  • Exact same Xcode version
  • Exact same build settings

If any one of those conditions change, then the build UUID will change. But if you run back to back builds for the purposes of testing this, then the dwarfdump command will show you the builds are identical via the same UUID.

We have documentation for this focused on what goes into debug symbol generation, but the Overview outlines what I said above.

— Ed Ford,  DTS Engineer

The stable identifier for builds coming from Xcode is the build UUID that is embedded as part of the Mach-O metadata. This is what ties an executable or framework together with its associated dSYM file for things like crash symbolication. So rather than using a checksum (without knowing what you're trying to accomplish), you might be better off by looking for the build UUID. You can get it by running dwarfdump --uuid /Path/To/Binary.

The build UUIDs produced by Xcode should be identical across builds for the following conditions:

  • Exact same source code
  • Exact same Xcode version
  • Exact same build settings

If any one of those conditions change, then the build UUID will change. But if you run back to back builds for the purposes of testing this, then the dwarfdump command will show you the builds are identical via the same UUID.

We have documentation for this focused on what goes into debug symbol generation, but the Overview outlines what I said above.

— Ed Ford,  DTS Engineer

Unfortunately it does not seem to work exactly as intended. I've switched from Swift Packages to Xcode Frameworks. When I arhive and export the IPA and run dwarfdump on the following files, I get the UUIDs:


Dwarfdump for App executable UUID: 828B8C63-D5CC-3923-A94F-668DF5823512 (arm64) /Users/jsa/Documents/Repositories/App/IPAs/Payload/App.app/App

dwarfdump for LibraryA of App.app/Frameworks/LibraryA.framework UUID: 76C2661C-6145-3E40-B476-29A000FFA3EA (arm64) /Users/jsa/Documents/Repositories/App/IPAs/Payload/App.app/Frameworks/LibraryA.framework/LibraryA

dwarfdump for LibraryB of App.app/Frameworks/LibraryA.framework/Frameworks/LibraryB UUID: A21C3BA2-4D81-3970-8D15-A21CCCAF78CD (arm64) /Users/jsa/Documents/Repositories/App/IPAs/Payload/App.app/Frameworks/LibraryA.framework/Frameworks/LibraryB.framework/LibraryB

dwarfdump for LibraryB of App.app/Frameworks/LibraryB.framework UUID: AB28D8FB-B918-3AD0-AAEA-DE28007E0E3F (arm64) /Users/jsa/Documents/Repositories/App/IPAs/Payload/App.app/Frameworks/LibraryB.framework/LibraryB


If I then change one print statement in LibraryB, I would expect only the UUID of LibraryB to change, but it seems both LibraryA and LibraryB changes. (Mind you LibraryA depends on LibraryB, but no code was altered in LibraryA)


Dwarfdump for App executable UUID: 828B8C63-D5CC-3923-A94F-668DF5823512 (arm64) /Users/jsa/Documents/Repositories/App/IPAs/Payload/App.app/App

dwarfdump for LibraryA of App.app/Frameworks/LibraryA.framework UUID: E971D17E-507F-303A-8E5B-4FEFB745CEDB (arm64) /Users/jsa/Documents/Repositories/SampleApp/IPAs/Payload/App.app/Frameworks/LibraryA.framework/LibraryA

dwarfdump for LibraryB of App.app/Frameworks/LibraryA.framework/Frameworks/LibraryB UUID: A21C3BA2-4D81-3970-8D15-A21CCCAF78CD (arm64) /Users/jsa/Documents/Repositories/SampleApp/IPAs/Payload/App.app/Frameworks/LibraryA.framework/Frameworks/LibraryB.framework/LibraryB

dwarfdump for LibraryB of App.app/Frameworks/LibraryB.framework UUID: 52CDA880-EADD-3B1C-BD3F-21996878F6F9 (arm64) /Users/jsa/Documents/Repositories/SampleApp/IPAs/Payload/App.app/Frameworks/LibraryB.framework/LibraryB


Thank you vey much for your previous response, hope to hear from you again. Kind regards!

One thing I'm noticing in your output above is the nested frameworks, App.app/Frameworks/LibraryA.framework/Frameworks/LibraryB.framework. Nesting like that is not supported on iOS, all frameworks need to be siblings located in App.app/Frameworks/. I can't say for sure with just log messages in front of me if that's a factor in the build UUID differences, but it could be. Are you able to model all of this in a small test project that you can share the link to? Also, I'd like to know what bigger picture problem you're trying to solve.

— Ed Ford,  DTS Engineer

Dear Apple Engineer,

Thank you very much for getting back to me. Absolutely, let me share everything.

  • https://biometricdk-my.sharepoint.com/:u:/g/personal/jsa_biometric_dk/Eb_vXDZPslhOuctfHhWky28BX9NVymf5rGDGHZbE1GZg_Q?e=M3GqXn

Use this link to download all the sample projects. Mind you, in this context, CameraHandlingFramework = LibraryB, MiddlemanFramework = LibraryA SampleApp = App

  • https://biometricdk-my.sharepoint.com/:u:/g/personal/jsa_biometric_dk/ES_QjOarp1JKp93sKAE6BNwBl1ilMenTmWxHW2QxsFcYeg?e=mo9q7h

Use this link to download a script I've built, which builds alle the projects, archives the frameworks and converts them to xcframeworks, archives the App to an IPA and dwarf-dumps all the executables in the IPA file.

  1. All the paths in the script are absolute, so please place the projects somewhere on your machine, and alter the script so the paths match you project destinations.
  2. Run the script, which should fail. CameraHandlingFramework.xcframework should have been created in the final_frameworks folder.
  3. Open MiddlemanFramework project, and reference the CameraHandlingFramework.xcframework in Middleman Framework project.
  4. Run the script again, it should fail. MiddlemanFramework.xcframework should have been created in the final_frameworks folder.
  5. Open Sample App project, and reference both CameraHandlingFramework.xcframework AND MiddlemanFramework.xcframework, which now should be available from final_frameworks folder.
  6. Run the script one more time, and you should see the UUIDs of the frameworks and App.
  7. Now change the string in generateStaticString() in CameraHandlingFramework project
  8. Run the script one last time. You will now experience the issue, where UUIDs are not behaving as they should (described in my previous comment).

Hope that made sense, let me know if there is anything else you need from me.

Kind regards,

Johan

Reproducible Builds on iOS
 
 
Q