Engineering Practice

Catch Breaking Framework Changes with Swift API Digester

Catch Breaking Framework Changes with Swift API Digester

Catch Breaking Framework Changes with Swift API Digester

When multiple apps depend on a Swift framework, removing a public method, narrowing a parameter type, or adding a required member to a public protocol can prevent downstream projects from compiling after an upgrade. Unit tests usually verify implementation behavior, but they do not explicitly tell you that a change has broken the public API. On OnceMac cloud Mac build nodes, Swift API Digester can be added to merge checks: save an API snapshot of the released version, generate a candidate snapshot with the exact same toolchain, and then compare the two.

Pin the comparison environment first

An API snapshot is not determined by source code alone. The Xcode version, Swift compiler, SDK, target architecture, build configuration, and conditional compilation flags can all affect the result. Before running the comparison, record these inputs in the logs and keep them fixed.

set -euo pipefail

xcodebuild -version
xcrun swift --version
SDK_PATH="$(xcrun --sdk iphonesimulator --show-sdk-path)"
echo "SDK_PATH=$SDK_PATH"

CI should select Xcode explicitly and use the same image or node template for both the baseline and candidate branches. The target triple must also remain identical—for example, use arm64-apple-ios17.0-simulator consistently. Do not generate a device interface on one side and a simulator interface on the other, or differences in platform conditions may be misidentified as code changes.

An API baseline is a release contract, not a build cache. It should be committed to version control, reviewed like source code, and never overwritten automatically by routine build jobs.

Build a framework that can be analyzed

The following example assumes that both the scheme and module are named MyKit. Start by removing the dedicated Derived Data directory, then build with the Release configuration. BUILD_LIBRARY_FOR_DISTRIBUTION=YES generates the interface files required for module stability and makes the check more representative of binary distribution.

DERIVED_DATA="$PWD/.build/api-dd"
PRODUCTS="$DERIVED_DATA/Build/Products/Release-iphonesimulator"

rm -rf "$DERIVED_DATA"

xcodebuild build \
  -scheme MyKit \
  -configuration Release \
  -destination "generic/platform=iOS Simulator" \
  -derivedDataPath "$DERIVED_DATA" \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
  SKIP_INSTALL=NO \
  CODE_SIGNING_ALLOWED=NO

After the build, verify that the module actually exists. This prevents Digester from producing a confusing “module not found” error because it searched the wrong path.

test -d "$PRODUCTS/MyKit.framework"
find "$PRODUCTS/MyKit.framework/Modules" -maxdepth 3 -type f -print

If the project manages dependencies through a workspace, add -workspace. If the scheme is not shared, CI cannot discover it. Share it in the project settings instead of making the script guess its location.

Generate and compare API snapshots

Build the current release baseline and the candidate code separately, then emit JSON for each. Keep the baseline file in the repository’s api-baselines/ directory and include the platform in its filename. There is no need to include machine-specific paths or build numbers.

mkdir -p api-baselines .build/api-report

xcrun swift-api-digester \
  -dump-sdk \
  -module MyKit \
  -sdk "$SDK_PATH" \
  -target arm64-apple-ios17.0-simulator \
  -F "$PRODUCTS" \
  -o ".build/api-report/current-ios-simulator.json"

xcrun swift-api-digester \
  -diagnose-sdk \
  -input-paths "api-baselines/MyKit-ios-simulator.json" \
  -input-paths ".build/api-report/current-ios-simulator.json" \
  > ".build/api-report/diagnostics.txt"

During the initial setup, copy the verified current-ios-simulator.json file into place as the baseline and commit it. From then on, CI should only read that file. If the command exits with a nonzero status, preserve the diagnostic output as a build artifact instead of showing only a generic “compatibility check failed” message.

Changes that require close review

Change Default assessment Recommended action
Remove a public type or method Breaking Restore the API or include the change in an explicit major-version release
Change a parameter, return value, or generic constraint Breaking Add a compatible overload and schedule a deprecation period
Add a required member to a public protocol High risk Consider providing a default implementation
Add a public method or type Usually compatible Review naming, visibility, and platform annotations
Change internal implementation only Should not appear Check whether access control was widened accidentally

public does not always represent an intentional commitment. If a declaration should not be called externally, narrow its access level instead of maintaining suppression rules indefinitely.

Turn diagnostics into a maintainable CI gate

Split the workflow into four steps: build, generate the snapshot, compare, and upload the report. Enable set -euo pipefail in the script and place Derived Data in a job-specific directory so parallel jobs cannot read one another’s artifacts. For repositories with multiple modules, maintain a module list and process each entry separately rather than reusing the previous module’s PRODUCTS path.

When the gate fails, reviewers need three pieces of information: the Xcode and Swift versions used, the baseline and candidate snapshots, and the complete diagnostic output. Update the baseline in the same merge request only after confirming that the change complies with the versioning policy. If a failed job is allowed to rewrite its own baseline, the check will always pass and become meaningless.

Order for investigating false positives

Check the toolchain first, then the SDK and target triple, followed by the build arguments and conditional compilation flags. Only after those match should you decide whether the difference is noise in Digester’s output. Use this checklist to shorten the investigation:

  1. Is xcodebuild -version exactly the same?
  2. Are the scheme, configuration, and destination identical?
  3. Is BUILD_LIBRARY_FOR_DISTRIBUTION enabled on both sides?
  4. Does the module search path point to artifacts from the current job?
  5. Does the baseline represent the most recently released interface rather than an arbitrary historical commit?
  6. Do the generated files contain volatile absolute paths such as the working directory?

Finish with a versioning policy

A compatibility gate detects changes; it cannot choose a version number for the team. Removing APIs, changing public types, or adding protocol requirements will usually require a breaking-release plan. New APIs still need naming and availability reviews. Deprecated APIs should remain available for the agreed period and be removed only in a later release.

The safest workflow is to store the baseline on the release branch, generate only candidate snapshots in merge requests, have CI produce machine-readable diagnostics, and let maintainers decide according to the versioning policy. This approach neither blocks every API change indiscriminately nor allows an accidental public edit to propagate directly to every downstream project.

Frequently asked questions

Does Swift API Digester replace unit tests?

No. It detects structural changes in public Swift APIs but does not validate runtime behavior, business logic, or Objective-C interfaces. Run it alongside unit and integration tests.

When should the API baseline be updated?

Update it only after reviewers confirm that the interface change follows the project’s versioning policy. Commit the baseline with the approved code change; never overwrite it automatically in routine CI.

Why can identical source code produce different API results?

Differences in Xcode, the Swift compiler, SDK, target architecture, or build configuration can alter the output. Baseline generation and comparison must use the same pinned toolchain.

Dedicated physical Mac mini

Move your next build to a dedicated physical node.

Choose the chip, memory, storage, node, and rental term to get a cloud Mac with resources that are never shared with other customers.

Choose configuration and rent