From ec2788cf8031f11b50244298a87bb95da40dede4 Mon Sep 17 00:00:00 2001
From: joshavant <830519+joshavant@users.noreply.github.com>
Date: Mon, 15 Jun 2026 14:24:33 +0200
Subject: [PATCH] fix: launch configured ios bundle
---
package.json | 2 +-
scripts/ios-run.sh | 57 +++++++++
scripts/test-projects.test-support.mjs | 1 +
test/scripts/ios-run.test.ts | 155 +++++++++++++++++++++++++
test/scripts/test-projects.test.ts | 1 +
5 files changed, 215 insertions(+), 1 deletion(-)
create mode 100755 scripts/ios-run.sh
create mode 100644 test/scripts/ios-run.test.ts
diff --git a/package.json b/package.json
index 1ac34275d74..3c25ef47ebd 100644
--- a/package.json
+++ b/package.json
@@ -1556,7 +1556,7 @@
"ios:build": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && cd apps/ios && xcodegen generate && xcodebuild -project OpenClaw.xcodeproj -scheme OpenClaw -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build'",
"ios:gen": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && cd apps/ios && xcodegen generate'",
"ios:open": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && cd apps/ios && xcodegen generate && open OpenClaw.xcodeproj'",
- "ios:run": "bash -lc './scripts/ios-configure-signing.sh && ./scripts/ios-write-version-xcconfig.sh && cd apps/ios && xcodegen generate && xcodebuild -project OpenClaw.xcodeproj -scheme OpenClaw -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build && xcrun simctl boot \"${IOS_SIM:-iPhone 17}\" || true && xcrun simctl launch booted ai.openclaw.ios'",
+ "ios:run": "bash scripts/ios-run.sh",
"ios:version": "node --import tsx scripts/ios-version.ts --json",
"ios:version:check": "node --import tsx scripts/ios-sync-versioning.ts --check",
"ios:version:pin": "node --import tsx scripts/ios-pin-version.ts",
diff --git a/scripts/ios-run.sh b/scripts/ios-run.sh
new file mode 100755
index 00000000000..71a47435b25
--- /dev/null
+++ b/scripts/ios-run.sh
@@ -0,0 +1,57 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+IOS_DIR="${ROOT_DIR}/apps/ios"
+
+APP_NAME="${IOS_APP_NAME:-OpenClaw}"
+CONFIGURATION="${IOS_CONFIGURATION:-Debug}"
+DERIVED_DATA_DIR="${IOS_DERIVED_DATA_DIR:-${IOS_DIR}/build/DerivedData}"
+IOS_DESTINATION="${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}"
+SIMULATOR_TARGET="${IOS_SIM:-iPhone 17}"
+
+XCODEBUILD_BIN="${IOS_RUN_XCODEBUILD_BIN:-xcodebuild}"
+XCODEGEN_BIN="${IOS_RUN_XCODEGEN_BIN:-xcodegen}"
+SIMCTL_BIN="${IOS_RUN_SIMCTL_BIN:-xcrun simctl}"
+PLIST_BUDDY_BIN="${IOS_RUN_PLIST_BUDDY_BIN:-/usr/libexec/PlistBuddy}"
+
+run_simctl() {
+ # shellcheck disable=SC2086
+ ${SIMCTL_BIN} "$@"
+}
+
+"${ROOT_DIR}/scripts/ios-configure-signing.sh"
+"${ROOT_DIR}/scripts/ios-write-version-xcconfig.sh"
+
+cd "${IOS_DIR}"
+"${XCODEGEN_BIN}" generate
+"${XCODEBUILD_BIN}" \
+ -project OpenClaw.xcodeproj \
+ -scheme OpenClaw \
+ -destination "${IOS_DESTINATION}" \
+ -configuration "${CONFIGURATION}" \
+ -derivedDataPath "${DERIVED_DATA_DIR}" \
+ build
+
+app_path="${DERIVED_DATA_DIR}/Build/Products/${CONFIGURATION}-iphonesimulator/${APP_NAME}.app"
+if [[ ! -d "${app_path}" ]]; then
+ echo "ERROR: Built app not found at ${app_path}" >&2
+ exit 1
+fi
+
+bundle_id="$("${PLIST_BUDDY_BIN}" -c 'Print :CFBundleIdentifier' "${app_path}/Info.plist" 2>/dev/null || true)"
+if [[ -z "${bundle_id}" ]]; then
+ echo "ERROR: Built app is missing CFBundleIdentifier: ${app_path}/Info.plist" >&2
+ exit 1
+fi
+
+boot_output=""
+if ! boot_output="$(run_simctl boot "${SIMULATOR_TARGET}" 2>&1)"; then
+ if [[ "${boot_output}" != *"Unable to boot device in current state: Booted"* ]]; then
+ printf '%s\n' "${boot_output}" >&2
+ exit 1
+ fi
+fi
+
+run_simctl install "${SIMULATOR_TARGET}" "${app_path}"
+run_simctl launch "${SIMULATOR_TARGET}" "${bundle_id}"
diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs
index 5cc1abed284..b35b5a870da 100644
--- a/scripts/test-projects.test-support.mjs
+++ b/scripts/test-projects.test-support.mjs
@@ -566,6 +566,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
["scripts/docker-e2e-rerun.mjs", ["test/scripts/docker-e2e-helper-cli.test.ts"]],
["scripts/docker-e2e-timings.mjs", ["test/scripts/docker-e2e-helper-cli.test.ts"]],
["scripts/generate-npm-shrinkwrap.mjs", ["test/scripts/generate-npm-shrinkwrap.test.ts"]],
+ ["scripts/ios-run.sh", ["test/scripts/ios-run.test.ts"]],
["scripts/kova-ci-summary.mjs", ["test/scripts/kova-ci-summary.test.ts"]],
["scripts/openclaw-npm-postpublish-verify.ts", ["test/openclaw-npm-postpublish-verify.test.ts"]],
["scripts/openclaw-npm-release-check.ts", ["test/openclaw-npm-release-check.test.ts"]],
diff --git a/test/scripts/ios-run.test.ts b/test/scripts/ios-run.test.ts
new file mode 100644
index 00000000000..afb02395fc1
--- /dev/null
+++ b/test/scripts/ios-run.test.ts
@@ -0,0 +1,155 @@
+// iOS run tests cover simulator launch orchestration without touching Xcode.
+import { execFileSync } from "node:child_process";
+import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { afterEach, describe, expect, it } from "vitest";
+
+const SCRIPT = path.join(process.cwd(), "scripts", "ios-run.sh");
+const BASH_BIN = process.platform === "win32" ? "bash" : "/bin/bash";
+
+const tempDirs: string[] = [];
+
+function bashArgs(scriptPath: string): string[] {
+ return process.platform === "win32" ? [scriptPath] : ["--noprofile", "--norc", scriptPath];
+}
+
+function writeExecutable(filePath: string, body: string): void {
+ writeFileSync(filePath, body, "utf8");
+ chmodSync(filePath, 0o755);
+}
+
+function makeFixture(bundleId: string): { root: string; script: string; logFile: string } {
+ const root = mkdtempSync(path.join(os.tmpdir(), "openclaw-ios-run-"));
+ tempDirs.push(root);
+
+ const scriptsDir = path.join(root, "scripts");
+ const iosDir = path.join(root, "apps", "ios");
+ const binDir = path.join(root, "bin");
+ const logFile = path.join(root, "commands.log");
+ mkdirSync(scriptsDir, { recursive: true });
+ mkdirSync(iosDir, { recursive: true });
+ mkdirSync(binDir, { recursive: true });
+
+ const script = path.join(scriptsDir, "ios-run.sh");
+ writeFileSync(script, readFileSync(SCRIPT, "utf8"), "utf8");
+ chmodSync(script, 0o755);
+
+ writeExecutable(
+ path.join(scriptsDir, "ios-configure-signing.sh"),
+ `#!/usr/bin/env bash
+set -euo pipefail
+`,
+ );
+ writeExecutable(
+ path.join(scriptsDir, "ios-write-version-xcconfig.sh"),
+ `#!/usr/bin/env bash
+set -euo pipefail
+`,
+ );
+ writeExecutable(
+ path.join(binDir, "xcodegen"),
+ `#!/usr/bin/env bash
+set -euo pipefail
+printf 'xcodegen %s\\n' "$*" >>"${logFile}"
+`,
+ );
+ writeExecutable(
+ path.join(binDir, "xcodebuild"),
+ `#!/usr/bin/env bash
+set -euo pipefail
+printf 'xcodebuild %s\\n' "$*" >>"${logFile}"
+derived=""
+configuration="Debug"
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -derivedDataPath)
+ derived="$2"
+ shift 2
+ ;;
+ -configuration)
+ configuration="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+app_dir="$derived/Build/Products/$configuration-iphonesimulator/OpenClaw.app"
+mkdir -p "$app_dir"
+cat >"$app_dir/Info.plist" <<'PLIST'
+
+
+CFBundleIdentifier${bundleId}
+PLIST
+`,
+ );
+ writeExecutable(
+ path.join(binDir, "simctl"),
+ `#!/usr/bin/env bash
+set -euo pipefail
+printf 'simctl %s\\n' "$*" >>"${logFile}"
+if [[ "$1" == "boot" ]]; then
+ if [[ "\${SIMCTL_BOOT_MODE:-}" == "booted" ]]; then
+ echo "Unable to boot device in current state: Booted" >&2
+ exit 1
+ fi
+ if [[ "\${SIMCTL_BOOT_MODE:-}" == "fail" ]]; then
+ echo "Unable to boot device in current state: Shutdown" >&2
+ exit 1
+ fi
+fi
+`,
+ );
+ writeExecutable(
+ path.join(binDir, "plistbuddy"),
+ `#!/usr/bin/env bash
+set -euo pipefail
+sed -n 's:.*CFBundleIdentifier\\([^<]*\\).*:\\1:p' "$3"
+`,
+ );
+
+ return { root, script, logFile };
+}
+
+function runIosRun(fixture: { root: string; script: string }, extraEnv = {}): string {
+ return execFileSync(BASH_BIN, bashArgs(fixture.script), {
+ env: {
+ ...process.env,
+ IOS_DERIVED_DATA_DIR: path.join(fixture.root, "DerivedData"),
+ IOS_RUN_XCODEBUILD_BIN: path.join(fixture.root, "bin", "xcodebuild"),
+ IOS_RUN_XCODEGEN_BIN: path.join(fixture.root, "bin", "xcodegen"),
+ IOS_RUN_SIMCTL_BIN: path.join(fixture.root, "bin", "simctl"),
+ IOS_RUN_PLIST_BUDDY_BIN: path.join(fixture.root, "bin", "plistbuddy"),
+ ...extraEnv,
+ },
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+}
+
+describe("scripts/ios-run.sh", () => {
+ afterEach(() => {
+ for (const dir of tempDirs.splice(0)) {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
+ it("installs and launches the configured app bundle identifier", () => {
+ const fixture = makeFixture("ai.openclawfoundation.app");
+
+ runIosRun(fixture, { SIMCTL_BOOT_MODE: "booted" });
+
+ expect(readFileSync(fixture.logFile, "utf8")).toContain(
+ "simctl launch iPhone 17 ai.openclawfoundation.app",
+ );
+ });
+
+ it("does not ignore simulator boot failures other than already booted", () => {
+ const fixture = makeFixture("ai.openclawfoundation.app");
+
+ expect(() => runIosRun(fixture, { SIMCTL_BOOT_MODE: "fail" })).toThrow();
+ expect(readFileSync(fixture.logFile, "utf8")).not.toContain("simctl launch");
+ });
+});
diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts
index 921bfaf3426..52044ef16ce 100644
--- a/test/scripts/test-projects.test.ts
+++ b/test/scripts/test-projects.test.ts
@@ -581,6 +581,7 @@ describe("scripts/test-projects changed-target routing", () => {
"scripts/package-openclaw-for-docker.mjs",
["test/scripts/package-openclaw-for-docker.test.ts"],
],
+ ["scripts/ios-run.sh", ["test/scripts/ios-run.test.ts"]],
["scripts/package-mac-app.sh", ["test/scripts/package-mac-app.test.ts"]],
["scripts/package-mac-dist.sh", ["test/scripts/package-mac-dist.test.ts"]],
["scripts/package-changelog.mjs", ["test/scripts/package-changelog.test.ts"]],