diff --git a/.bazelrc b/.bazelrc index 34db17a..fecd667 100644 --- a/.bazelrc +++ b/.bazelrc @@ -32,7 +32,13 @@ build:clang --host_cxxopt="-fvisibility=hidden" # iOS setup build:ios --ios_multi_cpus=sim_arm64 -build:ios --@rules_apple//apple/build_settings:ios_simulator_device="iPhone 16 Pro" +# The simulator device is not pinned here: which device profiles exist depends on +# the installed Xcode's iOS runtime, so any value checked in goes stale. Set it in +# .bazelrc.user, e.g. +# +# build:ios --@rules_apple//apple/build_settings:ios_simulator_device="iPhone 17" +# +# `xcrun simctl list devicetypes` lists what is available locally. # Platform-specific options for each supported platform. build:remote_linux_x64 --extra_execution_platforms=//platform/linux_x64 diff --git a/MODULE.bazel b/MODULE.bazel index b83de0b..a3ee6a5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -129,6 +129,29 @@ use_repo(maven, "maven") bazel_dep(name = "rules_kotlin", version = "2.3.20") bazel_dep(name = "rules_perl", version = "1.1.1") bazel_dep(name = "rules_swift", version = "4.0.0-rc2") + +# Swift Package Manager deps, used by //tools/simulator_manager. +bazel_dep(name = "rules_swift_package_manager", version = "1.22.0") + +swift_deps = use_extension( + "@rules_swift_package_manager//:extensions.bzl", + "swift_deps", +) +swift_deps.from_package( + declare_swift_deps_info = True, + resolved = "//tools/simulator_manager:Package.resolved", + swift = "//tools/simulator_manager:Package.swift", +) +use_repo( + swift_deps, + "swift_deps_info", + "swift_package", + "swiftpkg_shellout", + "swiftpkg_swift_argument_parser", + "swiftpkg_swift_nio", + "swiftpkg_swift_nio_extras", +) + bazel_dep(name = "rules_nodejs", version = "6.7.4") bazel_dep(name = "aspect_rules_ts", version = "3.8.10") bazel_dep(name = "aspect_bazel_lib", version = "2.22.5") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 4a9cca7..1906583 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1,5 +1,5 @@ { - "lockFileVersion": 26, + "lockFileVersion": 28, "registryFileHashes": { "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", @@ -42,6 +42,7 @@ "https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.2/MODULE.bazel": "30dfabbfae0139b1f0036e01c201dd4c0167da3017f0b7ef3820d78e07622989", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.4/MODULE.bazel": "a05cbd9bc16712a58dc27ffe0dceaefd0da59a9bd87a227379b2a934b26a39ab", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/MODULE.bazel": "004ba890363d05372a97248c37205ae64b6fa31047629cd2c0895a9d0c7779e8", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/source.json": "ac2c3213df8f985785f1d0aeb7f0f73d5324e6e67d593d9b9470fb74a25d4a9b", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c", @@ -118,6 +119,9 @@ "https://bcr.bazel.build/modules/boringssl/0.20240913.0/MODULE.bazel": "fcaa7503a5213290831a91ed1eb538551cf11ac0bc3a6ad92d0fef92c5bd25fb", "https://bcr.bazel.build/modules/boringssl/0.20241024.0/MODULE.bazel": "b540cff73d948cb79cb0bc108d7cef391d2098a25adabfda5043e4ef548dbc87", "https://bcr.bazel.build/modules/boringssl/0.20241024.0/source.json": "d843092e682b84188c043ac742965d7f96e04c846c7e338187e03238674909a9", + "https://bcr.bazel.build/modules/buildifier_prebuilt/7.3.1/MODULE.bazel": "537faf0ad9f5892910074b8e43b4c91c96f1d5d86b6ed04bdbe40cf68aa48b68", + "https://bcr.bazel.build/modules/buildifier_prebuilt/8.2.1.1/MODULE.bazel": "2e2e306add04b7c7cd21e73c9293dcbd7528a08a84338e919036f402eb6b1e2e", + "https://bcr.bazel.build/modules/buildifier_prebuilt/8.2.1.1/source.json": "4c86fd3a384a09613c2213fb1f71562d6d70471977e6e81173e6625fd6ce53bc", "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f", "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd", "https://bcr.bazel.build/modules/c-ares/1.15.0/MODULE.bazel": "ba0a78360fdc83f02f437a9e7df0532ad1fbaa59b722f6e715c11effebaa0166", @@ -129,6 +133,8 @@ "https://bcr.bazel.build/modules/cel-spec/0.24.0/MODULE.bazel": "e310c7aff8490ed689ccafd32729b77a660b9547f5a5ba9b20e967011c324b36", "https://bcr.bazel.build/modules/cel-spec/0.25.1/MODULE.bazel": "72d9f7a9cdf072c659a1afa145d217b42f51fb40ab2dfa348f8e3e6686a3931a", "https://bcr.bazel.build/modules/cel-spec/0.25.1/source.json": "af0408f0108ba99b834e426b83205ead3eb820a55dd9ef8a78b2512d751e4072", + "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.30.0/MODULE.bazel": "2430544fffd8569ad0ffc1a3a1a8c4e57968633b38ba062cf0f62cd1b92a2731", + "https://bcr.bazel.build/modules/cgrindel_bazel_starlib/0.30.0/source.json": "7be4b8aa40ecb041128100a38f676917ceba3552a9877a1ac7862b944dd4e0bb", "https://bcr.bazel.build/modules/civetweb/1.16/MODULE.bazel": "46a38f9daeb57392e3827fce7d40926be0c802bd23cdd6bfd3a96c804de42fae", "https://bcr.bazel.build/modules/civetweb/1.16/source.json": "ba8b9585adb8355cb51b999d57172fd05e7a762c56b8d4bac6db42c99de3beb7", "https://bcr.bazel.build/modules/curl/8.4.0/MODULE.bazel": "0bc250aa1cb69590049383df7a9537c809591fcf876c620f5f097c58fdc9bc10", @@ -157,9 +163,11 @@ "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel": "d1327ba0907d0275ed5103bfbbb13518f6c04955b402213319d0d6c0ce9839d4", "https://bcr.bazel.build/modules/gazelle/0.39.1/MODULE.bazel": "1fa3fefad240e535066fd0e6950dfccd627d36dc699ee0034645e51dbde3980f", "https://bcr.bazel.build/modules/gazelle/0.40.0/MODULE.bazel": "42ba5378ebe845fca43989a53186ab436d956db498acde790685fe0e8f9c6146", + "https://bcr.bazel.build/modules/gazelle/0.44.0/MODULE.bazel": "fd3177ca0938da57a1e416cad3f39b9c4334defbc717e89aba9d9ddbbb0341da", "https://bcr.bazel.build/modules/gazelle/0.45.0/MODULE.bazel": "ecd19ebe9f8e024e1ccffb6d997cc893a974bcc581f1ae08f386bdd448b10687", "https://bcr.bazel.build/modules/gazelle/0.46.0/MODULE.bazel": "3dec215dacf2427df87b524a2c99da387882a18d753f0b1b38675992bd0a99c6", "https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", + "https://bcr.bazel.build/modules/gazelle/0.50.0/MODULE.bazel": "5dce69aa1d7b5bc85d1e1cfe61f0a9a27426c46425ec1d064685f941a5977329", "https://bcr.bazel.build/modules/gazelle/0.51.1/MODULE.bazel": "54d11f87fd9ae1d7791a27d54378a847523ddf7910036c31ccb902528adfdebb", "https://bcr.bazel.build/modules/gazelle/0.51.1/source.json": "5fa9b417bb11343de5f15d9d898d6ae32c164117f9d10f1b5e59c70a207ee510", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", @@ -325,6 +333,7 @@ "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", "https://bcr.bazel.build/modules/rules_apple/3.5.1/MODULE.bazel": "3d1bbf65ad3692003d36d8a29eff54d4e5c1c5f4bfb60f79e28646a924d9101c", "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", + "https://bcr.bazel.build/modules/rules_apple/4.3.3/MODULE.bazel": "c5c2c4adeeac5f3f2f9b7f16abfa8be7ffefa596171d0d92bed4cae9ade0a498", "https://bcr.bazel.build/modules/rules_apple/5.0.0-rc2/MODULE.bazel": "3761cdbc000d4cffc21138e66c14e19b56ccb212636c12093d225cb118c28c8c", "https://bcr.bazel.build/modules/rules_apple/5.0.0-rc2/source.json": "7e412e23d5e3f43bd0ec84918f2e2e4dd19d6e84c66885f3a78fd8b085cb1b05", "https://bcr.bazel.build/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162", @@ -370,9 +379,11 @@ "https://bcr.bazel.build/modules/rules_go/0.49.0/MODULE.bazel": "61cfc1ba17123356d1b12b6c50f6e0162b2cc7fd6f51753c12471e973a0f72a5", "https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0", "https://bcr.bazel.build/modules/rules_go/0.51.0-rc2/MODULE.bazel": "edfc3a9cea7bedb0eaaff37b0d7817c1a4bf72b3c615580b0ffcee6c52690fd4", + "https://bcr.bazel.build/modules/rules_go/0.51.0/MODULE.bazel": "b6920f505935bfd69381651c942496d99b16e2a12f3dd5263b90ded16f3b4d0f", "https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a", "https://bcr.bazel.build/modules/rules_go/0.58.3/MODULE.bazel": "5582119a4a39558d8d1b1634bcae46043d4f43a31415e861c3551b2860040b5e", "https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae", + "https://bcr.bazel.build/modules/rules_go/0.60.0/MODULE.bazel": "4a57ff2ffc2a3570e3c5646575c5a4b07287e91bcdac5d1f72383d51502b48cb", "https://bcr.bazel.build/modules/rules_go/0.61.0/MODULE.bazel": "aeccf0e379a7144723596766d3a56bebcdfc15362bd64e34601a67260c6f7ccd", "https://bcr.bazel.build/modules/rules_go/0.61.0/source.json": "3b466ee8e8a044edf97928d91da79b86bc3543fe56639b8de4d4dfa95693e904", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", @@ -432,6 +443,8 @@ "https://bcr.bazel.build/modules/rules_license/0.0.8/MODULE.bazel": "5669c6fe49b5134dbf534db681ad3d67a2d49cfc197e4a95f1ca2fd7f3aebe96", "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", + "https://bcr.bazel.build/modules/rules_multitool/1.0.0/MODULE.bazel": "7f75dc8378368a10dcedcff6cbfee8254fff10748a219a6cb8de94fde4b6a574", + "https://bcr.bazel.build/modules/rules_multitool/1.0.0/source.json": "5b92dc9b267b024bb2174bdf5d8e32821abb53fd84545e00699c4e35826a1c78", "https://bcr.bazel.build/modules/rules_nodejs/5.8.2/MODULE.bazel": "6bc03c8f37f69401b888023bf511cb6ee4781433b0cb56236b2e55a21e3a026a", "https://bcr.bazel.build/modules/rules_nodejs/6.2.0/MODULE.bazel": "ec27907f55eb34705adb4e8257952162a2d4c3ed0f0b3b4c3c1aad1fac7be35e", "https://bcr.bazel.build/modules/rules_nodejs/6.7.4/MODULE.bazel": "e6a241a55c82e999145553d2e00a08fc6ebadf62b63d108fb5e984696ffd0bd2", @@ -472,6 +485,7 @@ "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", "https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f", "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8", + "https://bcr.bazel.build/modules/rules_python/1.8.0/MODULE.bazel": "c151c025dbcc93d8f62ab68ecc313c9176a868a0e6386981bf2a12aec77cbe7b", "https://bcr.bazel.build/modules/rules_python/2.0.2/MODULE.bazel": "9d26ecb43f248c2663fb29f3c9b833aa86c31e736096e58c7077f80038f8d32f", "https://bcr.bazel.build/modules/rules_python/2.0.2/source.json": "36a74dedc1498bfbf72855568a24e6aa30c70b5ac7e114bc244c62b3a58b47fc", "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251", @@ -497,9 +511,12 @@ "https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd", "https://bcr.bazel.build/modules/rules_swift/2.5.0/MODULE.bazel": "6d2fec72be0ad2e1fc2eab536cf9d3fb149037c6d2aa678078f3512c06a819e1", "https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400", - "https://bcr.bazel.build/modules/rules_swift/3.6.1/MODULE.bazel": "5a68bb05b6b8044f8a7bd7184d56150189e7eca05522ce525517c234fed50ed8", + "https://bcr.bazel.build/modules/rules_swift/3.6.0/MODULE.bazel": "277f4239428c80cef0106eceed62e20d7617dfbf837bea0fddb23f82793e5ffc", "https://bcr.bazel.build/modules/rules_swift/4.0.0-rc1/MODULE.bazel": "e372b183ec0dfb3a4a1333c8450ac01c0ecfee97f149d6919a39c7848e6e6ce7", - "https://bcr.bazel.build/modules/rules_swift/4.0.0-rc1/source.json": "3f6d91f16d7f495048ab83dd75cfd955066c6b28c2f18ad24ae2d40b138c462f", + "https://bcr.bazel.build/modules/rules_swift/4.0.0-rc2/MODULE.bazel": "cd6cbb1c0aed49c216e6154b247366efe0dee96d0ab01c7b281ce8c6f6e628e7", + "https://bcr.bazel.build/modules/rules_swift/4.0.0-rc2/source.json": "90d869d918eecba3a64a06dee4b0de18a6162e29ce821cbf9207a97a166648a9", + "https://bcr.bazel.build/modules/rules_swift_package_manager/1.22.0/MODULE.bazel": "acc6df3d9b4bfc817e20b73a71e5ee266f68527798e1d45f29fe5d6d49b8d05e", + "https://bcr.bazel.build/modules/rules_swift_package_manager/1.22.0/source.json": "36b3f5a856e8a7592b3377726aa0dbf752e0ad1cff097260dcade0379d8ec963", "https://bcr.bazel.build/modules/stardoc/0.5.0/MODULE.bazel": "f9f1f46ba8d9c3362648eea571c6f9100680efc44913618811b58cc9c02cd678", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", @@ -509,7 +526,8 @@ "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7", "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", - "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", + "https://bcr.bazel.build/modules/stardoc/0.8.0/MODULE.bazel": "bbad4298d7ba185684f5fcd71b049c95b0575d1248891fd80b8d7077d647c9d8", + "https://bcr.bazel.build/modules/stardoc/0.8.0/source.json": "7321db37080ee8a445dc60e8516a98ab3a27884d1457b892485d73759ccb7f4d", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", "https://bcr.bazel.build/modules/swift_argument_parser/1.7.0/MODULE.bazel": "40d4e44950e44973dcf8590bcee637591de196b5dbe3696d07eb342b74d53672", @@ -542,75 +560,9 @@ }, "selectedYankedVersions": {}, "moduleExtensions": { - "@@aspect_rules_js+//npm:extensions.bzl%pnpm": { - "general": { - "bzlTransitiveDigest": "IXGzF0f+mCyPLrswS1JgWRNT9Vi/geDTGnMCsoVAVtg=", - "usagesDigest": "VcsfDFwmBt+ECaA/4/v9Ozpk8OnXWBpmy21oeFuDhZg=", - "recordedInputs": [ - "REPO_MAPPING:aspect_bazel_lib+,bazel_skylib bazel_skylib+", - "REPO_MAPPING:aspect_bazel_lib+,bazel_tools bazel_tools", - "REPO_MAPPING:aspect_rules_js+,aspect_bazel_lib aspect_bazel_lib+", - "REPO_MAPPING:aspect_rules_js+,bazel_features bazel_features+", - "REPO_MAPPING:aspect_rules_js+,bazel_skylib bazel_skylib+", - "REPO_MAPPING:aspect_rules_js+,bazel_tools bazel_tools", - "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals", - "REPO_MAPPING:bazel_features+,bazel_features_version bazel_features++version_extension+bazel_features_version" - ], - "generatedRepoSpecs": { - "pnpm": { - "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_rule", - "attributes": { - "package": "pnpm", - "version": "8.6.7", - "root_package": "", - "link_workspace": "", - "link_packages": {}, - "integrity": "sha512-vRIWpD/L4phf9Bk2o/O2TDR8fFoJnpYrp2TKqTIZF/qZ2/rgL3qKXzHofHgbXsinwMoSEigz28sqk3pQ+yMEQQ==", - "url": "", - "commit": "", - "patch_args": [ - "-p0" - ], - "patches": [], - "custom_postinstall": "", - "npm_auth": "", - "npm_auth_basic": "", - "npm_auth_username": "", - "npm_auth_password": "", - "lifecycle_hooks": [], - "extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])", - "extract_full_archive": true, - "system_tar": "auto" - } - }, - "pnpm__links": { - "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_links", - "attributes": { - "package": "pnpm", - "version": "8.6.7", - "dev": false, - "root_package": "", - "link_packages": {}, - "deps": {}, - "transitive_closure": {}, - "lifecycle_build_target": false, - "lifecycle_hooks_env": [], - "lifecycle_hooks_execution_requirements": [ - "no-sandbox" - ], - "lifecycle_hooks_use_default_shell_env": false, - "bins": {}, - "package_visibility": [ - "//visibility:public" - ] - } - } - } - } - }, "@@aspect_rules_ts+//ts:extensions.bzl%ext": { "general": { - "bzlTransitiveDigest": "XQyG/pNDNTKYobWyPdbo1p8twKShTk51G5mVUm9v4/4=", + "bzlTransitiveDigest": "yvupRmFkj/wIgIBLuROt8+an8UBfd+/P1X9ljTISqSM=", "usagesDigest": "YFCF4Pe3ZMqh53Xb1GbuZYKE5Q121NDwst+POer5vxE=", "recordedInputs": [ "REPO_MAPPING:aspect_rules_ts+,aspect_rules_ts aspect_rules_ts+", @@ -630,71 +582,15 @@ } } }, - "@@aspect_tools_telemetry+//:extension.bzl%telemetry": { + "@@cgrindel_bazel_starlib+//tools/git:extensions.bzl%local_git_ext": { "general": { - "bzlTransitiveDigest": "cl5A2O84vDL6Tt+Qga8FCj1DUDGqn+e7ly5rZ+4xvcc=", - "usagesDigest": "ZfgvvqG7LOd6ntR6gVA0JP55mLnNTIlT/patFewZeI4=", - "recordedInputs": [ - "REPO_MAPPING:aspect_tools_telemetry+,bazel_lib bazel_lib+", - "REPO_MAPPING:aspect_tools_telemetry+,bazel_skylib bazel_skylib+" - ], - "generatedRepoSpecs": { - "aspect_tools_telemetry_report": { - "repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository", - "attributes": { - "deps": { - "aspect_rules_ts": "3.8.10", - "aspect_tools_telemetry": "0.3.3" - } - } - } - } - } - }, - "@@googleapis+//:extensions.bzl%switched_rules": { - "general": { - "bzlTransitiveDigest": "1ZTg77R3Ks/ksx8QMNIoTRJetbJPRlzGFlJa9hoUn+Y=", - "usagesDigest": "4qqLlQt9ukbtcljUbNd31KFkYT16jA3KGb6yYAFv+o4=", + "bzlTransitiveDigest": "Q/NQyKFI2DljjsevKbLpWlOrT/bOsP3m/A8uvcXQb40=", + "usagesDigest": "4Cm7oj9uqLvdpjcsuReczgtCIDAeyjWJKek8KeJsZEk=", "recordedInputs": [], - "generatedRepoSpecs": {} - } - }, - "@@pybind11_bazel+//:internal_configure.bzl%internal_configure_extension": { - "general": { - "bzlTransitiveDigest": "06cynZ1bCvvy8zHPrrDlXq+Z68xmjctHpfFxi+zEpJY=", - "usagesDigest": "D1r3lfzMuUBFxgG8V6o0bQTLMk3GkaGOaPzw53wrwyw=", - "recordedInputs": [ - "REPO_MAPPING:pybind11_bazel+,bazel_tools bazel_tools", - "FILE:@@pybind11_bazel+//MODULE.bazel e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34" - ], "generatedRepoSpecs": { - "pybind11": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "build_file": "@@pybind11_bazel+//:pybind11-BUILD.bazel", - "strip_prefix": "pybind11-2.12.0", - "urls": [ - "https://github.com/pybind/pybind11/archive/v2.12.0.zip" - ] - } - } - } - } - }, - "@@rules_android+//bzlmod_extensions:apksig.bzl%apksig_extension": { - "general": { - "bzlTransitiveDigest": "KrgiB8lWxqVdbipL9YIYOuGjS80xlZIM/EBvUg8SVd0=", - "usagesDigest": "0FXD4PX+vQ/jVne2oV4v3Cw5Mc9DZQ4yTcoRkAjj/X4=", - "recordedInputs": [ - "REPO_MAPPING:rules_android+,bazel_tools bazel_tools" - ], - "generatedRepoSpecs": { - "apksig": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://android.googlesource.com/platform/tools/apksig/+archive/24e3075e68ebe17c0b529bb24bfda819db5e2f3b.tar.gz", - "build_file": "@@rules_android+//bzlmod_extensions:apksig.BUILD" - } + "bazel_starlib_git_toolchains": { + "repoRuleId": "@@cgrindel_bazel_starlib+//tools/git:local_git.bzl%local_git", + "attributes": {} } } } @@ -978,6 +874,413 @@ } } }, + "@@rules_swift+//swift:extensions.bzl%system_sdk": { + "general": { + "bzlTransitiveDigest": "V1TzdrobdXQJdLMsCKPHtHj3WT5iS0Cm06VlhVYaHP0=", + "usagesDigest": "J8oyUQAY7Ef5fufrhzY+sU6ta1VtLkHJLLpJkrOaKek=", + "recordedInputs": [ + "ENV:DEVELOPER_DIR \\0", + "ENV:XCODE_VERSION \\0", + "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals", + "REPO_MAPPING:bazel_features+,bazel_features_version bazel_features++version_extension+bazel_features_version", + "REPO_MAPPING:rules_swift+,bazel_features bazel_features+", + "REPO_MAPPING:rules_swift+,bazel_skylib bazel_skylib+", + "REPO_MAPPING:rules_swift+,bazel_tools bazel_tools", + "FILE:@@rules_swift+//tools/explicit_modules/scan.py ff4a824fbabf55f28dc97c9a5f1f52f1f43990d67854253685211d2906f5f77c" + ], + "generatedRepoSpecs": { + "system_sdk_xcode_locator": { + "repoRuleId": "@@rules_swift+//tools/explicit_modules:xcode_locator_repo.bzl%xcode_locator_repo", + "attributes": {} + }, + "system_sdk_xcode_26_5_0_17F42": { + "repoRuleId": "@@rules_swift+//tools/explicit_modules:xcode_explicit_module_repo.bzl%xcode_explicit_module_repo", + "attributes": { + "exclude_modules": {}, + "sdks": [ + "MacOSX", + "iPhoneOS", + "iPhoneSimulator" + ], + "xcode_version": "26.5.0.17F42", + "xcode_locator": "@@rules_swift++system_sdk+system_sdk_xcode_locator//:xcode-locator-bin" + } + }, + "system_sdk_xcode_26_6_0_17F113": { + "repoRuleId": "@@rules_swift+//tools/explicit_modules:xcode_explicit_module_repo.bzl%xcode_explicit_module_repo", + "attributes": { + "exclude_modules": {}, + "sdks": [ + "MacOSX", + "iPhoneOS", + "iPhoneSimulator" + ], + "xcode_version": "26.6.0.17F113", + "xcode_locator": "@@rules_swift++system_sdk+system_sdk_xcode_locator//:xcode-locator-bin" + } + }, + "system_sdk": { + "repoRuleId": "@@rules_swift+//tools/explicit_modules:xcode_explicit_module_hub_repo.bzl%xcode_explicit_module_hub_repo", + "attributes": { + "xcode_versions": [ + "26.5.0.17F42", + "26.6.0.17F113" + ], + "default_manifest": "@@rules_swift++system_sdk+system_sdk_xcode_26_5_0_17F42//:module_names.json" + } + } + } + } + }, + "@@rules_swift_package_manager+//:extensions.bzl%swift_deps": { + "general": { + "bzlTransitiveDigest": "ylnAw755th8T+6Buou/hnD61W4Q1RtalydQd5hC2dG0=", + "usagesDigest": "vPdKyRAcS8hrI1NXPs5IzcYNELqwLu/trqqoslDboaw=", + "recordedInputs": [ + "REPO_MAPPING:rules_swift_package_manager+,bazel_skylib bazel_skylib+", + "REPO_MAPPING:rules_swift_package_manager+,bazel_tools bazel_tools", + "REPO_MAPPING:rules_swift_package_manager+,cgrindel_bazel_starlib cgrindel_bazel_starlib+", + "FILE:@@//tools/simulator_manager/Package.resolved e8f709681256a2efabdc0cce5da1cf48351fd552a9bcfdca1060b7919219deaf" + ], + "generatedRepoSpecs": { + "swift_deps_info": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_deps_info.bzl%swift_deps_info", + "attributes": { + "direct_dep_pkg_infos": { + "@swiftpkg_swift_argument_parser//:pkg_info.json": "swift-argument-parser", + "@swiftpkg_swift_nio//:pkg_info.json": "swift-nio", + "@swiftpkg_swift_nio_extras//:pkg_info.json": "swift-nio-extras", + "@swiftpkg_shellout//:pkg_info.json": "shellout" + } + } + }, + "swift_package": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package_tool_repo.bzl%swift_package_tool_repo", + "attributes": { + "env": {}, + "package": "tools/simulator_manager/Package.swift" + } + }, + "swiftpkg_swift_argument_parser": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_argument_parser", + "commit": "6a52f3251125d74daf04fcbd5e6f08a75d074382", + "remote": "https://github.com/apple/swift-argument-parser", + "version": "1.8.2", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_nio": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_nio", + "commit": "0b18836bd8b0162e7e17a995a3fbee20ed8f3b2b", + "remote": "https://github.com/apple/swift-nio", + "version": "2.101.3", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_nio_extras": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_nio_extras", + "commit": "88a51340f59cf181ebde888bd1b749296b3ec029", + "remote": "https://github.com/apple/swift-nio-extras", + "version": "1.34.3", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_shellout": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_shellout", + "commit": "e1577acf2b6e90086d01a6d5e2b8efdaae033568", + "remote": "https://github.com/JohnSundell/ShellOut", + "version": "2.3.0", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_algorithms": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_algorithms", + "commit": "87e50f483c54e6efd60e885f7f5aa946cee68023", + "remote": "https://github.com/apple/swift-algorithms.git", + "version": "1.2.1", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_asn1": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_asn1", + "commit": "a9a5efd40eaf558a2bcd48d64b1d1646be686008", + "remote": "https://github.com/apple/swift-asn1.git", + "version": "1.7.1", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_async_algorithms": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_async_algorithms", + "commit": "3da39bbc4e687d4192af7c9cf4eab805745a0b9c", + "remote": "https://github.com/apple/swift-async-algorithms.git", + "version": "1.1.5", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_atomics": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_atomics", + "commit": "0442cb5a3f98ab802acb777929fdb446bda11a34", + "remote": "https://github.com/apple/swift-atomics.git", + "version": "1.3.1", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_certificates": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_certificates", + "commit": "449dbbecd0f31e82b510ada227ca152caa8b5e98", + "remote": "https://github.com/apple/swift-certificates.git", + "version": "1.19.4", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_collections": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_collections", + "commit": "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "remote": "https://github.com/apple/swift-collections.git", + "version": "1.6.0", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_crypto": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_crypto", + "commit": "47d3869a7291f085c1fb9fb1e6d3b97a793f45c6", + "remote": "https://github.com/apple/swift-crypto.git", + "version": "4.5.1", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_http_structured_headers": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_http_structured_headers", + "commit": "933538faa42c432d385f02e07df0ace7c5ecfc47", + "remote": "https://github.com/apple/swift-http-structured-headers.git", + "version": "1.7.0", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_http_types": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_http_types", + "commit": "db774a277f60063a32d854f2980299caf06da041", + "remote": "https://github.com/apple/swift-http-types.git", + "version": "1.6.0", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_log": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_log", + "commit": "a878e7f8f46cfc0e1125e565b5c08e7d5272dc9a", + "remote": "https://github.com/apple/swift-log.git", + "version": "1.14.0", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_nio_http2": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_nio_http2", + "commit": "45bdf670248be5f16ec0340e125dca285536f0fb", + "remote": "https://github.com/apple/swift-nio-http2.git", + "version": "1.45.0", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_nio_ssl": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_nio_ssl", + "commit": "d930168b86f46ca51a4bc09c5ca45c1833db8067", + "remote": "https://github.com/apple/swift-nio-ssl.git", + "version": "2.37.2", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_numerics": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_numerics", + "commit": "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "remote": "https://github.com/apple/swift-numerics.git", + "version": "1.1.1", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_service_lifecycle": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_service_lifecycle", + "commit": "9829955b385e5bb88128b73f1b8389e9b9c3191a", + "remote": "https://github.com/swift-server/swift-service-lifecycle.git", + "version": "2.11.0", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + }, + "swiftpkg_swift_system": { + "repoRuleId": "@@rules_swift_package_manager+//swiftpkg/internal:swift_package.bzl%swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swift_system", + "commit": "50688cacbd41d547e9eb9f7a213542340b7c442b", + "remote": "https://github.com/apple/swift-system.git", + "version": "1.7.5", + "env": {}, + "env_inherit": [], + "cached_json_directory": "", + "replace_scm_with_registry": false, + "target_deps": {}, + "module_aliases": {}, + "dep_module_aliases": "" + } + } + }, + "moduleExtensionMetadata": { + "explicitRootModuleDirectDeps": [ + "swiftpkg_swift_argument_parser", + "swiftpkg_swift_nio", + "swiftpkg_swift_nio_extras", + "swiftpkg_shellout", + "swift_deps_info", + "swift_package" + ], + "explicitRootModuleDirectDevDeps": [], + "useAllRepos": "NO", + "reproducible": false + } + } + }, "@@tar.bzl+//tar:extensions.bzl%toolchains": { "general": { "bzlTransitiveDigest": "/2afh6fPjq/rcyE/jztQDK3ierehmFFngfvmqyRv72M=", @@ -1360,6 +1663,172 @@ "0be62073ef8f5a2d3b9adcefddf18c417dab0a7975c71488ac2694856e2ff976" ] }, + "1.23.5": { + "aix_ppc64": [ + "go1.23.5.aix-ppc64.tar.gz", + "8d8bc7d1b362dd91426da9352741db298ff73e3e0a3ccbe6f607f80ba17647a4" + ], + "darwin_amd64": [ + "go1.23.5.darwin-amd64.tar.gz", + "d8b310b0b6bd6a630307579165cfac8a37571483c7d6804a10dd73bbefb0827f" + ], + "darwin_arm64": [ + "go1.23.5.darwin-arm64.tar.gz", + "047bfce4fbd0da6426bd30cd19716b35a466b1c15a45525ce65b9824acb33285" + ], + "dragonfly_amd64": [ + "go1.23.5.dragonfly-amd64.tar.gz", + "2dec52821e1f04a538d00b2cafe70fa506f2eea94a551bfe3ce1238f1bd4966f" + ], + "freebsd_386": [ + "go1.23.5.freebsd-386.tar.gz", + "7204e7bc62913b12f18c61afe0bc1a92fd192c0e45a54125978592296cb84e49" + ], + "freebsd_amd64": [ + "go1.23.5.freebsd-amd64.tar.gz", + "90a119995ebc3e36082874df5fa8fe6da194946679d01ae8bef33c87aab99391" + ], + "freebsd_arm": [ + "go1.23.5.freebsd-arm.tar.gz", + "255d26d873e41ff2fc278013bb2e5f25cf2ebe8d0ec84c07e3bb1436216020d3" + ], + "freebsd_arm64": [ + "go1.23.5.freebsd-arm64.tar.gz", + "2785d9122654980b59ca38305a11b34f2a1e12d9f7eb41d52efc137c1fc29e61" + ], + "freebsd_riscv64": [ + "go1.23.5.freebsd-riscv64.tar.gz", + "8f66a94018ab666d56868f61c579aa81e549ac9700979ce6004445d315be2d37" + ], + "illumos_amd64": [ + "go1.23.5.illumos-amd64.tar.gz", + "4b7a69928385ec512a4e77a547e24118adbb92301d2be36187ff0852ba9e6303" + ], + "linux_386": [ + "go1.23.5.linux-386.tar.gz", + "6ecf6a41d0925358905fa2641db0e1c9037aa5b5bcd26ca6734caf50d9196417" + ], + "linux_amd64": [ + "go1.23.5.linux-amd64.tar.gz", + "cbcad4a6482107c7c7926df1608106c189417163428200ce357695cc7e01d091" + ], + "linux_arm64": [ + "go1.23.5.linux-arm64.tar.gz", + "47c84d332123883653b70da2db7dd57d2a865921ba4724efcdf56b5da7021db0" + ], + "linux_armv6l": [ + "go1.23.5.linux-armv6l.tar.gz", + "04e0b5cf5c216f0aa1bf8204d49312ad0845800ab0702dfe4357c0b1241027a3" + ], + "linux_loong64": [ + "go1.23.5.linux-loong64.tar.gz", + "e1d14ac2207c78d52b76ba086da18a004c70aeb58cba72cd9bef0da7d1602786" + ], + "linux_mips": [ + "go1.23.5.linux-mips.tar.gz", + "d9e937f2fac4fc863850fb4cc31ae76d5495029a62858ef09c78604472d354c0" + ], + "linux_mips64": [ + "go1.23.5.linux-mips64.tar.gz", + "59710d0782abafd47e40d1cf96aafa596bbdee09ac7c61062404604f49bd523e" + ], + "linux_mips64le": [ + "go1.23.5.linux-mips64le.tar.gz", + "bc528cd836b4aa6701a42093ed390ef9929639a0e2818759887dc5539e517cab" + ], + "linux_mipsle": [ + "go1.23.5.linux-mipsle.tar.gz", + "a0404764ea1fd4a175dc5193622b15be6ed1ab59cbfa478f5ae24531bafb6cbd" + ], + "linux_ppc64": [ + "go1.23.5.linux-ppc64.tar.gz", + "db110284a0c91d4545273f210ca95b9f89f6e3ac90f39eb819033a6b96f25897" + ], + "linux_ppc64le": [ + "go1.23.5.linux-ppc64le.tar.gz", + "db268bf5710b5b1b82ab38722ba6e4427d9e4942aed78c7d09195a9dff329613" + ], + "linux_riscv64": [ + "go1.23.5.linux-riscv64.tar.gz", + "d9da15778442464f32acfa777ac731fd4d47362b233b83a0932380cb6d2d5dc8" + ], + "linux_s390x": [ + "go1.23.5.linux-s390x.tar.gz", + "14924b917d35311eb130e263f34931043d4f9dc65f20684301bf8f60a72edcdf" + ], + "netbsd_386": [ + "go1.23.5.netbsd-386.tar.gz", + "7b8074102e7f039bd6473c44f58cb323c98dcda48df98ad1f78aaa2664769c8f" + ], + "netbsd_amd64": [ + "go1.23.5.netbsd-amd64.tar.gz", + "1a466b9c8900e66664b15c07548ecb156e8274cf1028ac5da84134728e6dbbed" + ], + "netbsd_arm": [ + "go1.23.5.netbsd-arm.tar.gz", + "901c9e72038926e37a4dbde8f03d1d81fcb9992850901a3da1da5a25ef93e65b" + ], + "netbsd_arm64": [ + "go1.23.5.netbsd-arm64.tar.gz", + "221f69a7c3a920e3666633ee0b4e5c810176982e74339ba4693226996dc636e4" + ], + "openbsd_386": [ + "go1.23.5.openbsd-386.tar.gz", + "42e46cbf73febb8e6ddf848765ce1c39573736383b132402cdc487eb6be3ad06" + ], + "openbsd_amd64": [ + "go1.23.5.openbsd-amd64.tar.gz", + "f49e81fce17aab21800fab7c4b10c97ab02f8a9c807fdf8641ccf2f87d69289f" + ], + "openbsd_arm": [ + "go1.23.5.openbsd-arm.tar.gz", + "d8bd7269d4670a46e702b64822254a654824347c35923ef1c444d2e8687381ea" + ], + "openbsd_arm64": [ + "go1.23.5.openbsd-arm64.tar.gz", + "9cb259adff431d4d28b18e3348e26fe07ea10380675051dcfd740934b5e8b9f2" + ], + "openbsd_ppc64": [ + "go1.23.5.openbsd-ppc64.tar.gz", + "72a03223c98fcecfb06e57c3edd584f99fb7f6574a42f59348473f354be1f379" + ], + "openbsd_riscv64": [ + "go1.23.5.openbsd-riscv64.tar.gz", + "c06432b859afb36657207382b7bac03f961b8fafc18176b501d239575a9ace64" + ], + "plan9_386": [ + "go1.23.5.plan9-386.tar.gz", + "b1f9b12b269ab5cd4aa7ae3dd3075c2407c1ea8bb1211e6835261f98931201cc" + ], + "plan9_amd64": [ + "go1.23.5.plan9-amd64.tar.gz", + "45b4026a103e2f6cd436e2b7ad24b24a40dd22c9903519b98b45c535574fa01a" + ], + "plan9_arm": [ + "go1.23.5.plan9-arm.tar.gz", + "6e28e26f8c1e8620006490260aa5743198843aa0003c400cb65cbf5e743b21c7" + ], + "solaris_amd64": [ + "go1.23.5.solaris-amd64.tar.gz", + "0496c9969f208bd597f3e63fb27068ce1c7ed776618da1007fcc1c8be83ca413" + ], + "windows_386": [ + "go1.23.5.windows-386.zip", + "8441605a005ea74c28d8c02ca5f2708c17b4df7e91796148b9f8760caafb05c1" + ], + "windows_amd64": [ + "go1.23.5.windows-amd64.zip", + "96d74945d7daeeb98a7978d0cf099321d7eb821b45f5c510373d545162d39c20" + ], + "windows_arm": [ + "go1.23.5.windows-arm.zip", + "0005b31dcf9732c280a5cceb6aa1c5ab8284bc2541d0256c221256080acf2a09" + ], + "windows_arm64": [ + "go1.23.5.windows-arm64.zip", + "4f20c2d8a5a387c227e3ef48c5506b22906139d8afd8d66a78ef3de8dda1d1c3" + ] + }, "1.24.0": { "aix_ppc64": [ "go1.24.0.aix-ppc64.tar.gz", @@ -1846,6 +2315,168 @@ "27bab004c72b3d7bd05a69b6ec0fc54a309b4b78cc569dd963d8b3ec28bfdb8c" ] }, + "1.25.6": { + "aix_ppc64": [ + "go1.25.6.aix-ppc64.tar.gz", + "13c8bca505dd902091304da8abfacaf3512f40c3faefae70db33337d9a42c90e" + ], + "darwin_amd64": [ + "go1.25.6.darwin-amd64.tar.gz", + "e2b5b237f5c262931b8e280ac4b8363f156e19bfad5270c099998932819670b7" + ], + "darwin_arm64": [ + "go1.25.6.darwin-arm64.tar.gz", + "984521ae978a5377c7d782fd2dd953291840d7d3d0bd95781a1f32f16d94a006" + ], + "dragonfly_amd64": [ + "go1.25.6.dragonfly-amd64.tar.gz", + "6fdcdd4f769fe73a9c5602eb25533954903520f2a2a1953415ec4f8abf5bda52" + ], + "freebsd_386": [ + "go1.25.6.freebsd-386.tar.gz", + "be22b65ded1d4015d7d9d328284c985932771d120a371c7df41b2d4d1a91e943" + ], + "freebsd_amd64": [ + "go1.25.6.freebsd-amd64.tar.gz", + "61e1d50e332359474ff6dcf4bc0bd34ba2d2cf4ef649593a5faa527f0ab84e2b" + ], + "freebsd_arm": [ + "go1.25.6.freebsd-arm.tar.gz", + "546c2c6e325e72531bf6c8122a2360db8f8381e2dc1e8d147ecb0cb49b5f5f93" + ], + "freebsd_arm64": [ + "go1.25.6.freebsd-arm64.tar.gz", + "648484146702dd58db0e2c3d15bda3560340d149ed574936e63285a823116b77" + ], + "freebsd_riscv64": [ + "go1.25.6.freebsd-riscv64.tar.gz", + "663d7a9532bb4ac03c7a36b13b677b36d71031cd757b8acaee085e36c9ec8bc2" + ], + "illumos_amd64": [ + "go1.25.6.illumos-amd64.tar.gz", + "c6adb151f8f50a25ef5a3f7b1be67155045daa766261e686ea210b93b46bbbd5" + ], + "linux_386": [ + "go1.25.6.linux-386.tar.gz", + "59fe62eee3cca65332acef3ebe9b6ff3272467e0a08bf7f68f96334902bf23b9" + ], + "linux_amd64": [ + "go1.25.6.linux-amd64.tar.gz", + "f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" + ], + "linux_arm64": [ + "go1.25.6.linux-arm64.tar.gz", + "738ef87d79c34272424ccdf83302b7b0300b8b096ed443896089306117943dd5" + ], + "linux_armv6l": [ + "go1.25.6.linux-armv6l.tar.gz", + "679f0e70b27c637116791e3c98afbf8c954deb2cd336364944d014f8e440e2ae" + ], + "linux_loong64": [ + "go1.25.6.linux-loong64.tar.gz", + "433fe54d8797700b44fc4f1d085f9cd50ab3511b9b484fdfbb7b6c32a2be2486" + ], + "linux_mips": [ + "go1.25.6.linux-mips.tar.gz", + "a5beaf2d135b8e9a2f3d91fa7e7d3761ffc97630484168bbc9a21f3901119c11" + ], + "linux_mips64": [ + "go1.25.6.linux-mips64.tar.gz", + "f2d72c1ac315d453f429f48900f43cd8d0aa296a2b82fa90dba7dfb907483fd8" + ], + "linux_mips64le": [ + "go1.25.6.linux-mips64le.tar.gz", + "9b808ef978fd6414edd16736daa4a601c7e2dadff3bd640ade8a976535c974d4" + ], + "linux_mipsle": [ + "go1.25.6.linux-mipsle.tar.gz", + "4e0b190b05c8359455d96d379c751d403554dcadf6765932845b2886e555bfd6" + ], + "linux_ppc64": [ + "go1.25.6.linux-ppc64.tar.gz", + "5d0f479023b1481c9188cc066eca1293e6f8a67a882a6d93afafccfb51981476" + ], + "linux_ppc64le": [ + "go1.25.6.linux-ppc64le.tar.gz", + "bee02dbe034b12b839ae7807a85a61c13bee09ee38f2eeba2074bd26c0c0ab73" + ], + "linux_riscv64": [ + "go1.25.6.linux-riscv64.tar.gz", + "82a6b989afda1681ecb1f4fa96f1006484f42643eb5e76bed58f7f97316bf84b" + ], + "linux_s390x": [ + "go1.25.6.linux-s390x.tar.gz", + "3d97cc5670a0da9cb177037782129f0bf499ecb47abc40488248548abd2c2c35" + ], + "netbsd_386": [ + "go1.25.6.netbsd-386.tar.gz", + "eb526fff2568fc9938d6eda6f0f50449661c693fcd89ab6f84e5e77e0a98d99b" + ], + "netbsd_amd64": [ + "go1.25.6.netbsd-amd64.tar.gz", + "959d786e3384403ac9d957c04d71da905b02f457406ca123662cbd4688f9ce6e" + ], + "netbsd_arm": [ + "go1.25.6.netbsd-arm.tar.gz", + "fe6c3957f7feaf17ac72ca27590cc4914c19162fc0912869048cb3dc92f5c3fd" + ], + "netbsd_arm64": [ + "go1.25.6.netbsd-arm64.tar.gz", + "ddb5ec67fc4a0510b23560b7c01413bd9dde513cebfb5441a93e934f7e0c6853" + ], + "openbsd_386": [ + "go1.25.6.openbsd-386.tar.gz", + "167a18ff7db53f1652f3a65c905056bc14e7ab4319357498d0af998a83f457a9" + ], + "openbsd_amd64": [ + "go1.25.6.openbsd-amd64.tar.gz", + "06ec42383ff1e17abc0472e0a92eb028cb40b16ea09e2a86f80fbe60912d62de" + ], + "openbsd_arm": [ + "go1.25.6.openbsd-arm.tar.gz", + "751df8eadd0f3d7be8ea6cda3af1e2e942099f6c97abcc0cfb5c8a0ac8e0cf3f" + ], + "openbsd_arm64": [ + "go1.25.6.openbsd-arm64.tar.gz", + "d9828a6162c0c0fdb2d7e9dc8285c43b18a3dab62bf5e83b5891a4384f3157ad" + ], + "openbsd_ppc64": [ + "go1.25.6.openbsd-ppc64.tar.gz", + "73090f93dc861f2be9dc06d8209f32cd7ce7864b9b3e28f0cd54a9e031672699" + ], + "openbsd_riscv64": [ + "go1.25.6.openbsd-riscv64.tar.gz", + "6d4932cb639c1172cf5861b031bd0a24f7341ef579aac15b392779e10c69343b" + ], + "plan9_386": [ + "go1.25.6.plan9-386.tar.gz", + "b9db67922a94abe580e7bde9172eee2c223ade914cd12790d955a24554c134d5" + ], + "plan9_amd64": [ + "go1.25.6.plan9-amd64.tar.gz", + "aa1ff9aa3e1ed09ecb21d09d736997d2de9f373fea9402815b3221946d17dcd5" + ], + "plan9_arm": [ + "go1.25.6.plan9-arm.tar.gz", + "94ec04501527876a542960096f0199495cbd9f9103b229d5299382aa51d9cc32" + ], + "solaris_amd64": [ + "go1.25.6.solaris-amd64.tar.gz", + "9a1e89979be591b44e63be766c6571f5dc27b5fc3b79965c943186fcdaca0386" + ], + "windows_386": [ + "go1.25.6.windows-386.zip", + "873da5cec02b6657ecd5b85e562a38fb5faf1b6e9ea81b2eb0b9a9b5aea5cb35" + ], + "windows_amd64": [ + "go1.25.6.windows-amd64.zip", + "19b4733b727ba5c611b5656187f3ac367d278d64c3d4199a845e39c0fdac5335" + ], + "windows_arm64": [ + "go1.25.6.windows-arm64.zip", + "8f2d8e6dd0849a2ec0ade1683bcfb7809e64d264a4273d8437841000a28ffb60" + ] + }, "1.26.4": { "aix_ppc64": [ "go1.26.4.aix-ppc64.tar.gz", @@ -3119,5 +3750,6 @@ } } } - } + }, + "factsVersions": {} } diff --git a/ios/BUILD b/ios/BUILD index b097253..0d0bd05 100644 --- a/ios/BUILD +++ b/ios/BUILD @@ -1,7 +1,23 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_apple//apple:ios.bzl", "ios_application", "ios_ui_test", "ios_unit_test") +load( + "@rules_apple//apple/testing/default_runner:ios_xctestrun_runner.bzl", + "ios_xctestrun_runner", +) load("@rules_swift//swift:swift.bzl", "swift_library") +# Leases each test its own simulator from //tools/simulator_manager, so +# concurrent actions on one worker never share a device. +ios_xctestrun_runner( + name = "ios_leased_simulator_runner", + clean_up_simulator_action = "//tools/simulator_manager:release_simulator", + create_simulator_action = "//tools/simulator_manager:lease_simulator", + random = False, + # reuse_simulator doubles as the exclusivity toggle for the lease: opting out + # of reuse means no other test shares this device. + reuse_simulator = False, +) + swift_library( name = "HelloAppLibrary", srcs = ["HelloApp.swift"], @@ -49,7 +65,36 @@ swift_library( ios_ui_test( name = "HelloAppUITest", minimum_os_version = "26.0", - runner = "@rules_apple//apple/testing/default_runner:ios_xctestrun_ordered_runner", + runner = ":ios_leased_simulator_runner", test_host = ":HelloApp", deps = [":HelloAppUITestLib"], + exec_properties = { + "sandboxAllowed": "False", + }, +) + +swift_library( + name = "HelloConcurrentTestLib", + testonly = True, + srcs = ["HelloConcurrentTest.swift"], + module_name = "HelloConcurrentTest", +) + +# Exercises concurrent leasing: run with --runs_per_test to get several +# simultaneous leases from one target. Unlike HelloAppUITest this has no test +# host, so it takes rules_apple's `simctl spawn` path rather than `xcodebuild`. +# +# bazel test --config=ios --runs_per_test=3 //ios:HelloConcurrentTest +# +# Each run's log reports its simulator UDID; no two concurrent runs should match. +# Manual because it is sensitive to how much memory the host has — see the +# simulator_manager README. +ios_unit_test( + name = "HelloConcurrentTest", + minimum_os_version = "26.0", + runner = ":ios_leased_simulator_runner", + tags = ["manual"], + timeout = "long", + deps = [":HelloConcurrentTestLib"], ) + diff --git a/ios/HelloConcurrentTest.swift b/ios/HelloConcurrentTest.swift new file mode 100644 index 0000000..78d196a --- /dev/null +++ b/ios/HelloConcurrentTest.swift @@ -0,0 +1,17 @@ +import XCTest + +/// Checks that concurrent tests each lease their own simulator. +/// +/// Unlike HelloAppConcurrentUITest this has no test host, so rules_apple runs it +/// via `simctl spawn` rather than `xcodebuild`. +/// +/// Several targets share this source; each logs its UDID, and no two should +/// match. The sleep holds the lease long enough for the runs to actually overlap. +class HelloConcurrentTest: XCTestCase { + func testHoldsItsOwnSimulator() { + let udid = ProcessInfo.processInfo.environment["SIMULATOR_UDID"] ?? "unknown" + print("CONCURRENT_UNIT_TEST_SIMULATOR_UDID=\(udid)") + XCTAssertEqual(1 + 1, 2) + Thread.sleep(forTimeInterval: 20) + } +} diff --git a/tools/simulator_manager/BUILD b/tools/simulator_manager/BUILD new file mode 100644 index 0000000..0d03a4f --- /dev/null +++ b/tools/simulator_manager/BUILD @@ -0,0 +1,59 @@ +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") +load("@rules_swift//swift:swift.bzl", "swift_binary", "swift_test") + +# A daemon that hands out leases on iOS simulators. Ported from +# https://github.com/bazelbuild/rules_apple/pull/2767, which builds it with an +# internal `macos_swift_tool` macro; a plain swift_binary is equivalent here. +swift_binary( + name = "simulator_manager", + srcs = glob(["Sources/*.swift"]), + tags = ["no-linux-ci"], + deps = [ + "@swiftpkg_shellout//:ShellOut", + "@swiftpkg_swift_argument_parser//:ArgumentParser", + "@swiftpkg_swift_nio//:NIO", + "@swiftpkg_swift_nio//:NIOCore", + "@swiftpkg_swift_nio//:NIOHTTP1", + "@swiftpkg_swift_nio//:NIOPosix", + "@swiftpkg_swift_nio_extras//:NIOExtras", + ], +) + +swift_test( + name = "simulator_manager_test", + srcs = glob(["Tests/*.swift"]), + tags = ["no-linux-ci"], + deps = [":simulator_manager"], +) + +sh_binary( + name = "start", + srcs = ["start.sh"], + data = [ + ":prepare_simulator", + ":simulator_manager", + ], + visibility = ["//visibility:public"], + deps = ["@bazel_tools//tools/bash/runfiles"], +) + +sh_binary( + name = "prepare_simulator", + srcs = ["prepare_simulator.sh"], +) + +# rules_apple's create/clean_up_simulator_action hooks. Using these instead of +# patching the runner template (as PR 2767 does) keeps rules_apple unmodified. +sh_binary( + name = "lease_simulator", + srcs = ["lease_simulator.sh"], + data = [":start"], + visibility = ["//visibility:public"], + deps = ["@bazel_tools//tools/bash/runfiles"], +) + +sh_binary( + name = "release_simulator", + srcs = ["release_simulator.sh"], + visibility = ["//visibility:public"], +) diff --git a/tools/simulator_manager/Package.resolved b/tools/simulator_manager/Package.resolved new file mode 100644 index 0000000..40627e7 --- /dev/null +++ b/tools/simulator_manager/Package.resolved @@ -0,0 +1,176 @@ +{ + "pins" : [ + { + "identity" : "shellout", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/ShellOut", + "state" : { + "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", + "version" : "2.3.0" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", + "version" : "1.8.2" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "a9a5efd40eaf558a2bcd48d64b1d1646be686008", + "version" : "1.7.1" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "3da39bbc4e687d4192af7c9cf4eab805745a0b9c", + "version" : "1.1.5" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "449dbbecd0f31e82b510ada227ca152caa8b5e98", + "version" : "1.19.4" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "47d3869a7291f085c1fb9fb1e6d3b97a793f45c6", + "version" : "4.5.1" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "933538faa42c432d385f02e07df0ace7c5ecfc47", + "version" : "1.7.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "db774a277f60063a32d854f2980299caf06da041", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "a878e7f8f46cfc0e1125e565b5c08e7d5272dc9a", + "version" : "1.14.0" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio", + "state" : { + "revision" : "0b18836bd8b0162e7e17a995a3fbee20ed8f3b2b", + "version" : "2.101.3" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras", + "state" : { + "revision" : "88a51340f59cf181ebde888bd1b749296b3ec029", + "version" : "1.34.3" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "45bdf670248be5f16ec0340e125dca285536f0fb", + "version" : "1.45.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "d930168b86f46ca51a4bc09c5ca45c1833db8067", + "version" : "2.37.2" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "9829955b385e5bb88128b73f1b8389e9b9c3191a", + "version" : "2.11.0" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "50688cacbd41d547e9eb9f7a213542340b7c442b", + "version" : "1.7.5" + } + } + ], + "version" : 2 +} diff --git a/tools/simulator_manager/Package.swift b/tools/simulator_manager/Package.swift new file mode 100644 index 0000000..379e04d --- /dev/null +++ b/tools/simulator_manager/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version:5.9 + +// Declares the external Swift packages that //tools/simulator_manager needs. +// rules_swift_package_manager resolves these into @swiftpkg_* repositories; the +// products are wired into the swift_binary in BUILD.bazel. + +import PackageDescription + +let package = Package( + name: "simulator_manager", + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.8.2"), + .package(url: "https://github.com/apple/swift-nio", from: "2.101.3"), + .package(url: "https://github.com/apple/swift-nio-extras", from: "1.34.3"), + .package(url: "https://github.com/JohnSundell/ShellOut", from: "2.3.0"), + ] +) diff --git a/tools/simulator_manager/README.md b/tools/simulator_manager/README.md new file mode 100644 index 0000000..301f0fa --- /dev/null +++ b/tools/simulator_manager/README.md @@ -0,0 +1,102 @@ +# `simulator_manager` + +A daemon that hands out "leases" on iOS simulators, so that concurrent test +actions on one machine never share a device. + +Tests lease a simulator of a given configuration (device type and OS version) and +release it when done. A lease can be *exclusive*, meaning that test has sole +access to the device — needed for app host and UI tests, which drive the device +through a session that cannot be shared with another test. The daemon releases a +lease automatically if the leasing process exits without releasing, so a crashed +test does not strand a device. + +Base simulators are created per configuration; leases are granted on clones of +the base. A clone that stays idle past the daemon's threshold is deleted, which +frees disk and memory on remote executors while still allowing a warm device to +be reused shortly after. + +## Why + +Without this, `rules_apple` looks up a simulator by name and reuses whatever it +finds. Every concurrent test action on a worker adopts the same device, and they +race over it. The loser fails with: + + TestHost encountered an error (Early unexpected exit, operation never + finished bootstrapping - no restart will be attempted. (Underlying Error: The + test runner exited with code 0 before establishing connection.)) + +## How it is wired up + +`rules_apple` is **not** patched or forked. Two hooks plug into extension points +that `ios_xctestrun_runner` already provides: + +- `lease_simulator.sh` — the runner's `create_simulator_action`. Starts the daemon + if needed, leases a device, prints its UDID. +- `release_simulator.sh` — the runner's `clean_up_simulator_action`. Releases the + lease. Runs whether the test passed or failed. + +`//ios:ios_leased_simulator_runner` is an `ios_xctestrun_runner` using both. Note +it sets `reuse_simulator = False`: that attribute doubles as the exclusivity +toggle, since tests opting out of reuse are exactly the ones needing sole +ownership. + +## Running tests + +The simulator device type must be set, or the lease hook fails fast rather than +booting a device for a test that cannot run. It is deliberately not pinned in +`.bazelrc`, because which device profiles exist depends on the installed Xcode's +iOS runtime. Set it in `.bazelrc.user`: + + build:ios --@rules_apple//apple/build_settings:ios_simulator_device="iPhone 17" + +Then: + + bazel test --config=ios //ios:HelloAppUITest + +Use `xcrun simctl list devicetypes` and `xcrun simctl list runtimes` to see what +is installed locally. A device type with no profile in the installed runtime +fails with `no matching runtimes found`. + +## Concurrency and machine capacity + +Use `--runs_per_test` to get several simultaneous leases from one target: + + bazel test --config=ios --runs_per_test=3 //ios:HelloConcurrentTest + bazel test --config=ios --runs_per_test=3 //ios:HelloAppUITest + +Each run's log reports its own `-destination id=`; no two concurrent runs should +match. Concurrency is bounded by host memory, not by the daemon. Measured on a +16 GiB `mac2.metal` remote worker with `//ios:HelloAppUITest`: + +| runs | result | +| --- | --- | +| 3 | all passed, 238.5s each, deviation 0.0s | +| 5 | 4 of 5 timed out; passing run took 151s | + +In both cases leasing was correct — every run got a distinct device. At 5 the +machine simply runs out of memory. Note simulator memory is invisible to +per-action resource accounting, because those processes are children of +`launchd_sim` rather than of the test action, so a pool can look within budget +while the machine swaps. Size executor counts with that in mind. + +## Provenance + +Vendored from , which is +marked "do not merge" and is not in any `rules_apple` release. Local changes: + +- The upstream `BUILD.bazel` builds the daemon with an internal `macos_swift_tool` + macro; a plain `swift_binary` is equivalent. +- Upstream's `start.sh` resolves paths in the author's own repository, and lists a + `:start_tunnel` target that the PR does not include. Both adjusted. +- Upstream patches `ios_xctestrun_runner.template.sh` to call the daemon + directly. That is replaced by the two hook scripts above, which reach the same + API through unmodified `rules_apple`. + +The daemon derives a runtime identifier by substituting dashes for dots in the +version, so it needs a runtime version (`26.4`) rather than a patch version +(`26.4.1`, which yields the nonexistent +`com.apple.CoreSimulator.SimRuntime.iOS-26-4-1`). `lease_simulator.sh` resolves +the real runtime version from `simctl` before leasing. + +Since this is vendored rather than depended on, updates must be re-copied by +hand; there is no version to bump. diff --git a/tools/simulator_manager/Sources/AccumulatedHTTPHandler.swift b/tools/simulator_manager/Sources/AccumulatedHTTPHandler.swift new file mode 100644 index 0000000..0595e09 --- /dev/null +++ b/tools/simulator_manager/Sources/AccumulatedHTTPHandler.swift @@ -0,0 +1,80 @@ +import NIO +import NIOHTTP1 +import os.log + +extension Logger { + static let accumulatedHTTP = simulatorManager(category: "accumulated_http") +} + +struct FullHTTPRequest { + let head: HTTPRequestHead + var body: ByteBuffer +} + +struct FullHTTPResponse { + let head: HTTPResponseHead + var body: ByteBuffer +} + +final class AccumulatedHTTPHandler: ChannelInboundHandler, ChannelOutboundHandler { + typealias InboundIn = HTTPServerRequestPart + typealias InboundOut = FullHTTPRequest + + typealias OutboundIn = FullHTTPResponse + typealias OutboundOut = HTTPServerResponsePart + + private var requestHead: HTTPRequestHead? + private var bodyBuffer: ByteBuffer? + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let part = self.unwrapInboundIn(data) + + switch part { + case .head(let head): + self.requestHead = head + self.bodyBuffer = context.channel.allocator.buffer(capacity: 0) + + case .body(var chunk): + self.bodyBuffer?.writeBuffer(&chunk) + + case .end: + if let head = requestHead, let body = bodyBuffer { + Logger.accumulatedHTTP.info( + """ + ▶️ Received \(head.method.rawValue, privacy: .public) request for \ + \(head.uri, privacy: .public) + """ + ) + + let fullRequest = FullHTTPRequest(head: head, body: body) + context.fireChannelRead(self.wrapInboundOut(fullRequest)) + } + + self.requestHead = nil + self.bodyBuffer = nil + } + } + + func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + let fullResponse = unwrapOutboundIn(data) + + Logger.accumulatedHTTP.info( + "◀️ Sending \(fullResponse.head.status, privacy: .public) response" + ) + + context.write(wrapOutboundOut(.head(fullResponse.head)), promise: nil) + + if fullResponse.body.readableBytes > 0 { + context.write(wrapOutboundOut(.body(.byteBuffer(fullResponse.body))), promise: nil) + } + + context.write(wrapOutboundOut(.end(nil)), promise: promise) + } + + func errorCaught(context: ChannelHandlerContext, error: Error) { + Logger.accumulatedHTTP.error( + "❌ \(error.localizedDescription, privacy: .public)" + ) + context.close(promise: nil) + } +} diff --git a/tools/simulator_manager/Sources/HTTPServer.swift b/tools/simulator_manager/Sources/HTTPServer.swift new file mode 100644 index 0000000..e926fe2 --- /dev/null +++ b/tools/simulator_manager/Sources/HTTPServer.swift @@ -0,0 +1,164 @@ +import Foundation +import NIO +import NIOExtras +import NIOHTTP1 +import NIOPosix +import os.log + +extension Logger { + static let httpServer = simulatorManager(category: "server") +} + +final class HTTPServer { + private let simulatorRequestHandler: SimulatorRequestHandler + + private let version: String + + private var serverShutdownHandler: (() -> Void)? + + init(simulatorRequestHandler: SimulatorRequestHandler, version: String) { + self.simulatorRequestHandler = simulatorRequestHandler + self.version = version + } + + func run(pidPath: String, unixSocketPath: String) async throws { + let socketURL = URL(fileURLWithPath: unixSocketPath) + let pidURL = URL(fileURLWithPath: pidPath) + + // Remove existing files if they exist + try? FileManager.default.removeItem(at: socketURL) + try? FileManager.default.removeItem(at: pidURL) + + try String(ProcessInfo.processInfo.processIdentifier).write( + to: pidURL, + atomically: true, + encoding: .utf8 + ) + + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + + do { + // This nested block is necessary to ensure that all the destructors for objects defined + // inside are called before the final call to `eventLoopGroup.syncShutdownGracefully()`. A + // possible side effect of not doing this is a run-time error "Cannot schedule tasks on an + // EventLoop that has already shut down". + let quiesce = ServerQuiescingHelper(group: eventLoopGroup) + let fullyShutdownPromise: EventLoopPromise = eventLoopGroup.next().makePromise() + serverShutdownHandler = { + Logger.httpServer.info("⚠️ Shutting down server") + quiesce.initiateShutdown(promise: fullyShutdownPromise) + } + + do { + let serverChannel = try await ServerBootstrap(group: eventLoopGroup) + .serverChannelOption(ChannelOptions.backlog, value: 256) + .serverChannelInitializer { channel in + return channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler( + quiesce.makeServerChannelHandler(channel: channel) + ) + } + } + .bind(unixDomainSocketPath: unixSocketPath, childChannelInitializer: { childChannel in + return childChannel.eventLoop.makeCompletedFuture { + try childChannel.pipeline.syncOperations.addHandlers([ + HTTPResponseEncoder(), + ByteToMessageHandler(HTTPRequestDecoder()), + AccumulatedHTTPHandler(), + SimulatorManagerHTTPHandler(), + ]) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: childChannel, + configuration: .init() + ) + } + }) + + Logger.httpServer.info("🔌 Server running on UDS at \(unixSocketPath, privacy: .public)") + + try await withThrowingDiscardingTaskGroup { group in + try await serverChannel.executeThenClose { inbound in + for try await connectionChannel in inbound { + group.addTask { + do { + try await self.handleConnection( + channel: connectionChannel + ) + } catch { + // We don't throw here, as it locks up the whole server + Logger.httpServer.error( + """ + ❌ Caught connection error: \(error, privacy: .public) + """ + ) + } + } + } + } + } + } catch { + Logger.httpServer.error("❌ Caught top-level error: \(error, privacy: .public)") + try await eventLoopGroup.shutdownGracefully() + throw error + } + + try await fullyShutdownPromise.futureResult.get() + } + + try await eventLoopGroup.shutdownGracefully() + Logger.httpServer.info("✅ Server shut down") + + // Cleanup files + try? FileManager.default.removeItem(at: socketURL) + try? FileManager.default.removeItem(at: pidURL) + } + + private func handleConnection( + channel: NIOAsyncChannel + ) async throws { + try await channel.executeThenClose { inbound, outbound in + for try await request in inbound { + try await outbound.write(handleRequest(request)) + } + } + } + + private func handleRequest( + _ request: SimulatorManagerRequest + ) async -> SimulatorManagerResponse { + switch request.path { + case "simulator": + do { + return try await simulatorRequestHandler.handleRequest( + method: request.method, + pathComponents: request.pathComponents, + queryParameters: request.queryParameters + ) + } catch { + Logger.httpServer.error( + "❌ simulatorRequestHandler.handleRequest error: \(error, privacy: .public)" + ) + + return .init( + status: .internalServerError, + message: "Internal server error: \(error)" + ) + } + + case "version": + return .init( + status: .ok, + message: version + ) + + case "shutdown": + Logger.httpServer.info("⚠️ Shutdown request received") + serverShutdownHandler?() + return .init(status: .ok, message: "Server shutting down") + + default: + return .init(status: .badRequest, message: "Unknown method: \(request.path)") + } + } +} diff --git a/tools/simulator_manager/Sources/LRUSet.swift b/tools/simulator_manager/Sources/LRUSet.swift new file mode 100644 index 0000000..972c08c --- /dev/null +++ b/tools/simulator_manager/Sources/LRUSet.swift @@ -0,0 +1,49 @@ +/// A `Set` that has a maximum capacity and evicts the least recently used item +// when full. +struct LRUSet { + private let capacity: Int + // An array to keep track of the order in which elements were inserted. + // The first element in the array is the least recently used. + private var order: [Element] = [] + + // A Set to enable fast O(1) membership tests. + private var storage: Set = [] + + init(capacity: Int) { + precondition(capacity > 0, "Capacity must be greater than zero.") + self.capacity = capacity + } + + // Returns an element that was evicted from the set. + mutating func insert(_ element: Element) -> Element? { + let evicted: Element? + if storage.contains(element) { + if let index = order.firstIndex(of: element) { + evicted = order.remove(at: index) + } else { + evicted = nil + } + + order.append(element) + } else { + if order.count >= capacity, let oldest = order.first { + order.removeFirst() + evicted = storage.remove(oldest) + } else { + evicted = nil + } + + order.append(element) + storage.insert(element) + } + return evicted + } + + func contains(_ element: Element) -> Bool { + return storage.contains(element) + } + + var elements: [Element] { + return order + } +} diff --git a/tools/simulator_manager/Sources/Logger.swift b/tools/simulator_manager/Sources/Logger.swift new file mode 100644 index 0000000..39507ba --- /dev/null +++ b/tools/simulator_manager/Sources/Logger.swift @@ -0,0 +1,10 @@ +import os.log + +extension Logger { + static func simulatorManager(category: String) -> Logger { + Logger( + subsystem: "com.example.tools.simulator_manager", + category: category + ) + } +} diff --git a/tools/simulator_manager/Sources/Main.swift b/tools/simulator_manager/Sources/Main.swift new file mode 100644 index 0000000..c9c12d5 --- /dev/null +++ b/tools/simulator_manager/Sources/Main.swift @@ -0,0 +1,79 @@ +import ArgumentParser + +@main +struct Main: AsyncParsableCommand { + // This is set externally to prevent having to recompile the manager just for `start.sh` changes + @Option(help: "Version of the simulator manager") + var version: String + + @Option(help: "Path to where the pid should be written") + var pidPath: String + + @Option(help: "Path to where the unix domain socket should be created") + var unixSocketPath: String + + @Option(help: "Number of seconds to wait before deleting a recently used idle simulator") + var deleteRecentlyUsedIdleAfter: UInt16 + + @Option(help: "Number of seconds to wait before deleting a non-recently used idle simulator") + var deleteIdleAfter: UInt16 + + @Option( + help: """ + The number of simulators to keep in the recently used list; affects wether \ + 'delete-recently-used-idle-after' or 'delete-idle-after' is used when determining when to \ + delete an unused simulator + """ + ) + var recentlyUsedCapacity = 1 + + @Option( + name: .customLong("startup-process"), + help: """ + The path to a startup process that will be run when the simulator manager is started. This \ + process will not be relaunched if it exits. + + To pass custom arguments to the process you should wrap it in a script. + + Setting this flag multiple times will result in multiple startup process being launched. + """ + ) + var startupProcesses: [String] = [] + + @Option(help: "Path to an executable that will run after a simulator clone is booted") + var postBoot: String + + func validate() throws { + guard recentlyUsedCapacity > 0 else { + throw ValidationError( + """ + 'recently-used-capacity' must be greater than 0. + """ + ) + } + + guard Set(startupProcesses).count == startupProcesses.count else { + throw ValidationError("'startup-process' must be unique.") + } + } + + func run() async throws { + let simulatorManager = SimulatorManager( + simulatorControl: RealSimulatorControl(), + deleteRecentlyUsedIdleAfter: deleteRecentlyUsedIdleAfter, + deleteIdleAfter: deleteIdleAfter, + recentlyUsedCapacity: recentlyUsedCapacity, + deleteOnPIDExit: true, + startupProcesses: startupProcesses, + postBoot: postBoot + ) + try await simulatorManager.startChildProcesses() + + try await HTTPServer( + simulatorRequestHandler: SimulatorRequestHandler( + simulatorManager: simulatorManager + ), + version: version + ).run(pidPath: pidPath, unixSocketPath: unixSocketPath) + } +} diff --git a/tools/simulator_manager/Sources/PTY.swift b/tools/simulator_manager/Sources/PTY.swift new file mode 100644 index 0000000..6f11ed4 --- /dev/null +++ b/tools/simulator_manager/Sources/PTY.swift @@ -0,0 +1,38 @@ +import Darwin + +struct PTY { + let parent: Int32 + let child: Int32 + + init() throws { + var parentFd: Int32 = 0 + var childFd: Int32 = 0 + + // NULL for name/pw/termios/winsize = defaults + let result = openpty(&parentFd, &childFd, nil, nil, nil) + guard result == 0 else { + throw Errno(rawValue: errno) + } + + self.parent = parentFd + self.child = childFd + } +} + +/// Simple POSIX errno wrapper. +struct Errno: Error, RawRepresentable { + /// The raw POSIX error number. + let rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } +} + +extension Errno: CustomStringConvertible { + var description: String { + var buf = [CChar](repeating: 0, count: 256) + strerror_r(rawValue, &buf, buf.count) + return String(cString: buf) + } +} diff --git a/tools/simulator_manager/Sources/SimulatorControl.swift b/tools/simulator_manager/Sources/SimulatorControl.swift new file mode 100644 index 0000000..996fbad --- /dev/null +++ b/tools/simulator_manager/Sources/SimulatorControl.swift @@ -0,0 +1,628 @@ +import Foundation +import os.log +import ShellOut + +typealias SimulatorUDID = String + +extension Logger { + static let simulatorControl = simulatorManager(category: "control") +} + +struct SimulatorConfig: Hashable, Equatable { + let deviceType: String + let os: String + let version: String +} + +extension SimulatorConfig: CustomStringConvertible { + var description: String { + return "\(deviceType) (\(os) \(version))" + } +} + +struct SimCtlDevices: Decodable { + let devices: [String: [SimCtlDevice]] +} + +struct SimCtlDevice: Decodable { + let name: String + let udid: String +} + +struct ProcessError: Error { + let command: String + let context: String? + let exitCode: Int32 + let stdOut: String + let stdErr: String +} + +extension ProcessError: CustomStringConvertible { + var description: String { + let contextStr: String + if let context { + contextStr = " (\(context))" + } else { + contextStr = "" + } + + return """ + "\(command)"\(contextStr) failed with exit code \(exitCode): + \(stdOut)\(stdErr) + """ + } +} + +extension ProcessError: LocalizedError { + var errorDescription: String? { + return description + } +} + +protocol SimulatorControl: Actor { + // Creates a base simulator with the given config. + // + // It also boots and shuts down the simulator, making it ready for cloning. + // + // If an existing simulator with the same name already exists, that is returned instead of + // creating a new one. This is to support the simulator manager being restarted and losing state. + func createBase( + name: String, + with config: SimulatorConfig, + runtimeIdentifier: String + ) async throws -> SimulatorUDID + + // Clones a base simulator. + // + // It also boots the cloned simulator, making it ready for use. + // + // If an existing simulator with the same name already exists, that is returned instead of + // creating a new one. This is to support the simulator manager being restarted and losing state. + func clone( + _ baseSimulator: SimulatorUDID, + name: String, + deviceType: String, + runtimeIdentifier: String, + postBoot: String? + ) async throws -> SimulatorUDID + + func ensureBooted( + _ simulator: SimulatorUDID, + context: @escaping @autoclosure () -> String? + ) async throws + + func cleanTempFiles(in simulator: SimulatorUDID) + + func delete( + _ simulator: SimulatorUDID, + name: String, + context: @escaping @autoclosure () -> String? + ) async throws + + func getExisting( + name: String, + deviceType: String, + runtimeIdentifier: String, + context: @escaping @autoclosure () -> String? + ) async throws -> String? +} + +actor RealSimulatorControl: SimulatorControl { + private var createBaseTasks: [String: Task] = [:] + private var cloneTasks: [String: Task] = [:] + + private var deleteAndExistenceMutexes: [String: (SimulatorDeleteOrExistenceMutex, Int)] = [:] + + func createBase( + name: String, + with config: SimulatorConfig, + runtimeIdentifier: String + ) async throws -> SimulatorUDID { + if let existingTask = createBaseTasks[name] { + return try await existingTask.value + } + + // We use a task to prevent data races that can occur when the `await` on `simctl` blocks. This + // ensures that multiple callers trying create a base simulator will all wait for the same + // simulator to be returned. + let task = Task { + defer { + createBaseTasks.removeValue(forKey: name) + } + + if let existingUDID = try await getExisting( + name: name, + deviceType: config.deviceType, + runtimeIdentifier: runtimeIdentifier, + context: "createBase" + ) { + Logger.simulatorControl.info( + """ + 📱 Base simulator "\(name, privacy: .public)" already exists, skipping creation: \ + \(existingUDID, privacy: .public) + """ + ) + + do { + // Under weird circumstances, the base simulator might be booted. This could happen if + // the simulator manager is killed in the process of creating a new base. Always call + // shutdown just in case. + try await shutdown(existingUDID, context: "createBase existing: \(name)") + } catch { + // If we fail to do what we need to, then we need to delete the faulty base simulator + Logger.simulatorControl.error( + """ + 📱 Failed to set up base simulator "\(name)" \(existingUDID, privacy: .public); deleting + """ + ) + + // If we fail to delete, don't throw _that_ error, throw the original error + try? await delete(existingUDID, name: name, context: "createBase existing: \(name)") + + throw error + } + + return existingUDID + } + + Logger.simulatorControl.info( + #"📱 Creating \#(config, privacy: .public) base simulator "\#(name, privacy: .public)""# + ) + + let udid = try await simctl( + ["create", name, config.deviceType, runtimeIdentifier] + ).trimmingCharacters(in: .whitespacesAndNewlines) + + do { + try await ensureBooted(udid, context: "createBase new: \(name)") + + // FIXME: Find a better way to know the simulator is ready + // Give the simulator some time to do some post-boot processing + try await Task.sleep(for: .seconds(5)) + + try await shutdown(udid, context: "createBase new: \(name)") + } catch { + // If we fail to do what we need to, then we need to delete the faulty base simulator + Logger.simulatorControl.error( + #""📱 Failed to set up base simulator "\#(name)" \#(udid, privacy: .public); deleting"# + ) + + // If we fail to delete, don't throw _that_ error, throw the original error + try? await delete(udid, name: name, context: "createBase new: \(name)") + + throw error + } + + Logger.simulatorControl.info( + """ + 📱 Created \(config, privacy: .public) base simulator \ + "\(name, privacy: .public)": \(udid, privacy: .public) + """ + ) + + return udid + } + + createBaseTasks[name] = task + + return try await task.value + } + + func clone( + _ baseSimulator: SimulatorUDID, + name: String, + deviceType: String, + runtimeIdentifier: String, + postBoot: String? = nil + ) async throws -> SimulatorUDID { + if let existingTask = cloneTasks[name] { + return try await existingTask.value + } + + // We use a task to prevent data races that can occur when the `await` on `simctl` blocks. This + // ensures that multiple callers trying create a base simulator will all wait for the same + // simulator to be returned. + let task = Task { + defer { + cloneTasks.removeValue(forKey: name) + } + + let udid: String + let isExisting: Bool + if let existingUDID = try await getExisting( + name: name, + deviceType: deviceType, + runtimeIdentifier: runtimeIdentifier, + context: "clone" + ) { + udid = existingUDID + isExisting = true + + // An existing simulator can be found if a previous simulator manager was killed before the + // clone was deleted. No tests _should_ be actively leasing the simulator. + Logger.simulatorControl.info( + """ + 📱 Cloned simulator "\(name, privacy: .public)" already exists, skipping creation: \ + \(udid, privacy: .public) + """ + ) + + // Wait for it to boot. This shouldn't be necessary, but sometimes the simulator will + // reboot because of a migration. + try await ensureBooted(udid, context: "clone, existing: \(name)") + } else { + isExisting = false + + Logger.simulatorControl.info( + """ + 📱 Cloning base simulator \(baseSimulator, privacy: .public) as \ + "\(name, privacy: .public)" + """ + ) + + udid = try await simctl( + ["clone", baseSimulator, name] + ).trimmingCharacters(in: .whitespacesAndNewlines) + + Logger.simulatorControl.info( + """ + 📱 Cloned base simulator \(baseSimulator, privacy: .public) as \ + "\(name, privacy: .public)": \(udid, privacy: .public) + """ + ) + + try await ensureBooted(udid, context: "clone, new: \(name)") + } + + if let postBoot { + Logger.simulatorControl.info( + """ + 📱 Running post-boot script "\(postBoot, privacy: .public)" on \ + \(udid, privacy: .public) + """ + ) + + do { + _ = try await subprocess(postBoot, env: ["SIMULATOR_UDID": udid]) + } catch { + throw NSError( + domain: "SimulatorControl", + code: 1, + userInfo: + [NSLocalizedDescriptionKey: "postBoot failed (isExisting: \(isExisting)): \(error)"] + ) + } + } + + return udid + } + + cloneTasks[name] = task + + return try await task.value + } + + func shutdown(_ simulator: SimulatorUDID, context: @escaping @autoclosure () -> String?) async throws { + do { + _ = try await simctl(["shutdown", simulator], context: context()) + } catch let error as ProcessError { + // Exit code 149 is related to the simulator already being shut down + guard error.exitCode == 149 else { + throw error + } + + Logger.simulatorControl.warning( + """ + ⚠️ Shutdown failed, but probably \"already shut down\": \(error, privacy: .public) + """ + ) + } + } + + func cleanTempFiles(in simulator: SimulatorUDID) { + let fileManager = FileManager.default + + // Remove all files and directories under + // `data/Library/Caches/com.apple.containermanagerd/Dead/`, ignoring errors. There seems to be + // a bug where the simulator moves files here but never cleans them up. Maybe it's waiting for + // a reboot or something, which we never do. + let deadCachesPath = + "\(NSHomeDirectory())/Library/Developer/CoreSimulator/Devices/\(simulator)/data/Library/Caches/com.apple.containermanagerd/Dead" + guard let contents = try? fileManager.contentsOfDirectory(atPath: deadCachesPath) else { + return + } + for item in contents { + let itemPath = "\(deadCachesPath)/\(item)" + try? fileManager.removeItem(atPath: itemPath) + } + } + + func delete( + _ simulator: SimulatorUDID, + name: String, + context: @escaping @autoclosure () -> String? + ) async throws { + try await deleteAndExistenceMutex(name: name) { mutex in + try await mutex.unlockedDelete(simulator, context: context()) + } + } + + func getExisting( + name: String, + deviceType: String, + runtimeIdentifier: String, + context: @escaping @autoclosure () -> String? + ) async throws -> String? { + return try await deleteAndExistenceMutex(name: name) { mutex in + return try await mutex.unlockedGetExisting( + name: name, + deviceType: deviceType, + runtimeIdentifier: runtimeIdentifier, + context: context() + ) + } + } + + func ensureBooted(_ simulator: SimulatorUDID, context: @escaping @autoclosure () -> String?) async throws { + for retriesLeft in (0...1).reversed() { + do { + // This private command boots the simulator if it isn't already, and waits for the + // appropriate amount of time until we can actually run tests + _ = try await simctl(["bootstatus", simulator, "-b"], context: context()) + break + } catch let error as ProcessError { + // Exit code 149 is related to the simulator already being booted + guard error.exitCode == 149 && retriesLeft > 0 else { + throw error + } + + // This is a known error that happens when the simulator is already booted. A retry + // should succeed. + Logger.simulatorControl.warning( + """ + ⚠️ Boot of simulator \(simulator, privacy: .public) failed, but probably \"already \ + booted\": \(error, privacy: .public) + """ + ) + } + } + } + + private func deleteAndExistenceMutex( + name: String, + _ call: (_ mutex: SimulatorDeleteOrExistenceMutex) async throws -> T + ) async throws -> T { + var (mutex, referenceCount) = + deleteAndExistenceMutexes[name] ?? (SimulatorDeleteOrExistenceMutex(), 0) + referenceCount += 1 + deleteAndExistenceMutexes[name] = (mutex, referenceCount) + + defer { + guard let mutexAndRef = deleteAndExistenceMutexes[name] else { + preconditionFailure( + """ + State of `deleteAndExistenceMutexes` changed unexpectedly. Expected value for "\(name)". + """ + ) + } + let mutex = mutexAndRef.0 + var referenceCount = mutexAndRef.1 + referenceCount -= 1 + + if referenceCount == 0 { + deleteAndExistenceMutexes.removeValue(forKey: name) + } else { + deleteAndExistenceMutexes[name] = (mutex, referenceCount) + } + } + + return try await mutex.withLock { + try await call(mutex) + } + } +} + +// An instance of this actor is created for each simulator name that is being checked for existence +// or being deleted. The actor is only called through `withLock()`, which will suspend on multiple +// calls to ensure that these operations are serialized. Without this, someone could try to call +// `clone()` while a deletion is pending, which will call `getExisting()`, and it can return the +// simulator that is in the process of being deleted. +actor SimulatorDeleteOrExistenceMutex { + private var isLocked = false + private var waiters: [CheckedContinuation] = [] + + /// Acquires, runs the work, and then releases the lock. + func withLock(_ work: () async throws -> T) async throws -> T { + await lock() + defer { unlock() } + return try await work() + } + + /// Acquires the lock. If already locked, will suspend until unlocked. + private func lock() async { + if !isLocked { + isLocked = true + } else { + await withCheckedContinuation { cont in + waiters.append(cont) + } + } + } + + /// Releases the lock and wakes one waiter (if any). + private func unlock() { + if !waiters.isEmpty { + let cont = waiters.removeFirst() + cont.resume() + } else { + isLocked = false + } + } + + func unlockedGetExisting( + name: String, + deviceType: String, + runtimeIdentifier: String, + context: @escaping @autoclosure () -> String? + ) async throws -> String? { + Logger.simulatorControl.debug( + #"🔍 Trying to find existing simulator "\#(name, privacy: .public)""# + ) + + let output = try await simctl(["list", "devices", "-j", deviceType], context: context()) + + guard let jsonData = output.data(using: .utf8) else { + throw NSError( + domain: "SimulatorControl", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Failed to convert output to data"] + ) + } + + let jsonDecoder = JSONDecoder() + + let devicesByRuntime: [String: [SimCtlDevice]] + do { + devicesByRuntime = try jsonDecoder.decode(SimCtlDevices.self, from: jsonData).devices + } catch { + let json = String(data: jsonData, encoding: .utf8) ?? "" + Logger.simulatorControl.error( + """ + ❌ Failed to decode 'simctl list devices -j': \(error, privacy: .public). + Output: \(json, privacy: .public) + """ + ) + throw NSError( + domain: "SimulatorControl", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Failed to decode output: \(error) - \(json)"] + ) + } + + if let devices = devicesByRuntime[runtimeIdentifier] { + for device in devices { + if device.name == name { + let udid = device.udid + + Logger.simulatorControl.debug( + #"🔍 Found existing simulator "\#(name, privacy: .public)": \#(udid, privacy: .public)"# + ) + + // Sometimes the simulator is not actually on disk, but it is in the list. If this + // happens, "delete" it so simctl stops reporting it as existing. + if !FileManager.default.fileExists( + atPath: + "\(NSHomeDirectory())/Library/Developer/CoreSimulator/Devices/\(udid)" + ) { + Logger.simulatorControl.debug( + """ + ⚠️ Simulator \(udid, privacy: .public) doesn't actually exist on disk; "deleting" + """ + ) + + // If we fail to delete, don't throw an error + try? await unlockedDelete(udid, context: context()) + + return nil + } + + return udid + } + } + } + + Logger.simulatorControl.debug( + #"🔍 No existing simulator "\#(name, privacy: .public)" found"# + ) + + return nil + } + + func unlockedDelete( + _ simulator: SimulatorUDID, + context: @escaping @autoclosure () -> String? + ) async throws { + Logger.simulatorControl.info("🗑️ Deleting simulator \(simulator, privacy: .public)") + + do { + _ = try await simctl(["delete", simulator], context: context()) + } catch { + Logger.simulatorControl.error( + """ + ❌ Failed to delete simulator \(simulator, privacy: .public): \ + \(error, privacy: .public) + """ + ) + + throw error + } + + Logger.simulatorControl.info("🗑️ Deleted simulator \(simulator, privacy: .public)") + } +} + +private func simctl( + _ args: [String], + context: @escaping @autoclosure () -> String? = nil +) async throws -> String { + return try await subprocess("/usr/bin/xcrun", ["simctl"] + args, context: context()) +} + +private func subprocess( + _ executable: String, + _ args: [String] = [], + env: [String: String] = [:], + context: @escaping @autoclosure () -> String? = nil +) async throws -> String { + return try await Task { try syncSubprocess(executable, args, env: env, context: context()) }.value +} + +private func syncSubprocess( + _ executable: String, + _ args: [String] = [], + env: [String: String] = [:], + context: @escaping @autoclosure () -> String? = nil +) throws -> String { + let quotedArgs = args.map { "'\($0)'" } + + var newEnv = ProcessInfo.processInfo.environment.merging(env) { _, new in new } + newEnv["PWD"] = FileManager.default.currentDirectoryPath + + let process = Process() + process.environment = ProcessInfo.processInfo.environment.merging(env) { _, new in new } + + let command = "\(executable) \(quotedArgs.joined(separator: " "))" + + Logger.simulatorControl.debug(#"🛠️ Running "\#(command, privacy: .public)""#) + + do { + return try shellOut( + to: executable, + arguments: quotedArgs, + process: process + ) + } catch let error as ShellOutError { + throw ProcessError( + command: command, + context: context(), + exitCode: error.terminationStatus, + stdOut: error.output, + stdErr: error.message + ) + } +} + +extension SimulatorConfig { + func baseDeviceName() -> String { + return "EXAMPLE_BAZEL_BASE_\(deviceType)_\(version)" + } + + func cloneDeviceName(index: Int) -> String { + return "EXAMPLE_BAZEL_CLONE_\(deviceType)_\(version)_\(index)" + } + + func runtimeIdentifier() -> String { + let runtimeVersion = version.replacingOccurrences(of: ".", with: "-") + return "com.apple.CoreSimulator.SimRuntime.\(os)-\(runtimeVersion)" + } +} diff --git a/tools/simulator_manager/Sources/SimulatorManager.swift b/tools/simulator_manager/Sources/SimulatorManager.swift new file mode 100644 index 0000000..7236fbe --- /dev/null +++ b/tools/simulator_manager/Sources/SimulatorManager.swift @@ -0,0 +1,679 @@ +import Foundation +import os +import ShellOut + +typealias PID = pid_t + +extension Logger { + static let simulatorManager = simulatorManager(category: "manager") + static let childProcess = simulatorManager(category: "manager.child-process") +} + +enum SimulatorManagerError: Error { + case alreadyLeased(udid: SimulatorUDID) + case noLease +} + +private struct SimulatorLease { + let udid: SimulatorUDID + let config: SimulatorConfig + let exclusive: Bool + let slotIndex: Int +} + +private enum SimulatorSlot { + case empty + case pendingCreation(Task, exclusive: Bool) + case active(SimulatorUDID, exclusive: Bool) + case pendingDeletion(SimulatorUDID, Task) + case deleting(SimulatorUDID) +} + +extension SimulatorSlot { + var sortOrder: Int { + switch self { + // Try to use active or pending creation simulators first (should only be + // one of either for non-exclusive) + case .active: + return 0 + case .pendingCreation: + return 1 + + // Use a pending deletion before any empty slots + case .pendingDeletion: + return 2 + + // Finally use empty slots + case .empty: + return 3 + + // Deleting simulator can't be used, so put it at the end + case .deleting: + return 4 + } + } +} + +private enum SimulatorSlotResult { + case active(SimulatorUDID, slotIndex: Int) + case pending(Task, slotIndex: Int) +} + +actor SimulatorManager { + private let simulatorControl: SimulatorControl + + private var simulatorSlots: [SimulatorConfig: [SimulatorSlot]] = [:] + private var referenceCount: [SimulatorUDID: Int] = [:] + private var leases: [PID: SimulatorLease] = [:] + + private var leaserExitListeners: [PID: DispatchSourceProcess] = [:] + + private var getBaseSimulatorTasks: [SimulatorConfig: Task] = [:] + + private let deleteIdleAfter: UInt16 + private let deleteRecentlyUsedIdleAfter: UInt16 + private let deleteOnPIDExit: Bool + + private var recentlyLeased: LRUSet + + private var startupProcessPaths: [String] + private var postBoot: String? + private var childProcessTasks: [Task] = [] + private var childProcesses: [String: (Process, DispatchSourceRead, DispatchSourceRead)] = [:] + + init( + simulatorControl: SimulatorControl, + deleteRecentlyUsedIdleAfter: UInt16, + deleteIdleAfter: UInt16, + recentlyUsedCapacity: Int, + deleteOnPIDExit: Bool, + startupProcesses: [String] = [], + postBoot: String? = nil + ) { + self.simulatorControl = simulatorControl + self.deleteIdleAfter = deleteIdleAfter + self.deleteRecentlyUsedIdleAfter = deleteRecentlyUsedIdleAfter + self.deleteOnPIDExit = deleteOnPIDExit + self.recentlyLeased = LRUSet(capacity: recentlyUsedCapacity) + self.startupProcessPaths = startupProcesses + self.postBoot = postBoot + + // Change the working directory to some place stable, since on RBE the runfiles directory can + // get cleaned up + FileManager.default.changeCurrentDirectoryPath("/tmp") + } + + deinit { + for task in childProcessTasks { + task.cancel() + } + + for (process, outWatcher, errWatcher) in childProcesses.values { + process.terminate() + outWatcher.cancel() + errWatcher.cancel() + } + } + + func startChildProcesses() throws { + for path in startupProcessPaths { + childProcessTasks.append(createStartChildProcessTask(path: path)) + } + } + + func lease( + to leaser: PID, + exclusive: Bool, + config: SimulatorConfig + ) async throws -> SimulatorUDID { + // Each process can only lease one simulator at a time + if let existingLease = leases[leaser] { + throw SimulatorManagerError.alreadyLeased(udid: existingLease.udid) + } + + Logger.simulatorManager.info( + """ + 🔒 Leasing \(exclusive ? "exclusive" : "non-exclusive", privacy: .public) \ + \(config, privacy: .public) simulator for PID \(leaser, privacy: .public) + """ + ) + + // `getSimulator()` will increment the reference count for the simulator + let (simulator, slotIndex) = try await getSimulator(for: config, exclusive: exclusive) + + _ = recentlyLeased.insert(config) + + leases[leaser] = .init( + udid: simulator, + config: config, + exclusive: exclusive, + slotIndex: slotIndex + ) + + Logger.simulatorManager.info( + "🔒 Leased simulator \(simulator, privacy: .public) to PID \(leaser, privacy: .public)" + ) + + if deleteOnPIDExit { + registerReleaseOnExit(for: leaser) + } + + return simulator + } + + func release(for leaser: PID) async throws { + guard let lease = leases.removeValue(forKey: leaser) else { + // If the manager recently restarted, we might not have the state of all leases. Since + // `SimulatorControl` will return existing simulators matching a given name, the dangling + // simulator will eventually get picked back up again and properly reference counted. So we + // will return an error here, and ignore it in the test runner. + throw SimulatorManagerError.noLease + } + + Logger.simulatorManager.info( + "🔓 Releasing simulator \(lease.udid, privacy: .public) for PID \(leaser, privacy: .public)" + ) + + removeReleaseOnExit(for: leaser) + + await simulatorControl.cleanTempFiles(in: lease.udid) + + try await decrementReferenceCount( + for: lease.udid, + config: lease.config, + slotIndex: lease.slotIndex + ) + } + + private func getBase( + for config: SimulatorConfig + ) async throws -> SimulatorUDID { + if let existingTask = getBaseSimulatorTasks[config] { + return try await existingTask.value + } + + // We use a task to prevent data races that can occur when the `await` on `simulatorControl` + // blocks. This ensures that multiple callers trying to get a base simulator will all wait + // for the same simulator to be returned. + let task = Task { + defer { + getBaseSimulatorTasks.removeValue(forKey: config) + } + + Logger.simulatorManager.info("📱 Creating \(config, privacy: .public) base simulator") + + let baseSimulator = + try await simulatorControl + .createBase( + name: config.baseDeviceName(), + with: config, + runtimeIdentifier: config.runtimeIdentifier() + ) + + Logger.simulatorManager.info( + "📱 Created \(config, privacy: .public) base simulator: \(baseSimulator, privacy: .public)" + ) + + return baseSimulator + } + + getBaseSimulatorTasks[config] = task + + return try await task.value + } + + private func incrementReferenceCount(for simulator: SimulatorUDID) { + var count = referenceCount[simulator] ?? 0 + count += 1 + referenceCount[simulator] = count + + Logger.simulatorManager.debug( + """ + 🔼 Reference count for simulator \(simulator, privacy: .public) is now \ + \(count, privacy: .public) + """ + ) + } + + private func decrementReferenceCount( + for simulator: SimulatorUDID, + config: SimulatorConfig, + slotIndex: Int + ) async throws { + guard var count = referenceCount[simulator] else { + // Simulator was already deleted, nothing to do + return + } + + count -= 1 + referenceCount[simulator] = count + + Logger.simulatorManager.debug( + "🔽 Reference count for \(simulator, privacy: .public) is now \(count, privacy: .public)" + ) + + guard count == 0 else { + return + } + + // Wait a bit before deleting simulators, to allow them to be reused + await pendingDeletion(simulator, config: config, slotIndex: slotIndex) + } + + // Warning: We must update slots before we `await` on anything in this function (unless that + // method updates slots before `await`ing on anything). + private func getSimulator( + for config: SimulatorConfig, + exclusive: Bool + ) async throws -> (simulator: SimulatorUDID, slotIndex: Int) { + if simulatorSlots.keys.contains(config) == false { + simulatorSlots[config] = [] + } + + // Need to sort so we reuse the the correct slots + let sortedSlots = simulatorSlots[config]!.enumerated().sorted { lhs, rhs in + let lhsSortOrder = lhs.element.sortOrder + let rhsSortOrder = rhs.element.sortOrder + + guard lhsSortOrder == rhsSortOrder else { + // Sort by sort order first + return lhsSortOrder < rhsSortOrder + } + + // If the sort order is the same, sort by index + return lhs.offset < rhs.offset + } + + for (index, slot) in sortedSlots { + switch slot { + case .active(let simulator, false) where exclusive != true: + // We have an active non-exclusive simulator, so reuse it + return try await ( + reuseSimulator(simulator, config: config, exclusive: exclusive, slotIndex: index), + slotIndex: index + ) + + case .pendingDeletion(let simulator, let task): + // We have a pending deletion, so we can reuse it + Logger.simulatorManager.info( + """ + ♻️ Turning a pending deletion of simulator \(simulator, privacy: .public) into an \ + active \(exclusive ? "exclusive" : "non-exclusive", privacy: .public) simulator + """ + ) + + simulatorSlots[config]![index] = .active(simulator, exclusive: exclusive) + + task.cancel() + + return try await ( + reuseSimulator(simulator, config: config, exclusive: exclusive, slotIndex: index), + slotIndex: index + ) + + case .empty: + let task = createCloneTask(config: config, exclusive: exclusive, slotIndex: index) + simulatorSlots[config]![index] = .pendingCreation(task, exclusive: exclusive) + return try await (task.value, slotIndex: index) + + case .pendingCreation(let task, false) where exclusive == false: + // We have a non-exclusive simulator pending creation, so reuse it + let simulator = try await task.value + + // We call `incrementReferenceCount()` instead of `reuseSimulator()` here, because the + // simulator is freshly created, so we can (hopefully) assume it is in a good state + incrementReferenceCount(for: simulator) + + return (simulator, slotIndex: index) + + default: + // Ignore incompatible slots + break + } + } + + // If we got here, we need to add a new slot + let index = simulatorSlots[config]!.count + let task = createCloneTask(config: config, exclusive: exclusive, slotIndex: index) + simulatorSlots[config]!.append(.pendingCreation(task, exclusive: exclusive)) + return try await (task.value, slotIndex: index) + } + + private func reuseSimulator( + _ simulator: SimulatorUDID, + config: SimulatorConfig, + exclusive: Bool, + slotIndex: Int + ) async throws -> SimulatorUDID { + incrementReferenceCount(for: simulator) + + do { + // Wait for it to boot. This shouldn't be necessary, but sometimes the simulator will + // reboot because of a migration. This also guards against a simulator being deleted out + // from under us, as it will error, and we can then "delete" it and return a new one. + try await simulatorControl.ensureBooted( + simulator, + context: "getSimulator, reused: \(config.cloneDeviceName(index: slotIndex))" + ) + + return simulator + } catch let error as ProcessError { + // 148 happens for "Invalid device". So it either has already been deleted or it's corrupt + // in some way. Either way, we will "delete" it and return a new one. + guard error.exitCode == 148 else { + throw error + } + + Logger.simulatorManager.warning( + """ + ⚠️ Boot of existing simulator \(simulator, privacy: .public) failed; deleting and \ + returning a new simulator: \(error, privacy: .public) + """ + ) + + // If we fail to delete, don't throw an error + try? await delete( + simulator, + config: config, + slotIndex: slotIndex, + // We can't clean up slots, since we assign to it below + cleanUpSlots: false, + context: "getSimulator, reused: \(config.cloneDeviceName(index: slotIndex))" + ) + + let task = createCloneTask(config: config, exclusive: exclusive, slotIndex: slotIndex) + simulatorSlots[config]![slotIndex] = .pendingCreation(task, exclusive: exclusive) + return try await task.value + } + } + + private func createCloneTask( + config: SimulatorConfig, + exclusive: Bool, + slotIndex: Int + ) -> Task { + return Task { + do { + let simulator = try await simulatorControl.clone( + getBase(for: config), + name: config.cloneDeviceName(index: slotIndex), + deviceType: config.deviceType, + runtimeIdentifier: config.runtimeIdentifier(), + postBoot: postBoot + ) + + simulatorSlots[config]![slotIndex] = .active(simulator, exclusive: exclusive) + + // We want to increment the reference count as soon as we get back from `await`, to ensure + // that when we suspend and potentially decrement the reference count, we don't delete the + // simulator before we have a chance to use it. Also, since we created the simulator, we + // should be responsible for incrementing the reference count. Any functions that reuse + // this task need to increment the reference count as well. + incrementReferenceCount(for: simulator) + + return simulator + } catch { + // If we fail to create the clone, we need to empty the slot, instead + // of leaving it in a pending state + simulatorSlots[config]![slotIndex] = .empty + + throw error + } + } + } + + private func registerReleaseOnExit(for leaser: PID) { + let processSource = + DispatchSource.makeProcessSource(identifier: leaser, eventMask: .exit, queue: .main) + + var handledExit = false + let onExitHandler: () -> Void = { [weak self] in + // Avoid double handling of exit in case the process exits between + // `processSource.resume()` and the check with `kill` + guard !handledExit else { return } + handledExit = true + + Task { + guard let self else { return } + + Logger.simulatorManager.debug("👋 PID \(leaser, privacy: .public) exited") + + try await self.release(for: leaser) + } + } + + processSource.setEventHandler { onExitHandler() } + processSource.resume() + + // Check to see if the process is already dead and cancel the source if it is, which will + // trigger `setCancelHandler`, which releases the simulator + guard kill(leaser, 0) == 0 else { + processSource.cancel() + onExitHandler() + return + } + + leaserExitListeners[leaser] = processSource + } + + private func removeReleaseOnExit(for leaser: PID) { + guard let leaserExitListener = leaserExitListeners[leaser] else { return } + leaserExitListeners.removeValue(forKey: leaser) + leaserExitListener.cancel() + } + + private func pendingDeletion( + _ simulator: SimulatorUDID, + config: SimulatorConfig, + slotIndex: Int + ) async { + guard deleteIdleAfter > 0 || deleteRecentlyUsedIdleAfter > 0 else { + // If we fail to delete, don't throw an error + try? await delete( + simulator, + config: config, + slotIndex: slotIndex, + cleanUpSlots: true, + context: "pendingDeletion immediate" + ) + return + } + + let task = Task { + Logger.simulatorManager.info( + """ + 💤 Scheduling delete of simulator \(simulator, privacy: .public) in \ + \(self.deleteIdleAfter, privacy: .public) to \ + \(self.deleteRecentlyUsedIdleAfter, privacy: .public) seconds + """ + ) + + let now = Date() + let shortDeadline = now.addingTimeInterval(TimeInterval(deleteIdleAfter)) + let recentlyUsedDeadline = now.addingTimeInterval(TimeInterval(deleteRecentlyUsedIdleAfter)) + + while true { + let remainingTime: TimeInterval + if recentlyLeased.contains(config) { + remainingTime = recentlyUsedDeadline.timeIntervalSinceNow + } else { + remainingTime = shortDeadline.timeIntervalSinceNow + } + + if remainingTime <= 0 { + break + } + + // Sleep for up-to 1 second before next check + try await Task.sleep(for: .seconds(min(remainingTime, 1))) + } + + guard case .pendingDeletion(let slotSimulator, _) = simulatorSlots[config]![slotIndex], + simulator == slotSimulator else { + // Simulator was reused, no need to delete + return + } + + // If we fail to delete, don't throw an error + try? await delete( + simulator, + config: config, + slotIndex: slotIndex, + cleanUpSlots: true, + context: "pendingDeletion delayed" + ) + } + + simulatorSlots[config]![slotIndex] = .pendingDeletion(simulator, task) + } + + private func delete( + _ simulator: SimulatorUDID, + config: SimulatorConfig, + slotIndex: Int, + cleanUpSlots: Bool, + context: @escaping @autoclosure () -> String? + ) async throws { + let name = config.cloneDeviceName(index: slotIndex) + + Logger.simulatorManager.info( + "🗑️ Deleting simulator \(simulator, privacy: .public) (\(name, privacy: .public))" + ) + + simulatorSlots[config]![slotIndex] = .deleting(simulator) + + referenceCount.removeValue(forKey: simulator) + + defer { + // Even if we fail to delete, we need to set the slot to empty + simulatorSlots[config]![slotIndex] = .empty + + if cleanUpSlots { + // Shorten up the array by removing any empty slots at the end + while case .empty = simulatorSlots[config]!.last { + simulatorSlots[config]!.removeLast() + } + } + } + + try await simulatorControl.delete(simulator, name: name, context: context()) + + Logger.simulatorManager.info( + "🗑️ Deleted simulator \(simulator, privacy: .public) (\(name, privacy: .public)" + ) + } + + // MARK: Child Process Management + + private nonisolated func createStartChildProcessTask(path: String) -> Task { + return Task.detached { [weak self] in + let process: Process + do { + guard let self else { return } + process = try await self.createProcess(path: path) + // `self` drops out of scope here, so `SimulatorManager` can deinit + } catch { + Logger.simulatorManager.info( + """ + ❌ Failed to create child process at "\(path, privacy: .public)": \ + \(error, privacy: .public) + """ + ) + return + } + + await withCheckedContinuation { cont in + process.terminationHandler = { proc in + let exitCode = proc.terminationStatus + Logger.simulatorManager.warning( + """ + ⚠️ "\(path, privacy: .public)" exited with code: \(exitCode, privacy: .public) + """ + ) + cont.resume() + } + + do { + Logger.simulatorManager.info( + #"🧒 Starting "\#(path, privacy: .public)""# + ) + try process.run() + } catch { + Logger.simulatorManager.info( + """ + ❌ Failed to start "\(path, privacy: .public)": \ + \(error, privacy: .public) + """ + ) + cont.resume() + } + } + } + } + + private func createProcess(path: String) throws -> Process { + let process = Process() + + process.executableURL = URL(fileURLWithPath: path) + + let outPTY = try PTY() + process.standardOutput = FileHandle(fileDescriptor: outPTY.child, closeOnDealloc: true) + let outQueue = DispatchQueue(label: "com.example.simulator_manager.child_process.out") + let outWatcher = watch(fd: outPTY.parent, queue: outQueue) { line in + Logger.childProcess.info("[\(path, privacy: .public)] \(line, privacy: .public)") + } + + let errPTY = try PTY() + process.standardError = FileHandle(fileDescriptor: errPTY.child, closeOnDealloc: true) + let errQueue = DispatchQueue(label: "com.example.simulator_manager.child_process.err") + let errWatcher = watch(fd: errPTY.parent, queue: errQueue) { line in + Logger.childProcess.error("[\(path, privacy: .public)] \(line, privacy: .public)") + } + + childProcesses[path] = (process, outWatcher, errWatcher) + + return process + } +} + +/// Installs a DispatchSourceRead on `fd`. +private func watch( + fd: Int32, + queue: DispatchQueue, + onLine: @escaping (String) -> Void +) -> DispatchSourceRead { + let src = DispatchSource.makeReadSource(fileDescriptor: fd, queue: queue) + var buffer = Data() + + src.setEventHandler { + var tmp = [UInt8](repeating: 0, count: 4096) + let n = read(fd, &tmp, tmp.count) + guard n > 0 else { + src.cancel() + close(fd) + return + } + + buffer.append(contentsOf: tmp[0..?) { + let response = unwrapOutboundIn(data) + + write(context: context, response: response) + } + + private func write(context: ChannelHandlerContext, response: SimulatorManagerResponse) { + let message = response.message + "\n" + + context.write( + wrapOutboundOut( + .init( + head: .init( + version: .http1_1, + status: response.status, + headers: .defaultHeaders(for: message) + ), + body: context.channel.allocator.buffer(string: message) + ) + ), + promise: nil + ) + } +} + +extension HTTPHeaders { + static func defaultHeaders(for message: String) -> HTTPHeaders { + var headers = HTTPHeaders() + headers.add(name: "Content-Length", value: "\(message.utf8.count)") + headers.add(name: "Content-Type", value: "text/plain") + return headers + } +} diff --git a/tools/simulator_manager/Sources/SimulatorRequestHandler.swift b/tools/simulator_manager/Sources/SimulatorRequestHandler.swift new file mode 100644 index 0000000..48ecd5d --- /dev/null +++ b/tools/simulator_manager/Sources/SimulatorRequestHandler.swift @@ -0,0 +1,92 @@ +import NIOHTTP1 + +final class SimulatorRequestHandler { + private let simulatorManager: SimulatorManager + + init(simulatorManager: SimulatorManager) { + self.simulatorManager = simulatorManager + } + + func handleRequest( + method: HTTPMethod, + pathComponents: [String], + queryParameters: [String: String] + ) async throws -> SimulatorManagerResponse { + switch method { + case .POST: + guard pathComponents.count >= 1 else { + return .init( + status: .badRequest, + message: "Must specify " + ) + } + + guard let leaser = PID(pathComponents[0]) else { + return .init(status: .badRequest, message: "Leaser PID must be an integer") + } + guard let exclusiveString = queryParameters["exclusive"] else { + return .init(status: .badRequest, message: "Must specify 'exclusive' query parameter") + } + let exclusive = exclusiveString == "1" + guard let deviceType = queryParameters["deviceType"] else { + return .init(status: .badRequest, message: "Must specify 'deviceType' query parameter") + } + guard let os = queryParameters["os"] else { + return .init(status: .badRequest, message: "Must specify 'os' query parameter") + } + guard let version = queryParameters["version"] else { + return .init(status: .badRequest, message: "Must specify 'version' query parameter") + } + + let config = SimulatorConfig( + deviceType: deviceType, + os: os, + version: version + ) + + do { + return try await .init( + status: .created, + message: simulatorManager + .lease(to: leaser, exclusive: exclusive, config: config) + ) + } catch SimulatorManagerError.alreadyLeased(let udid) { + return .init( + status: .badRequest, + // FIXME: Get this from the error itself + message: "PID \(leaser) has already leased another simulator: \(udid)" + ) + } + + case .DELETE: + guard pathComponents.count >= 1 else { + return .init( + status: .badRequest, + message: "Must specify " + ) + } + + guard let leaser = PID(pathComponents[0]) else { + return .init(status: .badRequest, message: "Leaser PID must be an integer") + } + + do { + try await simulatorManager.release(for: leaser) + + return .init( + status: .ok, + message: "Success" + ) + } catch SimulatorManagerError.noLease { + return .init( + status: .notFound, + // FIXME: Get this message from the error itself + message: "PID \(leaser) doesn't have a simulator leased" + ) + } + + default: + return .init(status: .methodNotAllowed, message: "Unsupported HTTP method: \(method)") + } + } +} diff --git a/tools/simulator_manager/Tests/SimulatorManagerTests.swift b/tools/simulator_manager/Tests/SimulatorManagerTests.swift new file mode 100644 index 0000000..5117dc0 --- /dev/null +++ b/tools/simulator_manager/Tests/SimulatorManagerTests.swift @@ -0,0 +1,213 @@ +@testable import simulator_manager +import XCTest + +final class SimulatorManagerTests: XCTestCase { + func test_lease_release_lease() async throws { + let mockSimulatorControl = MockSimulatorControl() + let simulatorManager = SimulatorManager( + simulatorControl: mockSimulatorControl, + deleteRecentlyUsedIdleAfter: 0, + deleteIdleAfter: 0, + recentlyUsedCapacity: 1, + deleteOnPIDExit: false + ) + let leaser: PID = 1234 + let config = SimulatorConfig(deviceType: "iPhone 14", os: "iOS", version: "16.4") + + let simulator1 = + try await simulatorManager + .lease(to: leaser, exclusive: false, config: config) + let maybeBaseSimulators1 = await mockSimulatorControl.baseSimulators[config] + let baseSimulators1 = try XCTUnwrap(maybeBaseSimulators1) + XCTAssert(baseSimulators1.count == 1) + let baseSimulator1 = baseSimulators1[0] + let maybeBaseForClone = await mockSimulatorControl.baseForClones[simulator1] + let baseForClone = try XCTUnwrap(maybeBaseForClone) + + try await simulatorManager.release(for: leaser) + let maybeBaseSimulators2 = await mockSimulatorControl.baseSimulators[config] + let baseSimulators2 = try XCTUnwrap(maybeBaseSimulators2) + XCTAssert(baseSimulators2.count == 1) + let baseSimulator2 = baseSimulators2[0] + + let simulator2 = + try await simulatorManager + .lease(to: leaser, exclusive: false, config: config) + let maybeBaseSimulators3 = await mockSimulatorControl.baseSimulators[config] + let baseSimulators3 = try XCTUnwrap(maybeBaseSimulators3) + XCTAssert(baseSimulators3.count == 2) + let baseSimulator3 = baseSimulators3[1] + + // Simulator is a clone (not the same as base) + XCTAssertNotEqual(simulator1, baseSimulator1) + XCTAssertEqual(baseForClone, baseSimulator1) + + // Even with reuse, after last use of a simulator it's deleted + XCTAssertNotEqual(simulator1, simulator2) + + XCTAssertEqual(baseSimulator1, baseSimulator2) + + // We don't cache our clones + XCTAssertNotEqual(baseSimulator2, baseSimulator3) + } + + func test_lease_reuse() async throws { + let mockSimulatorControl = MockSimulatorControl() + let simulatorManager = SimulatorManager( + simulatorControl: mockSimulatorControl, + deleteRecentlyUsedIdleAfter: 0, + deleteIdleAfter: 0, + recentlyUsedCapacity: 1, + deleteOnPIDExit: false + ) + let leaser1: PID = 1234 + let leaser2: PID = 1235 + let config = SimulatorConfig(deviceType: "iPhone 14", os: "iOS", version: "16.4") + + let simulator1 = + try await simulatorManager + .lease(to: leaser1, exclusive: false, config: config) + let simulator2 = + try await simulatorManager + .lease(to: leaser2, exclusive: false, config: config) + + XCTAssertEqual(simulator1, simulator2) + } + + func test_lease_exclusive() async throws { + let mockSimulatorControl = MockSimulatorControl() + let simulatorManager = SimulatorManager( + simulatorControl: mockSimulatorControl, + deleteRecentlyUsedIdleAfter: 0, + deleteIdleAfter: 0, + recentlyUsedCapacity: 1, + deleteOnPIDExit: false + ) + let leaser1: PID = 1234 + let leaser2: PID = 1235 + let config = SimulatorConfig(deviceType: "iPhone 14", os: "iOS", version: "16.4") + + let simulator1 = + try await simulatorManager + .lease(to: leaser1, exclusive: true, config: config) + let simulator2 = + try await simulatorManager + .lease(to: leaser2, exclusive: true, config: config) + + XCTAssertNotEqual(simulator1, simulator2) + } + + func test_lease_twice() async throws { + let mockSimulatorControl = MockSimulatorControl() + let simulatorManager = SimulatorManager( + simulatorControl: mockSimulatorControl, + deleteRecentlyUsedIdleAfter: 0, + deleteIdleAfter: 0, + recentlyUsedCapacity: 1, + deleteOnPIDExit: false + ) + let leaser: PID = 1234 + let config = SimulatorConfig(deviceType: "iPhone 14", os: "iOS", version: "16.4") + + _ = + try await simulatorManager + .lease(to: leaser, exclusive: true, config: config) + + // swiftformat:disable:next hoistAwait + try await assertThrowsAsyncError( + await simulatorManager + .lease(to: leaser, exclusive: true, config: config) + ) + } + + func test_release_alone() async throws { + let mockSimulatorControl = MockSimulatorControl() + let simulatorManager = SimulatorManager( + simulatorControl: mockSimulatorControl, + deleteRecentlyUsedIdleAfter: 0, + deleteIdleAfter: 0, + recentlyUsedCapacity: 1, + deleteOnPIDExit: false + ) + let leaser: PID = 1234 + + // swiftformat:disable:next hoistAwait + try await assertThrowsAsyncError(await simulatorManager.release(for: leaser)) + } +} + +func assertThrowsAsyncError( + _ expression: @autoclosure () async throws -> some Any, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line, + _ errorHandler: (_ error: Error) -> Void = { _ in } +) async { + do { + _ = try await expression() + // expected error to be thrown, but it was not + let customMessage = message() + if customMessage.isEmpty { + XCTFail("Asynchronous call did not throw an error.", file: file, line: line) + } else { + XCTFail(customMessage, file: file, line: line) + } + } catch { + errorHandler(error) + } +} + +actor MockSimulatorControl: SimulatorControl { + var baseSimulators: [SimulatorConfig: [SimulatorUDID]] = [:] + var baseForClones: [SimulatorUDID: SimulatorUDID] = [:] + + func createBase( + name: String, + with config: SimulatorConfig, + runtimeIdentifier: String + ) async throws -> SimulatorUDID { + let simulator = UUID().uuidString + baseSimulators[config, default: []].append(simulator) + return simulator + } + + func clone( + _ simulator: SimulatorUDID, + name: String, + deviceType: String, + runtimeIdentifier: String, + postBoot: String? + ) async throws -> SimulatorUDID { + let clone = UUID().uuidString + baseForClones[clone] = simulator + return clone + } + + func ensureBooted( + _ simulator: SimulatorUDID, + context: @escaping @autoclosure () -> String? + ) async throws { + return + } + + func cleanTempFiles(in simulator: SimulatorUDID) { + return + } + + func delete( + _ simulator: SimulatorUDID, + name: String, + context: @escaping @autoclosure () -> String? + ) async throws { + return + } + + func getExisting( + name: String, + deviceType: String, + runtimeIdentifier: String, + context: @escaping @autoclosure () -> String? + ) async throws -> String? { + return nil + } +} diff --git a/tools/simulator_manager/lease_simulator.sh b/tools/simulator_manager/lease_simulator.sh new file mode 100755 index 0000000..4a58916 --- /dev/null +++ b/tools/simulator_manager/lease_simulator.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# rules_apple `create_simulator_action`: leases a simulator from the simulator +# manager daemon and prints its UDID. +# +# The daemon hands out leases on clones of a per-configuration base simulator, so +# no two concurrent actions share a device. It also releases a lease +# automatically if the leasing process dies, which is what makes this safe +# against crashed tests. +# +# rules_apple contract: read config from SIMULATOR_* env vars, print only the +# UDID to stdout. + +set -euo pipefail + +readonly socket="/tmp/simulator_manager.sock" + +# --- begin runfiles.bash initialization --- +set +e +f=bazel_tools/tools/bash/runfiles/runfiles.bash +# shellcheck disable=SC1090 +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || + source "$0.runfiles/$f" 2>/dev/null || + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || + { echo >&2 "ERROR: cannot find $f"; exit 1; } +set -e +# --- end runfiles.bash initialization --- + +if [[ -z "${SIMULATOR_DEVICE_TYPE:-}" ]]; then + echo >&2 "error: SIMULATOR_DEVICE_TYPE must be set; set the device_type on the" \ + "test runner or the ios_simulator_device build setting" + exit 1 +fi + +# rules_apple reads this script's stdout as the UDID and nothing else, so the +# daemon bootstrap's progress logging has to go to stderr. +"$(rlocation _main/tools/simulator_manager/start)" >&2 + +# The daemon derives the runtime identifier by substituting dots for dashes, so +# it needs the runtime's own version (26.4), not a patch version (26.4.1) which +# would produce the nonexistent com.apple.CoreSimulator.SimRuntime.iOS-26-4-1. +version="$( + xcrun simctl list runtimes -j | + python3 -c ' +import json, sys +requested = sys.argv[1] +runtimes = [ + r for r in json.load(sys.stdin)["runtimes"] + if r["platform"] == "iOS" and r["isAvailable"] +] +if not runtimes: + sys.exit("no available iOS simulator runtimes") +# An exact or prefix match on the requested version wins; otherwise take the +# newest available runtime, matching rules_apple defaulting to latest. +for r in runtimes: + if requested and (r["version"] == requested or r["version"].startswith(requested + ".")): + print(r["identifier"].removeprefix("com.apple.CoreSimulator.SimRuntime.iOS-").replace("-", ".")) + break +else: + print(runtimes[0]["identifier"].removeprefix("com.apple.CoreSimulator.SimRuntime.iOS-").replace("-", ".")) +' "${SIMULATOR_OS_VERSION:-}" +)" + +# rules_apple's `reuse_simulator` doubles as the exclusivity toggle: tests that +# opt out of reuse are the ones (UI, app-host) that need sole ownership of the +# device. +if [[ -n "${SIMULATOR_REUSE_SIMULATOR:-}" ]]; then + exclusive=0 +else + exclusive=1 +fi + +# Lease is keyed on the test runner's pid so the daemon can reclaim the device if +# this test dies without releasing. +readonly lease_pid="${XCTESTRUN_RUNNER_PID:-$$}" + +url_encoded_device_type="${SIMULATOR_DEVICE_TYPE// /%20}" + +curl \ + --silent \ + --fail-with-body \ + --unix-socket "$socket" \ + --retry 5 \ + --retry-connrefused \ + --request POST \ + "http:/-/simulator/$lease_pid?exclusive=$exclusive&deviceType=$url_encoded_device_type&os=iOS&version=$version" diff --git a/tools/simulator_manager/prepare_simulator.sh b/tools/simulator_manager/prepare_simulator.sh new file mode 100755 index 0000000..cb27276 --- /dev/null +++ b/tools/simulator_manager/prepare_simulator.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -euo pipefail + +readonly plist="/tmp/$SIMULATOR_UDID.simulator.defaults.plist" +trap 'rm -f "$plist"' EXIT + +function set_or_add_to_simulator_app_defaults() { + local -r _keypath="$1" + local -r _type="$2" + local -r _value="$3" + IFS=':' read -r -a _parts <<< "$_keypath" + + local -r _last_index=$((${#_parts[@]} - 1)) + + # Export the plist to a temporary file + /usr/bin/defaults export com.apple.iphonesimulator "$plist" + + # Create intermediate dicts + local _prefix="" + for ((i = 0; i < ${#_parts[@]} - 1; i++)); do + _prefix="$_prefix:${_parts[i]}" + /usr/libexec/PlistBuddy -c "Print $_prefix" "$plist" \ + > /dev/null 2>&1 || /usr/libexec/PlistBuddy -c "Add $_prefix dict" "$plist" + done + + # Set or add the final key + local -r _full="$_prefix:${_parts[$_last_index]}" + if + ! /usr/libexec/PlistBuddy -c "Set $_full $_type $_value" "$plist" \ + 2> /dev/null + then + /usr/libexec/PlistBuddy -c "Add $_full $_type $_value" "$plist" + fi + + # Import the modified plist back to the simulator + /usr/bin/defaults import com.apple.iphonesimulator "$plist" +} + +# Disable hardware keyboard +set_or_add_to_simulator_app_defaults \ + "DevicePreferences:$SIMULATOR_UDID:ConnectHardwareKeyboard" "bool" "false" + +# Disable slide typing prompt +/usr/bin/xcrun simctl spawn "$SIMULATOR_UDID" \ + defaults write com.apple.keyboard.preferences \ + DidShowContinuousPathIntroduction 1 diff --git a/tools/simulator_manager/release_simulator.sh b/tools/simulator_manager/release_simulator.sh new file mode 100755 index 0000000..1abd80c --- /dev/null +++ b/tools/simulator_manager/release_simulator.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# rules_apple `clean_up_simulator_action`: releases the lease taken by +# lease_simulator.sh. +# +# Releasing returns the clone to the daemon's idle pool rather than deleting it, +# so the next test of the same configuration reuses a booted device. The daemon +# deletes clones that stay idle past its threshold. +# +# Runs regardless of test success or failure. A failure to release is not fatal: +# the daemon reclaims leases whose owning process has exited. + +set -euo pipefail + +readonly socket="/tmp/simulator_manager.sock" +readonly lease_pid="${XCTESTRUN_RUNNER_PID:-$$}" + +if ! response=$( + curl \ + --silent \ + --fail-with-body \ + --unix-socket "$socket" \ + --request DELETE \ + "http:/-/simulator/$lease_pid" +); then + echo >&2 "warning: failed to release simulator lease for pid $lease_pid: $response" + exit 0 +fi diff --git a/tools/simulator_manager/start.sh b/tools/simulator_manager/start.sh new file mode 100755 index 0000000..4297b1c --- /dev/null +++ b/tools/simulator_manager/start.sh @@ -0,0 +1,242 @@ +#!/bin/bash + +set -euo pipefail + +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# Only change this version after all testing has happened. +# +# If this version is changed then CI won't use the staging RBE pool, and the +# changes will impact all executors in the default pool. +# +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +readonly non_staging_version=41 + +if [[ -z "${EXAMPLE_CI_STAGING_VERSION:-}" ]]; then + readonly expected_version="$non_staging_version" +else + readonly expected_version="staging-$EXAMPLE_CI_STAGING_VERSION" +fi + +function check_need_shutdown() { + local -r current_version="$1" + + if [[ -z "$current_version" ]]; then + return 1 + fi + + if [[ -z "${EXAMPLE_CI_STAGING_VERSION:-}" ]]; then + if [[ $current_version =~ ^[0-9]+$ ]]; then + [[ "$current_version" -lt "$expected_version" ]] + else + return 0 + fi + else + [[ "$current_version" != "$expected_version" ]] + fi +} + +# While we are only allowing a single simulator on remote executors we need to +# more aggressively clean up unused simulators. If this is too large, then we +# have an extra 6GB memory usage sticking around. If this is too small, then we +# will constantly be deleting and creating simulators, adding at least 10 +# seconds to test runtimes. +readonly delete_after_idle_secs=0 +readonly delete_recently_used_after_idle_secs="${EXAMPLE_SIMULATOR_MANAGER_DELETE_RECENTLY_USED_AFTER_IDLE_SECS:-60}" # TODO: Tweak this more. Adjusted to 1 minute to help BuildBuddy with disk space issues. +readonly recently_used_capacity="${EXAMPLE_SIMULATOR_MANAGER_RECENTLY_USED_CAPACITY:-1}" + +readonly mutex_timeout=60 +readonly shutdown_timeout=45 +readonly startup_timeout=10 +readonly stale_mutex_seconds=120 +readonly socket="/tmp/simulator_manager.sock" +readonly pid_path="/tmp/simulator_manager.pid" +readonly mutex_path="/tmp/simulator_manager_start.lock" +readonly scripts_path="/tmp/simulator_manager.scripts" + +function exitMutex() { + rmdir "$mutex_path" 2> /dev/null || true + trap - EXIT +} + +function enterMutex() { + if [[ -d "$mutex_path" ]]; then + local mtime + mtime=$(stat -f %B "$mutex_path") + local now + now=$(date +%s) + if (((now - mtime) > stale_mutex_seconds)); then + echo >&2 "$(date '+[%H:%M:%S]') ⚠️ Deleting stale lock file. This" \ + "shouldn't happen." + rm -rf "$mutex_path" + fi + fi + + for _ in $(seq 1 "$mutex_timeout"); do + if mkdir "$mutex_path" 2> /dev/null; then + trap 'exitMutex' EXIT + return 0 + fi + sleep 1 + done + + return 1 +} + +if ! enterMutex; then + echo >&2 "$(date '+[%H:%M:%S]') ❌ Failed to acquire lock after" \ + "$mutex_timeout seconds; exiting" + exit 1 +fi + +if + { server_pid=$(< "$pid_path"); } 2> /dev/null \ + && ps -p "$server_pid" > /dev/null +then + echo "$(date '+[%H:%M:%S]') Existing simulator manager found; checking" \ + "version" + if version=$( + curl \ + --silent \ + --fail-with-body \ + --unix-socket "$socket" \ + -XGET \ + 'http:/-/version' + ); then + echo "$(date '+[%H:%M:%S]') Existing simulator manager version is $version" + else + echo >&2 "$(date '+[%H:%M:%S]') ❌ Failed to get simulator manager" \ + "version: $version" + server_pid="" + version="" + fi +else + echo "$(date '+[%H:%M:%S]') Existing simulator manager not found" + server_pid="" + version="" +fi + +if check_need_shutdown "$version"; then + echo "$(date '+[%H:%M:%S]') Shutting down existing simulator manager to" \ + "upgrade to version $expected_version" + if response=$( + curl \ + --silent \ + --fail-with-body \ + --unix-socket "$socket" \ + -XPOST \ + 'http:/-/shutdown' + ); then + echo "$(date '+[%H:%M:%S]') Sent graceful shutdown request to simulator" \ + "manager" + + for _ in $(seq 1 "$shutdown_timeout"); do + if ! ps -p "$server_pid" > /dev/null; then + echo "$(date '+[%H:%M:%S]') Simulator manager shut down successfully" + break + fi + sleep 1 + done + if ps -p "$server_pid" > /dev/null; then + echo >&2 "$(date '+[%H:%M:%S]') 🛑 Simulator manager did not shutdown" \ + "in $shutdown_timeout seconds; killing it" + kill -9 "$server_pid" + fi + else + echo >&2 "$(date '+[%H:%M:%S]') 🛑 Failed to gracefully shut down" \ + "simulator manager ($response); killing it" + kill -9 "$server_pid" + fi + + # Kill all straggler processes, in case they started under a different pid + killall simulator_manager 2> /dev/null || true + + server_pid="" + version="" +fi + +if [[ -z "$version" ]]; then + rm -f "$pid_path" || true + + echo "$(date '+[%H:%M:%S]') Starting simulator manager" + + # --- begin runfiles.bash initialization v3 --- + # Copy-pasted from the Bazel Bash runfiles library v3. + set +e + f=bazel_tools/tools/bash/runfiles/runfiles.bash + # shellcheck disable=SC1090 + source "${RUNFILES_DIR:-/dev/null}/$f" 2> /dev/null \ + || source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2> /dev/null \ + || source "$0.runfiles/$f" 2> /dev/null \ + || source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2> /dev/null \ + || source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2> /dev/null \ + || { + echo >&2 "ERROR: cannot find $f" + exit 1 + } + f= + set -e + # --- end runfiles.bash initialization v3 --- + + simulator_manager="$( + rlocation _main/tools/simulator_manager/simulator_manager + )" + bazel_prepare_simulator="$( + rlocation _main/tools/simulator_manager/prepare_simulator + )" + + # We need to copy the prepare_simulator script to a location that will stick + # around after the test runner exits. On RBE the workspace is deleted after + # an action runs, and if the simulator manager tries to run this script then + # it will fail with an error like: + # + # shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory + # chdir: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory + readonly prepare_simulator="$scripts_path/prepare_simulator.sh" + mkdir -p "$scripts_path" + cp "$bazel_prepare_simulator" "$prepare_simulator" + + # Run server in background, in a new process group + # Adjust `PATH` since it's limited when the simulator manager is started in a + # test runner (in particular it doesn't have `/usr/sbin:/sbin` in `PATH`) + PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ + perl -e 'use POSIX setsid; setsid or die "setsid: $!"; exec @ARGV' \ + "$simulator_manager" \ + --version \ + "$expected_version" \ + --pid-path \ + "$pid_path" \ + --unix-socket-path \ + "$socket" \ + --delete-recently-used-idle-after \ + "$delete_recently_used_after_idle_secs" \ + --delete-idle-after \ + "$delete_after_idle_secs" \ + --recently-used-capacity \ + "$recently_used_capacity" \ + "--post-boot" \ + "$prepare_simulator" \ + > /dev/null 2>&1 \ + & + + for _ in $(seq 1 "$startup_timeout"); do + if [[ -f "$pid_path" ]]; then + break + fi + sleep 1 + done + if [[ ! -f "$pid_path" ]]; then + echo >&2 "$(date '+[%H:%M:%S]') error: ❌ Failed to start simulator" \ + "manager in 10 seconds; exiting" + exit 1 + fi + + server_pid=$(< "$pid_path") + + echo "$(date '+[%H:%M:%S]') ✅ Started simulator manager with pid $server_pid" +else + echo "$(date '+[%H:%M:%S]') ✅ Simulator manager with version $version" \ + "already running with pid $server_pid" +fi + +exitMutex