xcode.sh (22437B)
1 #!/bin/bash 2 set -euo pipefail 3 4 for variable in XCODE_DERIVED_DATA XCODE_SOURCE_PACKAGES XCODE_PACKAGE_CACHE; do 5 if [[ -z "${!variable:-}" ]]; then 6 echo "error: $variable is required; run this command through cargo extbuild" >&2 7 exit 1 8 fi 9 done 10 11 output_args=( 12 -quiet 13 -derivedDataPath "$XCODE_DERIVED_DATA" 14 -clonedSourcePackagesDirPath "$XCODE_SOURCE_PACKAGES" 15 -packageCachePath "$XCODE_PACKAGE_CACHE" 16 ) 17 offline_args=( 18 -disableAutomaticPackageResolution 19 -onlyUsePackageVersionsFromResolvedFile 20 -skipPackagePluginValidation 21 ) 22 23 operation=${1:-} 24 case "$operation" in 25 resolve) 26 exec xcodebuild \ 27 -resolvePackageDependencies \ 28 -project Tera.xcodeproj \ 29 -scheme Tera \ 30 "${output_args[@]}" 31 ;; 32 package-build) 33 exec xcodebuild \ 34 -scheme TeraApp \ 35 -destination 'generic/platform=iOS Simulator' \ 36 "${output_args[@]}" \ 37 "${offline_args[@]}" \ 38 ARCHS=arm64 \ 39 build 40 ;; 41 package-test) 42 destination=${2:?package-test requires a simulator destination} 43 exec xcodebuild \ 44 -scheme TeraAppPublicAPITests \ 45 -destination "$destination" \ 46 "${output_args[@]}" \ 47 "${offline_args[@]}" \ 48 test 49 ;; 50 project-build) 51 configuration=${2:?project-build requires a configuration} 52 case "$configuration" in 53 Debug|Release) ;; 54 *) echo "error: unsupported configuration: $configuration" >&2; exit 1 ;; 55 esac 56 exec xcodebuild \ 57 -project Tera.xcodeproj \ 58 -scheme Tera \ 59 -configuration "$configuration" \ 60 -destination 'generic/platform=iOS Simulator' \ 61 "${output_args[@]}" \ 62 "${offline_args[@]}" \ 63 build 64 ;; 65 project-test) 66 if [[ $# -lt 3 || $# -gt 4 ]]; then 67 echo "error: project-test requires destination, target and optional UI selector" >&2 68 exit 64 69 fi 70 destination=${2:?project-test requires a simulator destination} 71 test_target=${3:?project-test requires a test target} 72 case "$test_target" in 73 TeraTests|TeraUITests) ;; 74 *) echo "error: unsupported test target: $test_target" >&2; exit 1 ;; 75 esac 76 selector=${4:-} 77 selected_target=$test_target 78 if [[ -n "$selector" ]]; then 79 if [[ "$test_target" != TeraUITests || ! "$selector" =~ ^([A-Za-z][A-Za-z0-9]*UITests)(/(test[A-Za-z0-9]+))?$ ]]; then 80 echo "error: unsupported UI test selector" >&2 81 exit 64 82 fi 83 test_class=${BASH_REMATCH[1]} 84 test_method=${BASH_REMATCH[3]:-} 85 repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) 86 test_source="$repo_root/TeraUITests/$test_class.swift" 87 if [[ ! -f "$test_source" ]] || ! grep -Eq "^final class $test_class: XCTestCase" "$test_source"; then 88 echo "error: UI test class is not an owned test source" >&2 89 exit 64 90 fi 91 if [[ -n "$test_method" ]] && ! grep -Eq "^[[:space:]]+func $test_method\(\)" "$test_source"; then 92 echo "error: UI test method is not an owned test source" >&2 93 exit 64 94 fi 95 selected_target="$test_target/$selector" 96 fi 97 exec xcodebuild \ 98 -project Tera.xcodeproj \ 99 -scheme Tera \ 100 -destination "$destination" \ 101 "${output_args[@]}" \ 102 "${offline_args[@]}" \ 103 "-only-testing:$selected_target" \ 104 test 105 ;; 106 physical-app-build) 107 destination=${2:?physical app build requires a device destination} 108 xcconfig=${3:?physical app build requires an xcconfig} 109 physical_automation=${TERA_IOS_PHYSICAL_AUTOMATION:-} 110 development_team=${TERA_IOS_DEVELOPMENT_TEAM:?TERA_IOS_DEVELOPMENT_TEAM is required} 111 if [[ "$physical_automation" != "1" ]]; then 112 echo "error: physical app build requires TERA_IOS_PHYSICAL_AUTOMATION=1" >&2 113 exit 64 114 fi 115 if [[ ! "$destination" =~ ^id=[A-Fa-f0-9-]+$ ]]; then 116 echo "error: physical app build destination must be one exact device id" >&2 117 exit 64 118 fi 119 if [[ ! "$development_team" =~ ^[A-Z0-9]{10}$ ]]; then 120 echo "error: physical app build development team is invalid" >&2 121 exit 64 122 fi 123 if [[ "$xcconfig" != "$XCODE_DERIVED_DATA"/* || ! -f "$xcconfig" ]]; then 124 echo "error: physical app build xcconfig must be a regular file under XCODE_DERIVED_DATA" >&2 125 exit 64 126 fi 127 device_id=${destination#id=} 128 lock_state_file=$(mktemp "${TMPDIR:-/tmp}/tera-ios-lock-state.XXXXXX") 129 trap 'unlink "$lock_state_file"' EXIT 130 if ! xcrun devicectl device info lockState \ 131 --device "$device_id" \ 132 --quiet \ 133 --timeout 10 \ 134 --json-output "$lock_state_file" 135 then 136 echo "error: physical app build device lock state is unavailable" >&2 137 exit 1 138 fi 139 passcode_required=$(/usr/bin/plutil \ 140 -extract result.passcodeRequired raw -o - "$lock_state_file") 141 unlocked_since_boot=$(/usr/bin/plutil \ 142 -extract result.unlockedSinceBoot raw -o - "$lock_state_file") 143 if [[ "$passcode_required" != "false" || "$unlocked_since_boot" != "true" ]]; then 144 echo "error: physical app build device is locked; refusing to invoke Xcode" >&2 145 exit 1 146 fi 147 exec xcodebuild \ 148 -project Tera.xcodeproj \ 149 -scheme Tera \ 150 -configuration Debug \ 151 -destination "$destination" \ 152 -xcconfig "$xcconfig" \ 153 "${output_args[@]}" \ 154 "${offline_args[@]}" \ 155 "DEVELOPMENT_TEAM=$development_team" \ 156 CODE_SIGN_STYLE=Automatic \ 157 "CODE_SIGN_IDENTITY=Apple Development" \ 158 build 159 ;; 160 local-social-ui-test) 161 destination=${2:?local-social-ui-test requires a simulator destination} 162 result_name=${3:?local-social-ui-test requires a result name} 163 scenario=${4:-five-flow} 164 qualification_run_id=${TERA_IOS_UI_TEST_RUN_ID:?TERA_IOS_UI_TEST_RUN_ID is required} 165 relay_port=${TERA_IOS_LOCAL_SOCIAL_RELAY_PORT:-21000} 166 blossom_port=${TERA_IOS_LOCAL_SOCIAL_BLOSSOM_PORT:-21100} 167 if [[ ! "$destination" =~ ^platform=iOS\ Simulator,id=[A-Fa-f0-9-]+$ ]]; then 168 echo "error: local-social-ui-test destination must be one exact simulator id" >&2 169 exit 64 170 fi 171 if [[ ! "$result_name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]]; then 172 echo "error: local-social-ui-test result name is invalid" >&2 173 exit 64 174 fi 175 if [[ ! "$qualification_run_id" =~ ^[a-z0-9][a-z0-9-]{6,62}[a-z0-9]$ ]]; then 176 echo "error: local-social-ui-test run id is invalid" >&2 177 exit 64 178 fi 179 for port in "$relay_port" "$blossom_port"; do 180 if [[ ! "$port" =~ ^[0-9]+$ ]] || ((port < 1024 || port > 65535)); then 181 echo "error: local-social-ui-test port is invalid" >&2 182 exit 64 183 fi 184 done 185 if [[ "$relay_port" == "$blossom_port" ]]; then 186 echo "error: local-social-ui-test ports must be distinct" >&2 187 exit 64 188 fi 189 case "$scenario" in 190 five-flow) 191 test_selector=TeraUITests/TeraRemoteQualificationUITests/testLocalSocialFiveFlowScenario 192 evidence_command=verify 193 simulator_id=${destination##*id=} 194 previous_content_size= 195 requested_content_size=large 196 ;; 197 accessibility) 198 test_selector=TeraUITests/TeraRemoteQualificationUITests/testLocalSocialAccessibilitySemantics 199 evidence_command=verify-accessibility 200 simulator_id=${destination##*id=} 201 previous_content_size= 202 requested_content_size=accessibility-extra-extra-extra-large 203 persona_fixture= 204 persona_result= 205 ;; 206 persona) 207 test_selector=TeraUITests/TeraRemoteQualificationUITests/testLocalSocialDeterministicPersonas 208 evidence_command=verify-persona 209 simulator_id=${destination##*id=} 210 previous_content_size= 211 requested_content_size=accessibility-extra-extra-extra-large 212 persona_fixture=test-fixtures/local-social-personas.v1.json 213 persona_result="$XCODE_RESULTS/$result_name.persona-result.json" 214 ;; 215 *) 216 echo "error: unsupported local-social-ui-test scenario: $scenario" >&2 217 exit 64 218 ;; 219 esac 220 : "${XCODE_RESULTS:?XCODE_RESULTS is required}" 221 mkdir -p "$XCODE_RESULTS" 222 result_bundle="$XCODE_RESULTS/$result_name.xcresult" 223 evidence="$XCODE_RESULTS/$result_name.fixture.json" 224 ready="$XCODE_RESULTS/$result_name.ready.json" 225 control="$XCODE_RESULTS/$result_name.enable-uploads" 226 if [[ -e "$result_bundle" || -e "$evidence" || -e "$ready" || -e "$control" || ( -n "${persona_result:-}" && -e "$persona_result" ) ]]; then 227 echo "error: local-social-ui-test result already exists: $result_name" >&2 228 exit 1 229 fi 230 if [[ "$scenario" == persona ]]; then 231 if [[ -n "$(git status --porcelain --untracked-files=all)" ]]; then 232 echo "error: persona qualification requires one clean exact source tree" >&2 233 exit 1 234 fi 235 source_commit=$(git rev-parse HEAD) 236 source_tree=$(git rev-parse 'HEAD^{tree}') 237 upstream=$(git rev-parse '@{upstream}') 238 xcode_identity=$(xcodebuild -version | tr '\n' ' ') 239 simulator_sdk_build=$(xcrun --sdk iphonesimulator --show-sdk-build-version) 240 app_build_sha256=$( 241 printf 'radroots.ios.local-social.app-build.v1\0%s\0%s\0%s\0%s\0%s\0%s\0%s\0' \ 242 "$source_commit" "$source_tree" TeraUITests test Debug \ 243 "$xcode_identity" "$simulator_sdk_build" \ 244 | shasum -a 256 | awk '{print $1}' 245 ) 246 persona_repair_commit=${TERA_IOS_UI_TEST_FORWARD_REPAIR_COMMIT:-} 247 if [[ "$source_commit" != "$upstream" ]]; then 248 echo "error: persona qualification source is not equal to its configured upstream" >&2 249 exit 1 250 fi 251 if [[ -n "$persona_repair_commit" ]]; then 252 if [[ ! "$persona_repair_commit" =~ ^[0-9a-f]{40}$ ]] || \ 253 ! git merge-base --is-ancestor "$persona_repair_commit" "$source_commit" 254 then 255 echo "error: persona forward-repair commit is invalid" >&2 256 exit 1 257 fi 258 fi 259 sh scripts/persona-verifier.sh verify-persona-fixture \ 260 --fixture "$persona_fixture" \ 261 --fixture-schema test-fixtures/local-social-personas.v1.schema.json \ 262 --result-schema test-fixtures/local-social-persona-results.v1.schema.json \ 263 --attempt-schema test-fixtures/local-social-persona-attempt-evidence.v1.schema.json \ 264 --result-v2-schema test-fixtures/local-social-persona-results.v2.schema.json 265 sh scripts/persona-verifier.sh serve \ 266 --relay-port "$relay_port" \ 267 --blossom-port "$blossom_port" \ 268 --evidence "$evidence" \ 269 --ready "$ready" \ 270 --control "$control" \ 271 --persona-fixture "$persona_fixture" & 272 else 273 sh scripts/persona-verifier.sh serve \ 274 --relay-port "$relay_port" \ 275 --blossom-port "$blossom_port" \ 276 --evidence "$evidence" \ 277 --ready "$ready" \ 278 --control "$control" & 279 fi 280 fixture_pid=$! 281 cleanup_fixture() { 282 kill "$fixture_pid" 2>/dev/null || true 283 wait "$fixture_pid" 2>/dev/null || true 284 if [[ -n "$previous_content_size" ]]; then 285 xcrun simctl ui "$simulator_id" content_size "$previous_content_size" 286 previous_content_size= 287 fi 288 } 289 trap cleanup_fixture EXIT INT TERM 290 for _ in {1..100}; do 291 [[ -f "$ready" ]] && break 292 kill -0 "$fixture_pid" 2>/dev/null || { 293 echo "error: local-social fixture exited before readiness" >&2 294 exit 1 295 } 296 sleep 0.1 297 done 298 if [[ ! -f "$ready" ]]; then 299 echo "error: local-social fixture readiness timed out" >&2 300 exit 1 301 fi 302 xcrun simctl boot "$simulator_id" >/dev/null 2>&1 || true 303 xcrun simctl bootstatus "$simulator_id" -b >/dev/null 304 previous_content_size=$(xcrun simctl ui "$simulator_id" content_size) 305 case "$previous_content_size" in 306 extra-small|small|medium|large|extra-large|extra-extra-large|extra-extra-extra-large|accessibility-medium|accessibility-large|accessibility-extra-large|accessibility-extra-extra-large|accessibility-extra-extra-extra-large) ;; 307 *) 308 echo "error: simulator content-size state is unavailable" >&2 309 exit 1 310 ;; 311 esac 312 xcrun simctl ui "$simulator_id" content_size "$requested_content_size" 313 set +e 314 xcodebuild \ 315 -project Tera.xcodeproj \ 316 -scheme Tera \ 317 -configuration Debug \ 318 -destination "$destination" \ 319 "${output_args[@]}" \ 320 "${offline_args[@]}" \ 321 -resultBundlePath "$result_bundle" \ 322 "-only-testing:$test_selector" \ 323 "TERA_IOS_UI_TEST_RUN_ID=$qualification_run_id" \ 324 "TERA_IOS_UI_TEST_NOSTR_RELAY_URLS=ws://127.0.0.1:$relay_port" \ 325 "TERA_IOS_UI_TEST_BLOSSOM_ORIGINS=http://127.0.0.1:$blossom_port" \ 326 "TERA_IOS_UI_TEST_FIXTURE_CONTROL=$control" \ 327 "TERA_IOS_UI_TEST_FIXTURE_EVIDENCE=$evidence" \ 328 "TERA_IOS_UI_TEST_NETWORK_PROFILE=simulator" \ 329 "TERA_IOS_UI_TEST_SOURCE_COMMIT=${source_commit:-}" \ 330 "TERA_IOS_UI_TEST_SOURCE_TREE=${source_tree:-}" \ 331 "TERA_IOS_UI_TEST_APP_BUILD_SHA256=${app_build_sha256:-}" \ 332 "TERA_IOS_UI_TEST_SIMULATOR_ID=$simulator_id" \ 333 test 334 test_status=$? 335 set -e 336 cleanup_fixture 337 trap - EXIT INT TERM 338 if ((test_status != 0)); then 339 exit "$test_status" 340 fi 341 if [[ "$scenario" == persona ]]; then 342 persona_result_command=(sh scripts/persona-verifier.sh "$evidence_command" \ 343 --fixture "$persona_fixture" \ 344 --fixture-schema test-fixtures/local-social-personas.v1.schema.json \ 345 --result-schema test-fixtures/local-social-persona-results.v1.schema.json \ 346 --attempt-schema test-fixtures/local-social-persona-attempt-evidence.v1.schema.json \ 347 --result-v2-schema test-fixtures/local-social-persona-results.v2.schema.json \ 348 --evidence "$evidence" \ 349 --result-bundle "$result_bundle" \ 350 --output "$persona_result" \ 351 --source-commit "$source_commit" \ 352 --source-tree "$source_tree" \ 353 --run-id "$qualification_run_id" \ 354 --simulator-id "$simulator_id") 355 if [[ -n "$persona_repair_commit" ]]; then 356 persona_result_command+=(--forward-repair-commit "$persona_repair_commit") 357 fi 358 "${persona_result_command[@]}" 359 else 360 sh scripts/persona-verifier.sh "$evidence_command" --evidence "$evidence" 361 fi 362 ;; 363 remote-ui-test) 364 destination=${2:?remote-ui-test requires a simulator destination} 365 test_selector=${3:?remote-ui-test requires a TeraUITests selector} 366 result_name=${4:?remote-ui-test requires a result name} 367 qualification_run_id=${TERA_IOS_UI_TEST_RUN_ID:?TERA_IOS_UI_TEST_RUN_ID is required} 368 blossom_origins=${TERA_IOS_UI_TEST_BLOSSOM_ORIGINS:?TERA_IOS_UI_TEST_BLOSSOM_ORIGINS is required} 369 relay_urls=${TERA_IOS_UI_TEST_NOSTR_RELAY_URLS:-} 370 if [[ ! "$destination" =~ ^platform=iOS\ Simulator,id=[A-Fa-f0-9-]+$ ]]; then 371 echo "error: remote-ui-test destination must be one exact simulator id" >&2 372 exit 64 373 fi 374 if [[ ! "$test_selector" =~ ^TeraUITests(/[-A-Za-z0-9_]+){0,2}$ ]]; then 375 echo "error: remote-ui-test selector is invalid" >&2 376 exit 64 377 fi 378 if [[ ! "$result_name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]]; then 379 echo "error: remote-ui-test result name is invalid" >&2 380 exit 64 381 fi 382 if [[ ! "$qualification_run_id" =~ ^[a-z0-9][a-z0-9-]{6,62}[a-z0-9]$ ]]; then 383 echo "error: remote-ui-test run id is invalid" >&2 384 exit 64 385 fi 386 result_bundle="$XCODE_RESULTS/$result_name.xcresult" 387 if [[ -e "$result_bundle" ]]; then 388 echo "error: remote-ui-test result already exists: $result_bundle" >&2 389 exit 1 390 fi 391 mkdir -p "$XCODE_RESULTS" 392 exec xcodebuild \ 393 -project Tera.xcodeproj \ 394 -scheme Tera \ 395 -configuration Debug \ 396 -destination "$destination" \ 397 "${output_args[@]}" \ 398 "${offline_args[@]}" \ 399 -resultBundlePath "$result_bundle" \ 400 "-only-testing:$test_selector" \ 401 "TERA_IOS_UI_TEST_RUN_ID=$qualification_run_id" \ 402 "TERA_IOS_UI_TEST_NOSTR_RELAY_URLS=$relay_urls" \ 403 "TERA_IOS_UI_TEST_BLOSSOM_ORIGINS=$blossom_origins" \ 404 "TERA_IOS_UI_TEST_NETWORK_PROFILE=public" \ 405 test 406 ;; 407 physical-ui-build|physical-ui-test) 408 destination=${2:?physical UI qualification requires a device destination} 409 test_selector=${3:?physical UI qualification requires a TeraUITests selector} 410 result_name=${4:-} 411 physical_automation=${TERA_IOS_UI_TEST_PHYSICAL_AUTOMATION:-} 412 development_team=${TERA_IOS_DEVELOPMENT_TEAM:?TERA_IOS_DEVELOPMENT_TEAM is required} 413 qualification_run_id=${TERA_IOS_UI_TEST_RUN_ID:?TERA_IOS_UI_TEST_RUN_ID is required} 414 blossom_origins=${TERA_IOS_UI_TEST_BLOSSOM_ORIGINS:?TERA_IOS_UI_TEST_BLOSSOM_ORIGINS is required} 415 relay_urls=${TERA_IOS_UI_TEST_NOSTR_RELAY_URLS:-} 416 if [[ "$physical_automation" != "1" ]]; then 417 echo "error: physical UI qualification requires TERA_IOS_UI_TEST_PHYSICAL_AUTOMATION=1" >&2 418 exit 64 419 fi 420 if [[ ! "$destination" =~ ^id=[A-Fa-f0-9-]+$ ]]; then 421 echo "error: physical-ui-test destination must be one exact device id" >&2 422 exit 64 423 fi 424 if [[ ! "$test_selector" =~ ^TeraUITests(/[-A-Za-z0-9_]+){0,2}$ ]]; then 425 echo "error: physical-ui-test selector is invalid" >&2 426 exit 64 427 fi 428 if [[ ! "$development_team" =~ ^[A-Z0-9]{10}$ ]]; then 429 echo "error: physical-ui-test development team is invalid" >&2 430 exit 64 431 fi 432 if [[ ! "$qualification_run_id" =~ ^[a-z0-9][a-z0-9-]{6,62}[a-z0-9]$ ]]; then 433 echo "error: physical-ui-test run id is invalid" >&2 434 exit 64 435 fi 436 device_id=${destination#id=} 437 lock_state_file=$(mktemp "${TMPDIR:-/tmp}/tera-ios-lock-state.XXXXXX") 438 trap 'unlink "$lock_state_file"' EXIT 439 if ! xcrun devicectl device info lockState \ 440 --device "$device_id" \ 441 --quiet \ 442 --timeout 10 \ 443 --json-output "$lock_state_file" 444 then 445 echo "error: physical UI qualification device lock state is unavailable" >&2 446 exit 1 447 fi 448 passcode_required=$(/usr/bin/plutil \ 449 -extract result.passcodeRequired raw -o - "$lock_state_file") 450 unlocked_since_boot=$(/usr/bin/plutil \ 451 -extract result.unlockedSinceBoot raw -o - "$lock_state_file") 452 if [[ "$passcode_required" != "false" || "$unlocked_since_boot" != "true" ]]; then 453 echo "error: physical UI qualification device is locked; refusing to invoke Xcode" >&2 454 exit 1 455 fi 456 physical_args=( 457 -project Tera.xcodeproj \ 458 -scheme Tera \ 459 -configuration Debug \ 460 -destination "$destination" \ 461 "${output_args[@]}" \ 462 "${offline_args[@]}" \ 463 "-only-testing:$test_selector" \ 464 "DEVELOPMENT_TEAM=$development_team" \ 465 CODE_SIGN_STYLE=Automatic \ 466 "CODE_SIGN_IDENTITY=Apple Development" \ 467 "TERA_IOS_UI_TEST_RUN_ID=$qualification_run_id" \ 468 "TERA_IOS_UI_TEST_NOSTR_RELAY_URLS=$relay_urls" \ 469 "TERA_IOS_UI_TEST_BLOSSOM_ORIGINS=$blossom_origins" \ 470 "TERA_IOS_UI_TEST_NETWORK_PROFILE=public" 471 ) 472 if [[ "$operation" == "physical-ui-build" ]]; then 473 exec xcodebuild "${physical_args[@]}" build-for-testing 474 fi 475 if [[ ! "$result_name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]]; then 476 echo "error: physical-ui-test result name is invalid" >&2 477 exit 64 478 fi 479 result_bundle="$XCODE_RESULTS/$result_name.xcresult" 480 if [[ -e "$result_bundle" ]]; then 481 echo "error: physical-ui-test result already exists: $result_bundle" >&2 482 exit 1 483 fi 484 mkdir -p "$XCODE_RESULTS" 485 exec xcodebuild \ 486 "${physical_args[@]}" \ 487 -resultBundlePath "$result_bundle" \ 488 test-without-building 489 ;; 490 *) 491 echo "usage: $0 {resolve|package-build|package-test|project-build|project-test|local-social-ui-test|remote-ui-test|physical-ui-build|physical-ui-test}" >&2 492 exit 64 493 ;; 494 esac