An iOS project may compile successfully on a development branch, yet its Archive can quietly pick up dynamic frameworks, absolute links to directories on a developer machine, or global symbols that were intended for internal use only. Reviewing project files rarely captures all of these changes because the actual deliverable is the linked Mach-O binary. A more reliable approach is to have cloud Mac CI inspect the archived artifacts directly and turn dependency and symbol changes into reviewable diffs.
Define the changes to block first
This type of gate should not decide whether a particular library is “good” or “bad.” Instead, it should answer three verifiable questions:
- What do the main executable and embedded frameworks actually depend on?
- Do bundled libraries referenced through
@rpathreally exist inside the app bundle? - Are newly added global symbols part of the intended interface?
A baseline is not a permanent allowlist. It is a manually approved snapshot of an artifact. The baseline can be updated after a toolchain upgrade, dependency update, or interface change, but that update must be reviewed together with the code changes.
Run these checks after creating a Release Archive. Debug builds may contain diagnostic libraries and produce different optimization results, so they do not represent the final delivery boundary. During the initial rollout, generate reports without blocking builds. Once the team has reviewed and approved the baseline, make subsequent differences fail the pipeline.
Collect binaries from the Archive
Create a clean, standalone archive first so that historical artifacts from a developer machine cannot be reused:
rm -rf out/App.xcarchive
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$PWD/out/App.xcarchive" \
clean archive
If signing is handled by a separate downstream job, set CODE_SIGNING_ALLOWED=NO when appropriate for the project. Do not add this override mechanically to every project. For projects that use specific capabilities or custom build phases, first verify that an unsigned archive is complete.
The main app is typically located under Products/Applications in the archive. At a minimum, inspect the app’s main executable and the executable in every framework under the Frameworks directory. Extension targets should also be included separately because each has its own dependency boundary.
| Object | Primary checks | Common issue |
|---|---|---|
| Main app | System libraries, bundled frameworks, global symbols | Non-system absolute path |
| Dynamic framework | Transitive dependencies, install name, public symbols | Dependency not shipped in the bundle |
| App Extension | Independent dependency and symbol baseline | Incorrectly reusing assumptions from the main app |
Build dependency and symbol inventories
The following script reads the main executable and embedded frameworks, then writes separately sorted inventories of their dependencies and global symbols. Sorting prevents changes in tool output order from creating meaningless diffs.
set -euo pipefail
archive="${1:?archive path required}"
app=$(find "$archive/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)
test -n "$app"
mkdir -p audit/current
main=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Info.plist")
printf '%s
' "$app/$main" > audit/current/binaries.txt
if test -d "$app/Frameworks"; then
while IFS= read -r framework; do
executable=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$framework/Info.plist")
printf '%s
' "$framework/$executable"
done < <(find "$app/Frameworks" -maxdepth 1 -name '*.framework' -type d | sort)
fi >> audit/current/binaries.txt
: > audit/current/dependencies.txt
: > audit/current/symbols.txt
while IFS= read -r binary; do
name=${binary#"$app/"}
otool -L "$binary" |
sed '1d' |
awk -v n="$name" '{$1=$1; print n " " $1}' \
>> audit/current/dependencies.txt
xcrun nm -gU "$binary" |
awk -v n="$name" 'NF {print n " " $NF}' \
>> audit/current/symbols.txt
done < audit/current/binaries.txt
LC_ALL=C sort -u -o audit/current/dependencies.txt audit/current/dependencies.txt
LC_ALL=C sort -u -o audit/current/symbols.txt audit/current/symbols.txt
When committing the baseline, retain only relative object names rather than recording the cloud Mac’s working directory. The pipeline can then run:
diff -u audit/baseline/dependencies.txt audit/current/dependencies.txt
diff -u audit/baseline/symbols.txt audit/current/symbols.txt
Turn the report into a reliable gate
A full-text diff alone can be overly sensitive. A more practical strategy is to enforce hard rules first and then inspect changes against the baseline. Dependencies beginning with /System/Library/Frameworks/, /usr/lib/, or a controlled @rpath/ are generally valid; other absolute paths should fail immediately. For @rpath/Example.framework/Example, the script should also verify that the corresponding framework exists in the bundle instead of allowing it solely because the prefix is valid.
Symbol changes are better suited to manual review. Mangled Swift names can be very long, so do not infer risk from string length. Focus on sudden large groups of global symbols, debugging helper interfaces, test entry points, and C or Objective-C symbols that should be hidden but have become visible across modules.
Reduce false positives
Pin the Xcode version and Release configuration, and make sure every comparison uses the same scheme. If the project contains multiple extensions, maintain a separate baseline for each binary rather than merging all results into one set. When a toolchain upgrade changes system symbols, update the baseline in the upgrade merge request so that the origin of later differences remains explainable.
Trace failures back through the artifacts
When a new dependency appears, first use otool -L to identify the binary that introduced it, then inspect the target’s linker settings, conditional compilation, and dependency embedding phase. If an @rpath target is missing, check whether the framework was linked but never copied into the bundle. For unexpected symbols, use nm -gU to locate the object, then inspect visibility declarations, bridging code, and linker export settings.
Do not overwrite the baseline immediately after a failure. The correct sequence is to identify the source of the change, determine whether it affects the delivery boundary, fix or approve the change, and only then regenerate the baseline. A gate built this way does not depend on the temporary state of any particular cloud Mac, and it leaves a reviewable record of the binary boundary for every release.
Frequently asked questions
Why not inspect only the dependencies declared by the project?
Declarations are not the final artifact. Conditional builds, linker flags, and dependency scripts can alter an archive, so the gate should inspect its Mach-O binaries.
Should every exported-symbol change block a release?
Not permanently. The gate should require review; after an intentional API change or toolchain upgrade is confirmed, the updated baseline can be committed.
Can a symbol inventory detect secrets embedded in an app?
Not by itself. It reveals interface and linkage changes, while secret scanning must also cover source files, configuration, resources, and build logs.
Dedicated physical node
Run development and build tasks on a dedicated cloud Mac
Choose the model, node region, and rental period for each task. Every instance runs on a dedicated physical machine in a non-virtualized environment.