Engineering article

Preflight iOS asset catalogs with actool on a cloud Mac

Preflight iOS asset catalogs with actool on a cloud Mac

A single missing icon slot often remains hidden until a full archive is nearly complete. In an iOS pipeline running on a cloud Mac, that means dependency resolution, source compilation, and testing have already consumed time before the job fails because of one metadata error in Assets.xcassets. A more direct approach is to extract the actool invocation that Xcode performs internally and run it as a standalone preflight, allowing asset issues to stop the pipeline much earlier.

Define what an actool preflight can catch

actool reads asset catalogs and compiles their resources for a specified platform, deployment target, and device type. Running it independently can detect invalid Contents.json files, image slots whose files are missing, unrecognized attributes, duplicate names, and AppIcon configurations that do not match the target platform.

It does not replace a complete build. References in source code to nonexistent asset names, different asset catalogs assigned to different targets, or build settings that override the AppIcon name may still surface only during xcodebuild. A practical pipeline order is:

Stage What it checks Cost of failure
JSON syntax check Whether Contents.json can be parsed Lowest
actool preflight Asset structure and platform parameters Relatively low
Xcode build Targets, source references, and linking Relatively high
Testing and archiving Runtime behavior and deliverable artifacts Highest

The purpose of the preflight is not to reproduce all of Xcode. It is to catch deterministic asset errors with clear fixes as early as possible.

Pin the toolchain and input parameters

First, verify which Xcode installation the current job actually uses. Do not assume that an interactive shell and the CI process share the same developer directory.

xcode-select -p
xcodebuild -version
xcrun --find actool
xcrun actool --version

If a node has multiple Xcode versions installed, set DEVELOPER_DIR explicitly at the job entry point and write xcodebuild -version to the build log. The platform, minimum OS version, device type, and AppIcon name used by the script must also match the project’s build settings. Otherwise, a successful preflight proves only that the catalog compiles with a different set of parameters, not that it works for the real target.

Before running actool, add a lightweight check for every JSON file in the directory:

find App/Assets.xcassets -name Contents.json -print0 |
while IFS= read -r -d '' file; do
  plutil -lint "$file"
done

This step pinpoints leftover merge-conflict data, trailing garbage, and corrupted files at their exact paths.

Write a script that CI can run directly

The following script writes its output to .ci-artifacts/actool and preserves the complete diagnostics on failure so CI can collect them as artifacts. Pass the directory, deployment target, and AppIcon name through arguments or project configuration instead of maintaining separate copies of the script.

#!/bin/bash
set -euo pipefail

CATALOG="${1:-App/Assets.xcassets}"
DEPLOYMENT_TARGET="${DEPLOYMENT_TARGET:-16.0}"
APP_ICON_NAME="${APP_ICON_NAME:-AppIcon}"
OUT=".ci-artifacts/actool"

rm -rf "$OUT"
mkdir -p "$OUT/compiled"
export LANG=en_US.UTF-8

if ! xcrun actool "$CATALOG" \
  --compile "$OUT/compiled" \
  --platform iphoneos \
  --minimum-deployment-target "$DEPLOYMENT_TARGET" \
  --target-device iphone \
  --app-icon "$APP_ICON_NAME" \
  --output-partial-info-plist "$OUT/partial-info.plist" \
  --notices --warnings --errors \
  >"$OUT/actool.log" 2>&1; then
  cat "$OUT/actool.log"
  exit 1
fi

cat "$OUT/actool.log"
plutil -lint "$OUT/partial-info.plist"

Using set -euo pipefail prevents the pipeline from continuing after a command fails. The log is written to a file before being printed so both the exit status and the diagnostics are preserved. Piping the command directly into a complex pipeline can easily result in reading the status of the final command instead.

Handle warnings and common false assumptions

Do not immediately turn every warning into a failure

Older projects may have accumulated unassigned device appearances, unused image sets, or naming notices. During the initial rollout, preserve the logs and establish a baseline before selecting deterministic issues that should block the pipeline. Failing on every line containing warning: usually encourages teams to disable the check rather than fix their assets.

Invalid JSON, missing asset files, missing required AppIcon slots, and duplicate names should block immediately. For notices such as unused assets, record the count first, set a cleanup deadline, and tighten the policy afterward.

Treat simulator and device platforms separately

Passing a preflight for iphonesimulator alone does not guarantee that iphoneos will also pass. Device requirements, icon rules, and output formats can produce different diagnostics. If the pipeline builds both types of target, run the preflight twice and use separate output directories so the second run does not overwrite the first result.

For targets that include iPad, add the corresponding --target-device ipad option. Do not declare only iphone merely to make the check pass. The device set must come from the target’s actual configuration.

Add the preflight to the pipeline and verify it

There is no need to wait until dependencies have been installed, because actool normally depends only on Xcode and assets stored in the repository. A better location is immediately after checkout and before dependency resolution. This provides fast feedback on asset errors even when a package-management service is temporarily slow.

For the initial rollout, prepare three failure samples: corrupt a Contents.json file, temporarily move a referenced icon file, and change the AppIcon name to a value that does not exist. Confirm that every job exits with a nonzero status, the log identifies the specific directory, and the system saves .ci-artifacts/actool. Then restore the files and verify that both the preflight and the complete build pass.

If the repository contains multiple application targets, maintain an inventory of each target, asset directory, platform, and AppIcon name, then iterate over the entries individually. Do not pass every directory to one command at once. This keeps failures directly associated with the responsible module and prevents one target’s parameters from masking problems in another.

Keep both gates in the final pipeline: actool provides fast detection of deterministic errors in asset directories, while the complete Xcode build validates target integration and the final artifact. Once those responsibilities are clearly separated, asset problems surface earlier and failure logs point more directly to the location that needs to be fixed.

Frequently asked questions

Can an actool preflight replace a complete Xcode build?

No. It catches asset catalog and invocation errors early, but the final build must still run with the same scheme, SDK, and release configuration used for delivery.

Should every actool warning fail CI?

Not initially. Record and classify the existing warnings first, then block deterministic defects such as invalid JSON, missing required icons, or duplicate resource names.

OwnAMac Cloud Mac

Move your next build to a dedicated Apple Silicon physical node

Choose from three configurations and five nodes based on task size, with compute and storage kept separate from other tenants. All nodes operate normally 365 days a year; actual availability is determined by the status returned in real time by the console.

Choose a configuration and order