@react-native-harness/platform-android 1.1.0-rc.2 → 1.1.0-rc.4
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/README.md +9 -2
- package/dist/__tests__/adb.test.js +283 -10
- package/dist/__tests__/avd-config.test.d.ts +2 -0
- package/dist/__tests__/avd-config.test.d.ts.map +1 -0
- package/dist/__tests__/avd-config.test.js +174 -0
- package/dist/__tests__/ci-action.test.d.ts +2 -0
- package/dist/__tests__/ci-action.test.d.ts.map +1 -0
- package/dist/__tests__/ci-action.test.js +46 -0
- package/dist/__tests__/emulator-startup.test.d.ts +2 -0
- package/dist/__tests__/emulator-startup.test.d.ts.map +1 -0
- package/dist/__tests__/emulator-startup.test.js +19 -0
- package/dist/__tests__/environment.test.d.ts +2 -0
- package/dist/__tests__/environment.test.d.ts.map +1 -0
- package/dist/__tests__/environment.test.js +117 -0
- package/dist/__tests__/instance.test.d.ts +2 -0
- package/dist/__tests__/instance.test.d.ts.map +1 -0
- package/dist/__tests__/instance.test.js +423 -0
- package/dist/__tests__/targets.test.d.ts +2 -0
- package/dist/__tests__/targets.test.d.ts.map +1 -0
- package/dist/__tests__/targets.test.js +49 -0
- package/dist/adb.d.ts +23 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +259 -16
- package/dist/app-monitor.d.ts.map +1 -1
- package/dist/app-monitor.js +27 -7
- package/dist/assertions.d.ts +5 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +6 -0
- package/dist/avd-config.d.ts +41 -0
- package/dist/avd-config.d.ts.map +1 -0
- package/dist/avd-config.js +173 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/emulator-startup.d.ts +3 -0
- package/dist/emulator-startup.d.ts.map +1 -0
- package/dist/emulator-startup.js +17 -0
- package/dist/emulator.d.ts +6 -0
- package/dist/emulator.d.ts.map +1 -0
- package/dist/emulator.js +27 -0
- package/dist/environment.d.ts +31 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +317 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +14 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/instance.d.ts +6 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +232 -0
- package/dist/reader.d.ts +6 -0
- package/dist/reader.d.ts.map +1 -0
- package/dist/reader.js +57 -0
- package/dist/runner.d.ts +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +9 -52
- package/dist/targets.d.ts +1 -1
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +4 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +381 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +107 -0
- package/package.json +4 -4
- package/src/__tests__/adb.test.ts +419 -15
- package/src/__tests__/avd-config.test.ts +206 -0
- package/src/__tests__/ci-action.test.ts +81 -0
- package/src/__tests__/emulator-startup.test.ts +32 -0
- package/src/__tests__/environment.test.ts +212 -0
- package/src/__tests__/instance.test.ts +610 -0
- package/src/__tests__/targets.test.ts +53 -0
- package/src/adb.ts +430 -28
- package/src/app-monitor.ts +56 -18
- package/src/avd-config.ts +290 -0
- package/src/config.ts +8 -0
- package/src/emulator-startup.ts +28 -0
- package/src/environment.ts +554 -0
- package/src/errors.ts +19 -0
- package/src/factory.ts +4 -0
- package/src/index.ts +7 -1
- package/src/instance.ts +380 -0
- package/src/runner.ts +19 -70
- package/src/targets.ts +18 -8
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { spawn } from '@react-native-harness/tools';
|
|
2
|
+
import { logger } from '@react-native-harness/tools';
|
|
3
|
+
import { createWriteStream } from 'node:fs';
|
|
4
|
+
import { access, cp, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import https from 'node:https';
|
|
9
|
+
|
|
10
|
+
const CMDLINE_TOOLS_PATH_SEGMENTS = ['cmdline-tools', 'latest'];
|
|
11
|
+
const ANDROID_REPOSITORY_INDEX_URL =
|
|
12
|
+
'https://dl.google.com/android/repository/repository2-1.xml';
|
|
13
|
+
const androidEnvironmentLogger = logger.child('android-environment');
|
|
14
|
+
|
|
15
|
+
export type AndroidSystemImageArch = 'x86_64' | 'arm64-v8a' | 'armeabi-v7a';
|
|
16
|
+
|
|
17
|
+
type AndroidSdkRootOptions = {
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
platform?: NodeJS.Platform;
|
|
20
|
+
homeDirectory?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getConfiguredAndroidSdkRoot = (
|
|
24
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
25
|
+
): string | null => {
|
|
26
|
+
return env.ANDROID_HOME ?? env.ANDROID_SDK_ROOT ?? null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getDefaultUnixAndroidSdkRoot = ({
|
|
30
|
+
platform = process.platform,
|
|
31
|
+
homeDirectory = os.homedir(),
|
|
32
|
+
}: Omit<AndroidSdkRootOptions, 'env'> = {}): string | null => {
|
|
33
|
+
if (platform === 'darwin') {
|
|
34
|
+
return path.join(homeDirectory, 'Library', 'Android', 'sdk');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (platform === 'linux') {
|
|
38
|
+
return path.join(homeDirectory, 'Android', 'Sdk');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const canBootstrapAndroidSdk = (
|
|
45
|
+
platform: NodeJS.Platform = process.platform,
|
|
46
|
+
) => {
|
|
47
|
+
return platform === 'darwin' || platform === 'linux';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const pathExists = async (filePath: string): Promise<boolean> => {
|
|
51
|
+
try {
|
|
52
|
+
await access(filePath);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const quoteShell = (value: string): string => {
|
|
60
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const downloadText = async (url: string): Promise<string> => {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const request = https.get(url, (response) => {
|
|
66
|
+
const { statusCode = 0, headers } = response;
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
statusCode >= 300 &&
|
|
70
|
+
statusCode < 400 &&
|
|
71
|
+
typeof headers.location === 'string'
|
|
72
|
+
) {
|
|
73
|
+
response.resume();
|
|
74
|
+
resolve(downloadText(headers.location));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (statusCode !== 200) {
|
|
79
|
+
response.resume();
|
|
80
|
+
reject(
|
|
81
|
+
new Error(
|
|
82
|
+
`Failed to download Android repository index from ${url} (status ${statusCode}).`,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
response.setEncoding('utf8');
|
|
89
|
+
|
|
90
|
+
let body = '';
|
|
91
|
+
response.on('data', (chunk: string) => {
|
|
92
|
+
body += chunk;
|
|
93
|
+
});
|
|
94
|
+
response.once('end', () => {
|
|
95
|
+
resolve(body);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
request.once('error', reject);
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const downloadFile = async (
|
|
104
|
+
url: string,
|
|
105
|
+
destinationPath: string,
|
|
106
|
+
): Promise<void> => {
|
|
107
|
+
await new Promise<void>((resolve, reject) => {
|
|
108
|
+
const request = https.get(url, (response) => {
|
|
109
|
+
const { statusCode = 0, headers } = response;
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
statusCode >= 300 &&
|
|
113
|
+
statusCode < 400 &&
|
|
114
|
+
typeof headers.location === 'string'
|
|
115
|
+
) {
|
|
116
|
+
response.resume();
|
|
117
|
+
resolve(downloadFile(headers.location, destinationPath));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (statusCode !== 200) {
|
|
122
|
+
response.resume();
|
|
123
|
+
reject(
|
|
124
|
+
new Error(
|
|
125
|
+
`Failed to download Android command-line tools from ${url} (status ${statusCode}).`,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const output = createWriteStream(destinationPath);
|
|
132
|
+
pipeline(response, output).then(resolve).catch(reject);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
request.once('error', reject);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const getCommandLineToolsArchiveUrl = async (
|
|
140
|
+
platform: NodeJS.Platform = process.platform,
|
|
141
|
+
): Promise<string> => {
|
|
142
|
+
const archivePlatform =
|
|
143
|
+
platform === 'darwin' ? 'mac' : platform === 'linux' ? 'linux' : null;
|
|
144
|
+
|
|
145
|
+
if (!archivePlatform) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'Automatic Android SDK bootstrap is only supported on macOS and Linux.',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const repositoryIndex = await downloadText(ANDROID_REPOSITORY_INDEX_URL);
|
|
152
|
+
const archivePattern = new RegExp(
|
|
153
|
+
`commandlinetools-${archivePlatform}-(\\d+)_latest\\.zip`,
|
|
154
|
+
'g',
|
|
155
|
+
);
|
|
156
|
+
const matches = [...repositoryIndex.matchAll(archivePattern)];
|
|
157
|
+
|
|
158
|
+
if (matches.length === 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Failed to resolve Android command-line tools archive for ${archivePlatform}.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const newestArchive = matches
|
|
165
|
+
.map((match) => ({
|
|
166
|
+
fileName: match[0],
|
|
167
|
+
revision: Number(match[1]),
|
|
168
|
+
}))
|
|
169
|
+
.sort((left, right) => right.revision - left.revision)[0];
|
|
170
|
+
|
|
171
|
+
return `https://dl.google.com/android/repository/${newestArchive.fileName}`;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const ensureAndroidCommandLineTools = async (
|
|
175
|
+
sdkRoot: string,
|
|
176
|
+
platform: NodeJS.Platform = process.platform,
|
|
177
|
+
): Promise<void> => {
|
|
178
|
+
if (
|
|
179
|
+
(await pathExists(getSdkManagerBinaryPath(sdkRoot))) &&
|
|
180
|
+
(await pathExists(getAvdManagerBinaryPath(sdkRoot)))
|
|
181
|
+
) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!canBootstrapAndroidSdk(platform)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
'Android command-line tools are missing. Set ANDROID_HOME or ANDROID_SDK_ROOT to an initialized SDK.',
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
androidEnvironmentLogger.info(
|
|
192
|
+
'Bootstrapping Android command-line tools in %s',
|
|
193
|
+
sdkRoot,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await mkdir(sdkRoot, { recursive: true });
|
|
197
|
+
|
|
198
|
+
const temporaryDirectory = await mkdtemp(
|
|
199
|
+
path.join(os.tmpdir(), 'android-cmdline-tools-'),
|
|
200
|
+
);
|
|
201
|
+
const archivePath = path.join(temporaryDirectory, 'cmdline-tools.zip');
|
|
202
|
+
const extractedPath = path.join(temporaryDirectory, 'extracted');
|
|
203
|
+
const sourceDirectory = path.join(extractedPath, 'cmdline-tools');
|
|
204
|
+
const targetDirectory = path.join(sdkRoot, ...CMDLINE_TOOLS_PATH_SEGMENTS);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
await downloadFile(
|
|
208
|
+
await getCommandLineToolsArchiveUrl(platform),
|
|
209
|
+
archivePath,
|
|
210
|
+
);
|
|
211
|
+
await spawn('unzip', ['-q', archivePath, '-d', extractedPath]);
|
|
212
|
+
await rm(targetDirectory, { force: true, recursive: true });
|
|
213
|
+
await mkdir(path.dirname(targetDirectory), { recursive: true });
|
|
214
|
+
await cp(sourceDirectory, targetDirectory, { recursive: true });
|
|
215
|
+
} finally {
|
|
216
|
+
await rm(temporaryDirectory, { force: true, recursive: true });
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const acceptAndroidLicenses = async (sdkRoot: string): Promise<void> => {
|
|
221
|
+
const sdkManagerBinaryPath = getSdkManagerBinaryPath(sdkRoot);
|
|
222
|
+
|
|
223
|
+
await spawn(
|
|
224
|
+
'bash',
|
|
225
|
+
[
|
|
226
|
+
'-lc',
|
|
227
|
+
`yes | ${quoteShell(sdkManagerBinaryPath)} --sdk_root=${quoteShell(
|
|
228
|
+
sdkRoot,
|
|
229
|
+
)} --licenses >/dev/null`,
|
|
230
|
+
],
|
|
231
|
+
{
|
|
232
|
+
env: getAndroidProcessEnv({
|
|
233
|
+
...process.env,
|
|
234
|
+
ANDROID_HOME: sdkRoot,
|
|
235
|
+
ANDROID_SDK_ROOT: sdkRoot,
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const getPackageVerificationPath = (
|
|
242
|
+
sdkRoot: string,
|
|
243
|
+
packageName: string,
|
|
244
|
+
): string | null => {
|
|
245
|
+
if (packageName === 'platform-tools') {
|
|
246
|
+
return getAdbBinaryPath(sdkRoot);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (packageName === 'emulator') {
|
|
250
|
+
return getEmulatorBinaryPath(sdkRoot);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (packageName.startsWith('platforms;android-')) {
|
|
254
|
+
return path.join(sdkRoot, packageName.replace(';', '/'));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (packageName.startsWith('system-images;android-')) {
|
|
258
|
+
return path.join(sdkRoot, packageName.replaceAll(';', path.sep));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const getMissingAndroidSdkPackages = async (
|
|
265
|
+
sdkRoot: string,
|
|
266
|
+
packages: readonly string[],
|
|
267
|
+
): Promise<string[]> => {
|
|
268
|
+
const missingPackages: string[] = [];
|
|
269
|
+
|
|
270
|
+
for (const packageName of packages) {
|
|
271
|
+
const verificationPath = getPackageVerificationPath(sdkRoot, packageName);
|
|
272
|
+
|
|
273
|
+
if (!verificationPath) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!(await pathExists(verificationPath))) {
|
|
278
|
+
missingPackages.push(packageName);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return missingPackages;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const installAndroidSdkPackages = async (
|
|
286
|
+
sdkRoot: string,
|
|
287
|
+
packages: readonly string[],
|
|
288
|
+
): Promise<void> => {
|
|
289
|
+
if (packages.length === 0) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const sdkManagerBinaryPath = getSdkManagerBinaryPath(sdkRoot);
|
|
294
|
+
const packageArgs = packages
|
|
295
|
+
.map((packageName) => quoteShell(packageName))
|
|
296
|
+
.join(' ');
|
|
297
|
+
|
|
298
|
+
androidEnvironmentLogger.info(
|
|
299
|
+
'Installing missing Android SDK packages: %s',
|
|
300
|
+
packages.join(', '),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
await acceptAndroidLicenses(sdkRoot);
|
|
304
|
+
await spawn(
|
|
305
|
+
'bash',
|
|
306
|
+
[
|
|
307
|
+
'-lc',
|
|
308
|
+
`yes | ${quoteShell(sdkManagerBinaryPath)} --sdk_root=${quoteShell(
|
|
309
|
+
sdkRoot,
|
|
310
|
+
)} ${packageArgs}`,
|
|
311
|
+
],
|
|
312
|
+
{
|
|
313
|
+
env: getAndroidProcessEnv({
|
|
314
|
+
...process.env,
|
|
315
|
+
ANDROID_HOME: sdkRoot,
|
|
316
|
+
ANDROID_SDK_ROOT: sdkRoot,
|
|
317
|
+
}),
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export const getAndroidSdkRoot = (
|
|
323
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
324
|
+
options: Omit<AndroidSdkRootOptions, 'env'> = {},
|
|
325
|
+
): string | null => {
|
|
326
|
+
return (
|
|
327
|
+
getConfiguredAndroidSdkRoot(env) ?? getDefaultUnixAndroidSdkRoot(options)
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const getRequiredAndroidSdkRoot = (
|
|
332
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
333
|
+
options: Omit<AndroidSdkRootOptions, 'env'> = {},
|
|
334
|
+
): string => {
|
|
335
|
+
const sdkRoot = getAndroidSdkRoot(env, options);
|
|
336
|
+
|
|
337
|
+
if (!sdkRoot) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
'Android SDK root is not configured. Set ANDROID_HOME or ANDROID_SDK_ROOT.',
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return sdkRoot;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
export const getHostAndroidSystemImageArch = (
|
|
347
|
+
architecture: string = process.arch,
|
|
348
|
+
): AndroidSystemImageArch => {
|
|
349
|
+
switch (architecture) {
|
|
350
|
+
case 'arm64':
|
|
351
|
+
return 'arm64-v8a';
|
|
352
|
+
case 'arm':
|
|
353
|
+
return 'armeabi-v7a';
|
|
354
|
+
case 'x64':
|
|
355
|
+
default:
|
|
356
|
+
return 'x86_64';
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export const getAndroidPlatformPackage = (apiLevel: number): string => {
|
|
361
|
+
return `platforms;android-${apiLevel}`;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
export const getAndroidSystemImagePackage = (
|
|
365
|
+
apiLevel: number,
|
|
366
|
+
architecture: AndroidSystemImageArch = getHostAndroidSystemImageArch(),
|
|
367
|
+
): string => {
|
|
368
|
+
return `system-images;android-${apiLevel};default;${architecture}`;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
export const getRequiredAndroidSdkPackages = ({
|
|
372
|
+
apiLevel,
|
|
373
|
+
includeEmulator = false,
|
|
374
|
+
architecture = getHostAndroidSystemImageArch(),
|
|
375
|
+
}: {
|
|
376
|
+
apiLevel?: number;
|
|
377
|
+
includeEmulator?: boolean;
|
|
378
|
+
architecture?: AndroidSystemImageArch;
|
|
379
|
+
} = {}): string[] => {
|
|
380
|
+
const packages = ['platform-tools'];
|
|
381
|
+
|
|
382
|
+
if (!includeEmulator) {
|
|
383
|
+
return packages;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
packages.push('emulator');
|
|
387
|
+
|
|
388
|
+
if (typeof apiLevel === 'number') {
|
|
389
|
+
packages.push(getAndroidPlatformPackage(apiLevel));
|
|
390
|
+
packages.push(getAndroidSystemImagePackage(apiLevel, architecture));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return packages;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const getMissingAndroidSdkPackagesForEnvironment = async (
|
|
397
|
+
packages: readonly string[],
|
|
398
|
+
{
|
|
399
|
+
env = process.env,
|
|
400
|
+
platform = process.platform,
|
|
401
|
+
homeDirectory = os.homedir(),
|
|
402
|
+
}: AndroidSdkRootOptions = {},
|
|
403
|
+
): Promise<{ sdkRoot: string; missingPackages: string[] }> => {
|
|
404
|
+
const sdkRoot = getRequiredAndroidSdkRoot(env, { platform, homeDirectory });
|
|
405
|
+
|
|
406
|
+
await mkdir(sdkRoot, { recursive: true });
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
sdkRoot,
|
|
410
|
+
missingPackages: await getMissingAndroidSdkPackages(sdkRoot, packages),
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
export const ensureAndroidSdkPackages = async (
|
|
415
|
+
packages: readonly string[],
|
|
416
|
+
{
|
|
417
|
+
env = process.env,
|
|
418
|
+
platform = process.platform,
|
|
419
|
+
homeDirectory = os.homedir(),
|
|
420
|
+
}: AndroidSdkRootOptions = {},
|
|
421
|
+
): Promise<string> => {
|
|
422
|
+
const { sdkRoot, missingPackages } =
|
|
423
|
+
await getMissingAndroidSdkPackagesForEnvironment(packages, {
|
|
424
|
+
env,
|
|
425
|
+
platform,
|
|
426
|
+
homeDirectory,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (missingPackages.length === 0) {
|
|
430
|
+
return sdkRoot;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
await ensureAndroidCommandLineTools(sdkRoot, platform);
|
|
434
|
+
|
|
435
|
+
await installAndroidSdkPackages(sdkRoot, missingPackages);
|
|
436
|
+
|
|
437
|
+
const unresolvedPackages = await getMissingAndroidSdkPackages(
|
|
438
|
+
sdkRoot,
|
|
439
|
+
packages,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
if (unresolvedPackages.length > 0) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Android SDK packages are still missing after installation: ${unresolvedPackages.join(
|
|
445
|
+
', ',
|
|
446
|
+
)}`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return sdkRoot;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
export const ensureAndroidAdbAvailable = async (
|
|
454
|
+
options: AndroidSdkRootOptions = {},
|
|
455
|
+
): Promise<string> => {
|
|
456
|
+
return ensureAndroidSdkPackages(['platform-tools'], options);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
export const ensureAndroidEmulatorAvailable = async (
|
|
460
|
+
options: AndroidSdkRootOptions = {},
|
|
461
|
+
): Promise<string> => {
|
|
462
|
+
return ensureAndroidSdkPackages(['emulator'], options);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
export const ensureAndroidAvdProvisioningAvailable = async (
|
|
466
|
+
apiLevel: number,
|
|
467
|
+
architecture: AndroidSystemImageArch = getHostAndroidSystemImageArch(),
|
|
468
|
+
options: AndroidSdkRootOptions = {},
|
|
469
|
+
): Promise<string> => {
|
|
470
|
+
return ensureAndroidSdkPackages(
|
|
471
|
+
[
|
|
472
|
+
getAndroidPlatformPackage(apiLevel),
|
|
473
|
+
getAndroidSystemImagePackage(apiLevel, architecture),
|
|
474
|
+
],
|
|
475
|
+
options,
|
|
476
|
+
);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
export const ensureAndroidDiscoveryEnvironment = async (): Promise<string> => {
|
|
480
|
+
initializeAndroidProcessEnv();
|
|
481
|
+
|
|
482
|
+
return ensureAndroidAdbAvailable();
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export const ensureAndroidPhysicalDeviceEnvironment =
|
|
486
|
+
async (): Promise<string> => {
|
|
487
|
+
initializeAndroidProcessEnv();
|
|
488
|
+
|
|
489
|
+
return ensureAndroidAdbAvailable();
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
export const ensureAndroidEmulatorEnvironment = async (
|
|
493
|
+
apiLevel: number,
|
|
494
|
+
): Promise<string> => {
|
|
495
|
+
initializeAndroidProcessEnv();
|
|
496
|
+
|
|
497
|
+
await ensureAndroidAdbAvailable();
|
|
498
|
+
await ensureAndroidEmulatorAvailable();
|
|
499
|
+
|
|
500
|
+
return ensureAndroidAvdProvisioningAvailable(apiLevel);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
export const getAndroidProcessEnv = (
|
|
504
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
505
|
+
): NodeJS.ProcessEnv => {
|
|
506
|
+
const sdkRoot = getAndroidSdkRoot(env);
|
|
507
|
+
|
|
508
|
+
if (!sdkRoot) {
|
|
509
|
+
return env;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const platformToolsPath = path.join(sdkRoot, 'platform-tools');
|
|
513
|
+
const emulatorPath = path.join(sdkRoot, 'emulator');
|
|
514
|
+
const cmdlineToolsPath = path.join(sdkRoot, ...CMDLINE_TOOLS_PATH_SEGMENTS);
|
|
515
|
+
const cmdlineToolsBinPath = path.join(cmdlineToolsPath, 'bin');
|
|
516
|
+
const currentPath = env.PATH ?? '';
|
|
517
|
+
const pathEntries = [
|
|
518
|
+
platformToolsPath,
|
|
519
|
+
emulatorPath,
|
|
520
|
+
cmdlineToolsPath,
|
|
521
|
+
cmdlineToolsBinPath,
|
|
522
|
+
currentPath,
|
|
523
|
+
].filter((entry) => entry !== '');
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
...env,
|
|
527
|
+
ANDROID_HOME: sdkRoot,
|
|
528
|
+
ANDROID_SDK_ROOT: sdkRoot,
|
|
529
|
+
ANDROID_AVD_HOME: path.join(os.homedir(), '.android', 'avd'),
|
|
530
|
+
PATH: pathEntries.join(path.delimiter),
|
|
531
|
+
};
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
export const initializeAndroidProcessEnv = (): void => {
|
|
535
|
+
Object.assign(process.env, getAndroidProcessEnv());
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
export const getAdbBinaryPath = (
|
|
539
|
+
sdkRoot: string = getRequiredAndroidSdkRoot(),
|
|
540
|
+
): string => path.join(sdkRoot, 'platform-tools', 'adb');
|
|
541
|
+
|
|
542
|
+
export const getEmulatorBinaryPath = (
|
|
543
|
+
sdkRoot: string = getRequiredAndroidSdkRoot(),
|
|
544
|
+
): string => path.join(sdkRoot, 'emulator', 'emulator');
|
|
545
|
+
|
|
546
|
+
export const getSdkManagerBinaryPath = (
|
|
547
|
+
sdkRoot: string = getRequiredAndroidSdkRoot(),
|
|
548
|
+
): string =>
|
|
549
|
+
path.join(sdkRoot, ...CMDLINE_TOOLS_PATH_SEGMENTS, 'bin', 'sdkmanager');
|
|
550
|
+
|
|
551
|
+
export const getAvdManagerBinaryPath = (
|
|
552
|
+
sdkRoot: string = getRequiredAndroidSdkRoot(),
|
|
553
|
+
): string =>
|
|
554
|
+
path.join(sdkRoot, ...CMDLINE_TOOLS_PATH_SEGMENTS, 'bin', 'avdmanager');
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class HarnessAppPathError extends Error {
|
|
2
|
+
constructor(reason: 'missing' | 'invalid', appPath?: string) {
|
|
3
|
+
super(
|
|
4
|
+
reason === 'missing'
|
|
5
|
+
? 'App is not installed on the emulator and HARNESS_APP_PATH is not set.'
|
|
6
|
+
: `HARNESS_APP_PATH points to a missing APK: ${appPath ?? '<unknown>'}`
|
|
7
|
+
);
|
|
8
|
+
this.name = 'HarnessAppPathError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class HarnessEmulatorConfigError extends Error {
|
|
13
|
+
constructor(deviceName: string) {
|
|
14
|
+
super(
|
|
15
|
+
`Android emulator "${deviceName}" is not running and no AVD config was provided. Add the "avd" property to this runner config so Harness can create and boot the emulator.`
|
|
16
|
+
);
|
|
17
|
+
this.name = 'HarnessEmulatorConfigError';
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/factory.ts
CHANGED
|
@@ -31,4 +31,8 @@ export const androidPlatform = (
|
|
|
31
31
|
config,
|
|
32
32
|
runner: import.meta.resolve('./runner.js'),
|
|
33
33
|
platformId: 'android',
|
|
34
|
+
getResourceLockKey: () =>
|
|
35
|
+
config.device.type === 'emulator'
|
|
36
|
+
? `android:emulator:${config.device.name}`
|
|
37
|
+
: `android:physical:${config.device.manufacturer}:${config.device.model}`,
|
|
34
38
|
});
|
package/src/index.ts
CHANGED
|
@@ -4,4 +4,10 @@ export {
|
|
|
4
4
|
androidPlatform,
|
|
5
5
|
} from './factory.js';
|
|
6
6
|
export type { AndroidPlatformConfig } from './config.js';
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
getNormalizedAvdCacheConfig,
|
|
9
|
+
resolveAvdCachingEnabled,
|
|
10
|
+
} from './avd-config.js';
|
|
11
|
+
export { getHostAndroidSystemImageArch } from './environment.js';
|
|
12
|
+
export { HarnessAppPathError, HarnessEmulatorConfigError } from './errors.js';
|
|
13
|
+
export { getRunTargets } from './targets.js';
|