ranger-compiler 3.2.0 → 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.
- package/CHANGELOG.md +2482 -0
- package/LICENSE +28 -0
- package/LICENSE-MIT +21 -0
- package/README.md +1650 -1895
- package/dist/Lang.rgr +10946 -5852
- package/dist/README.md +3 -2
- package/dist/api.d.ts +1157 -29
- package/dist/api.js +66623 -38073
- package/dist/lib/ACEEditor.rgr +2 -0
- package/dist/lib/Ajax.rgr +2 -0
- package/dist/lib/CmdParams.rgr +2 -0
- package/dist/lib/Crypto.rgr +2 -0
- package/dist/lib/DOMLib.rgr +2 -0
- package/dist/lib/Engine3D.rgr +2 -0
- package/dist/lib/ImmutableVector.rgr +2 -0
- package/dist/lib/IndexedDB.rgr +2 -0
- package/dist/lib/IsoDate/DateMath.rgr +2 -0
- package/dist/lib/IsoDate/IsoCalendar.rgr +2 -0
- package/dist/lib/IsoDate/IsoDateParse.rgr +2 -0
- package/dist/lib/IsoDateLib.rgr +2 -0
- package/dist/lib/JSON.rgr +4906 -48
- package/dist/lib/JinxProcess.rgr +2 -0
- package/dist/lib/RangerProcess.rgr +2 -0
- package/dist/lib/Regex/RegexMatch.rgr +2 -0
- package/dist/lib/RegexLib.rgr +2 -0
- package/dist/lib/SQL.rgr +2 -0
- package/dist/lib/ServiceLib.rgr +2 -0
- package/dist/lib/Shell.rgr +326 -0
- package/dist/lib/Storage.rgr +2 -0
- package/dist/lib/Time.rgr +2 -0
- package/dist/lib/Timers.rgr +2 -0
- package/dist/lib/TypedArrays.rgr +2 -0
- package/dist/lib/ViewLib.rgr +2 -0
- package/dist/lib/WebLib.rgr +2 -0
- package/dist/lib/WebServerLib.rgr +2 -0
- package/dist/lib/apple/AppleAppBuilder.rgr +572 -0
- package/dist/lib/apple/AppleAppSpec.rgr +201 -0
- package/dist/lib/apple/AppleDevice.rgr +295 -0
- package/dist/lib/apple/AppleDeviceDoctor.rgr +453 -0
- package/dist/lib/apple/AppleSigning.rgr +379 -0
- package/dist/lib/apple/AppleSimulator.rgr +248 -0
- package/dist/lib/apple/AppleTarget.rgr +185 -0
- package/dist/lib/apple/AppleToolchain.rgr +458 -0
- package/dist/lib/apple/README.md +311 -0
- package/dist/lib/apple/apple_test.rgr +669 -0
- package/dist/lib/core/README.md +251 -0
- package/dist/lib/core/RgBase.rgr +313 -0
- package/dist/lib/core/RgNum.rgr +653 -0
- package/dist/lib/core/RgText.rgr +680 -0
- package/dist/lib/core/RgU32.rgr +309 -0
- package/dist/lib/ranger-dir.rgr +2 -0
- package/dist/lib/shell_test.rgr +193 -0
- package/dist/lib/stdlib.rgr +1176 -665
- package/dist/lib/stdops.rgr +2 -0
- package/dist/package.json +1 -1
- package/dist/rgrc.js +72955 -40753
- package/dist/stdops.rgr +2 -0
- package/package.json +1121 -339
|
@@ -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
|
+
}
|