@oniroproject/core 0.10.0 → 0.12.0
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 +35 -2
- package/dist/index.cjs +1784 -401
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +313 -4
- package/dist/index.d.ts +313 -4
- package/dist/index.js +1772 -401
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -38,6 +38,8 @@ var defaultPaths = {
|
|
|
38
38
|
sdkRootDir: () => path.join(os.homedir(), "setup-ohos-sdk"),
|
|
39
39
|
cmdToolsPath: () => path.join(os.homedir(), "command-line-tools"),
|
|
40
40
|
emulatorDir: () => path.join(os.homedir(), "oniro-emulator"),
|
|
41
|
+
harmonyosAuthDir: () => path.join(os.homedir(), ".oniro", "harmonyos"),
|
|
42
|
+
harmonyosSigningDir: () => path.join(os.homedir(), ".ohos", "config"),
|
|
41
43
|
hapPath: "entry/build/default/outputs/default/entry-default-signed.hap",
|
|
42
44
|
emulatorUrl: "https://github.com/eclipse-oniro4openharmony/device_board_oniro/releases/latest/download/oniro_emulator.zip",
|
|
43
45
|
// The Huawei mirror only hosts a public Linux build of the command-line tools.
|
|
@@ -169,7 +171,8 @@ var ALL_SDKS = [
|
|
|
169
171
|
{ version: "5.1.0", api: "18", tarballStrip: { linuxWindows: 1, darwin: 3 } },
|
|
170
172
|
{ version: "5.1.1", api: "19", tarballStrip: { linuxWindows: 1, darwin: 3 } },
|
|
171
173
|
{ version: "6.0", api: "20", tarballStrip: { linuxWindows: 0, darwin: 3 } },
|
|
172
|
-
{ version: "6.1", api: "23", tarballStrip: { linuxWindows: 0, darwin: 3 } }
|
|
174
|
+
{ version: "6.1", api: "23", tarballStrip: { linuxWindows: 0, darwin: 3 } },
|
|
175
|
+
{ version: "7.0", api: "26", tarballStrip: { linuxWindows: 1, darwin: 3 }, archiveSuffix: "_20260829" }
|
|
173
176
|
];
|
|
174
177
|
var OHOS_URL_BASE = "https://repo.huaweicloud.com/openharmony/os";
|
|
175
178
|
|
|
@@ -185,31 +188,113 @@ function getOsFolder() {
|
|
|
185
188
|
function getSdkFilename(version) {
|
|
186
189
|
const platform7 = os2.platform();
|
|
187
190
|
const v = version ?? ALL_SDKS[ALL_SDKS.length - 1].version;
|
|
188
|
-
const
|
|
191
|
+
const release = ALL_SDKS.find((r) => r.version === v);
|
|
192
|
+
const strips = release?.tarballStrip ?? { linuxWindows: 1, darwin: 3 };
|
|
193
|
+
const archive = (base) => `${base}${release?.archiveSuffix ?? ""}.tar.gz`;
|
|
189
194
|
if (platform7 === "linux") {
|
|
190
|
-
return { filename: "ohos-sdk-windows_linux-public
|
|
195
|
+
return { filename: archive("ohos-sdk-windows_linux-public"), osFolder: "linux", strip: strips.linuxWindows };
|
|
191
196
|
}
|
|
192
197
|
if (platform7 === "darwin") {
|
|
193
198
|
if (os2.arch() === "arm64") {
|
|
194
|
-
return { filename: "L2-SDK-MAC-M1-PUBLIC
|
|
199
|
+
return { filename: archive("L2-SDK-MAC-M1-PUBLIC"), osFolder: "darwin", strip: strips.darwin };
|
|
195
200
|
}
|
|
196
|
-
return { filename: "ohos-sdk-mac-public
|
|
201
|
+
return { filename: archive("ohos-sdk-mac-public"), osFolder: "darwin", strip: strips.darwin };
|
|
197
202
|
}
|
|
198
203
|
if (platform7 === "win32") {
|
|
199
|
-
return { filename: "ohos-sdk-windows_linux-public
|
|
204
|
+
return { filename: archive("ohos-sdk-windows_linux-public"), osFolder: "windows", strip: strips.linuxWindows };
|
|
200
205
|
}
|
|
201
206
|
throw new UnsupportedPlatformError(platform7);
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
// src/sdk/paths.ts
|
|
210
|
+
import * as fs2 from "fs";
|
|
211
|
+
import * as os4 from "os";
|
|
212
|
+
import * as path3 from "path";
|
|
213
|
+
|
|
214
|
+
// src/harmonyos/sdk.ts
|
|
205
215
|
import * as fs from "fs";
|
|
206
216
|
import * as os3 from "os";
|
|
207
217
|
import * as path2 from "path";
|
|
218
|
+
function readSdkPkg(sdkPath) {
|
|
219
|
+
let data;
|
|
220
|
+
try {
|
|
221
|
+
data = JSON.parse(fs.readFileSync(path2.join(sdkPath, "default", "sdk-pkg.json"), "utf8")).data;
|
|
222
|
+
} catch {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
const text = (value) => typeof value === "string" || typeof value === "number" ? String(value) || void 0 : void 0;
|
|
226
|
+
const platformVersion = text(data?.platformVersion);
|
|
227
|
+
const apiVersion = text(data?.apiVersion);
|
|
228
|
+
return {
|
|
229
|
+
displayName: text(data?.displayName),
|
|
230
|
+
version: platformVersion && apiVersion ? `${platformVersion}(${apiVersion})` : void 0
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function resolveSdk(root, requireHarmonyOs) {
|
|
234
|
+
const nested = path2.join(root, "sdk");
|
|
235
|
+
const sdkPath = [nested, root].find((dir) => fs.existsSync(path2.join(dir, "default", "openharmony")));
|
|
236
|
+
if (!sdkPath) return null;
|
|
237
|
+
const pkg = readSdkPkg(sdkPath);
|
|
238
|
+
if (requireHarmonyOs && !pkg.displayName?.startsWith("HarmonyOS")) return null;
|
|
239
|
+
return { sdkPath, installRoot: sdkPath === nested ? root : path2.dirname(root), ...pkg };
|
|
240
|
+
}
|
|
241
|
+
function studioCandidates() {
|
|
242
|
+
if (process.platform === "darwin") {
|
|
243
|
+
return [
|
|
244
|
+
"/Applications/DevEco-Studio.app/Contents",
|
|
245
|
+
path2.join(os3.homedir(), "Applications", "DevEco-Studio.app", "Contents")
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
if (process.platform === "win32") {
|
|
249
|
+
return [path2.join(process.env.ProgramFiles ?? "C:\\Program Files", "Huawei", "DevEco Studio")];
|
|
250
|
+
}
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
function findHarmonyOsSdk(opts) {
|
|
254
|
+
const logger = opts.logger ?? noopLogger;
|
|
255
|
+
const configured = opts.config.get("harmonyosSdkPath", "");
|
|
256
|
+
if (configured) {
|
|
257
|
+
const sdk = resolveSdk(configured, false);
|
|
258
|
+
if (sdk) return sdk;
|
|
259
|
+
logger.warn(`ONIRO_HARMONYOS_SDK_PATH points at ${configured}, which holds no SDK; ignoring it.`);
|
|
260
|
+
}
|
|
261
|
+
const candidates = [
|
|
262
|
+
...studioCandidates(),
|
|
263
|
+
opts.config.get("cmdToolsPath", defaultPaths.cmdToolsPath())
|
|
264
|
+
];
|
|
265
|
+
for (const candidate of candidates) {
|
|
266
|
+
const sdk = resolveSdk(candidate, true);
|
|
267
|
+
if (sdk) {
|
|
268
|
+
logger.debug(`[harmonyos] Using the ${sdk.displayName} SDK at ${sdk.sdkPath}.`);
|
|
269
|
+
return sdk;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
function requireHarmonyOsSdk(opts) {
|
|
275
|
+
const sdk = findHarmonyOsSdk(opts);
|
|
276
|
+
if (sdk) return sdk;
|
|
277
|
+
throw new OniroError(
|
|
278
|
+
"No HarmonyOS SDK found. It ships inside DevEco Studio or the HarmonyOS command-line tools and cannot be downloaded the way the OpenHarmony SDK is. Install one, then set ONIRO_HARMONYOS_SDK_PATH to the install (or its sdk directory)."
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
function findHarmonyOsTool(sdk, name) {
|
|
282
|
+
const names = process.platform === "win32" ? [`${name}.bat`, `${name}.cmd`, `${name}.exe`] : [name];
|
|
283
|
+
for (const dir of [path2.join(sdk.installRoot, "bin"), path2.join(sdk.installRoot, "tools", name, "bin")]) {
|
|
284
|
+
for (const candidate of names) {
|
|
285
|
+
const full = path2.join(dir, candidate);
|
|
286
|
+
if (fs.existsSync(full)) return full;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/sdk/paths.ts
|
|
208
293
|
function getSdkRootDir(config) {
|
|
209
294
|
return config.get("sdkRootDir", defaultPaths.sdkRootDir());
|
|
210
295
|
}
|
|
211
296
|
function getOhosBaseSdkHome(config) {
|
|
212
|
-
return
|
|
297
|
+
return path3.join(getSdkRootDir(config), getOsFolder());
|
|
213
298
|
}
|
|
214
299
|
function getCmdToolsPath(config) {
|
|
215
300
|
return config.get("cmdToolsPath", defaultPaths.cmdToolsPath());
|
|
@@ -217,31 +302,31 @@ function getCmdToolsPath(config) {
|
|
|
217
302
|
function getEmulatorDir(config) {
|
|
218
303
|
return config.get("emulatorDir", defaultPaths.emulatorDir());
|
|
219
304
|
}
|
|
220
|
-
function toLongPath(p, platform7 =
|
|
305
|
+
function toLongPath(p, platform7 = os4.platform()) {
|
|
221
306
|
if (platform7 !== "win32") return p;
|
|
222
307
|
try {
|
|
223
|
-
return
|
|
308
|
+
return fs2.realpathSync.native(p);
|
|
224
309
|
} catch {
|
|
225
310
|
return p;
|
|
226
311
|
}
|
|
227
312
|
}
|
|
228
313
|
function pickExisting(candidates) {
|
|
229
314
|
for (const c of candidates) {
|
|
230
|
-
if (
|
|
315
|
+
if (fs2.existsSync(c)) return c;
|
|
231
316
|
}
|
|
232
317
|
return candidates[0];
|
|
233
318
|
}
|
|
234
319
|
function getCmdToolsBin(config, name = "ohpm") {
|
|
235
|
-
const binDir =
|
|
236
|
-
if (
|
|
320
|
+
const binDir = path3.join(getCmdToolsPath(config), "bin");
|
|
321
|
+
if (os4.platform() === "win32") {
|
|
237
322
|
return pickExisting([
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
323
|
+
path3.join(binDir, `${name}.exe`),
|
|
324
|
+
path3.join(binDir, `${name}.cmd`),
|
|
325
|
+
path3.join(binDir, `${name}.bat`),
|
|
326
|
+
path3.join(binDir, name)
|
|
242
327
|
]);
|
|
243
328
|
}
|
|
244
|
-
return
|
|
329
|
+
return path3.join(binDir, name);
|
|
245
330
|
}
|
|
246
331
|
function getOhpmPath(config) {
|
|
247
332
|
return getCmdToolsBin(config, "ohpm");
|
|
@@ -249,45 +334,52 @@ function getOhpmPath(config) {
|
|
|
249
334
|
function getHdcPath(config) {
|
|
250
335
|
const override = config.get("hdcPath", "");
|
|
251
336
|
if (override) return override;
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
337
|
+
const hdc = hdcInSdk(path3.join(getCmdToolsPath(config), "sdk"));
|
|
338
|
+
if (fs2.existsSync(hdc)) return hdc;
|
|
339
|
+
const harmonySdk = findHarmonyOsSdk({ config });
|
|
340
|
+
const harmonyHdc = harmonySdk ? hdcInSdk(harmonySdk.sdkPath) : void 0;
|
|
341
|
+
return harmonyHdc && fs2.existsSync(harmonyHdc) ? harmonyHdc : hdc;
|
|
342
|
+
}
|
|
343
|
+
function hdcInSdk(sdkDir) {
|
|
344
|
+
const base = path3.join(sdkDir, "default", "openharmony", "toolchains");
|
|
345
|
+
if (os4.platform() === "win32") {
|
|
254
346
|
return pickExisting([
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
347
|
+
path3.join(base, "hdc.exe"),
|
|
348
|
+
path3.join(base, "hdc.bat"),
|
|
349
|
+
path3.join(base, "hdc.cmd"),
|
|
350
|
+
path3.join(base, "hdc")
|
|
259
351
|
]);
|
|
260
352
|
}
|
|
261
|
-
return
|
|
353
|
+
return path3.join(base, "hdc");
|
|
262
354
|
}
|
|
263
|
-
function getHvigorwPath(config, projectDir, platform7 =
|
|
355
|
+
function getHvigorwPath(config, projectDir, platform7 = os4.platform()) {
|
|
264
356
|
const posixFirst = ["hvigorw", "hvigorw.bat", "hvigorw.cmd"];
|
|
265
357
|
const windowsFirst = ["hvigorw.bat", "hvigorw.cmd", "hvigorw"];
|
|
266
358
|
const names = platform7 === "win32" ? windowsFirst : posixFirst;
|
|
267
|
-
const local = pickExisting(names.map((n) =>
|
|
268
|
-
if (
|
|
359
|
+
const local = pickExisting(names.map((n) => path3.join(projectDir, n)));
|
|
360
|
+
if (fs2.existsSync(local) && isProjectLocalHvigorwWorking(projectDir)) {
|
|
269
361
|
return local;
|
|
270
362
|
}
|
|
271
363
|
return getCmdToolsBin(config, "hvigorw");
|
|
272
364
|
}
|
|
273
365
|
function isProjectLocalHvigorwWorking(projectDir) {
|
|
274
|
-
const wrapper =
|
|
275
|
-
const nodeModules =
|
|
276
|
-
return
|
|
366
|
+
const wrapper = path3.join(projectDir, "hvigor", "hvigor-wrapper.js");
|
|
367
|
+
const nodeModules = path3.join(projectDir, "hvigor", "node_modules");
|
|
368
|
+
return fs2.existsSync(wrapper) && fs2.existsSync(nodeModules);
|
|
277
369
|
}
|
|
278
370
|
|
|
279
371
|
// src/sdk/detectProjectSdk.ts
|
|
280
|
-
import * as
|
|
281
|
-
import * as
|
|
372
|
+
import * as fs3 from "fs";
|
|
373
|
+
import * as path4 from "path";
|
|
282
374
|
import JSON5 from "json5";
|
|
283
375
|
function detectProjectSdkVersion(projectRoot, logger = noopLogger) {
|
|
284
|
-
const buildProfilePath =
|
|
285
|
-
if (!
|
|
376
|
+
const buildProfilePath = path4.join(projectRoot, "build-profile.json5");
|
|
377
|
+
if (!fs3.existsSync(buildProfilePath)) {
|
|
286
378
|
logger.warn(`build-profile.json5 not found at ${projectRoot}`);
|
|
287
379
|
return void 0;
|
|
288
380
|
}
|
|
289
381
|
try {
|
|
290
|
-
const content =
|
|
382
|
+
const content = fs3.readFileSync(buildProfilePath, "utf-8");
|
|
291
383
|
const config = JSON5.parse(content);
|
|
292
384
|
const product = config.app?.products?.[0];
|
|
293
385
|
const v = product?.compileSdkVersion;
|
|
@@ -299,26 +391,26 @@ function detectProjectSdkVersion(projectRoot, logger = noopLogger) {
|
|
|
299
391
|
}
|
|
300
392
|
|
|
301
393
|
// src/sdk/tmp.ts
|
|
302
|
-
import * as
|
|
303
|
-
import * as
|
|
304
|
-
import * as
|
|
394
|
+
import * as fs4 from "fs";
|
|
395
|
+
import * as os5 from "os";
|
|
396
|
+
import * as path5 from "path";
|
|
305
397
|
var INSTALL_TMP_DIRNAME = ".oniro-tmp";
|
|
306
398
|
var TMPFS_MAGIC = 16914836;
|
|
307
399
|
var RAMFS_MAGIC = 2240043254;
|
|
308
|
-
var HINT_EXAMPLE =
|
|
400
|
+
var HINT_EXAMPLE = os5.platform() === "win32" ? "D:\\oniro-tmp" : "~/.cache/oniro-tmp";
|
|
309
401
|
var TMP_DIR_HINT = `Point the scratch directory at a filesystem with more room using --tmp-dir <path> (or the ONIRO_TMP_DIR environment variable), e.g. --tmp-dir ${HINT_EXAMPLE}.`;
|
|
310
402
|
function resolveTmpRoot(config, override, installRoot) {
|
|
311
403
|
const explicit = (override ?? "").trim();
|
|
312
|
-
if (explicit) return
|
|
404
|
+
if (explicit) return path5.resolve(explicit);
|
|
313
405
|
const configured = config.get("tmpDir", "").trim();
|
|
314
|
-
if (configured) return
|
|
315
|
-
if (installRoot) return
|
|
316
|
-
return
|
|
406
|
+
if (configured) return path5.resolve(configured);
|
|
407
|
+
if (installRoot) return path5.join(path5.resolve(installRoot), INSTALL_TMP_DIRNAME);
|
|
408
|
+
return os5.tmpdir();
|
|
317
409
|
}
|
|
318
410
|
function createTempWorkDir(root, prefix) {
|
|
319
411
|
try {
|
|
320
|
-
|
|
321
|
-
return
|
|
412
|
+
fs4.mkdirSync(root, { recursive: true });
|
|
413
|
+
return fs4.mkdtempSync(path5.join(root, prefix));
|
|
322
414
|
} catch (err) {
|
|
323
415
|
if (isOutOfSpaceError(err)) throw toSpaceError(err, root, "a temporary directory");
|
|
324
416
|
throw new OniroError(
|
|
@@ -329,10 +421,10 @@ ${TMP_DIR_HINT}`,
|
|
|
329
421
|
}
|
|
330
422
|
}
|
|
331
423
|
function removeTempWorkDir(dir, root) {
|
|
332
|
-
|
|
333
|
-
if (!root ||
|
|
424
|
+
fs4.rmSync(dir, { recursive: true, force: true });
|
|
425
|
+
if (!root || path5.basename(root) !== INSTALL_TMP_DIRNAME) return;
|
|
334
426
|
try {
|
|
335
|
-
|
|
427
|
+
fs4.rmdirSync(root);
|
|
336
428
|
} catch {
|
|
337
429
|
}
|
|
338
430
|
}
|
|
@@ -342,18 +434,18 @@ function createInstallTempDir(opts) {
|
|
|
342
434
|
try {
|
|
343
435
|
return { dir: createTempWorkDir(root, opts.prefix), root };
|
|
344
436
|
} catch (err) {
|
|
345
|
-
const fallback =
|
|
346
|
-
if (requested || isOutOfSpaceError(err) ||
|
|
437
|
+
const fallback = os5.tmpdir();
|
|
438
|
+
if (requested || isOutOfSpaceError(err) || path5.resolve(fallback) === path5.resolve(root)) throw err;
|
|
347
439
|
const reason = err instanceof Error ? err.message.split(/\r?\n/)[0] : String(err);
|
|
348
440
|
(opts.logger ?? noopLogger).warn(`${reason} Falling back to '${fallback}'.`);
|
|
349
441
|
return { dir: createTempWorkDir(fallback, opts.prefix), root: fallback };
|
|
350
442
|
}
|
|
351
443
|
}
|
|
352
444
|
function nearestExisting(dir) {
|
|
353
|
-
let current =
|
|
445
|
+
let current = path5.resolve(dir);
|
|
354
446
|
for (; ; ) {
|
|
355
|
-
if (
|
|
356
|
-
const parent =
|
|
447
|
+
if (fs4.existsSync(current)) return current;
|
|
448
|
+
const parent = path5.dirname(current);
|
|
357
449
|
if (parent === current) return null;
|
|
358
450
|
current = parent;
|
|
359
451
|
}
|
|
@@ -362,7 +454,7 @@ function getFreeSpaceBytes(dir) {
|
|
|
362
454
|
const target = nearestExisting(dir);
|
|
363
455
|
if (!target) return null;
|
|
364
456
|
try {
|
|
365
|
-
const stats =
|
|
457
|
+
const stats = fs4.statfsSync(target);
|
|
366
458
|
return Number(stats.bsize) * Number(stats.bavail);
|
|
367
459
|
} catch {
|
|
368
460
|
return null;
|
|
@@ -372,7 +464,7 @@ function isRamBackedPath(dir) {
|
|
|
372
464
|
const target = nearestExisting(dir);
|
|
373
465
|
if (!target) return false;
|
|
374
466
|
try {
|
|
375
|
-
const type = Number(
|
|
467
|
+
const type = Number(fs4.statfsSync(target).type);
|
|
376
468
|
return type === TMPFS_MAGIC || type === RAMFS_MAGIC;
|
|
377
469
|
} catch {
|
|
378
470
|
return false;
|
|
@@ -436,16 +528,16 @@ function errorPath(err) {
|
|
|
436
528
|
return null;
|
|
437
529
|
}
|
|
438
530
|
function isInside(child, parent) {
|
|
439
|
-
const rel =
|
|
440
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
531
|
+
const rel = path5.relative(path5.resolve(parent), path5.resolve(child));
|
|
532
|
+
return rel === "" || !rel.startsWith("..") && !path5.isAbsolute(rel);
|
|
441
533
|
}
|
|
442
534
|
function toSpaceError(err, tmpRoot, what) {
|
|
443
535
|
if (err instanceof InsufficientSpaceError) return err;
|
|
444
536
|
if (!isOutOfSpaceError(err)) return err;
|
|
445
537
|
const failed = errorPath(err);
|
|
446
|
-
const failedDir = failed ?
|
|
538
|
+
const failedDir = failed ? path5.dirname(path5.resolve(failed)) : path5.resolve(tmpRoot);
|
|
447
539
|
const inTmp = isInside(failedDir, tmpRoot);
|
|
448
|
-
const dir = inTmp ?
|
|
540
|
+
const dir = inTmp ? path5.resolve(tmpRoot) : failedDir;
|
|
449
541
|
return new InsufficientSpaceError(
|
|
450
542
|
spaceMessage(dir, what, { freeBytes: getFreeSpaceBytes(dir), tmpDirHint: inTmp }),
|
|
451
543
|
dir,
|
|
@@ -454,8 +546,8 @@ function toSpaceError(err, tmpRoot, what) {
|
|
|
454
546
|
}
|
|
455
547
|
|
|
456
548
|
// src/sdk/download.ts
|
|
457
|
-
import * as
|
|
458
|
-
import * as
|
|
549
|
+
import * as fs5 from "fs";
|
|
550
|
+
import * as path6 from "path";
|
|
459
551
|
import * as crypto from "crypto";
|
|
460
552
|
import { pipeline } from "stream";
|
|
461
553
|
import { promisify } from "util";
|
|
@@ -465,26 +557,26 @@ var pipelineAsync = promisify(pipeline);
|
|
|
465
557
|
async function downloadFile(opts) {
|
|
466
558
|
const { url, dest, progress, abortSignal } = opts;
|
|
467
559
|
const proto = url.startsWith("https") ? https : http;
|
|
468
|
-
return new Promise((
|
|
560
|
+
return new Promise((resolve6, reject) => {
|
|
469
561
|
const s = Math.max(0, Math.min(100, opts.start ?? 0));
|
|
470
562
|
const r = Math.max(0, Math.min(100 - s, opts.range ?? 100));
|
|
471
563
|
let settled = false;
|
|
472
564
|
const done = (err) => {
|
|
473
565
|
if (settled) return;
|
|
474
566
|
settled = true;
|
|
475
|
-
err ? reject(err) :
|
|
567
|
+
err ? reject(err) : resolve6();
|
|
476
568
|
};
|
|
477
569
|
if (abortSignal?.aborted) {
|
|
478
570
|
done(new CancelledError("Download cancelled before start."));
|
|
479
571
|
return;
|
|
480
572
|
}
|
|
481
|
-
const destDir =
|
|
573
|
+
const destDir = path6.dirname(path6.resolve(dest));
|
|
482
574
|
const what = opts.what ?? `the download of '${url}'`;
|
|
483
575
|
const writeFailed = (err) => isOutOfSpaceError(err) ? toSpaceError(err, opts.tmpRoot ?? destDir, what) : new OniroError(`Error writing '${dest}': ${err instanceof Error ? err.message : String(err)}`, err);
|
|
484
|
-
const file =
|
|
576
|
+
const file = fs5.createWriteStream(dest);
|
|
485
577
|
let activeResponse;
|
|
486
578
|
const failWith = (err) => {
|
|
487
|
-
file.close(() =>
|
|
579
|
+
file.close(() => fs5.rm(dest, { force: true }, () => done(err)));
|
|
488
580
|
};
|
|
489
581
|
file.on("error", (err) => {
|
|
490
582
|
try {
|
|
@@ -556,9 +648,9 @@ async function downloadFile(opts) {
|
|
|
556
648
|
});
|
|
557
649
|
}
|
|
558
650
|
async function verifySha256(filePath, sha256Path) {
|
|
559
|
-
const expected =
|
|
651
|
+
const expected = fs5.readFileSync(sha256Path, "utf8").split(/\s+/)[0];
|
|
560
652
|
const hash = crypto.createHash("sha256");
|
|
561
|
-
await pipelineAsync(
|
|
653
|
+
await pipelineAsync(fs5.createReadStream(filePath), hash);
|
|
562
654
|
const actual = hash.digest("hex");
|
|
563
655
|
if (actual !== expected) {
|
|
564
656
|
throw new ChecksumMismatchError(expected, actual);
|
|
@@ -566,9 +658,9 @@ async function verifySha256(filePath, sha256Path) {
|
|
|
566
658
|
}
|
|
567
659
|
|
|
568
660
|
// src/sdk/extract.ts
|
|
569
|
-
import * as
|
|
570
|
-
import * as
|
|
571
|
-
import * as
|
|
661
|
+
import * as fs6 from "fs";
|
|
662
|
+
import * as os6 from "os";
|
|
663
|
+
import * as path7 from "path";
|
|
572
664
|
import { pipeline as pipeline2 } from "stream";
|
|
573
665
|
import { promisify as promisify2 } from "util";
|
|
574
666
|
import * as tar from "tar";
|
|
@@ -585,12 +677,12 @@ async function extractZipWithProgress(opts) {
|
|
|
585
677
|
const r = Math.max(0, Math.min(100 - s, opts.range ?? 100));
|
|
586
678
|
let processed = 0;
|
|
587
679
|
let lastOverall = Math.round(s);
|
|
588
|
-
await
|
|
589
|
-
const destRoot =
|
|
680
|
+
await fs6.promises.mkdir(dest, { recursive: true });
|
|
681
|
+
const destRoot = path7.resolve(dest);
|
|
590
682
|
const safeResolveTarget = (entryName) => {
|
|
591
|
-
const resolved =
|
|
592
|
-
const rel =
|
|
593
|
-
if (rel.startsWith("..") ||
|
|
683
|
+
const resolved = path7.resolve(destRoot, entryName);
|
|
684
|
+
const rel = path7.relative(destRoot, resolved);
|
|
685
|
+
if (rel.startsWith("..") || path7.isAbsolute(rel)) {
|
|
594
686
|
throw new OniroError(`Blocked ZIP entry with illegal path: ${entryName}`);
|
|
595
687
|
}
|
|
596
688
|
return resolved;
|
|
@@ -602,25 +694,25 @@ async function extractZipWithProgress(opts) {
|
|
|
602
694
|
const attr = file.attr ? file.attr >>> 16 : 0;
|
|
603
695
|
const isSymlink = "isSymbolicLink" in file ? Boolean(file.isSymbolicLink) : (attr & 61440) === 40960;
|
|
604
696
|
if (file.isDirectory) {
|
|
605
|
-
await
|
|
697
|
+
await fs6.promises.mkdir(targetPath, { recursive: true });
|
|
606
698
|
} else if (isSymlink) {
|
|
607
|
-
await
|
|
699
|
+
await fs6.promises.mkdir(path7.dirname(targetPath), { recursive: true });
|
|
608
700
|
const linkTargetBuffer = await zip.entryData(file);
|
|
609
701
|
const linkTarget = linkTargetBuffer.toString("utf8");
|
|
610
702
|
try {
|
|
611
|
-
const isWin =
|
|
612
|
-
await
|
|
703
|
+
const isWin = os6.platform() === "win32";
|
|
704
|
+
await fs6.promises.symlink(linkTarget, targetPath, isWin ? "file" : void 0);
|
|
613
705
|
} catch (e) {
|
|
614
706
|
logger.warn(`Failed to create symlink at ${targetPath}: ${String(e)}`);
|
|
615
707
|
}
|
|
616
708
|
} else {
|
|
617
|
-
await
|
|
709
|
+
await fs6.promises.mkdir(path7.dirname(targetPath), { recursive: true });
|
|
618
710
|
const readStream = await zip.stream(file);
|
|
619
|
-
const writeStream =
|
|
711
|
+
const writeStream = fs6.createWriteStream(targetPath);
|
|
620
712
|
await pipelineAsync2(readStream, writeStream);
|
|
621
713
|
if (attr > 0) {
|
|
622
714
|
try {
|
|
623
|
-
await
|
|
715
|
+
await fs6.promises.chmod(targetPath, attr & 511);
|
|
624
716
|
} catch {
|
|
625
717
|
}
|
|
626
718
|
}
|
|
@@ -652,41 +744,41 @@ async function extractTarball(tarPath, dest, strip = 0) {
|
|
|
652
744
|
}
|
|
653
745
|
|
|
654
746
|
// src/sdk/list.ts
|
|
655
|
-
import * as
|
|
656
|
-
import * as
|
|
747
|
+
import * as fs7 from "fs";
|
|
748
|
+
import * as path8 from "path";
|
|
657
749
|
function getSupportedSdksForUi(config) {
|
|
658
|
-
const base =
|
|
750
|
+
const base = path8.join(getSdkRootDir(config), getOsFolder());
|
|
659
751
|
return ALL_SDKS.map((sdk) => ({
|
|
660
752
|
version: sdk.version,
|
|
661
753
|
api: sdk.api,
|
|
662
|
-
installed:
|
|
754
|
+
installed: fs7.existsSync(path8.join(base, sdk.api))
|
|
663
755
|
})).sort((a, b) => Number(b.api) - Number(a.api));
|
|
664
756
|
}
|
|
665
757
|
function getInstalledSdks(config) {
|
|
666
758
|
const sdkRoot = getSdkRootDir(config);
|
|
667
759
|
const versions = /* @__PURE__ */ new Set();
|
|
668
|
-
if (!
|
|
760
|
+
if (!fs7.existsSync(sdkRoot)) return [];
|
|
669
761
|
for (const osFolder of ["linux", "darwin", "windows"]) {
|
|
670
|
-
const osPath =
|
|
671
|
-
if (!
|
|
672
|
-
for (const api of
|
|
673
|
-
const apiPath =
|
|
674
|
-
if (
|
|
762
|
+
const osPath = path8.join(sdkRoot, osFolder);
|
|
763
|
+
if (!fs7.existsSync(osPath) || !fs7.statSync(osPath).isDirectory()) continue;
|
|
764
|
+
for (const api of fs7.readdirSync(osPath)) {
|
|
765
|
+
const apiPath = path8.join(osPath, api);
|
|
766
|
+
if (fs7.statSync(apiPath).isDirectory()) versions.add(api);
|
|
675
767
|
}
|
|
676
768
|
}
|
|
677
769
|
return ALL_SDKS.filter((sdk) => versions.has(sdk.api)).map((sdk) => sdk.version);
|
|
678
770
|
}
|
|
679
771
|
|
|
680
772
|
// src/sdk/remove.ts
|
|
681
|
-
import * as
|
|
682
|
-
import * as
|
|
773
|
+
import * as fs8 from "fs";
|
|
774
|
+
import * as path9 from "path";
|
|
683
775
|
function removeSdk(config, api) {
|
|
684
776
|
const sdkRoot = getSdkRootDir(config);
|
|
685
777
|
let removed = false;
|
|
686
778
|
for (const osFolder of ["linux", "darwin", "windows"]) {
|
|
687
|
-
const sdkPath =
|
|
688
|
-
if (
|
|
689
|
-
|
|
779
|
+
const sdkPath = path9.join(sdkRoot, osFolder, api);
|
|
780
|
+
if (fs8.existsSync(sdkPath)) {
|
|
781
|
+
fs8.rmSync(sdkPath, { recursive: true, force: true });
|
|
690
782
|
removed = true;
|
|
691
783
|
}
|
|
692
784
|
}
|
|
@@ -694,18 +786,18 @@ function removeSdk(config, api) {
|
|
|
694
786
|
}
|
|
695
787
|
|
|
696
788
|
// src/sdk/install.ts
|
|
697
|
-
import * as
|
|
698
|
-
import * as
|
|
789
|
+
import * as fs10 from "fs";
|
|
790
|
+
import * as path10 from "path";
|
|
699
791
|
|
|
700
792
|
// src/sdk/move.ts
|
|
701
|
-
import * as
|
|
793
|
+
import * as fs9 from "fs";
|
|
702
794
|
function movePath(src, dest) {
|
|
703
795
|
try {
|
|
704
|
-
|
|
796
|
+
fs9.renameSync(src, dest);
|
|
705
797
|
} catch (err) {
|
|
706
798
|
if (err.code !== "EXDEV") throw err;
|
|
707
|
-
|
|
708
|
-
|
|
799
|
+
fs9.cpSync(src, dest, { recursive: true });
|
|
800
|
+
fs9.rmSync(src, { recursive: true, force: true });
|
|
709
801
|
}
|
|
710
802
|
}
|
|
711
803
|
|
|
@@ -726,12 +818,12 @@ async function downloadAndInstallSdk(opts) {
|
|
|
726
818
|
prefix: "oniro-sdk-",
|
|
727
819
|
logger
|
|
728
820
|
});
|
|
729
|
-
const tarPath =
|
|
730
|
-
const sha256Path =
|
|
731
|
-
const extractDir =
|
|
732
|
-
|
|
733
|
-
const sdkInstallDir =
|
|
734
|
-
|
|
821
|
+
const tarPath = path10.join(tmpDir, filename);
|
|
822
|
+
const sha256Path = path10.join(tmpDir, `${filename}.sha256`);
|
|
823
|
+
const extractDir = path10.join(tmpDir, "extract");
|
|
824
|
+
fs10.mkdirSync(extractDir);
|
|
825
|
+
const sdkInstallDir = path10.join(sdkRoot, osFolder, api);
|
|
826
|
+
fs10.mkdirSync(path10.dirname(sdkInstallDir), { recursive: true });
|
|
735
827
|
const checkCancelled = () => {
|
|
736
828
|
if (abortSignal?.aborted) throw new CancelledError();
|
|
737
829
|
};
|
|
@@ -748,13 +840,13 @@ async function downloadAndInstallSdk(opts) {
|
|
|
748
840
|
await extractTarball(tarPath, extractDir, strip);
|
|
749
841
|
progress?.report({ message: "Extracting SDK (this may take a while)...", increment: 10 });
|
|
750
842
|
checkCancelled();
|
|
751
|
-
const osContentPath =
|
|
752
|
-
if (!
|
|
843
|
+
const osContentPath = path10.join(extractDir, osFolder);
|
|
844
|
+
if (!fs10.existsSync(osContentPath)) {
|
|
753
845
|
throw new OniroError(
|
|
754
846
|
`Expected folder '${osFolder}' not found in extracted SDK. Tarball layout may have changed.`
|
|
755
847
|
);
|
|
756
848
|
}
|
|
757
|
-
const zipFiles =
|
|
849
|
+
const zipFiles = fs10.readdirSync(osContentPath).filter((n2) => n2.endsWith(".zip"));
|
|
758
850
|
const componentsStart = 60;
|
|
759
851
|
const componentsBudget = 35;
|
|
760
852
|
const n = zipFiles.length;
|
|
@@ -769,16 +861,16 @@ async function downloadAndInstallSdk(opts) {
|
|
|
769
861
|
for (const zipFile of zipFiles) {
|
|
770
862
|
checkCancelled();
|
|
771
863
|
logger.info(`Extracting component ${zipFile}`);
|
|
772
|
-
const zipPath =
|
|
864
|
+
const zipPath = path10.join(osContentPath, zipFile);
|
|
773
865
|
const thisBudget = base + (rem > 0 ? 1 : 0);
|
|
774
866
|
if (rem > 0) rem--;
|
|
775
867
|
await extractZipWithProgress({ zipPath, dest: osContentPath, progress, start: cursor, range: thisBudget, logger });
|
|
776
868
|
cursor += thisBudget;
|
|
777
|
-
|
|
869
|
+
fs10.unlinkSync(zipPath);
|
|
778
870
|
}
|
|
779
871
|
progress?.report({ message: "Finalizing installation...", increment: 0 });
|
|
780
|
-
if (
|
|
781
|
-
|
|
872
|
+
if (fs10.existsSync(sdkInstallDir)) {
|
|
873
|
+
fs10.rmSync(sdkInstallDir, { recursive: true, force: true });
|
|
782
874
|
}
|
|
783
875
|
movePath(osContentPath, sdkInstallDir);
|
|
784
876
|
progress?.report({ message: "Finalizing installation...", increment: 3 });
|
|
@@ -794,13 +886,13 @@ async function downloadAndInstallSdk(opts) {
|
|
|
794
886
|
}
|
|
795
887
|
|
|
796
888
|
// src/cmdTools/install.ts
|
|
797
|
-
import * as
|
|
798
|
-
import * as
|
|
799
|
-
import * as
|
|
889
|
+
import * as fs11 from "fs";
|
|
890
|
+
import * as os7 from "os";
|
|
891
|
+
import * as path12 from "path";
|
|
800
892
|
import { spawnSync } from "child_process";
|
|
801
893
|
|
|
802
894
|
// src/hdc/spawnCompat.ts
|
|
803
|
-
import * as
|
|
895
|
+
import * as path11 from "path";
|
|
804
896
|
import { spawn } from "child_process";
|
|
805
897
|
var WINDOWS_BATCH_RE = /\.(bat|cmd)$/i;
|
|
806
898
|
var CMD_META_RE = /([()[\]%!^"`<>&|;, *?])/g;
|
|
@@ -824,7 +916,7 @@ function escapeCmdCommand(command) {
|
|
|
824
916
|
function buildCmdWrapper(command, args) {
|
|
825
917
|
assertCmdSafe(command, `command ${command}`);
|
|
826
918
|
args.forEach((a, i) => assertCmdSafe(a, `argument ${i} (${a})`));
|
|
827
|
-
const normalized =
|
|
919
|
+
const normalized = path11.normalize(command);
|
|
828
920
|
const double = CMD_SHIM_RE.test(normalized);
|
|
829
921
|
const line = [escapeCmdCommand(normalized), ...args.map((a) => escapeCmdArgument(a, double))].join(" ");
|
|
830
922
|
return {
|
|
@@ -872,7 +964,7 @@ function killProcessTree(child) {
|
|
|
872
964
|
}
|
|
873
965
|
|
|
874
966
|
// src/cmdTools/install.ts
|
|
875
|
-
function getCmdToolsDownloadUrl(config, platform7 =
|
|
967
|
+
function getCmdToolsDownloadUrl(config, platform7 = os7.platform()) {
|
|
876
968
|
if (platform7 === "linux") {
|
|
877
969
|
return config.get("cmdToolsUrlLinux", defaultPaths.cmdToolsUrlLinux);
|
|
878
970
|
}
|
|
@@ -898,16 +990,16 @@ function getCmdToolsDownloadUrl(config, platform7 = os6.platform()) {
|
|
|
898
990
|
}
|
|
899
991
|
function findCmdToolsSourceDir(extractPath) {
|
|
900
992
|
const known = [
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
993
|
+
path12.join(extractPath, "command-line-tools"),
|
|
994
|
+
path12.join(extractPath, "oh-command-line-tools"),
|
|
995
|
+
path12.join(extractPath, "commandline-tools")
|
|
904
996
|
];
|
|
905
997
|
for (const c of known) {
|
|
906
|
-
if (
|
|
998
|
+
if (fs11.existsSync(c)) return c;
|
|
907
999
|
}
|
|
908
|
-
const entries =
|
|
1000
|
+
const entries = fs11.readdirSync(extractPath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path12.join(extractPath, e.name));
|
|
909
1001
|
for (const dir of entries) {
|
|
910
|
-
if (
|
|
1002
|
+
if (fs11.existsSync(path12.join(dir, "bin"))) return dir;
|
|
911
1003
|
}
|
|
912
1004
|
throw new OniroError("Could not locate command line tools folder in the extracted archive.");
|
|
913
1005
|
}
|
|
@@ -919,12 +1011,12 @@ async function installCmdTools(opts) {
|
|
|
919
1011
|
const { dir: tmpDir, root: tmpRoot } = createInstallTempDir({
|
|
920
1012
|
config,
|
|
921
1013
|
override: opts.tmpDir,
|
|
922
|
-
installRoot:
|
|
1014
|
+
installRoot: path12.dirname(CMD_PATH),
|
|
923
1015
|
prefix: "oniro-cmdtools-",
|
|
924
1016
|
logger
|
|
925
1017
|
});
|
|
926
|
-
const zipPath = opts.localZipPath ??
|
|
927
|
-
const extractPath =
|
|
1018
|
+
const zipPath = opts.localZipPath ?? path12.join(tmpDir, "oh-command-line-tools.zip");
|
|
1019
|
+
const extractPath = path12.join(tmpDir, "oh-command-line-tools");
|
|
928
1020
|
try {
|
|
929
1021
|
if (!opts.localZipPath) {
|
|
930
1022
|
const url = getCmdToolsDownloadUrl(config);
|
|
@@ -933,22 +1025,22 @@ async function installCmdTools(opts) {
|
|
|
933
1025
|
}
|
|
934
1026
|
progress?.report({ message: "Extracting tools...", increment: 0 });
|
|
935
1027
|
await extractZipWithProgress({ zipPath, dest: extractPath, progress, start: 50, range: 45, logger });
|
|
936
|
-
|
|
1028
|
+
fs11.mkdirSync(CMD_PATH, { recursive: true });
|
|
937
1029
|
const srcDir = findCmdToolsSourceDir(extractPath);
|
|
938
|
-
for (const entry of
|
|
939
|
-
const src =
|
|
940
|
-
const dest =
|
|
941
|
-
if (
|
|
942
|
-
if (
|
|
1030
|
+
for (const entry of fs11.readdirSync(srcDir)) {
|
|
1031
|
+
const src = path12.join(srcDir, entry);
|
|
1032
|
+
const dest = path12.join(CMD_PATH, entry);
|
|
1033
|
+
if (fs11.statSync(src).isDirectory()) {
|
|
1034
|
+
if (fs11.existsSync(dest)) fs11.rmSync(dest, { recursive: true, force: true });
|
|
943
1035
|
movePath(src, dest);
|
|
944
1036
|
} else {
|
|
945
|
-
|
|
1037
|
+
fs11.copyFileSync(src, dest);
|
|
946
1038
|
}
|
|
947
1039
|
}
|
|
948
|
-
const binDir =
|
|
949
|
-
if (
|
|
950
|
-
for (const file of
|
|
951
|
-
|
|
1040
|
+
const binDir = path12.join(CMD_PATH, "bin");
|
|
1041
|
+
if (fs11.existsSync(binDir) && os7.platform() !== "win32") {
|
|
1042
|
+
for (const file of fs11.readdirSync(binDir)) {
|
|
1043
|
+
fs11.chmodSync(path12.join(binDir, file), 493);
|
|
952
1044
|
}
|
|
953
1045
|
}
|
|
954
1046
|
progress?.report({ message: "Finalizing installation...", increment: 5 });
|
|
@@ -961,16 +1053,16 @@ async function installCmdTools(opts) {
|
|
|
961
1053
|
}
|
|
962
1054
|
function getCmdToolsStatus(config) {
|
|
963
1055
|
const cmdPath = getCmdToolsPath(config);
|
|
964
|
-
const binDir =
|
|
965
|
-
const candidates =
|
|
966
|
-
const bin = candidates.map((c) =>
|
|
1056
|
+
const binDir = path12.join(cmdPath, "bin");
|
|
1057
|
+
const candidates = os7.platform() === "win32" ? ["ohpm.exe", "ohpm.cmd", "ohpm.bat", "ohpm"] : ["ohpm"];
|
|
1058
|
+
const bin = candidates.map((c) => path12.join(binDir, c)).find((p) => fs11.existsSync(p));
|
|
967
1059
|
if (!bin) {
|
|
968
1060
|
return { installed: false, status: "Not installed" };
|
|
969
1061
|
}
|
|
970
1062
|
try {
|
|
971
|
-
const versionFile =
|
|
972
|
-
if (
|
|
973
|
-
const content =
|
|
1063
|
+
const versionFile = path12.join(cmdPath, "version.txt");
|
|
1064
|
+
if (fs11.existsSync(versionFile)) {
|
|
1065
|
+
const content = fs11.readFileSync(versionFile, "utf8");
|
|
974
1066
|
for (const line of content.split(/\r?\n/)) {
|
|
975
1067
|
const m = line.match(/^\s*#\s*Version:\s*(.+)$/);
|
|
976
1068
|
if (m?.[1]) return { installed: true, status: `Installed (${m[1].trim()})` };
|
|
@@ -997,14 +1089,14 @@ function isCmdToolsInstalled(config) {
|
|
|
997
1089
|
}
|
|
998
1090
|
function removeCmdTools(config) {
|
|
999
1091
|
const cmdPath = getCmdToolsPath(config);
|
|
1000
|
-
if (
|
|
1001
|
-
|
|
1092
|
+
if (fs11.existsSync(cmdPath)) {
|
|
1093
|
+
fs11.rmSync(cmdPath, { recursive: true, force: true });
|
|
1002
1094
|
}
|
|
1003
1095
|
}
|
|
1004
1096
|
|
|
1005
1097
|
// src/sign/encryptKey.ts
|
|
1006
|
-
import * as
|
|
1007
|
-
import * as
|
|
1098
|
+
import * as fs12 from "fs";
|
|
1099
|
+
import * as path13 from "path";
|
|
1008
1100
|
import * as crypto2 from "crypto";
|
|
1009
1101
|
var DAEMON_ROOT_KEY_COMPONENT_LENGTH = 16;
|
|
1010
1102
|
var DAEMON_SALT_KEY_LENGTH = 16;
|
|
@@ -1076,33 +1168,33 @@ function getRootKey(fdComponents, salt) {
|
|
|
1076
1168
|
function createAndStoreKey(dir, keyLength) {
|
|
1077
1169
|
const key = crypto2.randomBytes(keyLength);
|
|
1078
1170
|
const hash = crypto2.createHash("sha256").update(key).digest("hex");
|
|
1079
|
-
const filePath =
|
|
1080
|
-
|
|
1081
|
-
|
|
1171
|
+
const filePath = path13.join(dir, hash);
|
|
1172
|
+
fs12.writeFileSync(filePath, key);
|
|
1173
|
+
fs12.chmodSync(filePath, KEY_FILE_PERMISSIONS);
|
|
1082
1174
|
return key;
|
|
1083
1175
|
}
|
|
1084
1176
|
function createAndStoreEnKey(rootKey, ceDir) {
|
|
1085
1177
|
const workKey = crypto2.randomBytes(DAEMON_WORK_KEY_LENGTH);
|
|
1086
1178
|
const encrypted = encrypt(rootKey, workKey);
|
|
1087
1179
|
const hash = crypto2.createHash("sha256").update(encrypted).digest("hex");
|
|
1088
|
-
const filePath =
|
|
1089
|
-
|
|
1090
|
-
|
|
1180
|
+
const filePath = path13.join(ceDir, hash);
|
|
1181
|
+
fs12.writeFileSync(filePath, encrypted);
|
|
1182
|
+
fs12.chmodSync(filePath, KEY_FILE_PERMISSIONS);
|
|
1091
1183
|
}
|
|
1092
1184
|
function createMaterial(materialPath) {
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
const fdDir =
|
|
1096
|
-
const acDir =
|
|
1097
|
-
const ceDir =
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1185
|
+
fs12.mkdirSync(materialPath, { recursive: true });
|
|
1186
|
+
fs12.chmodSync(materialPath, KEY_FILE_DIRECTORY_PERMISSIONS);
|
|
1187
|
+
const fdDir = path13.join(materialPath, "fd");
|
|
1188
|
+
const acDir = path13.join(materialPath, "ac");
|
|
1189
|
+
const ceDir = path13.join(materialPath, "ce");
|
|
1190
|
+
fs12.mkdirSync(fdDir, { recursive: true });
|
|
1191
|
+
fs12.mkdirSync(acDir, { recursive: true });
|
|
1192
|
+
fs12.mkdirSync(ceDir, { recursive: true });
|
|
1101
1193
|
const fdSubDirs = ["0", "1", "2"];
|
|
1102
1194
|
const fdComponents = [];
|
|
1103
1195
|
for (const sub of fdSubDirs) {
|
|
1104
|
-
const subDir =
|
|
1105
|
-
|
|
1196
|
+
const subDir = path13.join(fdDir, sub);
|
|
1197
|
+
fs12.mkdirSync(subDir, { recursive: true });
|
|
1106
1198
|
const comp = createAndStoreKey(subDir, DAEMON_ROOT_KEY_COMPONENT_LENGTH);
|
|
1107
1199
|
fdComponents.push(comp);
|
|
1108
1200
|
}
|
|
@@ -1111,24 +1203,24 @@ function createMaterial(materialPath) {
|
|
|
1111
1203
|
createAndStoreEnKey(rootKey, ceDir);
|
|
1112
1204
|
}
|
|
1113
1205
|
function readSingleFile(dir) {
|
|
1114
|
-
if (!
|
|
1206
|
+
if (!fs12.existsSync(dir) || !fs12.statSync(dir).isDirectory()) {
|
|
1115
1207
|
throw new Error(`Missing signing material directory: ${dir}`);
|
|
1116
1208
|
}
|
|
1117
|
-
const files =
|
|
1209
|
+
const files = fs12.readdirSync(dir).filter((f) => f !== ".DS_Store");
|
|
1118
1210
|
if (files.length !== 1) {
|
|
1119
1211
|
throw new Error(`Signing material in ${dir} is illegal (expected exactly one file).`);
|
|
1120
1212
|
}
|
|
1121
|
-
return
|
|
1213
|
+
return fs12.readFileSync(path13.join(dir, files[0]));
|
|
1122
1214
|
}
|
|
1123
1215
|
function getKey(materialPath) {
|
|
1124
|
-
if (!
|
|
1216
|
+
if (!fs12.existsSync(materialPath) || !fs12.statSync(materialPath).isDirectory()) {
|
|
1125
1217
|
throw new Error("Material directory does not exist.");
|
|
1126
1218
|
}
|
|
1127
|
-
const fdDir =
|
|
1128
|
-
const fdComponents = ["0", "1", "2"].map((sub) => readSingleFile(
|
|
1129
|
-
const salt = readSingleFile(
|
|
1219
|
+
const fdDir = path13.join(materialPath, "fd");
|
|
1220
|
+
const fdComponents = ["0", "1", "2"].map((sub) => readSingleFile(path13.join(fdDir, sub)));
|
|
1221
|
+
const salt = readSingleFile(path13.join(materialPath, "ac"));
|
|
1130
1222
|
const rootKey = getRootKey(fdComponents, salt);
|
|
1131
|
-
const workMaterial = readSingleFile(
|
|
1223
|
+
const workMaterial = readSingleFile(path13.join(materialPath, "ce"));
|
|
1132
1224
|
return decrypt(rootKey, workMaterial);
|
|
1133
1225
|
}
|
|
1134
1226
|
function encryptPwd(password, materialPath) {
|
|
@@ -1143,8 +1235,8 @@ function decryptPwd(encryptedHex, materialPath) {
|
|
|
1143
1235
|
}
|
|
1144
1236
|
|
|
1145
1237
|
// src/sign/generateSigningConfigs.ts
|
|
1146
|
-
import * as
|
|
1147
|
-
import * as
|
|
1238
|
+
import * as fs13 from "fs";
|
|
1239
|
+
import * as path14 from "path";
|
|
1148
1240
|
import { execFileSync } from "child_process";
|
|
1149
1241
|
import JSON52 from "json5";
|
|
1150
1242
|
|
|
@@ -1214,17 +1306,17 @@ var SIGNING_KIND_CONFIG = {
|
|
|
1214
1306
|
};
|
|
1215
1307
|
function copyFilesToProject(projectDir, paths, kind, logger, applicationCertPath) {
|
|
1216
1308
|
logger.info("[sign] Copying signing material into project...");
|
|
1217
|
-
const signaturesDir =
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1309
|
+
const signaturesDir = path14.join(projectDir, "signatures");
|
|
1310
|
+
fs13.mkdirSync(signaturesDir, { recursive: true });
|
|
1311
|
+
fs13.copyFileSync(paths.keystore, path14.join(signaturesDir, "OpenHarmony.p12"));
|
|
1312
|
+
fs13.copyFileSync(paths.profileCert, path14.join(signaturesDir, "OpenHarmonyProfileRelease.pem"));
|
|
1313
|
+
fs13.copyFileSync(paths.unsignedProfileTemplate, path14.join(signaturesDir, "UnsgnedReleasedProfileTemplate.json"));
|
|
1222
1314
|
if (kind === "application-release") {
|
|
1223
|
-
const dest =
|
|
1315
|
+
const dest = path14.join(signaturesDir, "OpenHarmonyApplication.cer");
|
|
1224
1316
|
if (applicationCertPath) {
|
|
1225
|
-
|
|
1317
|
+
fs13.copyFileSync(applicationCertPath, dest);
|
|
1226
1318
|
} else {
|
|
1227
|
-
|
|
1319
|
+
fs13.writeFileSync(dest, OPEN_HARMONY_APPLICATION_CERT);
|
|
1228
1320
|
}
|
|
1229
1321
|
}
|
|
1230
1322
|
}
|
|
@@ -1233,7 +1325,7 @@ function resolveAppFeature(apl, override) {
|
|
|
1233
1325
|
return apl === "normal" ? "hos_normal_app" : "hos_system_app";
|
|
1234
1326
|
}
|
|
1235
1327
|
function extractLeafCertificate(chainPath) {
|
|
1236
|
-
const certContent =
|
|
1328
|
+
const certContent = fs13.readFileSync(chainPath, "utf-8");
|
|
1237
1329
|
const parts = certContent.split("-----END CERTIFICATE-----");
|
|
1238
1330
|
if (parts.length < 3) {
|
|
1239
1331
|
throw new OniroError(`${chainPath} does not contain enough certificates.`);
|
|
@@ -1243,20 +1335,20 @@ function extractLeafCertificate(chainPath) {
|
|
|
1243
1335
|
function modifyProfileTemplate(projectDir, apl, appFeature, logger, options = {}) {
|
|
1244
1336
|
const kind = options.kind ?? pickSigningKind(apl);
|
|
1245
1337
|
logger.info(`[sign] Modifying profile template (apl=${apl}, app-feature=${appFeature}, kind=${kind})...`);
|
|
1246
|
-
const appJsonPath =
|
|
1247
|
-
const profileTemplatePath =
|
|
1248
|
-
if (!
|
|
1338
|
+
const appJsonPath = path14.join(projectDir, "AppScope/app.json5");
|
|
1339
|
+
const profileTemplatePath = path14.join(projectDir, "signatures/UnsgnedReleasedProfileTemplate.json");
|
|
1340
|
+
if (!fs13.existsSync(appJsonPath)) {
|
|
1249
1341
|
throw new OniroError(`${appJsonPath} does not exist.`);
|
|
1250
1342
|
}
|
|
1251
1343
|
let appJson;
|
|
1252
1344
|
try {
|
|
1253
|
-
appJson = JSON52.parse(
|
|
1345
|
+
appJson = JSON52.parse(fs13.readFileSync(appJsonPath, "utf-8"));
|
|
1254
1346
|
} catch (e) {
|
|
1255
1347
|
throw new OniroError(`Error parsing ${appJsonPath}: ${e.message}`, e);
|
|
1256
1348
|
}
|
|
1257
1349
|
let profileTemplate;
|
|
1258
1350
|
try {
|
|
1259
|
-
profileTemplate = JSON.parse(
|
|
1351
|
+
profileTemplate = JSON.parse(fs13.readFileSync(profileTemplatePath, "utf-8"));
|
|
1260
1352
|
} catch (e) {
|
|
1261
1353
|
throw new OniroError(`Error parsing ${profileTemplatePath}: ${e.message}`, e);
|
|
1262
1354
|
}
|
|
@@ -1269,7 +1361,7 @@ function modifyProfileTemplate(projectDir, apl, appFeature, logger, options = {}
|
|
|
1269
1361
|
profileTemplate["bundle-info"]["bundle-name"] = appJson.app.bundleName;
|
|
1270
1362
|
profileTemplate["bundle-info"]["apl"] = apl;
|
|
1271
1363
|
profileTemplate["bundle-info"]["app-feature"] = appFeature;
|
|
1272
|
-
const distCertChainPath =
|
|
1364
|
+
const distCertChainPath = path14.join(
|
|
1273
1365
|
projectDir,
|
|
1274
1366
|
"signatures",
|
|
1275
1367
|
SIGNING_KIND_CONFIG[kind].certBasename
|
|
@@ -1279,13 +1371,13 @@ function modifyProfileTemplate(projectDir, apl, appFeature, logger, options = {}
|
|
|
1279
1371
|
profileTemplate.acls = profileTemplate.acls ?? {};
|
|
1280
1372
|
profileTemplate.acls["allowed-acls"] = options.acls;
|
|
1281
1373
|
}
|
|
1282
|
-
|
|
1374
|
+
fs13.writeFileSync(profileTemplatePath, JSON.stringify(profileTemplate, null, 2));
|
|
1283
1375
|
}
|
|
1284
1376
|
function generateP7bFile(projectDir, paths, logger, passwords) {
|
|
1285
1377
|
logger.info("[sign] Generating P7b profile via hap-sign-tool...");
|
|
1286
|
-
const signaturesDir =
|
|
1287
|
-
const profileTemplatePath =
|
|
1288
|
-
const outputProfilePath =
|
|
1378
|
+
const signaturesDir = path14.join(projectDir, "signatures");
|
|
1379
|
+
const profileTemplatePath = path14.join(signaturesDir, "UnsgnedReleasedProfileTemplate.json");
|
|
1380
|
+
const outputProfilePath = path14.join(signaturesDir, "app1-profile.p7b");
|
|
1289
1381
|
const args = [
|
|
1290
1382
|
"-jar",
|
|
1291
1383
|
paths.signTool,
|
|
@@ -1319,11 +1411,11 @@ function generateP7bFile(projectDir, paths, logger, passwords) {
|
|
|
1319
1411
|
}
|
|
1320
1412
|
}
|
|
1321
1413
|
function detectSigningConfigNames(projectDir) {
|
|
1322
|
-
const buildProfilePath =
|
|
1323
|
-
if (!
|
|
1414
|
+
const buildProfilePath = path14.join(projectDir, "build-profile.json5");
|
|
1415
|
+
if (!fs13.existsSync(buildProfilePath)) return ["default"];
|
|
1324
1416
|
let parsed;
|
|
1325
1417
|
try {
|
|
1326
|
-
parsed = JSON52.parse(
|
|
1418
|
+
parsed = JSON52.parse(fs13.readFileSync(buildProfilePath, "utf-8"));
|
|
1327
1419
|
} catch {
|
|
1328
1420
|
return ["default"];
|
|
1329
1421
|
}
|
|
@@ -1340,14 +1432,14 @@ function detectSigningConfigNames(projectDir) {
|
|
|
1340
1432
|
}
|
|
1341
1433
|
function updateBuildProfile(projectDir, kind, logger, passwords) {
|
|
1342
1434
|
logger.info("[sign] Writing signing configs into build-profile.json5...");
|
|
1343
|
-
const materialDir =
|
|
1344
|
-
const buildProfilePath =
|
|
1435
|
+
const materialDir = path14.join(projectDir, "signatures", "material");
|
|
1436
|
+
const buildProfilePath = path14.join(projectDir, "build-profile.json5");
|
|
1345
1437
|
const encryptedStorePassword = encryptPwd(passwords.store, materialDir);
|
|
1346
1438
|
const encryptedKeyPassword = encryptPwd(passwords.key, materialDir);
|
|
1347
1439
|
let buildProfile = { app: {} };
|
|
1348
|
-
if (
|
|
1440
|
+
if (fs13.existsSync(buildProfilePath)) {
|
|
1349
1441
|
try {
|
|
1350
|
-
buildProfile = JSON52.parse(
|
|
1442
|
+
buildProfile = JSON52.parse(fs13.readFileSync(buildProfilePath, "utf-8"));
|
|
1351
1443
|
} catch (e) {
|
|
1352
1444
|
throw new OniroError(`Error parsing ${buildProfilePath}: ${e.message}`, e);
|
|
1353
1445
|
}
|
|
@@ -1368,13 +1460,13 @@ function updateBuildProfile(projectDir, kind, logger, passwords) {
|
|
|
1368
1460
|
storeFile: "./signatures/OpenHarmony.p12"
|
|
1369
1461
|
}
|
|
1370
1462
|
}));
|
|
1371
|
-
|
|
1463
|
+
fs13.writeFileSync(buildProfilePath, JSON.stringify(buildProfile, null, 2));
|
|
1372
1464
|
}
|
|
1373
1465
|
function prepareMaterialDirectory(projectDir, logger) {
|
|
1374
1466
|
logger.info("[sign] Generating fresh signing material...");
|
|
1375
|
-
const materialDir =
|
|
1376
|
-
if (
|
|
1377
|
-
|
|
1467
|
+
const materialDir = path14.join(projectDir, "signatures", "material");
|
|
1468
|
+
if (fs13.existsSync(materialDir)) {
|
|
1469
|
+
fs13.rmSync(materialDir, { recursive: true, force: true });
|
|
1378
1470
|
}
|
|
1379
1471
|
createMaterial(materialDir);
|
|
1380
1472
|
}
|
|
@@ -1397,15 +1489,15 @@ function generateSigningConfigs(options) {
|
|
|
1397
1489
|
"Could not detect project SDK version (compileSdkVersion missing in build-profile.json5)."
|
|
1398
1490
|
);
|
|
1399
1491
|
}
|
|
1400
|
-
const sdkPath =
|
|
1401
|
-
if (!
|
|
1492
|
+
const sdkPath = path14.join(sdkHome, String(sdkVersion));
|
|
1493
|
+
if (!fs13.existsSync(sdkPath)) {
|
|
1402
1494
|
throw new OniroError(`SDK path does not exist: ${sdkPath}`);
|
|
1403
1495
|
}
|
|
1404
1496
|
const paths = {
|
|
1405
|
-
signTool:
|
|
1406
|
-
keystore:
|
|
1407
|
-
profileCert:
|
|
1408
|
-
unsignedProfileTemplate:
|
|
1497
|
+
signTool: path14.join(sdkPath, "toolchains/lib/hap-sign-tool.jar"),
|
|
1498
|
+
keystore: path14.join(sdkPath, "toolchains/lib/OpenHarmony.p12"),
|
|
1499
|
+
profileCert: path14.join(sdkPath, "toolchains/lib/OpenHarmonyProfileRelease.pem"),
|
|
1500
|
+
unsignedProfileTemplate: path14.join(sdkPath, "toolchains/lib/UnsgnedReleasedProfileTemplate.json")
|
|
1409
1501
|
};
|
|
1410
1502
|
const kind = pickSigningKind(apl);
|
|
1411
1503
|
const passwords = options.passwords ?? DEFAULT_PASSWORDS;
|
|
@@ -1419,8 +1511,8 @@ function generateSigningConfigs(options) {
|
|
|
1419
1511
|
}
|
|
1420
1512
|
|
|
1421
1513
|
// src/emulator/install.ts
|
|
1422
|
-
import * as
|
|
1423
|
-
import * as
|
|
1514
|
+
import * as fs14 from "fs";
|
|
1515
|
+
import * as path15 from "path";
|
|
1424
1516
|
async function installEmulator(opts) {
|
|
1425
1517
|
const { config, progress, abortSignal } = opts;
|
|
1426
1518
|
const logger = opts.logger ?? noopLogger;
|
|
@@ -1430,20 +1522,20 @@ async function installEmulator(opts) {
|
|
|
1430
1522
|
const { dir: tmpDir, root: tmpRoot } = createInstallTempDir({
|
|
1431
1523
|
config,
|
|
1432
1524
|
override: opts.tmpDir,
|
|
1433
|
-
installRoot:
|
|
1525
|
+
installRoot: path15.dirname(emulatorDir),
|
|
1434
1526
|
prefix: "oniro-emulator-",
|
|
1435
1527
|
logger
|
|
1436
1528
|
});
|
|
1437
|
-
const tmpZip =
|
|
1529
|
+
const tmpZip = path15.join(tmpDir, "oniro_emulator.zip");
|
|
1438
1530
|
try {
|
|
1439
|
-
|
|
1531
|
+
fs14.mkdirSync(emulatorDir, { recursive: true });
|
|
1440
1532
|
progress?.report({ message: "Downloading emulator...", increment: 0 });
|
|
1441
1533
|
await downloadFile({ url, dest: tmpZip, progress, abortSignal, start: 0, range: 50, what, tmpRoot });
|
|
1442
1534
|
progress?.report({ message: "Extracting emulator...", increment: 0 });
|
|
1443
1535
|
await extractZipWithProgress({ zipPath: tmpZip, dest: emulatorDir, progress, start: 50, range: 45, logger });
|
|
1444
|
-
const runSh =
|
|
1445
|
-
if (
|
|
1446
|
-
|
|
1536
|
+
const runSh = path15.join(emulatorDir, "images", "run.sh");
|
|
1537
|
+
if (fs14.existsSync(runSh)) {
|
|
1538
|
+
fs14.chmodSync(runSh, 493);
|
|
1447
1539
|
}
|
|
1448
1540
|
progress?.report({ message: "Finalizing installation...", increment: 5 });
|
|
1449
1541
|
} catch (err) {
|
|
@@ -1453,24 +1545,24 @@ async function installEmulator(opts) {
|
|
|
1453
1545
|
}
|
|
1454
1546
|
}
|
|
1455
1547
|
function isEmulatorInstalled(config) {
|
|
1456
|
-
const imagesDir =
|
|
1457
|
-
return
|
|
1548
|
+
const imagesDir = path15.join(getEmulatorDir(config), "images");
|
|
1549
|
+
return fs14.existsSync(path15.join(imagesDir, "run.sh")) || fs14.existsSync(path15.join(imagesDir, "run.bat"));
|
|
1458
1550
|
}
|
|
1459
1551
|
function removeEmulator(config) {
|
|
1460
1552
|
const emulatorDir = getEmulatorDir(config);
|
|
1461
|
-
if (
|
|
1462
|
-
|
|
1553
|
+
if (fs14.existsSync(emulatorDir)) {
|
|
1554
|
+
fs14.rmSync(emulatorDir, { recursive: true, force: true });
|
|
1463
1555
|
}
|
|
1464
1556
|
}
|
|
1465
1557
|
|
|
1466
1558
|
// src/emulator/lifecycle.ts
|
|
1467
|
-
import * as
|
|
1468
|
-
import * as
|
|
1469
|
-
import * as
|
|
1559
|
+
import * as fs16 from "fs";
|
|
1560
|
+
import * as os8 from "os";
|
|
1561
|
+
import * as path16 from "path";
|
|
1470
1562
|
import { spawn as spawn2 } from "child_process";
|
|
1471
1563
|
|
|
1472
1564
|
// src/hdc/exec.ts
|
|
1473
|
-
import * as
|
|
1565
|
+
import * as fs15 from "fs";
|
|
1474
1566
|
function errorMessage(err) {
|
|
1475
1567
|
return err instanceof Error ? err.message : String(err);
|
|
1476
1568
|
}
|
|
@@ -1478,12 +1570,12 @@ function runProcess(opts) {
|
|
|
1478
1570
|
const { command, args, cwd, env, onOutput } = opts;
|
|
1479
1571
|
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
1480
1572
|
const { abortSignal } = opts;
|
|
1481
|
-
return new Promise((
|
|
1573
|
+
return new Promise((resolve6, reject) => {
|
|
1482
1574
|
if (abortSignal?.aborted) {
|
|
1483
1575
|
reject(new CancelledError(`Command aborted before start: ${command}`));
|
|
1484
1576
|
return;
|
|
1485
1577
|
}
|
|
1486
|
-
if (needsCmdWrapper(command) && !
|
|
1578
|
+
if (needsCmdWrapper(command) && !fs15.existsSync(command)) {
|
|
1487
1579
|
reject(new OniroError(`Failed to spawn ${command}: ENOENT (no such file)`));
|
|
1488
1580
|
return;
|
|
1489
1581
|
}
|
|
@@ -1543,7 +1635,7 @@ ${stderr.trim()}` : "")
|
|
|
1543
1635
|
);
|
|
1544
1636
|
return;
|
|
1545
1637
|
}
|
|
1546
|
-
|
|
1638
|
+
resolve6({ code: code ?? -1, stdout, stderr });
|
|
1547
1639
|
});
|
|
1548
1640
|
});
|
|
1549
1641
|
}
|
|
@@ -1591,7 +1683,7 @@ function ensureRemoteOk(result, command) {
|
|
|
1591
1683
|
}
|
|
1592
1684
|
|
|
1593
1685
|
// src/emulator/lifecycle.ts
|
|
1594
|
-
var PID_FILE =
|
|
1686
|
+
var PID_FILE = path16.join(os8.tmpdir(), "oniro_emulator.pid");
|
|
1595
1687
|
async function attemptHdcConnection(config, address = "127.0.0.1:55555", logger = noopLogger) {
|
|
1596
1688
|
try {
|
|
1597
1689
|
await hdcExec({ config, args: ["start", "-r"], logger });
|
|
@@ -1604,24 +1696,24 @@ async function attemptHdcConnection(config, address = "127.0.0.1:55555", logger
|
|
|
1604
1696
|
}
|
|
1605
1697
|
}
|
|
1606
1698
|
function resolveLauncher(imagesPath, headless, connect) {
|
|
1607
|
-
const runSh =
|
|
1608
|
-
const runBat =
|
|
1699
|
+
const runSh = path16.join(imagesPath, "run.sh");
|
|
1700
|
+
const runBat = path16.join(imagesPath, "run.bat");
|
|
1609
1701
|
const extra = [];
|
|
1610
1702
|
if (headless) extra.push("--headless");
|
|
1611
1703
|
if (connect) extra.push("--connect", connect);
|
|
1612
|
-
if (
|
|
1613
|
-
if (
|
|
1704
|
+
if (os8.platform() === "win32") {
|
|
1705
|
+
if (fs16.existsSync(runBat)) {
|
|
1614
1706
|
return { command: "cmd.exe", args: ["/c", runBat, ...extra], cwd: imagesPath };
|
|
1615
1707
|
}
|
|
1616
|
-
if (
|
|
1708
|
+
if (fs16.existsSync(runSh)) {
|
|
1617
1709
|
return { command: "bash", args: [runSh, ...extra], cwd: imagesPath };
|
|
1618
1710
|
}
|
|
1619
1711
|
throw new OniroError(`No emulator launcher (run.bat or run.sh) found in ${imagesPath}.`);
|
|
1620
1712
|
}
|
|
1621
|
-
if (
|
|
1713
|
+
if (fs16.existsSync(runSh)) {
|
|
1622
1714
|
return { command: "bash", args: [runSh, ...extra], cwd: imagesPath };
|
|
1623
1715
|
}
|
|
1624
|
-
if (
|
|
1716
|
+
if (fs16.existsSync(runBat)) {
|
|
1625
1717
|
return { command: "bash", args: [runBat, ...extra], cwd: imagesPath };
|
|
1626
1718
|
}
|
|
1627
1719
|
throw new OniroError(`No emulator launcher (run.sh or run.bat) found in ${imagesPath}.`);
|
|
@@ -1629,9 +1721,9 @@ function resolveLauncher(imagesPath, headless, connect) {
|
|
|
1629
1721
|
async function startEmulator(opts) {
|
|
1630
1722
|
const { config } = opts;
|
|
1631
1723
|
const logger = opts.logger ?? noopLogger;
|
|
1632
|
-
if (
|
|
1724
|
+
if (os8.platform() !== "win32" && fs16.existsSync(PID_FILE)) {
|
|
1633
1725
|
try {
|
|
1634
|
-
const pid = Number(
|
|
1726
|
+
const pid = Number(fs16.readFileSync(PID_FILE, "utf8").trim());
|
|
1635
1727
|
if (Number.isFinite(pid) && pid > 0) {
|
|
1636
1728
|
try {
|
|
1637
1729
|
process.kill(pid, 0);
|
|
@@ -1647,13 +1739,13 @@ async function startEmulator(opts) {
|
|
|
1647
1739
|
logger.warn(`Could not read PID file: ${err.message}`);
|
|
1648
1740
|
}
|
|
1649
1741
|
}
|
|
1650
|
-
const imagesPath =
|
|
1651
|
-
if (!
|
|
1742
|
+
const imagesPath = path16.join(getEmulatorDir(config), "images");
|
|
1743
|
+
if (!fs16.existsSync(imagesPath)) {
|
|
1652
1744
|
throw new OniroError(`Emulator images directory not found at ${imagesPath}. Run \`oniro-app emulator install\` first.`);
|
|
1653
1745
|
}
|
|
1654
1746
|
const launcher = resolveLauncher(imagesPath, opts.headless === true, opts.connect);
|
|
1655
|
-
const logPath = opts.logFile ?? (
|
|
1656
|
-
const logFd =
|
|
1747
|
+
const logPath = opts.logFile ?? (os8.platform() === "win32" ? "NUL" : "/dev/null");
|
|
1748
|
+
const logFd = fs16.openSync(logPath, "a");
|
|
1657
1749
|
let child;
|
|
1658
1750
|
try {
|
|
1659
1751
|
child = spawn2(launcher.command, [...launcher.args], {
|
|
@@ -1663,19 +1755,19 @@ async function startEmulator(opts) {
|
|
|
1663
1755
|
windowsHide: true
|
|
1664
1756
|
});
|
|
1665
1757
|
} finally {
|
|
1666
|
-
|
|
1758
|
+
fs16.closeSync(logFd);
|
|
1667
1759
|
}
|
|
1668
1760
|
if (!child.pid) {
|
|
1669
1761
|
throw new OniroError(`Failed to spawn launcher (${launcher.command} ${launcher.args.join(" ")}).`);
|
|
1670
1762
|
}
|
|
1671
|
-
|
|
1763
|
+
fs16.writeFileSync(PID_FILE, String(child.pid));
|
|
1672
1764
|
child.unref();
|
|
1673
1765
|
logger.info(`Emulator launched (PID ${child.pid}). Logs: ${logPath}`);
|
|
1674
|
-
const earlyExit = new Promise((
|
|
1675
|
-
child.once("exit", (code) =>
|
|
1676
|
-
child.once("error", () =>
|
|
1766
|
+
const earlyExit = new Promise((resolve6) => {
|
|
1767
|
+
child.once("exit", (code) => resolve6(code));
|
|
1768
|
+
child.once("error", () => resolve6(-1));
|
|
1677
1769
|
});
|
|
1678
|
-
const liveness = new Promise((
|
|
1770
|
+
const liveness = new Promise((resolve6) => setTimeout(() => resolve6("alive"), 2e3));
|
|
1679
1771
|
const outcome = await Promise.race([earlyExit, liveness]);
|
|
1680
1772
|
if (outcome !== "alive") {
|
|
1681
1773
|
const tail = readTail(logPath, 4e3);
|
|
@@ -1715,24 +1807,24 @@ ${tail}` : ""}`);
|
|
|
1715
1807
|
}
|
|
1716
1808
|
function readTail(filePath, bytes) {
|
|
1717
1809
|
try {
|
|
1718
|
-
const stat =
|
|
1810
|
+
const stat = fs16.statSync(filePath);
|
|
1719
1811
|
const start = Math.max(0, stat.size - bytes);
|
|
1720
|
-
const fd =
|
|
1812
|
+
const fd = fs16.openSync(filePath, "r");
|
|
1721
1813
|
try {
|
|
1722
1814
|
const buf = Buffer.alloc(stat.size - start);
|
|
1723
|
-
|
|
1815
|
+
fs16.readSync(fd, buf, 0, buf.length, start);
|
|
1724
1816
|
return buf.toString("utf8");
|
|
1725
1817
|
} finally {
|
|
1726
|
-
|
|
1818
|
+
fs16.closeSync(fd);
|
|
1727
1819
|
}
|
|
1728
1820
|
} catch {
|
|
1729
1821
|
return "";
|
|
1730
1822
|
}
|
|
1731
1823
|
}
|
|
1732
1824
|
async function stopEmulator(logger = noopLogger) {
|
|
1733
|
-
if (
|
|
1825
|
+
if (fs16.existsSync(PID_FILE)) {
|
|
1734
1826
|
try {
|
|
1735
|
-
const pid = Number(
|
|
1827
|
+
const pid = Number(fs16.readFileSync(PID_FILE, "utf8").trim());
|
|
1736
1828
|
if (Number.isFinite(pid) && pid > 0) {
|
|
1737
1829
|
try {
|
|
1738
1830
|
process.kill(pid, "SIGTERM");
|
|
@@ -1745,13 +1837,13 @@ async function stopEmulator(logger = noopLogger) {
|
|
|
1745
1837
|
logger.warn(`Could not read PID file: ${err.message}`);
|
|
1746
1838
|
}
|
|
1747
1839
|
}
|
|
1748
|
-
const [killCmd, ...killArgs] =
|
|
1840
|
+
const [killCmd, ...killArgs] = os8.platform() === "win32" ? ["taskkill", "/IM", "qemu-system-x86_64.exe", "/F"] : ["pkill", "-f", "qemu-system-x86_64"];
|
|
1749
1841
|
try {
|
|
1750
1842
|
await runProcess({ command: killCmd, args: killArgs, logger });
|
|
1751
1843
|
} catch (err) {
|
|
1752
1844
|
logger.warn(`[emulator] could not run ${killCmd}: ${err.message}`);
|
|
1753
1845
|
}
|
|
1754
|
-
|
|
1846
|
+
fs16.rm(PID_FILE, { force: true }, (err) => {
|
|
1755
1847
|
if (err) logger.warn(`Could not remove PID file: ${err.message}`);
|
|
1756
1848
|
});
|
|
1757
1849
|
}
|
|
@@ -1874,7 +1966,7 @@ async function getDisplaySize(config, opts = {}) {
|
|
|
1874
1966
|
|
|
1875
1967
|
// src/hdc/wait.ts
|
|
1876
1968
|
function delay(ms, signal) {
|
|
1877
|
-
return new Promise((
|
|
1969
|
+
return new Promise((resolve6, reject) => {
|
|
1878
1970
|
if (signal?.aborted) {
|
|
1879
1971
|
reject(new CancelledError("Wait cancelled."));
|
|
1880
1972
|
return;
|
|
@@ -1889,7 +1981,7 @@ function delay(ms, signal) {
|
|
|
1889
1981
|
};
|
|
1890
1982
|
const timer = setTimeout(() => {
|
|
1891
1983
|
cleanup();
|
|
1892
|
-
|
|
1984
|
+
resolve6();
|
|
1893
1985
|
}, ms);
|
|
1894
1986
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1895
1987
|
});
|
|
@@ -1918,20 +2010,20 @@ async function waitForCondition(opts) {
|
|
|
1918
2010
|
}
|
|
1919
2011
|
|
|
1920
2012
|
// src/hdc/app.ts
|
|
1921
|
-
import * as
|
|
1922
|
-
import * as
|
|
2013
|
+
import * as fs18 from "fs";
|
|
2014
|
+
import * as path18 from "path";
|
|
1923
2015
|
|
|
1924
2016
|
// src/hdc/project.ts
|
|
1925
|
-
import * as
|
|
1926
|
-
import * as
|
|
2017
|
+
import * as fs17 from "fs";
|
|
2018
|
+
import * as path17 from "path";
|
|
1927
2019
|
import JSON53 from "json5";
|
|
1928
2020
|
function readJson5(filePath) {
|
|
1929
|
-
const content =
|
|
2021
|
+
const content = fs17.readFileSync(filePath, "utf-8");
|
|
1930
2022
|
return JSON53.parse(content);
|
|
1931
2023
|
}
|
|
1932
2024
|
function getBundleName(projectDir) {
|
|
1933
|
-
const appJsonPath =
|
|
1934
|
-
if (!
|
|
2025
|
+
const appJsonPath = path17.join(projectDir, "AppScope", "app.json5");
|
|
2026
|
+
if (!fs17.existsSync(appJsonPath)) {
|
|
1935
2027
|
throw new OniroError(`Could not find app.json5 at ${appJsonPath}`);
|
|
1936
2028
|
}
|
|
1937
2029
|
const appJson = readJson5(appJsonPath);
|
|
@@ -1941,8 +2033,8 @@ function getBundleName(projectDir) {
|
|
|
1941
2033
|
return appJson.app.bundleName;
|
|
1942
2034
|
}
|
|
1943
2035
|
function getMainAbility(projectDir, moduleName = "entry", abilityName) {
|
|
1944
|
-
const moduleJsonPath =
|
|
1945
|
-
if (!
|
|
2036
|
+
const moduleJsonPath = path17.join(projectDir, moduleName, "src", "main", "module.json5");
|
|
2037
|
+
if (!fs17.existsSync(moduleJsonPath)) {
|
|
1946
2038
|
throw new OniroError(`Could not find module.json5 at ${moduleJsonPath}`);
|
|
1947
2039
|
}
|
|
1948
2040
|
const moduleJson = readJson5(moduleJsonPath);
|
|
@@ -1965,8 +2057,8 @@ function getMainAbility(projectDir, moduleName = "entry", abilityName) {
|
|
|
1965
2057
|
async function installApp(opts) {
|
|
1966
2058
|
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
1967
2059
|
const relativeHapPath = opts.hapPath ?? opts.config.get("hapPath", defaultPaths.hapPath);
|
|
1968
|
-
const hapPath =
|
|
1969
|
-
if (!
|
|
2060
|
+
const hapPath = path18.isAbsolute(relativeHapPath) ? relativeHapPath : path18.join(opts.projectDir, relativeHapPath);
|
|
2061
|
+
if (!fs18.existsSync(hapPath)) {
|
|
1970
2062
|
throw new OniroError(`HAP file not found at: ${hapPath}. Build and sign the app first.`);
|
|
1971
2063
|
}
|
|
1972
2064
|
const result = await hdcExec({
|
|
@@ -2055,7 +2147,7 @@ function listRunningProcesses(config, options = {}) {
|
|
|
2055
2147
|
const logger = options.logger ?? noopLogger;
|
|
2056
2148
|
const timeoutMs = options.timeoutMs ?? 1e3;
|
|
2057
2149
|
const hdc = getHdcPath(config);
|
|
2058
|
-
return new Promise((
|
|
2150
|
+
return new Promise((resolve6, reject) => {
|
|
2059
2151
|
let proc;
|
|
2060
2152
|
try {
|
|
2061
2153
|
proc = spawnCompat(hdc, ["track-jpid"]);
|
|
@@ -2075,7 +2167,7 @@ function listRunningProcesses(config, options = {}) {
|
|
|
2075
2167
|
if (options.targetProcessName && name === options.targetProcessName && !resolved) {
|
|
2076
2168
|
resolved = true;
|
|
2077
2169
|
killProcessTree(proc);
|
|
2078
|
-
|
|
2170
|
+
resolve6(pid);
|
|
2079
2171
|
return;
|
|
2080
2172
|
}
|
|
2081
2173
|
}
|
|
@@ -2096,7 +2188,7 @@ function listRunningProcesses(config, options = {}) {
|
|
|
2096
2188
|
if (options.targetProcessName) {
|
|
2097
2189
|
reject(new OniroError(`Could not find process for bundle: ${options.targetProcessName}`));
|
|
2098
2190
|
} else {
|
|
2099
|
-
|
|
2191
|
+
resolve6(processes);
|
|
2100
2192
|
}
|
|
2101
2193
|
}, timeoutMs);
|
|
2102
2194
|
proc.on("close", () => {
|
|
@@ -2106,7 +2198,7 @@ function listRunningProcesses(config, options = {}) {
|
|
|
2106
2198
|
if (options.targetProcessName) {
|
|
2107
2199
|
reject(new OniroError(`Could not find process for bundle: ${options.targetProcessName}`));
|
|
2108
2200
|
} else {
|
|
2109
|
-
|
|
2201
|
+
resolve6(processes);
|
|
2110
2202
|
}
|
|
2111
2203
|
});
|
|
2112
2204
|
});
|
|
@@ -2171,19 +2263,39 @@ async function isReachable(config, deviceSerial) {
|
|
|
2171
2263
|
}
|
|
2172
2264
|
}
|
|
2173
2265
|
var TCP_SERIAL_RE = /^.+:\d+$/;
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2266
|
+
var WEDGED_POLLS_BEFORE_RESTART = 3;
|
|
2267
|
+
function tcpReconnector(config, deviceSerial, logger) {
|
|
2268
|
+
let wedgedPolls = 0;
|
|
2269
|
+
return async () => {
|
|
2270
|
+
if (!deviceSerial || !TCP_SERIAL_RE.test(deviceSerial)) return;
|
|
2271
|
+
let claimsConnected = false;
|
|
2272
|
+
try {
|
|
2273
|
+
const res = await hdcExec({ config, args: ["tconn", deviceSerial], timeoutMs: 5e3, logger });
|
|
2274
|
+
claimsConnected = /already connected|repeat operation/i.test(`${res.stdout}
|
|
2275
|
+
${res.stderr}`);
|
|
2276
|
+
} catch {
|
|
2277
|
+
}
|
|
2278
|
+
if (!claimsConnected || await isReachable(config, deviceSerial)) {
|
|
2279
|
+
wedgedPolls = 0;
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
if (++wedgedPolls < WEDGED_POLLS_BEFORE_RESTART) return;
|
|
2283
|
+
wedgedPolls = 0;
|
|
2284
|
+
logger.warn(`hdc reports ${deviceSerial} as connected but it does not answer; restarting the hdc server.`);
|
|
2285
|
+
try {
|
|
2286
|
+
await hdcExec({ config, args: ["start", "-r"], timeoutMs: 15e3, logger });
|
|
2287
|
+
await hdcExec({ config, args: ["tconn", deviceSerial], timeoutMs: 5e3, logger });
|
|
2288
|
+
} catch {
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2180
2291
|
}
|
|
2181
2292
|
async function waitForBundle(opts) {
|
|
2182
2293
|
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
2183
2294
|
const timeoutMs = opts.timeoutMs ?? 18e4;
|
|
2295
|
+
const reconnectIfTcp = tcpReconnector(opts.config, opts.deviceSerial, logger);
|
|
2184
2296
|
await waitForCondition({
|
|
2185
2297
|
probe: async () => {
|
|
2186
|
-
await reconnectIfTcp(
|
|
2298
|
+
await reconnectIfTcp();
|
|
2187
2299
|
return await findRunningProcess({ config: opts.config, bundle: opts.bundle, deviceSerial: opts.deviceSerial, timeoutMs: 8e3 }) !== null;
|
|
2188
2300
|
},
|
|
2189
2301
|
timeoutMs,
|
|
@@ -2196,9 +2308,10 @@ async function waitForBundle(opts) {
|
|
|
2196
2308
|
async function waitForBoot(opts) {
|
|
2197
2309
|
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
2198
2310
|
const timeoutMs = opts.timeoutMs ?? 18e4;
|
|
2311
|
+
const reconnectIfTcp = tcpReconnector(opts.config, opts.deviceSerial, logger);
|
|
2199
2312
|
await waitForCondition({
|
|
2200
2313
|
probe: async () => {
|
|
2201
|
-
await reconnectIfTcp(
|
|
2314
|
+
await reconnectIfTcp();
|
|
2202
2315
|
if (!await isReachable(opts.config, opts.deviceSerial)) return false;
|
|
2203
2316
|
if (!opts.untilPidOf) return true;
|
|
2204
2317
|
return await findRunningProcess({ config: opts.config, bundle: opts.untilPidOf, deviceSerial: opts.deviceSerial, timeoutMs: 8e3 }) !== null;
|
|
@@ -2250,12 +2363,12 @@ async function reboot(opts) {
|
|
|
2250
2363
|
}
|
|
2251
2364
|
|
|
2252
2365
|
// src/hdc/screen.ts
|
|
2253
|
-
import * as
|
|
2254
|
-
import * as
|
|
2255
|
-
import * as
|
|
2366
|
+
import * as fs19 from "fs";
|
|
2367
|
+
import * as os9 from "os";
|
|
2368
|
+
import * as path19 from "path";
|
|
2256
2369
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2257
2370
|
function tmpName(ext) {
|
|
2258
|
-
return
|
|
2371
|
+
return path19.join(os9.tmpdir(), `oniro_screen_${randomBytes2(6).toString("hex")}.${ext}`);
|
|
2259
2372
|
}
|
|
2260
2373
|
async function takeScreenshot(opts) {
|
|
2261
2374
|
const remote = "/data/local/tmp/oniro_screenshot.jpeg";
|
|
@@ -2265,11 +2378,11 @@ async function takeScreenshot(opts) {
|
|
|
2265
2378
|
const local = tmpName("jpeg");
|
|
2266
2379
|
try {
|
|
2267
2380
|
await recvFile({ config: opts.config, remote, local, deviceSerial: opts.deviceSerial, timeoutMs });
|
|
2268
|
-
const pixels = await
|
|
2381
|
+
const pixels = await fs19.promises.readFile(local);
|
|
2269
2382
|
const size = await getDisplaySize(opts.config, { deviceSerial: opts.deviceSerial }) ?? { width: 0, height: 0 };
|
|
2270
2383
|
return { pixels, width: size.width, height: size.height };
|
|
2271
2384
|
} finally {
|
|
2272
|
-
|
|
2385
|
+
fs19.promises.unlink(local).catch(() => {
|
|
2273
2386
|
});
|
|
2274
2387
|
void shell({ config: opts.config, command: `rm -f ${remote}`, deviceSerial: opts.deviceSerial }).catch(() => {
|
|
2275
2388
|
});
|
|
@@ -2288,9 +2401,9 @@ async function captureBurst(opts) {
|
|
|
2288
2401
|
const local = tmpName(`burst_${i}.jpeg`);
|
|
2289
2402
|
try {
|
|
2290
2403
|
await recvFile({ config: opts.config, remote: `${remoteDir}/f${i}.jpeg`, local, deviceSerial: opts.deviceSerial });
|
|
2291
|
-
frames.push(await
|
|
2404
|
+
frames.push(await fs19.promises.readFile(local));
|
|
2292
2405
|
} finally {
|
|
2293
|
-
|
|
2406
|
+
fs19.promises.unlink(local).catch(() => {
|
|
2294
2407
|
});
|
|
2295
2408
|
}
|
|
2296
2409
|
}
|
|
@@ -2416,9 +2529,9 @@ async function sendRawTouch(opts) {
|
|
|
2416
2529
|
}
|
|
2417
2530
|
|
|
2418
2531
|
// src/hdc/dump.ts
|
|
2419
|
-
import * as
|
|
2420
|
-
import * as
|
|
2421
|
-
import * as
|
|
2532
|
+
import * as fs20 from "fs";
|
|
2533
|
+
import * as os10 from "os";
|
|
2534
|
+
import * as path20 from "path";
|
|
2422
2535
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
2423
2536
|
function parseBounds(s) {
|
|
2424
2537
|
const m = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/.exec(s ?? "");
|
|
@@ -2503,10 +2616,10 @@ async function dumpLayout(opts) {
|
|
|
2503
2616
|
if (!m) throw new OniroError(`Could not parse dumpLayout output:
|
|
2504
2617
|
${dump.stdout}`);
|
|
2505
2618
|
const remote = m[1];
|
|
2506
|
-
const local =
|
|
2619
|
+
const local = path20.join(os10.tmpdir(), `oniro_layout_${randomBytes3(6).toString("hex")}.json`);
|
|
2507
2620
|
try {
|
|
2508
2621
|
await recvFile({ config: opts.config, remote, local, deviceSerial: opts.deviceSerial, timeoutMs });
|
|
2509
|
-
const parsed = JSON.parse(await
|
|
2622
|
+
const parsed = JSON.parse(await fs20.promises.readFile(local, "utf8"));
|
|
2510
2623
|
const display = await getDisplaySize(opts.config, { deviceSerial: opts.deviceSerial });
|
|
2511
2624
|
if (opts.prune === false) {
|
|
2512
2625
|
return { display, tree: parsed };
|
|
@@ -2515,7 +2628,7 @@ ${dump.stdout}`);
|
|
|
2515
2628
|
const tree = Array.isArray(prunedRaw) ? { children: prunedRaw } : prunedRaw ?? { children: [] };
|
|
2516
2629
|
return { display, tree };
|
|
2517
2630
|
} finally {
|
|
2518
|
-
|
|
2631
|
+
fs20.promises.unlink(local).catch(() => {
|
|
2519
2632
|
});
|
|
2520
2633
|
void shell({ config: opts.config, command: `rm -f ${remote}`, deviceSerial: opts.deviceSerial }).catch(() => {
|
|
2521
2634
|
});
|
|
@@ -2526,7 +2639,7 @@ ${dump.stdout}`);
|
|
|
2526
2639
|
function setHilogLevel(opts) {
|
|
2527
2640
|
const logger = opts.logger ?? noopLogger;
|
|
2528
2641
|
const hdc = getHdcPath(opts.config);
|
|
2529
|
-
return new Promise((
|
|
2642
|
+
return new Promise((resolve6, reject) => {
|
|
2530
2643
|
let child;
|
|
2531
2644
|
try {
|
|
2532
2645
|
child = spawnCompat(hdc, ["shell", "hilog", "-b", opts.level]);
|
|
@@ -2546,7 +2659,7 @@ function setHilogLevel(opts) {
|
|
|
2546
2659
|
});
|
|
2547
2660
|
child.once("close", (code) => {
|
|
2548
2661
|
if (code === 0) {
|
|
2549
|
-
|
|
2662
|
+
resolve6();
|
|
2550
2663
|
} else {
|
|
2551
2664
|
const msg = stderr.trim() || `hdc shell hilog -b ${opts.level} exited with code ${code}`;
|
|
2552
2665
|
logger.error(`[hilog] ${msg}`);
|
|
@@ -2601,7 +2714,7 @@ async function resolveProcessId(config, bundle, deviceSerial) {
|
|
|
2601
2714
|
return proc?.pid;
|
|
2602
2715
|
}
|
|
2603
2716
|
function waitForLog(opts) {
|
|
2604
|
-
return new Promise((
|
|
2717
|
+
return new Promise((resolve6, reject) => {
|
|
2605
2718
|
const deadline = Date.now() + opts.timeoutMs;
|
|
2606
2719
|
let settled = false;
|
|
2607
2720
|
let child = null;
|
|
@@ -2620,7 +2733,7 @@ function waitForLog(opts) {
|
|
|
2620
2733
|
if (settled) return;
|
|
2621
2734
|
settled = true;
|
|
2622
2735
|
cleanup();
|
|
2623
|
-
|
|
2736
|
+
resolve6(entry);
|
|
2624
2737
|
};
|
|
2625
2738
|
const fail = (err) => {
|
|
2626
2739
|
if (settled) return;
|
|
@@ -2667,7 +2780,7 @@ function waitForLog(opts) {
|
|
|
2667
2780
|
});
|
|
2668
2781
|
}
|
|
2669
2782
|
function watchLog(opts) {
|
|
2670
|
-
return new Promise((
|
|
2783
|
+
return new Promise((resolve6, reject) => {
|
|
2671
2784
|
const matches = [];
|
|
2672
2785
|
let settled = false;
|
|
2673
2786
|
let child = null;
|
|
@@ -2684,7 +2797,7 @@ function watchLog(opts) {
|
|
|
2684
2797
|
if (settled) return;
|
|
2685
2798
|
settled = true;
|
|
2686
2799
|
cleanup();
|
|
2687
|
-
|
|
2800
|
+
resolve6(opts.dedup === false ? matches : dedupEntries(matches));
|
|
2688
2801
|
};
|
|
2689
2802
|
const onAbort = () => {
|
|
2690
2803
|
if (settled) return;
|
|
@@ -2740,8 +2853,43 @@ async function dumpLog(opts) {
|
|
|
2740
2853
|
}
|
|
2741
2854
|
|
|
2742
2855
|
// src/build/runHvigorw.ts
|
|
2743
|
-
import * as
|
|
2744
|
-
import * as
|
|
2856
|
+
import * as fs22 from "fs";
|
|
2857
|
+
import * as path22 from "path";
|
|
2858
|
+
|
|
2859
|
+
// src/harmonyos/target.ts
|
|
2860
|
+
import * as fs21 from "fs";
|
|
2861
|
+
import * as path21 from "path";
|
|
2862
|
+
import JSON54 from "json5";
|
|
2863
|
+
function detectRuntimeOs(projectDir, productName) {
|
|
2864
|
+
let products = [];
|
|
2865
|
+
try {
|
|
2866
|
+
const parsed = JSON54.parse(fs21.readFileSync(path21.join(projectDir, "build-profile.json5"), "utf-8"));
|
|
2867
|
+
if (Array.isArray(parsed.app?.products)) products = parsed.app.products;
|
|
2868
|
+
} catch {
|
|
2869
|
+
return "OpenHarmony";
|
|
2870
|
+
}
|
|
2871
|
+
const product = products.find((p) => p.name === productName) ?? products[0];
|
|
2872
|
+
return product?.runtimeOS === "HarmonyOS" ? "HarmonyOS" : "OpenHarmony";
|
|
2873
|
+
}
|
|
2874
|
+
var HARMONYOS_SDK_VERSIONS = /* @__PURE__ */ new Map([
|
|
2875
|
+
[12, "5.0.0"],
|
|
2876
|
+
[13, "5.0.1"],
|
|
2877
|
+
[14, "5.0.2"],
|
|
2878
|
+
[15, "5.0.3"],
|
|
2879
|
+
[16, "5.0.4"],
|
|
2880
|
+
[17, "5.0.5"],
|
|
2881
|
+
[18, "5.1.0"],
|
|
2882
|
+
[19, "5.1.1"],
|
|
2883
|
+
[20, "6.0.0"],
|
|
2884
|
+
[21, "6.0.1"],
|
|
2885
|
+
[24, "6.1.1"]
|
|
2886
|
+
]);
|
|
2887
|
+
function harmonyOsSdkLabelForApi(api) {
|
|
2888
|
+
const version = HARMONYOS_SDK_VERSIONS.get(api);
|
|
2889
|
+
return version ? `${version}(${api})` : void 0;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// src/build/runHvigorw.ts
|
|
2745
2893
|
function buildHvigorwArgs(opts) {
|
|
2746
2894
|
const args = [opts.task ?? "assembleHap", "--mode", "module"];
|
|
2747
2895
|
args.push("-p", `product=${opts.product ?? "default"}`);
|
|
@@ -2756,28 +2904,36 @@ function runHvigorw(opts) {
|
|
|
2756
2904
|
const { config } = opts;
|
|
2757
2905
|
const projectDir = toLongPath(opts.projectDir);
|
|
2758
2906
|
const logger = opts.logger ?? noopLogger;
|
|
2759
|
-
if (!
|
|
2907
|
+
if (!fs22.existsSync(path22.join(projectDir, "build-profile.json5"))) {
|
|
2760
2908
|
throw new OniroError(`Not an OpenHarmony project: ${projectDir} (build-profile.json5 not found).`);
|
|
2761
2909
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2910
|
+
let hvigorw;
|
|
2911
|
+
let sdkEnv;
|
|
2912
|
+
if (detectRuntimeOs(projectDir, opts.product) === "HarmonyOS") {
|
|
2913
|
+
const sdk = requireHarmonyOsSdk({ config, logger });
|
|
2914
|
+
sdkEnv = { DEVECO_SDK_HOME: sdk.sdkPath };
|
|
2915
|
+
const resolved = getHvigorwPath(config, projectDir);
|
|
2916
|
+
hvigorw = resolved.startsWith(projectDir) ? resolved : findHarmonyOsTool(sdk, "hvigorw") ?? resolved;
|
|
2917
|
+
logger.info(`[build] Building for HarmonyOS against the SDK at ${sdk.sdkPath}.`);
|
|
2918
|
+
} else {
|
|
2919
|
+
if (!fs22.existsSync(getCmdToolsPath(config))) {
|
|
2920
|
+
throw new CmdToolsNotInstalledError(getCmdToolsPath(config));
|
|
2921
|
+
}
|
|
2922
|
+
sdkEnv = { OHOS_BASE_SDK_HOME: getOhosBaseSdkHome(config) };
|
|
2923
|
+
hvigorw = getHvigorwPath(config, projectDir);
|
|
2764
2924
|
}
|
|
2765
|
-
|
|
2766
|
-
if (!fs20.existsSync(hvigorw)) {
|
|
2925
|
+
if (!fs22.existsSync(hvigorw)) {
|
|
2767
2926
|
throw new OniroError(`hvigorw wrapper not found at ${hvigorw}.`);
|
|
2768
2927
|
}
|
|
2769
2928
|
const args = buildHvigorwArgs(opts);
|
|
2770
2929
|
if (process.platform !== "win32") {
|
|
2771
2930
|
try {
|
|
2772
|
-
|
|
2931
|
+
fs22.chmodSync(hvigorw, 493);
|
|
2773
2932
|
} catch {
|
|
2774
2933
|
}
|
|
2775
2934
|
}
|
|
2776
|
-
const env = {
|
|
2777
|
-
|
|
2778
|
-
OHOS_BASE_SDK_HOME: getOhosBaseSdkHome(config)
|
|
2779
|
-
};
|
|
2780
|
-
return new Promise((resolve4, reject) => {
|
|
2935
|
+
const env = { ...process.env, ...sdkEnv };
|
|
2936
|
+
return new Promise((resolve6, reject) => {
|
|
2781
2937
|
if (opts.abortSignal?.aborted) {
|
|
2782
2938
|
reject(new CancelledError("hvigorw build cancelled."));
|
|
2783
2939
|
return;
|
|
@@ -2821,15 +2977,15 @@ function runHvigorw(opts) {
|
|
|
2821
2977
|
settled = true;
|
|
2822
2978
|
cleanup();
|
|
2823
2979
|
const exitCode = code ?? 1;
|
|
2824
|
-
if (exitCode === 0)
|
|
2980
|
+
if (exitCode === 0) resolve6({ exitCode });
|
|
2825
2981
|
else reject(new OniroError(`hvigorw exited with code ${exitCode}`));
|
|
2826
2982
|
});
|
|
2827
2983
|
});
|
|
2828
2984
|
}
|
|
2829
2985
|
|
|
2830
2986
|
// src/build/discoverHaps.ts
|
|
2831
|
-
import * as
|
|
2832
|
-
import * as
|
|
2987
|
+
import * as fs23 from "fs";
|
|
2988
|
+
import * as path23 from "path";
|
|
2833
2989
|
function moduleOfHap(hapFileName) {
|
|
2834
2990
|
const dash = hapFileName.indexOf("-");
|
|
2835
2991
|
return dash > 0 ? hapFileName.slice(0, dash) : hapFileName.replace(/\.hap$/i, "");
|
|
@@ -2840,9 +2996,9 @@ async function discoverHaps(opts) {
|
|
|
2840
2996
|
const result = {};
|
|
2841
2997
|
const walk = async (dir, depth) => {
|
|
2842
2998
|
if (depth > maxDepth) return;
|
|
2843
|
-
const entries = await
|
|
2999
|
+
const entries = await fs23.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2844
3000
|
for (const e of entries) {
|
|
2845
|
-
const full =
|
|
3001
|
+
const full = path23.join(dir, e.name);
|
|
2846
3002
|
if (e.isDirectory()) {
|
|
2847
3003
|
if (e.name === "node_modules" || e.name === "oh_modules" || e.name === ".git") continue;
|
|
2848
3004
|
await walk(full, depth + 1);
|
|
@@ -2859,11 +3015,11 @@ async function discoverHaps(opts) {
|
|
|
2859
3015
|
}
|
|
2860
3016
|
|
|
2861
3017
|
// src/build/ohpm.ts
|
|
2862
|
-
import * as
|
|
2863
|
-
import * as
|
|
3018
|
+
import * as fs24 from "fs";
|
|
3019
|
+
import * as path24 from "path";
|
|
2864
3020
|
function runOhpm(opts) {
|
|
2865
3021
|
return runProcess({
|
|
2866
|
-
command: getOhpmPath(opts.config),
|
|
3022
|
+
command: opts.ohpmPath ?? getOhpmPath(opts.config),
|
|
2867
3023
|
args: [...opts.args],
|
|
2868
3024
|
cwd: opts.projectDir,
|
|
2869
3025
|
timeoutMs: opts.timeoutMs ?? 6e5,
|
|
@@ -2873,12 +3029,13 @@ function runOhpm(opts) {
|
|
|
2873
3029
|
});
|
|
2874
3030
|
}
|
|
2875
3031
|
async function ensureOhModules(opts) {
|
|
2876
|
-
if (
|
|
3032
|
+
if (fs24.existsSync(path24.join(opts.projectDir, "oh_modules"))) {
|
|
2877
3033
|
return { installed: false };
|
|
2878
3034
|
}
|
|
2879
3035
|
const res = await runOhpm({
|
|
2880
3036
|
config: opts.config,
|
|
2881
3037
|
projectDir: opts.projectDir,
|
|
3038
|
+
ohpmPath: opts.ohpmPath,
|
|
2882
3039
|
args: ["install", "--all"],
|
|
2883
3040
|
abortSignal: opts.abortSignal,
|
|
2884
3041
|
onOutput: opts.onOutput,
|
|
@@ -2926,10 +3083,13 @@ async function buildHap(opts) {
|
|
|
2926
3083
|
const warnings = [];
|
|
2927
3084
|
const start = Date.now();
|
|
2928
3085
|
const projectDir = toLongPath(opts.projectDir);
|
|
3086
|
+
const isHarmonyOs = detectRuntimeOs(projectDir, opts.product) === "HarmonyOS";
|
|
2929
3087
|
if (opts.autoInstallDeps !== false) {
|
|
3088
|
+
const harmonySdk = isHarmonyOs ? findHarmonyOsSdk({ config: opts.config, logger: opts.logger }) : null;
|
|
2930
3089
|
const r = await ensureOhModules({
|
|
2931
3090
|
config: opts.config,
|
|
2932
3091
|
projectDir,
|
|
3092
|
+
ohpmPath: (harmonySdk && findHarmonyOsTool(harmonySdk, "ohpm")) ?? void 0,
|
|
2933
3093
|
abortSignal: opts.abortSignal,
|
|
2934
3094
|
onOutput: opts.onOutput,
|
|
2935
3095
|
logger: opts.logger
|
|
@@ -2952,7 +3112,7 @@ async function buildHap(opts) {
|
|
|
2952
3112
|
const anySigned = Object.values(discoveredHaps).some((m) => m.signed.length > 0);
|
|
2953
3113
|
if (!anySigned) {
|
|
2954
3114
|
warnings.push(
|
|
2955
|
-
"No signed HAPs found \u2014 configure signingConfigs in build-profile.json5 (unsigned HAPs are rejected by most devices)."
|
|
3115
|
+
isHarmonyOs ? "No signed HAPs found \u2014 run `oniro-app sign --harmonyos` to have AppGallery Connect issue a certificate and profile." : "No signed HAPs found \u2014 configure signingConfigs in build-profile.json5 (unsigned HAPs are rejected by most devices)."
|
|
2956
3116
|
);
|
|
2957
3117
|
}
|
|
2958
3118
|
return { exitCode, durationMs: Date.now() - start, discoveredHaps, warnings };
|
|
@@ -2969,21 +3129,21 @@ function isValidBundleName(bundleName) {
|
|
|
2969
3129
|
}
|
|
2970
3130
|
|
|
2971
3131
|
// src/project/templates.ts
|
|
2972
|
-
import * as
|
|
2973
|
-
import * as
|
|
3132
|
+
import * as fs25 from "fs";
|
|
3133
|
+
import * as path25 from "path";
|
|
2974
3134
|
function toHumanTemplateName(folderName) {
|
|
2975
3135
|
return folderName.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
2976
3136
|
}
|
|
2977
3137
|
function listTemplates(templateRoot) {
|
|
2978
|
-
if (!
|
|
2979
|
-
const entries =
|
|
3138
|
+
if (!fs25.existsSync(templateRoot)) return [];
|
|
3139
|
+
const entries = fs25.readdirSync(templateRoot, { withFileTypes: true });
|
|
2980
3140
|
return entries.filter((e) => e.isDirectory()).map((e) => {
|
|
2981
|
-
const dir =
|
|
2982
|
-
const metaPath =
|
|
3141
|
+
const dir = path25.join(templateRoot, e.name);
|
|
3142
|
+
const metaPath = path25.join(dir, "template.json");
|
|
2983
3143
|
let meta = {};
|
|
2984
|
-
if (
|
|
3144
|
+
if (fs25.existsSync(metaPath)) {
|
|
2985
3145
|
try {
|
|
2986
|
-
meta = JSON.parse(
|
|
3146
|
+
meta = JSON.parse(fs25.readFileSync(metaPath, "utf8"));
|
|
2987
3147
|
} catch {
|
|
2988
3148
|
}
|
|
2989
3149
|
}
|
|
@@ -2998,57 +3158,57 @@ function listTemplates(templateRoot) {
|
|
|
2998
3158
|
function validateTemplateLayout(templateDir, defaultModuleName) {
|
|
2999
3159
|
const required = [
|
|
3000
3160
|
"build-profile.json5",
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3161
|
+
path25.join("AppScope", "app.json5"),
|
|
3162
|
+
path25.join(defaultModuleName, "src", "main", "module.json5"),
|
|
3163
|
+
path25.join(defaultModuleName, "oh-package.json5"),
|
|
3004
3164
|
"hvigorfile.ts"
|
|
3005
3165
|
];
|
|
3006
|
-
return required.filter((rel) => !
|
|
3166
|
+
return required.filter((rel) => !fs25.existsSync(path25.join(templateDir, rel)));
|
|
3007
3167
|
}
|
|
3008
3168
|
|
|
3009
3169
|
// src/project/createScaffold.ts
|
|
3010
|
-
import * as
|
|
3011
|
-
import * as
|
|
3170
|
+
import * as fs27 from "fs";
|
|
3171
|
+
import * as path26 from "path";
|
|
3012
3172
|
|
|
3013
3173
|
// src/project/jsonHelpers.ts
|
|
3014
|
-
import * as
|
|
3015
|
-
import
|
|
3174
|
+
import * as fs26 from "fs";
|
|
3175
|
+
import JSON55 from "json5";
|
|
3016
3176
|
function readJson5File(filePath) {
|
|
3017
|
-
return
|
|
3177
|
+
return JSON55.parse(fs26.readFileSync(filePath, "utf8"));
|
|
3018
3178
|
}
|
|
3019
3179
|
function writeJson5File(filePath, value) {
|
|
3020
|
-
|
|
3180
|
+
fs26.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
3021
3181
|
}
|
|
3022
3182
|
function readJsonFile(filePath) {
|
|
3023
|
-
return JSON.parse(
|
|
3183
|
+
return JSON.parse(fs26.readFileSync(filePath, "utf8"));
|
|
3024
3184
|
}
|
|
3025
3185
|
function writeJsonFile(filePath, value) {
|
|
3026
|
-
|
|
3186
|
+
fs26.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
3027
3187
|
}
|
|
3028
3188
|
|
|
3029
3189
|
// src/project/createScaffold.ts
|
|
3030
3190
|
var IGNORED_DIRS = /* @__PURE__ */ new Set(["oh_modules", "node_modules", "build", ".hvigor"]);
|
|
3031
3191
|
async function pathExists(p) {
|
|
3032
3192
|
try {
|
|
3033
|
-
await
|
|
3193
|
+
await fs27.promises.access(p);
|
|
3034
3194
|
return true;
|
|
3035
3195
|
} catch {
|
|
3036
3196
|
return false;
|
|
3037
3197
|
}
|
|
3038
3198
|
}
|
|
3039
3199
|
async function copyDirRecursive(srcDir, destDir) {
|
|
3040
|
-
await
|
|
3041
|
-
const entries = await
|
|
3200
|
+
await fs27.promises.mkdir(destDir, { recursive: true });
|
|
3201
|
+
const entries = await fs27.promises.readdir(srcDir, { withFileTypes: true });
|
|
3042
3202
|
for (const entry of entries) {
|
|
3043
|
-
const src =
|
|
3044
|
-
const dest =
|
|
3203
|
+
const src = path26.join(srcDir, entry.name);
|
|
3204
|
+
const dest = path26.join(destDir, entry.name);
|
|
3045
3205
|
if (entry.isDirectory()) {
|
|
3046
3206
|
await copyDirRecursive(src, dest);
|
|
3047
3207
|
} else if (entry.isSymbolicLink()) {
|
|
3048
3208
|
continue;
|
|
3049
3209
|
} else if (entry.isFile()) {
|
|
3050
3210
|
if (entry.name === "template.json") continue;
|
|
3051
|
-
await
|
|
3211
|
+
await fs27.promises.copyFile(src, dest);
|
|
3052
3212
|
}
|
|
3053
3213
|
}
|
|
3054
3214
|
}
|
|
@@ -3056,9 +3216,9 @@ async function normalizeJson5ToJson(projectDir, logger) {
|
|
|
3056
3216
|
const stack = [projectDir];
|
|
3057
3217
|
while (stack.length > 0) {
|
|
3058
3218
|
const currentDir = stack.pop();
|
|
3059
|
-
const entries = await
|
|
3219
|
+
const entries = await fs27.promises.readdir(currentDir, { withFileTypes: true });
|
|
3060
3220
|
for (const entry of entries) {
|
|
3061
|
-
const fullPath =
|
|
3221
|
+
const fullPath = path26.join(currentDir, entry.name);
|
|
3062
3222
|
if (entry.isDirectory()) {
|
|
3063
3223
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3064
3224
|
stack.push(fullPath);
|
|
@@ -3080,23 +3240,23 @@ function toJavaPropertiesPath(p) {
|
|
|
3080
3240
|
function createOrUpdateLocalProperties(projectDir, sdkDir) {
|
|
3081
3241
|
const content = `sdk.dir=${toJavaPropertiesPath(sdkDir)}
|
|
3082
3242
|
`;
|
|
3083
|
-
|
|
3243
|
+
fs27.writeFileSync(path26.join(projectDir, "local.properties"), content, "utf8");
|
|
3084
3244
|
}
|
|
3085
3245
|
function renameIfExists(fromPath, toPath) {
|
|
3086
|
-
if (
|
|
3087
|
-
|
|
3246
|
+
if (fs27.existsSync(fromPath) && fromPath !== toPath) {
|
|
3247
|
+
fs27.renameSync(fromPath, toPath);
|
|
3088
3248
|
}
|
|
3089
3249
|
}
|
|
3090
3250
|
function updateTemplateConfigs(projectDir, args, sdkBaseDir) {
|
|
3091
|
-
const appJsonPath =
|
|
3092
|
-
if (
|
|
3251
|
+
const appJsonPath = path26.join(projectDir, "AppScope", "app.json5");
|
|
3252
|
+
if (fs27.existsSync(appJsonPath)) {
|
|
3093
3253
|
const appJson = readJson5File(appJsonPath);
|
|
3094
3254
|
appJson.app = appJson.app ?? {};
|
|
3095
3255
|
appJson.app.bundleName = args.bundleName;
|
|
3096
3256
|
writeJson5File(appJsonPath, appJson);
|
|
3097
3257
|
}
|
|
3098
|
-
const stringsPath =
|
|
3099
|
-
if (
|
|
3258
|
+
const stringsPath = path26.join(projectDir, "AppScope", "resources", "base", "element", "string.json");
|
|
3259
|
+
if (fs27.existsSync(stringsPath)) {
|
|
3100
3260
|
const strings = readJsonFile(stringsPath);
|
|
3101
3261
|
if (Array.isArray(strings.string)) {
|
|
3102
3262
|
const appName = strings.string.find((s) => s.name === "app_name");
|
|
@@ -3104,13 +3264,13 @@ function updateTemplateConfigs(projectDir, args, sdkBaseDir) {
|
|
|
3104
3264
|
}
|
|
3105
3265
|
writeJsonFile(stringsPath, strings);
|
|
3106
3266
|
}
|
|
3107
|
-
const buildProfilePath =
|
|
3108
|
-
if (
|
|
3267
|
+
const buildProfilePath = path26.join(projectDir, "build-profile.json5");
|
|
3268
|
+
if (fs27.existsSync(buildProfilePath)) {
|
|
3109
3269
|
const buildProfile = readJson5File(buildProfilePath);
|
|
3110
3270
|
buildProfile.app = buildProfile.app ?? {};
|
|
3111
3271
|
if (Array.isArray(buildProfile.app.products) && buildProfile.app.products.length > 0) {
|
|
3112
|
-
buildProfile.app.products[0].compileSdkVersion = args.
|
|
3113
|
-
buildProfile.app.products[0].compatibleSdkVersion = args.
|
|
3272
|
+
buildProfile.app.products[0].compileSdkVersion = args.sdkVersion;
|
|
3273
|
+
buildProfile.app.products[0].compatibleSdkVersion = args.sdkVersion;
|
|
3114
3274
|
}
|
|
3115
3275
|
if (Array.isArray(buildProfile.modules) && buildProfile.modules.length > 0) {
|
|
3116
3276
|
buildProfile.modules[0].name = args.moduleName;
|
|
@@ -3118,23 +3278,23 @@ function updateTemplateConfigs(projectDir, args, sdkBaseDir) {
|
|
|
3118
3278
|
}
|
|
3119
3279
|
writeJson5File(buildProfilePath, buildProfile);
|
|
3120
3280
|
}
|
|
3121
|
-
const moduleJsonPath =
|
|
3122
|
-
if (
|
|
3281
|
+
const moduleJsonPath = path26.join(projectDir, args.moduleName, "src", "main", "module.json5");
|
|
3282
|
+
if (fs27.existsSync(moduleJsonPath)) {
|
|
3123
3283
|
const moduleJson = readJson5File(moduleJsonPath);
|
|
3124
3284
|
moduleJson.module = moduleJson.module ?? {};
|
|
3125
3285
|
moduleJson.module.name = args.moduleName;
|
|
3126
3286
|
writeJson5File(moduleJsonPath, moduleJson);
|
|
3127
3287
|
}
|
|
3128
|
-
const moduleOhPackagePath =
|
|
3129
|
-
if (
|
|
3288
|
+
const moduleOhPackagePath = path26.join(projectDir, args.moduleName, "oh-package.json5");
|
|
3289
|
+
if (fs27.existsSync(moduleOhPackagePath)) {
|
|
3130
3290
|
const pkg = readJson5File(moduleOhPackagePath);
|
|
3131
3291
|
pkg.name = args.moduleName;
|
|
3132
3292
|
writeJson5File(moduleOhPackagePath, pkg);
|
|
3133
3293
|
}
|
|
3134
|
-
const vscodeDir =
|
|
3135
|
-
|
|
3294
|
+
const vscodeDir = path26.join(projectDir, ".vscode");
|
|
3295
|
+
fs27.mkdirSync(vscodeDir, { recursive: true });
|
|
3136
3296
|
const hapPath = `${args.moduleName}/build/default/outputs/default/${args.moduleName}-default-signed.hap`;
|
|
3137
|
-
writeJsonFile(
|
|
3297
|
+
writeJsonFile(path26.join(vscodeDir, "settings.json"), {
|
|
3138
3298
|
"oniro.hapPath": hapPath,
|
|
3139
3299
|
"files.associations": { "*.json5": "jsonc" }
|
|
3140
3300
|
});
|
|
@@ -3151,7 +3311,7 @@ async function createScaffold(opts) {
|
|
|
3151
3311
|
if (!opts.location || !await pathExists(opts.location)) {
|
|
3152
3312
|
throw new OniroError(`Location does not exist: ${opts.location}`);
|
|
3153
3313
|
}
|
|
3154
|
-
const templateDir =
|
|
3314
|
+
const templateDir = path26.join(opts.templateRoot, opts.templateId);
|
|
3155
3315
|
if (!await pathExists(templateDir)) {
|
|
3156
3316
|
throw new OniroError(`Template not found: ${templateDir}`);
|
|
3157
3317
|
}
|
|
@@ -3164,38 +3324,60 @@ async function createScaffold(opts) {
|
|
|
3164
3324
|
throw new OniroError(`Template '${opts.templateId}' is missing required files:
|
|
3165
3325
|
- ${missing.join("\n- ")}`);
|
|
3166
3326
|
}
|
|
3167
|
-
|
|
3327
|
+
let sdkVersion = opts.sdkApi;
|
|
3328
|
+
let sdkBaseDir = getOhosBaseSdkHome(opts.config);
|
|
3329
|
+
if (detectRuntimeOs(templateDir) === "HarmonyOS") {
|
|
3330
|
+
const harmonySdk = findHarmonyOsSdk({ config: opts.config, logger });
|
|
3331
|
+
const label = harmonySdk?.version?.endsWith(`(${opts.sdkApi})`) ? harmonySdk.version : harmonyOsSdkLabelForApi(opts.sdkApi);
|
|
3332
|
+
if (!label) {
|
|
3333
|
+
throw new OniroError(
|
|
3334
|
+
`No HarmonyOS release is known for API ${opts.sdkApi}. Known: ${[...HARMONYOS_SDK_VERSIONS.keys()].join(", ")}` + (harmonySdk?.version ? `, and the installed SDK's ${harmonySdk.version}.` : ".")
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
sdkVersion = label;
|
|
3338
|
+
if (!harmonySdk) {
|
|
3339
|
+
logger.warn("[create] No HarmonyOS SDK found; set ONIRO_HARMONYOS_SDK_PATH before building.");
|
|
3340
|
+
} else {
|
|
3341
|
+
sdkBaseDir = harmonySdk.sdkPath;
|
|
3342
|
+
if (harmonySdk.version && harmonySdk.version !== label) {
|
|
3343
|
+
logger.warn(
|
|
3344
|
+
`[create] The HarmonyOS SDK at ${harmonySdk.sdkPath} is ${harmonySdk.version}, so a project declaring ${label} will not build against it. Install ${label}, or create with the installed API level.`
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
const projectDir = path26.join(opts.location, opts.projectName);
|
|
3168
3350
|
if (await pathExists(projectDir)) {
|
|
3169
3351
|
if (!opts.overwrite) {
|
|
3170
3352
|
throw new OniroError(`Destination already exists: ${projectDir}. Pass overwrite: true to replace it.`);
|
|
3171
3353
|
}
|
|
3172
|
-
await
|
|
3354
|
+
await fs27.promises.rm(projectDir, { recursive: true, force: true });
|
|
3173
3355
|
}
|
|
3174
3356
|
logger.info(`[create] Scaffolding ${opts.templateId} at ${projectDir}`);
|
|
3175
3357
|
await copyDirRecursive(templateDir, projectDir);
|
|
3176
3358
|
if (moduleName !== defaultModuleName) {
|
|
3177
|
-
renameIfExists(
|
|
3359
|
+
renameIfExists(path26.join(projectDir, defaultModuleName), path26.join(projectDir, moduleName));
|
|
3178
3360
|
}
|
|
3179
3361
|
updateTemplateConfigs(
|
|
3180
3362
|
projectDir,
|
|
3181
3363
|
{
|
|
3182
3364
|
projectName: opts.projectName,
|
|
3183
3365
|
bundleName: opts.bundleName,
|
|
3184
|
-
|
|
3366
|
+
sdkVersion,
|
|
3185
3367
|
moduleName
|
|
3186
3368
|
},
|
|
3187
|
-
|
|
3369
|
+
sdkBaseDir
|
|
3188
3370
|
);
|
|
3189
3371
|
await normalizeJson5ToJson(projectDir, logger);
|
|
3190
3372
|
return { projectDir };
|
|
3191
3373
|
}
|
|
3192
3374
|
|
|
3193
3375
|
// src/project/listModules.ts
|
|
3194
|
-
import * as
|
|
3195
|
-
import * as
|
|
3376
|
+
import * as fs28 from "fs";
|
|
3377
|
+
import * as path27 from "path";
|
|
3196
3378
|
function listModules(opts) {
|
|
3197
|
-
const buildProfile =
|
|
3198
|
-
if (!
|
|
3379
|
+
const buildProfile = path27.join(opts.projectDir, "build-profile.json5");
|
|
3380
|
+
if (!fs28.existsSync(buildProfile)) {
|
|
3199
3381
|
throw new OniroError(`build-profile.json5 not found at ${buildProfile}`);
|
|
3200
3382
|
}
|
|
3201
3383
|
const parsed = readJson5File(buildProfile);
|
|
@@ -3207,12 +3389,12 @@ function listModules(opts) {
|
|
|
3207
3389
|
}
|
|
3208
3390
|
|
|
3209
3391
|
// src/project/abilities.ts
|
|
3210
|
-
import * as
|
|
3211
|
-
import * as
|
|
3392
|
+
import * as fs29 from "fs";
|
|
3393
|
+
import * as path28 from "path";
|
|
3212
3394
|
function listAbilities(opts) {
|
|
3213
3395
|
const moduleName = opts.moduleName ?? "entry";
|
|
3214
|
-
const moduleJsonPath =
|
|
3215
|
-
if (!
|
|
3396
|
+
const moduleJsonPath = path28.join(opts.projectDir, moduleName, "src", "main", "module.json5");
|
|
3397
|
+
if (!fs29.existsSync(moduleJsonPath)) {
|
|
3216
3398
|
throw new OniroError(`Could not find module.json5 at ${moduleJsonPath}`);
|
|
3217
3399
|
}
|
|
3218
3400
|
const parsed = readJson5File(moduleJsonPath);
|
|
@@ -3220,12 +3402,12 @@ function listAbilities(opts) {
|
|
|
3220
3402
|
}
|
|
3221
3403
|
|
|
3222
3404
|
// src/project/prepareSigning.ts
|
|
3223
|
-
import * as
|
|
3224
|
-
import * as
|
|
3405
|
+
import * as fs30 from "fs";
|
|
3406
|
+
import * as path29 from "path";
|
|
3225
3407
|
function hasSigningArtifacts(dir) {
|
|
3226
|
-
if (!
|
|
3227
|
-
const buildProfile =
|
|
3228
|
-
if (!
|
|
3408
|
+
if (!fs30.existsSync(path29.join(dir, "signatures"))) return false;
|
|
3409
|
+
const buildProfile = path29.join(dir, "build-profile.json5");
|
|
3410
|
+
if (!fs30.existsSync(buildProfile)) return false;
|
|
3229
3411
|
try {
|
|
3230
3412
|
const parsed = readJson5File(buildProfile);
|
|
3231
3413
|
return Array.isArray(parsed.app?.signingConfigs) && parsed.app.signingConfigs.length > 0;
|
|
@@ -3315,8 +3497,8 @@ async function diffHapAssets(opts) {
|
|
|
3315
3497
|
}
|
|
3316
3498
|
|
|
3317
3499
|
// src/install/apply.ts
|
|
3318
|
-
import * as
|
|
3319
|
-
import * as
|
|
3500
|
+
import * as fs31 from "fs";
|
|
3501
|
+
import * as path30 from "path";
|
|
3320
3502
|
function decideInstallMethod(s) {
|
|
3321
3503
|
if (!s.signInfoInconsistent) return "replace";
|
|
3322
3504
|
if (s.isSystemBundle && !s.allowUninstall) return "refuse";
|
|
@@ -3337,7 +3519,7 @@ async function pidOf(config, bundle, deviceSerial) {
|
|
|
3337
3519
|
async function resolveHapPath(opts) {
|
|
3338
3520
|
let hapPath = opts.hapPath;
|
|
3339
3521
|
if (hapPath) {
|
|
3340
|
-
if (!
|
|
3522
|
+
if (!path30.isAbsolute(hapPath) && opts.projectDir) hapPath = path30.join(opts.projectDir, hapPath);
|
|
3341
3523
|
} else {
|
|
3342
3524
|
if (!opts.projectDir) throw new OniroError("applyChanges requires hapPath or projectDir.");
|
|
3343
3525
|
const haps = await discoverHaps({ projectDir: opts.projectDir });
|
|
@@ -3356,7 +3538,7 @@ async function resolveHapPath(opts) {
|
|
|
3356
3538
|
hapPath = withSigned[0][1].signed[0];
|
|
3357
3539
|
}
|
|
3358
3540
|
}
|
|
3359
|
-
if (!
|
|
3541
|
+
if (!fs31.existsSync(hapPath)) throw new OniroError(`HAP not found at ${hapPath}.`);
|
|
3360
3542
|
return hapPath;
|
|
3361
3543
|
}
|
|
3362
3544
|
async function applyChanges(opts) {
|
|
@@ -3366,7 +3548,7 @@ async function applyChanges(opts) {
|
|
|
3366
3548
|
const allowUninstall = opts.allowUninstall ?? false;
|
|
3367
3549
|
const hapPath = await resolveHapPath(opts);
|
|
3368
3550
|
let assetsChanged = false;
|
|
3369
|
-
if (opts.installedHapPath &&
|
|
3551
|
+
if (opts.installedHapPath && fs31.existsSync(opts.installedHapPath)) {
|
|
3370
3552
|
try {
|
|
3371
3553
|
const diff = await diffHapAssets({ installedHap: opts.installedHapPath, newHap: hapPath });
|
|
3372
3554
|
assetsChanged = diff.addedAssetPaths.length > 0 || diff.removedAssetPaths.length > 0;
|
|
@@ -3407,20 +3589,1201 @@ async function applyChanges(opts) {
|
|
|
3407
3589
|
return { method, preInstallPid, postInstallPid, replaced: installMethod === "replace", cacheCleared };
|
|
3408
3590
|
}
|
|
3409
3591
|
|
|
3592
|
+
// src/harmonyos/auth/endpoints.ts
|
|
3593
|
+
var HUAWEI_APP_ID = "1009";
|
|
3594
|
+
var HUAWEI_API_VERSION = "1.0.0";
|
|
3595
|
+
var LOGIN_TIMEOUT_MS = 6e5;
|
|
3596
|
+
var HUAWEI_SITES = {
|
|
3597
|
+
CN: {
|
|
3598
|
+
code: "CN",
|
|
3599
|
+
siteId: "1",
|
|
3600
|
+
siteParam: "CN",
|
|
3601
|
+
loginUrl: "https://cn.devecostudio.huawei.com",
|
|
3602
|
+
agcBaseUrl: "https://connect-api.cloud.huawei.com",
|
|
3603
|
+
language: "zh_CN"
|
|
3604
|
+
},
|
|
3605
|
+
SG: {
|
|
3606
|
+
code: "SG",
|
|
3607
|
+
siteId: "5",
|
|
3608
|
+
siteParam: "SG",
|
|
3609
|
+
loginUrl: "https://sg.devecostudio.huawei.com",
|
|
3610
|
+
agcBaseUrl: "https://connect-api-dra.cloud.huawei.com",
|
|
3611
|
+
language: "zh_CN"
|
|
3612
|
+
},
|
|
3613
|
+
EU: {
|
|
3614
|
+
code: "EU",
|
|
3615
|
+
siteId: "7",
|
|
3616
|
+
siteParam: "DE",
|
|
3617
|
+
loginUrl: "https://de.devecostudio.huawei.com",
|
|
3618
|
+
agcBaseUrl: "https://connect-api-dre.cloud.huawei.com",
|
|
3619
|
+
language: "de_DE"
|
|
3620
|
+
},
|
|
3621
|
+
RU: {
|
|
3622
|
+
code: "RU",
|
|
3623
|
+
siteId: "8",
|
|
3624
|
+
siteParam: "RU",
|
|
3625
|
+
loginUrl: "https://ru.devecostudio.huawei.com",
|
|
3626
|
+
agcBaseUrl: "https://connect-api-drru.cloud.huawei.com",
|
|
3627
|
+
language: "ru_RU"
|
|
3628
|
+
}
|
|
3629
|
+
};
|
|
3630
|
+
var AGC_TEAMS_URL = "https://connect-api.cloud.huawei.com/api/ups/user-permission-service/v1/user-team-list";
|
|
3631
|
+
function siteForId(siteId) {
|
|
3632
|
+
return Object.values(HUAWEI_SITES).find((s) => s.siteId === siteId) ?? null;
|
|
3633
|
+
}
|
|
3634
|
+
var AUTH_PATHS = {
|
|
3635
|
+
/** The browser authenticates here, then redirects to the loopback server. */
|
|
3636
|
+
AUTH_APPLY: "console/DevEcoIDE/apply",
|
|
3637
|
+
/** Exchanges the browser's temp token for a JWT. */
|
|
3638
|
+
TEMP_TOKEN_CHECK: "authrouter/auth/api/temptoken/check",
|
|
3639
|
+
/** Validates a JWT and, with `refresh`, mints an AGC access token. */
|
|
3640
|
+
JWT_TOKEN_CHECK: "authrouter/auth/api/jwToken/check",
|
|
3641
|
+
LOGIN_SUCCESS: "console/DevEcoCLI/loginSuccess",
|
|
3642
|
+
LOGIN_FAILED: "console/DevEcoCLI/loginFailed",
|
|
3643
|
+
LOGOUT: "authrouter/auth/api/logout",
|
|
3644
|
+
/**
|
|
3645
|
+
* Whether an account without real-name verification accepted the developer
|
|
3646
|
+
* agreement, which is what lets it sign. From DevEco Studio (`space.agr.url`).
|
|
3647
|
+
*/
|
|
3648
|
+
UNREALNAME_AGREEMENT: "authrouter/unrealname/agreement"
|
|
3649
|
+
};
|
|
3650
|
+
|
|
3651
|
+
// src/harmonyos/auth/tokenStore.ts
|
|
3652
|
+
import * as fs32 from "fs";
|
|
3653
|
+
import * as os11 from "os";
|
|
3654
|
+
import * as path31 from "path";
|
|
3655
|
+
import * as crypto3 from "crypto";
|
|
3656
|
+
var ALGORITHM = "aes-256-gcm";
|
|
3657
|
+
var KEY_LENGTH = 32;
|
|
3658
|
+
var IV_LENGTH = 12;
|
|
3659
|
+
var TOKEN_FILE = "token.enc";
|
|
3660
|
+
var KEY_FILE = "kek.bin";
|
|
3661
|
+
var DIR_MODE = 448;
|
|
3662
|
+
var FILE_MODE = 384;
|
|
3663
|
+
function encrypt2(key, plaintext) {
|
|
3664
|
+
const iv = crypto3.randomBytes(IV_LENGTH);
|
|
3665
|
+
const cipher = crypto3.createCipheriv(ALGORITHM, key, iv);
|
|
3666
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
3667
|
+
return {
|
|
3668
|
+
version: 1,
|
|
3669
|
+
algorithm: ALGORITHM,
|
|
3670
|
+
ciphertext: ciphertext.toString("base64"),
|
|
3671
|
+
iv: iv.toString("base64"),
|
|
3672
|
+
authTag: cipher.getAuthTag().toString("base64")
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3675
|
+
function decrypt2(key, blob) {
|
|
3676
|
+
const decipher = crypto3.createDecipheriv(ALGORITHM, key, Buffer.from(blob.iv, "base64"));
|
|
3677
|
+
decipher.setAuthTag(Buffer.from(blob.authTag, "base64"));
|
|
3678
|
+
return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "base64")), decipher.final()]);
|
|
3679
|
+
}
|
|
3680
|
+
function isEncryptedBlob(value) {
|
|
3681
|
+
if (!value || typeof value !== "object") return false;
|
|
3682
|
+
const b = value;
|
|
3683
|
+
return b.algorithm === ALGORITHM && typeof b.ciphertext === "string" && typeof b.iv === "string" && typeof b.authTag === "string";
|
|
3684
|
+
}
|
|
3685
|
+
function permissionHint(dir) {
|
|
3686
|
+
return process.platform === "win32" ? `Permission denied writing ${dir}. Grant write permission to that directory.` : `Permission denied writing ${dir}. Try: chown -R "$(whoami)" ${dir}`;
|
|
3687
|
+
}
|
|
3688
|
+
function ensureDir(dir) {
|
|
3689
|
+
try {
|
|
3690
|
+
fs32.mkdirSync(dir, { recursive: true, mode: DIR_MODE });
|
|
3691
|
+
} catch (err) {
|
|
3692
|
+
if (err.code === "EACCES") {
|
|
3693
|
+
throw new OniroError(permissionHint(dir), err);
|
|
3694
|
+
}
|
|
3695
|
+
throw err;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
function createTokenStore(opts) {
|
|
3699
|
+
const logger = opts.logger ?? noopLogger;
|
|
3700
|
+
const authDir = opts.config.get("harmonyosAuthDir", defaultPaths.harmonyosAuthDir());
|
|
3701
|
+
const tokenPath = path31.join(authDir, TOKEN_FILE);
|
|
3702
|
+
const keyPath = path31.join(os11.homedir(), ".local", "share", "oniro-app", "keys", KEY_FILE);
|
|
3703
|
+
function loadOrCreateKey() {
|
|
3704
|
+
if (fs32.existsSync(keyPath)) {
|
|
3705
|
+
const existing = fs32.readFileSync(keyPath);
|
|
3706
|
+
if (existing.length === KEY_LENGTH) return existing;
|
|
3707
|
+
logger.warn("[harmonyos] The token key file was malformed; regenerating (you will need to log in again).");
|
|
3708
|
+
}
|
|
3709
|
+
ensureDir(path31.dirname(keyPath));
|
|
3710
|
+
const key = crypto3.randomBytes(KEY_LENGTH);
|
|
3711
|
+
fs32.writeFileSync(keyPath, key, { mode: FILE_MODE });
|
|
3712
|
+
return key;
|
|
3713
|
+
}
|
|
3714
|
+
return {
|
|
3715
|
+
tokenPath,
|
|
3716
|
+
save(session) {
|
|
3717
|
+
if (!session.jwtToken) throw new OniroError("Refusing to store an empty token.");
|
|
3718
|
+
const payload = Buffer.from(JSON.stringify(session), "utf8");
|
|
3719
|
+
const blob = encrypt2(loadOrCreateKey(), payload);
|
|
3720
|
+
ensureDir(authDir);
|
|
3721
|
+
fs32.writeFileSync(tokenPath, JSON.stringify(blob, null, 2), { mode: FILE_MODE });
|
|
3722
|
+
},
|
|
3723
|
+
load() {
|
|
3724
|
+
if (!fs32.existsSync(tokenPath)) return null;
|
|
3725
|
+
try {
|
|
3726
|
+
const blob = JSON.parse(fs32.readFileSync(tokenPath, "utf8"));
|
|
3727
|
+
if (!isEncryptedBlob(blob)) return null;
|
|
3728
|
+
const { jwtToken, siteId } = JSON.parse(decrypt2(loadOrCreateKey(), blob).toString("utf8"));
|
|
3729
|
+
return typeof jwtToken === "string" && jwtToken && typeof siteId === "string" ? { jwtToken, siteId } : null;
|
|
3730
|
+
} catch (err) {
|
|
3731
|
+
const code = err.code;
|
|
3732
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
3733
|
+
logger.warn(`[harmonyos] Cannot read ${tokenPath}: ${permissionHint(authDir)}`);
|
|
3734
|
+
return null;
|
|
3735
|
+
}
|
|
3736
|
+
logger.debug(`[harmonyos] Discarding unreadable token at ${tokenPath}.`);
|
|
3737
|
+
try {
|
|
3738
|
+
fs32.rmSync(tokenPath, { force: true });
|
|
3739
|
+
} catch {
|
|
3740
|
+
}
|
|
3741
|
+
return null;
|
|
3742
|
+
}
|
|
3743
|
+
},
|
|
3744
|
+
clear() {
|
|
3745
|
+
try {
|
|
3746
|
+
fs32.rmSync(tokenPath, { force: true });
|
|
3747
|
+
} catch (err) {
|
|
3748
|
+
throw new OniroError(`Failed to remove ${tokenPath}.`, err);
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
};
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
// src/harmonyos/auth/session.ts
|
|
3755
|
+
import * as crypto5 from "crypto";
|
|
3756
|
+
|
|
3757
|
+
// src/harmonyos/http.ts
|
|
3758
|
+
import { EnvHttpProxyAgent, fetch } from "undici";
|
|
3759
|
+
var DEFAULT_HEADERS = {
|
|
3760
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
3761
|
+
"accept-language": "zh-CN",
|
|
3762
|
+
accept: "application/json, text/plain, */*"
|
|
3763
|
+
};
|
|
3764
|
+
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
3765
|
+
var SENSITIVE_PARAMS = /* @__PURE__ */ new Set(["temptoken", "jwttoken", "code", "oauth2token", "accesstoken"]);
|
|
3766
|
+
function redactUrl(url) {
|
|
3767
|
+
let parsed;
|
|
3768
|
+
try {
|
|
3769
|
+
parsed = new URL(url);
|
|
3770
|
+
} catch {
|
|
3771
|
+
return url;
|
|
3772
|
+
}
|
|
3773
|
+
for (const key of [...parsed.searchParams.keys()]) {
|
|
3774
|
+
if (SENSITIVE_PARAMS.has(key.toLowerCase())) parsed.searchParams.set(key, "***");
|
|
3775
|
+
}
|
|
3776
|
+
return parsed.toString();
|
|
3777
|
+
}
|
|
3778
|
+
function previewBody(body, limit = 300) {
|
|
3779
|
+
const trimmed = body.trim();
|
|
3780
|
+
if (!trimmed) return "(empty body)";
|
|
3781
|
+
if (/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(trimmed)) return "(a token-shaped value)";
|
|
3782
|
+
const collapsed = trimmed.replace(/\s+/g, " ");
|
|
3783
|
+
return collapsed.length > limit ? `${collapsed.slice(0, limit)}\u2026` : collapsed;
|
|
3784
|
+
}
|
|
3785
|
+
function describeResponse(response) {
|
|
3786
|
+
const status = `HTTP ${response.statusCode}${response.statusText ? ` ${response.statusText}` : ""}`;
|
|
3787
|
+
return `${status}: ${previewBody(response.data)}`;
|
|
3788
|
+
}
|
|
3789
|
+
function buildUrl(url, query) {
|
|
3790
|
+
if (!query) return url;
|
|
3791
|
+
const parsed = new URL(url);
|
|
3792
|
+
for (const [key, value] of Object.entries(query)) {
|
|
3793
|
+
if (value !== void 0 && value !== null) parsed.searchParams.set(key, String(value));
|
|
3794
|
+
}
|
|
3795
|
+
return parsed.toString();
|
|
3796
|
+
}
|
|
3797
|
+
function createHttpClient(opts = {}) {
|
|
3798
|
+
const logger = opts.logger ?? noopLogger;
|
|
3799
|
+
const dispatcher = new EnvHttpProxyAgent();
|
|
3800
|
+
async function send(method, url, options) {
|
|
3801
|
+
const target = buildUrl(url, options.query);
|
|
3802
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
3803
|
+
const headers = { ...DEFAULT_HEADERS, ...options.headers };
|
|
3804
|
+
let body;
|
|
3805
|
+
if (options.body !== void 0) {
|
|
3806
|
+
body = JSON.stringify(options.body);
|
|
3807
|
+
headers["content-type"] = "application/json";
|
|
3808
|
+
}
|
|
3809
|
+
logger.debug(`[harmonyos] ${method} ${redactUrl(target)}`);
|
|
3810
|
+
try {
|
|
3811
|
+
return await fetch(target, {
|
|
3812
|
+
method,
|
|
3813
|
+
headers,
|
|
3814
|
+
body,
|
|
3815
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
3816
|
+
dispatcher
|
|
3817
|
+
});
|
|
3818
|
+
} catch (err) {
|
|
3819
|
+
const reason = err instanceof Error && err.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : err instanceof Error ? err.message : String(err);
|
|
3820
|
+
throw new OniroError(
|
|
3821
|
+
`${method} ${redactUrl(target)} failed: ${reason}. Check the network connection and any proxy configuration (HTTPS_PROXY / HTTP_PROXY).`,
|
|
3822
|
+
err
|
|
3823
|
+
);
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
async function request(method, url, options = {}) {
|
|
3827
|
+
const response = await send(method, url, options);
|
|
3828
|
+
return { data: await response.text(), statusCode: response.status, statusText: response.statusText };
|
|
3829
|
+
}
|
|
3830
|
+
return {
|
|
3831
|
+
get: (url, o) => request("GET", url, o),
|
|
3832
|
+
post: (url, o) => request("POST", url, o),
|
|
3833
|
+
delete: (url, o) => request("DELETE", url, o),
|
|
3834
|
+
async getBinary(url, o = {}) {
|
|
3835
|
+
const response = await send("GET", url, o);
|
|
3836
|
+
if (!response.ok) {
|
|
3837
|
+
throw new OniroError(`Download of ${redactUrl(response.url || url)} failed (HTTP ${response.status}).`);
|
|
3838
|
+
}
|
|
3839
|
+
return Buffer.from(await response.arrayBuffer());
|
|
3840
|
+
}
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
// src/harmonyos/auth/callbackServer.ts
|
|
3845
|
+
import * as http2 from "http";
|
|
3846
|
+
import * as crypto4 from "crypto";
|
|
3847
|
+
var CALLBACK_PATH = "/callback";
|
|
3848
|
+
var MAX_BODY_BYTES = 65536;
|
|
3849
|
+
function codeMatches(code, secret) {
|
|
3850
|
+
const a = Buffer.from(code ?? "", "utf8");
|
|
3851
|
+
const b = Buffer.from(secret, "utf8");
|
|
3852
|
+
return a.length === b.length && crypto4.timingSafeEqual(a, b);
|
|
3853
|
+
}
|
|
3854
|
+
function isQuit(quit) {
|
|
3855
|
+
return quit === "true" || quit === "access_denied" || quit === "quit";
|
|
3856
|
+
}
|
|
3857
|
+
async function startCallbackServer(opts) {
|
|
3858
|
+
let resolveCallback;
|
|
3859
|
+
let rejectCallback;
|
|
3860
|
+
const callback = new Promise((resolve6, reject) => {
|
|
3861
|
+
resolveCallback = resolve6;
|
|
3862
|
+
rejectCallback = reject;
|
|
3863
|
+
});
|
|
3864
|
+
callback.catch(() => {
|
|
3865
|
+
});
|
|
3866
|
+
const server = http2.createServer((req, res) => {
|
|
3867
|
+
const host = (req.headers.host ?? "").toLowerCase();
|
|
3868
|
+
const port2 = server.address()?.port;
|
|
3869
|
+
if (host !== `127.0.0.1:${port2}` && host !== `localhost:${port2}`) {
|
|
3870
|
+
res.writeHead(400);
|
|
3871
|
+
res.end("Bad Host");
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
const url = new URL(req.url ?? "", `http://${host}`);
|
|
3875
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
3876
|
+
res.writeHead(404);
|
|
3877
|
+
res.end("Not Found");
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
const finish = (params) => {
|
|
3881
|
+
if (!codeMatches(params.get("code"), opts.clientSecret)) {
|
|
3882
|
+
res.writeHead(400);
|
|
3883
|
+
res.end("Bad Request");
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3886
|
+
if (isQuit(params.get("quit"))) {
|
|
3887
|
+
res.writeHead(302, { Location: `${opts.baseUrl}/${opts.failedPath}` });
|
|
3888
|
+
res.end();
|
|
3889
|
+
rejectCallback(new CancelledError("Login was cancelled in the browser."));
|
|
3890
|
+
return;
|
|
3891
|
+
}
|
|
3892
|
+
const tempToken = params.get("tempToken");
|
|
3893
|
+
const siteId = params.get("siteId");
|
|
3894
|
+
if (!tempToken || !siteId) {
|
|
3895
|
+
res.writeHead(400);
|
|
3896
|
+
res.end("Bad Request");
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
res.writeHead(302, { Location: `${opts.baseUrl}/${opts.successPath}` });
|
|
3900
|
+
res.end();
|
|
3901
|
+
resolveCallback({ tempToken, siteId });
|
|
3902
|
+
};
|
|
3903
|
+
if (req.method === "POST") {
|
|
3904
|
+
let body = "";
|
|
3905
|
+
let size = 0;
|
|
3906
|
+
req.on("data", (chunk) => {
|
|
3907
|
+
size += chunk.length;
|
|
3908
|
+
if (size > MAX_BODY_BYTES) {
|
|
3909
|
+
req.destroy(new Error("Request body too large"));
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3912
|
+
body += chunk.toString();
|
|
3913
|
+
});
|
|
3914
|
+
req.on("end", () => finish(body.trim() ? new URLSearchParams(body) : url.searchParams));
|
|
3915
|
+
} else {
|
|
3916
|
+
finish(url.searchParams);
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
server.keepAliveTimeout = 1;
|
|
3920
|
+
await new Promise((resolve6, reject) => {
|
|
3921
|
+
server.once("error", (err) => reject(new OniroError("Failed to start the local login callback server.", err)));
|
|
3922
|
+
server.listen(0, "127.0.0.1", resolve6);
|
|
3923
|
+
});
|
|
3924
|
+
const port = server.address().port;
|
|
3925
|
+
return {
|
|
3926
|
+
port,
|
|
3927
|
+
waitForCallback(timeoutMs) {
|
|
3928
|
+
let timer;
|
|
3929
|
+
const timeout = new Promise((_, reject) => {
|
|
3930
|
+
timer = setTimeout(
|
|
3931
|
+
() => reject(new OniroError(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the browser login to complete.`)),
|
|
3932
|
+
timeoutMs
|
|
3933
|
+
);
|
|
3934
|
+
});
|
|
3935
|
+
return Promise.race([callback, timeout]).finally(() => clearTimeout(timer));
|
|
3936
|
+
},
|
|
3937
|
+
stop() {
|
|
3938
|
+
return new Promise((resolve6) => {
|
|
3939
|
+
server.closeAllConnections?.();
|
|
3940
|
+
const timer = setTimeout(resolve6, 100);
|
|
3941
|
+
server.close(() => {
|
|
3942
|
+
clearTimeout(timer);
|
|
3943
|
+
resolve6();
|
|
3944
|
+
});
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
// src/harmonyos/auth/browser.ts
|
|
3951
|
+
import { spawn as spawn3 } from "child_process";
|
|
3952
|
+
function isSafeUrl(url) {
|
|
3953
|
+
try {
|
|
3954
|
+
const parsed = new URL(url);
|
|
3955
|
+
return ["http:", "https:"].includes(parsed.protocol) && parsed.hostname !== "" && !url.includes('"');
|
|
3956
|
+
} catch {
|
|
3957
|
+
return false;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
function escapeForCmd(s) {
|
|
3961
|
+
return s.replace(/[&|<>()^%!]/g, (c) => `^${c}`);
|
|
3962
|
+
}
|
|
3963
|
+
function openBrowser(url) {
|
|
3964
|
+
if (!isSafeUrl(url)) {
|
|
3965
|
+
return Promise.reject(new OniroError(`Refusing to open unsafe URL: ${JSON.stringify(url)}`));
|
|
3966
|
+
}
|
|
3967
|
+
const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", '""', escapeForCmd(url)]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
3968
|
+
const child = spawn3(command, args, {
|
|
3969
|
+
stdio: "ignore",
|
|
3970
|
+
shell: false,
|
|
3971
|
+
detached: true,
|
|
3972
|
+
windowsHide: true
|
|
3973
|
+
});
|
|
3974
|
+
child.unref();
|
|
3975
|
+
return new Promise((resolve6, reject) => {
|
|
3976
|
+
child.on("error", (err) => reject(new OniroError(`Failed to open a browser (${command}).`, err)));
|
|
3977
|
+
child.on(
|
|
3978
|
+
"exit",
|
|
3979
|
+
(code) => code === 0 ? resolve6() : reject(new OniroError(`Browser launcher '${command}' exited with code ${code}.`))
|
|
3980
|
+
);
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
// src/harmonyos/auth/session.ts
|
|
3985
|
+
var NOT_SIGNED_IN = "Not signed in. Run `oniro-app auth login` first.";
|
|
3986
|
+
function createHarmonyOsSession(opts) {
|
|
3987
|
+
const logger = opts.logger ?? noopLogger;
|
|
3988
|
+
const http3 = opts.http ?? createHttpClient({ logger });
|
|
3989
|
+
const store = opts.tokenStore ?? createTokenStore({ config: opts.config, logger });
|
|
3990
|
+
async function exchangeTempToken(site, tempToken) {
|
|
3991
|
+
const response = await http3.get(`${site.loginUrl}/${AUTH_PATHS.TEMP_TOKEN_CHECK}`, {
|
|
3992
|
+
query: {
|
|
3993
|
+
// The page sometimes appends extra `&`-joined parameters to the token.
|
|
3994
|
+
tempToken: tempToken.split("&")[0],
|
|
3995
|
+
site: site.siteParam,
|
|
3996
|
+
version: HUAWEI_API_VERSION,
|
|
3997
|
+
appid: HUAWEI_APP_ID
|
|
3998
|
+
}
|
|
3999
|
+
});
|
|
4000
|
+
const jwtToken = response.data.trim();
|
|
4001
|
+
if (response.statusCode === 200 && /^[^.]+\.[^.]+\.[^.]+$/.test(jwtToken)) return jwtToken;
|
|
4002
|
+
throw new OniroError(
|
|
4003
|
+
`${site.loginUrl} would not exchange the login token for a session (${describeResponse(response)}). Login tokens are single-use and short-lived; run \`oniro-app auth login\` again.`
|
|
4004
|
+
);
|
|
4005
|
+
}
|
|
4006
|
+
async function userFromJwt(site, jwtToken, refresh) {
|
|
4007
|
+
const response = await http3.get(`${site.loginUrl}/${AUTH_PATHS.JWT_TOKEN_CHECK}`, {
|
|
4008
|
+
headers: { refresh: String(refresh), jwtToken }
|
|
4009
|
+
});
|
|
4010
|
+
let info;
|
|
4011
|
+
try {
|
|
4012
|
+
if (response.statusCode !== 200) throw new Error();
|
|
4013
|
+
info = JSON.parse(response.data);
|
|
4014
|
+
} catch {
|
|
4015
|
+
throw new OniroError(`Could not validate the session with ${site.loginUrl} (${describeResponse(response)}).`);
|
|
4016
|
+
}
|
|
4017
|
+
const user = info.userInfo;
|
|
4018
|
+
if (!info.status || !user?.accessToken) return null;
|
|
4019
|
+
return {
|
|
4020
|
+
userId: user.userId ?? "",
|
|
4021
|
+
userName: user.name ?? "",
|
|
4022
|
+
accessToken: user.accessToken,
|
|
4023
|
+
countryCode: user.nationalCode ?? "",
|
|
4024
|
+
siteCode: site.code,
|
|
4025
|
+
isRealName: String(user.realName) === "true"
|
|
4026
|
+
};
|
|
4027
|
+
}
|
|
4028
|
+
async function getUserInfo(o = {}) {
|
|
4029
|
+
const stored = store.load();
|
|
4030
|
+
const site = stored && siteForId(stored.siteId);
|
|
4031
|
+
const user = site ? await userFromJwt(site, stored.jwtToken, o.refresh ?? false) : null;
|
|
4032
|
+
if (stored && !user) {
|
|
4033
|
+
logger.debug("[harmonyos] The stored session is no longer valid; clearing it.");
|
|
4034
|
+
store.clear();
|
|
4035
|
+
}
|
|
4036
|
+
return user;
|
|
4037
|
+
}
|
|
4038
|
+
async function requireUser() {
|
|
4039
|
+
const user = await getUserInfo({ refresh: true });
|
|
4040
|
+
if (!user) throw new OniroError(NOT_SIGNED_IN);
|
|
4041
|
+
return user;
|
|
4042
|
+
}
|
|
4043
|
+
async function getDeveloperAgreement(user) {
|
|
4044
|
+
try {
|
|
4045
|
+
const response = await http3.get(`${HUAWEI_SITES[user.siteCode].loginUrl}/${AUTH_PATHS.UNREALNAME_AGREEMENT}`, {
|
|
4046
|
+
headers: { accessToken: user.accessToken }
|
|
4047
|
+
});
|
|
4048
|
+
const payload = JSON.parse(response.data);
|
|
4049
|
+
if (response.statusCode !== 200 || String(payload.success) !== "true" || !payload.body) return null;
|
|
4050
|
+
return { signed: payload.body.signedFlag === true, latest: payload.body.signLastestFlag === true };
|
|
4051
|
+
} catch {
|
|
4052
|
+
return null;
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
return {
|
|
4056
|
+
tokenPath: store.tokenPath,
|
|
4057
|
+
getUserInfo,
|
|
4058
|
+
getDeveloperAgreement,
|
|
4059
|
+
async login(o = {}) {
|
|
4060
|
+
const start = HUAWEI_SITES.CN.loginUrl;
|
|
4061
|
+
const clientSecret = crypto5.randomUUID().replace(/-/g, "");
|
|
4062
|
+
const server = await startCallbackServer({
|
|
4063
|
+
clientSecret,
|
|
4064
|
+
baseUrl: start,
|
|
4065
|
+
successPath: AUTH_PATHS.LOGIN_SUCCESS,
|
|
4066
|
+
failedPath: AUTH_PATHS.LOGIN_FAILED
|
|
4067
|
+
});
|
|
4068
|
+
try {
|
|
4069
|
+
const loginUrl = `${start}/${AUTH_PATHS.AUTH_APPLY}?port=${server.port}&appid=${HUAWEI_APP_ID}&code=${clientSecret}`;
|
|
4070
|
+
opts.onLoginUrl?.(loginUrl);
|
|
4071
|
+
openBrowser(loginUrl).catch((err) => {
|
|
4072
|
+
logger.warn(`[harmonyos] Could not open a browser automatically: ${err.message}`);
|
|
4073
|
+
});
|
|
4074
|
+
const callback = await server.waitForCallback(o.timeoutMs ?? LOGIN_TIMEOUT_MS);
|
|
4075
|
+
const site = siteForId(callback.siteId);
|
|
4076
|
+
if (!site) {
|
|
4077
|
+
throw new OniroError(`The account belongs to a Huawei region oniro-app does not know (siteId ${callback.siteId}).`);
|
|
4078
|
+
}
|
|
4079
|
+
const jwtToken = await exchangeTempToken(site, callback.tempToken);
|
|
4080
|
+
const user = await userFromJwt(site, jwtToken, false);
|
|
4081
|
+
if (!user) throw new OniroError("Login did not yield a usable session. Try again.");
|
|
4082
|
+
store.save({ jwtToken, siteId: site.siteId });
|
|
4083
|
+
logger.info(`[harmonyos] Signed in as ${user.userName || user.userId} (${site.code}).`);
|
|
4084
|
+
return user;
|
|
4085
|
+
} finally {
|
|
4086
|
+
await server.stop();
|
|
4087
|
+
}
|
|
4088
|
+
},
|
|
4089
|
+
async logout() {
|
|
4090
|
+
const stored = store.load();
|
|
4091
|
+
if (!stored) return false;
|
|
4092
|
+
const site = siteForId(stored.siteId);
|
|
4093
|
+
try {
|
|
4094
|
+
if (site) {
|
|
4095
|
+
await http3.post(`${site.loginUrl}/${AUTH_PATHS.LOGOUT}`, {
|
|
4096
|
+
query: { jwtToken: stored.jwtToken },
|
|
4097
|
+
timeoutMs: 5e3
|
|
4098
|
+
});
|
|
4099
|
+
}
|
|
4100
|
+
} catch {
|
|
4101
|
+
} finally {
|
|
4102
|
+
store.clear();
|
|
4103
|
+
}
|
|
4104
|
+
return true;
|
|
4105
|
+
},
|
|
4106
|
+
async listTeams() {
|
|
4107
|
+
const user = await requireUser();
|
|
4108
|
+
const response = await http3.get(AGC_TEAMS_URL, {
|
|
4109
|
+
headers: {
|
|
4110
|
+
oauth2Token: user.accessToken,
|
|
4111
|
+
uid: user.userId,
|
|
4112
|
+
source: "cli",
|
|
4113
|
+
lang: HUAWEI_SITES[user.siteCode].language
|
|
4114
|
+
},
|
|
4115
|
+
timeoutMs: 15e3
|
|
4116
|
+
});
|
|
4117
|
+
if (response.statusCode === 401) {
|
|
4118
|
+
throw new OniroError("The session has expired. Run `oniro-app auth login` again.");
|
|
4119
|
+
}
|
|
4120
|
+
if (response.statusCode !== 200) {
|
|
4121
|
+
throw new OniroError(`Could not list AGC teams (${describeResponse(response)}).`);
|
|
4122
|
+
}
|
|
4123
|
+
return parseTeamList(response.data);
|
|
4124
|
+
},
|
|
4125
|
+
async resolveAgcAuth(o = {}) {
|
|
4126
|
+
const user = await requireUser();
|
|
4127
|
+
if (!user.isRealName) {
|
|
4128
|
+
const agreement = await getDeveloperAgreement(user);
|
|
4129
|
+
if (agreement && !(agreement.signed && agreement.latest)) {
|
|
4130
|
+
throw new OniroError(
|
|
4131
|
+
"This Huawei account has not accepted the current HUAWEI Developer Basic Service Agreement, which accounts without real-name verification need before AppGallery Connect issues signing material. Accept it in AppGallery Connect (https://developer.huawei.com/consumer/en/service/josp/agc/index.html) or by running automatic signing once in DevEco Studio, then try again."
|
|
4132
|
+
);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
return {
|
|
4136
|
+
uid: user.userId,
|
|
4137
|
+
// Personal accounts sign under their own user id; `--team-id` picks another team.
|
|
4138
|
+
teamId: o.teamId ?? user.userId,
|
|
4139
|
+
accessToken: user.accessToken,
|
|
4140
|
+
baseUrl: HUAWEI_SITES[user.siteCode].agcBaseUrl
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
function parseTeamList(body) {
|
|
4146
|
+
let payload;
|
|
4147
|
+
try {
|
|
4148
|
+
payload = JSON.parse(body);
|
|
4149
|
+
} catch {
|
|
4150
|
+
return [];
|
|
4151
|
+
}
|
|
4152
|
+
if (payload.ret && payload.ret.code !== 0) {
|
|
4153
|
+
throw new OniroError(
|
|
4154
|
+
`AppGallery Connect refused the team list (code ${payload.ret.code}${payload.ret.msg ? `: ${payload.ret.msg}` : ""}).`
|
|
4155
|
+
);
|
|
4156
|
+
}
|
|
4157
|
+
if (!Array.isArray(payload.teams)) return [];
|
|
4158
|
+
return payload.teams.map((t) => ({ id: String(t?.id ?? ""), name: String(t?.name ?? "") })).filter((t) => t.id);
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
// src/harmonyos/signing/errors.ts
|
|
4162
|
+
var AgcError = class extends OniroError {
|
|
4163
|
+
constructor(message, statusCode, retCode) {
|
|
4164
|
+
super(message);
|
|
4165
|
+
this.statusCode = statusCode;
|
|
4166
|
+
this.retCode = retCode;
|
|
4167
|
+
this.name = "AgcError";
|
|
4168
|
+
}
|
|
4169
|
+
statusCode;
|
|
4170
|
+
retCode;
|
|
4171
|
+
};
|
|
4172
|
+
var SIGNING_ERRORS = {
|
|
4173
|
+
FORBIDDEN: "This account has no AppGallery Connect permission for the selected team. Ask the team administrator for access, or pass --team-id for a team you do have access to (`oniro-app auth team list`).",
|
|
4174
|
+
UNAUTHORIZED: "AppGallery Connect rejected the access token. Run `oniro-app auth login` again.",
|
|
4175
|
+
NETWORK: "Could not reach AppGallery Connect. Check the network connection and any proxy configuration (HTTPS_PROXY / HTTP_PROXY).",
|
|
4176
|
+
USER_NOT_HARMONY: "This Huawei account is not enrolled for HarmonyOS development. Request HarmonyOS access for it in AppGallery Connect.",
|
|
4177
|
+
CERT_REQUEST: "AppGallery Connect did not issue the debug certificate.",
|
|
4178
|
+
CERT_LIMIT: "This team has reached its debug-certificate limit. Delete an unused certificate in AppGallery Connect and retry.",
|
|
4179
|
+
DEVICE_ADD: "AppGallery Connect did not register the device.",
|
|
4180
|
+
DEVICE_LIMIT: "This team has reached its device limit. Remove an unused device in AppGallery Connect and retry.",
|
|
4181
|
+
DEVICE_NAME_REPEAT: "A device with that name is already registered. Retry.",
|
|
4182
|
+
DEVICE_NONE: "No device is connected and none is registered to this team, but a debug profile must name at least one. Connect a device and retry.",
|
|
4183
|
+
PROFILE_ADD: "AppGallery Connect did not issue the provisioning profile.",
|
|
4184
|
+
PROFILE_NAME_REPEAT: "A provisioning profile with that name already exists in AppGallery Connect. Retry.",
|
|
4185
|
+
PROVISION_LIMIT: "This team has reached its debug-profile limit. Delete unused profiles in AppGallery Connect and retry.",
|
|
4186
|
+
CERT_INVALID: "AppGallery Connect returned an unreadable certificate. Retry with --force.",
|
|
4187
|
+
CERT_EXPIRED: "The signing certificate is outside its validity window. Check the system clock, then retry with --force.",
|
|
4188
|
+
CERT_PROFILE_MISMATCH: "The provisioning profile was not issued for the downloaded certificate. Retry with --force.",
|
|
4189
|
+
KEYSTORE_CERT_MISMATCH: "The local keystore does not hold the key the downloaded certificate was issued for. Retry with --force.",
|
|
4190
|
+
BUNDLE_NAME_INVALID: "Invalid bundle name. It must be 7\u2013128 characters, start with a letter, and have at least three dot-separated segments (for example com.example.app)."
|
|
4191
|
+
};
|
|
4192
|
+
var RET_CODE_ERRORS = {
|
|
4193
|
+
"205389872": SIGNING_ERRORS.CERT_LIMIT,
|
|
4194
|
+
"205389904": SIGNING_ERRORS.USER_NOT_HARMONY,
|
|
4195
|
+
"205389859": SIGNING_ERRORS.DEVICE_LIMIT,
|
|
4196
|
+
"205389857": SIGNING_ERRORS.DEVICE_NAME_REPEAT,
|
|
4197
|
+
"205389938": SIGNING_ERRORS.PROVISION_LIMIT,
|
|
4198
|
+
"205389830": SIGNING_ERRORS.PROFILE_NAME_REPEAT
|
|
4199
|
+
};
|
|
4200
|
+
var PROXY_BLOCKED = "Openproxy_Blocked_URL_list";
|
|
4201
|
+
function readRet(body) {
|
|
4202
|
+
try {
|
|
4203
|
+
let ret = JSON.parse(body).ret;
|
|
4204
|
+
if (typeof ret === "string") ret = JSON.parse(ret);
|
|
4205
|
+
return ret && typeof ret === "object" ? ret : null;
|
|
4206
|
+
} catch {
|
|
4207
|
+
return null;
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
var FALLBACK = {
|
|
4211
|
+
cert: SIGNING_ERRORS.CERT_REQUEST,
|
|
4212
|
+
device: SIGNING_ERRORS.DEVICE_ADD,
|
|
4213
|
+
profile: SIGNING_ERRORS.PROFILE_ADD
|
|
4214
|
+
};
|
|
4215
|
+
function mapAgcError(operation, statusCode, reasonPhrase, body) {
|
|
4216
|
+
if (statusCode === 403) {
|
|
4217
|
+
return new AgcError(reasonPhrase === PROXY_BLOCKED ? SIGNING_ERRORS.NETWORK : SIGNING_ERRORS.FORBIDDEN, 403);
|
|
4218
|
+
}
|
|
4219
|
+
if (statusCode === 401) return new AgcError(SIGNING_ERRORS.UNAUTHORIZED, 401);
|
|
4220
|
+
const ret = readRet(body);
|
|
4221
|
+
const retCode = ret?.code === void 0 ? void 0 : String(ret.code);
|
|
4222
|
+
const msg = typeof ret?.msg === "string" && ret.msg.trim() ? ret.msg : void 0;
|
|
4223
|
+
return new AgcError(retCode && RET_CODE_ERRORS[retCode] || msg || FALLBACK[operation], statusCode, retCode);
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
// src/harmonyos/signing/profile.ts
|
|
4227
|
+
import * as fs33 from "fs";
|
|
4228
|
+
import { X509Certificate } from "crypto";
|
|
4229
|
+
import forge from "node-forge";
|
|
4230
|
+
function extractProfileJson(buffer) {
|
|
4231
|
+
const text = buffer.toString("latin1");
|
|
4232
|
+
for (let start = text.indexOf("{"); start !== -1; start = text.indexOf("{", start + 1)) {
|
|
4233
|
+
let depth = 0;
|
|
4234
|
+
let inString = false;
|
|
4235
|
+
let escaped = false;
|
|
4236
|
+
for (let i = start; i < text.length; i++) {
|
|
4237
|
+
const ch = text[i];
|
|
4238
|
+
if (escaped) escaped = false;
|
|
4239
|
+
else if (inString) {
|
|
4240
|
+
if (ch === "\\") escaped = true;
|
|
4241
|
+
else if (ch === '"') inString = false;
|
|
4242
|
+
} else if (ch === '"') inString = true;
|
|
4243
|
+
else if (ch === "{") depth++;
|
|
4244
|
+
else if (ch === "}" && --depth === 0) {
|
|
4245
|
+
const json = Buffer.from(text.slice(start, i + 1), "latin1").toString("utf8");
|
|
4246
|
+
try {
|
|
4247
|
+
if ("bundle-info" in JSON.parse(json)) return json;
|
|
4248
|
+
} catch {
|
|
4249
|
+
}
|
|
4250
|
+
break;
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
return null;
|
|
4255
|
+
}
|
|
4256
|
+
function readProvisionProfile(profilePath) {
|
|
4257
|
+
let raw;
|
|
4258
|
+
try {
|
|
4259
|
+
const json = extractProfileJson(fs33.readFileSync(profilePath));
|
|
4260
|
+
if (!json) return null;
|
|
4261
|
+
raw = JSON.parse(json);
|
|
4262
|
+
} catch {
|
|
4263
|
+
return null;
|
|
4264
|
+
}
|
|
4265
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
4266
|
+
const strings = (v) => Array.isArray(v) ? v.filter((x2) => typeof x2 === "string") : [];
|
|
4267
|
+
const notAfter = raw.validity?.["not-after"];
|
|
4268
|
+
const bundle = raw["bundle-info"] ?? {};
|
|
4269
|
+
return {
|
|
4270
|
+
bundleName: str(bundle["bundle-name"]),
|
|
4271
|
+
developerId: str(bundle["developer-id"]),
|
|
4272
|
+
// Epoch seconds.
|
|
4273
|
+
notAfter: typeof notAfter === "number" ? new Date(notAfter * 1e3) : void 0,
|
|
4274
|
+
deviceIds: strings(raw["debug-info"]?.["device-ids"]).map((d) => d.toUpperCase()),
|
|
4275
|
+
aclPermissions: strings(raw.acls?.["allowed-acls"]),
|
|
4276
|
+
developmentCertificate: str(bundle["development-certificate"])
|
|
4277
|
+
};
|
|
4278
|
+
}
|
|
4279
|
+
function parseCertificate(pem) {
|
|
4280
|
+
try {
|
|
4281
|
+
return new X509Certificate(pem);
|
|
4282
|
+
} catch {
|
|
4283
|
+
throw new AgcError(SIGNING_ERRORS.CERT_INVALID);
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
function keystoreHoldsKey(p12Path, keyAlias, keyPwd, certificate) {
|
|
4287
|
+
try {
|
|
4288
|
+
const der = forge.util.createBuffer(fs33.readFileSync(p12Path).toString("binary"));
|
|
4289
|
+
const p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(der), keyPwd);
|
|
4290
|
+
const certBag = forge.pki.oids.certBag ?? "1.2.840.113549.1.12.10.1.3";
|
|
4291
|
+
const wanted = certificate.publicKey.export({ type: "spki", format: "der" });
|
|
4292
|
+
return (p12.getBags({ bagType: certBag })[certBag] ?? []).some((bag) => {
|
|
4293
|
+
if (bag.attributes?.friendlyName?.[0]?.toLowerCase() !== keyAlias.toLowerCase()) return false;
|
|
4294
|
+
const asn1 = bag.cert ? forge.pki.certificateToAsn1(bag.cert) : bag.asn1;
|
|
4295
|
+
if (!asn1) return false;
|
|
4296
|
+
const bagCert = new X509Certificate(Buffer.from(forge.asn1.toDer(asn1).getBytes(), "binary"));
|
|
4297
|
+
return bagCert.publicKey.export({ type: "spki", format: "der" }).equals(wanted);
|
|
4298
|
+
});
|
|
4299
|
+
} catch {
|
|
4300
|
+
return false;
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
function verifySigningMaterial(opts) {
|
|
4304
|
+
const pems = fs33.readFileSync(opts.cerPath, "utf8").match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
4305
|
+
const chain = (pems ?? []).map(parseCertificate);
|
|
4306
|
+
const leaf = chain.find((c) => !c.ca) ?? chain.at(-1);
|
|
4307
|
+
if (!leaf) throw new AgcError(SIGNING_ERRORS.CERT_INVALID);
|
|
4308
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
4309
|
+
if (chain.some((c) => now < new Date(c.validFrom) || now > new Date(c.validTo))) {
|
|
4310
|
+
throw new AgcError(SIGNING_ERRORS.CERT_EXPIRED);
|
|
4311
|
+
}
|
|
4312
|
+
const issuedFor = readProvisionProfile(opts.profilePath)?.developmentCertificate;
|
|
4313
|
+
if (!issuedFor || parseCertificate(issuedFor).fingerprint256 !== leaf.fingerprint256) {
|
|
4314
|
+
throw new AgcError(SIGNING_ERRORS.CERT_PROFILE_MISMATCH);
|
|
4315
|
+
}
|
|
4316
|
+
if (!keystoreHoldsKey(opts.p12Path, opts.keyAlias, opts.keyPwd, leaf)) {
|
|
4317
|
+
throw new AgcError(SIGNING_ERRORS.KEYSTORE_CERT_MISMATCH);
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
// src/harmonyos/signing/autoSign.ts
|
|
4322
|
+
import * as crypto9 from "crypto";
|
|
4323
|
+
import * as fs38 from "fs";
|
|
4324
|
+
import * as path37 from "path";
|
|
4325
|
+
import JSON57 from "json5";
|
|
4326
|
+
|
|
4327
|
+
// src/harmonyos/signing/agc.ts
|
|
4328
|
+
import * as crypto6 from "crypto";
|
|
4329
|
+
import * as fs34 from "fs";
|
|
4330
|
+
import * as path32 from "path";
|
|
4331
|
+
var ENDPOINTS = {
|
|
4332
|
+
CERT_LIST: "/api/cps/harmony-cert-manage/v1/cert/list",
|
|
4333
|
+
CERT_ADD: "/api/cps/harmony-cert-manage/v1/cert/add",
|
|
4334
|
+
CERT_DELETE: "/api/cps/harmony-cert-manage/v1/cert/delete",
|
|
4335
|
+
/** Exchanges an object id for a short-lived signed download URL. */
|
|
4336
|
+
OBJECT_URL: "/api/amis/app-manage/v1/objects/url/reapply",
|
|
4337
|
+
DEVICE_LIST: "/api/cps/device-manage/v1/device/list",
|
|
4338
|
+
DEVICE_ADD: "/api/cps/device-manage/v1/device/add",
|
|
4339
|
+
/** Debug ("test") provisioning profiles — the only kind oniro-app issues. */
|
|
4340
|
+
PROVISION_ADD: "/api/cps/provision-manage/v1/ide/test/provision/add",
|
|
4341
|
+
PROVISION_DELETE: "/api/cps/provision-manage/v1/provision/delete"
|
|
4342
|
+
};
|
|
4343
|
+
var DEVICE_TYPE_CODES = { liteWearable: 1, wearable: 2, tv: 3, phone: 4 };
|
|
4344
|
+
function createAgcClient(http3, auth) {
|
|
4345
|
+
async function call(operation, method, endpoint, opts = {}) {
|
|
4346
|
+
const response = await http3[method](`${auth.baseUrl}${endpoint}`, {
|
|
4347
|
+
...opts,
|
|
4348
|
+
headers: { uid: auth.uid, teamId: auth.teamId, oauth2Token: auth.accessToken }
|
|
4349
|
+
});
|
|
4350
|
+
if (response.statusCode !== 200 || readRet(response.data)?.code !== 0) {
|
|
4351
|
+
throw mapAgcError(operation, response.statusCode, response.statusText, response.data);
|
|
4352
|
+
}
|
|
4353
|
+
return JSON.parse(response.data);
|
|
4354
|
+
}
|
|
4355
|
+
return {
|
|
4356
|
+
async listCertificates() {
|
|
4357
|
+
return (await call("cert", "post", ENDPOINTS.CERT_LIST)).certList ?? [];
|
|
4358
|
+
},
|
|
4359
|
+
async addCertificate(certName, csr) {
|
|
4360
|
+
await call("cert", "post", ENDPOINTS.CERT_ADD, { body: { csr, certName, certType: "1" } });
|
|
4361
|
+
},
|
|
4362
|
+
async deleteCertificate(certId) {
|
|
4363
|
+
await call("cert", "delete", ENDPOINTS.CERT_DELETE, { body: { certIds: [certId] } });
|
|
4364
|
+
},
|
|
4365
|
+
/** Every device registered to the team, across pages. */
|
|
4366
|
+
async listDevices() {
|
|
4367
|
+
const pageSize = 100;
|
|
4368
|
+
const devices = [];
|
|
4369
|
+
for (let start = 1; ; start++) {
|
|
4370
|
+
const page = await call("device", "get", ENDPOINTS.DEVICE_LIST, {
|
|
4371
|
+
query: { encodeFlag: 0, start, pageSize }
|
|
4372
|
+
});
|
|
4373
|
+
devices.push(...page.list ?? []);
|
|
4374
|
+
if (!page.list?.length || devices.length >= (page.totalCount ?? 0)) return devices;
|
|
4375
|
+
}
|
|
4376
|
+
},
|
|
4377
|
+
async addDevice(udid, deviceKind) {
|
|
4378
|
+
const deviceName = `oniro_app_device_${crypto6.randomBytes(4).toString("hex")}`;
|
|
4379
|
+
await call("device", "post", ENDPOINTS.DEVICE_ADD, {
|
|
4380
|
+
body: { deviceName, udid, deviceType: String(DEVICE_TYPE_CODES[deviceKind] ?? DEVICE_TYPE_CODES.phone) }
|
|
4381
|
+
});
|
|
4382
|
+
},
|
|
4383
|
+
/** Issue a debug profile; returns its id and object handle. */
|
|
4384
|
+
async addDebugProfile(opts) {
|
|
4385
|
+
return call("profile", "post", ENDPOINTS.PROVISION_ADD, {
|
|
4386
|
+
body: {
|
|
4387
|
+
provisionName: opts.provisionName,
|
|
4388
|
+
packageName: opts.bundleName,
|
|
4389
|
+
certList: [opts.certId],
|
|
4390
|
+
deviceList: opts.deviceIds,
|
|
4391
|
+
...opts.aclPermissions.length ? { aclPermissionList: opts.aclPermissions } : {}
|
|
4392
|
+
}
|
|
4393
|
+
});
|
|
4394
|
+
},
|
|
4395
|
+
async deleteProfile(id) {
|
|
4396
|
+
await call("profile", "delete", ENDPOINTS.PROVISION_DELETE, { query: { id } });
|
|
4397
|
+
},
|
|
4398
|
+
/**
|
|
4399
|
+
* Download a certificate or profile object to `dest`, checking the digest AGC
|
|
4400
|
+
* supplies with the URL. Nothing is written unless it matches.
|
|
4401
|
+
*/
|
|
4402
|
+
async download(operation, objectId, dest) {
|
|
4403
|
+
const { urlsInfo } = await call(
|
|
4404
|
+
operation,
|
|
4405
|
+
"post",
|
|
4406
|
+
ENDPOINTS.OBJECT_URL,
|
|
4407
|
+
{ body: { sourceUrls: objectId } }
|
|
4408
|
+
);
|
|
4409
|
+
const target = urlsInfo?.[0];
|
|
4410
|
+
if (!target) throw mapAgcError(operation, 200, "", "");
|
|
4411
|
+
const buffer = await http3.getBinary(target.newUrl, { timeoutMs: 6e4 });
|
|
4412
|
+
const actual = crypto6.createHash("sha256").update(buffer).digest("hex");
|
|
4413
|
+
if (target.sha256 && actual !== target.sha256.toLowerCase()) {
|
|
4414
|
+
throw new ChecksumMismatchError(target.sha256, actual);
|
|
4415
|
+
}
|
|
4416
|
+
fs34.mkdirSync(path32.dirname(dest), { recursive: true, mode: 448 });
|
|
4417
|
+
fs34.writeFileSync(dest, buffer, { mode: 384 });
|
|
4418
|
+
}
|
|
4419
|
+
};
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
// src/harmonyos/signing/devices.ts
|
|
4423
|
+
function parseUdid(stdout) {
|
|
4424
|
+
return stdout.match(/\b[A-Fa-f0-9]{64}\b/)?.[0].toUpperCase() ?? "";
|
|
4425
|
+
}
|
|
4426
|
+
function parseDeviceKind(stdout) {
|
|
4427
|
+
return ["liteWearable", "wearable", "tv"].find((kind) => stdout.includes(kind)) ?? "phone";
|
|
4428
|
+
}
|
|
4429
|
+
async function collectLocalDevices(config, logger) {
|
|
4430
|
+
const devices = [];
|
|
4431
|
+
for (const target of await listDevices(config, { logger, timeoutMs: 15e3 })) {
|
|
4432
|
+
if (target.status !== "Connected") continue;
|
|
4433
|
+
const shell2 = async (...args) => (await hdcExec({ config, deviceSerial: target.serial, args: ["shell", ...args], timeoutMs: 15e3, logger })).stdout;
|
|
4434
|
+
try {
|
|
4435
|
+
const udid = parseUdid(await shell2("bm", "get", "-u"));
|
|
4436
|
+
if (!udid) {
|
|
4437
|
+
logger.warn(`[harmonyos] ${target.serial} did not report a UDID; skipping it.`);
|
|
4438
|
+
continue;
|
|
4439
|
+
}
|
|
4440
|
+
devices.push({ serial: target.serial, udid, kind: parseDeviceKind(await shell2("getprop", "hw_sc.build.os.deviceType")) });
|
|
4441
|
+
} catch (err) {
|
|
4442
|
+
logger.warn(`[harmonyos] Could not read ${target.serial}: ${err.message}`);
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
return devices;
|
|
4446
|
+
}
|
|
4447
|
+
async function registerDevices(agc, connected, logger) {
|
|
4448
|
+
let devices = await agc.listDevices();
|
|
4449
|
+
const registered = new Set(devices.map((d) => d.udid.toUpperCase()));
|
|
4450
|
+
const unregistered = connected.filter((d) => !registered.has(d.udid));
|
|
4451
|
+
for (const device of unregistered) {
|
|
4452
|
+
logger.info(`[harmonyos] Registering ${device.serial} with AppGallery Connect...`);
|
|
4453
|
+
await agc.addDevice(device.udid, device.kind);
|
|
4454
|
+
}
|
|
4455
|
+
if (unregistered.length > 0) devices = await agc.listDevices();
|
|
4456
|
+
const deviceIds = devices.map((d) => d.id);
|
|
4457
|
+
if (deviceIds.length === 0) throw new AgcError(SIGNING_ERRORS.DEVICE_NONE);
|
|
4458
|
+
if (connected.length === 0) {
|
|
4459
|
+
logger.warn(
|
|
4460
|
+
`[harmonyos] No device is connected; the profile will name the ${deviceIds.length} device(s) already registered to this team.`
|
|
4461
|
+
);
|
|
4462
|
+
}
|
|
4463
|
+
return deviceIds;
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
// src/harmonyos/signing/keystore.ts
|
|
4467
|
+
import * as crypto7 from "crypto";
|
|
4468
|
+
import * as fs36 from "fs";
|
|
4469
|
+
import * as path34 from "path";
|
|
4470
|
+
|
|
4471
|
+
// src/harmonyos/java.ts
|
|
4472
|
+
import * as fs35 from "fs";
|
|
4473
|
+
import * as path33 from "path";
|
|
4474
|
+
var JAVA_BIN = process.platform === "win32" ? "java.exe" : "java";
|
|
4475
|
+
function findJava(sdk, env = process.env) {
|
|
4476
|
+
const candidates = [
|
|
4477
|
+
env.JAVA_HOME && path33.join(env.JAVA_HOME, "bin", JAVA_BIN),
|
|
4478
|
+
sdk && path33.join(sdk.installRoot, "jbr", "bin", JAVA_BIN),
|
|
4479
|
+
// macOS: the JBR is itself a bundle inside Studio's Contents.
|
|
4480
|
+
sdk && path33.join(sdk.installRoot, "jbr", "Contents", "Home", "bin", JAVA_BIN),
|
|
4481
|
+
...(env.PATH ?? "").split(path33.delimiter).map((dir) => dir && path33.join(dir, JAVA_BIN))
|
|
4482
|
+
];
|
|
4483
|
+
const found = candidates.find((c) => !!c && fs35.existsSync(c));
|
|
4484
|
+
if (found) return found;
|
|
4485
|
+
throw new OniroError(
|
|
4486
|
+
"No Java runtime found. HarmonyOS signing runs the SDK's hap-sign-tool.jar, which needs one: install a JDK and set JAVA_HOME, or install DevEco Studio, whose bundled runtime is used automatically."
|
|
4487
|
+
);
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
// src/harmonyos/signing/keystore.ts
|
|
4491
|
+
var KEY_ALIAS = "debugKey";
|
|
4492
|
+
var SIGN_ALG = "SHA256withECDSA";
|
|
4493
|
+
function redactArgs(args) {
|
|
4494
|
+
return args.map((arg, i) => args[i - 1] === "-keyPwd" || args[i - 1] === "-keystorePwd" ? "******" : arg);
|
|
4495
|
+
}
|
|
4496
|
+
async function generateKeystoreAndCsr(opts) {
|
|
4497
|
+
const jar = path34.join(opts.sdk.sdkPath, "default", "openharmony", "toolchains", "lib", "hap-sign-tool.jar");
|
|
4498
|
+
if (!fs36.existsSync(jar)) {
|
|
4499
|
+
throw new OniroError(`hap-sign-tool.jar not found at ${jar}; the HarmonyOS SDK looks incomplete.`);
|
|
4500
|
+
}
|
|
4501
|
+
const java = findJava(opts.sdk);
|
|
4502
|
+
const keyPwd = crypto7.randomBytes(12).toString("base64url").replaceAll(/[-_]/g, "");
|
|
4503
|
+
fs36.mkdirSync(path34.dirname(opts.p12Path), { recursive: true, mode: 448 });
|
|
4504
|
+
const run = async (what, args) => {
|
|
4505
|
+
const argv = ["-jar", jar, ...args, "-keyAlias", KEY_ALIAS, "-keystoreFile", opts.p12Path, "-keystorePwd", keyPwd, "-keyPwd", keyPwd];
|
|
4506
|
+
opts.logger.debug(`[harmonyos] ${java} ${redactArgs(argv).join(" ")}`);
|
|
4507
|
+
const result = await runProcess({ command: java, args: argv, timeoutMs: 12e4, logger: opts.logger });
|
|
4508
|
+
if (result.code !== 0) {
|
|
4509
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
4510
|
+
throw new OniroError(`hap-sign-tool failed to ${what} (exit ${result.code}).${detail ? `
|
|
4511
|
+
${detail}` : ""}`);
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
opts.logger.info("[harmonyos] Generating the local keystore and certificate signing request...");
|
|
4515
|
+
await run("generate the key pair", ["generate-keypair", "-keyAlg", "ECC", "-keySize", "NIST-P-256"]);
|
|
4516
|
+
await run("generate the CSR", ["generate-csr", "-subject", "CN=DebugKey", "-signAlg", SIGN_ALG, "-outFile", opts.csrPath]);
|
|
4517
|
+
return { keyPwd, csr: fs36.readFileSync(opts.csrPath, "utf8") };
|
|
4518
|
+
}
|
|
4519
|
+
|
|
4520
|
+
// src/harmonyos/signing/paths.ts
|
|
4521
|
+
import * as crypto8 from "crypto";
|
|
4522
|
+
import * as path35 from "path";
|
|
4523
|
+
function sanitizeNamePart(raw) {
|
|
4524
|
+
const safe = (raw?.trim() ?? "").replace(/[\\/:*?"<>|=.-]+/g, "_");
|
|
4525
|
+
return safe.slice(0, 64) || "default";
|
|
4526
|
+
}
|
|
4527
|
+
function resolveSigningMaterialPaths(config, productName, projectRoot) {
|
|
4528
|
+
const hash = crypto8.createHash("sha256").update(projectRoot, "utf8").digest("base64url").replaceAll(/[-_]/g, "");
|
|
4529
|
+
const base = path35.join(
|
|
4530
|
+
config.get("harmonyosSigningDir", defaultPaths.harmonyosSigningDir()),
|
|
4531
|
+
`${sanitizeNamePart(productName)}_${path35.basename(projectRoot)}_${hash}=`
|
|
4532
|
+
);
|
|
4533
|
+
return { p12Path: `${base}.p12`, csrPath: `${base}.csr`, cerPath: `${base}.cer`, profilePath: `${base}.p7b` };
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
// src/harmonyos/signing/project.ts
|
|
4537
|
+
import * as fs37 from "fs";
|
|
4538
|
+
import * as path36 from "path";
|
|
4539
|
+
import JSON56 from "json5";
|
|
4540
|
+
|
|
4541
|
+
// src/harmonyos/signing/aclPermissionList.ts
|
|
4542
|
+
var ACL_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
4543
|
+
"ohos.permission.ACCESS_DDK_HID",
|
|
4544
|
+
"ohos.permission.ACCESS_DDK_SCSI_PERIPHERAL",
|
|
4545
|
+
"ohos.permission.ACCESS_DDK_USB",
|
|
4546
|
+
"ohos.permission.ACCESS_DDK_USB_SERIAL",
|
|
4547
|
+
"ohos.permission.ACCESS_DISK_PHY_INFO",
|
|
4548
|
+
"ohos.permission.ACCESS_FIDO2_ONLINEAUTH",
|
|
4549
|
+
"ohos.permission.ACCESS_NET_TRACE_INFO",
|
|
4550
|
+
"ohos.permission.ACCESS_USER_FULL_DISK",
|
|
4551
|
+
"ohos.permission.ACCESS_VIRTUAL_SCREEN",
|
|
4552
|
+
"ohos.permission.CUSTOMIZE_SAVE_BUTTON",
|
|
4553
|
+
"ohos.permission.CUSTOM_SCREEN_RECORDING",
|
|
4554
|
+
"ohos.permission.DLP_GET_HIDE_STATUS",
|
|
4555
|
+
"ohos.permission.FILE_ACCESS_PERSIST",
|
|
4556
|
+
"ohos.permission.GET_ABILITY_INFO",
|
|
4557
|
+
"ohos.permission.GET_ETHERNET_LOCAL_MAC",
|
|
4558
|
+
"ohos.permission.GET_IP_MAC_INFO",
|
|
4559
|
+
"ohos.permission.GET_WIFI_LOCAL_MAC",
|
|
4560
|
+
"ohos.permission.GET_WIFI_PEERS_MAC",
|
|
4561
|
+
"ohos.permission.HOOK_KEY_EVENT",
|
|
4562
|
+
"ohos.permission.INPUT_MONITORING",
|
|
4563
|
+
"ohos.permission.INTERCEPT_INPUT_EVENT",
|
|
4564
|
+
"ohos.permission.KEEP_BACKGROUND_RUNNING_SYSTEM",
|
|
4565
|
+
"ohos.permission.LINKTURBO",
|
|
4566
|
+
"ohos.permission.MANAGE_APN_SETTING",
|
|
4567
|
+
"ohos.permission.MANAGE_PASTEBOARD_APP_SHARE_OPTION",
|
|
4568
|
+
"ohos.permission.MANAGE_SCREEN_TIME_GUARD",
|
|
4569
|
+
"ohos.permission.MANAGE_UDMF_APP_SHARE_OPTION",
|
|
4570
|
+
"ohos.permission.PERSISTENT_BLUETOOTH_PEERS_MAC",
|
|
4571
|
+
"ohos.permission.PERSONAL_MANAGE_RESTRICTIONS",
|
|
4572
|
+
"ohos.permission.PRELOAD_FILE",
|
|
4573
|
+
"ohos.permission.READ_AUDIO",
|
|
4574
|
+
"ohos.permission.READ_CONTACTS",
|
|
4575
|
+
"ohos.permission.READ_IMAGEVIDEO",
|
|
4576
|
+
"ohos.permission.READ_LOCAL_DEVICE_NAME",
|
|
4577
|
+
"ohos.permission.READ_PASTEBOARD",
|
|
4578
|
+
"ohos.permission.READ_WRITE_DESKTOP_DIRECTORY",
|
|
4579
|
+
"ohos.permission.READ_WRITE_DOCUMENTS_DIRECTORY",
|
|
4580
|
+
"ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY",
|
|
4581
|
+
"ohos.permission.READ_WRITE_USB_DEV",
|
|
4582
|
+
"ohos.permission.READ_WRITE_USER_FILE",
|
|
4583
|
+
"ohos.permission.SET_PAC_URL",
|
|
4584
|
+
"ohos.permission.SET_SYSTEMSHARE_APPLAUNCHTRUSTLIST",
|
|
4585
|
+
"ohos.permission.SHORT_TERM_WRITE_IMAGEVIDEO",
|
|
4586
|
+
"ohos.permission.START_PROVISIONING_MESSAGE",
|
|
4587
|
+
"ohos.permission.SUBSCRIBE_NOTIFICATION",
|
|
4588
|
+
"ohos.permission.SYSTEM_FLOAT_WINDOW",
|
|
4589
|
+
"ohos.permission.USE_FLOAT_BALL",
|
|
4590
|
+
"ohos.permission.USE_FRAUD_APP_PICKER",
|
|
4591
|
+
"ohos.permission.USE_FRAUD_CALL_LOG_PICKER",
|
|
4592
|
+
"ohos.permission.USE_FRAUD_MESSAGES_PICKER",
|
|
4593
|
+
"ohos.permission.WEB_NATIVE_MESSAGING",
|
|
4594
|
+
"ohos.permission.WRITE_AUDIO",
|
|
4595
|
+
"ohos.permission.WRITE_CONTACTS",
|
|
4596
|
+
"ohos.permission.WRITE_IMAGEVIDEO",
|
|
4597
|
+
"ohos.permission.atomicService.MANAGE_STORAGE",
|
|
4598
|
+
"ohos.permission.kernel.ALLOW_EXECUTABLE_FORT_MEMORY",
|
|
4599
|
+
"ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY",
|
|
4600
|
+
"ohos.permission.kernel.DISABLE_CODE_MEMORY_PROTECTION",
|
|
4601
|
+
"ohos.permission.kernel.DISABLE_GOTPLT_RO_PROTECTION"
|
|
4602
|
+
]);
|
|
4603
|
+
|
|
4604
|
+
// src/harmonyos/signing/project.ts
|
|
4605
|
+
function readRequestedPermissions(moduleConfigPath) {
|
|
4606
|
+
if (!fs37.existsSync(moduleConfigPath)) return [];
|
|
4607
|
+
try {
|
|
4608
|
+
const parsed = JSON56.parse(fs37.readFileSync(moduleConfigPath, "utf-8"));
|
|
4609
|
+
return (parsed.module?.requestPermissions ?? []).map((p) => p?.name).filter((n) => typeof n === "string" && n.length > 0);
|
|
4610
|
+
} catch {
|
|
4611
|
+
return [];
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
function collectAclPermissions(projectDir, logger = noopLogger) {
|
|
4615
|
+
const requested = /* @__PURE__ */ new Set();
|
|
4616
|
+
for (const module of listModules({ projectDir })) {
|
|
4617
|
+
const moduleRoot = path36.resolve(projectDir, module.srcPath);
|
|
4618
|
+
for (const sourceSet of ["main", "ohosTest"]) {
|
|
4619
|
+
for (const name of readRequestedPermissions(
|
|
4620
|
+
path36.join(moduleRoot, "src", sourceSet, "module.json5")
|
|
4621
|
+
)) {
|
|
4622
|
+
requested.add(name);
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
const acls = [...requested].filter((p) => ACL_PERMISSIONS.has(p)).sort();
|
|
4627
|
+
if (acls.length > 0) {
|
|
4628
|
+
logger.info(`[harmonyos] Project requests ${acls.length} ACL permission(s): ${acls.join(", ")}`);
|
|
4629
|
+
}
|
|
4630
|
+
return acls;
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
// src/harmonyos/signing/autoSign.ts
|
|
4634
|
+
function shouldRegenerate(input) {
|
|
4635
|
+
if (input.force) return "--force was passed";
|
|
4636
|
+
const missing = Object.values(input.paths).find((file) => !fs38.existsSync(file));
|
|
4637
|
+
if (missing) return `${missing} is missing`;
|
|
4638
|
+
const config = input.buildProfile.app?.signingConfigs?.find((c) => c.name === input.productName);
|
|
4639
|
+
if (config?.type !== "HarmonyOS" || config.material?.profile !== input.paths.profilePath) {
|
|
4640
|
+
return "build-profile.json5 has no signingConfig for this material";
|
|
4641
|
+
}
|
|
4642
|
+
const profile = readProvisionProfile(input.paths.profilePath);
|
|
4643
|
+
if (!profile) return "the provisioning profile could not be read";
|
|
4644
|
+
if (profile.bundleName !== input.bundleName) return `the profile is for bundle '${profile.bundleName}'`;
|
|
4645
|
+
if (profile.developerId && profile.developerId !== input.teamId) {
|
|
4646
|
+
return `the profile was issued to team '${profile.developerId}'`;
|
|
4647
|
+
}
|
|
4648
|
+
if (profile.notAfter && profile.notAfter <= (input.now ?? /* @__PURE__ */ new Date())) return "the profile has expired";
|
|
4649
|
+
const unnamed = input.connectedUdids.filter((udid) => !profile.deviceIds.includes(udid.toUpperCase()));
|
|
4650
|
+
if (unnamed.length > 0) return `${unnamed.length} connected device(s) are not named in the profile`;
|
|
4651
|
+
const acls = input.aclPermissions.filter((p) => !profile.aclPermissions.includes(p));
|
|
4652
|
+
if (acls.length > 0) return `the profile lacks ACL permission(s) ${acls.join(", ")}`;
|
|
4653
|
+
return null;
|
|
4654
|
+
}
|
|
4655
|
+
function writeSigningConfig(opts) {
|
|
4656
|
+
const app = opts.buildProfile.app ??= {};
|
|
4657
|
+
const entry = {
|
|
4658
|
+
name: opts.productName,
|
|
4659
|
+
// `type` is what makes hvigor treat this as HarmonyOS signing material.
|
|
4660
|
+
type: "HarmonyOS",
|
|
4661
|
+
material: {
|
|
4662
|
+
certpath: opts.paths.cerPath,
|
|
4663
|
+
keyAlias: KEY_ALIAS,
|
|
4664
|
+
keyPassword: opts.encryptedPassword,
|
|
4665
|
+
profile: opts.paths.profilePath,
|
|
4666
|
+
signAlg: SIGN_ALG,
|
|
4667
|
+
storeFile: opts.paths.p12Path,
|
|
4668
|
+
storePassword: opts.encryptedPassword
|
|
4669
|
+
}
|
|
4670
|
+
};
|
|
4671
|
+
app.signingConfigs = [...(app.signingConfigs ?? []).filter((c) => c.name !== opts.productName), entry];
|
|
4672
|
+
for (const product of app.products ?? []) {
|
|
4673
|
+
if (product.name === opts.productName) product.signingConfig = opts.productName;
|
|
4674
|
+
}
|
|
4675
|
+
fs38.writeFileSync(opts.buildProfilePath, JSON.stringify(opts.buildProfile, null, 2));
|
|
4676
|
+
}
|
|
4677
|
+
async function harmonyOsAutoSign(opts) {
|
|
4678
|
+
const logger = opts.logger ?? noopLogger;
|
|
4679
|
+
const productName = opts.productName ?? "default";
|
|
4680
|
+
const projectDir = path37.resolve(opts.projectDir);
|
|
4681
|
+
const buildProfilePath = path37.join(projectDir, "build-profile.json5");
|
|
4682
|
+
let buildProfile;
|
|
4683
|
+
try {
|
|
4684
|
+
buildProfile = JSON57.parse(fs38.readFileSync(buildProfilePath, "utf-8"));
|
|
4685
|
+
} catch (err) {
|
|
4686
|
+
throw new OniroError(`Could not read ${buildProfilePath}: ${err.message}`, err);
|
|
4687
|
+
}
|
|
4688
|
+
const product = buildProfile.app?.products?.find((p) => p.name === productName);
|
|
4689
|
+
if (!product) throw new OniroError(`${buildProfilePath} declares no product named '${productName}'.`);
|
|
4690
|
+
const bundleName = product.bundleName || getBundleName(projectDir);
|
|
4691
|
+
if (!isValidBundleName(bundleName) || bundleName.split(".").length < 3 || bundleName.length < 7 || bundleName.length > 128) {
|
|
4692
|
+
throw new AgcError(SIGNING_ERRORS.BUNDLE_NAME_INVALID);
|
|
4693
|
+
}
|
|
4694
|
+
const sdk = requireHarmonyOsSdk({ config: opts.config, logger });
|
|
4695
|
+
const auth = await opts.session.resolveAgcAuth({ teamId: opts.teamId });
|
|
4696
|
+
const aclPermissions = collectAclPermissions(projectDir, logger);
|
|
4697
|
+
const paths = resolveSigningMaterialPaths(opts.config, productName, projectDir);
|
|
4698
|
+
logger.info(`[harmonyos] Signing ${bundleName} (product '${productName}', team ${auth.teamId}).`);
|
|
4699
|
+
const connected = await collectLocalDevices(opts.config, logger);
|
|
4700
|
+
const result = (regenerated) => ({
|
|
4701
|
+
bundleName,
|
|
4702
|
+
teamId: auth.teamId,
|
|
4703
|
+
paths,
|
|
4704
|
+
regenerated,
|
|
4705
|
+
deviceIds: readProvisionProfile(paths.profilePath)?.deviceIds ?? []
|
|
4706
|
+
});
|
|
4707
|
+
const reason = shouldRegenerate({
|
|
4708
|
+
paths,
|
|
4709
|
+
buildProfile,
|
|
4710
|
+
productName,
|
|
4711
|
+
bundleName,
|
|
4712
|
+
teamId: auth.teamId,
|
|
4713
|
+
connectedUdids: connected.map((d) => d.udid),
|
|
4714
|
+
aclPermissions,
|
|
4715
|
+
force: opts.force
|
|
4716
|
+
});
|
|
4717
|
+
if (!reason) {
|
|
4718
|
+
logger.info("[harmonyos] Reusing the existing signing material, which is still valid.");
|
|
4719
|
+
return result(false);
|
|
4720
|
+
}
|
|
4721
|
+
logger.info(`[harmonyos] Regenerating signing material: ${reason}.`);
|
|
4722
|
+
const agc = createAgcClient(opts.http ?? createHttpClient({ logger }), auth);
|
|
4723
|
+
const deviceIds = await registerDevices(agc, connected, logger);
|
|
4724
|
+
for (const file of Object.values(paths)) fs38.rmSync(file, { force: true });
|
|
4725
|
+
const certName = `oniro_debug_${auth.teamId.replace(/[\\/.:]/g, "")}.cer`;
|
|
4726
|
+
const stale = (await agc.listCertificates()).find((c) => c.certName === certName);
|
|
4727
|
+
if (stale) {
|
|
4728
|
+
logger.info(`[harmonyos] Replacing the previous '${certName}' certificate in AppGallery Connect...`);
|
|
4729
|
+
await agc.deleteCertificate(stale.id);
|
|
4730
|
+
}
|
|
4731
|
+
const { keyPwd, csr } = await generateKeystoreAndCsr({ sdk, p12Path: paths.p12Path, csrPath: paths.csrPath, logger });
|
|
4732
|
+
logger.info("[harmonyos] Requesting a debug certificate from AppGallery Connect...");
|
|
4733
|
+
await agc.addCertificate(certName, csr);
|
|
4734
|
+
const cert = (await agc.listCertificates()).find((c) => c.certName === certName);
|
|
4735
|
+
if (!cert) throw new AgcError(SIGNING_ERRORS.CERT_REQUEST);
|
|
4736
|
+
await agc.download("cert", cert.certObjectId, paths.cerPath);
|
|
4737
|
+
logger.info("[harmonyos] Issuing the debug provisioning profile...");
|
|
4738
|
+
const profile = await agc.addDebugProfile({
|
|
4739
|
+
provisionName: `oniro_${crypto9.randomBytes(6).toString("hex")}`,
|
|
4740
|
+
bundleName,
|
|
4741
|
+
certId: cert.id,
|
|
4742
|
+
deviceIds,
|
|
4743
|
+
aclPermissions
|
|
4744
|
+
});
|
|
4745
|
+
try {
|
|
4746
|
+
await agc.download("profile", profile.provisionFileUrl, paths.profilePath);
|
|
4747
|
+
} finally {
|
|
4748
|
+
await agc.deleteProfile(profile.id).catch((err) => {
|
|
4749
|
+
logger.debug(`[harmonyos] Could not delete profile ${profile.id} from AppGallery Connect: ${err.message}`);
|
|
4750
|
+
});
|
|
4751
|
+
}
|
|
4752
|
+
try {
|
|
4753
|
+
verifySigningMaterial({ ...paths, keyAlias: KEY_ALIAS, keyPwd });
|
|
4754
|
+
} catch (err) {
|
|
4755
|
+
fs38.rmSync(paths.profilePath, { force: true });
|
|
4756
|
+
throw err;
|
|
4757
|
+
}
|
|
4758
|
+
const materialDir = path37.join(path37.dirname(paths.p12Path), "material");
|
|
4759
|
+
try {
|
|
4760
|
+
getKey(materialDir);
|
|
4761
|
+
} catch {
|
|
4762
|
+
createMaterial(materialDir);
|
|
4763
|
+
}
|
|
4764
|
+
writeSigningConfig({ buildProfilePath, buildProfile, productName, paths, encryptedPassword: encryptPwd(keyPwd, materialDir) });
|
|
4765
|
+
logger.info(`[harmonyos] Wrote signingConfigs['${productName}'] to ${buildProfilePath}.`);
|
|
4766
|
+
return result(true);
|
|
4767
|
+
}
|
|
4768
|
+
|
|
3410
4769
|
// src/index.ts
|
|
3411
|
-
var VERSION = "0.
|
|
4770
|
+
var VERSION = "0.12.0";
|
|
3412
4771
|
export {
|
|
3413
4772
|
ALL_SDKS,
|
|
3414
4773
|
APL_VALUES,
|
|
3415
4774
|
APP_FEATURE_VALUES,
|
|
4775
|
+
AgcError,
|
|
3416
4776
|
CancelledError,
|
|
3417
4777
|
ChecksumMismatchError,
|
|
3418
4778
|
CmdToolsNotInstalledError,
|
|
3419
4779
|
CommandFailedError,
|
|
4780
|
+
HARMONYOS_SDK_VERSIONS,
|
|
4781
|
+
HUAWEI_SITES,
|
|
3420
4782
|
INSTALL_TMP_DIRNAME,
|
|
3421
4783
|
InsufficientSpaceError,
|
|
3422
4784
|
OHOS_URL_BASE,
|
|
3423
4785
|
OniroError,
|
|
4786
|
+
SIGNING_ERRORS,
|
|
3424
4787
|
SdkNotInstalledError,
|
|
3425
4788
|
TMP_DIR_HINT,
|
|
3426
4789
|
UnsupportedPlatformError,
|
|
@@ -3436,10 +4799,12 @@ export {
|
|
|
3436
4799
|
buildRawTouchCommand,
|
|
3437
4800
|
captureBurst,
|
|
3438
4801
|
consoleLogger,
|
|
4802
|
+
createHarmonyOsSession,
|
|
3439
4803
|
createInstallTempDir,
|
|
3440
4804
|
createMaterial,
|
|
3441
4805
|
createScaffold,
|
|
3442
4806
|
createTempWorkDir,
|
|
4807
|
+
createTokenStore,
|
|
3443
4808
|
decideInstallMethod,
|
|
3444
4809
|
decideReboot,
|
|
3445
4810
|
decryptPwd,
|
|
@@ -3448,6 +4813,7 @@ export {
|
|
|
3448
4813
|
defaultPaths,
|
|
3449
4814
|
detectAaStartFailure,
|
|
3450
4815
|
detectProjectSdkVersion,
|
|
4816
|
+
detectRuntimeOs,
|
|
3451
4817
|
detectSigningConfigNames,
|
|
3452
4818
|
diffEntryNames,
|
|
3453
4819
|
diffHapAssets,
|
|
@@ -3470,6 +4836,7 @@ export {
|
|
|
3470
4836
|
extractZipWithProgress,
|
|
3471
4837
|
findAppProcessId,
|
|
3472
4838
|
findCmdToolsSourceDir,
|
|
4839
|
+
findHarmonyOsSdk,
|
|
3473
4840
|
findRunningProcess,
|
|
3474
4841
|
forceStop,
|
|
3475
4842
|
formatBytes,
|
|
@@ -3494,6 +4861,8 @@ export {
|
|
|
3494
4861
|
getSdkFilename,
|
|
3495
4862
|
getSdkRootDir,
|
|
3496
4863
|
getSupportedSdksForUi,
|
|
4864
|
+
harmonyOsAutoSign,
|
|
4865
|
+
harmonyOsSdkLabelForApi,
|
|
3497
4866
|
hdcExec,
|
|
3498
4867
|
installApp,
|
|
3499
4868
|
installCmdTools,
|
|
@@ -3528,12 +4897,14 @@ export {
|
|
|
3528
4897
|
pruneLayout,
|
|
3529
4898
|
readJson5File,
|
|
3530
4899
|
readJsonFile,
|
|
4900
|
+
readProvisionProfile,
|
|
3531
4901
|
reboot,
|
|
3532
4902
|
recvFile,
|
|
3533
4903
|
removeCmdTools,
|
|
3534
4904
|
removeEmulator,
|
|
3535
4905
|
removeSdk,
|
|
3536
4906
|
removeTempWorkDir,
|
|
4907
|
+
requireHarmonyOsSdk,
|
|
3537
4908
|
resolveTmpRoot,
|
|
3538
4909
|
runCodelinter,
|
|
3539
4910
|
runHvigorw,
|