1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | const path = require('path')
|
27 | const fs = require('fs')
|
28 | const util = require('util')
|
29 |
|
30 | let simctl
|
31 | let bplist
|
32 | let plist
|
33 |
|
34 | function findFirstAvailableDevice (list) {
|
35 | |
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 | let ret_obj = {
|
46 | name: null,
|
47 | id: null,
|
48 | runtime: null
|
49 | }
|
50 |
|
51 | let available_runtimes = {}
|
52 |
|
53 | list.runtimes.forEach(function (runtime) {
|
54 | available_runtimes[ runtime.name ] = (runtime.availability === '(available)')
|
55 | })
|
56 |
|
57 | Object.keys(list.devices).some(function (deviceGroup) {
|
58 | return list.devices[deviceGroup].some(function (device) {
|
59 |
|
60 |
|
61 | let normalizedRuntimeName = fixRuntimeName(deviceGroup)
|
62 | if (available_runtimes[normalizedRuntimeName]) {
|
63 | ret_obj = {
|
64 | name: device.name,
|
65 | id: device.udid,
|
66 | runtime: normalizedRuntimeName
|
67 | }
|
68 | return true
|
69 | }
|
70 | return false
|
71 | })
|
72 | })
|
73 |
|
74 | return ret_obj
|
75 | }
|
76 |
|
77 | function findRuntimesGroupByDeviceProperty (list, deviceProperty, availableOnly, options = {}) {
|
78 | |
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | let runtimes = {}
|
87 | let available_runtimes = {}
|
88 |
|
89 | list.runtimes.forEach(function (runtime) {
|
90 | available_runtimes[ runtime.name ] = (runtime.availability === '(available)')
|
91 | })
|
92 |
|
93 | Object.keys(list.devices).forEach(function (deviceGroup) {
|
94 | list.devices[deviceGroup].forEach(function (device) {
|
95 |
|
96 |
|
97 | let normalizedRuntimeName = fixRuntimeName(deviceGroup)
|
98 |
|
99 | let devicePropertyValue = device[deviceProperty]
|
100 |
|
101 | if (options.lowerCase) {
|
102 | devicePropertyValue = devicePropertyValue.toLowerCase()
|
103 | }
|
104 | if (!runtimes[devicePropertyValue]) {
|
105 | runtimes[devicePropertyValue] = []
|
106 | }
|
107 | if (availableOnly) {
|
108 | if (available_runtimes[normalizedRuntimeName]) {
|
109 | runtimes[devicePropertyValue].push(normalizedRuntimeName)
|
110 | }
|
111 | } else {
|
112 | runtimes[devicePropertyValue].push(normalizedRuntimeName)
|
113 | }
|
114 | })
|
115 | })
|
116 |
|
117 | return runtimes
|
118 | }
|
119 |
|
120 | function findAvailableRuntime (list, device_name) {
|
121 | device_name = device_name.toLowerCase()
|
122 |
|
123 | let all_druntimes = findRuntimesGroupByDeviceProperty(list, 'name', true, { lowerCase: true })
|
124 | let druntime = all_druntimes[ filterDeviceName(device_name) ] || all_druntimes[ device_name ]
|
125 | let runtime_found = druntime && druntime.length > 0
|
126 |
|
127 | if (!runtime_found) {
|
128 | console.error(util.format('No available runtimes could be found for "%s".', device_name))
|
129 | process.exit(1)
|
130 | }
|
131 |
|
132 |
|
133 | return druntime.sort().pop()
|
134 | }
|
135 |
|
136 | function getDeviceFromDeviceTypeId (devicetypeid) {
|
137 | |
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 | let ret_obj = {
|
148 | name: null,
|
149 | id: null,
|
150 | runtime: null
|
151 | }
|
152 |
|
153 | let options = { 'silent': true }
|
154 | let list = simctl.list(options).json
|
155 | list = fixSimCtlList(list)
|
156 |
|
157 | let arr = []
|
158 | if (devicetypeid) {
|
159 | arr = devicetypeid.split(',')
|
160 | }
|
161 |
|
162 |
|
163 |
|
164 | let devicetype = null
|
165 | if (arr.length < 1) {
|
166 | let dv = findFirstAvailableDevice(list)
|
167 | console.error(util.format('--devicetypeid was not specified, using first available device: %s.', dv.name))
|
168 | return dv
|
169 | } else {
|
170 | devicetype = arr[0].trim()
|
171 | if (arr.length > 1) {
|
172 | ret_obj.runtime = arr[1].trim()
|
173 | }
|
174 | }
|
175 |
|
176 |
|
177 | let prefix = 'com.apple.CoreSimulator.SimDeviceType.'
|
178 | if (devicetype.indexOf(prefix) !== 0) {
|
179 | devicetype = prefix + devicetype
|
180 | }
|
181 |
|
182 |
|
183 | let devicename_found = list.devicetypes.some(function (deviceGroup) {
|
184 | if (deviceGroup.identifier === devicetype) {
|
185 | ret_obj.name = deviceGroup.name
|
186 | return true
|
187 | }
|
188 |
|
189 | return false
|
190 | })
|
191 |
|
192 |
|
193 | if (!devicename_found) {
|
194 | console.error(util.format('Device type "%s" could not be found.', devicetype))
|
195 | process.exit(1)
|
196 | }
|
197 |
|
198 |
|
199 | if (!ret_obj.runtime) {
|
200 | ret_obj.runtime = findAvailableRuntime(list, ret_obj.name)
|
201 | }
|
202 |
|
203 |
|
204 | if (ret_obj.runtime.indexOf('OS') === -1) {
|
205 | ret_obj.runtime = util.format('iOS %s', ret_obj.runtime)
|
206 | }
|
207 |
|
208 |
|
209 | let deviceid_found = Object.keys(list.devices).some(function (deviceGroup) {
|
210 |
|
211 |
|
212 | let normalizedRuntimeName = fixRuntimeName(deviceGroup)
|
213 |
|
214 | if (normalizedRuntimeName === ret_obj.runtime) {
|
215 | return list.devices[deviceGroup].some(function (device) {
|
216 | if (filterDeviceName(device.name).toLowerCase() === filterDeviceName(ret_obj.name).toLowerCase()) {
|
217 | ret_obj.id = device.udid
|
218 | return true
|
219 | }
|
220 | return false
|
221 | })
|
222 | }
|
223 | return false
|
224 | })
|
225 |
|
226 | if (!deviceid_found) {
|
227 | console.error(
|
228 | util.format('Device id for device name "%s" and runtime "%s" could not be found, or is not available.', ret_obj.name, ret_obj.runtime)
|
229 | )
|
230 | process.exit(1)
|
231 | }
|
232 |
|
233 | return ret_obj
|
234 | }
|
235 |
|
236 |
|
237 |
|
238 | function parseEnvironmentVariables (envVariables, fixsymctl) {
|
239 | envVariables = envVariables || []
|
240 | fixsymctl = typeof fixsymctl !== 'undefined' ? fixsymctl : true
|
241 |
|
242 | let envMap = {}
|
243 | envVariables.forEach(function (variable) {
|
244 | let envPair = variable.split('=', 2)
|
245 | if (envPair.length === 2) {
|
246 | let key = envPair[0]
|
247 | let value = envPair[1]
|
248 | if (fixsymctl) {
|
249 | key = 'SIMCTL_CHILD_' + key
|
250 | }
|
251 | envMap[ key ] = value
|
252 | }
|
253 | })
|
254 | return envMap
|
255 | }
|
256 |
|
257 |
|
258 |
|
259 | function withInjectedEnvironmentVariablesToProcess (process, envVariables, action) {
|
260 | let oldVariables = Object.assign({}, process.env)
|
261 |
|
262 |
|
263 | for (let key in envVariables) {
|
264 | let value = envVariables[key]
|
265 | process.env[key] = value
|
266 | }
|
267 |
|
268 | action()
|
269 |
|
270 |
|
271 | process.env = oldVariables
|
272 | }
|
273 |
|
274 |
|
275 | function filterDeviceName (deviceName) {
|
276 |
|
277 | if (/^iPad Pro/i.test(deviceName)) {
|
278 | return deviceName.replace(/-/g, ' ').trim()
|
279 | }
|
280 |
|
281 | if (deviceName.indexOf('ʀ') > -1) {
|
282 | return deviceName.replace('ʀ', 'R')
|
283 | }
|
284 | return deviceName
|
285 | }
|
286 |
|
287 | function fixNameKey (array, mapping) {
|
288 | if (!array || !mapping) {
|
289 | return array
|
290 | }
|
291 |
|
292 | return array.map(function (elem) {
|
293 | let name = mapping[elem.name]
|
294 | if (name) {
|
295 | elem.name = name
|
296 | }
|
297 | return elem
|
298 | })
|
299 | }
|
300 |
|
301 | function fixSimCtlList (list) {
|
302 |
|
303 | let deviceTypeNameMap = {
|
304 | 'iPhone2017-A': 'iPhone 8',
|
305 | 'iPhone2017-B': 'iPhone 8 Plus',
|
306 | 'iPhone2017-C': 'iPhone X',
|
307 | 'Watch2017 - 38mm': 'Apple Watch Series 3 - 38mm',
|
308 | 'Watch2017 - 42mm': 'Apple Watch Series 3 - 42mm'
|
309 | }
|
310 | list.devicetypes = fixNameKey(list.devicetypes, deviceTypeNameMap)
|
311 |
|
312 |
|
313 |
|
314 | let deviceNameMap = {
|
315 | 'Apple TV 1080p': 'Apple TV',
|
316 | 'iPad Pro': 'iPad Pro (9.7-inch)'
|
317 | }
|
318 | Object.keys(list.devices).forEach(function (key) {
|
319 | list.devices[key] = fixNameKey(list.devices[key], deviceNameMap)
|
320 | })
|
321 |
|
322 | return list
|
323 | }
|
324 |
|
325 | function fixRuntimeName (runtimeName) {
|
326 |
|
327 | const pattern = /^com\.apple\.CoreSimulator\.SimRuntime\.(([a-zA-Z0-9]+)-(\S+))$/i
|
328 | const match = pattern.exec(runtimeName)
|
329 |
|
330 | if (match) {
|
331 | const [ , , os, version ] = match
|
332 |
|
333 | return `${os} ${version.replace('-', '.')}`
|
334 | }
|
335 |
|
336 | return runtimeName
|
337 | }
|
338 |
|
339 | let lib = {
|
340 |
|
341 | init: function () {
|
342 | if (!simctl) {
|
343 | simctl = require('simctl')
|
344 | }
|
345 | let output = simctl.check_prerequisites()
|
346 | if (output.code !== 0) {
|
347 | console.error(output.output)
|
348 | }
|
349 |
|
350 | if (!bplist) {
|
351 | bplist = require('bplist-parser')
|
352 | }
|
353 |
|
354 | return output.code
|
355 | },
|
356 |
|
357 |
|
358 | showsdks: function (args) {
|
359 | let options = { silent: true, runtimes: true }
|
360 | let list = simctl.list(options).json
|
361 |
|
362 | let output = 'Simulator SDK Roots:\n'
|
363 | list.runtimes.forEach(function (runtime) {
|
364 | if (runtime.availability === '(available)') {
|
365 | output += util.format('"%s" (%s)\n', runtime.name, runtime.buildversion)
|
366 | output += util.format('\t(unknown)\n')
|
367 | }
|
368 | })
|
369 |
|
370 | return output
|
371 | },
|
372 |
|
373 |
|
374 |
|
375 | getdevicetypes: function (args) {
|
376 | let options = { silent: true }
|
377 | let list = simctl.list(options).json
|
378 | list = fixSimCtlList(list)
|
379 |
|
380 | let druntimes = findRuntimesGroupByDeviceProperty(list, 'name', true, { lowerCase: true })
|
381 | let name_id_map = {}
|
382 |
|
383 | list.devicetypes.forEach(function (device) {
|
384 | name_id_map[ filterDeviceName(device.name).toLowerCase() ] = device.identifier
|
385 | })
|
386 |
|
387 | list = []
|
388 | let remove = function (devicename, runtime) {
|
389 |
|
390 | list.push(util.format('%s, %s', name_id_map[ devicename ].replace(/^com.apple.CoreSimulator.SimDeviceType./, ''), runtime.replace(/^iOS /, '')))
|
391 | }
|
392 |
|
393 | let cur = function (devicename) {
|
394 | return function (runtime) {
|
395 | remove(devicename, runtime)
|
396 | }
|
397 | }
|
398 |
|
399 | for (let deviceName in druntimes) {
|
400 | let runtimes = druntimes[ deviceName ]
|
401 | let dname = filterDeviceName(deviceName).toLowerCase()
|
402 |
|
403 | if (!(dname in name_id_map)) {
|
404 | continue
|
405 | }
|
406 |
|
407 | runtimes.forEach(cur(dname))
|
408 | }
|
409 | return list
|
410 | },
|
411 |
|
412 |
|
413 |
|
414 | showdevicetypes: function (args) {
|
415 | let output = ''
|
416 | this.getdevicetypes().forEach(function (device) {
|
417 | output += util.format('%s\n', device)
|
418 | })
|
419 |
|
420 | return output
|
421 | },
|
422 |
|
423 |
|
424 | launch: function (app_path, devicetypeid, log, exit, setenv, argv) {
|
425 | let wait_for_debugger = false
|
426 | let info_plist_path
|
427 | let app_identifier
|
428 |
|
429 | info_plist_path = path.join(app_path, 'Info.plist')
|
430 | if (!fs.existsSync(info_plist_path)) {
|
431 | console.error(info_plist_path + ' file not found.')
|
432 | process.exit(1)
|
433 | }
|
434 |
|
435 | bplist.parseFile(info_plist_path, function (err, obj) {
|
436 | if (err) {
|
437 |
|
438 | if (!plist) {
|
439 | plist = require('plist')
|
440 | }
|
441 | obj = plist.parse(fs.readFileSync(info_plist_path, 'utf8'))
|
442 | if (obj) {
|
443 | app_identifier = obj.CFBundleIdentifier
|
444 | } else {
|
445 | throw err
|
446 | }
|
447 | } else {
|
448 | app_identifier = obj[0].CFBundleIdentifier
|
449 | }
|
450 |
|
451 | argv = argv || []
|
452 | setenv = setenv || []
|
453 |
|
454 | let environmentVariables = parseEnvironmentVariables(setenv)
|
455 |
|
456 | withInjectedEnvironmentVariablesToProcess(process, environmentVariables, function () {
|
457 |
|
458 |
|
459 | let device = getDeviceFromDeviceTypeId(devicetypeid)
|
460 |
|
461 |
|
462 | simctl.extensions.start(device.id)
|
463 | simctl.install(device.id, app_path)
|
464 | simctl.launch(wait_for_debugger, device.id, app_identifier, argv)
|
465 | simctl.extensions.log(device.id, log)
|
466 | if (log) {
|
467 | console.log(util.format('logPath: %s', path.resolve(log)))
|
468 | }
|
469 | if (exit) {
|
470 | process.exit(0)
|
471 | }
|
472 | })
|
473 | })
|
474 | },
|
475 |
|
476 | install: function (app_path, devicetypeid, log, exit) {
|
477 | let info_plist_path
|
478 |
|
479 | info_plist_path = path.join(app_path, 'Info.plist')
|
480 | if (!fs.existsSync(info_plist_path)) {
|
481 | console.error(info_plist_path + ' file not found.')
|
482 | process.exit(1)
|
483 | }
|
484 |
|
485 | bplist.parseFile(info_plist_path, function (err, obj) {
|
486 | if (err) {
|
487 | throw err
|
488 | }
|
489 |
|
490 |
|
491 |
|
492 | let device = getDeviceFromDeviceTypeId(devicetypeid)
|
493 |
|
494 |
|
495 | simctl.extensions.start(device.id)
|
496 | simctl.install(device.id, app_path)
|
497 |
|
498 | simctl.extensions.log(device.id, log)
|
499 | if (log) {
|
500 | console.log(util.format('logPath: %s', path.resolve(log)))
|
501 | }
|
502 | if (exit) {
|
503 | process.exit(0)
|
504 | }
|
505 | })
|
506 | },
|
507 |
|
508 | start: function (devicetypeid) {
|
509 | let device = getDeviceFromDeviceTypeId(devicetypeid)
|
510 | simctl.extensions.start(device.id)
|
511 | },
|
512 |
|
513 | _parseEnvironmentVariables: parseEnvironmentVariables
|
514 |
|
515 | }
|
516 |
|
517 | module.exports = lib
|