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

I built your sample project, and while I can see what you mean about the different build UUIDs, but the set up of your build configuration is the real source of why those UUIDs change here.

The contents of CameraHandlingFramework is a static library, which is depended upon by both MiddleMan and SampleApp. During the build process, that static library is getting turned into a dynamic library as an implementation detail of the build, so now you're winding up with two different libraries called CameraHandlingFramework that have independently converted to a dynamic library during the build. So they are getting different build UUIDs because the inputs creating them as dynamic libraries are independent and different, which breaks the "identical build settings" part of what I said above regarding stable build UUIDs.

In addition to the UUID side of this, this configuration will cause you 2 other significant problems:

  • As I mentioned previously, a nesting like App.app/Frameworks/LibraryA.framework/Frameworks/LibraryB.framework is not supported by iOS
  • And even if it were, you now have two copies of the same library with the same function names loaded into your process.

That second point is undefined behavior — there's 2 possible functions the system can call for something in those libraries, and which one the system picks is undefined. It might not be noticeable here because the code in those libraries is identical, but imagine if the code was different in each library, and you couldn't guarantee which library would be used at any point in time.

The answer to all of these issues is for your CameraHandlingFramework to be a dynamic library, not a static library. A general rule of thumb to use here is that if more than one target in an app is going to depend on it, it should be a dynamic library and not a static library. As part of that switch, the MiddleMan framework also should not be configured to embed CameraHandlingFramework. This then avoids the problem in my first bullet point above.

With that switch, you'll get an app that has a single copy of CameraHandlingFramework, which lives as a sibling to MiddleMan inside of SampleApp.app/Frameworks. With the build UUIDs then, the dynamic library inside of the original XCFramework that you prebuild will carry through to the final sample app, since it's only built once, and there's no transformation from static to dynamic in the middle.

— Ed Ford,  DTS Engineer

Hi! Thank you so much for getting back to us!

I've attempted to implement your suggestions, but with limited luck (although I feel like we're getting closer!).

Instead of compiling each library and adding them as a dependancy to each other, I'm now just adding .framework files to the main project, compiling and extracting the DWARFDUMPs from the compiled Apps dependancies all at once (see script attached at bottom). Adding the frameworks as .framework (not compiled) to the App, results in the IPA frameworks folder only containing one instance of each framework (as siblings). I've also made sure CameraHandlingFramework is dynamic and not embedded in MiddlemanFramework.

The result is not quite as expected, please see table:


RunsChangeSample AppSample App/Middleman FrameworkSample App/Camera Handling
1.1return "String from bottom package! 1" (change in CameraHandling)DE644B74-0A0A-31DF-8BDA-DA08CEC3577001EC531B-9669-3CDF-8027-95D6903B81E56F5D51D9-6EBD-3145-890C-B08A349F9FDB
1.2return "String from bottom package! 2" (change in CameraHandling)DE644B74-0A0A-31DF-8BDA-DA08CEC35770891DACF9-A25C-3D64-83E3-0617063F93C5FFFE9A73-86F8-38B4-B58E-1FAF88FE6798
1.3return "String from bottom package! 1" (change in CameraHandling)DE644B74-0A0A-31DF-8BDA-DA08CEC357708017A92D-3016-3426-80EB-574F23BF075C6F5D51D9-6EBD-3145-890C-B08A349F9FDB
2.1return StringGenerator.generateStaticString() (change in Middleman)DE644B74-0A0A-31DF-8BDA-DA08CEC357708017A92D-3016-3426-80EB-574F23BF075C6F5D51D9-6EBD-3145-890C-B08A349F9FDB
2.2print("hello")\nreturn StringGenerator.generateStaticString() (change in Middleman)20159395-93F7-3E7D-8849-B1B72ABF3E20578F83DD-FEC8-31F3-822F-32A5550979B36F5D51D9-6EBD-3145-890C-B08A349F9FDB
2.3return StringGenerator.generateStaticString() (change in Middleman)DE644B74-0A0A-31DF-8BDA-DA08CEC35770428E2617-034A-38DE-85A9-4838200DEDD26F5D51D9-6EBD-3145-890C-B08A349F9FDB

The first attempts (1.x), we change a simple string in the camera handling framework without any other changes. Sample App dwarfdump remains the same (great), but the Middleman framework dwarfdump changes every time the camera handling framework is changed (unexpected).

The second attempts (2.x), we add a simple print statement in the middleman framework. It behaves more strange than when changing camera handling, as it doesn't revert back to its original dwarfdump when changes are reverted back to original. Sample App dwarfdump also changes upon change of middleman framework, but at least reverts back to normal, when changes in middleman framework is reverted back to normal. Camera Handling in this instance does not change at all (great!).

Please see the updated project and updated script in the following links:

  • Script: https://biometricdk-my.sharepoint.com/:f:/g/personal/jsa_biometric_dk/EpF2-PKqyfJEhuDu9iNA7ZAB6tAm1USEMj6hoJqcF8kFSQ?e=WQlnCE
  • Project(s): https://biometricdk-my.sharepoint.com/:f:/g/personal/jsa_biometric_dk/Er2vL6tLsA9GvA_LD_vCq_MBiBFyiAmfScyWIx9p39963A?e=4Eoz34

Paths in projects and scripts are defined absolutely, you have to fix this (fyi).

It would be great if you could tweak the projects so we could get the desired result, which is:

  1. When Camera Handling Framework is changed (not interface, but internal code that doesn't require adaptation from hosts), that Sample App and Middleman Framework dwarfdump UUIDs persisted.
  2. When Middleman Framework is changed (not interface, but internal code that doesn't require adaptation from Sample App), that Sample App and Camera Handling framework dwarfdump UUIDs persisted.
  3. When Sample App is changed, Middleman Framework and Camera Handling Framework dwarfdump UUIDs persisted.

Thank you so much, You've been a great help so far improving our understanding. I feel like we're close! Thanks again, Kind regards!

@DTS Engineer Any update on this? Kind regards.

@JohanBiometric, I've been away for a little bit, so I'm just catching up to your follow-up here now.

One of the things that I see confusing the understanding of things here is that Middleman Framework has its MACH_O_TYPE build setting set to static. That's not invalid, but for the sake of discussion, let's have you configure everything as such:

  • All framework code has MACH_O_TYPE set to Dynamic Library
  • App target lists the frameworks as Embed & Sign in the General tab
  • Middleman Framework target has reference to Camera framework described as Do Not Embed in the General tab.

This configuration will result in the following file structure inside of the Xcode archive (abbreviated for clarity in this discussion):

SampleApp3.xcarchive
├── dSYMs
│   ├── CameraHandlingFramework.framework.dSYM
│   ├── MiddlemanFramework.framework.dSYM
│   └── SampleApp.app.dSYM
└── Products
    └── Applications
        └── SampleApp.app
            ├── Frameworks
            │   ├── CameraHandlingFramework.framework
            │   └── MiddlemanFramework.framework
            ├── Info.plist
            └── SampleApp

This configuration has your 3 binaries inside the built app (executable or dynamic framework), and the 3 matching dSYM files. One of the most important reasons build UUIDs matter is to aid in crash symbolication, and so if you run dwarfdump on either the framework binary or on the inner contents of the dSYM file that contains the DWARF (it's a few layers down in the dSYM bundle hierarchy), the UUID values match. In your project, the Middleman framework is missing from that dSYM directory because it was built as a static library, though you were still getting a .framework inside of the app too. With this configuration, you should see the build UUIDs remain the same when the criteria I mentioned previously is met:

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

That is to say, once you get the project configured as I suggest above, changing that string inside your string generation function should affect only the build UUID of the single library file on disk (and its associated dSYM file), and not any of the others, because only the source code to one final linked binary is affected.

A few posts ago, I asked what you're looking to accomplish with a stable build UUID, which is relevant to bring up again here. I presume you configured the build to contain a static framework for a particular reason, since that's not the default configuration created by File > New Target > Framework in Xcode. There's a few related topics here — dynamic vs static libraries, mergable libraries, as examples — which affect how things are built, and that in turn will affect the build UUIDs, and I don't know right now how those choices will meet or not meet your goal for the build UUID.

— Ed Ford,  DTS Engineer

Reproducible Builds on iOS
 
 
Q