iOS Build Validation

22 Sep 2018


This post is about techniques useful for validating iOS builds for people familiar with iOS development.

Background

An iOS app is the product of running multiple compilers, code generators, and scripts. The complexity of this process adds fragility to development, causing late nights and hot fixes for developers of top apps in the App Store. Through build validation, hard to catch regressions and bugs can be squashed ahead of time.

During a complex migration of an iOS app from Xcode to Bazel, techniques mentioned at the Bazel Conference were applied to ensure the iOS Bazel build was the same as the Xcode one.

“Build validation” can be useful to assert correctness in other contexts, like build rule changes, Xcode updates/changes, validating XCHammer generated Xcode projects, comparing CI builds, or large refactors.

Build Validation and Techniques

Validating the product of an iOS build is not achievable by naively introspecting the output .app. If 2 builds produced identical output, then it’d be possible to compare the raw bits on disk. In practice, this won’t work: some tools can’t produce an identical output every time. Additionally, bit level comparisons alone wouldn’t provide enough info to identify root causes of deltas.

By intelligently validating the build steps, inputs, and outputs, it’s possible to produce better insights. The remainder of the post is about validating a .app and validating the steps to produce a .app.

Output validation

A .app is produced from a build to be later loaded on a users device. The application directory contains several bits e.g. the main binary of the app, dynamically loadable code ( frameworks ), application extensions, nib files, compiled asset catalogs, machine learning models, arbitrary text files, images. The Apple developer website includes a complete description of this package.

Output validation at the .app level can find differences regardless of how each was produced. This process requires processing each file in the .app uniquely. Here’s a few non exhaustive examples of things worth checking between 2 built apps:

Executables and binaries

Binaries can be checked by reading segments to textual output. For example:

What’s in the symbol table?

nm -an Some.app/Some

What libraries are linked?

otool -l Some.app/Some

Other information, like size is worth checking

What’s the size of the app?

du -hs Some.app/

Exhaustive techniques like binary size profiling could be applied too.

App Capabilities

Capabilities can be checked by introspecting plists, entitlements, and provisioning.

What entitlements are used?

codesign -d --entitlements :-  Some.app/Some

Whats in the provisioning profile?

security -D -i Some.app/Some/embedded.mobileprovision

Build command validation

Today, iOS build systems run many commands: an invocation to some program with some arguments and inputs. By validating the commands ran during the build, differences in tooling and inputs can be uncovered.

First, there needs to be a way to extract the commands. A tool like XcodeCompilationDatabase can extract most of the compiler invocations from an Xcode build. After extracting the actual commands, it’s possible to compare the inputs e.g. a checksum of clang, a hash of a source file, the compiler arguments.

Clang compilation

By using a CompilationDatabase, it’s possible to read in the actual invocations used for source files:

{ "Some.c": "/path/to/clang -Wall -c Some.c -o Some.o" }

By reading this file, we can tell that Some.c is part of the build. Now, it’s possible to compute a checksum of the source, Some.c, the compiler, /path/to/clang and the corresponding arguments. In practice, comparing 2 clang invocations is not trivial and requires knowing about how the compiler will use each argument. e.g.: ordering matters, there are special rules in clang to determine how arguments are parsed, header files are determined by searching of include paths.

A few data structures and utilities exist in clang that can help normalize arguments, like the diagtool. In some scenarios, it’s insightful to validate at the object file level by either raw bit comparison or with tools like nm.

Conclusion

Build validation can unveil insights by analyzing the build process and it’s outputs. By breaking down a build into it’s elemental functions, abstractions like Bazel Rules or Xcode Build Settings are easy to validate and reason about. This process is no silver bullet by any means, but is useful in addition to standard practices like testing or QA.

A reuseable system to apply these techniques would be beneficial for the community at large. A Swift library could be implemented to write build validation programs for many use cases.

Published on 22 Sep 2018 Find me on Twitter!