UNPKG

15.3 kBJavaScriptView Raw
1/*
2The MIT License (MIT)
3
4Copyright (c) 2014 Shazron Abdullah
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in
14all copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22THE SOFTWARE.
23*/
24// jscs:disable maximumLineLength
25
26const path = require('path')
27const fs = require('fs')
28const util = require('util')
29
30let simctl
31let bplist
32let plist
33
34function findFirstAvailableDevice (list) {
35 /*
36 // Example result:
37 {
38 name : 'iPhone 6',
39 id : 'A1193D97-F5EE-468D-9DBA-786F403766E6',
40 runtime : 'iOS 8.3'
41 }
42 */
43
44 // the object to return
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 // deviceGroup has not been normalized, it can either be the namespaced name, or the
60 // human readable name. We normalize it
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
77function findRuntimesGroupByDeviceProperty (list, deviceProperty, availableOnly, options = {}) {
78 /*
79 // Example result:
80 {
81 "iPhone 6" : [ "iOS 8.2", "iOS 8.3"],
82 "iPhone 6 Plus" : [ "iOS 8.2", "iOS 8.3"]
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 // deviceGroup has not been normalized, it can either be the namespaced name, or the
96 // human readable name. We normalize it
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
120function 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 // return most modern runtime
133 return druntime.sort().pop()
134}
135
136function getDeviceFromDeviceTypeId (devicetypeid) {
137 /*
138 // Example result:
139 {
140 name : 'iPhone 6',
141 id : 'A1193D97-F5EE-468D-9DBA-786F403766E6',
142 runtime : 'iOS 8.3'
143 }
144 */
145
146 // the object to return
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 // get the devicetype from --devicetypeid
163 // --devicetypeid is a string in the form "devicetype, runtime_version" (optional: runtime_version)
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 // check whether devicetype has the "com.apple.CoreSimulator.SimDeviceType." prefix, if not, add it
177 let prefix = 'com.apple.CoreSimulator.SimDeviceType.'
178 if (devicetype.indexOf(prefix) !== 0) {
179 devicetype = prefix + devicetype
180 }
181
182 // now find the devicename from the devicetype
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 // device name not found, exit
193 if (!devicename_found) {
194 console.error(util.format('Device type "%s" could not be found.', devicetype))
195 process.exit(1)
196 }
197
198 // if runtime_version was not specified, we use a default. Use first available that has the device
199 if (!ret_obj.runtime) {
200 ret_obj.runtime = findAvailableRuntime(list, ret_obj.name)
201 }
202
203 // prepend iOS to runtime version, if necessary
204 if (ret_obj.runtime.indexOf('OS') === -1) {
205 ret_obj.runtime = util.format('iOS %s', ret_obj.runtime)
206 }
207
208 // now find the deviceid (by runtime and devicename)
209 let deviceid_found = Object.keys(list.devices).some(function (deviceGroup) {
210 // deviceGroup has not been normalized, it can either be the namespaced name, or the
211 // human readable name. We normalize it
212 let normalizedRuntimeName = fixRuntimeName(deviceGroup)
213 // found the runtime, now find the actual device matching devicename
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// Parses array of KEY=Value strings into map of strings
237// If fixsymctl == true, updates variables for correct usage with simctl
238function 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// Injects specified environt variables to the process and then runs action
258// returns environment variables back to original state after action completes
259function withInjectedEnvironmentVariablesToProcess (process, envVariables, action) {
260 let oldVariables = Object.assign({}, process.env)
261
262 // Inject additional environment variables to process
263 for (let key in envVariables) {
264 let value = envVariables[key]
265 process.env[key] = value
266 }
267
268 action()
269
270 // restore old envs
271 process.env = oldVariables
272}
273
274// replace hyphens in iPad Pro name which differ in 'Device Types' and 'Devices'
275function filterDeviceName (deviceName) {
276 // replace hyphens in iPad Pro name which differ in 'Device Types' and 'Devices'
277 if (/^iPad Pro/i.test(deviceName)) {
278 return deviceName.replace(/-/g, ' ').trim()
279 }
280 // replace ʀ in iPhone Xʀ
281 if (deviceName.indexOf('ʀ') > -1) {
282 return deviceName.replace('ʀ', 'R')
283 }
284 return deviceName
285}
286
287function 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
301function fixSimCtlList (list) {
302 // Xcode 9 `xcrun simctl list devicetypes` have obfuscated names for 2017 iPhones and Apple Watches.
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 // `iPad Pro` in iOS 9.3 has mapped to `iPad Pro (9.7 inch)`
313 // `Apple TV 1080p` has mapped to `Apple TV`
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
325function fixRuntimeName (runtimeName) {
326 // looking for format 'com.apple.CoreSimulator.SimRuntime.iOS-12-0'
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 // all or nothing -- os, version will always have a value for match
333 return `${os} ${version.replace('-', '.')}`
334 }
335
336 return runtimeName
337}
338
339let 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 // jscs:disable disallowUnusedParams
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 // jscs:enable disallowUnusedParams
373
374 // jscs:disable disallowUnusedParams
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 // remove "iOS" prefix in runtime, remove prefix "com.apple.CoreSimulator.SimDeviceType." in id
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 // jscs:enable disallowUnusedParams
412
413 // jscs:disable disallowUnusedParams
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 // jscs:enable disallowUnusedParams
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 // try to see if a regular plist parser will work
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 // get the deviceid from --devicetypeid
458 // --devicetypeid is a string in the form "devicetype, runtime_version" (optional: runtime_version)
459 let device = getDeviceFromDeviceTypeId(devicetypeid)
460
461 // so now we have the deviceid, we can proceed
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 // get the deviceid from --devicetypeid
491 // --devicetypeid is a string in the form "devicetype, runtime_version" (optional: runtime_version)
492 let device = getDeviceFromDeviceTypeId(devicetypeid)
493
494 // so now we have the deviceid, we can proceed
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
517module.exports = lib