feat(folio): pin ios recipes to one simulator udid and ask when several match

This commit is contained in:
pj committed 2026-09-05 16:40:27 +05:30
1 parent 419ed9d1c0
commit c25265944e
2 files changed
+112 -22

No files matched your search

+20 -7
View File
@@ -42,10 +42,18 @@ cheap to rebuild, so a lone local one is the single case worth guessing at.
## iOS ## iOS
```sh ```sh
just ios # default device: iPhone 17 Pro just ios # asks which simulator, unless one is booted
IOS_DEVICE="iPhone 15" just ios # pick a different simulator IOS_DEVICE="iPhone 15" just ios # name it, by name or UDID
``` ```
`IOS_DEVICE` follows the same rule as `ANDROID_DEVICE`: a lone booted simulator
is taken without asking, anything else is asked about, and with no terminal to
ask on it refuses and lists what is installed. A name is matched against booted
simulators first and available ones second, and it can name several, since the
same iPhone exists under every installed runtime. When it does, you pick which,
and everything after that addresses the chosen UDID: the build destination, the
install, the launch and `--ios-device` all get the one simulator.
`just ios` regenerates `app/iosApp/iosApp.xcodeproj` from `app/iosApp/project.yml`, `just ios` regenerates `app/iosApp/iosApp.xcodeproj` from `app/iosApp/project.yml`,
builds the KMP framework (`Shared.framework` from `:app:shared`), links it builds the KMP framework (`Shared.framework` from `:app:shared`), links it
into the SwiftUI host, uninstalls any previous copy, installs, and launches. into the SwiftUI host, uninstalls any previous copy, installs, and launches.
@@ -135,13 +143,18 @@ each pick was made.
## Run a sanderling test (iOS) ## Run a sanderling test (iOS)
```sh ```sh
just test-ios # default simulator: iPhone 17 Pro just test-ios # asks which simulator, unless one is booted
IOS_DEVICE="iPhone 15" just test-ios # pick a different simulator IOS_DEVICE="iPhone 15" just test-ios # name it, by name or UDID
``` ```
`just test-ios` boots the simulator if needed, runs `just ios` to install `just test-ios` settles on a simulator once for the whole run, boots it if
and launch the app, then invokes `sanderling test --platform ios`. Same needed, runs `just ios` to install and launch the app, then invokes `sanderling
`DURATION`, `SEED`, and `OUTPUT` env vars as the Android target. test --platform ios`. Same `DURATION`, `SEED`, and `OUTPUT` env vars as the
Android target.
A physical iPhone is a different target: `just test-ios-device` requires
`IOS_DEVICE` to name it, and says so rather than running, because an empty one
resolves to a booted simulator and would fuzz that instead.
## How it connects to sanderling ## How it connects to sanderling
+92 -15
View File
@@ -7,7 +7,7 @@ android_device := env_var_or_default("ANDROID_DEVICE", "")
duration := env_var_or_default("DURATION", "1m") duration := env_var_or_default("DURATION", "1m")
seed := env_var_or_default("SEED", "0") seed := env_var_or_default("SEED", "0")
output := env_var_or_default("OUTPUT", justfile_directory() / "sanderling" / "runs") output := env_var_or_default("OUTPUT", justfile_directory() / "sanderling" / "runs")
ios_device := env_var_or_default("IOS_DEVICE", "iPhone 17 Pro") ios_device := env_var_or_default("IOS_DEVICE", "")
ios_app := justfile_directory() / "app" / "iosApp" / "build" / "Build" / "Products" / "Debug-iphonesimulator" / "iosApp.app" ios_app := justfile_directory() / "app" / "iosApp" / "build" / "Build" / "Products" / "Debug-iphonesimulator" / "iosApp.app"
ios_app_device := justfile_directory() / "app" / "iosApp" / "build" / "Build" / "Products" / "Debug-iphoneos" / "iosApp.app" ios_app_device := justfile_directory() / "app" / "iosApp" / "build" / "Build" / "Products" / "Debug-iphoneos" / "iosApp.app"
apk := justfile_directory() / "app" / "androidApp" / "build" / "outputs" / "apk" / "debug" / "androidApp-debug.apk" apk := justfile_directory() / "app" / "androidApp" / "build" / "outputs" / "apk" / "debug" / "androidApp-debug.apk"
@@ -248,33 +248,109 @@ clean:
./gradlew clean ./gradlew clean
rm -rf app/iosApp/build rm -rf app/iosApp/build
# Print the UDID of the simulator the iOS recipes drive. IOS_DEVICE names it by
# name or UDID, matched against booted simulators before merely available ones,
# the way sanderling matches --ios-device. A UDID rather than a name because the
# same iPhone exists under every installed runtime, and a seed only means
# something against one of them. Without IOS_DEVICE, a lone booted simulator is
# the one case taken without asking.
_require-ios-device:
#!/usr/bin/env bash
set -euo pipefail
rows="$(xcrun simctl list devices available | awk '
/^-- / { runtime = $0; sub(/^-- /, "", runtime); sub(/ --$/, "", runtime); next }
runtime ~ /^iOS/ && match($0, /\([0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}\)/) {
name = substr($0, 1, RSTART - 1)
gsub(/^[ \t]+|[ \t]+$/, "", name)
state = substr($0, RSTART + RLENGTH)
gsub(/[()[:space:]]/, "", state)
printf "%s\t%s\t%s\t%s\n", substr($0, RSTART + 1, RLENGTH - 2), name, runtime, state
}')"
catalog() {
printf '%s\n' "$rows" | awk -F'\t' 'NF { printf " %s (%s) %s%s\n", $2, $3, $1, ($4 == "Booted" ? " booted" : "") }'
}
refuse() {
cat >&2 <<EOF
folio: $1
$(catalog)
Set IOS_DEVICE to the simulator name or UDID to use. It can live in
examples/folio/.env:
IOS_DEVICE="iPhone 17 Pro" just test-ios
EOF
exit 1
}
if [[ -z "$rows" ]]; then
echo "folio: no iOS simulator is available. Create one in Xcode, or see 'xcrun simctl list devices'." >&2
exit 1
fi
if [[ -n "{{ios_device}}" ]]; then
matches="$(printf '%s\n' "$rows" | awk -F'\t' -v want="{{ios_device}}" '$4 == "Booted" && ($1 == want || $2 == want)')"
[[ -n "$matches" ]] || matches="$(printf '%s\n' "$rows" | awk -F'\t' -v want="{{ios_device}}" '$1 == want || $2 == want')"
[[ -n "$matches" ]] || refuse "IOS_DEVICE={{ios_device}} is not an available simulator."
else
matches="$(printf '%s\n' "$rows" | awk -F'\t' '$4 == "Booted"')"
[[ -n "$matches" ]] || matches="$rows"
fi
if [[ "$(printf '%s\n' "$matches" | grep -c .)" -eq 1 ]]; then
printf '%s\n' "$matches" | cut -f1
exit 0
fi
items="$(printf '%s\n' "$matches" | awk -F'\t' 'NF { printf "%s\t%s (%s) %s%s\n", $1, $2, $3, $1, ($4 == "Booted" ? " booted" : "") }')"
answer="$(FOLIO_PICK_ITEMS="$items" just _pick "folio: a run installs folio on the simulator, clears its state and drives it. Pick one:")"
case "$answer" in
"picked "*)
udid="${answer#picked }"
echo "folio: using $udid (IOS_DEVICE=$udid in examples/folio/.env skips this)" >&2
echo "$udid"
exit 0
;;
cancelled)
echo "folio: cancelled." >&2
exit 1
;;
esac
refuse "more than one simulator answers to that, and there is no terminal here to ask on."
# Regenerate iosApp.xcodeproj from project.yml. # Regenerate iosApp.xcodeproj from project.yml.
ios-gen: ios-gen:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
cd app/iosApp && xcodegen generate cd app/iosApp && xcodegen generate
# Build + install + launch on a booted iOS simulator (boots IOS_DEVICE if none). # Build + install + launch on the simulator given, or the one folio settles on.
ios: ios udid="":
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
export ANDROID_HOME="$(just _android-home)" export ANDROID_HOME="$(just _android-home)"
if ! xcrun simctl list devices booted | grep -q Booted; then # Every step addresses this UDID rather than "booted", so the build, the
xcrun simctl boot "{{ios_device}}" # install and the launch cannot land on different simulators.
udid="{{udid}}"
[[ -n "$udid" ]] || udid="$(just _require-ios-device)"
if ! xcrun simctl list devices booted | grep -q "$udid"; then
xcrun simctl boot "$udid"
open -a Simulator open -a Simulator
sleep 3 sleep 3
fi fi
just ios-gen just ios-gen
xcodebuild -project app/iosApp/iosApp.xcodeproj -scheme iosApp \ xcodebuild -project app/iosApp/iosApp.xcodeproj -scheme iosApp \
-destination 'platform=iOS Simulator,name={{ios_device}}' \ -destination "platform=iOS Simulator,id=$udid" \
-derivedDataPath app/iosApp/build \ -derivedDataPath app/iosApp/build \
build | tail -5 build | tail -5
# Installing over the top keeps the data container, and folio's signed-in # Installing over the top keeps the data container, and folio's signed-in
# session with it, so a run started straight after would open on the last # session with it, so a run started straight after would open on the last
# run's Home screen instead of Login and diverge at step 1. # run's Home screen instead of Login and diverge at step 1.
xcrun simctl uninstall booted app.folio || true xcrun simctl uninstall "$udid" app.folio || true
xcrun simctl install booted "{{ios_app}}" xcrun simctl install "$udid" "{{ios_app}}"
xcrun simctl launch booted app.folio xcrun simctl launch "$udid" app.folio
# Build the folio app for a connected physical iOS device (signed via .env creds). # Build the folio app for a connected physical iOS device (signed via .env creds).
# Signing reads ASC_API_* and DEVELOPMENT_TEAM/SANDERLING_IOS_TEAM from .env; # Signing reads ASC_API_* and DEVELOPMENT_TEAM/SANDERLING_IOS_TEAM from .env;
@@ -357,17 +433,14 @@ web-build:
test-ios: test-ios:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
just ios udid="$(just _require-ios-device)"
ios_device_flag=() just ios "$udid"
if [[ -n "{{ios_device}}" ]]; then
ios_device_flag=(--ios-device "{{ios_device}}")
fi
"{{sanderling}}" test \ "{{sanderling}}" test \
--platform ios \ --platform ios \
--spec "{{justfile_directory()}}/sanderling/spec.ts" \ --spec "{{justfile_directory()}}/sanderling/spec.ts" \
--bundle-id app.folio \ --bundle-id app.folio \
--ios-app-path "{{ios_app}}" \ --ios-app-path "{{ios_app}}" \
"${ios_device_flag[@]}" \ --ios-device "$udid" \
--duration "{{duration}}" \ --duration "{{duration}}" \
--seed "{{seed}}" \ --seed "{{seed}}" \
--output "{{output}}" --output "{{output}}"
@@ -378,6 +451,10 @@ test-ios:
test-ios-device: test-ios-device:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
if [[ -z "{{ios_device}}" ]]; then
echo "folio: set IOS_DEVICE to the connected device's name, UDID or CoreDevice id (see 'xcrun devicectl list devices'). Left empty, sanderling resolves a booted simulator and fuzzes that instead." >&2
exit 1
fi
just ios-device just ios-device
"{{sanderling}}" test \ "{{sanderling}}" test \
--platform ios \ --platform ios \