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,201 @@
|
|
|
1
|
+
; SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
; =============================================================================
|
|
4
|
+
; AppleAppSpec.rgr -- the app, as everything but the code.
|
|
5
|
+
; =============================================================================
|
|
6
|
+
;
|
|
7
|
+
; An .app bundle is a directory with an executable in it and a property list
|
|
8
|
+
; that explains the executable to the system. Xcode builds that plist from a
|
|
9
|
+
; project file, a target, a scheme and a build settings sheet; a command line
|
|
10
|
+
; build has to say it in one place, and this is that place.
|
|
11
|
+
;
|
|
12
|
+
; The plist writer is here too, because the keys are not free-form: an iOS app
|
|
13
|
+
; that omits `CFBundleExecutable` does not launch, one that omits
|
|
14
|
+
; `UIDeviceFamily` is an iPhone app on an iPad, and one that omits
|
|
15
|
+
; `UILaunchScreen` is letterboxed into a phone-sized box on every iPad no
|
|
16
|
+
; matter what its families say. Those three are the ones a hand-built bundle
|
|
17
|
+
; gets wrong, so they are written from the spec rather than left to a caller.
|
|
18
|
+
; =============================================================================
|
|
19
|
+
|
|
20
|
+
class AppleAppSpec {
|
|
21
|
+
|
|
22
|
+
; The bundle and executable name. No spaces -- this is a file name.
|
|
23
|
+
def name:string "RangerApp"
|
|
24
|
+
; What the home screen shows under the icon.
|
|
25
|
+
def displayName:string "Ranger App"
|
|
26
|
+
; Reverse DNS, and the identity the simulator installs and launches by.
|
|
27
|
+
def bundleId:string "fi.ranger.app"
|
|
28
|
+
def version:string "1.0"
|
|
29
|
+
def build:string "1"
|
|
30
|
+
|
|
31
|
+
; Every Swift file that goes into the executable, in no particular order.
|
|
32
|
+
; For a Ranger app this is the generated .swift plus the host sources.
|
|
33
|
+
def sources:[string]
|
|
34
|
+
; Files and directories copied into the bundle as they are. A stylesheet, a
|
|
35
|
+
; font, an icon.
|
|
36
|
+
def resources:[string]
|
|
37
|
+
; Anything extra for swiftc: -O, -warnings-as-errors, a framework path.
|
|
38
|
+
def swiftFlags:[string]
|
|
39
|
+
; Frameworks to link that swiftc does not pull in on its own.
|
|
40
|
+
def frameworks:[string]
|
|
41
|
+
|
|
42
|
+
; Extra Info.plist entries, as already-formatted XML fragments. The escape
|
|
43
|
+
; hatch for a key this class does not know about.
|
|
44
|
+
def extraPlist:[string]
|
|
45
|
+
|
|
46
|
+
; The .mobileprovision for a DEVICE build. Ignored for the simulator, which
|
|
47
|
+
; needs no profile at all. When set, it is copied into the bundle as
|
|
48
|
+
; `embedded.mobileprovision` and the entitlements it grants are read out of
|
|
49
|
+
; it and handed to codesign.
|
|
50
|
+
def provisioningProfile:string ""
|
|
51
|
+
|
|
52
|
+
; Portrait plus both landscapes, which is what a page that reflows wants.
|
|
53
|
+
; An empty list leaves the key out and lets the system decide.
|
|
54
|
+
def orientations:[string]
|
|
55
|
+
|
|
56
|
+
Constructor () {
|
|
57
|
+
orientations = ([] _:string ( "UIInterfaceOrientationPortrait" "UIInterfaceOrientationLandscapeLeft" "UIInterfaceOrientationLandscapeRight" ))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn addSource:void (path:string) {
|
|
61
|
+
push sources path
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn addResource:void (path:string) {
|
|
65
|
+
push resources path
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn addFramework:void (name:string) {
|
|
69
|
+
push frameworks name
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
; The bundle directory name. `.app` is not decoration -- the simulator
|
|
73
|
+
; installs a directory by that extension and nothing else.
|
|
74
|
+
fn bundleName:string () {
|
|
75
|
+
return (name + ".app")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
; The property list, written out. XML rather than JSON or binary because it is
|
|
80
|
+
; the format every Apple tool reads without conversion, and because a build that
|
|
81
|
+
; goes wrong is one a person has to be able to read.
|
|
82
|
+
class AppleInfoPlist {
|
|
83
|
+
|
|
84
|
+
; Escape the five characters an XML text node cannot hold. A display name
|
|
85
|
+
; with an ampersand in it is not exotic, and an unescaped one makes the
|
|
86
|
+
; plist unparseable rather than wrong -- the app simply will not install.
|
|
87
|
+
sfn escape:string (s:string) {
|
|
88
|
+
def out:string s
|
|
89
|
+
out = (join (strsplit out "&") "&")
|
|
90
|
+
out = (join (strsplit out "<") "<")
|
|
91
|
+
out = (join (strsplit out ">") ">")
|
|
92
|
+
out = (join (strsplit out "\"") """)
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
sfn stringKey:string (key:string value:string) {
|
|
97
|
+
return ("\t<key>" + (AppleInfoPlist.escape(key)) + "</key>\n\t<string>" + (AppleInfoPlist.escape(value)) + "</string>")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
sfn boolKey:string (key:string value:boolean) {
|
|
101
|
+
def word:string "<false/>"
|
|
102
|
+
if value {
|
|
103
|
+
word = "<true/>"
|
|
104
|
+
}
|
|
105
|
+
return ("\t<key>" + (AppleInfoPlist.escape(key)) + "</key>\n\t" + word)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
sfn intArrayKey:string (key:string values:[int]) {
|
|
109
|
+
def rows:[string]
|
|
110
|
+
push rows ("\t<key>" + (AppleInfoPlist.escape(key)) + "</key>")
|
|
111
|
+
push rows "\t<array>"
|
|
112
|
+
for values v:int i {
|
|
113
|
+
push rows ("\t\t<integer>" + v + "</integer>")
|
|
114
|
+
}
|
|
115
|
+
push rows "\t</array>"
|
|
116
|
+
return (join rows "\n")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
sfn stringArrayKey:string (key:string values:[string]) {
|
|
120
|
+
def rows:[string]
|
|
121
|
+
push rows ("\t<key>" + (AppleInfoPlist.escape(key)) + "</key>")
|
|
122
|
+
push rows "\t<array>"
|
|
123
|
+
for values v:string i {
|
|
124
|
+
push rows ("\t\t<string>" + (AppleInfoPlist.escape(v)) + "</string>")
|
|
125
|
+
}
|
|
126
|
+
push rows "\t</array>"
|
|
127
|
+
return (join rows "\n")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
; The whole plist for one app on one target.
|
|
131
|
+
sfn build:string (spec:AppleAppSpec target:AppleTarget) {
|
|
132
|
+
def rows:[string]
|
|
133
|
+
push rows "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
|
134
|
+
push rows "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
|
|
135
|
+
push rows "<plist version=\"1.0\">"
|
|
136
|
+
push rows "<dict>"
|
|
137
|
+
push rows (AppleInfoPlist.stringKey("CFBundleDevelopmentRegion" "en"))
|
|
138
|
+
; The one key with no default worth guessing: without it the system has
|
|
139
|
+
; a directory and no idea which file in it to run.
|
|
140
|
+
push rows (AppleInfoPlist.stringKey("CFBundleExecutable" spec.name))
|
|
141
|
+
push rows (AppleInfoPlist.stringKey("CFBundleIdentifier" spec.bundleId))
|
|
142
|
+
push rows (AppleInfoPlist.stringKey("CFBundleInfoDictionaryVersion" "6.0"))
|
|
143
|
+
push rows (AppleInfoPlist.stringKey("CFBundleName" spec.name))
|
|
144
|
+
push rows (AppleInfoPlist.stringKey("CFBundleDisplayName" spec.displayName))
|
|
145
|
+
push rows (AppleInfoPlist.stringKey("CFBundlePackageType" "APPL"))
|
|
146
|
+
push rows (AppleInfoPlist.stringKey("CFBundleShortVersionString" spec.version))
|
|
147
|
+
push rows (AppleInfoPlist.stringKey("CFBundleVersion" spec.build))
|
|
148
|
+
|
|
149
|
+
def platformName:string "iPhoneOS"
|
|
150
|
+
if (target.isWatch()) {
|
|
151
|
+
platformName = "WatchOS"
|
|
152
|
+
if target.isSimulator {
|
|
153
|
+
platformName = "WatchSimulator"
|
|
154
|
+
}
|
|
155
|
+
} {
|
|
156
|
+
if target.isSimulator {
|
|
157
|
+
platformName = "iPhoneSimulator"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
def platforms:[string]
|
|
161
|
+
push platforms platformName
|
|
162
|
+
push rows (AppleInfoPlist.stringArrayKey("CFBundleSupportedPlatforms" platforms))
|
|
163
|
+
|
|
164
|
+
if (target.isWatch()) {
|
|
165
|
+
; A standalone watch app -- no paired iPhone app to be an appendage
|
|
166
|
+
; of, which is what watchOS 6 and later allow and what makes a watch
|
|
167
|
+
; build from the command line reasonable at all. WKApplication is
|
|
168
|
+
; what watchOS 7 and later read, WKWatchOnly says there is no
|
|
169
|
+
; companion, and WKRunsIndependentlyOfCompanionApp is what the
|
|
170
|
+
; watchOS 6 generation read for the same thing. All three, because
|
|
171
|
+
; which one is consulted depends on the OS and none of them costs
|
|
172
|
+
; anything.
|
|
173
|
+
push rows (AppleInfoPlist.boolKey("WKApplication" true))
|
|
174
|
+
push rows (AppleInfoPlist.boolKey("WKWatchOnly" true))
|
|
175
|
+
push rows (AppleInfoPlist.boolKey("WKRunsIndependentlyOfCompanionApp" true))
|
|
176
|
+
push rows (AppleInfoPlist.stringKey("MinimumOSVersion" target.minVersion))
|
|
177
|
+
} {
|
|
178
|
+
push rows (AppleInfoPlist.stringKey("MinimumOSVersion" target.minVersion))
|
|
179
|
+
push rows (AppleInfoPlist.intArrayKey("UIDeviceFamily" target.families))
|
|
180
|
+
if ((array_length spec.orientations) > 0) {
|
|
181
|
+
push rows (AppleInfoPlist.stringArrayKey("UISupportedInterfaceOrientations" spec.orientations))
|
|
182
|
+
push rows (AppleInfoPlist.stringArrayKey("UISupportedInterfaceOrientations~ipad" spec.orientations))
|
|
183
|
+
}
|
|
184
|
+
; An empty UILaunchScreen dictionary is the modern way to say "this
|
|
185
|
+
; app has no launch storyboard and is still full screen". Leave it
|
|
186
|
+
; out and iOS runs the app letterboxed in a phone-sized window on
|
|
187
|
+
; every iPad, which looks exactly like a layout bug.
|
|
188
|
+
push rows "\t<key>UILaunchScreen</key>"
|
|
189
|
+
push rows "\t<dict/>"
|
|
190
|
+
push rows (AppleInfoPlist.boolKey("UIRequiresFullScreen" false))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for spec.extraPlist row:string i {
|
|
194
|
+
push rows row
|
|
195
|
+
}
|
|
196
|
+
push rows "</dict>"
|
|
197
|
+
push rows "</plist>"
|
|
198
|
+
push rows ""
|
|
199
|
+
return (join rows "\n")
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
; SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
; =============================================================================
|
|
4
|
+
; AppleDevice.rgr -- the iPhones and iPads that are plugged in.
|
|
5
|
+
; =============================================================================
|
|
6
|
+
;
|
|
7
|
+
; `xcrun devicectl list devices` prints a table. This reads it, and like the
|
|
8
|
+
; simulator parser it is a PURE FUNCTION over the text: the one part of the
|
|
9
|
+
; device story that has edge cases is therefore the one part that can be tested
|
|
10
|
+
; on a machine with nothing plugged into it.
|
|
11
|
+
;
|
|
12
|
+
; Devices:
|
|
13
|
+
; Name Hostname Identifier State Model
|
|
14
|
+
; ───────────── ──────────────────── ───────────────────────────────────── ────────── ─────
|
|
15
|
+
; Tero iPhone 00008101-...local 1A2B3C4D-5E6F-7890-ABCD-EF1234567890 connected iPhone 14 Pro (iPhone15,2)
|
|
16
|
+
;
|
|
17
|
+
; The fields are found by splitting on runs of TWO OR MORE spaces rather than by
|
|
18
|
+
; slicing at the separator line's column offsets. Column offsets look tidier and
|
|
19
|
+
; are wrong: `strlen` counts UTF-16 units on some targets, runes on others and
|
|
20
|
+
; BYTES on C++, so a device named "Terön iPhone" would slice at three different
|
|
21
|
+
; places in three languages. Two spaces is two spaces everywhere.
|
|
22
|
+
;
|
|
23
|
+
; The IDENTIFIER is what `devicectl --device` wants, and it is not the 40-hex
|
|
24
|
+
; UDID that older tools used -- it is a UUID that devicectl assigns. Finding it
|
|
25
|
+
; by shape rather than by column position is also what makes this survive a
|
|
26
|
+
; column being added, which Xcode has done before.
|
|
27
|
+
; =============================================================================
|
|
28
|
+
|
|
29
|
+
Import "AppleSimulator.rgr"
|
|
30
|
+
|
|
31
|
+
class AppleDeviceInfo {
|
|
32
|
+
|
|
33
|
+
def name:string ""
|
|
34
|
+
; What `devicectl --device` takes.
|
|
35
|
+
def identifier:string ""
|
|
36
|
+
; "connected", "available (paired)", "unavailable"...
|
|
37
|
+
def state:string ""
|
|
38
|
+
def model:string ""
|
|
39
|
+
|
|
40
|
+
Constructor () {
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
; "connected" means devicectl has a tunnel to the device AT THIS MOMENT.
|
|
44
|
+
fn isConnected:boolean () {
|
|
45
|
+
return ((indexOf state "connected") >= 0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
; "available (paired)" is a device devicectl knows and has no tunnel to
|
|
49
|
+
; right now. It is NOT an absent device: the tunnel is raised on demand by
|
|
50
|
+
; the first real operation -- `devicectl device info apps` on a phone in
|
|
51
|
+
; this state prints "Acquired tunnel connection to device" and then answers
|
|
52
|
+
; -- so an install onto it works. Refusing it here is the difference
|
|
53
|
+
; between a build that runs and one that stops on a cable that is plugged
|
|
54
|
+
; in.
|
|
55
|
+
;
|
|
56
|
+
; "unavailable" CONTAINS "available", so the negative is tested first; the
|
|
57
|
+
; substring test that reads right is the one that is wrong.
|
|
58
|
+
fn isPaired:boolean () {
|
|
59
|
+
if ((indexOf state "unavailable") >= 0) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
return ((indexOf state "available") >= 0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
; A device worth handing to devicectl: one with a tunnel, or one that can
|
|
66
|
+
; raise one.
|
|
67
|
+
fn isUsable:boolean () {
|
|
68
|
+
if (this.isConnected()) {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
return (this.isPaired())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn describe:string () {
|
|
75
|
+
return (name + " (" + model + ") " + identifier + " -- " + state)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class AppleDeviceList {
|
|
80
|
+
|
|
81
|
+
; Split a table row on runs of two or more spaces, dropping empty pieces.
|
|
82
|
+
sfn columns:[string] (line:string) {
|
|
83
|
+
def out:[string]
|
|
84
|
+
def n:int (strlen line)
|
|
85
|
+
def cur:string ""
|
|
86
|
+
def spaces:int 0
|
|
87
|
+
def i:int 0
|
|
88
|
+
while (i < n) {
|
|
89
|
+
def ch:string (substring line i (i + 1))
|
|
90
|
+
if (ch == " ") {
|
|
91
|
+
spaces = spaces + 1
|
|
92
|
+
} {
|
|
93
|
+
; One space is inside a field; two ends it.
|
|
94
|
+
if (spaces >= 2) {
|
|
95
|
+
if ((strlen (trim cur)) > 0) {
|
|
96
|
+
push out (trim cur)
|
|
97
|
+
}
|
|
98
|
+
cur = ""
|
|
99
|
+
} {
|
|
100
|
+
if (spaces == 1) {
|
|
101
|
+
cur = (cur + " ")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
spaces = 0
|
|
105
|
+
cur = (cur + ch)
|
|
106
|
+
}
|
|
107
|
+
i = i + 1
|
|
108
|
+
}
|
|
109
|
+
if ((strlen (trim cur)) > 0) {
|
|
110
|
+
push out (trim cur)
|
|
111
|
+
}
|
|
112
|
+
return out
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
sfn parse:[AppleDeviceInfo] (text:string) {
|
|
116
|
+
def found:[AppleDeviceInfo]
|
|
117
|
+
def lines:[string] (strsplit text "\n")
|
|
118
|
+
for lines rawLine:string li {
|
|
119
|
+
def cols:[string] (AppleDeviceList.columns(rawLine))
|
|
120
|
+
def cnt:int (array_length cols)
|
|
121
|
+
if (cnt >= 3) {
|
|
122
|
+
; The identifier is found by SHAPE, so a new column before or
|
|
123
|
+
; after it changes nothing. The header row and the separator
|
|
124
|
+
; row hold no UUID and fall out here.
|
|
125
|
+
def at:int (0 - 1)
|
|
126
|
+
def ci:int 0
|
|
127
|
+
while (ci < cnt) {
|
|
128
|
+
if (at < 0) {
|
|
129
|
+
if (AppleSimulatorList.looksLikeUdid((itemAt cols ci))) {
|
|
130
|
+
at = ci
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
ci = ci + 1
|
|
134
|
+
}
|
|
135
|
+
if (at > 0) {
|
|
136
|
+
def dev:AppleDeviceInfo (new AppleDeviceInfo)
|
|
137
|
+
dev.name = (itemAt cols 0)
|
|
138
|
+
dev.identifier = (itemAt cols at)
|
|
139
|
+
if ((at + 1) < cnt) {
|
|
140
|
+
dev.state = (itemAt cols (at + 1))
|
|
141
|
+
}
|
|
142
|
+
def rest:[string]
|
|
143
|
+
def mi:int (at + 2)
|
|
144
|
+
while (mi < cnt) {
|
|
145
|
+
push rest (itemAt cols mi)
|
|
146
|
+
mi = mi + 1
|
|
147
|
+
}
|
|
148
|
+
dev.model = (join rest " ")
|
|
149
|
+
push found dev
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return found
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
; The device to install onto. `nameWanted` is a substring of the name, or
|
|
157
|
+
; the identifier itself; "" takes the first usable one, which is the whole
|
|
158
|
+
; point of a one-command device build.
|
|
159
|
+
;
|
|
160
|
+
; A CONNECTED device wins over a merely paired one when both match, because
|
|
161
|
+
; the connected one needs no tunnel raised and is therefore faster and
|
|
162
|
+
; surer. A paired one is still returned when it is all there is.
|
|
163
|
+
sfn pick@(optional):AppleDeviceInfo (devices:[AppleDeviceInfo] nameWanted:string) {
|
|
164
|
+
def wanted:string (trim nameWanted)
|
|
165
|
+
def best@(optional):AppleDeviceInfo
|
|
166
|
+
def fallback@(optional):AppleDeviceInfo
|
|
167
|
+
for devices d:AppleDeviceInfo i {
|
|
168
|
+
if (d.isUsable()) {
|
|
169
|
+
def matches:boolean false
|
|
170
|
+
if ((strlen wanted) == 0) {
|
|
171
|
+
matches = true
|
|
172
|
+
} {
|
|
173
|
+
if ((indexOf d.name wanted) >= 0) {
|
|
174
|
+
matches = true
|
|
175
|
+
}
|
|
176
|
+
if (d.identifier == wanted) {
|
|
177
|
+
matches = true
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if matches {
|
|
181
|
+
if (d.isConnected()) {
|
|
182
|
+
if (null? best) {
|
|
183
|
+
best = d
|
|
184
|
+
}
|
|
185
|
+
} {
|
|
186
|
+
if (null? fallback) {
|
|
187
|
+
fallback = d
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!null? best) {
|
|
194
|
+
return best
|
|
195
|
+
}
|
|
196
|
+
return fallback
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
; =============================================================================
|
|
201
|
+
; What `xcrun devicectl device info details --device ID` says about one device.
|
|
202
|
+
; =============================================================================
|
|
203
|
+
;
|
|
204
|
+
; The listing that `list devices` prints has one word for the whole story --
|
|
205
|
+
; "connected", "available (paired)" -- and that word is not enough to tell a
|
|
206
|
+
; device that cannot be used from one that merely has no tunnel open. `info
|
|
207
|
+
; details` has the fields that actually decide it:
|
|
208
|
+
;
|
|
209
|
+
; ▿ deviceProperties:
|
|
210
|
+
; • ddiServicesAvailable: false
|
|
211
|
+
; • developerModeStatus: enabled
|
|
212
|
+
; • osVersionNumber: 26.3
|
|
213
|
+
; ▿ connectionProperties:
|
|
214
|
+
; • pairingState: paired
|
|
215
|
+
; • tunnelState: unavailable
|
|
216
|
+
;
|
|
217
|
+
; `developerModeStatus` is the one a person has to fix on the phone itself, and
|
|
218
|
+
; `ddiServicesAvailable` is the one that fixes itself the moment anything asks
|
|
219
|
+
; for a developer service. Telling them apart is the difference between "go
|
|
220
|
+
; enable a setting" and "wait four seconds".
|
|
221
|
+
;
|
|
222
|
+
; Parsed as a PURE FUNCTION over the text, like the listing above it: the
|
|
223
|
+
; bullets are decoration, so a value is read as whatever follows "key:" on the
|
|
224
|
+
; line that has it. A field Apple adds, moves or re-indents changes nothing.
|
|
225
|
+
; =============================================================================
|
|
226
|
+
|
|
227
|
+
class AppleDeviceDetails {
|
|
228
|
+
|
|
229
|
+
; "enabled", "disabled", ""
|
|
230
|
+
def developerMode:string ""
|
|
231
|
+
; The developer disk image is mounted and its services can be called.
|
|
232
|
+
def ddiAvailable:boolean false
|
|
233
|
+
; "connected", "disconnected", "unavailable"
|
|
234
|
+
def tunnelState:string ""
|
|
235
|
+
def pairingState:string ""
|
|
236
|
+
def osVersion:string ""
|
|
237
|
+
def marketingName:string ""
|
|
238
|
+
def udid:string ""
|
|
239
|
+
|
|
240
|
+
Constructor () {
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn developerModeEnabled:boolean () {
|
|
244
|
+
return (developerMode == "enabled")
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
; The text after "key:" on the first line that mentions it, trimmed. "" when
|
|
248
|
+
; no line does -- which is also the answer for a device that reported the
|
|
249
|
+
; field empty, and both mean the same thing to every caller here.
|
|
250
|
+
;
|
|
251
|
+
; The line is SPLIT on ":" rather than sliced at the offset `indexOf` gives
|
|
252
|
+
; for the key. The lines have "•" and "▿" in them, and an offset into a
|
|
253
|
+
; string with multi-byte characters before it is counted in runes on some
|
|
254
|
+
; targets and in BYTES on others -- so the slice that is right on Node is
|
|
255
|
+
; off by two per bullet on Go. Splitting asks no arithmetic and is the same
|
|
256
|
+
; answer everywhere. `AppleDeviceList.columns` avoids the same trap for the
|
|
257
|
+
; same reason.
|
|
258
|
+
sfn valueFor:string (text:string key:string) {
|
|
259
|
+
def lines:[string] (strsplit text "\n")
|
|
260
|
+
def found:string ""
|
|
261
|
+
def seen:boolean false
|
|
262
|
+
for lines raw:string i {
|
|
263
|
+
if (seen == false) {
|
|
264
|
+
def parts:[string] (strsplit raw ":")
|
|
265
|
+
def cnt:int (array_length parts)
|
|
266
|
+
if (cnt >= 2) {
|
|
267
|
+
if ((indexOf (itemAt parts 0) key) >= 0) {
|
|
268
|
+
seen = true
|
|
269
|
+
; A value with a ":" in it -- a time, a URL -- keeps it.
|
|
270
|
+
def rest:[string]
|
|
271
|
+
def pi:int 1
|
|
272
|
+
while (pi < cnt) {
|
|
273
|
+
push rest (itemAt parts pi)
|
|
274
|
+
pi = pi + 1
|
|
275
|
+
}
|
|
276
|
+
found = (trim (join rest ":"))
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return found
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
sfn parse:AppleDeviceDetails (text:string) {
|
|
285
|
+
def d:AppleDeviceDetails (new AppleDeviceDetails)
|
|
286
|
+
d.developerMode = (AppleDeviceDetails.valueFor(text "developerModeStatus"))
|
|
287
|
+
d.tunnelState = (AppleDeviceDetails.valueFor(text "tunnelState"))
|
|
288
|
+
d.pairingState = (AppleDeviceDetails.valueFor(text "pairingState"))
|
|
289
|
+
d.osVersion = (AppleDeviceDetails.valueFor(text "osVersionNumber"))
|
|
290
|
+
d.marketingName = (AppleDeviceDetails.valueFor(text "marketingName"))
|
|
291
|
+
d.udid = (AppleDeviceDetails.valueFor(text "udid"))
|
|
292
|
+
d.ddiAvailable = ((AppleDeviceDetails.valueFor(text "ddiServicesAvailable")) == "true")
|
|
293
|
+
return d
|
|
294
|
+
}
|
|
295
|
+
}
|