diff --git a/Justfile b/Justfile new file mode 100644 index 00000000..6917b299 --- /dev/null +++ b/Justfile @@ -0,0 +1,123 @@ +set dotenv-load +set dotenv-filename := ".env" +set windows-shell := ["sh", "-cu"] + +default: + @just list + +list: + @printf "%s\n" \ + "run [release] [logs]" \ + "clean [build|derived-data|spm|all]..." + +run mode="" logs="": + #!/usr/bin/env sh + set -eu + + mode="{{ mode }}" + logs="{{ logs }}" + configuration="Debug" + attach_logs=false + + if [ "$mode" = "logs" ]; then + attach_logs=true + mode="" + fi + if [ -n "$logs" ]; then + if [ "$logs" != "logs" ]; then + echo "usage: just run [release] [logs]" >&2 + exit 1 + fi + attach_logs=true + fi + if [ -n "$mode" ]; then + if [ "$mode" != "release" ]; then + echo "usage: just run [release] [logs]" >&2 + exit 1 + fi + configuration="Release" + fi + + if [ "$configuration" = "Release" ]; then + echo "Running Release configuration (mainnet)." + fi + + if [ "$attach_logs" = "true" ]; then + BITKIT_CONFIGURATION="$configuration" BITKIT_ATTACH_LOGS=1 ./run.sh + else + BITKIT_CONFIGURATION="$configuration" BITKIT_ATTACH_LOGS=0 ./run.sh + fi + +clean *targets: + #!/usr/bin/env sh + set -eu + + set -- {{ targets }} + if [ "$#" -eq 0 ]; then + set -- build + fi + + clean_build=false + clean_derived_data=false + clean_spm=false + + for target in "$@"; do + case "$target" in + build) + clean_build=true + ;; + derived-data | derived) + clean_derived_data=true + ;; + spm | swiftpm) + clean_spm=true + ;; + all) + clean_build=true + clean_derived_data=true + clean_spm=true + ;; + *) + echo "usage: just clean [build|derived-data|spm|all]..." >&2 + exit 1 + ;; + esac + done + + remove_path() { + path="$1" + + case "$path" in + "" | "/" | "$HOME" | "$HOME/") + echo "Refusing to remove unsafe path: $path" >&2 + exit 1 + ;; + esac + + if [ -e "$path" ] || [ -L "$path" ]; then + echo "Removing $path" + rm -rf "$path" + fi + } + + if [ "$clean_build" = "true" ]; then + remove_path "${BITKIT_DERIVED_DATA_PATH:-build}" + fi + + if [ "$clean_derived_data" = "true" ]; then + xcode_derived_data_root="${BITKIT_XCODE_DERIVED_DATA_ROOT:-$HOME/Library/Developer/Xcode/DerivedData}" + for path in "$xcode_derived_data_root"/Bitkit-*; do + remove_path "$path" + done + fi + + if [ "$clean_spm" = "true" ]; then + remove_path "${BITKIT_DERIVED_DATA_PATH:-build}/SourcePackages" + xcode_derived_data_root="${BITKIT_XCODE_DERIVED_DATA_ROOT:-$HOME/Library/Developer/Xcode/DerivedData}" + for path in "$xcode_derived_data_root"/Bitkit-*/SourcePackages; do + remove_path "$path" + done + remove_path "${BITKIT_SWIFTPM_CACHE_PATH:-$HOME/Library/Caches/org.swift.swiftpm}" + remove_path "$HOME/.swiftpm/cache" + remove_path "$HOME/.swiftpm/repositories" + fi diff --git a/run.sh b/run.sh index b579e4e0..7c97919d 100755 --- a/run.sh +++ b/run.sh @@ -3,20 +3,161 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -DERIVED_DATA_PATH="$ROOT_DIR/build" -APP_PATH="$DERIVED_DATA_PATH/Build/Products/Debug-iphoneos/Bitkit.app" -BUNDLE_ID="to.bitkit" +PROJECT_PATH="$ROOT_DIR/Bitkit.xcodeproj" +WORKSPACE_PATH="$PROJECT_PATH/project.xcworkspace" +SCHEME="${BITKIT_SCHEME:-Bitkit}" +CONFIGURATION="${BITKIT_CONFIGURATION:-Debug}" +DERIVED_DATA_PATH="${BITKIT_DERIVED_DATA_PATH:-$ROOT_DIR/build}" +APP_NAME="${BITKIT_APP_NAME:-Bitkit.app}" +BUNDLE_ID="${BITKIT_BUNDLE_ID:-to.bitkit}" +BUILD_CLEAN_RETRIES="${BITKIT_BUILD_CLEAN_RETRIES:-1}" +DESTINATION_TIMEOUT="${BITKIT_DESTINATION_TIMEOUT:-45}" +DEVICE_LIST_TIMEOUT="${BITKIT_DEVICE_LIST_TIMEOUT:-15}" +INSTALL_TIMEOUT="${BITKIT_INSTALL_TIMEOUT:-120}" +DEVICE_SELECTOR="${BITKIT_DEVICE:-${1:-}}" +RESOLVE_PACKAGES="${BITKIT_RESOLVE_PACKAGES:-1}" +ALLOW_PROVISIONING_UPDATES="${BITKIT_ALLOW_PROVISIONING_UPDATES:-1}" +FORCE_CLEAN="${BITKIT_CLEAN:-0}" +LAUNCH_AFTER_INSTALL="${BITKIT_LAUNCH:-1}" +ATTACH_LOGS="${BITKIT_ATTACH_LOGS:-1}" -DEVICE_LIST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/bitkit-devices.XXXXXX")" -DEVICE_LIST_JSON="$DEVICE_LIST_DIR/devices.json" -LAUNCH_LOG="$DEVICE_LIST_DIR/launch.log" -trap 'rm -rf "$DEVICE_LIST_DIR"' EXIT +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/bitkit-run.XXXXXX")" +DEVICE_LIST_JSON="$TMP_DIR/devices.json" +trap 'rm -rf "$TMP_DIR"' EXIT if ! command -v python3 >/dev/null 2>&1; then echo "python3 is required to parse the devicectl device list." >&2 exit 1 fi +if [[ -z "$DERIVED_DATA_PATH" || "$DERIVED_DATA_PATH" == "/" ]]; then + echo "error: unsafe derived data path: $DERIVED_DATA_PATH" >&2 + exit 1 +fi + +bool_enabled() { + local value + value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" + + case "$value" in + 1 | true | yes | y | on) + return 0 + ;; + *) + return 1 + ;; + esac +} + +run_logged() { + local log_path="$1" + shift + + set +e + "$@" 2>&1 | tee "$log_path" + local status="${PIPESTATUS[0]}" + set -e + + return "$status" +} + +clean_build_folder() { + echo "Cleaning Xcode build products and intermediates in $DERIVED_DATA_PATH..." + rm -rf \ + "$DERIVED_DATA_PATH/Build" \ + "$DERIVED_DATA_PATH/Index.noindex" \ + "$DERIVED_DATA_PATH/ModuleCache.noindex" \ + "$DERIVED_DATA_PATH/CompilationCache.noindex" \ + "$DERIVED_DATA_PATH/SDKStatCaches.noindex" \ + "$DERIVED_DATA_PATH/Logs/Build" +} + +resolve_packages() { + if ! bool_enabled "$RESOLVE_PACKAGES"; then + echo "Skipping Swift package resolution because BITKIT_RESOLVE_PACKAGES=$RESOLVE_PACKAGES." + return + fi + + echo "Resolving Swift packages..." + mkdir -p "$DERIVED_DATA_PATH/SourcePackages" + + run_logged "$TMP_DIR/package-resolve.log" \ + xcodebuild \ + -resolvePackageDependencies \ + -workspace "$WORKSPACE_PATH" \ + -scheme "$SCHEME" \ + -clonedSourcePackagesDirPath "$DERIVED_DATA_PATH/SourcePackages" +} + +build_app() { + local xcodebuild_args=( + -workspace "$WORKSPACE_PATH" + -scheme "$SCHEME" + -configuration "$CONFIGURATION" + -destination "platform=iOS,id=$XCODE_DEVICE_ID" + -destination-timeout "$DESTINATION_TIMEOUT" + -derivedDataPath "$DERIVED_DATA_PATH" + -clonedSourcePackagesDirPath "$DERIVED_DATA_PATH/SourcePackages" + ) + + if bool_enabled "$ALLOW_PROVISIONING_UPDATES"; then + xcodebuild_args+=( + -allowProvisioningUpdates + -allowProvisioningDeviceRegistration + ) + fi + + if bool_enabled "$FORCE_CLEAN"; then + clean_build_folder + fi + + local max_attempts=$((BUILD_CLEAN_RETRIES + 1)) + local attempt=1 + + while ((attempt <= max_attempts)); do + if ((attempt == 1)); then + echo "Building $SCHEME $CONFIGURATION app..." + else + echo "Retrying build after clean ($attempt/$max_attempts)..." + fi + + if run_logged "$TMP_DIR/build-attempt-$attempt.log" xcodebuild "${xcodebuild_args[@]}" build; then + return 0 + fi + + if ((attempt == max_attempts)); then + echo "Build failed after $max_attempts attempt(s)." >&2 + return 1 + fi + + echo "Build failed. Running the Clean Build Folder equivalent and trying again..." >&2 + clean_build_folder + ((attempt += 1)) + done +} + +find_app_path() { + local expected_path="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION-iphoneos/$APP_NAME" + local products_dir="$DERIVED_DATA_PATH/Build/Products" + local found_path + + if [[ -d "$expected_path" ]]; then + printf '%s\n' "$expected_path" + return 0 + fi + + if [[ -d "$products_dir" ]]; then + found_path="$(find "$products_dir" -type d -name "$APP_NAME" -print -quit)" + if [[ -n "$found_path" ]]; then + printf '%s\n' "$found_path" + return 0 + fi + fi + + echo "error: app bundle not found under $products_dir" >&2 + return 1 +} + remove_static_framework_stubs() { local app_path="$1" local frameworks_dir="$app_path/Frameworks" @@ -77,37 +218,130 @@ remove_static_framework_stubs() { fi } +install_app() { + local attempt=1 + local max_attempts=2 + + while ((attempt <= max_attempts)); do + echo "Installing $APP_PATH..." + + if run_logged "$TMP_DIR/install-attempt-$attempt.log" \ + xcrun devicectl device install app \ + --device "$DEVICE_ID" \ + --timeout "$INSTALL_TIMEOUT" \ + "$APP_PATH"; then + return 0 + fi + + if ((attempt == max_attempts)); then + echo "Install failed after $max_attempts attempt(s)." >&2 + echo "The script did not uninstall $BUNDLE_ID automatically, to avoid erasing local app data." >&2 + return 1 + fi + + echo "Install failed. Waiting briefly and retrying once..." >&2 + sleep 2 + ((attempt += 1)) + done +} + +launch_app() { + local attempt=1 + local max_attempts=2 + local launch_log + local launch_args + + while ((attempt <= max_attempts)); do + launch_log="$TMP_DIR/launch-attempt-$attempt.log" + echo "Launching $BUNDLE_ID..." + launch_args=( + xcrun devicectl device process launch + --device "$DEVICE_ID" + "$BUNDLE_ID" + --terminate-existing + ) + + if bool_enabled "$ATTACH_LOGS"; then + launch_args+=(--console) + fi + + if run_logged "$launch_log" "${launch_args[@]}"; then + if ! bool_enabled "$ATTACH_LOGS"; then + echo "Launched $BUNDLE_ID." + fi + return 0 + fi + + if grep -qiE "BSErrorCodeDescription = Locked|device was not, or could not be, unlocked|Unable to launch .* because .* unlocked" "$launch_log"; then + echo "Installed successfully, but launch failed because $DEVICE_NAME is locked." >&2 + echo "Unlock the iPhone and rerun ./run.sh, or launch Bitkit manually." >&2 + return 1 + fi + + if ((attempt == max_attempts)); then + echo "Launch failed after $max_attempts attempt(s)." >&2 + return 1 + fi + + echo "Launch failed. Waiting briefly and retrying once..." >&2 + sleep 2 + ((attempt += 1)) + done +} + echo "Looking for connected iPhones..." xcrun devicectl list devices \ --filter "hardwareProperties.deviceType == 'iPhone' AND hardwareProperties.reality == 'physical'" \ - --timeout 10 \ + --timeout "$DEVICE_LIST_TIMEOUT" \ --json-output "$DEVICE_LIST_JSON" >/dev/null if ! DEVICE_INFO="$( - python3 - "$DEVICE_LIST_JSON" <<'PY' + python3 - "$DEVICE_LIST_JSON" "$DEVICE_SELECTOR" <<'PY' import json import sys with open(sys.argv[1], "r", encoding="utf-8") as file: payload = json.load(file) +selector = sys.argv[2].strip().casefold() devices = payload.get("result", {}).get("devices", []) +eligible_devices = [] -if not devices: - raise SystemExit(1) +for device in devices: + hardware = device.get("hardwareProperties", {}) + properties = device.get("deviceProperties", {}) + connection = device.get("connectionProperties", {}) + identifier = device.get("identifier") + udid = hardware.get("udid") + name = properties.get("name", "Unknown iPhone") -device = devices[0] -name = device.get("deviceProperties", {}).get("name", "Unknown iPhone") -identifier = device.get("identifier") -udid = device.get("hardwareProperties", {}).get("udid") + if hardware.get("deviceType") != "iPhone" or hardware.get("reality") != "physical" or not identifier: + continue -if not identifier: + searchable = [identifier, udid or "", name] + if selector and not any(selector in value.casefold() for value in searchable): + continue + + score = ( + connection.get("tunnelState") == "connected", + connection.get("pairingState") == "paired", + connection.get("lastConnectionDate") or "", + ) + eligible_devices.append((score, identifier, udid or identifier, name)) + +if not eligible_devices: raise SystemExit(1) +eligible_devices.sort(reverse=True) +_, identifier, udid, name = eligible_devices[0] print(f"{identifier}\t{udid or identifier}\t{name}") PY )"; then - echo "No connected physical iPhone found." >&2 + if [[ -n "$DEVICE_SELECTOR" ]]; then + echo "No connected physical iPhone matched '$DEVICE_SELECTOR'." >&2 + else + echo "No connected physical iPhone found." >&2 + fi exit 1 fi @@ -117,27 +351,19 @@ XCODE_DEVICE_ID="${REMAINING_DEVICE_INFO%%$'\t'*}" DEVICE_NAME="${REMAINING_DEVICE_INFO#*$'\t'}" echo "Using $DEVICE_NAME ($DEVICE_ID)" -echo "Building Debug app..." -xcodebuild \ - -project "$ROOT_DIR/Bitkit.xcodeproj" \ - -scheme Bitkit \ - -configuration Debug \ - -destination "platform=iOS,id=$XCODE_DEVICE_ID" \ - -derivedDataPath "$DERIVED_DATA_PATH" \ - build + +resolve_packages +build_app + +APP_PATH="$(find_app_path)" echo "Removing static framework stubs..." remove_static_framework_stubs "$APP_PATH" -echo "Installing $APP_PATH..." -xcrun devicectl device install app --device "$DEVICE_ID" "$APP_PATH" - -echo "Launching $BUNDLE_ID..." -if ! xcrun devicectl device process launch --device "$DEVICE_ID" "$BUNDLE_ID" --terminate-existing --console 2>&1 | tee "$LAUNCH_LOG"; then - if grep -qiE "BSErrorCodeDescription = Locked|device was not, or could not be, unlocked|Unable to launch .* because .* unlocked" "$LAUNCH_LOG"; then - echo "Installed successfully, but launch failed because $DEVICE_NAME is locked." >&2 - echo "Unlock the iPhone and rerun ./run.sh, or launch Bitkit manually." >&2 - fi +install_app - exit 1 +if bool_enabled "$LAUNCH_AFTER_INSTALL"; then + launch_app +else + echo "Skipping launch because BITKIT_LAUNCH=$LAUNCH_AFTER_INSTALL." fi