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"]],