Skip to content

Commit df4fab5

Browse files
committed
WIP: feat(devicectl): Use devicectl for deploying to devices
1 parent d39c787 commit df4fab5

File tree

7 files changed

+288
-118
lines changed

7 files changed

+288
-118
lines changed

lib/listDevices.js

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,61 @@
1-
/*
2-
Licensed to the Apache Software Foundation (ASF) under one
3-
or more contributor license agreements. See the NOTICE file
4-
distributed with this work for additional information
5-
regarding copyright ownership. The ASF licenses this file
6-
to you under the Apache License, Version 2.0 (the
7-
"License"); you may not use this file except in compliance
8-
with the License. You may obtain a copy of the License at
9-
10-
http://www.apache.org/licenses/LICENSE-2.0
11-
12-
Unless required by applicable law or agreed to in writing,
13-
software distributed under the License is distributed on an
14-
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15-
KIND, either express or implied. See the License for the
16-
specific language governing permissions and limitations
17-
under the License.
18-
*/
1+
/**
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
199
20-
const execa = require('execa');
10+
http://www.apache.org/licenses/LICENSE-2.0
2111
22-
const DEVICE_REGEX = /-o (iPhone|iPad|iPod)@.*?"USB Serial Number" = "([^"]*)"/gs;
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
*/
19+
20+
const childProcess = require('node:child_process');
21+
const devicectl = require('devicectl');
2322

2423
/**
25-
* Gets list of connected iOS devices
26-
* @return {Promise} Promise fulfilled with list of available iOS devices
24+
* Gets list of available iOS devices for deployment.
25+
* @return {Promise} Promise fulfilled with list of available iOS devices.
2726
*/
2827
function listDevices () {
29-
return execa('ioreg', ['-p', 'IOUSB', '-l'])
30-
.then(({ stdout }) => {
31-
return [...matchAll(stdout, DEVICE_REGEX)]
32-
.map(m => m.slice(1).reverse().join(' '));
33-
});
28+
const availableDevices = listFromDeviceCtl();
29+
const connectedDevices = listFromUSB();
30+
31+
// We prefer devicectl for newer devices, so filter out any duplicate
32+
// devices from the ioreg list.
33+
// Sadly the UDID format is very slightly different between the two...
34+
const targets = availableDevices.map(d => d.target.replace('-', ''));
35+
const devices = [].concat(availableDevices, connectedDevices.filter(d => !targets.includes(d.target)));
36+
37+
return Promise.resolve(devices.map(d => `${d.target} ${d.name}`));
38+
}
39+
40+
function listFromDeviceCtl () {
41+
const result = devicectl.list().json.result.devices;
42+
43+
return result
44+
.filter((d) => d.connectionProperties.transportType === 'wired')
45+
.map((d) => ({
46+
target: d.hardwareProperties.udid,
47+
name: `${d.deviceProperties.name} (${d.hardwareProperties.marketingName}, ${d.deviceProperties.osVersionNumber})`
48+
}));
3449
}
3550

36-
// TODO: Should be replaced with String#matchAll once available
37-
function * matchAll (s, r) {
38-
let match;
39-
while ((match = r.exec(s))) yield match;
51+
const DEVICE_REGEX = /-o (iPhone|iPad|iPod)@.*?"USB Serial Number" = "([^"]*)"/gs;
52+
function listFromUSB () {
53+
const result = childProcess.spawnSync('ioreg', ['-p', 'IOUSB', '-l'], { encoding: 'utf8' });
54+
55+
return [...result.stdout.matchAll(DEVICE_REGEX)].map((m) => ({
56+
target: m.pop(),
57+
name: m.slice(1).reverse().join(' ')
58+
}));
4059
}
4160

4261
exports.run = listDevices;

lib/run.js

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const path = require('node:path');
2222
const bplist = require('bplist-parser');
2323
const plist = require('plist');
2424
const execa = require('execa');
25+
const devicectl = require('devicectl');
2526
const { CordovaError, events } = require('cordova-common');
2627
const build = require('./build');
2728
const check_reqs = require('./check_reqs');
@@ -64,7 +65,11 @@ module.exports.run = function (runOptions) {
6465
// we also explicitly set device flag in options as we pass
6566
// those parameters to other api (build as an example)
6667
runOptions.device = true;
67-
return check_reqs.check_ios_deploy();
68+
runOptions.target = devices[0].split(' ')[0];
69+
70+
if (!runOptions.target.includes('-')) {
71+
return check_reqs.check_ios_deploy();
72+
}
6873
}
6974
});
7075
}
@@ -90,7 +95,7 @@ module.exports.run = function (runOptions) {
9095
const buildOutputDir = path.join(projectPath, 'build', `${configuration}-iphoneos`);
9196
const appPath = path.join(buildOutputDir, `${productName}.app`);
9297

93-
return module.exports.checkDeviceConnected()
98+
return module.exports.checkDeviceConnected(runOptions.target)
9499
.then(() => {
95100
// Unpack IPA
96101
const ipafile = path.join(buildOutputDir, `${productName}.ipa`);
@@ -172,8 +177,13 @@ function filterSupportedArgs (args) {
172177
* Checks if any iOS device is connected
173178
* @return {Promise} Fullfilled when any device is connected, rejected otherwise
174179
*/
175-
function checkDeviceConnected () {
176-
return execa('ios-deploy', ['-c', '-t', '1'], { stdio: 'inherit' });
180+
function checkDeviceConnected (target = '') {
181+
if (target.includes('-')) {
182+
const details = devicectl.info(devicectl.InfoTypes.Details, target).json?.result;
183+
return details?.connectionProperties?.transportType === 'wired';
184+
} else {
185+
return execa('ios-deploy', ['-c', '-t', '1'], { stdio: 'inherit' });
186+
}
177187
}
178188

179189
/**
@@ -184,13 +194,19 @@ function checkDeviceConnected () {
184194
*/
185195
function deployToDevice (appPath, target, extraArgs) {
186196
events.emit('log', 'Deploying to device');
187-
const args = ['--justlaunch', '-d', '-b', appPath];
188-
if (target) {
189-
args.push('-i', target);
197+
if (target.includes('-')) {
198+
return devicectl.install(target, appPath)
199+
.then(() => getBundleIdentifierFromApp(appPath))
200+
.then(appIdentifier => devicectl.launch(target, appIdentifier, extraArgs));
190201
} else {
191-
args.push('--no-wifi');
202+
const args = ['--justlaunch', '-d', '-b', appPath];
203+
if (target) {
204+
args.push('-i', target);
205+
} else {
206+
args.push('--no-wifi');
207+
}
208+
return execa('ios-deploy', args.concat(extraArgs), { stdio: 'inherit' });
192209
}
193-
return execa('ios-deploy', args.concat(extraArgs), { stdio: 'inherit' });
194210
}
195211

196212
/**
@@ -223,48 +239,52 @@ async function deployToSim (appPath, target) {
223239
return module.exports.startSim(appPath, target);
224240
}
225241

226-
async function startSim (appPath, target) {
227-
const projectPath = path.join(path.dirname(appPath), '../..');
228-
const logPath = path.join(projectPath, 'cordova/console.log');
229-
const deviceTypeId = `com.apple.CoreSimulator.SimDeviceType.${target}`;
230-
231-
try {
232-
const infoPlistPath = path.join(appPath, 'Info.plist');
233-
if (!fs.existsSync(infoPlistPath)) {
234-
throw new Error(`${infoPlistPath} file not found.`);
235-
}
236-
237-
bplist.parseFile(infoPlistPath, function (err, obj) {
238-
let appIdentifier;
242+
function getBundleIdentifierFromApp (appPath) {
243+
const infoPlistPath = path.join(appPath, 'Info.plist');
244+
if (!fs.existsSync(infoPlistPath)) {
245+
return Promise.reject(new Error(`${infoPlistPath} file not found.`));
246+
}
239247

248+
return new Promise((resolve, reject) => {
249+
bplist.parseFile(infoPlistPath, (err, obj) => {
240250
if (err) {
241251
obj = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'));
242252
if (obj) {
243-
appIdentifier = obj.CFBundleIdentifier;
253+
return resolve(obj.CFBundleIdentifier);
244254
} else {
245-
throw err;
255+
return reject(err);
246256
}
247257
} else {
248-
appIdentifier = obj[0].CFBundleIdentifier;
258+
return resolve(obj[0].CFBundleIdentifier);
249259
}
260+
});
261+
});
262+
}
250263

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

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

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

266-
process.exit(0);
267-
});
276+
// so now we have the deviceid, we can proceed
277+
try {
278+
startSimulator(device, { appPath, appIdentifier, logPath, waitForDebugger: false });
279+
} catch (e) {
280+
events.emit('warn', `Failed to start simulator with error: "${e.message}"`);
281+
}
282+
283+
if (logPath) {
284+
events.emit('log', `Log Path: ${path.resolve(logPath)}`);
285+
}
286+
287+
process.exit(0);
268288
} catch (e) {
269289
events.emit('warn', `Failed to launch simulator with error: ${e.message}`);
270290
process.exit(1);

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,15 @@
3535
"engines": {
3636
"node": "^20.17.0 || >=22.9.0"
3737
},
38+
"exports": {
39+
".": "./lib/Api.js",
40+
"./package.json": "./package.json",
41+
"./lib/*": "./lib/*.js"
42+
},
3843
"dependencies": {
3944
"bplist-parser": "^0.3.2",
4045
"cordova-common": "^6.0.0",
46+
"devicectl": "^0.0.1",
4147
"elementtree": "^0.1.7",
4248
"execa": "^5.1.1",
4349
"nopt": "^9.0.0",

tests/spec/fixtures/sample-ioreg-output.txt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,72 @@
2424
| "IOKitDiagnostics" = {"Foobar"="Foobar"}
2525
| }
2626
|
27+
+-o AppleT6000USBXHCI@02000000 <class AppleT6000USBXHCI, id 0x100000473, registered, matched, active, busy 0 (598 ms), retain 98>
28+
| | {
29+
| | "IOClass" = "AppleT6000USBXHCI"
30+
| | "kUSBSleepPortCurrentLimit" = 3000
31+
| | "UsbHostControllerSoftRetryPolicy" = 0
32+
| | "IOPersonalityPublisher" = "com.apple.driver.usb.AppleSynopsysUSB40XHCI"
33+
| | "IOMatchedAtBoot" = Yes
34+
| | "IOPowerManagement" = {"ChildrenPowerState"=3,"DevicePowerState"=0,"CurrentPowerState"=3,"CapabilityFlags"=32768,"MaxPowerState"=3,"DriverPowerState"=0}
35+
| | "IOProviderClass" = "AppleARMIODevice"
36+
| | "IOProbeScore" = 10002
37+
| | "locationID" = 33554432
38+
| | "kUSBWakePortCurrentLimit" = 3000
39+
| | "IONameMatch" = "usb-drd,t6000"
40+
| | "CFBundleIdentifierKernel" = "com.apple.driver.usb.AppleSynopsysUSB40XHCI"
41+
| | "UsbHostControllerDeferRegisterService" = Yes
42+
| | "IOMatchCategory" = "usb-host"
43+
| | "CFBundleIdentifier" = "com.apple.driver.usb.AppleSynopsysUSB40XHCI"
44+
| | "Revision" = <0103>
45+
| | "IONameMatched" = "usb-drd,t6000"
46+
| | "UsbHostControllerUSB4LPMPolicy" = 1
47+
| | "UsbHostControllerTierLimit" = 6
48+
| | "controller-statistics" = {"kControllerStatIOCount"=46575,"kControllerStatPowerStateTime"={"kPowerStateInitialize"="0ms (0%)","kPowerStateOff"="327655037ms (97%)","kPowerStateSle$
49+
| | "kUSBSleepSupported" = Yes
50+
| | }
51+
| |
52+
| +-o iPad@02100000 <class IOUSBHostDevice, id 0x1003271c8, registered, matched, active, busy 0 (276 ms), retain 62>
53+
| {
54+
| "kUSBSerialNumberString" = "IPADMINI_UDID"
55+
| "bDeviceClass" = 0
56+
| "UsbAppleDeviceECID" = 6259374711472158
57+
| "bDeviceSubClass" = 0
58+
| "iSerialNumber" = 3
59+
| "UsbPowerSinkCapability" = 2400
60+
| "IOServiceDEXTEntitlements" = (("com.apple.developer.driverkit.transport.usb"))
61+
| "iProduct" = 2
62+
| "USB Serial Number" = "IPADMINI_UDID"
63+
| "USB Vendor Name" = "Apple Inc."
64+
| "USBSpeed" = 3
65+
| "IOPowerManagement" = {"PowerOverrideOn"=Yes,"DevicePowerState"=2,"CurrentPowerState"=2,"CapabilityFlags"=32768,"MaxPowerState"=2,"DriverPowerState"=0}
66+
| "bNumConfigurations" = 5
67+
| "kUSBProductString" = "iPad"
68+
| "kCDCDoNotMatchThisDevice" = Yes
69+
| "kCallInterfaceOpenWithGate" = Yes
70+
| "kUSBVendorString" = "Apple Inc."
71+
| "USB Product Name" = "iPad"
72+
| "iManufacturer" = 1
73+
| "idVendor" = 1452
74+
| "Device Speed" = 2
75+
| "kUSBCurrentConfiguration" = 5
76+
| "idProduct" = 4779
77+
| "bcdDevice" = 5121
78+
| "UsbDeviceSignature" = <ac05ab120114303030303831313030303136334344453345333138303145000000060101010100010200030000fffe02fffd01020d000a0001>
79+
| "sessionID" = 7864622857660
80+
| "USB Address" = 1
81+
| "kUSBPreferredConfiguration" = 3
82+
| "IOCFPlugInTypes" = {"9dc7b780-9ec0-11d4-a54f-000a27052861"="IOUSBHostFamily.kext/Contents/PlugIns/IOUSBLib.bundle"}
83+
| "SupportsIPhoneOS" = Yes
84+
| "USBPortType" = 0
85+
| "bDeviceProtocol" = 0
86+
| "locationID" = 34603008
87+
| "kUSBAddress" = 1
88+
| "bcdUSB" = 528
89+
| "IOGeneralInterest" = "IOCommand is not serializable"
90+
| "bMaxPacketSize0" = 64
91+
| }
92+
|
2793
+-o AppleUSBXHCI Root Hub Simulation@14000000 <class AppleUSBRootHubDevice, id 0x100020cab, registered, matched, active, busy 0 (2 ms), retain 9>
2894
| {
2995
| "iManufacturer" = 0

0 commit comments

Comments
 (0)