Revision control

Copy as Markdown

#!/bin/bash
# Copyright (C) 2026 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
# Installs, runs, or uninstalls an app on a simulator via xcrun simctl,
# or on a real device via xcrun devicectl.
#
# Usage: qt-apple-runner.sh <action> <app_executable> [test_args...]
#
# <action> is one of:
# install - (re)install the app, starting from a clean data container
# run - launch the app and propagate its exit code
# uninstall - remove the app from the target
#
# If the first argument is not one of the actions above it is treated as the
# app executable, and the combined install + run + uninstall flow is run.
#
# <app_executable> is the binary inside the .app bundle, as passed by CTest
# when CROSSCOMPILING_EMULATOR is set. The bundle path is derived from it.
#
# Simulator: Set APPLE_SIMULATOR_UDID to target a specific booted simulator.
# If unset, the first booted simulator matching the app's platform is used.
#
# Device: Set APPLE_DEVICE_UDID to target a specific connected device.
# If unset, the first connected device is used.
die() { echo "qt-apple-runner.sh: $*" >&2; exit 254; }
case "$1" in
install|run|uninstall|all) action="$1"; shift ;;
# Anything else isn't an action, but the app executable, in which case we
# fall back to the combined install + run + uninstall flow.
*) action="all" ;;
esac
app="$1"; shift
[ -n "$app" ] || die "⚠️ No app executable specified"
bundle=$(dirname "$app")
[ -d "$bundle" ] || die "⚠️ Bundle not found: $bundle"
bundle_id=$(/usr/libexec/PlistBuddy -c "Print:CFBundleIdentifier" \
"$bundle/Info.plist" 2>/dev/null) \
|| die "⚠️ Failed to read CFBundleIdentifier from $bundle/Info.plist"
platform=$(vtool -show-build "$app" 2>/dev/null \
| awk '/^ *platform /{print $2; exit}')
case "$platform" in
*SIMULATOR) target_type=simulator ;;
*) target_type=device ;;
esac
case "$platform" in
IOS*) runtime="iOS" ;;
TVOS*) runtime="tvOS" ;;
WATCHOS*) runtime="watchOS" ;;
XROS*) runtime="xrOS" ;;
*) runtime="" ;;
esac
# Resolve the target the action will operate on. All actions need this.
if [ "$target_type" = "simulator" ]; then
if [ -n "$APPLE_SIMULATOR_UDID" ]; then
udid="$APPLE_SIMULATOR_UDID"
else
udid=$(xcrun simctl list devices booted --json 2>/dev/null \
| jq -r --arg f "$runtime" \
'.devices | to_entries[]
| select($f == "" or (.key | contains($f)))
| .value[] | select(.state == "Booted") | .udid' \
| head -1)
[ -n "$udid" ] || die "⚠️ No booted $runtime simulator found; boot one or set APPLE_SIMULATOR_UDID"
fi
else
if [ -n "$APPLE_DEVICE_UDID" ]; then
udid="$APPLE_DEVICE_UDID"
else
udid=$(xcrun devicectl list devices --quiet --json-output /dev/stdout 2>/dev/null \
| jq -r --arg r "$runtime" \
'.result.devices[]
| select(.connectionProperties.tunnelState == "connected")
| select($r == "" or .hardwareProperties.platform == $r)
| .hardwareProperties.udid' \
| head -1)
[ -n "$udid" ] || die "⚠️ No connected $runtime device found; connect one or set APPLE_DEVICE_UDID"
fi
fi
forward_env() {
local prefix="$1"
while IFS='=' read -r key rest; do
case "$key" in
LC_*|LANG) ;;
*) [ -n "$key" ] && export "${prefix}${key}=${rest}" ;;
esac
done < <(env)
}
# Uninstalls the app from the target, clearing its data container.
uninstall_app() {
if [ "$target_type" = "simulator" ]; then
xcrun simctl uninstall "$udid" "$bundle_id" 2>/dev/null || true
else
xcrun devicectl device uninstall app --device "$udid" "$bundle_id" 2>/dev/null || true
fi
}
# Installs the app, starting from a clean data container.
install_app() {
# Uninstall first so each run starts with a clean data container
uninstall_app
if [ "$target_type" = "simulator" ]; then
xcrun simctl install "$udid" "$bundle" \
|| die "⚠️ Failed to install $bundle on simulator $udid"
else
xcrun devicectl device install app --device "$udid" "$bundle" \
|| die "⚠️ Failed to install $bundle on device $udid"
fi
}
# Launches the already-installed app and propagates its exit code.
run_app() {
# Let the platform integration know it should set a custom cwd
# and write the exit code to a file.
export QT_RUNNING_VIA_TEST_RUNNER=1
if [ "$target_type" = "simulator" ]; then
forward_env "SIMCTL_CHILD_"
xcrun simctl launch --console-pty "$udid" "$bundle_id" "$@" \
|| die "⚠️ Failed to launch $bundle on simulator $udid"
echo "DONE!"
tmp_dir="$(xcrun simctl get_app_container "$udid" "$bundle_id" data)/tmp" \
|| die "⚠️ Failed to locate data container for $bundle_id on simulator $udid"
else
forward_env "DEVICECTL_CHILD_"
# Note: devicectl doesn't reliably propagate the app's exit code (e.g. it reports
# 0 even when dyld fails to load a library), so we don't trust the launch exit code
# and rely on the one Qt writes to a file in the data container.
xcrun devicectl device process launch --console \
--device "$udid" -- "$bundle_id" "$@" || true
echo "DONE!"
# Stage a local copy of the data container's tmp/ so the post-processing
# below can treat device and simulator the same way.
tmp_dir=$(mktemp -d) || die "⚠️ Failed to resolve tmp dir"
trap 'rm -rf "$tmp_dir"' EXIT
xcrun devicectl device copy from \
--quiet \
--device "$udid" \
--domain-type appDataContainer \
--domain-identifier "$bundle_id" \
--source "tmp" \
--destination "$tmp_dir" 2>/dev/null || true
fi
exit_code=$(cat "$tmp_dir/qt_exit_code.txt" 2>/dev/null)
exit_code="${exit_code:-253}"
if [ -d "$tmp_dir/testrunner" ]; then
cp -r "$tmp_dir/testrunner/." .
fi
}
case "$action" in
install) install_app ;;
run) run_app "$@" ;;
uninstall) uninstall_app ;;
all) install_app; run_app "$@"; uninstall_app ;;
esac
exit "${exit_code:-0}"