Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 33 additions & 14 deletions lib/listDevices.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,45 @@
under the License.
*/

const execa = require('execa');

const DEVICE_REGEX = /-o (iPhone|iPad|iPod)@.*?"USB Serial Number" = "([^"]*)"/gs;
const childProcess = require('node:child_process');
const devicectl = require('devicectl');

/**
* Gets list of connected iOS devices
* @return {Promise} Promise fulfilled with list of available iOS devices
* Gets list of available iOS devices for deployment.
* @return {Promise} Promise fulfilled with list of available iOS devices.
*/
function listDevices () {
return execa('ioreg', ['-p', 'IOUSB', '-l'])
.then(({ stdout }) => {
return [...matchAll(stdout, DEVICE_REGEX)]
.map(m => m.slice(1).reverse().join(' '));
});
const availableDevices = listFromDeviceCtl();
const connectedDevices = listFromUSB();

// We prefer devicectl for newer devices, so filter out any duplicate
// devices from the ioreg list.
// Sadly the UDID format is very slightly different between the two...
const targets = availableDevices.map(d => d.target.replace('-', ''));
const devices = [].concat(availableDevices, connectedDevices.filter(d => !targets.includes(d.target)));

return Promise.resolve(devices.map(d => `${d.target} ${d.name}`));
}

// TODO: Should be replaced with String#matchAll once available
function * matchAll (s, r) {
let match;
while ((match = r.exec(s))) yield match;
function listFromDeviceCtl () {
const result = devicectl.list().json.result.devices;

return result
.filter((d) => d.connectionProperties.transportType === 'wired')
.map((d) => ({
target: d.hardwareProperties.udid,
name: `${d.deviceProperties.name} (${d.hardwareProperties.marketingName}, ${d.deviceProperties.osVersionNumber})`
}));
}

const DEVICE_REGEX = /-o (iPhone|iPad|iPod)@.*?"USB Serial Number" = "([^"]*)"/gs;
function listFromUSB () {
const result = childProcess.spawnSync('ioreg', ['-p', 'IOUSB', '-l'], { encoding: 'utf8' });

return [...result.stdout.matchAll(DEVICE_REGEX)].map((m) => ({
target: m.pop(),
name: m.slice(1).reverse().join(' ')
}));
}

exports.run = listDevices;
105 changes: 66 additions & 39 deletions lib/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const path = require('node:path');
const bplist = require('bplist-parser');
const plist = require('plist');
const execa = require('execa');
const devicectl = require('devicectl');
const { CordovaError, events } = require('cordova-common');
const build = require('./build');
const check_reqs = require('./check_reqs');
Expand Down Expand Up @@ -64,7 +65,11 @@ module.exports.run = function (runOptions) {
// we also explicitly set device flag in options as we pass
// those parameters to other api (build as an example)
runOptions.device = true;
return check_reqs.check_ios_deploy();
runOptions.target = devices[0].split(' ')[0];

if (!runOptions.target.includes('-')) {
return check_reqs.check_ios_deploy();
}
}
});
}
Expand All @@ -90,7 +95,7 @@ module.exports.run = function (runOptions) {
const buildOutputDir = path.join(projectPath, 'build', `${configuration}-iphoneos`);
const appPath = path.join(buildOutputDir, `${productName}.app`);

return module.exports.checkDeviceConnected()
return module.exports.checkDeviceConnected(runOptions.target)
.then(() => {
// Unpack IPA
const ipafile = path.join(buildOutputDir, `${productName}.ipa`);
Expand Down Expand Up @@ -172,8 +177,20 @@ function filterSupportedArgs (args) {
* Checks if any iOS device is connected
* @return {Promise} Fullfilled when any device is connected, rejected otherwise
*/
function checkDeviceConnected () {
return execa('ios-deploy', ['-c', '-t', '1'], { stdio: 'inherit' });
function checkDeviceConnected (target = '') {
if (target.includes('-')) {
const details = devicectl.info(devicectl.InfoTypes.Details, target).json?.result;

return new Promise((resolve, reject) => {
if (details?.connectionProperties?.transportType === 'wired') {
resolve();
} else {
reject(new CordovaError(`Run destination device '${target}' is not connected.`));
}
});
} else {
return execa('ios-deploy', ['-c', '-t', '1'], { stdio: 'inherit' });
}
}

/**
Expand All @@ -184,13 +201,19 @@ function checkDeviceConnected () {
*/
function deployToDevice (appPath, target, extraArgs) {
events.emit('log', 'Deploying to device');
const args = ['--justlaunch', '-d', '-b', appPath];
if (target) {
args.push('-i', target);
if (target.includes('-')) {
devicectl.install(target, appPath, { stdio: 'inherit' });
return getBundleIdentifierFromApp(appPath)
.then(appIdentifier => devicectl.launch(target, appIdentifier, extraArgs, { stdio: 'inherit', console: true }));
} else {
args.push('--no-wifi');
const args = ['--justlaunch', '-d', '-b', appPath];
if (target) {
args.push('-i', target);
} else {
args.push('--no-wifi');
}
return execa('ios-deploy', args.concat(extraArgs), { stdio: 'inherit' });
}
return execa('ios-deploy', args.concat(extraArgs), { stdio: 'inherit' });
}

/**
Expand Down Expand Up @@ -223,48 +246,52 @@ async function deployToSim (appPath, target) {
return module.exports.startSim(appPath, target);
}

async function startSim (appPath, target) {
const projectPath = path.join(path.dirname(appPath), '../..');
const logPath = path.join(projectPath, 'cordova/console.log');
const deviceTypeId = `com.apple.CoreSimulator.SimDeviceType.${target}`;

try {
const infoPlistPath = path.join(appPath, 'Info.plist');
if (!fs.existsSync(infoPlistPath)) {
throw new Error(`${infoPlistPath} file not found.`);
}

bplist.parseFile(infoPlistPath, function (err, obj) {
let appIdentifier;
function getBundleIdentifierFromApp (appPath) {
const infoPlistPath = path.join(appPath, 'Info.plist');
if (!fs.existsSync(infoPlistPath)) {
return Promise.reject(new Error(`${infoPlistPath} file not found.`));
}

return new Promise((resolve, reject) => {
bplist.parseFile(infoPlistPath, (err, obj) => {
if (err) {
obj = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'));
if (obj) {
appIdentifier = obj.CFBundleIdentifier;
return resolve(obj.CFBundleIdentifier);
} else {
throw err;
return reject(err);
}
} else {
appIdentifier = obj[0].CFBundleIdentifier;
return resolve(obj[0].CFBundleIdentifier);
}
});
});
}

// get the deviceid from --devicetypeid
// --devicetypeid is a string in the form "devicetype, runtime_version" (optional: runtime_version)
const device = getDeviceFromDeviceTypeId(deviceTypeId);
async function startSim (appPath, target) {
const projectPath = path.join(path.dirname(appPath), '../..');
const logPath = path.join(projectPath, 'cordova/console.log');
const deviceTypeId = `com.apple.CoreSimulator.SimDeviceType.${target}`;

// so now we have the deviceid, we can proceed
try {
startSimulator(device, { appPath, appIdentifier, logPath, waitForDebugger: false });
} catch (e) {
events.emit('warn', `Failed to start simulator with error: "${e.message}"`);
}
try {
const appIdentifier = await getBundleIdentifierFromApp(appPath);

if (logPath) {
events.emit('log', `Log Path: ${path.resolve(logPath)}`);
}
// get the deviceid from --devicetypeid
// --devicetypeid is a string in the form "devicetype, runtime_version" (optional: runtime_version)
const device = getDeviceFromDeviceTypeId(deviceTypeId);

process.exit(0);
});
// so now we have the deviceid, we can proceed
try {
startSimulator(device, { appPath, appIdentifier, logPath, waitForDebugger: false });
} catch (e) {
events.emit('warn', `Failed to start simulator with error: "${e.message}"`);
}

if (logPath) {
events.emit('log', `Log Path: ${path.resolve(logPath)}`);
}

process.exit(0);
} catch (e) {
events.emit('warn', `Failed to launch simulator with error: ${e.message}`);
process.exit(1);
Expand Down
Loading
Loading