appium-xcuitest-driver 11.4.1 → 11.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/sign-wda.mjs +415 -259
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [11.4.2](https://github.com/appium/appium-xcuitest-driver/compare/v11.4.1...v11.4.2) (2026-05-15)
|
|
2
|
+
|
|
3
|
+
### Miscellaneous Chores
|
|
4
|
+
|
|
5
|
+
* Refactor wda signer code ([#2839](https://github.com/appium/appium-xcuitest-driver/issues/2839)) ([3aafb36](https://github.com/appium/appium-xcuitest-driver/commit/3aafb3600a103a129d4b9dd4452e4c9c4e1223b1))
|
|
6
|
+
|
|
1
7
|
## [11.4.1](https://github.com/appium/appium-xcuitest-driver/compare/v11.4.0...v11.4.1) (2026-05-15)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-xcuitest-driver",
|
|
3
|
-
"version": "11.4.
|
|
3
|
+
"version": "11.4.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "appium-xcuitest-driver",
|
|
9
|
-
"version": "11.4.
|
|
9
|
+
"version": "11.4.2",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@appium/strongbox": "^1.0.0-rc.1",
|
package/package.json
CHANGED
package/scripts/sign-wda.mjs
CHANGED
|
@@ -2,11 +2,12 @@ import {fs, logger} from 'appium/support.js';
|
|
|
2
2
|
import {exec} from 'teen_process';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {pathToFileURL} from 'node:url';
|
|
5
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
6
6
|
import {mkdtemp, rm} from 'node:fs/promises';
|
|
7
7
|
import {Command} from 'commander';
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const scriptFilePath = fileURLToPath(import.meta.url);
|
|
10
|
+
const SCRIPT_NAME = path.basename(scriptFilePath, path.extname(scriptFilePath));
|
|
10
11
|
const RESIGNER_BINARY_NAME = 'resigner';
|
|
11
12
|
const MOBILEPROVISION_EXTENSION = '.mobileprovision';
|
|
12
13
|
const DEFAULT_PROFILE_DIR_CANDIDATES = [
|
|
@@ -21,12 +22,76 @@ const DEFAULT_WDA_BUNDLE_IDS = [
|
|
|
21
22
|
|
|
22
23
|
const log = logger.getLogger(SCRIPT_NAME);
|
|
23
24
|
|
|
24
|
-
class
|
|
25
|
+
class Resigner {
|
|
26
|
+
/** @type {string} */
|
|
27
|
+
_wdaPath;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} wdaPath Path to the WebDriverAgent `.app` bundle.
|
|
31
|
+
*/
|
|
32
|
+
constructor(wdaPath) {
|
|
33
|
+
this._wdaPath = wdaPath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {SignOptions} options
|
|
38
|
+
* @returns {Promise<void>}
|
|
39
|
+
*/
|
|
40
|
+
async signWDA(options) {
|
|
41
|
+
await this._requireBinary();
|
|
42
|
+
const args = this._buildSignArgs(options);
|
|
43
|
+
log.info(`Running resigner to sign ${this._wdaPath}`);
|
|
44
|
+
await exec(RESIGNER_BINARY_NAME, args, {
|
|
45
|
+
env: {
|
|
46
|
+
...process.env,
|
|
47
|
+
P12_PASSWORD: options.p12Password,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
log.info('WDA signed successfully');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @returns {Promise<string>}
|
|
55
|
+
*/
|
|
56
|
+
async inspectWDA() {
|
|
57
|
+
await this._requireBinary();
|
|
58
|
+
log.info(`Inspecting signed WDA at ${this._wdaPath}`);
|
|
59
|
+
const {stdout} = await exec(RESIGNER_BINARY_NAME, ['--inspect', this._wdaPath]);
|
|
60
|
+
return String(stdout || '').trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
25
63
|
/**
|
|
26
|
-
*
|
|
27
|
-
* @returns {
|
|
28
|
-
|
|
29
|
-
|
|
64
|
+
* @param {SignOptions} options
|
|
65
|
+
* @returns {string[]}
|
|
66
|
+
*/
|
|
67
|
+
_buildSignArgs(options) {
|
|
68
|
+
const args = [
|
|
69
|
+
'--p12-file', options.p12File,
|
|
70
|
+
'--profile', options.profileDir,
|
|
71
|
+
'--force',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
if (options.bundleId) {
|
|
75
|
+
args.push(
|
|
76
|
+
...[
|
|
77
|
+
// To re-apply the same mapping again for past failure cases for safety.
|
|
78
|
+
options.bundleId,
|
|
79
|
+
...DEFAULT_WDA_BUNDLE_IDS,
|
|
80
|
+
].flatMap((bundleId) => [
|
|
81
|
+
'--bundle-id-remap',
|
|
82
|
+
`${bundleId}=${options.bundleId}`,
|
|
83
|
+
])
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
args.push(this._wdaPath);
|
|
88
|
+
return args;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @returns {Promise<void>}
|
|
93
|
+
*/
|
|
94
|
+
async _requireBinary() {
|
|
30
95
|
try {
|
|
31
96
|
await fs.which(RESIGNER_BINARY_NAME);
|
|
32
97
|
} catch {
|
|
@@ -35,275 +100,351 @@ class RunCmd {
|
|
|
35
100
|
}
|
|
36
101
|
}
|
|
37
102
|
|
|
103
|
+
class ProvisioningProfilesHelper {
|
|
104
|
+
/** @type {string | undefined} */
|
|
105
|
+
_profileDir;
|
|
38
106
|
|
|
39
|
-
class RunInspectWDA extends RunCmd {
|
|
40
107
|
/**
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
108
|
+
* @param {string | undefined} profileDir Explicit directory, or `undefined` to auto-discover.
|
|
109
|
+
*/
|
|
110
|
+
constructor(profileDir) {
|
|
111
|
+
this._profileDir = profileDir;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @returns {Promise<string>}
|
|
44
116
|
*/
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
|
|
117
|
+
async resolveRoot() {
|
|
118
|
+
const profileDir = this._profileDir;
|
|
119
|
+
if (profileDir) {
|
|
120
|
+
return await this._validate(profileDir, 'Provided');
|
|
48
121
|
}
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
122
|
+
|
|
123
|
+
for (const candidate of DEFAULT_PROFILE_DIR_CANDIDATES) {
|
|
124
|
+
if (!(await fs.exists(candidate))) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
await this._validate(candidate, 'Discovered');
|
|
129
|
+
log.info(`Using discovered provisioning profile directory: ${candidate}`);
|
|
130
|
+
return candidate;
|
|
131
|
+
} catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
55
134
|
}
|
|
135
|
+
|
|
136
|
+
throw new Error(
|
|
137
|
+
`No provisioning profile directory could be discovered. ` +
|
|
138
|
+
`Please provide --profile-dir explicitly. Checked: ${DEFAULT_PROFILE_DIR_CANDIDATES.join(', ')}`
|
|
139
|
+
);
|
|
56
140
|
}
|
|
57
|
-
}
|
|
58
141
|
|
|
59
|
-
class RunSignWDA extends RunCmd {
|
|
60
142
|
/**
|
|
61
|
-
*
|
|
62
|
-
* @param {
|
|
63
|
-
* @returns {Promise<
|
|
143
|
+
* @param {string} dir
|
|
144
|
+
* @param {string} source
|
|
145
|
+
* @returns {Promise<string>}
|
|
64
146
|
*/
|
|
65
|
-
async
|
|
66
|
-
if (!(await fs.exists(
|
|
67
|
-
throw new Error(
|
|
147
|
+
async _validate(dir, source) {
|
|
148
|
+
if (!(await fs.exists(dir))) {
|
|
149
|
+
throw new Error(`${source} provisioning profile directory does not exist: ${dir}`);
|
|
68
150
|
}
|
|
69
|
-
await this.requireResignerBinary();
|
|
70
|
-
const resolvedProfileDir = await resolveProfileDir(options.profileDir);
|
|
71
|
-
|
|
72
|
-
let p12File = options.p12File;
|
|
73
|
-
let tempDir;
|
|
74
|
-
let p12Password = options.p12Password;
|
|
75
151
|
|
|
152
|
+
let entries;
|
|
76
153
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
p12File = result.p12File;
|
|
82
|
-
tempDir = result.tempDir;
|
|
83
|
-
p12Password = generatedPassword;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!p12File) {
|
|
87
|
-
throw new Error('No p12 file available for signing');
|
|
88
|
-
}
|
|
154
|
+
entries = await fs.readdir(dir);
|
|
155
|
+
} catch {
|
|
156
|
+
throw new Error(`${source} provisioning profile directory is not a readable directory: ${dir}`);
|
|
157
|
+
}
|
|
89
158
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
bundleId: options.bundleId,
|
|
95
|
-
});
|
|
96
|
-
} finally {
|
|
97
|
-
// Clean up temp directory if it was created
|
|
98
|
-
if (tempDir) {
|
|
99
|
-
try {
|
|
100
|
-
await rm(tempDir, {recursive: true, force: true});
|
|
101
|
-
log.info(`Cleaned up temporary directory: ${tempDir}`);
|
|
102
|
-
} catch (err) {
|
|
103
|
-
log.warn(`Failed to clean up temporary directory ${tempDir}: ${err.message}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
159
|
+
if (!entries.some((name) => name.toLowerCase().endsWith(MOBILEPROVISION_EXTENSION))) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`${source} provisioning profile directory does not contain any ${MOBILEPROVISION_EXTENSION} files: ${dir}`
|
|
162
|
+
);
|
|
106
163
|
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
164
|
|
|
110
|
-
|
|
111
|
-
* Generate a random password for temporary .p12 files.
|
|
112
|
-
* @returns {string} A random 12-character alphanumeric password
|
|
113
|
-
*/
|
|
114
|
-
function generateRandomPassword() {
|
|
115
|
-
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
116
|
-
let password = '';
|
|
117
|
-
for (let i = 0; i < 12; i++) {
|
|
118
|
-
password += chars[Math.floor(Math.random() * chars.length)];
|
|
165
|
+
return dir;
|
|
119
166
|
}
|
|
120
|
-
return password;
|
|
121
167
|
}
|
|
122
168
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
169
|
+
class P12Converter {
|
|
170
|
+
/** @type {string} */
|
|
171
|
+
_certPath;
|
|
172
|
+
/** @type {string} */
|
|
173
|
+
_keyPath;
|
|
174
|
+
/** @type {string} */
|
|
175
|
+
_p12Password;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {string} certPath
|
|
179
|
+
* @param {string} keyPath
|
|
180
|
+
* @param {string} p12Password
|
|
181
|
+
*/
|
|
182
|
+
constructor(certPath, keyPath, p12Password) {
|
|
183
|
+
this._certPath = certPath;
|
|
184
|
+
this._keyPath = keyPath;
|
|
185
|
+
this._p12Password = p12Password;
|
|
134
186
|
}
|
|
135
|
-
|
|
136
|
-
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @returns {string}
|
|
190
|
+
*/
|
|
191
|
+
static generateRandomPassword() {
|
|
192
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
193
|
+
let password = '';
|
|
194
|
+
for (let i = 0; i < 12; i++) {
|
|
195
|
+
password += chars[Math.floor(Math.random() * chars.length)];
|
|
196
|
+
}
|
|
197
|
+
return password;
|
|
137
198
|
}
|
|
138
199
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
200
|
+
/**
|
|
201
|
+
* @returns {Promise<{p12File: string, tempDir: string}>}
|
|
202
|
+
*/
|
|
203
|
+
async convert() {
|
|
204
|
+
await this._assertCertAndKeyExist();
|
|
205
|
+
await this._requireOpenSsl();
|
|
206
|
+
|
|
207
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'wda-sign-'));
|
|
208
|
+
const certPemPath = path.join(tempDir, 'certificate.pem');
|
|
209
|
+
const p12FilePath = path.join(tempDir, 'certificate.p12');
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await this._convertCerToPem(certPemPath);
|
|
213
|
+
await this._exportPkcs12(certPemPath, p12FilePath);
|
|
214
|
+
log.info(`Successfully created temporary .p12 file: ${p12FilePath}`);
|
|
215
|
+
return {p12File: p12FilePath, tempDir};
|
|
216
|
+
} catch (err) {
|
|
217
|
+
try {
|
|
218
|
+
await rm(tempDir, {recursive: true, force: true});
|
|
219
|
+
} catch {}
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
144
222
|
}
|
|
145
223
|
|
|
146
|
-
|
|
147
|
-
|
|
224
|
+
/**
|
|
225
|
+
* @returns {Promise<void>}
|
|
226
|
+
*/
|
|
227
|
+
async _assertCertAndKeyExist() {
|
|
228
|
+
const certPath = this._certPath;
|
|
229
|
+
const keyPath = this._keyPath;
|
|
230
|
+
if (!(await fs.exists(certPath))) {
|
|
231
|
+
throw new Error(`Certificate file does not exist: ${certPath}`);
|
|
232
|
+
}
|
|
233
|
+
if (!(await fs.exists(keyPath))) {
|
|
234
|
+
throw new Error(`Private key file does not exist: ${keyPath}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
148
237
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
238
|
+
/**
|
|
239
|
+
* @returns {Promise<void>}
|
|
240
|
+
*/
|
|
241
|
+
async _requireOpenSsl() {
|
|
242
|
+
try {
|
|
243
|
+
await fs.which('openssl');
|
|
244
|
+
} catch {
|
|
245
|
+
throw new Error(
|
|
246
|
+
'OpenSSL binary is not available in the PATH. ' +
|
|
247
|
+
'It is required to convert .cer and .key files to .p12 format.'
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
152
251
|
|
|
153
|
-
|
|
252
|
+
/**
|
|
253
|
+
* @param {string} certPemPath
|
|
254
|
+
* @returns {Promise<void>}
|
|
255
|
+
*/
|
|
256
|
+
async _convertCerToPem(certPemPath) {
|
|
257
|
+
const certPath = this._certPath;
|
|
154
258
|
log.info(`Converting certificate from ${certPath} to PEM format`);
|
|
155
259
|
await exec('openssl', [
|
|
156
260
|
'x509',
|
|
157
261
|
'-in', certPath,
|
|
158
262
|
'-inform', 'DER',
|
|
159
|
-
'-out',
|
|
263
|
+
'-out', certPemPath,
|
|
160
264
|
]);
|
|
265
|
+
}
|
|
161
266
|
|
|
162
|
-
|
|
267
|
+
/**
|
|
268
|
+
* @param {string} certPemPath
|
|
269
|
+
* @param {string} p12FilePath
|
|
270
|
+
* @returns {Promise<void>}
|
|
271
|
+
*/
|
|
272
|
+
async _exportPkcs12(certPemPath, p12FilePath) {
|
|
273
|
+
const keyPath = this._keyPath;
|
|
274
|
+
const p12Password = this._p12Password;
|
|
163
275
|
log.info(`Creating .p12 file from certificate and key`);
|
|
164
276
|
await exec('openssl', [
|
|
165
277
|
'pkcs12',
|
|
166
278
|
'-export',
|
|
167
|
-
'-in',
|
|
279
|
+
'-in', certPemPath,
|
|
168
280
|
'-inkey', keyPath,
|
|
169
|
-
'-out',
|
|
281
|
+
'-out', p12FilePath,
|
|
170
282
|
'-passout', `pass:${p12Password}`,
|
|
171
283
|
]);
|
|
172
|
-
|
|
173
|
-
log.info(`Successfully created temporary .p12 file: ${p12File}`);
|
|
174
|
-
return {p12File, tempDir};
|
|
175
|
-
} catch (err) {
|
|
176
|
-
// Clean up temp dir on error
|
|
177
|
-
try {
|
|
178
|
-
await rm(tempDir, {recursive: true, force: true});
|
|
179
|
-
} catch {}
|
|
180
|
-
throw err;
|
|
181
284
|
}
|
|
182
285
|
}
|
|
183
286
|
|
|
184
287
|
/**
|
|
185
|
-
*
|
|
186
|
-
* @param {string} dir
|
|
187
|
-
* @param {string} source
|
|
188
|
-
* @returns {Promise<string>}
|
|
288
|
+
* Shared helpers for workflows that operate on a WDA `.app` bundle path.
|
|
189
289
|
*/
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
290
|
+
class WdaBundleWorkflow {
|
|
291
|
+
/**
|
|
292
|
+
* @param {string} wdaPath
|
|
293
|
+
* @returns {Promise<void>}
|
|
294
|
+
*/
|
|
295
|
+
async _assertWdaExists(wdaPath) {
|
|
296
|
+
if (!(await fs.exists(wdaPath))) {
|
|
297
|
+
throw new Error(`WDA path does not exist: ${wdaPath}`);
|
|
298
|
+
}
|
|
193
299
|
}
|
|
300
|
+
}
|
|
194
301
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
302
|
+
class SignWdaWorkflow extends WdaBundleWorkflow {
|
|
303
|
+
/**
|
|
304
|
+
* @param {object} [deps]
|
|
305
|
+
* @param {(wdaPath: string) => Resigner} [deps.createResigner]
|
|
306
|
+
* @param {(profileDir: string | undefined) => ProvisioningProfilesHelper} [deps.createProvisioning]
|
|
307
|
+
* @param {(certPath: string, keyPath: string, p12Password: string) => P12Converter} [deps.createP12]
|
|
308
|
+
*/
|
|
309
|
+
constructor(deps = {}) {
|
|
310
|
+
super();
|
|
311
|
+
this._createResigner = deps.createResigner ?? ((wdaPath) => new Resigner(wdaPath));
|
|
312
|
+
this._createProvisioning =
|
|
313
|
+
deps.createProvisioning ?? ((profileDir) => new ProvisioningProfilesHelper(profileDir));
|
|
314
|
+
this._createP12 =
|
|
315
|
+
deps.createP12 ?? ((certPath, keyPath, p12Password) => new P12Converter(certPath, keyPath, p12Password));
|
|
200
316
|
}
|
|
201
317
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
318
|
+
/**
|
|
319
|
+
* @param {SignWDAOptions} options
|
|
320
|
+
* @returns {Promise<void>}
|
|
321
|
+
*/
|
|
322
|
+
async run(options) {
|
|
323
|
+
await this._assertWdaExists(options.wdaPath);
|
|
324
|
+
const resolvedProfileDir = await this._createProvisioning(options.profileDir).resolveRoot();
|
|
207
325
|
|
|
208
|
-
|
|
209
|
-
|
|
326
|
+
let p12File = options.p12File;
|
|
327
|
+
let tempDir;
|
|
328
|
+
let p12Password = options.p12Password;
|
|
210
329
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
330
|
+
try {
|
|
331
|
+
if (options.p12Cert && options.p12Key) {
|
|
332
|
+
const generatedPassword = P12Converter.generateRandomPassword();
|
|
333
|
+
const result = await this._createP12(
|
|
334
|
+
options.p12Cert,
|
|
335
|
+
options.p12Key,
|
|
336
|
+
generatedPassword
|
|
337
|
+
).convert();
|
|
338
|
+
p12File = result.p12File;
|
|
339
|
+
tempDir = result.tempDir;
|
|
340
|
+
p12Password = generatedPassword;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!p12File) {
|
|
344
|
+
throw new Error('No p12 file available for signing');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await this._createResigner(options.wdaPath).signWDA({
|
|
348
|
+
p12File,
|
|
349
|
+
p12Password,
|
|
350
|
+
profileDir: resolvedProfileDir,
|
|
351
|
+
bundleId: options.bundleId,
|
|
352
|
+
});
|
|
353
|
+
} finally {
|
|
354
|
+
await this._cleanupTempDir(tempDir);
|
|
355
|
+
}
|
|
221
356
|
}
|
|
222
357
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
358
|
+
/**
|
|
359
|
+
* @param {string | undefined} tempDir
|
|
360
|
+
* @returns {Promise<void>}
|
|
361
|
+
*/
|
|
362
|
+
async _cleanupTempDir(tempDir) {
|
|
363
|
+
if (!tempDir) {
|
|
364
|
+
return;
|
|
226
365
|
}
|
|
227
366
|
try {
|
|
228
|
-
await
|
|
229
|
-
log.info(`
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
continue;
|
|
367
|
+
await rm(tempDir, {recursive: true, force: true});
|
|
368
|
+
log.info(`Cleaned up temporary directory: ${tempDir}`);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
log.warn(`Failed to clean up temporary directory ${tempDir}: ${err.message}`);
|
|
233
371
|
}
|
|
234
372
|
}
|
|
235
|
-
|
|
236
|
-
throw new Error(
|
|
237
|
-
`No provisioning profile directory could be discovered. ` +
|
|
238
|
-
`Please provide --profile-dir explicitly. Checked: ${DEFAULT_PROFILE_DIR_CANDIDATES.join(', ')}`
|
|
239
|
-
);
|
|
240
373
|
}
|
|
241
374
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
'--p12-file', options.p12File,
|
|
251
|
-
'--profile', options.profileDir,
|
|
252
|
-
'--force',
|
|
253
|
-
];
|
|
254
|
-
|
|
255
|
-
if (options.bundleId) {
|
|
256
|
-
args.push(
|
|
257
|
-
...[
|
|
258
|
-
// To re-apply the same mapping again for past failure cases for safe.
|
|
259
|
-
options.bundleId,
|
|
260
|
-
...DEFAULT_WDA_BUNDLE_IDS,
|
|
261
|
-
].flatMap((bundleId) => [
|
|
262
|
-
'--bundle-id-remap',
|
|
263
|
-
`${bundleId}=${options.bundleId}`,
|
|
264
|
-
])
|
|
265
|
-
);
|
|
375
|
+
class InspectWdaWorkflow extends WdaBundleWorkflow {
|
|
376
|
+
/**
|
|
377
|
+
* @param {object} [deps]
|
|
378
|
+
* @param {(wdaPath: string) => Resigner} [deps.createResigner]
|
|
379
|
+
*/
|
|
380
|
+
constructor(deps = {}) {
|
|
381
|
+
super();
|
|
382
|
+
this._createResigner = deps.createResigner ?? ((wdaPath) => new Resigner(wdaPath));
|
|
266
383
|
}
|
|
267
384
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
385
|
+
/**
|
|
386
|
+
* @param {InspectWDAOptions} options
|
|
387
|
+
* @returns {Promise<void>}
|
|
388
|
+
*/
|
|
389
|
+
async run(options) {
|
|
390
|
+
await this._assertWdaExists(options.wdaPath);
|
|
391
|
+
const inspectResult = await this._createResigner(options.wdaPath).inspectWDA();
|
|
392
|
+
if (inspectResult) {
|
|
393
|
+
log.info(`Resigner inspect result:\n---\n${inspectResult}`);
|
|
394
|
+
} else {
|
|
395
|
+
log.info('Resigner inspect finished, but no output was returned.');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
278
398
|
}
|
|
279
399
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
400
|
+
class SignWdaCli {
|
|
401
|
+
/**
|
|
402
|
+
* @param {object} [deps]
|
|
403
|
+
* @param {SignWdaWorkflow} [deps.signWorkflow]
|
|
404
|
+
* @param {InspectWdaWorkflow} [deps.inspectWorkflow]
|
|
405
|
+
*/
|
|
406
|
+
constructor(deps = {}) {
|
|
407
|
+
this._signWorkflow = deps.signWorkflow ?? new SignWdaWorkflow();
|
|
408
|
+
this._inspectWorkflow = deps.inspectWorkflow ?? new InspectWdaWorkflow();
|
|
409
|
+
}
|
|
290
410
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
411
|
+
/**
|
|
412
|
+
* @param {string[]} argv
|
|
413
|
+
* @returns {Promise<void>}
|
|
414
|
+
*/
|
|
415
|
+
async run(argv) {
|
|
416
|
+
const program = this._createProgram();
|
|
417
|
+
await program.parseAsync(argv);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @returns {Command}
|
|
422
|
+
*/
|
|
423
|
+
_createProgram() {
|
|
424
|
+
const program = new Command();
|
|
425
|
+
|
|
426
|
+
program
|
|
427
|
+
.name('appium driver run xcuitest sign-wda')
|
|
428
|
+
.description('Sign a WebDriverAgentRunner app bundle with code signing certificate')
|
|
429
|
+
.requiredOption('--wda-path <path>', 'Path to the WebDriverAgentRunner.app bundle to sign')
|
|
430
|
+
.option('--inspect', 'Run resigner inspect only (no signing)')
|
|
431
|
+
.option(
|
|
432
|
+
'--p12-file <path>',
|
|
433
|
+
'Path to the .p12 signing certificate file (requires P12_PASSWORD env var; mutually exclusive with --p12-cert/--p12-key)'
|
|
434
|
+
)
|
|
435
|
+
.option(
|
|
436
|
+
'--p12-cert <path>',
|
|
437
|
+
'Path to the .cer certificate file from Apple Developer portal (auto-converted to .p12 with generated password; mutually exclusive with --p12-file; must use with --p12-key)'
|
|
438
|
+
)
|
|
439
|
+
.option(
|
|
440
|
+
'--p12-key <path>',
|
|
441
|
+
'Path to the .key private key file from Apple Developer portal (auto-converted to .p12 with generated password; mutually exclusive with --p12-file; must use with --p12-cert)'
|
|
442
|
+
)
|
|
443
|
+
.option('--profile-dir <path>', 'Directory containing provisioning profiles (auto-discovered if omitted)')
|
|
444
|
+
.option('--bundle-id <id>', 'Target bundle ID for remapping (e.g., com.example.wda)')
|
|
445
|
+
.addHelpText(
|
|
446
|
+
'after',
|
|
447
|
+
`
|
|
307
448
|
EXAMPLES:
|
|
308
449
|
# Sign downloaded WDA with .p12 certificate (requires P12_PASSWORD)
|
|
309
450
|
P12_PASSWORD=mypassword appium driver run xcuitest sign-wda -- --wda-path ./wda-real/WebDriverAgentRunner-Runner.app \
|
|
@@ -327,64 +468,79 @@ EXAMPLES:
|
|
|
327
468
|
|
|
328
469
|
# Inspect a WDA app without signing
|
|
329
470
|
appium driver run xcuitest sign-wda -- --wda-path ./wda-real/WebDriverAgentRunner-Runner.app --inspect`,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
wdaPath: options.wdaPath,
|
|
335
|
-
});
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const p12Password = process.env.P12_PASSWORD;
|
|
471
|
+
)
|
|
472
|
+
.action(async (options) => {
|
|
473
|
+
await this._handleParsedOptions(options);
|
|
474
|
+
});
|
|
340
475
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const hasCertAndKey = !!(options.p12Cert && options.p12Key);
|
|
476
|
+
return program;
|
|
477
|
+
}
|
|
344
478
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
479
|
+
/**
|
|
480
|
+
* @param {object} options
|
|
481
|
+
* @returns {Promise<void>}
|
|
482
|
+
*/
|
|
483
|
+
async _handleParsedOptions(options) {
|
|
484
|
+
if (options.inspect) {
|
|
485
|
+
await this._inspectWorkflow.run({
|
|
486
|
+
wdaPath: options.wdaPath,
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
350
490
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
491
|
+
const p12Password = process.env.P12_PASSWORD;
|
|
492
|
+
this._checkSigningOptions(options, p12Password);
|
|
493
|
+
|
|
494
|
+
await this._signWorkflow.run({
|
|
495
|
+
wdaPath: options.wdaPath,
|
|
496
|
+
p12File: options.p12File,
|
|
497
|
+
p12Cert: options.p12Cert,
|
|
498
|
+
p12Key: options.p12Key,
|
|
499
|
+
p12Password: p12Password || '',
|
|
500
|
+
profileDir: options.profileDir,
|
|
501
|
+
bundleId: options.bundleId,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
356
504
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
505
|
+
/**
|
|
506
|
+
* @param {object} options
|
|
507
|
+
* @param {string | undefined} p12Password
|
|
508
|
+
* @returns {void}
|
|
509
|
+
*/
|
|
510
|
+
_checkSigningOptions(options, p12Password) {
|
|
511
|
+
const hasP12File = !!options.p12File;
|
|
512
|
+
const hasCertAndKey = !!(options.p12Cert && options.p12Key);
|
|
513
|
+
|
|
514
|
+
if (!hasP12File && !hasCertAndKey) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`Must provide either --p12-file or both --p12-cert and --p12-key for signing mode`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
362
519
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
520
|
+
if (hasP12File && hasCertAndKey) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`Cannot provide both --p12-file and --p12-cert/--p12-key; use one approach`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
369
525
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
p12Password: p12Password || '',
|
|
376
|
-
profileDir: options.profileDir,
|
|
377
|
-
bundleId: options.bundleId,
|
|
378
|
-
});
|
|
379
|
-
});
|
|
526
|
+
if ((options.p12Cert && !options.p12Key) || (!options.p12Cert && options.p12Key)) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
`Both --p12-cert and --p12-key must be provided together`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
380
531
|
|
|
381
|
-
|
|
532
|
+
if (hasP12File && !p12Password) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`Missing required option for signing mode: P12_PASSWORD env var (required when using --p12-file)`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
382
538
|
}
|
|
383
539
|
|
|
384
540
|
const isMainModule =
|
|
385
541
|
Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
386
542
|
if (isMainModule) {
|
|
387
|
-
await
|
|
543
|
+
await new SignWdaCli().run(process.argv);
|
|
388
544
|
}
|
|
389
545
|
|
|
390
546
|
/**
|