-
-
Notifications
You must be signed in to change notification settings - Fork 162
Expand file tree
/
Copy pathios-sign.ts
More file actions
327 lines (293 loc) · 11.4 KB
/
ios-sign.ts
File metadata and controls
327 lines (293 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
import { exec } from 'child_process';
import fs from 'fs';
import path from 'path';
import util from 'util';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import Applesign from 'applesign';
import archiver from 'archiver';
import { provision } from 'ios-mobileprovision-finder';
import Listr, { ListrContext } from 'listr';
import os from 'os';
import { Observable, Subscriber } from 'rxjs';
import { hideBin } from 'yargs/helpers';
import yargs from 'yargs/yargs';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { select } from '@inquirer/prompts';
const execAsync = util.promisify(exec);
const WDA_BUILD_PATH = '/appium_wda_ios/Build/Products/Debug-iphoneos';
let bundleIdName: { uuid: string; name: string }[] | null = null;
let freeBundleID: { uuid: string; name: string } | null = null;
let isFreeAccount = false;
async function getXcodeMajorVersion(): Promise<number> {
const { stdout } = await execAsync('xcodebuild -version');
const match = stdout.match(/Xcode (\d+)\./);
if (!match) {
throw new Error('Unable to determine Xcode version');
}
return parseInt(match[1], 10);
}
async function getProvisioningProfilePath(): Promise<string> {
const xcodeVersion = await getXcodeMajorVersion();
if (xcodeVersion <= 15) {
return path.join(os.homedir(), 'Library/MobileDevice/Provisioning Profiles');
} else {
return path.join(os.homedir(), 'Library/Developer/Xcode/UserData/Provisioning Profiles');
}
}
type Context = ListrContext & CliOptions;
interface CliOptions {
mobileProvisioningFile?: string;
wdaProjectPath?: string;
platform?: string;
bundleId?: string;
}
const getOptions = async () => {
const argv: CliOptions = await yargs(hideBin(process.argv)).options({
'mobile-provisioning-file': {
desc: 'Path to the mobile provisioning file which is used to sign the webdriver agent',
type: 'string',
},
'wda-project-path': {
desc: 'Path to webdriver agent xcode project',
type: 'string',
},
platform: {
desc: 'Platform type: ios, tvos, or both (default: ios)',
type: 'string',
choices: ['ios', 'tvos', 'both'],
default: 'ios',
},
}).argv;
return {
mobileProvisioningFile: argv.mobileProvisioningFile,
wdaProjectPath: argv.wdaProjectPath,
platform: argv.platform,
};
};
const getMobileProvisioningFile = async (mobileProvisioningFile?: string) => {
if (mobileProvisioningFile) {
if (!fs.existsSync(mobileProvisioningFile) || !fs.statSync(mobileProvisioningFile).isFile()) {
throw new Error(`Mobile provisioning file ${mobileProvisioningFile} does not exists`);
}
return mobileProvisioningFile;
} else {
const provisionFileDir = await getProvisioningProfilePath();
if (!fs.existsSync(provisionFileDir)) {
throw new Error(`Provisioning directory does not exist: ${provisionFileDir}`);
}
const files = fs
.readdirSync(provisionFileDir, { encoding: 'utf8' })
.filter((file) => file.endsWith('.mobileprovision'));
const provisioningFiles = files.map((file) => {
const fullPath = path.join(provisionFileDir, file);
const mp = provision.readFromFile(fullPath);
return { ...mp, _filePath: fullPath };
});
if (!provisioningFiles || !provisioningFiles.length) {
throw new Error('No mobileprovision file found on the machine');
}
const prompt = await select({
message: 'Select the mobileprovision to use for signing',
choices: provisioningFiles.map((file) => ({
value: file.UUID,
name: `${file.Name.split(':')[1] || file.Name} (Team: ${file.TeamName}) (${file.UUID})`,
})),
});
isFreeAccount = await select({
message: 'Is this a free or enterprise account provisioning profile?',
choices: [
{ value: true, name: 'Free Account' },
{ value: false, name: 'Enterprise Account' },
],
});
if (isFreeAccount) {
bundleIdName = provisioningFiles.map((file) => {
return { uuid: file.UUID, name: `${file.Name.split(':')[1] || file.Name}` };
});
isFreeAccount = true;
}
freeBundleID =
bundleIdName?.find((d: any) => {
return d.uuid === prompt;
}) || null;
return path.join(await getProvisioningProfilePath(), `${prompt}.mobileprovision`);
}
};
const getWdaProject = async (wdaProjectPath?: string) => {
if (wdaProjectPath) {
if (!fs.existsSync(wdaProjectPath) || !fs.statSync(wdaProjectPath).isDirectory()) {
throw new Error(`Unable to find webdriver agent project in path ${wdaProjectPath}`);
}
return wdaProjectPath;
}
try {
const { stdout } = await execAsync('find $HOME/.appium -name WebDriverAgent.xcodeproj');
return path.dirname(stdout.trim().split('\n')[0]);
} catch (err) {
throw new Error('Unable to find WebDriverAgent project');
}
};
/* Task definintions */
async function buildWebDriverAgent(projectDir: string, logger: any, bundleId?: string) {
try {
let buildCommand =
'xcodebuild clean build-for-testing -project WebDriverAgent.xcodeproj -derivedDataPath appium_wda_ios -scheme WebDriverAgentRunner -destination generic/platform=iOS CODE_SIGNING_ALLOWED=NO';
if (isFreeAccount) {
buildCommand += ` PRODUCT_BUNDLE_IDENTIFIER=${bundleId?.replace(/^\s+|\s+$/g, '')}`;
}
logger(buildCommand);
await execAsync(buildCommand, { cwd: projectDir, maxBuffer: undefined });
return `${projectDir}/${WDA_BUILD_PATH}/WebDriverAgentRunner-Runner.app`;
} catch (error) {
console.log(error);
throw new Error(`❌ Error building WebDriverAgent: ${(error as any)?.message}`);
}
}
async function zipPayloadDirectory(
outputZipPath: any,
folderPath: any,
observer: Subscriber<string>,
) {
return new Promise<void>((resolve, reject) => {
const output = fs.createWriteStream(outputZipPath);
const archive = archiver('zip', { zlib: { level: 9 } });
output.on('close', () => {
observer.next(`Zipped ${archive.pointer()} total bytes`);
observer.next(`Archive has been written to ${outputZipPath}`);
resolve();
});
archive.on('error', (err) => {
reject(err);
});
archive.pipe(output);
archive.directory(folderPath, 'Payload');
archive.finalize();
});
}
(async () => {
const cliOptions = await getOptions();
const mobileProvisioningFile = await getMobileProvisioningFile(cliOptions.mobileProvisioningFile);
const tasks = new Listr(
[
{
title: '🔍 Searching for WebDriverAgent.xcodeproj',
task: async (context: Context, task) => {
context.wdaProjectPath = await getWdaProject(cliOptions.wdaProjectPath);
task.title = `Found WebDriverAgent.xcodeproj at: ${context.wdaProjectPath}`;
},
},
{
title: '🏗️ Building WebDriverAgent',
task: (context: Context, task) => {
return new Observable((observer) => {
buildWebDriverAgent(context.wdaProjectPath, observer.next.bind(observer)).then(
(wdaAppPath) => {
context.wdaAppPath = wdaAppPath;
task.title = 'Successfully built WebDriverAgent';
observer.complete();
},
);
});
},
},
{
title: 'Preparing webdrivergaent ipa',
task: (context) => {
return new Observable((observer) => {
const wdaBuildPath = path.join(context.wdaProjectPath, WDA_BUILD_PATH);
const payloadDirectory = path.join(wdaBuildPath, 'Payload');
observer.next('Removing framework directory');
fs.readdirSync(`${context.wdaAppPath}/Frameworks`).forEach((f) =>
fs.rmSync(`${context.wdaAppPath}/Frameworks/${f}`, { recursive: true }),
);
const infoPlistPath = path.join(context.wdaAppPath, 'Info.plist');
const bundleId = freeBundleID?.name.replace(/^\s+|\s+$/g, '') || '';
// Read Info.plist
let infoPlistContent = fs.readFileSync(infoPlistPath, 'utf8');
// Replace CFBundleIdentifier
infoPlistContent = infoPlistContent.replace(
/<key>CFBundleIdentifier<\/key>\n\s*<string>(.*?)<\/string>/,
`<key>CFBundleIdentifier</key>\n<string>${bundleId}</string>`,
);
// Write Info.plist
fs.writeFileSync(infoPlistPath, infoPlistContent, 'utf8');
observer.next('Creating Payload directory');
execAsync(`mkdir -p ${payloadDirectory}`)
.then(() => {
observer.next('Payload directory created successfully');
})
.then(() => {
observer.next('🚚 Moving .app file to Payload directory...');
return execAsync(`mv ${context.wdaAppPath} ${payloadDirectory}`);
})
.then(() => {
observer.next('Packing Payload directory...');
return zipPayloadDirectory(
`${wdaBuildPath}/wda-resign.zip`,
payloadDirectory,
observer,
);
})
.then(() => observer.complete());
});
},
},
{
title: 'Signing WebDriverAgent ipa',
task: async (context, task) => {
const wdaBuildPath = path.join(context.wdaProjectPath, WDA_BUILD_PATH);
if (cliOptions.platform === 'both') {
const platforms = ['ios', 'tvos'];
const results = [];
for (const platform of platforms) {
const wdaFileName = platform === 'tvos' ? 'wda-resign_tvos.ipa' : 'wda-resign.ipa';
const ipaPath = `${wdaBuildPath}/${wdaFileName}`;
let appleOptions: any;
if (freeBundleID) {
appleOptions = {
mobileprovision: mobileProvisioningFile,
outfile: ipaPath,
bundleId: freeBundleID.name.replace(/^\s+|\s+$/g, ''),
};
} else {
appleOptions = {
mobileprovision: mobileProvisioningFile,
outfile: ipaPath,
};
}
const as = new Applesign(appleOptions);
await as.signIPA(path.join(wdaBuildPath, 'wda-resign.zip'));
results.push(`${platform}: ${ipaPath}`);
}
task.title = `Successfully signed WebDriverAgent files for both platforms: ${results.join(', ')}`;
} else {
// Single platform file creation
const wdaFileName =
cliOptions.platform === 'tvos' ? 'wda-resign_tvos.ipa' : 'wda-resign.ipa';
const ipaPath = `${wdaBuildPath}/${wdaFileName}`;
let appleOptions: any;
if (freeBundleID) {
appleOptions = {
mobileprovision: mobileProvisioningFile,
outfile: ipaPath,
bundleId: freeBundleID.name.replace(/^\s+|\s+$/g, ''),
};
} else {
appleOptions = {
mobileprovision: mobileProvisioningFile,
outfile: ipaPath,
};
}
const as = new Applesign(appleOptions);
await as.signIPA(path.join(wdaBuildPath, 'wda-resign.zip'));
task.title = `Successfully signed WebDriverAgent file for ${cliOptions.platform}: ${ipaPath}`;
}
},
},
],
{ exitOnError: true },
);
await tasks.run();
})();