#!/bin/bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PROJECT_PATH="$ROOT_DIR/HelloWorld.xcodeproj" TARGET_NAME="HelloWorld" TEAM_ID="${TEAM_ID:-HY3W4U256Z}" BUNDLE_ID="${BUNDLE_ID:-local.proxmox.helloworld.e2e}" CONFIGURATION="${CONFIGURATION:-Debug}" DEVICE_UDID="${DEVICE_UDID:-}" BUILD_ROOT="$ROOT_DIR/build/device" BUILD_ONLY=0 LAUNCH=0 usage() { echo "Usage: $0 [--udid UDID] [--bundle-id ID] [--team-id ID] [--configuration NAME] [--build-only] [--launch]" } while [[ $# -gt 0 ]]; do case "$1" in --udid) DEVICE_UDID="${2:?--udid requires a value}" shift 2 ;; --bundle-id) BUNDLE_ID="${2:?--bundle-id requires a value}" shift 2 ;; --team-id) TEAM_ID="${2:?--team-id requires a value}" shift 2 ;; --configuration) CONFIGURATION="${2:?--configuration requires a value}" shift 2 ;; --build-only) BUILD_ONLY=1 shift ;; --launch) LAUNCH=1 shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown argument: $1" >&2 usage >&2 exit 2 ;; esac done for command_name in xcodebuild security codesign python3; do if ! command -v "$command_name" >/dev/null 2>&1; then echo "Required command not found: $command_name" >&2 exit 1 fi done if command -v pymobiledevice3 >/dev/null 2>&1; then PMD=(pymobiledevice3) elif python3 -c 'import pymobiledevice3' >/dev/null 2>&1; then PMD=(python3 -m pymobiledevice3) else echo "pymobiledevice3 is required. Install it with: python3 -m pip install -U pymobiledevice3" >&2 exit 1 fi run_pmd() { PYTHONWARNINGS=ignore "${PMD[@]}" --no-color "$@" } if [[ -z "$DEVICE_UDID" ]]; then device_json="$(run_pmd usbmux list)" DEVICE_UDID="$(printf '%s' "$device_json" | python3 -c ' import json, sys devices = json.load(sys.stdin) print(devices[0].get("Identifier", "") if len(devices) == 1 else "") ')" if [[ -z "$DEVICE_UDID" ]]; then echo "Connect exactly one unlocked iPhone, or pass --udid explicitly." >&2 exit 1 fi fi echo "Building $BUNDLE_ID for device $DEVICE_UDID" xcodebuild \ -project "$PROJECT_PATH" \ -target "$TARGET_NAME" \ -configuration "$CONFIGURATION" \ -sdk iphoneos \ -allowProvisioningUpdates \ -allowProvisioningDeviceRegistration \ PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" \ DEVELOPMENT_TEAM="$TEAM_ID" \ SYMROOT="$BUILD_ROOT" \ OBJROOT="$BUILD_ROOT/Intermediates" \ build APP_PATH="$BUILD_ROOT/$CONFIGURATION-iphoneos/$TARGET_NAME.app" IPA_PATH="$BUILD_ROOT/$TARGET_NAME.ipa" if [[ ! -d "$APP_PATH" ]]; then echo "Expected app was not produced: $APP_PATH" >&2 exit 1 fi actual_bundle_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PATH/Info.plist")" if [[ "$actual_bundle_id" != "$BUNDLE_ID" ]]; then echo "Bundle ID mismatch: expected $BUNDLE_ID, found $actual_bundle_id" >&2 exit 1 fi codesign --verify --deep --strict "$APP_PATH" TEMP_DIR="$(mktemp -d /tmp/helloworld-install.XXXXXX)" cleanup() { if [[ -n "${TEMP_DIR:-}" && "$TEMP_DIR" == /tmp/helloworld-install.* ]]; then rm -rf "$TEMP_DIR" fi } trap cleanup EXIT PROFILE_PLIST="$TEMP_DIR/profile.plist" security cms -D -i "$APP_PATH/embedded.mobileprovision" > "$PROFILE_PLIST" if ! /usr/libexec/PlistBuddy -c 'Print :ProvisionedDevices' "$PROFILE_PLIST" 2>/dev/null | grep -Fq "$DEVICE_UDID"; then echo "Provisioning profile does not contain device UDID $DEVICE_UDID." >&2 echo "Register the phone with the Apple team, then rerun this script." >&2 exit 1 fi profile_name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$PROFILE_PLIST")" profile_expiration="$(/usr/libexec/PlistBuddy -c 'Print :ExpirationDate' "$PROFILE_PLIST")" mkdir -p "$TEMP_DIR/Payload" ditto "$APP_PATH" "$TEMP_DIR/Payload/$TARGET_NAME.app" ditto -c -k --keepParent "$TEMP_DIR/Payload" "$IPA_PATH" echo "Signature and provisioning profile validated." echo "Profile: $profile_name" echo "Expires: $profile_expiration" echo "IPA: $IPA_PATH" if [[ "$BUILD_ONLY" -eq 1 ]]; then echo "Build-only verification complete." exit 0 fi run_pmd apps install --udid "$DEVICE_UDID" "$APP_PATH" query_output="$(run_pmd apps query --udid "$DEVICE_UDID" "$BUNDLE_ID")" if ! printf '%s' "$query_output" | grep -Fq '"ProfileValidated": true'; then echo "The app installed, but the device did not report a validated profile." >&2 exit 1 fi echo "Installed and profile-validated: $BUNDLE_ID" if [[ "$LAUNCH" -eq 1 ]]; then if ! command -v xcrun >/dev/null 2>&1; then echo "xcrun is required for --launch." >&2 exit 1 fi device_list_json="$TEMP_DIR/devicectl.json" xcrun devicectl list devices --json-output "$device_list_json" >/dev/null core_device_id="$(python3 -c ' import json, sys path, udid = sys.argv[1:] with open(path, encoding="utf-8") as stream: devices = json.load(stream).get("result", {}).get("devices", []) for device in devices: if device.get("hardwareProperties", {}).get("udid") == udid: print(device.get("identifier", "")) break ' "$device_list_json" "$DEVICE_UDID")" if [[ -z "$core_device_id" ]]; then echo "Unable to map UDID to a CoreDevice identifier for launch." >&2 exit 1 fi xcrun devicectl device process launch --device "$core_device_id" "$BUNDLE_ID" echo "Launched $BUNDLE_ID" fi