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.
Files changed (58) hide show
  1. package/CHANGELOG.md +2482 -0
  2. package/LICENSE +28 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +1650 -1895
  5. package/dist/Lang.rgr +10946 -5852
  6. package/dist/README.md +3 -2
  7. package/dist/api.d.ts +1157 -29
  8. package/dist/api.js +66623 -38073
  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 +2 -0
  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 +72955 -40753
  57. package/dist/stdops.rgr +2 -0
  58. package/package.json +1121 -339
@@ -0,0 +1,669 @@
1
+ ; SPDX-License-Identifier: MIT
2
+
3
+ ; =============================================================================
4
+ ; apple_test.rgr -- the Apple build driver, checked without a Mac.
5
+ ; =============================================================================
6
+ ;
7
+ ; Nothing here runs an Apple tool. What it checks is the part of a build driver
8
+ ; that has bugs in it: which program is invoked, with which arguments, in which
9
+ ; order, and what the bundle it writes claims about itself. `Shell.dryRun`
10
+ ; records instead of executing, so the whole plan is a list of strings, and the
11
+ ; simulator parser is a pure function over the text simctl prints.
12
+ ;
13
+ ; node bin/output.js -es6 lib/apple/apple_test.rgr -nodecli -d=tmp -o=apple_test.js
14
+ ; node tmp/apple_test.js
15
+ ; =============================================================================
16
+
17
+ Import "../Shell.rgr"
18
+ Import "AppleTarget.rgr"
19
+ Import "AppleAppSpec.rgr"
20
+ Import "AppleToolchain.rgr"
21
+ Import "AppleAppBuilder.rgr"
22
+ Import "AppleSimulator.rgr"
23
+ Import "AppleDevice.rgr"
24
+ Import "AppleSigning.rgr"
25
+ Import "AppleDeviceDoctor.rgr"
26
+
27
+ class AppleTest {
28
+
29
+ def passed:int 0
30
+ def failed:int 0
31
+
32
+ fn check:void (name:string cond:boolean) {
33
+ if cond {
34
+ passed = passed + 1
35
+ } {
36
+ failed = failed + 1
37
+ print " FAIL " + name
38
+ }
39
+ }
40
+
41
+ fn checkHas:void (name:string haystack:string needle:string) {
42
+ this.check(name ((indexOf haystack needle) >= 0))
43
+ }
44
+
45
+ ; ------------------------------------------------------------------ targets
46
+
47
+ fn testTargets:void () {
48
+ def sim:AppleTarget (AppleTarget.iosSimulator("arm64"))
49
+ this.check("ios simulator triple" ((sim.swiftTriple()) == "arm64-apple-ios15.0-simulator"))
50
+ this.check("ios simulator sdk" (sim.sdk == "iphonesimulator"))
51
+ this.check("ios simulator is universal" ((array_length sim.families) == 2))
52
+ this.check("ios simulator ad hoc" (sim.isAdHocSigned()))
53
+ this.check("ios simulator is not a watch" ((sim.isWatch()) == false))
54
+
55
+ def intel:AppleTarget (AppleTarget.iosSimulator("x86_64"))
56
+ this.check("intel simulator triple" ((intel.swiftTriple()) == "x86_64-apple-ios15.0-simulator"))
57
+
58
+ def dev:AppleTarget (AppleTarget.iosDevice("Apple Development: Someone (ABC123)"))
59
+ this.check("ios device triple" ((dev.swiftTriple()) == "arm64-apple-ios15.0"))
60
+ this.check("ios device sdk" (dev.sdk == "iphoneos"))
61
+ this.check("ios device is not ad hoc" ((dev.isAdHocSigned()) == false))
62
+
63
+ def watch:AppleTarget (AppleTarget.watchSimulator("arm64"))
64
+ this.check("watch simulator triple" ((watch.swiftTriple()) == "arm64-apple-watchos9.0-simulator"))
65
+ this.check("watch simulator sdk" (watch.sdk == "watchsimulator"))
66
+ this.check("watch family" ((itemAt watch.families 0) == 4))
67
+ this.check("watch is a watch" (watch.isWatch()))
68
+
69
+ def watchDev:AppleTarget (AppleTarget.watchDevice("Apple Development: Someone (ABC123)"))
70
+ this.check("watch device triple" ((watchDev.swiftTriple()) == "arm64-apple-watchos9.0"))
71
+
72
+ def pad:AppleTarget (AppleTarget.iosSimulator("arm64"))
73
+ pad.onlyPad()
74
+ this.check("ipad only family" ((array_length pad.families) == 1))
75
+ this.check("ipad only is family 2" ((itemAt pad.families 0) == 2))
76
+ this.checkHas("describe names the triple" (pad.describe()) "arm64-apple-ios15.0-simulator")
77
+ this.checkHas("describe names the device" (pad.describe()) "iPad")
78
+ }
79
+
80
+ ; -------------------------------------------------------------------- plist
81
+
82
+ fn testPlist:void () {
83
+ def spec:AppleAppSpec (new AppleAppSpec)
84
+ spec.name = "RangerDashboard"
85
+ spec.displayName = "Ranger & Co"
86
+ spec.bundleId = "fi.ranger.dashboard"
87
+ spec.version = "2.1"
88
+ spec.build = "77"
89
+
90
+ def sim:AppleTarget (AppleTarget.iosSimulator("arm64"))
91
+ def xml:string (AppleInfoPlist.build(spec sim))
92
+
93
+ this.checkHas("plist declares the executable" xml "<key>CFBundleExecutable</key>\n\t<string>RangerDashboard</string>")
94
+ this.checkHas("plist declares the bundle id" xml "<string>fi.ranger.dashboard</string>")
95
+ this.checkHas("plist escapes the display name" xml "Ranger &amp; Co")
96
+ this.checkHas("plist has the launch screen" xml "<key>UILaunchScreen</key>")
97
+ this.checkHas("plist carries the min version" xml "<key>MinimumOSVersion</key>\n\t<string>15.0</string>")
98
+ this.checkHas("plist names the simulator platform" xml "iPhoneSimulator")
99
+ this.checkHas("plist is a universal app" xml "<integer>1</integer>")
100
+ this.checkHas("plist is an iPad app too" xml "<integer>2</integer>")
101
+ this.checkHas("plist allows landscape" xml "UIInterfaceOrientationLandscapeLeft")
102
+ this.check("plist has no watch key" ((indexOf xml "WKApplication") < 0))
103
+
104
+ def watch:AppleTarget (AppleTarget.watchSimulator("arm64"))
105
+ def wxml:string (AppleInfoPlist.build(spec watch))
106
+ this.checkHas("watch plist is standalone" wxml "<key>WKApplication</key>\n\t<true/>")
107
+ this.checkHas("watch plist has no companion" wxml "<key>WKWatchOnly</key>\n\t<true/>")
108
+ this.checkHas("watch plist runs on its own" wxml "<key>WKRunsIndependentlyOfCompanionApp</key>\n\t<true/>")
109
+ this.checkHas("watch plist names the platform" wxml "WatchSimulator")
110
+ this.check("watch plist has no device family" ((indexOf wxml "UIDeviceFamily") < 0))
111
+ this.check("watch plist has no launch screen" ((indexOf wxml "UILaunchScreen") < 0))
112
+
113
+ def devWatch:AppleTarget (AppleTarget.watchDevice("Someone"))
114
+ def dxml:string (AppleInfoPlist.build(spec devWatch))
115
+ this.checkHas("watch device platform" dxml "<string>WatchOS</string>")
116
+ }
117
+
118
+ ; ---------------------------------------------------------------- simulators
119
+
120
+ fn simctlFixture:string () {
121
+ def rows:[string]
122
+ push rows "== Devices =="
123
+ push rows "-- iOS 17.5 --"
124
+ push rows " iPhone SE (3rd generation) (11111111-2222-3333-4444-555555555555) (Shutdown) "
125
+ push rows " iPhone 15 Pro (AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE) (Booted) "
126
+ push rows " iPad Pro 13-inch (M4) (99999999-8888-7777-6666-555555555555) (Shutdown) "
127
+ push rows "-- watchOS 10.5 --"
128
+ push rows " Apple Watch Series 9 (45mm) (12345678-1234-1234-1234-123456789012) (Shutdown) "
129
+ push rows "-- Unavailable: com.apple.CoreSimulator.SimRuntime.tvOS-17-5 --"
130
+ push rows " Apple TV (DEADBEEF-0000-1111-2222-333333333333) (Shutdown) (unavailable, runtime profile not found)"
131
+ return (join rows "\n")
132
+ }
133
+
134
+ fn testSimulators:void () {
135
+ def all:[AppleSimulator] (AppleSimulatorList.parse((this.simctlFixture())))
136
+ this.check("four usable devices" ((array_length all) == 4))
137
+
138
+ def se:AppleSimulator (itemAt all 0)
139
+ this.check("name keeps its brackets" (se.name == "iPhone SE (3rd generation)"))
140
+ this.check("udid is read" (se.udid == "11111111-2222-3333-4444-555555555555"))
141
+ this.check("state is read" (se.state == "Shutdown"))
142
+ this.check("runtime is read" (se.runtime == "iOS 17.5"))
143
+
144
+ def pad:AppleSimulator (itemAt all 2)
145
+ this.check("ipad name with a model bracket" (pad.name == "iPad Pro 13-inch (M4)"))
146
+
147
+ def watch:AppleSimulator (itemAt all 3)
148
+ this.check("watch runtime" (watch.runtime == "watchOS 10.5"))
149
+
150
+ def ios:[AppleSimulator] (AppleSimulatorList.onPlatform(all "iOS"))
151
+ this.check("three iOS devices" ((array_length ios) == 3))
152
+ def watches:[AppleSimulator] (AppleSimulatorList.onPlatform(all "watchOS"))
153
+ this.check("one watch" ((array_length watches) == 1))
154
+
155
+ ; A booted device wins when nothing was asked for.
156
+ def anyPick@(optional):AppleSimulator (AppleSimulatorList.pick(ios ""))
157
+ this.check("picks something" (!null? anyPick))
158
+ def anyDev:AppleSimulator (unwrap anyPick)
159
+ this.check("prefers the booted one" (anyDev.name == "iPhone 15 Pro"))
160
+
161
+ ; A name that was asked for wins over a booted one that was not.
162
+ def padPick@(optional):AppleSimulator (AppleSimulatorList.pick(ios "iPad"))
163
+ this.check("finds the ipad" (!null? padPick))
164
+ def padDev:AppleSimulator (unwrap padPick)
165
+ this.check("named beats booted" (padDev.name == "iPad Pro 13-inch (M4)"))
166
+
167
+ def byUdid@(optional):AppleSimulator (AppleSimulatorList.pick(ios "11111111-2222-3333-4444-555555555555"))
168
+ this.check("finds by udid" (!null? byUdid))
169
+
170
+ def missing@(optional):AppleSimulator (AppleSimulatorList.pick(ios "Nokia 3310"))
171
+ this.check("a name that is not there is absent" (null? missing))
172
+
173
+ ; An unavailable runtime contributes nothing, however well formed.
174
+ this.check("unavailable runtimes are skipped" ((array_length all) == 4))
175
+ }
176
+
177
+ ; ------------------------------------------------------------- the whole plan
178
+
179
+ fn buildSpec:AppleAppSpec () {
180
+ def spec:AppleAppSpec (new AppleAppSpec)
181
+ spec.name = "RangerDashboard"
182
+ spec.displayName = "Ranger Dashboard"
183
+ spec.bundleId = "fi.ranger.dashboard"
184
+ spec.addSource("generated/ui_ios.swift")
185
+ spec.addSource("ios/DashboardView.swift")
186
+ spec.addResource("assets/demo.css")
187
+ spec.addFramework("UIKit")
188
+ return spec
189
+ }
190
+
191
+ fn testBuildPlan:void () {
192
+ def sh:Shell (new Shell)
193
+ sh.dryRun = true
194
+ def tools:AppleToolchain (new AppleToolchain (sh))
195
+ def builder:AppleAppBuilder (new AppleAppBuilder (sh tools))
196
+ builder.verbose = false
197
+ builder.buildRoot = "build"
198
+
199
+ def spec:AppleAppSpec (this.buildSpec())
200
+ def target:AppleTarget (AppleTarget.iosSimulator("arm64"))
201
+ def res:AppleBuildResult (builder.build(spec target))
202
+
203
+ this.check("dry build succeeds" res.ok)
204
+ this.check("bundle path" (res.appPath == "build/ios-simulator/RangerDashboard.app"))
205
+
206
+ def plan:string (join sh.commands "\n")
207
+ this.checkHas("compiles with xcrun swiftc" plan "xcrun --sdk iphonesimulator swiftc")
208
+ this.checkHas("compiles for the simulator triple" plan "-target arm64-apple-ios15.0-simulator")
209
+ this.checkHas("emits an executable" plan "-emit-executable")
210
+ this.checkHas("names the binary after the bundle" plan "-o build/ios-simulator/RangerDashboard.app/RangerDashboard")
211
+ this.checkHas("links UIKit" plan "-framework UIKit")
212
+ this.checkHas("compiles the generated file" plan "generated/ui_ios.swift")
213
+ this.checkHas("compiles the host file" plan "ios/DashboardView.swift")
214
+ this.checkHas("copies the stylesheet" plan "cp -R assets/demo.css build/ios-simulator/RangerDashboard.app")
215
+ this.checkHas("signs the bundle" plan "codesign --force --sign - ")
216
+ this.check("no xcodebuild anywhere" ((indexOf plan "xcodebuild") < 0))
217
+ this.check("no xcodeproj anywhere" ((indexOf plan "xcodeproj") < 0))
218
+
219
+ ; The order matters: the plist has to be linted before anything is
220
+ ; compiled into a bundle that may not install, and signing is last
221
+ ; because signing anything before the binary is in it signs the wrong
222
+ ; thing.
223
+ def lintAt:int (indexOf plan "plutil -lint")
224
+ def swiftAt:int (indexOf plan "swiftc")
225
+ def signAt:int (indexOf plan "codesign")
226
+ this.check("plist is linted before compiling" (lintAt < swiftAt))
227
+ this.check("signing is last" (signAt > swiftAt))
228
+
229
+ ; Every step is recorded whether or not it ran.
230
+ this.check("steps were recorded" ((array_length res.steps) > 5))
231
+ ; The ordering check above is only worth anything if both commands are
232
+ ; really in the plan.
233
+ this.check("the plist lint is in the plan" (lintAt >= 0))
234
+ this.check("the compile is in the plan" (swiftAt >= 0))
235
+ this.check("the signing is in the plan" (signAt >= 0))
236
+ ; A simulator build has no profile to embed and no entitlements to
237
+ ; grant: signing it ad hoc is the whole of it.
238
+ this.check("no profile is embedded for the simulator" ((indexOf plan "embedded.mobileprovision") < 0))
239
+ this.check("no entitlements for the simulator" ((indexOf plan "--entitlements") < 0))
240
+ }
241
+
242
+ ; A real iPhone. Three things have to happen that the simulator needs none
243
+ ; of, and they have to happen in the right order relative to signing.
244
+ fn testDevicePlan:void () {
245
+ def sh:Shell (new Shell)
246
+ sh.dryRun = true
247
+ def tools:AppleToolchain (new AppleToolchain (sh))
248
+ def builder:AppleAppBuilder (new AppleAppBuilder (sh tools))
249
+ builder.verbose = false
250
+
251
+ def spec:AppleAppSpec (this.buildSpec())
252
+ spec.provisioningProfile = "signing/Ranger_Dashboard_Dev.mobileprovision"
253
+ def target:AppleTarget (AppleTarget.iosDevice("Apple Development: Someone (ABC123)"))
254
+ def res:AppleBuildResult (builder.build(spec target))
255
+ this.check("device dry build succeeds" res.ok)
256
+ this.check("device bundle path" (res.appPath == "build/ios-device/RangerDashboard.app"))
257
+
258
+ def plan:string (join sh.commands "\n")
259
+ this.checkHas("device sdk" plan "xcrun --sdk iphoneos swiftc")
260
+ this.checkHas("device triple has no simulator suffix" plan "-target arm64-apple-ios15.0 ")
261
+ this.checkHas("the profile goes in the bundle" plan "build/ios-device/RangerDashboard.app/embedded.mobileprovision")
262
+ this.checkHas("the profile is decoded" plan "security cms -D -i signing/Ranger_Dashboard_Dev.mobileprovision")
263
+ this.checkHas("entitlements are extracted from it" plan "plutil -extract Entitlements xml1 -o build/ios-device/entitlements.plist")
264
+ this.checkHas("signed with a real identity" plan "codesign --force --sign 'Apple Development: Someone (ABC123)'")
265
+ this.checkHas("signed with those entitlements" plan "--entitlements build/ios-device/entitlements.plist")
266
+
267
+ def embedAt:int (indexOf plan "embedded.mobileprovision")
268
+ def signAt:int (indexOf plan "codesign")
269
+ this.check("the profile is in place before signing" (embedAt < signAt))
270
+
271
+ ; Without a profile the build still goes through -- an identity already
272
+ ; provisioned for the device is a real workflow -- but nothing is
273
+ ; embedded and no entitlements are invented.
274
+ def sh2:Shell (new Shell)
275
+ sh2.dryRun = true
276
+ def tools2:AppleToolchain (new AppleToolchain (sh2))
277
+ def b2:AppleAppBuilder (new AppleAppBuilder (sh2 tools2))
278
+ b2.verbose = false
279
+ def bare:AppleAppSpec (this.buildSpec())
280
+ def res2:AppleBuildResult (b2.build(bare target))
281
+ this.check("device build without a profile still builds" res2.ok)
282
+ def plan2:string (join sh2.commands "\n")
283
+ this.check("nothing is embedded without a profile" ((indexOf plan2 "embedded.mobileprovision") < 0))
284
+ this.check("no entitlements without a profile" ((indexOf plan2 "--entitlements") < 0))
285
+ }
286
+
287
+ fn testWatchPlan:void () {
288
+ def sh:Shell (new Shell)
289
+ sh.dryRun = true
290
+ def tools:AppleToolchain (new AppleToolchain (sh))
291
+ def builder:AppleAppBuilder (new AppleAppBuilder (sh tools))
292
+ builder.verbose = false
293
+
294
+ def spec:AppleAppSpec (this.buildSpec())
295
+ def target:AppleTarget (AppleTarget.watchSimulator("arm64"))
296
+ def res:AppleBuildResult (builder.build(spec target))
297
+ this.check("watch dry build succeeds" res.ok)
298
+ this.check("watch bundle path" (res.appPath == "build/watchos-simulator/RangerDashboard.app"))
299
+
300
+ def plan:string (join sh.commands "\n")
301
+ this.checkHas("watch sdk" plan "xcrun --sdk watchsimulator swiftc")
302
+ this.checkHas("watch triple" plan "-target arm64-apple-watchos9.0-simulator")
303
+ }
304
+
305
+ fn testRunPlan:void () {
306
+ def sh:Shell (new Shell)
307
+ sh.dryRun = true
308
+ def tools:AppleToolchain (new AppleToolchain (sh))
309
+ def builder:AppleAppBuilder (new AppleAppBuilder (sh tools))
310
+ builder.verbose = false
311
+
312
+ def spec:AppleAppSpec (this.buildSpec())
313
+ def target:AppleTarget (AppleTarget.iosSimulator("arm64"))
314
+ def res:AppleBuildResult (builder.runOnSimulator(spec target "iPad Pro"))
315
+ this.check("dry run reaches launch" res.ok)
316
+
317
+ def plan:string (join sh.commands "\n")
318
+ this.checkHas("boots a device" plan "xcrun simctl boot")
319
+ this.checkHas("shows the simulator window" plan "open -a Simulator")
320
+ this.checkHas("waits for the boot" plan "xcrun simctl bootstatus")
321
+ this.checkHas("installs the bundle" plan "xcrun simctl install")
322
+ this.checkHas("launches by bundle id" plan "xcrun simctl launch --terminate-running-process")
323
+ this.checkHas("launches the right app" plan "fi.ranger.dashboard")
324
+
325
+ ; A device target has no simulator to run on, and saying so is better
326
+ ; than booting something unrelated.
327
+ def sh2:Shell (new Shell)
328
+ sh2.dryRun = true
329
+ def tools2:AppleToolchain (new AppleToolchain (sh2))
330
+ def b2:AppleAppBuilder (new AppleAppBuilder (sh2 tools2))
331
+ b2.verbose = false
332
+ def devTarget:AppleTarget (AppleTarget.iosDevice("Someone"))
333
+ def devRes:AppleBuildResult (b2.runOnSimulator(spec devTarget ""))
334
+ this.check("a device target is refused" (devRes.ok == false))
335
+ this.checkHas("and says why" devRes.message "device target")
336
+ }
337
+
338
+ ; The quoting is what makes a logged command line true rather than merely
339
+ ; suggestive, and a Ranger project path with a space in it is ordinary.
340
+ fn testQuoting:void () {
341
+ def sh:Shell (new Shell)
342
+ sh.dryRun = true
343
+ def tools:AppleToolchain (new AppleToolchain (sh))
344
+ def builder:AppleAppBuilder (new AppleAppBuilder (sh tools))
345
+ builder.verbose = false
346
+ def spec:AppleAppSpec (new AppleAppSpec)
347
+ spec.name = "App"
348
+ spec.bundleId = "fi.ranger.app"
349
+ spec.addSource("/Users/someone/My Projects/app.swift")
350
+ def target:AppleTarget (AppleTarget.iosSimulator("arm64"))
351
+ builder.build(spec target)
352
+ def plan:string (join sh.commands "\n")
353
+ this.checkHas("a path with a space is quoted" plan "'/Users/someone/My Projects/app.swift'")
354
+ }
355
+
356
+ ; ------------------------------------------------------- connected devices
357
+
358
+ fn devicectlFixture:string () {
359
+ def rows:[string]
360
+ push rows "Devices:"
361
+ push rows "Name Hostname Identifier State Model"
362
+ push rows "--------------- --------------------------- ------------------------------------- ------------------- -----"
363
+ push rows "Tero iPhone 00008101-001A.coredevice 1A2B3C4D-5E6F-7890-ABCD-EF1234567890 connected iPhone 14 Pro (iPhone15,2)"
364
+ push rows "Testi iPad Air 00008103-001B.coredevice 99999999-8888-7777-6666-555555555555 available (paired) iPad Air (iPad13,16)"
365
+ push rows "Vanha iPhone 00008020-001C.coredevice 11111111-2222-3333-4444-555555555555 connected iPhone 11 (iPhone12,1)"
366
+ push rows "Rikki iPhone 00008030-001D.coredevice 22222222-3333-4444-5555-666666666666 unavailable iPhone SE (iPhone14,6)"
367
+ return (join rows "\n")
368
+ }
369
+
370
+ fn testDevices:void () {
371
+ def all:[AppleDeviceInfo] (AppleDeviceList.parse((this.devicectlFixture())))
372
+ this.check("four devices are listed" ((array_length all) == 4))
373
+
374
+ def first:AppleDeviceInfo (itemAt all 0)
375
+ ; A name with a single space inside it must survive: the fields are
376
+ ; split on runs of TWO spaces, not on every space.
377
+ this.check("a name with a space in it" (first.name == "Tero iPhone"))
378
+ this.check("the identifier is the devicectl one" (first.identifier == "1A2B3C4D-5E6F-7890-ABCD-EF1234567890"))
379
+ this.check("the state is read" (first.state == "connected"))
380
+ this.check("and the model, brackets and all" (first.model == "iPhone 14 Pro (iPhone15,2)"))
381
+ this.check("a connected device is connected" (first.isConnected()))
382
+
383
+ def paired:AppleDeviceInfo (itemAt all 1)
384
+ ; "available (paired)" is a device devicectl has no tunnel to at this
385
+ ; moment. It is not connected, and it IS usable: the tunnel comes up
386
+ ; when the first real operation asks for it.
387
+ this.check("a paired device is not connected" ((paired.isConnected()) == false))
388
+ this.check("a paired device is paired" (paired.isPaired()))
389
+ this.check("and it is usable" (paired.isUsable()))
390
+ this.check("a two-word state survives" (paired.state == "available (paired)"))
391
+
392
+ ; "unavailable" contains "available". A substring test that forgot that
393
+ ; would call this one usable, which is the bug this checks for.
394
+ def broken:AppleDeviceInfo (itemAt all 3)
395
+ this.check("an unavailable device says so" (broken.state == "unavailable"))
396
+ this.check("an unavailable device is not paired" ((broken.isPaired()) == false))
397
+ this.check("nor usable" ((broken.isUsable()) == false))
398
+
399
+ ; The header and the separator hold no identifier and fall out.
400
+ def headerCols:[string] (AppleDeviceList.columns("Name Hostname Identifier"))
401
+ this.check("the header splits into columns" ((array_length headerCols) == 3))
402
+
403
+ def anyPick@(optional):AppleDeviceInfo (AppleDeviceList.pick(all ""))
404
+ this.check("something is picked" (!null? anyPick))
405
+ def any:AppleDeviceInfo (unwrap anyPick)
406
+ this.check("and it is the first CONNECTED one" (any.name == "Tero iPhone"))
407
+
408
+ ; A paired device is a candidate on its own name: devicectl raises the
409
+ ; tunnel when it installs.
410
+ def padPick@(optional):AppleDeviceInfo (AppleDeviceList.pick(all "iPad"))
411
+ this.check("a paired device is picked by name" (!null? padPick))
412
+ def pad:AppleDeviceInfo (unwrap padPick)
413
+ this.check("and it is the paired one" (pad.name == "Testi iPad Air"))
414
+
415
+ ; ...but an unavailable one is not, by name or by identifier.
416
+ def brokenPick@(optional):AppleDeviceInfo (AppleDeviceList.pick(all "Rikki"))
417
+ this.check("an unavailable device is refused" (null? brokenPick))
418
+
419
+ ; With nothing connected, the paired device is what "" picks -- the
420
+ ; case this whole distinction exists for.
421
+ def onlyPaired:[AppleDeviceInfo]
422
+ push onlyPaired paired
423
+ push onlyPaired broken
424
+ def pairedPick@(optional):AppleDeviceInfo (AppleDeviceList.pick(onlyPaired ""))
425
+ this.check("a paired device is picked when it is all there is" (!null? pairedPick))
426
+ def fell:AppleDeviceInfo (unwrap pairedPick)
427
+ this.check("and the unavailable one is still skipped" (fell.name == "Testi iPad Air"))
428
+
429
+ def oldPick@(optional):AppleDeviceInfo (AppleDeviceList.pick(all "Vanha"))
430
+ this.check("a named connected device is found" (!null? oldPick))
431
+ def byId@(optional):AppleDeviceInfo (AppleDeviceList.pick(all "11111111-2222-3333-4444-555555555555"))
432
+ this.check("and so is one named by identifier" (!null? byId))
433
+ def missing@(optional):AppleDeviceInfo (AppleDeviceList.pick(all "Nokia"))
434
+ this.check("a name that is not there is absent" (null? missing))
435
+
436
+ def none:[AppleDeviceInfo] (AppleDeviceList.parse(""))
437
+ this.check("nothing plugged in is no devices" ((array_length none) == 0))
438
+ }
439
+
440
+ ; -------------------------------------------------------------- signing
441
+
442
+ fn identityFixture:string () {
443
+ def rows:[string]
444
+ push rows " 1) A1B2C3D4E5F60718293A4B5C6D7E8F901A2B3C4D \"Apple Development: Tero Tolonen (ABC123DEF4)\""
445
+ push rows " 2) 0918273645AABBCCDDEEFF001122334455667788 \"Apple Distribution: Tero Tolonen (ABC123DEF4)\""
446
+ push rows " 2 valid identities found"
447
+ return (join rows "\n")
448
+ }
449
+
450
+ fn testSigning:void () {
451
+ def ids:[AppleSigningIdentity] (AppleSigning.parseIdentities((this.identityFixture())))
452
+ this.check("two identities are read" ((array_length ids) == 2))
453
+ def dev:AppleSigningIdentity (itemAt ids 0)
454
+ this.check("the fingerprint" (dev.sha1 == "A1B2C3D4E5F60718293A4B5C6D7E8F901A2B3C4D"))
455
+ this.check("the quoted name, spaces and all" (dev.name == "Apple Development: Tero Tolonen (ABC123DEF4)"))
456
+ this.check("the team id is the last bracket" ((dev.teamId()) == "ABC123DEF4"))
457
+ this.check("a development certificate is one" (dev.isDevelopment()))
458
+ def dist:AppleSigningIdentity (itemAt ids 1)
459
+ this.check("a distribution certificate is not" ((dist.isDevelopment()) == false))
460
+ this.check("the trailing count line is not an identity" ((array_length ids) == 2))
461
+ ; The fingerprint is 40 hex characters. A line that quotes something and
462
+ ; carries a word that is not one is not a certificate, and reading it as
463
+ ; one would put a name in the keychain lookup that finds nothing.
464
+ def malformed:[AppleSigningIdentity] (AppleSigning.parseIdentities(" 1) TOOSHORT \"Apple Development: Someone (X)\""))
465
+ this.check("a line with no fingerprint is not an identity" ((array_length malformed) == 0))
466
+
467
+ ; Testing on a cable wants the DEVELOPMENT one, whatever order the
468
+ ; keychain lists them in.
469
+ def autoPick@(optional):AppleSigningIdentity (AppleSigning.pickIdentity(ids ""))
470
+ this.check("development wins when nothing was asked for" ((unwrap autoPick).isDevelopment()))
471
+ def named@(optional):AppleSigningIdentity (AppleSigning.pickIdentity(ids "Distribution"))
472
+ this.check("but a named one wins over that" (((unwrap named).isDevelopment()) == false))
473
+ def byHash@(optional):AppleSigningIdentity (AppleSigning.pickIdentity(ids "A1B2C3D4E5F60718293A4B5C6D7E8F901A2B3C4D"))
474
+ this.check("and a fingerprint finds one too" (!null? byHash))
475
+ def noSuch@(optional):AppleSigningIdentity (AppleSigning.pickIdentity(ids "Nokia"))
476
+ this.check("a name that is not there is absent" (null? noSuch))
477
+ def emptyIds:[AppleSigningIdentity]
478
+ def fromNothing@(optional):AppleSigningIdentity (AppleSigning.pickIdentity(emptyIds ""))
479
+ this.check("an empty keychain answers absent" (null? fromNothing))
480
+
481
+ ; The profile match. An exact application-identifier and a wildcard are
482
+ ; both usable and the exact one is the one Xcode made for this app.
483
+ this.check("an exact app id matches" (AppleSigning.appIdMatches("ABC123DEF4.fi.ranger.dashboard" "fi.ranger.dashboard")))
484
+ this.check("a wildcard matches" (AppleSigning.appIdMatches("ABC123DEF4.*" "fi.ranger.dashboard")))
485
+ this.check("a prefixed wildcard matches under it" (AppleSigning.appIdMatches("ABC123DEF4.fi.ranger.*" "fi.ranger.dashboard")))
486
+ this.check("and not outside it" ((AppleSigning.appIdMatches("ABC123DEF4.fi.other.*" "fi.ranger.dashboard")) == false))
487
+ this.check("a different app id does not match" ((AppleSigning.appIdMatches("ABC123DEF4.com.example.other" "fi.ranger.dashboard")) == false))
488
+ this.check("a malformed app id does not match" ((AppleSigning.appIdMatches("nodots" "fi.ranger.dashboard")) == false))
489
+
490
+ def prof:AppleProvisioningProfile (new AppleProvisioningProfile)
491
+ prof.appId = "ABC123DEF4.fi.ranger.dashboard"
492
+ prof.devicesRaw = "[\"1A2B3C4D-5E6F-7890-ABCD-EF1234567890\"]"
493
+ this.check("the team id comes off the app id" ((prof.teamId()) == "ABC123DEF4"))
494
+ this.check("and the bundle pattern is the rest" ((prof.bundlePattern()) == "fi.ranger.dashboard"))
495
+ this.check("a profile with devices is a development one" (prof.isDevelopment()))
496
+ this.check("and it knows which device" (prof.hasDevice("1A2B3C4D-5E6F-7890-ABCD-EF1234567890")))
497
+ this.check("and which it does not" ((prof.hasDevice("00000000-0000-0000-0000-000000000000")) == false))
498
+ def store:AppleProvisioningProfile (new AppleProvisioningProfile)
499
+ store.appId = "ABC123DEF4.fi.ranger.dashboard"
500
+ this.check("a profile with no device list is not for a cable" ((store.isDevelopment()) == false))
501
+ }
502
+
503
+ ; ------------------------------------------------- the device run plan
504
+
505
+ fn testDeviceRunPlan:void () {
506
+ def sh:Shell (new Shell)
507
+ sh.dryRun = true
508
+ def tools:AppleToolchain (new AppleToolchain (sh))
509
+ def builder:AppleAppBuilder (new AppleAppBuilder (sh tools))
510
+ builder.verbose = false
511
+
512
+ def spec:AppleAppSpec (this.buildSpec())
513
+ def target:AppleTarget (AppleTarget.iosDevice("Someone"))
514
+ def res:AppleBuildResult (builder.runOnDevice(spec target "" false))
515
+ this.check("a device run reaches launch" res.ok)
516
+
517
+ def plan:string (join sh.commands "\n")
518
+ this.checkHas("it installs with devicectl" plan "xcrun devicectl device install app --device")
519
+ this.checkHas("and launches by bundle id" plan "xcrun devicectl device process launch --device")
520
+ this.checkHas("replacing whatever was running" plan "--terminate-existing")
521
+ this.checkHas("the right app" plan "fi.ranger.dashboard")
522
+ ; A device has nothing to boot and no window to bring forward, so none
523
+ ; of the simulator's five commands belong here.
524
+ this.check("nothing is booted" ((indexOf plan "simctl boot") < 0))
525
+ this.check("and no Simulator window is opened" ((indexOf plan "open -a Simulator") < 0))
526
+
527
+ ; --console attaches instead, which is the whole point of a test build
528
+ ; on a cable: a device has no console you can otherwise see.
529
+ def sh2:Shell (new Shell)
530
+ sh2.dryRun = true
531
+ def tools2:AppleToolchain (new AppleToolchain (sh2))
532
+ def b2:AppleAppBuilder (new AppleAppBuilder (sh2 tools2))
533
+ b2.verbose = false
534
+ b2.runOnDevice(spec target "" true)
535
+ def plan2:string (join sh2.commands "\n")
536
+ this.checkHas("--console attaches" plan2 "--console")
537
+ this.check("and does not also terminate" ((indexOf plan2 "--terminate-existing") < 0))
538
+
539
+ ; A simulator target has no cable, and saying so beats installing
540
+ ; nowhere.
541
+ def sh3:Shell (new Shell)
542
+ sh3.dryRun = true
543
+ def tools3:AppleToolchain (new AppleToolchain (sh3))
544
+ def b3:AppleAppBuilder (new AppleAppBuilder (sh3 tools3))
545
+ b3.verbose = false
546
+ def simTarget:AppleTarget (AppleTarget.iosSimulator("arm64"))
547
+ def refused:AppleBuildResult (b3.runOnDevice(spec simTarget "" false))
548
+ this.check("a simulator target is refused" (refused.ok == false))
549
+ this.checkHas("and says why" refused.message "simulator target")
550
+ }
551
+
552
+
553
+ ; ------------------------------------------------------- device details
554
+
555
+ ; Real output, trimmed to the fields that decide anything. The bullets and
556
+ ; the ▿ are Apple's, and are exactly why the parser reads what follows
557
+ ; "key:" rather than splitting the line up.
558
+ fn detailsFixture:string () {
559
+ def rows:[string]
560
+ push rows "Current device information:"
561
+ push rows "• identifier: 5FB20F02-191E-5AE6-B2FC-08A9676BCA54"
562
+ push rows "▿ hardwareProperties:"
563
+ push rows " • marketingName: iPhone 16"
564
+ push rows " • productType: iPhone17,3"
565
+ push rows " • udid: 00008140-00014D340C78801C"
566
+ push rows "▿ deviceProperties:"
567
+ push rows " • ddiServicesAvailable: false"
568
+ push rows " • developerModeStatus: enabled"
569
+ push rows " • osVersionNumber: 26.3"
570
+ push rows "▿ connectionProperties:"
571
+ push rows " • pairingState: paired"
572
+ push rows " • tunnelState: unavailable"
573
+ return (join rows "\n")
574
+ }
575
+
576
+ fn testDeviceDetails:void () {
577
+ def d:AppleDeviceDetails (AppleDeviceDetails.parse((this.detailsFixture())))
578
+ this.check("the developer mode status is read" (d.developerMode == "enabled"))
579
+ this.check("and answers as a boolean" (d.developerModeEnabled()))
580
+ this.check("a false ddi is false" (d.ddiAvailable == false))
581
+ this.check("the tunnel state is read" (d.tunnelState == "unavailable"))
582
+ this.check("the pairing state is read" (d.pairingState == "paired"))
583
+ this.check("the os version is read" (d.osVersion == "26.3"))
584
+ this.check("a value with a space in it survives" (d.marketingName == "iPhone 16"))
585
+ this.check("the udid is the hardware one" (d.udid == "00008140-00014D340C78801C"))
586
+
587
+ ; The two states that matter are read from the same text, so a device
588
+ ; with the image mounted must not read as one without it.
589
+ def mounted:AppleDeviceDetails (AppleDeviceDetails.parse("• ddiServicesAvailable: true\n• developerModeStatus: disabled"))
590
+ this.check("a true ddi is true" (mounted.ddiAvailable))
591
+ this.check("and developer mode off is off" ((mounted.developerModeEnabled()) == false))
592
+
593
+ ; Nothing to read is not a crash, and is not a device that passed.
594
+ def empty:AppleDeviceDetails (AppleDeviceDetails.parse(""))
595
+ this.check("no text is no developer mode" ((empty.developerModeEnabled()) == false))
596
+ this.check("no text is no image" (empty.ddiAvailable == false))
597
+ }
598
+
599
+ ; ---------------------------------------------------------------- doctor
600
+
601
+ ; A dry run asks the questions and gets nothing back, which is the same
602
+ ; shape as a Mac with nothing plugged in -- so the walk can be checked
603
+ ; without a device, which is the point of the whole file.
604
+ fn testDoctor:void () {
605
+ def sh:Shell (new Shell)
606
+ sh.dryRun = true
607
+ def doc:AppleDeviceDoctor (new AppleDeviceDoctor (sh))
608
+ push doc.simulatorHint " npm run ui:ios:run"
609
+
610
+ def ok:boolean (doc.checkDevice())
611
+ this.check("no device is not a pass" (ok == false))
612
+ this.check("and it is reported" ((array_length doc.checks) == 1))
613
+ def c:AppleCheckResult (itemAt doc.checks 0)
614
+ this.check("as the device check" (c.name == "device"))
615
+ this.check("which failed" (c.ok == false))
616
+ this.check("with something to do about it" ((array_length c.remedy) > 0))
617
+ this.check("the doctor knows it failed" (doc.failed()))
618
+
619
+ ; The caller's simulator advice is carried into the remedy, because
620
+ ; generic code cannot know which npm script runs which port.
621
+ def hinted:boolean false
622
+ for c.remedy line:string i {
623
+ if ((indexOf line "ui:ios:run") >= 0) {
624
+ hinted = true
625
+ }
626
+ }
627
+ this.check("and the caller's simulator hint is in it" hinted)
628
+
629
+ ; The listing was asked for, and the USB bus after it -- the second
630
+ ; question is what tells a missing cable from a missing pairing.
631
+ def askedList:boolean false
632
+ def askedUsb:boolean false
633
+ for sh.commands cmd:string i {
634
+ if ((indexOf cmd "devicectl list devices") >= 0) {
635
+ askedList = true
636
+ }
637
+ if ((indexOf cmd "SPUSBDataType") >= 0) {
638
+ askedUsb = true
639
+ }
640
+ }
641
+ this.check("the device listing was asked for" askedList)
642
+ this.check("and the USB bus after it" askedUsb)
643
+
644
+ this.check("padding fills to the width" ((strlen (AppleDeviceDoctor.pad("ok" 6))) == 6))
645
+ this.check("and never truncates" ((AppleDeviceDoctor.pad("developer image" 4)) == "developer image"))
646
+ }
647
+
648
+ sfn m@(main):void () {
649
+ def t:AppleTest (new AppleTest)
650
+ print "Apple build driver"
651
+ t.testTargets()
652
+ t.testPlist()
653
+ t.testSimulators()
654
+ t.testBuildPlan()
655
+ t.testDevicePlan()
656
+ t.testWatchPlan()
657
+ t.testRunPlan()
658
+ t.testDevices()
659
+ t.testDeviceDetails()
660
+ t.testDoctor()
661
+ t.testSigning()
662
+ t.testDeviceRunPlan()
663
+ t.testQuoting()
664
+ print " " + t.passed + " passed, " + t.failed + " failed"
665
+ if (t.failed > 0) {
666
+ exit 1
667
+ }
668
+ }
669
+ }