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