Validate App Thinning Device Variants on a Cloud Mac

Validate App Thinning Device Variants on a Cloud Mac

When the same Xcode archive enters the distribution pipeline, the content ultimately delivered to different devices is not identical. App Thinning selects executable slices and resource variants according to each device’s capabilities. Routine builds often verify only that archiving succeeded, making it easy to miss incorrect resource tags, irrelevant architectures, drift in embedded frameworks, or a size report that was never generated. A more reliable approach is to separate “archiving” from “device variant validation” into two independent stages on a Cloud Mac.

This article uses an existing project and a distributable archive configuration. It does not change the signing setup or require recompilation for every validation run. The goal is to export App Thinning results from a single .xcarchive, generate structural inventories, and make CI return a nonzero exit code for clearly defined anomalies.

Fix the validation inputs first

Before validation begins, record the Xcode path, app version, build number, and archive location. Do not let the job depend on the default toolchain of an interactive shell. A Cloud Mac may have multiple Xcode versions installed, so CI should explicitly select the version approved for the current pipeline.

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
ARCHIVE_PATH="$PWD/output/App.xcarchive"
EXPORT_DIR="$PWD/output/thinning"
REPORT_DIR="$PWD/output/evidence"

xcodebuild -version
test -d "$ARCHIVE_PATH"
rm -rf "$EXPORT_DIR" "$REPORT_DIR"
mkdir -p "$EXPORT_DIR" "$REPORT_DIR"

Next, read the application metadata from the archive to confirm that CI has not picked up an artifact left behind by a previous job.

INFO="$ARCHIVE_PATH/Info.plist"

/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleIdentifier" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleShortVersionString" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" "$INFO"

Bind the working directory to the job ID and create an empty directory when the job starts. Do not use a single global output directory that lets concurrent jobs share archives.

The input to App Thinning validation must be a single .xcarchive that has already passed the standard archive checks. If the input itself is uncertain, even a detailed comparison of device variants has no value.

Export all compatible device variants

Prepare a separate ExportOptions.plist. The method value should remain consistent with the Xcode version and existing distribution workflow used by the team. The important part of the following example is setting thinning to export every compatible variant.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>method</key>
  <string>app-store-connect</string>
  <key>thinning</key>
  <string>&lt;thin-for-all-variants&gt;</string>
  <key>stripSwiftSymbols</key>
  <true/>
</dict>
</plist>

When running the export, place both the results and logs in the current job directory:

xcodebuild \
  -exportArchive \
  -archivePath "$ARCHIVE_PATH" \
  -exportPath "$EXPORT_DIR" \
  -exportOptionsPlist "$PWD/ExportOptions.plist" \
  2>&1 | tee "$REPORT_DIR/export.log"

find "$EXPORT_DIR" -maxdepth 3 -type f -print \
  | LC_ALL=C sort \
  > "$REPORT_DIR/export-files.txt"

If the existing pipeline uses a different export method, do not change it merely to copy the example. Keep the existing method, validate only the thinning configuration in an isolated branch, and archive the plist that was actually used.

Inspect architectures and embedded content

A successful export does not prove that the results are correct. First locate and unpack each installation package, then inspect the architecture of the main executable. The script should not assume a fixed application directory name; it can discover the .app inside Payload.

mkdir -p "$REPORT_DIR/unpacked"

while IFS= read -r ipa; do
  name="$(basename "$ipa" .ipa)"
  dest="$REPORT_DIR/unpacked/$name"
  mkdir -p "$dest"
  ditto -x -k "$ipa" "$dest"

  app="$(find "$dest/Payload" -maxdepth 1 -name '*.app' -type d | head -n 1)"
  test -n "$app"

  executable="$(/usr/libexec/PlistBuddy \
    -c 'Print :CFBundleExecutable' "$app/Info.plist")"

  printf '%s: ' "$name"
  lipo -archs "$app/$executable"

  find "$app/Frameworks" -maxdepth 1 -type d -name '*.framework' 2>/dev/null \
    | sed "s#^$app/##" \
    | LC_ALL=C sort \
    > "$REPORT_DIR/$name-frameworks.txt"
done < <(find "$EXPORT_DIR" -type f -name '*.ipa' | LC_ALL=C sort)

For current iOS device distribution output, the job should fail immediately if the main app contains a simulator architecture. The framework inventory is suitable for comparison with an approved baseline stored in the repository. A new framework is not necessarily an error, but it must go through code review to prevent debugging components or temporary dependencies from entering the delivery package.

Do not compare hashes of entire installation packages

Different device variants are expected to contain different resources and executable slices. Signing data also introduces binary differences. Requiring every IPA to have the same SHA-256 hash would incorrectly treat normal thinning as a failure.

More useful comparison targets include:

  • the allowed architectures of the main executable;
  • relative path inventories under Frameworks and PlugIns;
  • required localizations, fonts, and data files;
  • debugging configurations, test resources, and source files that must not enter the finished product;
  • the unpacked size of each variant and its change relative to the historical baseline.

Turn resources and sizes into evidence

First confirm that the export process actually produced a size report, then generate a file inventory for each variant. File sizes can differ because of resource slicing, so baselines should be stored separately for each variant.

SIZE_REPORT="$(find "$EXPORT_DIR" -type f \
  -name 'App Thinning Size Report.txt' | head -n 1)"

test -n "$SIZE_REPORT"
cp "$SIZE_REPORT" "$REPORT_DIR/"

for app in "$REPORT_DIR"/unpacked/*/Payload/*.app; do
  variant="$(basename "$(dirname "$(dirname "$app")")")"
  (
    cd "$app"
    find . -type f -print0 \
      | xargs -0 stat -f '%z %N' \
      | LC_ALL=C sort
  ) > "$REPORT_DIR/$variant-files-and-sizes.txt"
done

Do not use a single, permanently fixed absolute number as the only size gate. Keep two checks in parallel: a hard limit approved by the team and an allowed increase relative to the latest stable baseline. The hard limit prevents obvious size overruns, while the relative baseline detects unusual growth introduced by a single commit.

Resource directories should also be checked for business-critical files. Offline templates, fonts required for the first screen, or specific language resources cannot be considered complete merely because the app launches. Maintain these paths in a text inventory in the repository and run test -e for each entry. This is easier to review than scattering numerous conditions throughout the script.

Define explicit CI failure conditions

At a minimum, the final script should block the following conditions: no IPA was exported, the size report is missing, the main app contains a disallowed architecture, critical resources are absent, the framework set changed without approval, or a variant exceeds the team’s size limit.

Preserve the following evidence as well:

  1. the Xcode version and DEVELOPER_DIR;
  2. the archive’s bundle identifier, version, and build number;
  3. the ExportOptions.plist that was actually used;
  4. the complete export log;
  5. architecture, framework, and file inventories for every variant;
  6. the App Thinning size report.

Do not delete the export directory immediately after a failure. Package and upload the evidence first, then let the job’s exit hook clean the workspace. When jobs run concurrently on PingMini Cloud Macs, each job should use an independent directory, and the currently available configurations should be confirmed in the console. The validation logic itself should not depend on any fixed node.

App Thinning is valuable not only because it reduces download size, but also because it changes the final deliverable. Treating device variants as first-class build artifacts is the only way to detect changes in resource tags, architectures, or dependencies before release and produce failures that are reproducible and easy to investigate.

Frequently asked questions

Should App Thinning validation inspect the archive or the exported packages?

It should inspect both. The archive verifies the common input, while exported packages reveal the actual architecture slices, resource variants, and embedded contents delivered for each device class.

Do different hashes between device variants indicate a failed build?

No. Device resources, executable slices, and signature data can legitimately differ. Compare structural inventories, allowed architectures, required resources, and unexpected additions instead.

Which App Thinning findings should fail a CI job?

Fail on a missing size report, an empty export, a disallowed architecture, missing required resources, an unexpected framework set, or a variant exceeding the team’s defined size ceiling.

Prepare a Dedicated Physical Mac for Your Next Build

Choose one of three fixed cloud Mac configurations for development, continuous integration, or local model testing, then configure it for the duration of your task.

Configure Your Cloud Mac