ranger-compiler 3.1.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +2527 -0
  2. package/LICENSE +28 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +1650 -1875
  5. package/dist/Lang.rgr +10946 -5662
  6. package/dist/README.md +3 -2
  7. package/dist/api.d.ts +2023 -61
  8. package/dist/api.js +68930 -27355
  9. package/dist/lib/ACEEditor.rgr +2 -0
  10. package/dist/lib/Ajax.rgr +2 -0
  11. package/dist/lib/CmdParams.rgr +2 -0
  12. package/dist/lib/Crypto.rgr +2 -0
  13. package/dist/lib/DOMLib.rgr +2 -0
  14. package/dist/lib/Engine3D.rgr +2 -0
  15. package/dist/lib/ImmutableVector.rgr +2 -0
  16. package/dist/lib/IndexedDB.rgr +2 -0
  17. package/dist/lib/IsoDate/DateMath.rgr +2 -0
  18. package/dist/lib/IsoDate/IsoCalendar.rgr +2 -0
  19. package/dist/lib/IsoDate/IsoDateParse.rgr +2 -0
  20. package/dist/lib/IsoDateLib.rgr +2 -0
  21. package/dist/lib/JSON.rgr +4906 -48
  22. package/dist/lib/JinxProcess.rgr +2 -0
  23. package/dist/lib/RangerProcess.rgr +2 -0
  24. package/dist/lib/Regex/RegexMatch.rgr +2 -0
  25. package/dist/lib/RegexLib.rgr +2 -0
  26. package/dist/lib/SQL.rgr +2 -0
  27. package/dist/lib/ServiceLib.rgr +2 -0
  28. package/dist/lib/Shell.rgr +326 -0
  29. package/dist/lib/Storage.rgr +2 -0
  30. package/dist/lib/Time.rgr +2 -0
  31. package/dist/lib/Timers.rgr +2 -0
  32. package/dist/lib/TypedArrays.rgr +2 -0
  33. package/dist/lib/ViewLib.rgr +2 -0
  34. package/dist/lib/WebLib.rgr +2 -0
  35. package/dist/lib/WebServerLib.rgr +2 -0
  36. package/dist/lib/apple/AppleAppBuilder.rgr +572 -0
  37. package/dist/lib/apple/AppleAppSpec.rgr +201 -0
  38. package/dist/lib/apple/AppleDevice.rgr +295 -0
  39. package/dist/lib/apple/AppleDeviceDoctor.rgr +453 -0
  40. package/dist/lib/apple/AppleSigning.rgr +379 -0
  41. package/dist/lib/apple/AppleSimulator.rgr +248 -0
  42. package/dist/lib/apple/AppleTarget.rgr +185 -0
  43. package/dist/lib/apple/AppleToolchain.rgr +458 -0
  44. package/dist/lib/apple/README.md +311 -0
  45. package/dist/lib/apple/apple_test.rgr +669 -0
  46. package/dist/lib/core/README.md +251 -0
  47. package/dist/lib/core/RgBase.rgr +313 -0
  48. package/dist/lib/core/RgNum.rgr +653 -0
  49. package/dist/lib/core/RgText.rgr +680 -0
  50. package/dist/lib/core/RgU32.rgr +309 -0
  51. package/dist/lib/ranger-dir.rgr +27 -5
  52. package/dist/lib/shell_test.rgr +193 -0
  53. package/dist/lib/stdlib.rgr +1176 -665
  54. package/dist/lib/stdops.rgr +2 -0
  55. package/dist/package.json +1 -1
  56. package/dist/rgrc.js +73013 -37819
  57. package/dist/stdops.rgr +2 -0
  58. package/package.json +1121 -205
@@ -0,0 +1,379 @@
1
+ ; SPDX-License-Identifier: MIT
2
+
3
+ ; =============================================================================
4
+ ; AppleSigning.rgr -- finding the identity and the profile so nobody types them.
5
+ ; =============================================================================
6
+ ;
7
+ ; A simulator build is signed ad hoc and needs nothing. A DEVICE build needs
8
+ ; three things that come as a set, and typing them out is what makes "build it
9
+ ; onto my phone" a three-flag command instead of one:
10
+ ;
11
+ ; the identity a certificate in the keychain, named like
12
+ ; "Apple Development: Someone (ABC123DEF4)". `security
13
+ ; find-identity` lists them.
14
+ ; the profile a .mobileprovision that names the app, the team and the
15
+ ; devices it may run on. Xcode writes them into a directory
16
+ ; and never mentions it again.
17
+ ; the match the profile has to be FOR this bundle id and has to list
18
+ ; THIS device, or the app installs and refuses to launch with
19
+ ; no message anyone can act on.
20
+ ;
21
+ ; All three are discoverable, so this file discovers them. `--identity` and
22
+ ; `--profile` still win when given; this is what happens when they are not.
23
+ ;
24
+ ; What it cannot do: CREATE a profile. That is a conversation with Apple's
25
+ ; developer portal, and the only command line tool that has it is `xcodebuild
26
+ ; -allowProvisioningUpdates`, which is the thing this whole directory exists to
27
+ ; avoid. Open the project in Xcode once, or register the device on the portal;
28
+ ; after that the profile is on the machine and this finds it.
29
+ ; =============================================================================
30
+
31
+ Import "../Shell.rgr"
32
+
33
+ class AppleSigningIdentity {
34
+
35
+ ; The 40-hex fingerprint. `codesign --sign` takes either this or the name;
36
+ ; the fingerprint is unambiguous when two certificates share a name, which
37
+ ; happens every time one is renewed.
38
+ def sha1:string ""
39
+ def name:string ""
40
+
41
+ Constructor () {
42
+ }
43
+
44
+ ; "Apple Development: Someone (ABC123DEF4)" -> "ABC123DEF4". The team id is
45
+ ; the last parenthesised group, and it is what a profile's
46
+ ; application-identifier is prefixed with.
47
+ fn teamId:string () {
48
+ def openAt:int (indexOf name "(")
49
+ if (openAt < 0) {
50
+ return ""
51
+ }
52
+ def closeAt:int (indexOf name ")")
53
+ if (closeAt <= (openAt + 1)) {
54
+ return ""
55
+ }
56
+ return (substring name (openAt + 1) closeAt)
57
+ }
58
+
59
+ ; A development certificate signs a build for a device you own. A
60
+ ; distribution one is for the store and cannot be tested on a cable.
61
+ fn isDevelopment:boolean () {
62
+ if ((indexOf name "Apple Development") == 0) {
63
+ return true
64
+ }
65
+ if ((indexOf name "iPhone Developer") == 0) {
66
+ return true
67
+ }
68
+ return false
69
+ }
70
+
71
+ fn describe:string () {
72
+ return (name + " " + sha1)
73
+ }
74
+ }
75
+
76
+ class AppleProvisioningProfile {
77
+
78
+ def path:string ""
79
+ ; The profile's own name, which is what Xcode shows.
80
+ def name:string ""
81
+ ; "ABC123DEF4.fi.ranger.dashboard", or "ABC123DEF4.*" for a wildcard.
82
+ def appId:string ""
83
+ ; The device identifiers this profile may install onto, as the raw JSON
84
+ ; array text. Searched as text rather than parsed: the question is only
85
+ ; "is this udid in there", and a JSON reader for one membership test is a
86
+ ; dependency for nothing.
87
+ def devicesRaw:string ""
88
+ def expires:string ""
89
+
90
+ Constructor () {
91
+ }
92
+
93
+ fn teamId:string () {
94
+ def dot:int (indexOf appId ".")
95
+ if (dot <= 0) {
96
+ return ""
97
+ }
98
+ return (substring appId 0 dot)
99
+ }
100
+
101
+ ; The bundle id this profile is for, with the team prefix taken off.
102
+ fn bundlePattern:string () {
103
+ def dot:int (indexOf appId ".")
104
+ if (dot < 0) {
105
+ return ""
106
+ }
107
+ return (substring appId (dot + 1) (strlen appId))
108
+ }
109
+
110
+ ; A development profile carries the devices it may run on. One with no such
111
+ ; list is an App Store or enterprise profile, which cannot be tested on a
112
+ ; cable.
113
+ fn isDevelopment:boolean () {
114
+ return ((strlen devicesRaw) > 0)
115
+ }
116
+
117
+ fn hasDevice:boolean (udid:string) {
118
+ if ((strlen udid) == 0) {
119
+ return false
120
+ }
121
+ return ((indexOf devicesRaw udid) >= 0)
122
+ }
123
+
124
+ fn describe:string () {
125
+ return (name + " [" + appId + "] " + path)
126
+ }
127
+ }
128
+
129
+ class AppleSigning {
130
+
131
+ def sh:Shell
132
+
133
+ Constructor (shell:Shell) {
134
+ sh = shell
135
+ }
136
+
137
+ ; --- identities -----------------------------------------------------------
138
+
139
+ ; `security find-identity -v -p codesigning` writes one indented line per
140
+ ; certificate:
141
+ ;
142
+ ; 1) A1B2...78 "Apple Development: Someone (ABC123DEF4)"
143
+ ; 1 valid identities found
144
+ ;
145
+ ; The quoted name is the field that matters and the only one with spaces in
146
+ ; it, so it is read between the first and last quote rather than by
147
+ ; splitting the line up.
148
+ sfn parseIdentities:[AppleSigningIdentity] (text:string) {
149
+ def found:[AppleSigningIdentity]
150
+ def lines:[string] (strsplit text "\n")
151
+ for lines rawLine:string i {
152
+ def line:string (trim rawLine)
153
+ def firstQuote:int (indexOf line "\"")
154
+ if (firstQuote > 0) {
155
+ def rest:string (substring line (firstQuote + 1) (strlen line))
156
+ def lastQuote:int (indexOf rest "\"")
157
+ if (lastQuote > 0) {
158
+ def name:string (substring rest 0 lastQuote)
159
+ ; The fingerprint is the word before the quote, after the
160
+ ; "N)" the listing numbers each line with.
161
+ def head:string (trim (substring line 0 firstQuote))
162
+ def parts:[string] (strsplit head " ")
163
+ def sha:string ""
164
+ for parts p:string pi {
165
+ if ((strlen p) == 40) {
166
+ sha = p
167
+ }
168
+ }
169
+ if ((strlen sha) == 40) {
170
+ def id:AppleSigningIdentity (new AppleSigningIdentity)
171
+ id.sha1 = sha
172
+ id.name = name
173
+ push found id
174
+ }
175
+ }
176
+ }
177
+ }
178
+ return found
179
+ }
180
+
181
+ ; A DEVELOPMENT identity wins, because this is for testing on a cable. A
182
+ ; caller who wants a distribution certificate names it.
183
+ sfn pickIdentity@(optional):AppleSigningIdentity (identities:[AppleSigningIdentity] wanted:string) {
184
+ def want:string (trim wanted)
185
+ def found@(optional):AppleSigningIdentity
186
+ if ((strlen want) > 0) {
187
+ for identities id:AppleSigningIdentity i {
188
+ if (null? found) {
189
+ if ((indexOf id.name want) >= 0) {
190
+ found = id
191
+ }
192
+ if (id.sha1 == want) {
193
+ found = id
194
+ }
195
+ }
196
+ }
197
+ return found
198
+ }
199
+ for identities id:AppleSigningIdentity i {
200
+ if (null? found) {
201
+ if (id.isDevelopment()) {
202
+ found = id
203
+ }
204
+ }
205
+ }
206
+ if (null? found) {
207
+ for identities id2:AppleSigningIdentity i2 {
208
+ if (null? found) {
209
+ found = id2
210
+ }
211
+ }
212
+ }
213
+ return found
214
+ }
215
+
216
+ fn findIdentities:[AppleSigningIdentity] () {
217
+ def argv:[string]
218
+ push argv "find-identity"
219
+ push argv "-v"
220
+ push argv "-p"
221
+ push argv "codesigning"
222
+ def res:ShellResult (sh.capture("security" argv))
223
+ if (res.failed()) {
224
+ def none:[AppleSigningIdentity]
225
+ return none
226
+ }
227
+ return (AppleSigning.parseIdentities(res.out))
228
+ }
229
+
230
+ ; --- profiles -------------------------------------------------------------
231
+
232
+ ; Where Xcode keeps them. It moved in Xcode 16, and an installation that has
233
+ ; been upgraded has profiles in both, so both are looked in.
234
+ fn profileDirs:[string] () {
235
+ def dirs:[string]
236
+ def home@(optional):string (env_var "HOME")
237
+ if (null? home) {
238
+ return dirs
239
+ }
240
+ def h:string (unwrap home)
241
+ push dirs (h + "/Library/Developer/Xcode/UserData/Provisioning Profiles")
242
+ push dirs (h + "/Library/MobileDevice/Provisioning Profiles")
243
+ return dirs
244
+ }
245
+
246
+ ; `ls` rather than a directory operator, because this library is already in
247
+ ; the business of running programs and `ls` is on every machine Xcode is.
248
+ fn profilePaths:[string] () {
249
+ def paths:[string]
250
+ def dirs:[string] (this.profileDirs())
251
+ for dirs dir:string di {
252
+ def argv:[string]
253
+ push argv dir
254
+ def res:ShellResult (sh.capture("ls" argv))
255
+ if (res.ok()) {
256
+ def names:[string] (res.outLines())
257
+ for names n:string ni {
258
+ def name:string (trim n)
259
+ if ((strlen name) > 16) {
260
+ if ((indexOf name ".mobileprovision") > 0) {
261
+ push paths (dir + "/" + name)
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ return paths
268
+ }
269
+
270
+ ; A .mobileprovision is a CMS envelope around a plist. `security cms -D`
271
+ ; opens it and `plutil -extract` reads one key back out, which is two tools
272
+ ; and no plist parser.
273
+ fn extractKey:string (plistPath:string key:string format:string) {
274
+ def argv:[string]
275
+ push argv "-extract"
276
+ push argv key
277
+ push argv format
278
+ push argv "-o"
279
+ push argv "-"
280
+ push argv plistPath
281
+ def res:ShellResult (sh.capture("plutil" argv))
282
+ if (res.failed()) {
283
+ return ""
284
+ }
285
+ return (trim res.out)
286
+ }
287
+
288
+ ; `workDir` is where the decoded plist is put; it is overwritten per
289
+ ; profile, because nothing needs two of them at once.
290
+ fn readProfile@(optional):AppleProvisioningProfile (path:string workDir:string) {
291
+ def none@(optional):AppleProvisioningProfile
292
+ def argv:[string]
293
+ push argv "cms"
294
+ push argv "-D"
295
+ push argv "-i"
296
+ push argv path
297
+ def decoded:ShellResult (sh.capture("security" argv))
298
+ if (decoded.failed()) {
299
+ return none
300
+ }
301
+ def plistPath:string (workDir + "/profile.plist")
302
+ if (sh.dryRun == false) {
303
+ create_dir workDir
304
+ write_file workDir "profile.plist" decoded.out
305
+ }
306
+ def p:AppleProvisioningProfile (new AppleProvisioningProfile)
307
+ p.path = path
308
+ p.name = (this.extractKey(plistPath "Name" "raw"))
309
+ p.appId = (this.extractKey(plistPath "Entitlements.application-identifier" "raw"))
310
+ p.devicesRaw = (this.extractKey(plistPath "ProvisionedDevices" "json"))
311
+ p.expires = (this.extractKey(plistPath "ExpirationDate" "raw"))
312
+ if ((strlen p.appId) == 0) {
313
+ return none
314
+ }
315
+ def hit@(optional):AppleProvisioningProfile p
316
+ return hit
317
+ }
318
+
319
+ ; Does this profile's application-identifier cover this bundle id? A
320
+ ; wildcard profile ends in ".*" and covers everything under its prefix,
321
+ ; which is what an Xcode "Team Provisioning Profile" is.
322
+ sfn appIdMatches:boolean (appId:string bundleId:string) {
323
+ def dot:int (indexOf appId ".")
324
+ if (dot < 0) {
325
+ return false
326
+ }
327
+ def pattern:string (substring appId (dot + 1) (strlen appId))
328
+ if (pattern == bundleId) {
329
+ return true
330
+ }
331
+ def starAt:int (indexOf pattern "*")
332
+ if (starAt < 0) {
333
+ return false
334
+ }
335
+ if (starAt == 0) {
336
+ return true
337
+ }
338
+ def prefix:string (substring pattern 0 starAt)
339
+ return ((indexOf bundleId prefix) == 0)
340
+ }
341
+
342
+ ; The profile to sign with: for this bundle id, listing this device, and an
343
+ ; exact bundle id beats a wildcard because that is the one Xcode made for
344
+ ; this app.
345
+ fn pickProfile@(optional):AppleProvisioningProfile (bundleId:string deviceUdid:string workDir:string) {
346
+ def exact@(optional):AppleProvisioningProfile
347
+ def wildcard@(optional):AppleProvisioningProfile
348
+ def paths:[string] (this.profilePaths())
349
+ for paths path:string pi {
350
+ def readOpt@(optional):AppleProvisioningProfile (this.readProfile(path workDir))
351
+ if (!null? readOpt) {
352
+ def p:AppleProvisioningProfile (unwrap readOpt)
353
+ if (AppleSigning.appIdMatches(p.appId bundleId)) {
354
+ if (p.isDevelopment()) {
355
+ def usable:boolean true
356
+ if ((strlen deviceUdid) > 0) {
357
+ usable = (p.hasDevice(deviceUdid))
358
+ }
359
+ if usable {
360
+ if ((p.bundlePattern()) == bundleId) {
361
+ if (null? exact) {
362
+ exact = p
363
+ }
364
+ } {
365
+ if (null? wildcard) {
366
+ wildcard = p
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+ if (!null? exact) {
375
+ return exact
376
+ }
377
+ return wildcard
378
+ }
379
+ }
@@ -0,0 +1,248 @@
1
+ ; SPDX-License-Identifier: MIT
2
+
3
+ ; =============================================================================
4
+ ; AppleSimulator.rgr -- reading `xcrun simctl list devices`.
5
+ ; =============================================================================
6
+ ;
7
+ ; simctl can answer in JSON, and this reads the PLAIN TEXT instead, on purpose:
8
+ ; the text form is stable, it is what a person sees when they run the command
9
+ ; themselves, and parsing it needs no JSON library on any target this compiles
10
+ ; to. The parser is a pure function over a string, which means the one part of
11
+ ; the simulator story that has edge cases is the one part that can be tested on
12
+ ; a machine with no simulator on it.
13
+ ;
14
+ ; The format:
15
+ ;
16
+ ; == Devices ==
17
+ ; -- iOS 17.5 --
18
+ ; iPhone SE (3rd generation) (A1B2...-...) (Shutdown)
19
+ ; iPad Pro 13-inch (M4) (C3D4...-...) (Booted)
20
+ ; -- watchOS 10.5 --
21
+ ; Apple Watch Series 9 (45mm) (E5F6...-...) (Shutdown)
22
+ ;
23
+ ; The trap is that a device NAME contains parentheses at least as often as not,
24
+ ; so the fields cannot be found by counting them from the left. They are found
25
+ ; from the RIGHT: the last group is the state, the one before it is the UDID,
26
+ ; and whatever is left is the name however many brackets it holds.
27
+ ; =============================================================================
28
+
29
+ class AppleSimulator {
30
+
31
+ def name:string ""
32
+ def udid:string ""
33
+ ; "Booted", "Shutdown", "Creating"...
34
+ def state:string ""
35
+ ; The runtime heading this device was listed under: "iOS 17.5".
36
+ def runtime:string ""
37
+
38
+ Constructor () {
39
+ }
40
+
41
+ fn isBooted:boolean () {
42
+ return (state == "Booted")
43
+ }
44
+
45
+ fn describe:string () {
46
+ return (name + " [" + runtime + "] " + udid + " (" + state + ")")
47
+ }
48
+ }
49
+
50
+ class AppleSimulatorList {
51
+
52
+ ; Peel one trailing "(...)" group off a line. Answers ([head, inner]), or
53
+ ; ([line, ""]) when there is no group to peel.
54
+ ; Characters are compared as one-character SUBSTRINGS rather than as code
55
+ ; points: `charAt` answers a `char`, and what a `char` is differs enough
56
+ ; between targets that the arithmetic form does not compile everywhere.
57
+ sfn pair:[string] (a:string b:string) {
58
+ def v:[string]
59
+ push v a
60
+ push v b
61
+ return v
62
+ }
63
+
64
+ sfn peelTail:[string] (line:string) {
65
+ def s:string (trim line)
66
+ def n:int (strlen s)
67
+ if (n < 2) {
68
+ return (AppleSimulatorList.pair(s ""))
69
+ }
70
+ if ((substring s (n - 1) n) != ")") {
71
+ return (AppleSimulatorList.pair(s ""))
72
+ }
73
+ ; Walk back to the matching "(", counting depth so a nested group in a
74
+ ; name does not end the search early.
75
+ def depth:int 0
76
+ def i:int (n - 1)
77
+ def openAt:int (0 - 1)
78
+ while (i >= 0) {
79
+ def c:string (substring s i (i + 1))
80
+ if (c == ")") {
81
+ depth = depth + 1
82
+ }
83
+ if (c == "(") {
84
+ depth = depth - 1
85
+ if (depth == 0) {
86
+ openAt = i
87
+ i = 0 - 1
88
+ }
89
+ }
90
+ if (i >= 0) {
91
+ i = i - 1
92
+ }
93
+ }
94
+ if (openAt < 0) {
95
+ return (AppleSimulatorList.pair(s ""))
96
+ }
97
+ def head:string (trim (substring s 0 openAt))
98
+ def inner:string (substring s (openAt + 1) (n - 1))
99
+ return (AppleSimulatorList.pair(head inner))
100
+ }
101
+
102
+ ; A runtime heading looks like "-- iOS 17.5 --". Anything else on a line
103
+ ; that starts with "--" is a section this parser has no use for.
104
+ sfn runtimeOf:string (line:string) {
105
+ def s:string (trim line)
106
+ def n:int (strlen s)
107
+ if (n < 5) {
108
+ return ""
109
+ }
110
+ if ((substring s 0 2) != "--") {
111
+ return ""
112
+ }
113
+ if ((substring s (n - 2) n) != "--") {
114
+ return ""
115
+ }
116
+ return (trim (substring s 2 (n - 2)))
117
+ }
118
+
119
+ ; Does this look like a UDID rather than part of a device name? simctl
120
+ ; writes a UUID, and the length plus the dash positions are enough to tell
121
+ ; it from "3rd generation" without a regex engine on every target.
122
+ sfn looksLikeUdid:boolean (s:string) {
123
+ if ((strlen s) != 36) {
124
+ return false
125
+ }
126
+ if ((substring s 8 9) != "-") {
127
+ return false
128
+ }
129
+ if ((substring s 13 14) != "-") {
130
+ return false
131
+ }
132
+ if ((substring s 18 19) != "-") {
133
+ return false
134
+ }
135
+ if ((substring s 23 24) != "-") {
136
+ return false
137
+ }
138
+ return true
139
+ }
140
+
141
+ sfn parse:[AppleSimulator] (text:string) {
142
+ def found:[AppleSimulator]
143
+ def runtime:string ""
144
+ def lines:[string] (strsplit text "\n")
145
+ for lines rawLine:string i {
146
+ def line:string (trim rawLine)
147
+ if ((strlen line) > 0) {
148
+ def heading:string (AppleSimulatorList.runtimeOf(line))
149
+ if ((strlen heading) > 0) {
150
+ runtime = heading
151
+ ; A runtime simctl cannot use is listed under an
152
+ ; "Unavailable: ..." heading. Its devices cannot be booted,
153
+ ; so they are not offered.
154
+ if ((indexOf heading "Unavailable") == 0) {
155
+ runtime = ""
156
+ }
157
+ } {
158
+ if ((strlen runtime) > 0) {
159
+ def statePeel:[string] (AppleSimulatorList.peelTail(line))
160
+ def state:string (itemAt statePeel 1)
161
+ if ((strlen state) > 0) {
162
+ def udidPeel:[string] (AppleSimulatorList.peelTail((itemAt statePeel 0)))
163
+ def udid:string (itemAt udidPeel 1)
164
+ if (AppleSimulatorList.looksLikeUdid(udid)) {
165
+ def dev:AppleSimulator (new AppleSimulator)
166
+ dev.name = (trim (itemAt udidPeel 0))
167
+ dev.udid = udid
168
+ dev.state = (trim state)
169
+ dev.runtime = runtime
170
+ push found dev
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+ return found
178
+ }
179
+
180
+ ; Everything on a given platform. `prefix` is "iOS" or "watchOS", matched
181
+ ; against the runtime heading, so "iOS 17.5" and "iOS 18.0" both answer to
182
+ ; "iOS" and neither answers to "watchOS".
183
+ sfn onPlatform:[AppleSimulator] (devices:[AppleSimulator] prefix:string) {
184
+ def out:[AppleSimulator]
185
+ for devices d:AppleSimulator i {
186
+ if ((indexOf d.runtime prefix) == 0) {
187
+ push out d
188
+ }
189
+ }
190
+ return out
191
+ }
192
+
193
+ ; The device to use, given what the caller asked for.
194
+ ;
195
+ ; A device that is ALREADY BOOTED wins over one that is not, whatever the
196
+ ; name preference says: booting a second simulator when one is open is slow,
197
+ ; and it puts the app on a window the person is not looking at.
198
+ sfn pick@(optional):AppleSimulator (devices:[AppleSimulator] nameWanted:string) {
199
+ def wanted:string (trim nameWanted)
200
+ def bootedMatch@(optional):AppleSimulator
201
+ def anyMatch@(optional):AppleSimulator
202
+ def bootedAny@(optional):AppleSimulator
203
+ def firstAny@(optional):AppleSimulator
204
+ for devices d:AppleSimulator i {
205
+ def isMatch:boolean false
206
+ if ((strlen wanted) > 0) {
207
+ if ((indexOf d.name wanted) >= 0) {
208
+ isMatch = true
209
+ }
210
+ if (d.udid == wanted) {
211
+ isMatch = true
212
+ }
213
+ }
214
+ if (d.isBooted()) {
215
+ if (null? bootedAny) {
216
+ bootedAny = d
217
+ }
218
+ if isMatch {
219
+ if (null? bootedMatch) {
220
+ bootedMatch = d
221
+ }
222
+ }
223
+ }
224
+ if (null? firstAny) {
225
+ firstAny = d
226
+ }
227
+ if isMatch {
228
+ if (null? anyMatch) {
229
+ anyMatch = d
230
+ }
231
+ }
232
+ }
233
+ if ((strlen wanted) > 0) {
234
+ if (!null? bootedMatch) {
235
+ return bootedMatch
236
+ }
237
+ if (!null? anyMatch) {
238
+ return anyMatch
239
+ }
240
+ def noSuchName@(optional):AppleSimulator
241
+ return noSuchName
242
+ }
243
+ if (!null? bootedAny) {
244
+ return bootedAny
245
+ }
246
+ return firstAny
247
+ }
248
+ }