@hot-updater/android-helper 0.25.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/LICENSE +33 -0
- package/dist/index.cjs +626 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +596 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Sungyu Kang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
ADDITIONAL DISCLAIMER
|
|
26
|
+
|
|
27
|
+
The authors of this software shall not be held responsible for any legal issues,
|
|
28
|
+
regulatory violations, policy violations, or disputes arising from the use,
|
|
29
|
+
distribution, or integration of this software.
|
|
30
|
+
|
|
31
|
+
Users are solely responsible for ensuring compliance with all applicable laws,
|
|
32
|
+
regulations, and third-party policies (including but not limited to app store
|
|
33
|
+
policies).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
let __hot_updater_cli_tools = require("@hot-updater/cli-tools");
|
|
25
|
+
__hot_updater_cli_tools = __toESM(__hot_updater_cli_tools);
|
|
26
|
+
let __hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
27
|
+
__hot_updater_plugin_core = __toESM(__hot_updater_plugin_core);
|
|
28
|
+
let path = require("path");
|
|
29
|
+
path = __toESM(path);
|
|
30
|
+
let execa = require("execa");
|
|
31
|
+
execa = __toESM(execa);
|
|
32
|
+
let fs = require("fs");
|
|
33
|
+
fs = __toESM(fs);
|
|
34
|
+
let node_os = require("node:os");
|
|
35
|
+
node_os = __toESM(node_os);
|
|
36
|
+
|
|
37
|
+
//#region src/utils/createGradleLogger.ts
|
|
38
|
+
const createGradleLogger = () => new __hot_updater_cli_tools.BuildLogger({
|
|
39
|
+
failurePatterns: ["BUILD FAILED"],
|
|
40
|
+
importantLogPatterns: [
|
|
41
|
+
"error:",
|
|
42
|
+
"Error:",
|
|
43
|
+
"ERROR:",
|
|
44
|
+
"> Task :",
|
|
45
|
+
"BUILD FAILED",
|
|
46
|
+
"BUILD SUCCESSFUL",
|
|
47
|
+
"FAILURE: Build failed with an exception.",
|
|
48
|
+
"* What went wrong:",
|
|
49
|
+
"Execution failed for task",
|
|
50
|
+
"Could not resolve",
|
|
51
|
+
"Failed to",
|
|
52
|
+
"Unable to load script",
|
|
53
|
+
"Metro encountered an error",
|
|
54
|
+
"React Native CLI",
|
|
55
|
+
"Android resource compilation failed",
|
|
56
|
+
"Duplicate class",
|
|
57
|
+
"Program type already present",
|
|
58
|
+
"actionable tasks:"
|
|
59
|
+
],
|
|
60
|
+
progressMapping: [
|
|
61
|
+
[[
|
|
62
|
+
"buildKotlinToolingMetadata",
|
|
63
|
+
"generateAutolinkingNewArchitectureFiles",
|
|
64
|
+
"generateCodegenSchemaFromJavaScript"
|
|
65
|
+
], 5],
|
|
66
|
+
[[
|
|
67
|
+
"createBundleReleaseJsAndAssets",
|
|
68
|
+
"bundleReleaseJsAndAssets",
|
|
69
|
+
"bundleDebugJsAndAssets"
|
|
70
|
+
], 15],
|
|
71
|
+
[[
|
|
72
|
+
"generateReleaseResValues",
|
|
73
|
+
"generateReleaseResources",
|
|
74
|
+
"mergeReleaseResources"
|
|
75
|
+
], 25],
|
|
76
|
+
[[
|
|
77
|
+
"processReleaseResources",
|
|
78
|
+
"processReleaseManifest",
|
|
79
|
+
"parseReleaseLocalResources"
|
|
80
|
+
], 35],
|
|
81
|
+
[["compileReleaseKotlin", "compileDebugKotlin"], 50],
|
|
82
|
+
[["compileReleaseJavaWithJavac", "compileDebugJavaWithJavac"], 60],
|
|
83
|
+
[[
|
|
84
|
+
"dexBuilderRelease",
|
|
85
|
+
"mergeDexRelease",
|
|
86
|
+
"transformClassesWithDex"
|
|
87
|
+
], 75],
|
|
88
|
+
[[
|
|
89
|
+
"compressReleaseAssets",
|
|
90
|
+
"optimizeReleaseResources",
|
|
91
|
+
"mergeReleaseAssets"
|
|
92
|
+
], 85],
|
|
93
|
+
[[
|
|
94
|
+
"packageRelease",
|
|
95
|
+
"packageDebug",
|
|
96
|
+
"assembleRelease",
|
|
97
|
+
"assembleDebug"
|
|
98
|
+
], 90],
|
|
99
|
+
[["bundleRelease", "bundleDebug"], 95],
|
|
100
|
+
[[/\d+ actionable tasks:/], 100]
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/builder/runGradle.ts
|
|
106
|
+
/**
|
|
107
|
+
* Clean up gradle error message by removing common noise
|
|
108
|
+
*/
|
|
109
|
+
const getCleanedErrorMessage = (error) => {
|
|
110
|
+
const gradleLinesToRemove = [
|
|
111
|
+
"FAILURE: Build failed with an exception.",
|
|
112
|
+
"* Try:",
|
|
113
|
+
"> Run with --stacktrace option to get the stack trace.",
|
|
114
|
+
"> Run with --info or --debug option to get more log output.",
|
|
115
|
+
"> Run with --scan to get full insights.",
|
|
116
|
+
"> Get more help at [undefined](https://help.gradle.org).",
|
|
117
|
+
"> Get more help at https://help.gradle.org.",
|
|
118
|
+
"BUILD FAILED"
|
|
119
|
+
];
|
|
120
|
+
return error.message.split("\n").filter((line) => !gradleLinesToRemove.some((l) => line.includes(l))).join("\n").trim();
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Generate gradle task names with module prefix
|
|
124
|
+
*/
|
|
125
|
+
function getTaskNames(moduleName, tasks) {
|
|
126
|
+
return tasks.map((task) => `${moduleName}:${task}`);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get gradle wrapper command based on platform
|
|
130
|
+
*/
|
|
131
|
+
const getGradleWrapper = () => process.platform.startsWith("win") ? "gradlew.bat" : "./gradlew";
|
|
132
|
+
/**
|
|
133
|
+
* Run gradle build with specified tasks and arguments
|
|
134
|
+
*/
|
|
135
|
+
async function runGradle({ tasks, args, androidProjectPath, appModuleName }) {
|
|
136
|
+
const gradleArgs = getTaskNames(appModuleName, tasks);
|
|
137
|
+
gradleArgs.push("-x", "lint");
|
|
138
|
+
if (args.extraParams) gradleArgs.push(...args.extraParams);
|
|
139
|
+
__hot_updater_cli_tools.p.log.info(`Run Gradle Settings:
|
|
140
|
+
Project ${androidProjectPath}
|
|
141
|
+
App Moudle ${appModuleName}
|
|
142
|
+
Tasks ${tasks.join(", ")}
|
|
143
|
+
Args ${gradleArgs.join(" ")}
|
|
144
|
+
`);
|
|
145
|
+
if ("port" in args && args.port != null) gradleArgs.push(`-PreactNativeDevServerPort=${args.port}`);
|
|
146
|
+
const logger = createGradleLogger();
|
|
147
|
+
logger.start(`${appModuleName}`);
|
|
148
|
+
try {
|
|
149
|
+
const process$1 = (0, execa.execa)(getGradleWrapper(), gradleArgs, { cwd: androidProjectPath });
|
|
150
|
+
for await (const line of process$1) if (line) logger.processLine(line);
|
|
151
|
+
logger.stop();
|
|
152
|
+
} catch (e) {
|
|
153
|
+
logger.stop("Build failed", false);
|
|
154
|
+
if (e instanceof execa.ExecaError) __hot_updater_cli_tools.p.log.error(getCleanedErrorMessage(e));
|
|
155
|
+
else if (e instanceof Error) __hot_updater_cli_tools.p.log.error(e.message);
|
|
156
|
+
throw new Error("Failed to build the app. See the error above for details from Gradle.");
|
|
157
|
+
}
|
|
158
|
+
return findBuildDirectory({
|
|
159
|
+
androidProjectPath,
|
|
160
|
+
moduleName: appModuleName,
|
|
161
|
+
tasks
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Find the build output directory and artifact path
|
|
166
|
+
*/
|
|
167
|
+
async function findBuildDirectory({ moduleName, tasks, androidProjectPath }) {
|
|
168
|
+
const selectedTask = tasks.find((t) => t.startsWith("install") || t.startsWith("assemble") || t.startsWith("bundle"));
|
|
169
|
+
if (!selectedTask) throw new Error(`Not supported gradle task: ${tasks.join(", ")}`);
|
|
170
|
+
const variantFromSelectedTask = selectedTask?.replace("install", "")?.replace("assemble", "")?.replace("bundle", "").split(/(?=[A-Z])/);
|
|
171
|
+
const variantPath = variantFromSelectedTask?.join("/")?.toLowerCase();
|
|
172
|
+
const variant = variantFromSelectedTask?.join("-")?.toLowerCase();
|
|
173
|
+
const isAabOutput = selectedTask?.includes("bundle") === true;
|
|
174
|
+
const buildDirectory = `${androidProjectPath}/${moduleName}/build/outputs/${isAabOutput ? "bundle" : "apk"}/${variantPath}`;
|
|
175
|
+
if (!buildDirectory) throw new Error("Failed to find Android gradle build directory.");
|
|
176
|
+
return {
|
|
177
|
+
buildDirectory,
|
|
178
|
+
buildArtifactPath: await getBuildOutputFilePath({
|
|
179
|
+
aab: isAabOutput,
|
|
180
|
+
appModuleName: moduleName,
|
|
181
|
+
buildDirectory,
|
|
182
|
+
variant
|
|
183
|
+
})
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get the path to the build output file (APK or AAB)
|
|
188
|
+
*/
|
|
189
|
+
async function getBuildOutputFilePath({ aab, appModuleName, buildDirectory, variant }) {
|
|
190
|
+
const outputFile = `${appModuleName}-${variant}.${aab ? "aab" : "apk"}`;
|
|
191
|
+
const outputFilePath = path.default.join(buildDirectory, outputFile);
|
|
192
|
+
if (fs.default.existsSync(outputFilePath)) return outputFilePath;
|
|
193
|
+
__hot_updater_cli_tools.p.log.error(`Failed to find the output file for ${outputFilePath}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/utils/enrichNativeBuildAndroidScheme.ts
|
|
199
|
+
const enrichNativeBuildAndroidScheme = async ({ schemeConfig }) => {
|
|
200
|
+
return {
|
|
201
|
+
aab: true,
|
|
202
|
+
variant: "Release",
|
|
203
|
+
appModuleName: "app",
|
|
204
|
+
applicationId: schemeConfig.packageName,
|
|
205
|
+
...schemeConfig
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/buildAndroid.ts
|
|
211
|
+
const buildAndroid = async ({ schemeConfig: _schemeConfig }) => {
|
|
212
|
+
const androidProjectPath = path.default.join((0, __hot_updater_cli_tools.getCwd)(), "android");
|
|
213
|
+
const minBundleId = (0, __hot_updater_plugin_core.generateMinBundleId)();
|
|
214
|
+
const schemeConfig = await enrichNativeBuildAndroidScheme({ schemeConfig: _schemeConfig });
|
|
215
|
+
return runGradle({
|
|
216
|
+
args: { extraParams: [`-PMIN_BUNDLE_ID=${minBundleId}`] },
|
|
217
|
+
appModuleName: schemeConfig.appModuleName,
|
|
218
|
+
tasks: schemeConfig.aab ? [`bundle${schemeConfig.variant}`] : [`assemble${schemeConfig.variant}`],
|
|
219
|
+
androidProjectPath
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/utils/device.ts
|
|
225
|
+
/**
|
|
226
|
+
* Get the path to adb executable from ANDROID_HOME or use system adb
|
|
227
|
+
*/
|
|
228
|
+
const getAdbPath = () => {
|
|
229
|
+
return process.env["ANDROID_HOME"] ? path.default.join(process.env["ANDROID_HOME"], "platform-tools", "adb") : "adb";
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Parses the output of the 'adb devices' command
|
|
233
|
+
*/
|
|
234
|
+
const parseDevicesResult = ({ result }) => {
|
|
235
|
+
if (!result) return [];
|
|
236
|
+
const devices = [];
|
|
237
|
+
const lines = result.trim().split(/\r?\n/);
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
const [device, state] = line.split(/[ ,\t]+/).filter((w) => w !== "");
|
|
240
|
+
if (device && state === "device") devices.push(device);
|
|
241
|
+
}
|
|
242
|
+
return devices;
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* Executes the commands needed to get a list of devices from ADB
|
|
246
|
+
*/
|
|
247
|
+
const getConnectedDevices = async () => {
|
|
248
|
+
const adbPath = getAdbPath();
|
|
249
|
+
try {
|
|
250
|
+
const { stdout } = await (0, execa.execa)(adbPath, ["devices"]);
|
|
251
|
+
return parseDevicesResult({ result: stdout });
|
|
252
|
+
} catch {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Runs ADB reverse tcp:8081 tcp:8081 to allow loading the jsbundle from the packager
|
|
258
|
+
* Set up port forwarding from device to development server using adb reverse
|
|
259
|
+
*/
|
|
260
|
+
async function tryRunAdbReverse({ port = 8081, deviceId }) {
|
|
261
|
+
try {
|
|
262
|
+
const adbPath = getAdbPath();
|
|
263
|
+
const adbArgs = [
|
|
264
|
+
"-s",
|
|
265
|
+
deviceId,
|
|
266
|
+
"reverse",
|
|
267
|
+
`tcp:${port}`,
|
|
268
|
+
`tcp:${port}`
|
|
269
|
+
];
|
|
270
|
+
__hot_updater_cli_tools.p.log.info(`Connecting "${deviceId}" to the development server`);
|
|
271
|
+
await (0, execa.execa)(adbPath, adbArgs);
|
|
272
|
+
} catch (_error) {
|
|
273
|
+
throw new Error(`Failed to connect "${deviceId}" to development server using "adb reverse"`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get name of Android emulator
|
|
278
|
+
*/
|
|
279
|
+
const getEmulatorName = async (deviceId) => {
|
|
280
|
+
const { stdout } = await (0, execa.execa)(getAdbPath(), [
|
|
281
|
+
"-s",
|
|
282
|
+
deviceId,
|
|
283
|
+
"emu",
|
|
284
|
+
"avd",
|
|
285
|
+
"name"
|
|
286
|
+
], { stdio: "pipe" });
|
|
287
|
+
if (!stdout) return "";
|
|
288
|
+
return stdout.split(node_os.default.EOL)[0].replace(/(\r\n|\n|\r)/gm, "").trim();
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Get Android device name in readable format
|
|
292
|
+
*/
|
|
293
|
+
const getPhoneName = async (deviceId) => {
|
|
294
|
+
const { stdout } = await (0, execa.execa)(getAdbPath(), [
|
|
295
|
+
"-s",
|
|
296
|
+
deviceId,
|
|
297
|
+
"shell",
|
|
298
|
+
"getprop",
|
|
299
|
+
"ro.product.model"
|
|
300
|
+
], { stdio: "pipe" });
|
|
301
|
+
return stdout.replace(/\[ro\.product\.model\]:\s*\[(.*)\]/, "$1").trim();
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Check if emulator has finished booting
|
|
305
|
+
*/
|
|
306
|
+
const isEmulatorBooted = async (device) => {
|
|
307
|
+
const adbPath = getAdbPath();
|
|
308
|
+
const adbArgs = [
|
|
309
|
+
"-s",
|
|
310
|
+
device,
|
|
311
|
+
"shell",
|
|
312
|
+
"getprop",
|
|
313
|
+
"sys.boot_completed"
|
|
314
|
+
];
|
|
315
|
+
try {
|
|
316
|
+
const { stdout } = await (0, execa.execa)(adbPath, adbArgs, { stdio: "pipe" });
|
|
317
|
+
return stdout.trim() === "1";
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const emulatorCommand = process.env["ANDROID_HOME"] ? `${process.env["ANDROID_HOME"]}/emulator/emulator` : "emulator";
|
|
323
|
+
/**
|
|
324
|
+
* Get a list of available Android emulators
|
|
325
|
+
*/
|
|
326
|
+
const getEmulatorNames = async () => {
|
|
327
|
+
try {
|
|
328
|
+
const { stdout } = await (0, execa.execa)(emulatorCommand, ["-list-avds"], { stdio: "pipe" });
|
|
329
|
+
return stdout.split(node_os.default.EOL).filter((name) => name !== "" && !name.includes(" "));
|
|
330
|
+
} catch {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const checkEmulatorBootStatus = async ({ port, emulatorName, loader, onBooted }) => {
|
|
335
|
+
const connected = (await getConnectedDevices()).find((d) => d.includes(`${port}`));
|
|
336
|
+
if (connected) {
|
|
337
|
+
loader.message(`Emulator "${emulatorName}" is connected. Waiting for boot`);
|
|
338
|
+
if (await isEmulatorBooted(connected)) onBooted(connected);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* Launch the Android emulator and wait for it to boot
|
|
343
|
+
*/
|
|
344
|
+
const launchEmulator = async (emulatorName, port, loader) => {
|
|
345
|
+
const manualCommand = `${emulatorCommand} -avd ${emulatorName}`;
|
|
346
|
+
const timeout = 120;
|
|
347
|
+
const cp = (0, execa.execa)(emulatorCommand, [
|
|
348
|
+
"-avd",
|
|
349
|
+
emulatorName,
|
|
350
|
+
"-port",
|
|
351
|
+
`${port}`
|
|
352
|
+
], {
|
|
353
|
+
detached: true,
|
|
354
|
+
stdio: "ignore"
|
|
355
|
+
});
|
|
356
|
+
cp.unref();
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
let bootCheckInterval;
|
|
359
|
+
let rejectTimeout;
|
|
360
|
+
const cleanup = () => {
|
|
361
|
+
clearTimeout(rejectTimeout);
|
|
362
|
+
clearInterval(bootCheckInterval);
|
|
363
|
+
};
|
|
364
|
+
const handleError = (message) => {
|
|
365
|
+
cleanup();
|
|
366
|
+
reject(new Error(message));
|
|
367
|
+
};
|
|
368
|
+
bootCheckInterval = setInterval(async () => {
|
|
369
|
+
await checkEmulatorBootStatus({
|
|
370
|
+
port,
|
|
371
|
+
emulatorName,
|
|
372
|
+
loader,
|
|
373
|
+
onBooted: (deviceId) => {
|
|
374
|
+
cleanup();
|
|
375
|
+
resolve(deviceId);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}, 1e3);
|
|
379
|
+
rejectTimeout = setTimeout(() => {
|
|
380
|
+
handleError(`It took too long to start and connect with Android emulator: ${emulatorName}. You can try starting the emulator manually from the terminal with: ${manualCommand}`);
|
|
381
|
+
}, timeout * 1e3);
|
|
382
|
+
cp.catch((error) => {
|
|
383
|
+
handleError(error);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
};
|
|
387
|
+
const defaultPort = 5554;
|
|
388
|
+
const maxPort = 5682;
|
|
389
|
+
/**
|
|
390
|
+
* Find an available port for emulator (starting from 5554)
|
|
391
|
+
*/
|
|
392
|
+
const getAvailableDevicePort = async (startPort = defaultPort) => {
|
|
393
|
+
/**
|
|
394
|
+
* The default value is 5554 for the first virtual device instance running on your machine.
|
|
395
|
+
* A virtual device normally occupies a pair of adjacent ports: a console port and an adb port.
|
|
396
|
+
* The console of the first virtual device running on a particular machine uses console port 5554 and adb port 5555.
|
|
397
|
+
* Subsequent instances use port numbers increasing by two.
|
|
398
|
+
* For example, 5556/5557, 5558/5559, and so on. The range is 5554 to 5682, allowing for 64 concurrent virtual devices.
|
|
399
|
+
*/
|
|
400
|
+
const devices = await getConnectedDevices();
|
|
401
|
+
for (let port = startPort; port <= maxPort; port += 2) if (!devices.some((d) => d.includes(port.toString()))) return port;
|
|
402
|
+
throw new Error("Failed to launch emulator: no available ports");
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* Try to launch an emulator (or find the first available one)
|
|
406
|
+
*/
|
|
407
|
+
const tryLaunchEmulator = async (name) => {
|
|
408
|
+
const port = await getAvailableDevicePort();
|
|
409
|
+
const loader = __hot_updater_cli_tools.p.spinner();
|
|
410
|
+
loader.start("Looking for available emulators");
|
|
411
|
+
const emulators = await getEmulatorNames();
|
|
412
|
+
const emulatorName = name ?? emulators[0];
|
|
413
|
+
let deviceId;
|
|
414
|
+
if (emulators.length > 0) try {
|
|
415
|
+
loader.message(`Launching emulator "${emulatorName}"`);
|
|
416
|
+
deviceId = await launchEmulator(emulatorName, port, loader);
|
|
417
|
+
loader.stop(`Launched ${emulatorName} emulator.`);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
loader.stop(`Failed to launch ${emulatorName} emulator. ${error.message}`, 1);
|
|
420
|
+
}
|
|
421
|
+
else loader.stop("No emulators found as an output of `emulator -list-avds`. Please launch an emulator manually or connect a device", 1);
|
|
422
|
+
return deviceId;
|
|
423
|
+
};
|
|
424
|
+
/**
|
|
425
|
+
* List all Android devices and emulators (connected and available)
|
|
426
|
+
*/
|
|
427
|
+
const listDevices = async () => {
|
|
428
|
+
const devices = await getConnectedDevices();
|
|
429
|
+
const allDevices = [];
|
|
430
|
+
for (const deviceId of devices) if (deviceId.includes("emulator")) {
|
|
431
|
+
const emulatorData = {
|
|
432
|
+
deviceId,
|
|
433
|
+
readableName: await getEmulatorName(deviceId),
|
|
434
|
+
connected: true,
|
|
435
|
+
type: "emulator"
|
|
436
|
+
};
|
|
437
|
+
allDevices.push(emulatorData);
|
|
438
|
+
} else {
|
|
439
|
+
const phoneData = {
|
|
440
|
+
deviceId,
|
|
441
|
+
readableName: await getPhoneName(deviceId),
|
|
442
|
+
type: "phone",
|
|
443
|
+
connected: true
|
|
444
|
+
};
|
|
445
|
+
allDevices.push(phoneData);
|
|
446
|
+
}
|
|
447
|
+
const emulatorNames = await getEmulatorNames();
|
|
448
|
+
for (const emulatorName of emulatorNames) {
|
|
449
|
+
if (allDevices.some((device) => device.readableName === emulatorName)) continue;
|
|
450
|
+
const emulatorData = {
|
|
451
|
+
deviceId: void 0,
|
|
452
|
+
readableName: emulatorName,
|
|
453
|
+
type: "emulator",
|
|
454
|
+
connected: false
|
|
455
|
+
};
|
|
456
|
+
allDevices.push(emulatorData);
|
|
457
|
+
}
|
|
458
|
+
return allDevices.sort((a, b) => {
|
|
459
|
+
if (a.type === "phone" && b.type === "emulator") return -1;
|
|
460
|
+
if (a.type === "emulator" && b.type === "phone") return 1;
|
|
461
|
+
return 0;
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
const matchingDevice = (devices, deviceArg) => {
|
|
465
|
+
const deviceByName = devices.find((device) => device.readableName === deviceArg);
|
|
466
|
+
const deviceById = devices.find((d) => d.deviceId === deviceArg);
|
|
467
|
+
return deviceByName || deviceById;
|
|
468
|
+
};
|
|
469
|
+
const selectTargetDevice = async ({ interactive, deviceOption }) => {
|
|
470
|
+
const availableDevices = await listDevices();
|
|
471
|
+
if (deviceOption === true && !interactive) {
|
|
472
|
+
__hot_updater_cli_tools.p.log.error("you can't select device manually with non-interactive cli mode(without -i option).");
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
if (typeof deviceOption === "string") {
|
|
476
|
+
const matchedDevice = matchingDevice(availableDevices, deviceOption);
|
|
477
|
+
if (!matchedDevice) {
|
|
478
|
+
__hot_updater_cli_tools.p.log.error(`device '${deviceOption}' isn't included in the attached devices.`);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
return matchedDevice;
|
|
482
|
+
}
|
|
483
|
+
if (interactive) {
|
|
484
|
+
if (!availableDevices.length) {
|
|
485
|
+
__hot_updater_cli_tools.p.log.error("you passed -d option but there is no attached adb devices.");
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
const device = await __hot_updater_cli_tools.p.select({
|
|
489
|
+
message: "Target Device",
|
|
490
|
+
options: availableDevices.map((d) => ({
|
|
491
|
+
value: d,
|
|
492
|
+
label: d.readableName
|
|
493
|
+
}))
|
|
494
|
+
});
|
|
495
|
+
if (__hot_updater_cli_tools.p.isCancel(device)) process.exit(1);
|
|
496
|
+
return device;
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const Device = {
|
|
500
|
+
getAdbPath,
|
|
501
|
+
getConnectedDevices,
|
|
502
|
+
tryRunAdbReverse,
|
|
503
|
+
tryLaunchEmulator,
|
|
504
|
+
listDevices,
|
|
505
|
+
selectTargetDevice
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/runner/tryInstallAppOnDevice.ts
|
|
510
|
+
const tryInstallAppOnDevice = async ({ device, apkPath }) => {
|
|
511
|
+
if (!device.deviceId) {
|
|
512
|
+
__hot_updater_cli_tools.p.log.warn(`No device with id "${device.deviceId}", skipping launching the app.`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const deviceId = device.deviceId;
|
|
516
|
+
const adbArgs = [
|
|
517
|
+
"-s",
|
|
518
|
+
deviceId,
|
|
519
|
+
"install",
|
|
520
|
+
"-r",
|
|
521
|
+
"-d"
|
|
522
|
+
];
|
|
523
|
+
adbArgs.push(apkPath);
|
|
524
|
+
const adbPath = Device.getAdbPath();
|
|
525
|
+
const loader = __hot_updater_cli_tools.p.spinner();
|
|
526
|
+
loader.start(`Installing the app on ${device.readableName} (id: ${deviceId})`);
|
|
527
|
+
try {
|
|
528
|
+
await (0, execa.execa)(adbPath, adbArgs);
|
|
529
|
+
loader.stop(`Installed the app on ${device.readableName} (id: ${deviceId}).`);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
const errorMessage = error.stdout || error.stderr || "Unknown error";
|
|
532
|
+
if (typeof errorMessage === "string" && errorMessage.includes("INSTALL_FAILED_INSUFFICIENT_STORAGE")) loader.message("Installation failed due to insufficient storage");
|
|
533
|
+
loader.stop(`Failed: Installing the app on ${device.readableName} (id: ${deviceId})`, 1);
|
|
534
|
+
throw new Error(typeof errorMessage === "string" ? errorMessage : "Installation failed");
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
//#endregion
|
|
539
|
+
//#region src/runner/tryLaunchAppOnDevice.ts
|
|
540
|
+
async function tryLaunchAppOnDevice({ device, port, mainActivity, applicationId, packageName }) {
|
|
541
|
+
if (!device.deviceId) {
|
|
542
|
+
__hot_updater_cli_tools.p.log.warn(`No device with id "${device.deviceId}", skipping launching the app.`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const deviceId = device.deviceId;
|
|
546
|
+
await Device.tryRunAdbReverse({
|
|
547
|
+
deviceId,
|
|
548
|
+
port
|
|
549
|
+
});
|
|
550
|
+
const activity = mainActivity || ".MainActivity";
|
|
551
|
+
const adbArgs = [
|
|
552
|
+
"shell",
|
|
553
|
+
"am",
|
|
554
|
+
"start",
|
|
555
|
+
"-n",
|
|
556
|
+
`${applicationId}/${activity.startsWith(packageName) || !activity.startsWith(".") && activity.includes(".") ? activity : activity.startsWith(".") ? [packageName, activity].join("") : [packageName, activity].filter(Boolean).join(".")}`,
|
|
557
|
+
"-a",
|
|
558
|
+
"android.intent.action.MAIN",
|
|
559
|
+
"-c",
|
|
560
|
+
"android.intent.category.LAUNCHER"
|
|
561
|
+
];
|
|
562
|
+
adbArgs.unshift("-s", deviceId);
|
|
563
|
+
const adbPath = Device.getAdbPath();
|
|
564
|
+
console.debug(`Running ${adbPath} ${adbArgs.join(" ")}.`);
|
|
565
|
+
const loader = __hot_updater_cli_tools.p.spinner();
|
|
566
|
+
loader.start(`Launching the app on ${device.readableName} (id: ${deviceId})`);
|
|
567
|
+
try {
|
|
568
|
+
await (0, execa.execa)(adbPath, adbArgs);
|
|
569
|
+
loader.stop(`Launched the app on ${device.readableName} (id: ${deviceId}) and listening on port ${port}.`);
|
|
570
|
+
} catch (_error) {
|
|
571
|
+
loader.stop("Failed to launch the app.", 1);
|
|
572
|
+
throw new Error(`Failed to launch the app on ${device.readableName}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/runAndroid.ts
|
|
578
|
+
const runAndroid = async ({ schemeConfig: _schemeConfig, runOption }) => {
|
|
579
|
+
const { interactive, device: deviceOption } = runOption;
|
|
580
|
+
const androidProjectPath = path.default.join((0, __hot_updater_cli_tools.getCwd)(), "android");
|
|
581
|
+
const schemeConfig = await enrichNativeBuildAndroidScheme({ schemeConfig: _schemeConfig });
|
|
582
|
+
if (schemeConfig.aab) {
|
|
583
|
+
__hot_updater_cli_tools.p.log.error("aab scheme can't not be run");
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
const device = await Device.selectTargetDevice({
|
|
587
|
+
deviceOption,
|
|
588
|
+
interactive
|
|
589
|
+
});
|
|
590
|
+
if (device) {
|
|
591
|
+
if (!(await Device.getConnectedDevices()).includes(device.deviceId || "")) {
|
|
592
|
+
if (device.type === "emulator") device.deviceId = await Device.tryLaunchEmulator(device.readableName);
|
|
593
|
+
}
|
|
594
|
+
if (!device.deviceId) throw new Error("Failed to run on any device");
|
|
595
|
+
} else if ((await Device.getConnectedDevices()).length === 0) {
|
|
596
|
+
__hot_updater_cli_tools.p.log.info("No devices found. Launching first available emulator.");
|
|
597
|
+
await Device.tryLaunchEmulator();
|
|
598
|
+
}
|
|
599
|
+
const task = `assemble${schemeConfig.variant}`;
|
|
600
|
+
const result = await runGradle({
|
|
601
|
+
args: { extraParams: [`-PMIN_BUNDLE_ID=${(0, __hot_updater_plugin_core.generateMinBundleId)()}`] },
|
|
602
|
+
appModuleName: schemeConfig.appModuleName,
|
|
603
|
+
tasks: [task],
|
|
604
|
+
androidProjectPath
|
|
605
|
+
});
|
|
606
|
+
const targetDevices = device ? [device] : (await Device.listDevices()).filter((d) => d.connected);
|
|
607
|
+
for (const targetDevice of targetDevices) {
|
|
608
|
+
await tryInstallAppOnDevice({
|
|
609
|
+
apkPath: result.buildArtifactPath,
|
|
610
|
+
device: targetDevice
|
|
611
|
+
});
|
|
612
|
+
await tryLaunchAppOnDevice({
|
|
613
|
+
applicationId: schemeConfig.applicationId,
|
|
614
|
+
device: targetDevice,
|
|
615
|
+
mainActivity: runOption.mainActivity,
|
|
616
|
+
packageName: schemeConfig.packageName,
|
|
617
|
+
port: runOption.port
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
__hot_updater_cli_tools.p.outro("Success 🎉");
|
|
621
|
+
return result;
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
//#endregion
|
|
625
|
+
exports.buildAndroid = buildAndroid;
|
|
626
|
+
exports.runAndroid = runAndroid;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NativeBuildAndroidScheme, NativeBuildOptions } from "@hot-updater/plugin-core";
|
|
2
|
+
|
|
3
|
+
//#region src/buildAndroid.d.ts
|
|
4
|
+
declare const buildAndroid: ({
|
|
5
|
+
schemeConfig: _schemeConfig
|
|
6
|
+
}: {
|
|
7
|
+
schemeConfig: NativeBuildAndroidScheme;
|
|
8
|
+
}) => Promise<{
|
|
9
|
+
buildDirectory: string;
|
|
10
|
+
buildArtifactPath: string;
|
|
11
|
+
}>;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/types.d.ts
|
|
14
|
+
interface AndroidNativeRunOptions extends NativeBuildOptions {
|
|
15
|
+
device?: string | boolean;
|
|
16
|
+
port?: string;
|
|
17
|
+
mainActivity?: string;
|
|
18
|
+
user?: string;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/runAndroid.d.ts
|
|
22
|
+
declare const runAndroid: ({
|
|
23
|
+
schemeConfig: _schemeConfig,
|
|
24
|
+
runOption
|
|
25
|
+
}: {
|
|
26
|
+
schemeConfig: NativeBuildAndroidScheme;
|
|
27
|
+
runOption: AndroidNativeRunOptions;
|
|
28
|
+
}) => Promise<{
|
|
29
|
+
buildDirectory: string;
|
|
30
|
+
buildArtifactPath: string;
|
|
31
|
+
}>;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { type AndroidNativeRunOptions, buildAndroid, runAndroid };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NativeBuildAndroidScheme, NativeBuildOptions } from "@hot-updater/plugin-core";
|
|
2
|
+
|
|
3
|
+
//#region src/buildAndroid.d.ts
|
|
4
|
+
declare const buildAndroid: ({
|
|
5
|
+
schemeConfig: _schemeConfig
|
|
6
|
+
}: {
|
|
7
|
+
schemeConfig: NativeBuildAndroidScheme;
|
|
8
|
+
}) => Promise<{
|
|
9
|
+
buildDirectory: string;
|
|
10
|
+
buildArtifactPath: string;
|
|
11
|
+
}>;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/types.d.ts
|
|
14
|
+
interface AndroidNativeRunOptions extends NativeBuildOptions {
|
|
15
|
+
device?: string | boolean;
|
|
16
|
+
port?: string;
|
|
17
|
+
mainActivity?: string;
|
|
18
|
+
user?: string;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/runAndroid.d.ts
|
|
22
|
+
declare const runAndroid: ({
|
|
23
|
+
schemeConfig: _schemeConfig,
|
|
24
|
+
runOption
|
|
25
|
+
}: {
|
|
26
|
+
schemeConfig: NativeBuildAndroidScheme;
|
|
27
|
+
runOption: AndroidNativeRunOptions;
|
|
28
|
+
}) => Promise<{
|
|
29
|
+
buildDirectory: string;
|
|
30
|
+
buildArtifactPath: string;
|
|
31
|
+
}>;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { type AndroidNativeRunOptions, buildAndroid, runAndroid };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { BuildLogger, getCwd, p } from "@hot-updater/cli-tools";
|
|
2
|
+
import { generateMinBundleId } from "@hot-updater/plugin-core";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { ExecaError, execa } from "execa";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
|
|
8
|
+
//#region src/utils/createGradleLogger.ts
|
|
9
|
+
const createGradleLogger = () => new BuildLogger({
|
|
10
|
+
failurePatterns: ["BUILD FAILED"],
|
|
11
|
+
importantLogPatterns: [
|
|
12
|
+
"error:",
|
|
13
|
+
"Error:",
|
|
14
|
+
"ERROR:",
|
|
15
|
+
"> Task :",
|
|
16
|
+
"BUILD FAILED",
|
|
17
|
+
"BUILD SUCCESSFUL",
|
|
18
|
+
"FAILURE: Build failed with an exception.",
|
|
19
|
+
"* What went wrong:",
|
|
20
|
+
"Execution failed for task",
|
|
21
|
+
"Could not resolve",
|
|
22
|
+
"Failed to",
|
|
23
|
+
"Unable to load script",
|
|
24
|
+
"Metro encountered an error",
|
|
25
|
+
"React Native CLI",
|
|
26
|
+
"Android resource compilation failed",
|
|
27
|
+
"Duplicate class",
|
|
28
|
+
"Program type already present",
|
|
29
|
+
"actionable tasks:"
|
|
30
|
+
],
|
|
31
|
+
progressMapping: [
|
|
32
|
+
[[
|
|
33
|
+
"buildKotlinToolingMetadata",
|
|
34
|
+
"generateAutolinkingNewArchitectureFiles",
|
|
35
|
+
"generateCodegenSchemaFromJavaScript"
|
|
36
|
+
], 5],
|
|
37
|
+
[[
|
|
38
|
+
"createBundleReleaseJsAndAssets",
|
|
39
|
+
"bundleReleaseJsAndAssets",
|
|
40
|
+
"bundleDebugJsAndAssets"
|
|
41
|
+
], 15],
|
|
42
|
+
[[
|
|
43
|
+
"generateReleaseResValues",
|
|
44
|
+
"generateReleaseResources",
|
|
45
|
+
"mergeReleaseResources"
|
|
46
|
+
], 25],
|
|
47
|
+
[[
|
|
48
|
+
"processReleaseResources",
|
|
49
|
+
"processReleaseManifest",
|
|
50
|
+
"parseReleaseLocalResources"
|
|
51
|
+
], 35],
|
|
52
|
+
[["compileReleaseKotlin", "compileDebugKotlin"], 50],
|
|
53
|
+
[["compileReleaseJavaWithJavac", "compileDebugJavaWithJavac"], 60],
|
|
54
|
+
[[
|
|
55
|
+
"dexBuilderRelease",
|
|
56
|
+
"mergeDexRelease",
|
|
57
|
+
"transformClassesWithDex"
|
|
58
|
+
], 75],
|
|
59
|
+
[[
|
|
60
|
+
"compressReleaseAssets",
|
|
61
|
+
"optimizeReleaseResources",
|
|
62
|
+
"mergeReleaseAssets"
|
|
63
|
+
], 85],
|
|
64
|
+
[[
|
|
65
|
+
"packageRelease",
|
|
66
|
+
"packageDebug",
|
|
67
|
+
"assembleRelease",
|
|
68
|
+
"assembleDebug"
|
|
69
|
+
], 90],
|
|
70
|
+
[["bundleRelease", "bundleDebug"], 95],
|
|
71
|
+
[[/\d+ actionable tasks:/], 100]
|
|
72
|
+
]
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/builder/runGradle.ts
|
|
77
|
+
/**
|
|
78
|
+
* Clean up gradle error message by removing common noise
|
|
79
|
+
*/
|
|
80
|
+
const getCleanedErrorMessage = (error) => {
|
|
81
|
+
const gradleLinesToRemove = [
|
|
82
|
+
"FAILURE: Build failed with an exception.",
|
|
83
|
+
"* Try:",
|
|
84
|
+
"> Run with --stacktrace option to get the stack trace.",
|
|
85
|
+
"> Run with --info or --debug option to get more log output.",
|
|
86
|
+
"> Run with --scan to get full insights.",
|
|
87
|
+
"> Get more help at [undefined](https://help.gradle.org).",
|
|
88
|
+
"> Get more help at https://help.gradle.org.",
|
|
89
|
+
"BUILD FAILED"
|
|
90
|
+
];
|
|
91
|
+
return error.message.split("\n").filter((line) => !gradleLinesToRemove.some((l) => line.includes(l))).join("\n").trim();
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Generate gradle task names with module prefix
|
|
95
|
+
*/
|
|
96
|
+
function getTaskNames(moduleName, tasks) {
|
|
97
|
+
return tasks.map((task) => `${moduleName}:${task}`);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get gradle wrapper command based on platform
|
|
101
|
+
*/
|
|
102
|
+
const getGradleWrapper = () => process.platform.startsWith("win") ? "gradlew.bat" : "./gradlew";
|
|
103
|
+
/**
|
|
104
|
+
* Run gradle build with specified tasks and arguments
|
|
105
|
+
*/
|
|
106
|
+
async function runGradle({ tasks, args, androidProjectPath, appModuleName }) {
|
|
107
|
+
const gradleArgs = getTaskNames(appModuleName, tasks);
|
|
108
|
+
gradleArgs.push("-x", "lint");
|
|
109
|
+
if (args.extraParams) gradleArgs.push(...args.extraParams);
|
|
110
|
+
p.log.info(`Run Gradle Settings:
|
|
111
|
+
Project ${androidProjectPath}
|
|
112
|
+
App Moudle ${appModuleName}
|
|
113
|
+
Tasks ${tasks.join(", ")}
|
|
114
|
+
Args ${gradleArgs.join(" ")}
|
|
115
|
+
`);
|
|
116
|
+
if ("port" in args && args.port != null) gradleArgs.push(`-PreactNativeDevServerPort=${args.port}`);
|
|
117
|
+
const logger = createGradleLogger();
|
|
118
|
+
logger.start(`${appModuleName}`);
|
|
119
|
+
try {
|
|
120
|
+
const process$1 = execa(getGradleWrapper(), gradleArgs, { cwd: androidProjectPath });
|
|
121
|
+
for await (const line of process$1) if (line) logger.processLine(line);
|
|
122
|
+
logger.stop();
|
|
123
|
+
} catch (e) {
|
|
124
|
+
logger.stop("Build failed", false);
|
|
125
|
+
if (e instanceof ExecaError) p.log.error(getCleanedErrorMessage(e));
|
|
126
|
+
else if (e instanceof Error) p.log.error(e.message);
|
|
127
|
+
throw new Error("Failed to build the app. See the error above for details from Gradle.");
|
|
128
|
+
}
|
|
129
|
+
return findBuildDirectory({
|
|
130
|
+
androidProjectPath,
|
|
131
|
+
moduleName: appModuleName,
|
|
132
|
+
tasks
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Find the build output directory and artifact path
|
|
137
|
+
*/
|
|
138
|
+
async function findBuildDirectory({ moduleName, tasks, androidProjectPath }) {
|
|
139
|
+
const selectedTask = tasks.find((t) => t.startsWith("install") || t.startsWith("assemble") || t.startsWith("bundle"));
|
|
140
|
+
if (!selectedTask) throw new Error(`Not supported gradle task: ${tasks.join(", ")}`);
|
|
141
|
+
const variantFromSelectedTask = selectedTask?.replace("install", "")?.replace("assemble", "")?.replace("bundle", "").split(/(?=[A-Z])/);
|
|
142
|
+
const variantPath = variantFromSelectedTask?.join("/")?.toLowerCase();
|
|
143
|
+
const variant = variantFromSelectedTask?.join("-")?.toLowerCase();
|
|
144
|
+
const isAabOutput = selectedTask?.includes("bundle") === true;
|
|
145
|
+
const buildDirectory = `${androidProjectPath}/${moduleName}/build/outputs/${isAabOutput ? "bundle" : "apk"}/${variantPath}`;
|
|
146
|
+
if (!buildDirectory) throw new Error("Failed to find Android gradle build directory.");
|
|
147
|
+
return {
|
|
148
|
+
buildDirectory,
|
|
149
|
+
buildArtifactPath: await getBuildOutputFilePath({
|
|
150
|
+
aab: isAabOutput,
|
|
151
|
+
appModuleName: moduleName,
|
|
152
|
+
buildDirectory,
|
|
153
|
+
variant
|
|
154
|
+
})
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get the path to the build output file (APK or AAB)
|
|
159
|
+
*/
|
|
160
|
+
async function getBuildOutputFilePath({ aab, appModuleName, buildDirectory, variant }) {
|
|
161
|
+
const outputFile = `${appModuleName}-${variant}.${aab ? "aab" : "apk"}`;
|
|
162
|
+
const outputFilePath = path.join(buildDirectory, outputFile);
|
|
163
|
+
if (fs.existsSync(outputFilePath)) return outputFilePath;
|
|
164
|
+
p.log.error(`Failed to find the output file for ${outputFilePath}`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/utils/enrichNativeBuildAndroidScheme.ts
|
|
170
|
+
const enrichNativeBuildAndroidScheme = async ({ schemeConfig }) => {
|
|
171
|
+
return {
|
|
172
|
+
aab: true,
|
|
173
|
+
variant: "Release",
|
|
174
|
+
appModuleName: "app",
|
|
175
|
+
applicationId: schemeConfig.packageName,
|
|
176
|
+
...schemeConfig
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/buildAndroid.ts
|
|
182
|
+
const buildAndroid = async ({ schemeConfig: _schemeConfig }) => {
|
|
183
|
+
const androidProjectPath = path.join(getCwd(), "android");
|
|
184
|
+
const minBundleId = generateMinBundleId();
|
|
185
|
+
const schemeConfig = await enrichNativeBuildAndroidScheme({ schemeConfig: _schemeConfig });
|
|
186
|
+
return runGradle({
|
|
187
|
+
args: { extraParams: [`-PMIN_BUNDLE_ID=${minBundleId}`] },
|
|
188
|
+
appModuleName: schemeConfig.appModuleName,
|
|
189
|
+
tasks: schemeConfig.aab ? [`bundle${schemeConfig.variant}`] : [`assemble${schemeConfig.variant}`],
|
|
190
|
+
androidProjectPath
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/utils/device.ts
|
|
196
|
+
/**
|
|
197
|
+
* Get the path to adb executable from ANDROID_HOME or use system adb
|
|
198
|
+
*/
|
|
199
|
+
const getAdbPath = () => {
|
|
200
|
+
return process.env["ANDROID_HOME"] ? path.join(process.env["ANDROID_HOME"], "platform-tools", "adb") : "adb";
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Parses the output of the 'adb devices' command
|
|
204
|
+
*/
|
|
205
|
+
const parseDevicesResult = ({ result }) => {
|
|
206
|
+
if (!result) return [];
|
|
207
|
+
const devices = [];
|
|
208
|
+
const lines = result.trim().split(/\r?\n/);
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
const [device, state] = line.split(/[ ,\t]+/).filter((w) => w !== "");
|
|
211
|
+
if (device && state === "device") devices.push(device);
|
|
212
|
+
}
|
|
213
|
+
return devices;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Executes the commands needed to get a list of devices from ADB
|
|
217
|
+
*/
|
|
218
|
+
const getConnectedDevices = async () => {
|
|
219
|
+
const adbPath = getAdbPath();
|
|
220
|
+
try {
|
|
221
|
+
const { stdout } = await execa(adbPath, ["devices"]);
|
|
222
|
+
return parseDevicesResult({ result: stdout });
|
|
223
|
+
} catch {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Runs ADB reverse tcp:8081 tcp:8081 to allow loading the jsbundle from the packager
|
|
229
|
+
* Set up port forwarding from device to development server using adb reverse
|
|
230
|
+
*/
|
|
231
|
+
async function tryRunAdbReverse({ port = 8081, deviceId }) {
|
|
232
|
+
try {
|
|
233
|
+
const adbPath = getAdbPath();
|
|
234
|
+
const adbArgs = [
|
|
235
|
+
"-s",
|
|
236
|
+
deviceId,
|
|
237
|
+
"reverse",
|
|
238
|
+
`tcp:${port}`,
|
|
239
|
+
`tcp:${port}`
|
|
240
|
+
];
|
|
241
|
+
p.log.info(`Connecting "${deviceId}" to the development server`);
|
|
242
|
+
await execa(adbPath, adbArgs);
|
|
243
|
+
} catch (_error) {
|
|
244
|
+
throw new Error(`Failed to connect "${deviceId}" to development server using "adb reverse"`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get name of Android emulator
|
|
249
|
+
*/
|
|
250
|
+
const getEmulatorName = async (deviceId) => {
|
|
251
|
+
const { stdout } = await execa(getAdbPath(), [
|
|
252
|
+
"-s",
|
|
253
|
+
deviceId,
|
|
254
|
+
"emu",
|
|
255
|
+
"avd",
|
|
256
|
+
"name"
|
|
257
|
+
], { stdio: "pipe" });
|
|
258
|
+
if (!stdout) return "";
|
|
259
|
+
return stdout.split(os.EOL)[0].replace(/(\r\n|\n|\r)/gm, "").trim();
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Get Android device name in readable format
|
|
263
|
+
*/
|
|
264
|
+
const getPhoneName = async (deviceId) => {
|
|
265
|
+
const { stdout } = await execa(getAdbPath(), [
|
|
266
|
+
"-s",
|
|
267
|
+
deviceId,
|
|
268
|
+
"shell",
|
|
269
|
+
"getprop",
|
|
270
|
+
"ro.product.model"
|
|
271
|
+
], { stdio: "pipe" });
|
|
272
|
+
return stdout.replace(/\[ro\.product\.model\]:\s*\[(.*)\]/, "$1").trim();
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Check if emulator has finished booting
|
|
276
|
+
*/
|
|
277
|
+
const isEmulatorBooted = async (device) => {
|
|
278
|
+
const adbPath = getAdbPath();
|
|
279
|
+
const adbArgs = [
|
|
280
|
+
"-s",
|
|
281
|
+
device,
|
|
282
|
+
"shell",
|
|
283
|
+
"getprop",
|
|
284
|
+
"sys.boot_completed"
|
|
285
|
+
];
|
|
286
|
+
try {
|
|
287
|
+
const { stdout } = await execa(adbPath, adbArgs, { stdio: "pipe" });
|
|
288
|
+
return stdout.trim() === "1";
|
|
289
|
+
} catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const emulatorCommand = process.env["ANDROID_HOME"] ? `${process.env["ANDROID_HOME"]}/emulator/emulator` : "emulator";
|
|
294
|
+
/**
|
|
295
|
+
* Get a list of available Android emulators
|
|
296
|
+
*/
|
|
297
|
+
const getEmulatorNames = async () => {
|
|
298
|
+
try {
|
|
299
|
+
const { stdout } = await execa(emulatorCommand, ["-list-avds"], { stdio: "pipe" });
|
|
300
|
+
return stdout.split(os.EOL).filter((name) => name !== "" && !name.includes(" "));
|
|
301
|
+
} catch {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const checkEmulatorBootStatus = async ({ port, emulatorName, loader, onBooted }) => {
|
|
306
|
+
const connected = (await getConnectedDevices()).find((d) => d.includes(`${port}`));
|
|
307
|
+
if (connected) {
|
|
308
|
+
loader.message(`Emulator "${emulatorName}" is connected. Waiting for boot`);
|
|
309
|
+
if (await isEmulatorBooted(connected)) onBooted(connected);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* Launch the Android emulator and wait for it to boot
|
|
314
|
+
*/
|
|
315
|
+
const launchEmulator = async (emulatorName, port, loader) => {
|
|
316
|
+
const manualCommand = `${emulatorCommand} -avd ${emulatorName}`;
|
|
317
|
+
const timeout = 120;
|
|
318
|
+
const cp = execa(emulatorCommand, [
|
|
319
|
+
"-avd",
|
|
320
|
+
emulatorName,
|
|
321
|
+
"-port",
|
|
322
|
+
`${port}`
|
|
323
|
+
], {
|
|
324
|
+
detached: true,
|
|
325
|
+
stdio: "ignore"
|
|
326
|
+
});
|
|
327
|
+
cp.unref();
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
let bootCheckInterval;
|
|
330
|
+
let rejectTimeout;
|
|
331
|
+
const cleanup = () => {
|
|
332
|
+
clearTimeout(rejectTimeout);
|
|
333
|
+
clearInterval(bootCheckInterval);
|
|
334
|
+
};
|
|
335
|
+
const handleError = (message) => {
|
|
336
|
+
cleanup();
|
|
337
|
+
reject(new Error(message));
|
|
338
|
+
};
|
|
339
|
+
bootCheckInterval = setInterval(async () => {
|
|
340
|
+
await checkEmulatorBootStatus({
|
|
341
|
+
port,
|
|
342
|
+
emulatorName,
|
|
343
|
+
loader,
|
|
344
|
+
onBooted: (deviceId) => {
|
|
345
|
+
cleanup();
|
|
346
|
+
resolve(deviceId);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}, 1e3);
|
|
350
|
+
rejectTimeout = setTimeout(() => {
|
|
351
|
+
handleError(`It took too long to start and connect with Android emulator: ${emulatorName}. You can try starting the emulator manually from the terminal with: ${manualCommand}`);
|
|
352
|
+
}, timeout * 1e3);
|
|
353
|
+
cp.catch((error) => {
|
|
354
|
+
handleError(error);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
const defaultPort = 5554;
|
|
359
|
+
const maxPort = 5682;
|
|
360
|
+
/**
|
|
361
|
+
* Find an available port for emulator (starting from 5554)
|
|
362
|
+
*/
|
|
363
|
+
const getAvailableDevicePort = async (startPort = defaultPort) => {
|
|
364
|
+
/**
|
|
365
|
+
* The default value is 5554 for the first virtual device instance running on your machine.
|
|
366
|
+
* A virtual device normally occupies a pair of adjacent ports: a console port and an adb port.
|
|
367
|
+
* The console of the first virtual device running on a particular machine uses console port 5554 and adb port 5555.
|
|
368
|
+
* Subsequent instances use port numbers increasing by two.
|
|
369
|
+
* For example, 5556/5557, 5558/5559, and so on. The range is 5554 to 5682, allowing for 64 concurrent virtual devices.
|
|
370
|
+
*/
|
|
371
|
+
const devices = await getConnectedDevices();
|
|
372
|
+
for (let port = startPort; port <= maxPort; port += 2) if (!devices.some((d) => d.includes(port.toString()))) return port;
|
|
373
|
+
throw new Error("Failed to launch emulator: no available ports");
|
|
374
|
+
};
|
|
375
|
+
/**
|
|
376
|
+
* Try to launch an emulator (or find the first available one)
|
|
377
|
+
*/
|
|
378
|
+
const tryLaunchEmulator = async (name) => {
|
|
379
|
+
const port = await getAvailableDevicePort();
|
|
380
|
+
const loader = p.spinner();
|
|
381
|
+
loader.start("Looking for available emulators");
|
|
382
|
+
const emulators = await getEmulatorNames();
|
|
383
|
+
const emulatorName = name ?? emulators[0];
|
|
384
|
+
let deviceId;
|
|
385
|
+
if (emulators.length > 0) try {
|
|
386
|
+
loader.message(`Launching emulator "${emulatorName}"`);
|
|
387
|
+
deviceId = await launchEmulator(emulatorName, port, loader);
|
|
388
|
+
loader.stop(`Launched ${emulatorName} emulator.`);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
loader.stop(`Failed to launch ${emulatorName} emulator. ${error.message}`, 1);
|
|
391
|
+
}
|
|
392
|
+
else loader.stop("No emulators found as an output of `emulator -list-avds`. Please launch an emulator manually or connect a device", 1);
|
|
393
|
+
return deviceId;
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* List all Android devices and emulators (connected and available)
|
|
397
|
+
*/
|
|
398
|
+
const listDevices = async () => {
|
|
399
|
+
const devices = await getConnectedDevices();
|
|
400
|
+
const allDevices = [];
|
|
401
|
+
for (const deviceId of devices) if (deviceId.includes("emulator")) {
|
|
402
|
+
const emulatorData = {
|
|
403
|
+
deviceId,
|
|
404
|
+
readableName: await getEmulatorName(deviceId),
|
|
405
|
+
connected: true,
|
|
406
|
+
type: "emulator"
|
|
407
|
+
};
|
|
408
|
+
allDevices.push(emulatorData);
|
|
409
|
+
} else {
|
|
410
|
+
const phoneData = {
|
|
411
|
+
deviceId,
|
|
412
|
+
readableName: await getPhoneName(deviceId),
|
|
413
|
+
type: "phone",
|
|
414
|
+
connected: true
|
|
415
|
+
};
|
|
416
|
+
allDevices.push(phoneData);
|
|
417
|
+
}
|
|
418
|
+
const emulatorNames = await getEmulatorNames();
|
|
419
|
+
for (const emulatorName of emulatorNames) {
|
|
420
|
+
if (allDevices.some((device) => device.readableName === emulatorName)) continue;
|
|
421
|
+
const emulatorData = {
|
|
422
|
+
deviceId: void 0,
|
|
423
|
+
readableName: emulatorName,
|
|
424
|
+
type: "emulator",
|
|
425
|
+
connected: false
|
|
426
|
+
};
|
|
427
|
+
allDevices.push(emulatorData);
|
|
428
|
+
}
|
|
429
|
+
return allDevices.sort((a, b) => {
|
|
430
|
+
if (a.type === "phone" && b.type === "emulator") return -1;
|
|
431
|
+
if (a.type === "emulator" && b.type === "phone") return 1;
|
|
432
|
+
return 0;
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
const matchingDevice = (devices, deviceArg) => {
|
|
436
|
+
const deviceByName = devices.find((device) => device.readableName === deviceArg);
|
|
437
|
+
const deviceById = devices.find((d) => d.deviceId === deviceArg);
|
|
438
|
+
return deviceByName || deviceById;
|
|
439
|
+
};
|
|
440
|
+
const selectTargetDevice = async ({ interactive, deviceOption }) => {
|
|
441
|
+
const availableDevices = await listDevices();
|
|
442
|
+
if (deviceOption === true && !interactive) {
|
|
443
|
+
p.log.error("you can't select device manually with non-interactive cli mode(without -i option).");
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
if (typeof deviceOption === "string") {
|
|
447
|
+
const matchedDevice = matchingDevice(availableDevices, deviceOption);
|
|
448
|
+
if (!matchedDevice) {
|
|
449
|
+
p.log.error(`device '${deviceOption}' isn't included in the attached devices.`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
return matchedDevice;
|
|
453
|
+
}
|
|
454
|
+
if (interactive) {
|
|
455
|
+
if (!availableDevices.length) {
|
|
456
|
+
p.log.error("you passed -d option but there is no attached adb devices.");
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
const device = await p.select({
|
|
460
|
+
message: "Target Device",
|
|
461
|
+
options: availableDevices.map((d) => ({
|
|
462
|
+
value: d,
|
|
463
|
+
label: d.readableName
|
|
464
|
+
}))
|
|
465
|
+
});
|
|
466
|
+
if (p.isCancel(device)) process.exit(1);
|
|
467
|
+
return device;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const Device = {
|
|
471
|
+
getAdbPath,
|
|
472
|
+
getConnectedDevices,
|
|
473
|
+
tryRunAdbReverse,
|
|
474
|
+
tryLaunchEmulator,
|
|
475
|
+
listDevices,
|
|
476
|
+
selectTargetDevice
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/runner/tryInstallAppOnDevice.ts
|
|
481
|
+
const tryInstallAppOnDevice = async ({ device, apkPath }) => {
|
|
482
|
+
if (!device.deviceId) {
|
|
483
|
+
p.log.warn(`No device with id "${device.deviceId}", skipping launching the app.`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const deviceId = device.deviceId;
|
|
487
|
+
const adbArgs = [
|
|
488
|
+
"-s",
|
|
489
|
+
deviceId,
|
|
490
|
+
"install",
|
|
491
|
+
"-r",
|
|
492
|
+
"-d"
|
|
493
|
+
];
|
|
494
|
+
adbArgs.push(apkPath);
|
|
495
|
+
const adbPath = Device.getAdbPath();
|
|
496
|
+
const loader = p.spinner();
|
|
497
|
+
loader.start(`Installing the app on ${device.readableName} (id: ${deviceId})`);
|
|
498
|
+
try {
|
|
499
|
+
await execa(adbPath, adbArgs);
|
|
500
|
+
loader.stop(`Installed the app on ${device.readableName} (id: ${deviceId}).`);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
const errorMessage = error.stdout || error.stderr || "Unknown error";
|
|
503
|
+
if (typeof errorMessage === "string" && errorMessage.includes("INSTALL_FAILED_INSUFFICIENT_STORAGE")) loader.message("Installation failed due to insufficient storage");
|
|
504
|
+
loader.stop(`Failed: Installing the app on ${device.readableName} (id: ${deviceId})`, 1);
|
|
505
|
+
throw new Error(typeof errorMessage === "string" ? errorMessage : "Installation failed");
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/runner/tryLaunchAppOnDevice.ts
|
|
511
|
+
async function tryLaunchAppOnDevice({ device, port, mainActivity, applicationId, packageName }) {
|
|
512
|
+
if (!device.deviceId) {
|
|
513
|
+
p.log.warn(`No device with id "${device.deviceId}", skipping launching the app.`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const deviceId = device.deviceId;
|
|
517
|
+
await Device.tryRunAdbReverse({
|
|
518
|
+
deviceId,
|
|
519
|
+
port
|
|
520
|
+
});
|
|
521
|
+
const activity = mainActivity || ".MainActivity";
|
|
522
|
+
const adbArgs = [
|
|
523
|
+
"shell",
|
|
524
|
+
"am",
|
|
525
|
+
"start",
|
|
526
|
+
"-n",
|
|
527
|
+
`${applicationId}/${activity.startsWith(packageName) || !activity.startsWith(".") && activity.includes(".") ? activity : activity.startsWith(".") ? [packageName, activity].join("") : [packageName, activity].filter(Boolean).join(".")}`,
|
|
528
|
+
"-a",
|
|
529
|
+
"android.intent.action.MAIN",
|
|
530
|
+
"-c",
|
|
531
|
+
"android.intent.category.LAUNCHER"
|
|
532
|
+
];
|
|
533
|
+
adbArgs.unshift("-s", deviceId);
|
|
534
|
+
const adbPath = Device.getAdbPath();
|
|
535
|
+
console.debug(`Running ${adbPath} ${adbArgs.join(" ")}.`);
|
|
536
|
+
const loader = p.spinner();
|
|
537
|
+
loader.start(`Launching the app on ${device.readableName} (id: ${deviceId})`);
|
|
538
|
+
try {
|
|
539
|
+
await execa(adbPath, adbArgs);
|
|
540
|
+
loader.stop(`Launched the app on ${device.readableName} (id: ${deviceId}) and listening on port ${port}.`);
|
|
541
|
+
} catch (_error) {
|
|
542
|
+
loader.stop("Failed to launch the app.", 1);
|
|
543
|
+
throw new Error(`Failed to launch the app on ${device.readableName}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/runAndroid.ts
|
|
549
|
+
const runAndroid = async ({ schemeConfig: _schemeConfig, runOption }) => {
|
|
550
|
+
const { interactive, device: deviceOption } = runOption;
|
|
551
|
+
const androidProjectPath = path.join(getCwd(), "android");
|
|
552
|
+
const schemeConfig = await enrichNativeBuildAndroidScheme({ schemeConfig: _schemeConfig });
|
|
553
|
+
if (schemeConfig.aab) {
|
|
554
|
+
p.log.error("aab scheme can't not be run");
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
const device = await Device.selectTargetDevice({
|
|
558
|
+
deviceOption,
|
|
559
|
+
interactive
|
|
560
|
+
});
|
|
561
|
+
if (device) {
|
|
562
|
+
if (!(await Device.getConnectedDevices()).includes(device.deviceId || "")) {
|
|
563
|
+
if (device.type === "emulator") device.deviceId = await Device.tryLaunchEmulator(device.readableName);
|
|
564
|
+
}
|
|
565
|
+
if (!device.deviceId) throw new Error("Failed to run on any device");
|
|
566
|
+
} else if ((await Device.getConnectedDevices()).length === 0) {
|
|
567
|
+
p.log.info("No devices found. Launching first available emulator.");
|
|
568
|
+
await Device.tryLaunchEmulator();
|
|
569
|
+
}
|
|
570
|
+
const task = `assemble${schemeConfig.variant}`;
|
|
571
|
+
const result = await runGradle({
|
|
572
|
+
args: { extraParams: [`-PMIN_BUNDLE_ID=${generateMinBundleId()}`] },
|
|
573
|
+
appModuleName: schemeConfig.appModuleName,
|
|
574
|
+
tasks: [task],
|
|
575
|
+
androidProjectPath
|
|
576
|
+
});
|
|
577
|
+
const targetDevices = device ? [device] : (await Device.listDevices()).filter((d) => d.connected);
|
|
578
|
+
for (const targetDevice of targetDevices) {
|
|
579
|
+
await tryInstallAppOnDevice({
|
|
580
|
+
apkPath: result.buildArtifactPath,
|
|
581
|
+
device: targetDevice
|
|
582
|
+
});
|
|
583
|
+
await tryLaunchAppOnDevice({
|
|
584
|
+
applicationId: schemeConfig.applicationId,
|
|
585
|
+
device: targetDevice,
|
|
586
|
+
mainActivity: runOption.mainActivity,
|
|
587
|
+
packageName: schemeConfig.packageName,
|
|
588
|
+
port: runOption.port
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
p.outro("Success 🎉");
|
|
592
|
+
return result;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
//#endregion
|
|
596
|
+
export { buildAndroid, runAndroid };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hot-updater/android-helper",
|
|
3
|
+
"version": "0.25.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"package.json"
|
|
18
|
+
],
|
|
19
|
+
"description": "Android development helper utilities for hot-updater",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": "https://github.com/gronxb/hot-updater",
|
|
22
|
+
"author": "gronxb <gron1gh1@gmail.com> (https://github.com/gronxb)",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"execa": "9.5.2",
|
|
28
|
+
"@hot-updater/core": "0.25.4",
|
|
29
|
+
"@hot-updater/cli-tools": "0.25.4",
|
|
30
|
+
"@hot-updater/plugin-core": "0.25.4"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown",
|
|
37
|
+
"test:type": "tsc --noEmit"
|
|
38
|
+
}
|
|
39
|
+
}
|