123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- /*
- The MIT License (MIT)
-
- Copyright (c) 2014 Shazron Abdullah
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- THE SOFTWARE.
- */
- // jscs:disable maximumLineLength
-
- const path = require('path')
- const fs = require('fs')
- const util = require('util')
-
- let simctl
- let bplist
- let plist
-
- function findFirstAvailableDevice (list) {
- /*
- // Example result:
- {
- name : 'iPhone 6',
- id : 'A1193D97-F5EE-468D-9DBA-786F403766E6',
- runtime : 'iOS 8.3'
- }
- */
-
- // the object to return
- let ret_obj = {
- name: null,
- id: null,
- runtime: null
- }
-
- let available_runtimes = {}
-
- list.runtimes.forEach(function (runtime) {
- available_runtimes[ runtime.name ] = (runtime.availability === '(available)')
- })
-
- Object.keys(list.devices).some(function (deviceGroup) {
- return list.devices[deviceGroup].some(function (device) {
- // deviceGroup has not been normalized, it can either be the namespaced name, or the
- // human readable name. We normalize it
- let normalizedRuntimeName = fixRuntimeName(deviceGroup)
- if (available_runtimes[normalizedRuntimeName]) {
- ret_obj = {
- name: device.name,
- id: device.udid,
- runtime: normalizedRuntimeName
- }
- return true
- }
- return false
- })
- })
-
- return ret_obj
- }
-
- function findRuntimesGroupByDeviceProperty (list, deviceProperty, availableOnly, options = {}) {
- /*
- // Example result:
- {
- "iPhone 6" : [ "iOS 8.2", "iOS 8.3"],
- "iPhone 6 Plus" : [ "iOS 8.2", "iOS 8.3"]
- }
- */
-
- let runtimes = {}
- let available_runtimes = {}
-
- list.runtimes.forEach(function (runtime) {
- // key value changed to "isAvailble" from "availability"
- available_runtimes[ runtime.name ] = (runtime.availability ? (runtime.availability === '(available)') : runtime.isAvailable)
- })
-
- Object.keys(list.devices).forEach(function (deviceGroup) {
- list.devices[deviceGroup].forEach(function (device) {
- // deviceGroup has not been normalized, it can either be the namespaced name, or the
- // human readable name. We normalize it
- let normalizedRuntimeName = fixRuntimeName(deviceGroup)
-
- let devicePropertyValue = device[deviceProperty]
-
- if (options.lowerCase) {
- devicePropertyValue = devicePropertyValue.toLowerCase()
- }
- if (!runtimes[devicePropertyValue]) {
- runtimes[devicePropertyValue] = []
- }
- if (availableOnly) {
- if (available_runtimes[normalizedRuntimeName]) {
- runtimes[devicePropertyValue].push(normalizedRuntimeName)
- }
- } else {
- runtimes[devicePropertyValue].push(normalizedRuntimeName)
- }
- })
- })
-
- return runtimes
- }
-
- function findAvailableRuntime (list, device_name) {
- device_name = device_name.toLowerCase()
-
- let all_druntimes = findRuntimesGroupByDeviceProperty(list, 'name', true, { lowerCase: true })
- let druntime = all_druntimes[ filterDeviceName(device_name) ] || all_druntimes[ device_name ]
- let runtime_found = druntime && druntime.length > 0
-
- if (!runtime_found) {
- console.error(util.format('No available runtimes could be found for "%s".', device_name))
- process.exit(1)
- }
-
- // return most modern runtime
- return druntime.sort().pop()
- }
-
- function getDeviceFromDeviceTypeId (devicetypeid) {
- /*
- // Example result:
- {
- name : 'iPhone 6',
- id : 'A1193D97-F5EE-468D-9DBA-786F403766E6',
- runtime : 'iOS 8.3'
- }
- */
-
- // the object to return
- let ret_obj = {
- name: null,
- id: null,
- runtime: null
- }
-
- let options = { 'silent': true }
- let list = simctl.list(options).json
- list = fixSimCtlList(list)
-
- let arr = []
- if (devicetypeid) {
- arr = devicetypeid.split(',')
- }
-
- // get the devicetype from --devicetypeid
- // --devicetypeid is a string in the form "devicetype, runtime_version" (optional: runtime_version)
- let devicetype = null
- if (arr.length < 1) {
- let dv = findFirstAvailableDevice(list)
- console.error(util.format('--devicetypeid was not specified, using first available device: %s.', dv.name))
- return dv
- } else {
- devicetype = arr[0].trim()
- if (arr.length > 1) {
- ret_obj.runtime = arr[1].trim()
- }
- }
-
- // check whether devicetype has the "com.apple.CoreSimulator.SimDeviceType." prefix, if not, add it
- let prefix = 'com.apple.CoreSimulator.SimDeviceType.'
- if (devicetype.indexOf(prefix) !== 0) {
- devicetype = prefix + devicetype
- }
-
- // now find the devicename from the devicetype
- let devicename_found = list.devicetypes.some(function (deviceGroup) {
- if (deviceGroup.identifier === devicetype) {
- ret_obj.name = deviceGroup.name
- return true
- }
-
- return false
- })
-
- // device name not found, exit
- if (!devicename_found) {
- console.error(util.format('Device type "%s" could not be found.', devicetype))
- process.exit(1)
- }
-
- // if runtime_version was not specified, we use a default. Use first available that has the device
- if (!ret_obj.runtime) {
- ret_obj.runtime = findAvailableRuntime(list, ret_obj.name)
- }
-
- // prepend iOS to runtime version, if necessary
- if (ret_obj.runtime.indexOf('OS') === -1) {
- ret_obj.runtime = util.format('iOS %s', ret_obj.runtime)
- }
-
- // now find the deviceid (by runtime and devicename)
- let deviceid_found = Object.keys(list.devices).some(function (deviceGroup) {
- // deviceGroup has not been normalized, it can either be the namespaced name, or the
- // human readable name. We normalize it
- let normalizedRuntimeName = fixRuntimeName(deviceGroup)
- // found the runtime, now find the actual device matching devicename
- if (normalizedRuntimeName === ret_obj.runtime) {
- return list.devices[deviceGroup].some(function (device) {
- if (filterDeviceName(device.name).toLowerCase() === filterDeviceName(ret_obj.name).toLowerCase()) {
- ret_obj.id = device.udid
- return true
- }
- return false
- })
- }
- return false
- })
-
- if (!deviceid_found) {
- console.error(
- 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)
- )
- process.exit(1)
- }
-
- return ret_obj
- }
-
- // Parses array of KEY=Value strings into map of strings
- // If fixsymctl == true, updates variables for correct usage with simctl
- function parseEnvironmentVariables (envVariables, fixsymctl) {
- envVariables = envVariables || []
- fixsymctl = typeof fixsymctl !== 'undefined' ? fixsymctl : true
-
- let envMap = {}
- envVariables.forEach(function (variable) {
- let envPair = variable.split('=', 2)
- if (envPair.length === 2) {
- let key = envPair[0]
- let value = envPair[1]
- if (fixsymctl) {
- key = 'SIMCTL_CHILD_' + key
- }
- envMap[ key ] = value
- }
- })
- return envMap
- }
-
- // Injects specified environt variables to the process and then runs action
- // returns environment variables back to original state after action completes
- function withInjectedEnvironmentVariablesToProcess (process, envVariables, action) {
- let oldVariables = Object.assign({}, process.env)
-
- // Inject additional environment variables to process
- for (let key in envVariables) {
- let value = envVariables[key]
- process.env[key] = value
- }
-
- action()
-
- // restore old envs
- process.env = oldVariables
- }
-
- // replace hyphens in iPad Pro name which differ in 'Device Types' and 'Devices'
- function filterDeviceName (deviceName) {
- // replace hyphens in iPad Pro name which differ in 'Device Types' and 'Devices'
- if (/^iPad Pro/i.test(deviceName)) {
- return deviceName.replace(/-/g, ' ').trim()
- }
- // replace ʀ in iPhone Xʀ
- if (deviceName.indexOf('ʀ') > -1) {
- return deviceName.replace('ʀ', 'R')
- }
- return deviceName
- }
-
- function fixNameKey (array, mapping) {
- if (!array || !mapping) {
- return array
- }
-
- return array.map(function (elem) {
- let name = mapping[elem.name]
- if (name) {
- elem.name = name
- }
- return elem
- })
- }
-
- function fixSimCtlList (list) {
- // Xcode 9 `xcrun simctl list devicetypes` have obfuscated names for 2017 iPhones and Apple Watches.
- let deviceTypeNameMap = {
- 'iPhone2017-A': 'iPhone 8',
- 'iPhone2017-B': 'iPhone 8 Plus',
- 'iPhone2017-C': 'iPhone X',
- 'Watch2017 - 38mm': 'Apple Watch Series 3 - 38mm',
- 'Watch2017 - 42mm': 'Apple Watch Series 3 - 42mm'
- }
- list.devicetypes = fixNameKey(list.devicetypes, deviceTypeNameMap)
-
- // `iPad Pro` in iOS 9.3 has mapped to `iPad Pro (9.7 inch)`
- // `Apple TV 1080p` has mapped to `Apple TV`
- let deviceNameMap = {
- 'Apple TV 1080p': 'Apple TV',
- 'iPad Pro': 'iPad Pro (9.7-inch)'
- }
- Object.keys(list.devices).forEach(function (key) {
- list.devices[key] = fixNameKey(list.devices[key], deviceNameMap)
- })
-
- return list
- }
-
- function fixRuntimeName (runtimeName) {
- // looking for format 'com.apple.CoreSimulator.SimRuntime.iOS-12-0'
- const pattern = /^com\.apple\.CoreSimulator\.SimRuntime\.(([a-zA-Z0-9]+)-(\S+))$/i
- const match = pattern.exec(runtimeName)
-
- if (match) {
- const [ , , os, version ] = match
- // all or nothing -- os, version will always have a value for match
- return `${os} ${version.replace('-', '.')}`
- }
-
- return runtimeName
- }
-
- let lib = {
-
- init: function () {
- if (!simctl) {
- simctl = require('simctl')
- }
- let output = simctl.check_prerequisites()
- if (output.code !== 0) {
- console.error(output.output)
- }
-
- if (!bplist) {
- bplist = require('bplist-parser')
- }
-
- return output.code
- },
-
- // jscs:disable disallowUnusedParams
- showsdks: function (args) {
- let options = { silent: true, runtimes: true }
- let list = simctl.list(options).json
-
- let output = 'Simulator SDK Roots:\n'
- list.runtimes.forEach(function (runtime) {
- if (runtime.availability === '(available)') {
- output += util.format('"%s" (%s)\n', runtime.name, runtime.buildversion)
- output += util.format('\t(unknown)\n')
- }
- })
-
- return output
- },
- // jscs:enable disallowUnusedParams
-
- // jscs:disable disallowUnusedParams
- getdevicetypes: function (args) {
- let options = { silent: true }
- let list = simctl.list(options).json
- list = fixSimCtlList(list)
-
- let druntimes = findRuntimesGroupByDeviceProperty(list, 'name', true, { lowerCase: true })
- let name_id_map = {}
-
- list.devicetypes.forEach(function (device) {
- name_id_map[ filterDeviceName(device.name).toLowerCase() ] = device.identifier
- })
-
- list = []
- let remove = function (devicename, runtime) {
- // remove "iOS" prefix in runtime, remove prefix "com.apple.CoreSimulator.SimDeviceType." in id
- list.push(util.format('%s, %s', name_id_map[ devicename ].replace(/^com.apple.CoreSimulator.SimDeviceType./, ''), runtime.replace(/^iOS /, '')))
- }
-
- let cur = function (devicename) {
- return function (runtime) {
- remove(devicename, runtime)
- }
- }
-
- for (let deviceName in druntimes) {
- let runtimes = druntimes[ deviceName ]
- let dname = filterDeviceName(deviceName).toLowerCase()
-
- if (!(dname in name_id_map)) {
- continue
- }
-
- runtimes.forEach(cur(dname))
- }
- return list
- },
- // jscs:enable disallowUnusedParams
-
- // jscs:disable disallowUnusedParams
- showdevicetypes: function (args) {
- let output = ''
- this.getdevicetypes().forEach(function (device) {
- output += util.format('%s\n', device)
- })
-
- return output
- },
- // jscs:enable disallowUnusedParams
-
- launch: function (app_path, devicetypeid, log, exit, setenv, argv) {
- let wait_for_debugger = false
- let info_plist_path
- let app_identifier
-
- info_plist_path = path.join(app_path, 'Info.plist')
- if (!fs.existsSync(info_plist_path)) {
- console.error(info_plist_path + ' file not found.')
- process.exit(1)
- }
-
- bplist.parseFile(info_plist_path, function (err, obj) {
- if (err) {
- // try to see if a regular plist parser will work
- if (!plist) {
- plist = require('plist')
- }
- obj = plist.parse(fs.readFileSync(info_plist_path, 'utf8'))
- if (obj) {
- app_identifier = obj.CFBundleIdentifier
- } else {
- throw err
- }
- } else {
- app_identifier = obj[0].CFBundleIdentifier
- }
-
- argv = argv || []
- setenv = setenv || []
-
- let environmentVariables = parseEnvironmentVariables(setenv)
-
- withInjectedEnvironmentVariablesToProcess(process, environmentVariables, function () {
- // get the deviceid from --devicetypeid
- // --devicetypeid is a string in the form "devicetype, runtime_version" (optional: runtime_version)
- let device = getDeviceFromDeviceTypeId(devicetypeid)
-
- // log device information
- console.log(util.format('device.name: %s', device.name))
- console.log(util.format('device.runtime: %s', device.runtime))
- console.log(util.format('device.id: %s', device.id))
-
- // so now we have the deviceid, we can proceed
- simctl.extensions.start(device.id)
- simctl.install(device.id, app_path)
- simctl.launch(wait_for_debugger, device.id, app_identifier, argv)
- simctl.extensions.log(device.id, log)
- if (log) {
- console.log(util.format('logPath: %s', path.resolve(log)))
- }
- if (exit) {
- process.exit(0)
- }
- })
- })
- },
-
- install: function (app_path, devicetypeid, log, exit) {
- let info_plist_path
-
- info_plist_path = path.join(app_path, 'Info.plist')
- if (!fs.existsSync(info_plist_path)) {
- console.error(info_plist_path + ' file not found.')
- process.exit(1)
- }
-
- bplist.parseFile(info_plist_path, function (err, obj) {
- if (err) {
- throw err
- }
-
- // get the deviceid from --devicetypeid
- // --devicetypeid is a string in the form "devicetype, runtime_version" (optional: runtime_version)
- let device = getDeviceFromDeviceTypeId(devicetypeid)
-
- // so now we have the deviceid, we can proceed
- simctl.extensions.start(device.id)
- simctl.install(device.id, app_path)
-
- simctl.extensions.log(device.id, log)
- if (log) {
- console.log(util.format('logPath: %s', path.resolve(log)))
- }
- if (exit) {
- process.exit(0)
- }
- })
- },
-
- start: function (devicetypeid) {
- let device = getDeviceFromDeviceTypeId(devicetypeid)
- simctl.extensions.start(device.id)
- },
-
- _parseEnvironmentVariables: parseEnvironmentVariables
-
- }
-
- module.exports = lib
|