@oniroproject/core 0.6.1 → 0.7.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 +255 -7
- package/dist/index.cjs +1723 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +907 -21
- package/dist/index.d.ts +907 -21
- package/dist/index.js +1665 -274
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,15 @@ var consoleLogger = {
|
|
|
15
15
|
warn: (m) => console.warn(m),
|
|
16
16
|
error: (m) => console.error(m)
|
|
17
17
|
};
|
|
18
|
+
function scopedLogger(logger, scope) {
|
|
19
|
+
const prefix = `[${scope}] `;
|
|
20
|
+
return {
|
|
21
|
+
debug: (m) => logger.debug(prefix + m),
|
|
22
|
+
info: (m) => logger.info(prefix + m),
|
|
23
|
+
warn: (m) => logger.warn(prefix + m),
|
|
24
|
+
error: (m) => logger.error(prefix + m)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
18
27
|
|
|
19
28
|
// src/ports/progress.ts
|
|
20
29
|
var noopProgress = {
|
|
@@ -119,6 +128,19 @@ var CancelledError = class extends OniroError {
|
|
|
119
128
|
this.name = "CancelledError";
|
|
120
129
|
}
|
|
121
130
|
};
|
|
131
|
+
var CommandFailedError = class extends OniroError {
|
|
132
|
+
constructor(command, exitCode, stderr) {
|
|
133
|
+
super(`Command failed (exit ${exitCode}): ${command}${stderr ? `
|
|
134
|
+
${stderr}` : ""}`);
|
|
135
|
+
this.command = command;
|
|
136
|
+
this.exitCode = exitCode;
|
|
137
|
+
this.stderr = stderr;
|
|
138
|
+
this.name = "CommandFailedError";
|
|
139
|
+
}
|
|
140
|
+
command;
|
|
141
|
+
exitCode;
|
|
142
|
+
stderr;
|
|
143
|
+
};
|
|
122
144
|
|
|
123
145
|
// src/ports/context.ts
|
|
124
146
|
function defaultContext(overrides = {}) {
|
|
@@ -130,15 +152,16 @@ function defaultContext(overrides = {}) {
|
|
|
130
152
|
|
|
131
153
|
// src/sdk/constants.ts
|
|
132
154
|
var ALL_SDKS = [
|
|
133
|
-
{ version: "4.0", api: "10" },
|
|
134
|
-
{ version: "4.1", api: "11" },
|
|
135
|
-
{ version: "5.0.0", api: "12" },
|
|
136
|
-
{ version: "5.0.1", api: "13" },
|
|
137
|
-
{ version: "5.0.2", api: "14" },
|
|
138
|
-
{ version: "5.0.3", api: "15" },
|
|
139
|
-
{ version: "5.1.0", api: "18" },
|
|
140
|
-
{ version: "
|
|
141
|
-
{ version: "6.
|
|
155
|
+
{ version: "4.0", api: "10", tarballStrip: { linuxWindows: 1, darwin: 3 } },
|
|
156
|
+
{ version: "4.1", api: "11", tarballStrip: { linuxWindows: 1, darwin: 3 } },
|
|
157
|
+
{ version: "5.0.0", api: "12", tarballStrip: { linuxWindows: 0, darwin: 3 } },
|
|
158
|
+
{ version: "5.0.1", api: "13", tarballStrip: { linuxWindows: 0, darwin: 3 } },
|
|
159
|
+
{ version: "5.0.2", api: "14", tarballStrip: { linuxWindows: 1, darwin: 3 } },
|
|
160
|
+
{ version: "5.0.3", api: "15", tarballStrip: { linuxWindows: 1, darwin: 3 } },
|
|
161
|
+
{ version: "5.1.0", api: "18", tarballStrip: { linuxWindows: 1, darwin: 3 } },
|
|
162
|
+
{ version: "5.1.1", api: "19", tarballStrip: { linuxWindows: 1, darwin: 3 } },
|
|
163
|
+
{ version: "6.0", api: "20", tarballStrip: { linuxWindows: 0, darwin: 3 } },
|
|
164
|
+
{ version: "6.1", api: "23", tarballStrip: { linuxWindows: 0, darwin: 3 } }
|
|
142
165
|
];
|
|
143
166
|
var OHOS_URL_BASE = "https://repo.huaweicloud.com/openharmony/os";
|
|
144
167
|
|
|
@@ -154,19 +177,18 @@ function getOsFolder() {
|
|
|
154
177
|
function getSdkFilename(version) {
|
|
155
178
|
const platform6 = os2.platform();
|
|
156
179
|
const v = version ?? ALL_SDKS[ALL_SDKS.length - 1].version;
|
|
180
|
+
const strips = ALL_SDKS.find((r) => r.version === v)?.tarballStrip ?? { linuxWindows: 1, darwin: 3 };
|
|
157
181
|
if (platform6 === "linux") {
|
|
158
|
-
|
|
159
|
-
return { filename: "ohos-sdk-windows_linux-public.tar.gz", osFolder: "linux", strip };
|
|
182
|
+
return { filename: "ohos-sdk-windows_linux-public.tar.gz", osFolder: "linux", strip: strips.linuxWindows };
|
|
160
183
|
}
|
|
161
184
|
if (platform6 === "darwin") {
|
|
162
185
|
if (os2.arch() === "arm64") {
|
|
163
|
-
return { filename: "L2-SDK-MAC-M1-PUBLIC.tar.gz", osFolder: "darwin", strip:
|
|
186
|
+
return { filename: "L2-SDK-MAC-M1-PUBLIC.tar.gz", osFolder: "darwin", strip: strips.darwin };
|
|
164
187
|
}
|
|
165
|
-
return { filename: "ohos-sdk-mac-public.tar.gz", osFolder: "darwin", strip:
|
|
188
|
+
return { filename: "ohos-sdk-mac-public.tar.gz", osFolder: "darwin", strip: strips.darwin };
|
|
166
189
|
}
|
|
167
190
|
if (platform6 === "win32") {
|
|
168
|
-
|
|
169
|
-
return { filename: "ohos-sdk-windows_linux-public.tar.gz", osFolder: "windows", strip };
|
|
191
|
+
return { filename: "ohos-sdk-windows_linux-public.tar.gz", osFolder: "windows", strip: strips.linuxWindows };
|
|
170
192
|
}
|
|
171
193
|
throw new UnsupportedPlatformError(platform6);
|
|
172
194
|
}
|
|
@@ -193,17 +215,20 @@ function pickExisting(candidates) {
|
|
|
193
215
|
}
|
|
194
216
|
return candidates[0];
|
|
195
217
|
}
|
|
196
|
-
function getCmdToolsBin(config) {
|
|
218
|
+
function getCmdToolsBin(config, name = "ohpm") {
|
|
197
219
|
const binDir = path2.join(getCmdToolsPath(config), "bin");
|
|
198
220
|
if (os3.platform() === "win32") {
|
|
199
221
|
return pickExisting([
|
|
200
|
-
path2.join(binDir,
|
|
201
|
-
path2.join(binDir,
|
|
202
|
-
path2.join(binDir,
|
|
203
|
-
path2.join(binDir,
|
|
222
|
+
path2.join(binDir, `${name}.exe`),
|
|
223
|
+
path2.join(binDir, `${name}.cmd`),
|
|
224
|
+
path2.join(binDir, `${name}.bat`),
|
|
225
|
+
path2.join(binDir, name)
|
|
204
226
|
]);
|
|
205
227
|
}
|
|
206
|
-
return path2.join(binDir,
|
|
228
|
+
return path2.join(binDir, name);
|
|
229
|
+
}
|
|
230
|
+
function getOhpmPath(config) {
|
|
231
|
+
return getCmdToolsBin(config, "ohpm");
|
|
207
232
|
}
|
|
208
233
|
function getHdcPath(config) {
|
|
209
234
|
const base = path2.join(getCmdToolsPath(config), "sdk", "default", "openharmony", "toolchains");
|
|
@@ -218,15 +243,20 @@ function getHdcPath(config) {
|
|
|
218
243
|
return path2.join(base, "hdc");
|
|
219
244
|
}
|
|
220
245
|
function getHvigorwPath(config, projectDir) {
|
|
221
|
-
const
|
|
222
|
-
return pickExisting([
|
|
246
|
+
const local = pickExisting([
|
|
223
247
|
path2.join(projectDir, "hvigorw"),
|
|
224
248
|
path2.join(projectDir, "hvigorw.bat"),
|
|
225
|
-
path2.join(projectDir, "hvigorw.cmd")
|
|
226
|
-
path2.join(cmdToolsBin, "hvigorw"),
|
|
227
|
-
path2.join(cmdToolsBin, "hvigorw.bat"),
|
|
228
|
-
path2.join(cmdToolsBin, "hvigorw.cmd")
|
|
249
|
+
path2.join(projectDir, "hvigorw.cmd")
|
|
229
250
|
]);
|
|
251
|
+
if (fs.existsSync(local) && isProjectLocalHvigorwWorking(projectDir)) {
|
|
252
|
+
return local;
|
|
253
|
+
}
|
|
254
|
+
return getCmdToolsBin(config, "hvigorw");
|
|
255
|
+
}
|
|
256
|
+
function isProjectLocalHvigorwWorking(projectDir) {
|
|
257
|
+
const wrapper = path2.join(projectDir, "hvigor", "hvigor-wrapper.js");
|
|
258
|
+
const nodeModules = path2.join(projectDir, "hvigor", "node_modules");
|
|
259
|
+
return fs.existsSync(wrapper) && fs.existsSync(nodeModules);
|
|
230
260
|
}
|
|
231
261
|
|
|
232
262
|
// src/sdk/detectProjectSdk.ts
|
|
@@ -482,22 +512,37 @@ function removeSdk(config, api) {
|
|
|
482
512
|
}
|
|
483
513
|
|
|
484
514
|
// src/sdk/install.ts
|
|
485
|
-
import * as
|
|
515
|
+
import * as fs8 from "fs";
|
|
486
516
|
import * as os5 from "os";
|
|
487
517
|
import * as path7 from "path";
|
|
518
|
+
|
|
519
|
+
// src/sdk/move.ts
|
|
520
|
+
import * as fs7 from "fs";
|
|
521
|
+
function movePath(src, dest) {
|
|
522
|
+
try {
|
|
523
|
+
fs7.renameSync(src, dest);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
if (err.code !== "EXDEV") throw err;
|
|
526
|
+
fs7.cpSync(src, dest, { recursive: true });
|
|
527
|
+
fs7.rmSync(src, { recursive: true, force: true });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/sdk/install.ts
|
|
488
532
|
async function downloadAndInstallSdk(opts) {
|
|
489
533
|
const { config, version, api, progress, abortSignal } = opts;
|
|
490
534
|
const logger = opts.logger ?? noopLogger;
|
|
491
535
|
const { filename, osFolder, strip } = getSdkFilename(version);
|
|
492
|
-
const
|
|
536
|
+
const urlBase = config.get("sdkUrlBase", OHOS_URL_BASE);
|
|
537
|
+
const downloadUrl = `${urlBase}/${version}-Release/${filename}`;
|
|
493
538
|
const sha256Url = `${downloadUrl}.sha256`;
|
|
494
|
-
const tmpDir =
|
|
539
|
+
const tmpDir = fs8.mkdtempSync(path7.join(os5.tmpdir(), "oniro-sdk-"));
|
|
495
540
|
const tarPath = path7.join(tmpDir, filename);
|
|
496
541
|
const sha256Path = path7.join(tmpDir, `${filename}.sha256`);
|
|
497
542
|
const extractDir = path7.join(tmpDir, "extract");
|
|
498
|
-
|
|
543
|
+
fs8.mkdirSync(extractDir);
|
|
499
544
|
const sdkInstallDir = path7.join(getSdkRootDir(config), osFolder, api);
|
|
500
|
-
|
|
545
|
+
fs8.mkdirSync(path7.dirname(sdkInstallDir), { recursive: true });
|
|
501
546
|
const checkCancelled = () => {
|
|
502
547
|
if (abortSignal?.aborted) throw new CancelledError();
|
|
503
548
|
};
|
|
@@ -515,12 +560,12 @@ async function downloadAndInstallSdk(opts) {
|
|
|
515
560
|
progress?.report({ message: "Extracting SDK (this may take a while)...", increment: 10 });
|
|
516
561
|
checkCancelled();
|
|
517
562
|
const osContentPath = path7.join(extractDir, osFolder);
|
|
518
|
-
if (!
|
|
563
|
+
if (!fs8.existsSync(osContentPath)) {
|
|
519
564
|
throw new OniroError(
|
|
520
565
|
`Expected folder '${osFolder}' not found in extracted SDK. Tarball layout may have changed.`
|
|
521
566
|
);
|
|
522
567
|
}
|
|
523
|
-
const zipFiles =
|
|
568
|
+
const zipFiles = fs8.readdirSync(osContentPath).filter((n2) => n2.endsWith(".zip"));
|
|
524
569
|
const componentsStart = 60;
|
|
525
570
|
const componentsBudget = 35;
|
|
526
571
|
const n = zipFiles.length;
|
|
@@ -540,26 +585,26 @@ async function downloadAndInstallSdk(opts) {
|
|
|
540
585
|
if (rem > 0) rem--;
|
|
541
586
|
await extractZipWithProgress({ zipPath, dest: osContentPath, progress, start: cursor, range: thisBudget, logger });
|
|
542
587
|
cursor += thisBudget;
|
|
543
|
-
|
|
588
|
+
fs8.unlinkSync(zipPath);
|
|
544
589
|
}
|
|
545
590
|
progress?.report({ message: "Finalizing installation...", increment: 0 });
|
|
546
|
-
if (
|
|
547
|
-
|
|
591
|
+
if (fs8.existsSync(sdkInstallDir)) {
|
|
592
|
+
fs8.rmSync(sdkInstallDir, { recursive: true, force: true });
|
|
548
593
|
}
|
|
549
|
-
|
|
594
|
+
movePath(osContentPath, sdkInstallDir);
|
|
550
595
|
progress?.report({ message: "Finalizing installation...", increment: 3 });
|
|
551
596
|
progress?.report({ message: "Cleaning up...", increment: 0 });
|
|
552
|
-
|
|
597
|
+
fs8.rmSync(tmpDir, { recursive: true, force: true });
|
|
553
598
|
progress?.report({ message: "Cleaning up...", increment: 2 });
|
|
554
599
|
} catch (err) {
|
|
555
600
|
logger.error(`SDK install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
556
|
-
|
|
601
|
+
fs8.rmSync(tmpDir, { recursive: true, force: true });
|
|
557
602
|
throw err;
|
|
558
603
|
}
|
|
559
604
|
}
|
|
560
605
|
|
|
561
606
|
// src/cmdTools/install.ts
|
|
562
|
-
import * as
|
|
607
|
+
import * as fs9 from "fs";
|
|
563
608
|
import * as os6 from "os";
|
|
564
609
|
import * as path8 from "path";
|
|
565
610
|
import { execFileSync } from "child_process";
|
|
@@ -594,11 +639,11 @@ function findCmdToolsSourceDir(extractPath) {
|
|
|
594
639
|
path8.join(extractPath, "commandline-tools")
|
|
595
640
|
];
|
|
596
641
|
for (const c of known) {
|
|
597
|
-
if (
|
|
642
|
+
if (fs9.existsSync(c)) return c;
|
|
598
643
|
}
|
|
599
|
-
const entries =
|
|
644
|
+
const entries = fs9.readdirSync(extractPath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path8.join(extractPath, e.name));
|
|
600
645
|
for (const dir of entries) {
|
|
601
|
-
if (
|
|
646
|
+
if (fs9.existsSync(path8.join(dir, "bin"))) return dir;
|
|
602
647
|
}
|
|
603
648
|
throw new OniroError("Could not locate command line tools folder in the extracted archive.");
|
|
604
649
|
}
|
|
@@ -606,7 +651,7 @@ async function installCmdTools(opts) {
|
|
|
606
651
|
const { config, progress, abortSignal } = opts;
|
|
607
652
|
const logger = opts.logger ?? noopLogger;
|
|
608
653
|
const CMD_PATH = getCmdToolsPath(config);
|
|
609
|
-
const tmpDir =
|
|
654
|
+
const tmpDir = fs9.mkdtempSync(path8.join(os6.tmpdir(), "oniro-cmdtools-"));
|
|
610
655
|
const zipPath = opts.localZipPath ?? path8.join(tmpDir, "oh-command-line-tools.zip");
|
|
611
656
|
const extractPath = path8.join(tmpDir, "oh-command-line-tools");
|
|
612
657
|
try {
|
|
@@ -617,42 +662,42 @@ async function installCmdTools(opts) {
|
|
|
617
662
|
}
|
|
618
663
|
progress?.report({ message: "Extracting tools...", increment: 0 });
|
|
619
664
|
await extractZipWithProgress({ zipPath, dest: extractPath, progress, start: 50, range: 45, logger });
|
|
620
|
-
|
|
665
|
+
fs9.mkdirSync(CMD_PATH, { recursive: true });
|
|
621
666
|
const srcDir = findCmdToolsSourceDir(extractPath);
|
|
622
|
-
for (const entry of
|
|
667
|
+
for (const entry of fs9.readdirSync(srcDir)) {
|
|
623
668
|
const src = path8.join(srcDir, entry);
|
|
624
669
|
const dest = path8.join(CMD_PATH, entry);
|
|
625
|
-
if (
|
|
626
|
-
if (
|
|
627
|
-
|
|
670
|
+
if (fs9.statSync(src).isDirectory()) {
|
|
671
|
+
if (fs9.existsSync(dest)) fs9.rmSync(dest, { recursive: true, force: true });
|
|
672
|
+
movePath(src, dest);
|
|
628
673
|
} else {
|
|
629
|
-
|
|
674
|
+
fs9.copyFileSync(src, dest);
|
|
630
675
|
}
|
|
631
676
|
}
|
|
632
677
|
const binDir = path8.join(CMD_PATH, "bin");
|
|
633
|
-
if (
|
|
634
|
-
for (const file of
|
|
635
|
-
|
|
678
|
+
if (fs9.existsSync(binDir) && os6.platform() !== "win32") {
|
|
679
|
+
for (const file of fs9.readdirSync(binDir)) {
|
|
680
|
+
fs9.chmodSync(path8.join(binDir, file), 493);
|
|
636
681
|
}
|
|
637
682
|
}
|
|
638
683
|
progress?.report({ message: "Finalizing installation...", increment: 5 });
|
|
639
684
|
progress?.report({ message: "Cleaning up...", increment: 0 });
|
|
640
685
|
} finally {
|
|
641
|
-
|
|
686
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
642
687
|
}
|
|
643
688
|
}
|
|
644
689
|
function getCmdToolsStatus(config) {
|
|
645
690
|
const cmdPath = getCmdToolsPath(config);
|
|
646
691
|
const binDir = path8.join(cmdPath, "bin");
|
|
647
692
|
const candidates = os6.platform() === "win32" ? ["ohpm.exe", "ohpm.cmd", "ohpm.bat", "ohpm"] : ["ohpm"];
|
|
648
|
-
const bin = candidates.map((c) => path8.join(binDir, c)).find((p) =>
|
|
693
|
+
const bin = candidates.map((c) => path8.join(binDir, c)).find((p) => fs9.existsSync(p));
|
|
649
694
|
if (!bin) {
|
|
650
695
|
return { installed: false, status: "Not installed" };
|
|
651
696
|
}
|
|
652
697
|
try {
|
|
653
698
|
const versionFile = path8.join(cmdPath, "version.txt");
|
|
654
|
-
if (
|
|
655
|
-
const content =
|
|
699
|
+
if (fs9.existsSync(versionFile)) {
|
|
700
|
+
const content = fs9.readFileSync(versionFile, "utf8");
|
|
656
701
|
for (const line of content.split(/\r?\n/)) {
|
|
657
702
|
const m = line.match(/^\s*#\s*Version:\s*(.+)$/);
|
|
658
703
|
if (m?.[1]) return { installed: true, status: `Installed (${m[1].trim()})` };
|
|
@@ -672,13 +717,13 @@ function isCmdToolsInstalled(config) {
|
|
|
672
717
|
}
|
|
673
718
|
function removeCmdTools(config) {
|
|
674
719
|
const cmdPath = getCmdToolsPath(config);
|
|
675
|
-
if (
|
|
676
|
-
|
|
720
|
+
if (fs9.existsSync(cmdPath)) {
|
|
721
|
+
fs9.rmSync(cmdPath, { recursive: true, force: true });
|
|
677
722
|
}
|
|
678
723
|
}
|
|
679
724
|
|
|
680
725
|
// src/sign/encryptKey.ts
|
|
681
|
-
import * as
|
|
726
|
+
import * as fs10 from "fs";
|
|
682
727
|
import * as path9 from "path";
|
|
683
728
|
import * as crypto2 from "crypto";
|
|
684
729
|
var DAEMON_ROOT_KEY_COMPONENT_LENGTH = 16;
|
|
@@ -752,8 +797,8 @@ function createAndStoreKey(dir, keyLength) {
|
|
|
752
797
|
const key = crypto2.randomBytes(keyLength);
|
|
753
798
|
const hash = crypto2.createHash("sha256").update(key).digest("hex");
|
|
754
799
|
const filePath = path9.join(dir, hash);
|
|
755
|
-
|
|
756
|
-
|
|
800
|
+
fs10.writeFileSync(filePath, key);
|
|
801
|
+
fs10.chmodSync(filePath, KEY_FILE_PERMISSIONS);
|
|
757
802
|
return key;
|
|
758
803
|
}
|
|
759
804
|
function createAndStoreEnKey(rootKey, ceDir) {
|
|
@@ -761,23 +806,23 @@ function createAndStoreEnKey(rootKey, ceDir) {
|
|
|
761
806
|
const encrypted = encrypt(rootKey, workKey);
|
|
762
807
|
const hash = crypto2.createHash("sha256").update(encrypted).digest("hex");
|
|
763
808
|
const filePath = path9.join(ceDir, hash);
|
|
764
|
-
|
|
765
|
-
|
|
809
|
+
fs10.writeFileSync(filePath, encrypted);
|
|
810
|
+
fs10.chmodSync(filePath, KEY_FILE_PERMISSIONS);
|
|
766
811
|
}
|
|
767
812
|
function createMaterial(materialPath) {
|
|
768
|
-
|
|
769
|
-
|
|
813
|
+
fs10.mkdirSync(materialPath, { recursive: true });
|
|
814
|
+
fs10.chmodSync(materialPath, KEY_FILE_DIRECTORY_PERMISSIONS);
|
|
770
815
|
const fdDir = path9.join(materialPath, "fd");
|
|
771
816
|
const acDir = path9.join(materialPath, "ac");
|
|
772
817
|
const ceDir = path9.join(materialPath, "ce");
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
818
|
+
fs10.mkdirSync(fdDir, { recursive: true });
|
|
819
|
+
fs10.mkdirSync(acDir, { recursive: true });
|
|
820
|
+
fs10.mkdirSync(ceDir, { recursive: true });
|
|
776
821
|
const fdSubDirs = ["0", "1", "2"];
|
|
777
822
|
const fdComponents = [];
|
|
778
823
|
for (const sub of fdSubDirs) {
|
|
779
824
|
const subDir = path9.join(fdDir, sub);
|
|
780
|
-
|
|
825
|
+
fs10.mkdirSync(subDir, { recursive: true });
|
|
781
826
|
const comp = createAndStoreKey(subDir, DAEMON_ROOT_KEY_COMPONENT_LENGTH);
|
|
782
827
|
fdComponents.push(comp);
|
|
783
828
|
}
|
|
@@ -786,17 +831,17 @@ function createMaterial(materialPath) {
|
|
|
786
831
|
createAndStoreEnKey(rootKey, ceDir);
|
|
787
832
|
}
|
|
788
833
|
function readSingleFile(dir) {
|
|
789
|
-
if (!
|
|
834
|
+
if (!fs10.existsSync(dir) || !fs10.statSync(dir).isDirectory()) {
|
|
790
835
|
throw new Error(`Missing signing material directory: ${dir}`);
|
|
791
836
|
}
|
|
792
|
-
const files =
|
|
837
|
+
const files = fs10.readdirSync(dir).filter((f) => f !== ".DS_Store");
|
|
793
838
|
if (files.length !== 1) {
|
|
794
839
|
throw new Error(`Signing material in ${dir} is illegal (expected exactly one file).`);
|
|
795
840
|
}
|
|
796
|
-
return
|
|
841
|
+
return fs10.readFileSync(path9.join(dir, files[0]));
|
|
797
842
|
}
|
|
798
843
|
function getKey(materialPath) {
|
|
799
|
-
if (!
|
|
844
|
+
if (!fs10.existsSync(materialPath) || !fs10.statSync(materialPath).isDirectory()) {
|
|
800
845
|
throw new Error("Material directory does not exist.");
|
|
801
846
|
}
|
|
802
847
|
const fdDir = path9.join(materialPath, "fd");
|
|
@@ -818,41 +863,120 @@ function decryptPwd(encryptedHex, materialPath) {
|
|
|
818
863
|
}
|
|
819
864
|
|
|
820
865
|
// src/sign/generateSigningConfigs.ts
|
|
821
|
-
import * as
|
|
866
|
+
import * as fs11 from "fs";
|
|
822
867
|
import * as path10 from "path";
|
|
823
868
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
824
869
|
import JSON52 from "json5";
|
|
870
|
+
|
|
871
|
+
// src/sign/applicationCert.ts
|
|
872
|
+
var OPEN_HARMONY_APPLICATION_CERT = `-----BEGIN CERTIFICATE-----
|
|
873
|
+
MIICRDCCAcmgAwIBAgIED+E4izAMBggqhkjOPQQDAwUAMGgxCzAJBgNVBAYTAkNO
|
|
874
|
+
MRQwEgYDVQQKEwtPcGVuSGFybW9ueTEZMBcGA1UECxMQT3Blbkhhcm1vbnkgVGVh
|
|
875
|
+
bTEoMCYGA1UEAxMfT3Blbkhhcm1vbnkgQXBwbGljYXRpb24gUm9vdCBDQTAeFw0y
|
|
876
|
+
MTAyMDIxMjE0MThaFw00OTEyMzExMjE0MThaMGgxCzAJBgNVBAYTAkNOMRQwEgYD
|
|
877
|
+
VQQKEwtPcGVuSGFybW9ueTEZMBcGA1UECxMQT3Blbkhhcm1vbnkgVGVhbTEoMCYG
|
|
878
|
+
A1UEAxMfT3Blbkhhcm1vbnkgQXBwbGljYXRpb24gUm9vdCBDQTB2MBAGByqGSM49
|
|
879
|
+
AgEGBSuBBAAiA2IABE023XmRaw2DnO8NSsb+KG/uY0FtS3u5LQucdr3qWVnRW5ui
|
|
880
|
+
QIL6ttNZBEeLTUeYcJZCpayg9Llf+1SmDA7dY4iP2EcRo4UN3rilovtfFfsmH4ty
|
|
881
|
+
3SApHVFzWUl+NwdH8KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
|
|
882
|
+
AQYwHQYDVR0OBBYEFBc6EKGrGXzlAE+s0Zgnsphadw7NMAwGCCqGSM49BAMDBQAD
|
|
883
|
+
ZwAwZAIwd1p3JzHN93eoPped1li0j64npgqNzwy4OrkehYAqNXpcpaEcLZ7UxW8E
|
|
884
|
+
I2lZJ3SbAjAkqySHb12sIwdSFKSN9KCMMEo/eUT5dUXlcKR2nZz0MJdxT5F51qcX
|
|
885
|
+
1CumzkcYhgU=
|
|
886
|
+
-----END CERTIFICATE-----
|
|
887
|
+
-----BEGIN CERTIFICATE-----
|
|
888
|
+
MIICYTCCAeWgAwIBAgIEHmXAPTAMBggqhkjOPQQDAwUAMGgxCzAJBgNVBAYTAkNO
|
|
889
|
+
MRQwEgYDVQQKEwtPcGVuSGFybW9ueTEZMBcGA1UECxMQT3Blbkhhcm1vbnkgVGVh
|
|
890
|
+
bTEoMCYGA1UEAxMfT3Blbkhhcm1vbnkgQXBwbGljYXRpb24gUm9vdCBDQTAeFw0y
|
|
891
|
+
MTAyMDIxMjE1MzJaFw00OTEyMzExMjE1MzJaMGMxCzAJBgNVBAYTAkNOMRQwEgYD
|
|
892
|
+
VQQKEwtPcGVuSGFybW9ueTEZMBcGA1UECxMQT3Blbkhhcm1vbnkgVGVhbTEjMCEG
|
|
893
|
+
A1UEAxMaT3Blbkhhcm1vbnkgQXBwbGljYXRpb24gQ0EwdjAQBgcqhkjOPQIBBgUr
|
|
894
|
+
gQQAIgNiAAQhnu7Hna8XNa2KyqRf5+lBJScE4xqf89N0g0OuqAb2re8nGsvWkw26
|
|
895
|
+
uDekfnBYicd+G3Cydqa2zFIwV7Talyg2ULW3r8KbGpyl84mJEPPRmCGJ+H9gtCsf
|
|
896
|
+
+OrJ4Y76LVWjYzBhMB8GA1UdIwQYMBaAFBc6EKGrGXzlAE+s0Zgnsphadw7NMA8G
|
|
897
|
+
A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTbhrciFtUL
|
|
898
|
+
oUu33SV7ufEFfaItRzAMBggqhkjOPQQDAwUAA2gAMGUCMG3cXjiDmXTvf7D4Omhf
|
|
899
|
+
qcc2nuO+EMfWE+N9ZhBP5UhV34mAGWi3SfLU6rcV0urWEQIxAMYIb3epOnKhUrcm
|
|
900
|
+
Lfu1WKzFlpYQwmw73RaCHP2I3k6NcuWOYeNwWXSNZ8o0nzvaLg==
|
|
901
|
+
-----END CERTIFICATE-----
|
|
902
|
+
-----BEGIN CERTIFICATE-----
|
|
903
|
+
MIICMzCCAbegAwIBAgIEaOC/zDAMBggqhkjOPQQDAwUAMGMxCzAJBgNVBAYTAkNO
|
|
904
|
+
MRQwEgYDVQQKEwtPcGVuSGFybW9ueTEZMBcGA1UECxMQT3Blbkhhcm1vbnkgVGVh
|
|
905
|
+
bTEjMCEGA1UEAxMaT3Blbkhhcm1vbnkgQXBwbGljYXRpb24gQ0EwHhcNMjEwMjAy
|
|
906
|
+
MTIxOTMxWhcNNDkxMjMxMTIxOTMxWjBoMQswCQYDVQQGEwJDTjEUMBIGA1UEChML
|
|
907
|
+
T3Blbkhhcm1vbnkxGTAXBgNVBAsTEE9wZW5IYXJtb255IFRlYW0xKDAmBgNVBAMT
|
|
908
|
+
H09wZW5IYXJtb255IEFwcGxpY2F0aW9uIFJlbGVhc2UwWTATBgcqhkjOPQIBBggq
|
|
909
|
+
hkjOPQMBBwNCAATbYOCQQpW5fdkYHN45v0X3AHax12jPBdEDosFRIZ1eXmxOYzSG
|
|
910
|
+
JwMfsHhUU90E8lI0TXYZnNmgM1sovubeQqATo1IwUDAfBgNVHSMEGDAWgBTbhrci
|
|
911
|
+
FtULoUu33SV7ufEFfaItRzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFPtxruhl
|
|
912
|
+
cRBQsJdwcZqLu9oNUVgaMAwGCCqGSM49BAMDBQADaAAwZQIxAJta0PQ2p4DIu/ps
|
|
913
|
+
LMdLCDgQ5UH1l0B4PGhBlMgdi2zf8nk9spazEQI/0XNwpft8QAIwHSuA2WelVi/o
|
|
914
|
+
zAlF08DnbJrOOtOnQq5wHOPlDYB4OtUzOYJk9scotrEnJxJzGsh/
|
|
915
|
+
-----END CERTIFICATE-----
|
|
916
|
+
`;
|
|
917
|
+
|
|
918
|
+
// src/sign/generateSigningConfigs.ts
|
|
825
919
|
var APL_VALUES = ["normal", "system_basic", "system_core"];
|
|
826
920
|
var APP_FEATURE_VALUES = ["hos_normal_app", "hos_system_app"];
|
|
827
|
-
|
|
921
|
+
var DEFAULT_PASSWORDS = { store: "123456", key: "123456" };
|
|
922
|
+
function pickSigningKind(apl) {
|
|
923
|
+
return apl === "normal" ? "profile-release" : "application-release";
|
|
924
|
+
}
|
|
925
|
+
var SIGNING_KIND_CONFIG = {
|
|
926
|
+
"profile-release": {
|
|
927
|
+
hapKeyAlias: "openharmony application profile release",
|
|
928
|
+
certBasename: "OpenHarmonyProfileRelease.pem"
|
|
929
|
+
},
|
|
930
|
+
"application-release": {
|
|
931
|
+
hapKeyAlias: "OpenHarmony Application Release",
|
|
932
|
+
certBasename: "OpenHarmonyApplication.cer"
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
function copyFilesToProject(projectDir, paths, kind, logger, applicationCertPath) {
|
|
828
936
|
logger.info("[sign] Copying signing material into project...");
|
|
829
937
|
const signaturesDir = path10.join(projectDir, "signatures");
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
938
|
+
fs11.mkdirSync(signaturesDir, { recursive: true });
|
|
939
|
+
fs11.copyFileSync(paths.keystore, path10.join(signaturesDir, "OpenHarmony.p12"));
|
|
940
|
+
fs11.copyFileSync(paths.profileCert, path10.join(signaturesDir, "OpenHarmonyProfileRelease.pem"));
|
|
941
|
+
fs11.copyFileSync(paths.unsignedProfileTemplate, path10.join(signaturesDir, "UnsgnedReleasedProfileTemplate.json"));
|
|
942
|
+
if (kind === "application-release") {
|
|
943
|
+
const dest = path10.join(signaturesDir, "OpenHarmonyApplication.cer");
|
|
944
|
+
if (applicationCertPath) {
|
|
945
|
+
fs11.copyFileSync(applicationCertPath, dest);
|
|
946
|
+
} else {
|
|
947
|
+
fs11.writeFileSync(dest, OPEN_HARMONY_APPLICATION_CERT);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
834
950
|
}
|
|
835
951
|
function resolveAppFeature(apl, override) {
|
|
836
952
|
if (override) return override;
|
|
837
953
|
return apl === "normal" ? "hos_normal_app" : "hos_system_app";
|
|
838
954
|
}
|
|
839
|
-
function
|
|
840
|
-
|
|
955
|
+
function extractLeafCertificate(chainPath) {
|
|
956
|
+
const certContent = fs11.readFileSync(chainPath, "utf-8");
|
|
957
|
+
const parts = certContent.split("-----END CERTIFICATE-----");
|
|
958
|
+
if (parts.length < 3) {
|
|
959
|
+
throw new OniroError(`${chainPath} does not contain enough certificates.`);
|
|
960
|
+
}
|
|
961
|
+
return parts[2].trim() + "\n-----END CERTIFICATE-----\n";
|
|
962
|
+
}
|
|
963
|
+
function modifyProfileTemplate(projectDir, apl, appFeature, logger, options = {}) {
|
|
964
|
+
const kind = options.kind ?? pickSigningKind(apl);
|
|
965
|
+
logger.info(`[sign] Modifying profile template (apl=${apl}, app-feature=${appFeature}, kind=${kind})...`);
|
|
841
966
|
const appJsonPath = path10.join(projectDir, "AppScope/app.json5");
|
|
842
967
|
const profileTemplatePath = path10.join(projectDir, "signatures/UnsgnedReleasedProfileTemplate.json");
|
|
843
|
-
|
|
844
|
-
if (!fs10.existsSync(appJsonPath)) {
|
|
968
|
+
if (!fs11.existsSync(appJsonPath)) {
|
|
845
969
|
throw new OniroError(`${appJsonPath} does not exist.`);
|
|
846
970
|
}
|
|
847
971
|
let appJson;
|
|
848
972
|
try {
|
|
849
|
-
appJson = JSON52.parse(
|
|
973
|
+
appJson = JSON52.parse(fs11.readFileSync(appJsonPath, "utf-8"));
|
|
850
974
|
} catch (e) {
|
|
851
975
|
throw new OniroError(`Error parsing ${appJsonPath}: ${e.message}`, e);
|
|
852
976
|
}
|
|
853
977
|
let profileTemplate;
|
|
854
978
|
try {
|
|
855
|
-
profileTemplate = JSON.parse(
|
|
979
|
+
profileTemplate = JSON.parse(fs11.readFileSync(profileTemplatePath, "utf-8"));
|
|
856
980
|
} catch (e) {
|
|
857
981
|
throw new OniroError(`Error parsing ${profileTemplatePath}: ${e.message}`, e);
|
|
858
982
|
}
|
|
@@ -865,16 +989,19 @@ function modifyProfileTemplate(projectDir, apl, appFeature, logger) {
|
|
|
865
989
|
profileTemplate["bundle-info"]["bundle-name"] = appJson.app.bundleName;
|
|
866
990
|
profileTemplate["bundle-info"]["apl"] = apl;
|
|
867
991
|
profileTemplate["bundle-info"]["app-feature"] = appFeature;
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
992
|
+
const distCertChainPath = path10.join(
|
|
993
|
+
projectDir,
|
|
994
|
+
"signatures",
|
|
995
|
+
SIGNING_KIND_CONFIG[kind].certBasename
|
|
996
|
+
);
|
|
997
|
+
profileTemplate["bundle-info"]["distribution-certificate"] = extractLeafCertificate(distCertChainPath);
|
|
998
|
+
if (options.acls !== void 0) {
|
|
999
|
+
profileTemplate.acls = profileTemplate.acls ?? {};
|
|
1000
|
+
profileTemplate.acls["allowed-acls"] = options.acls;
|
|
872
1001
|
}
|
|
873
|
-
|
|
874
|
-
profileTemplate["bundle-info"]["distribution-certificate"] = thirdCert;
|
|
875
|
-
fs10.writeFileSync(profileTemplatePath, JSON.stringify(profileTemplate, null, 2));
|
|
1002
|
+
fs11.writeFileSync(profileTemplatePath, JSON.stringify(profileTemplate, null, 2));
|
|
876
1003
|
}
|
|
877
|
-
function generateP7bFile(projectDir, paths, logger) {
|
|
1004
|
+
function generateP7bFile(projectDir, paths, logger, passwords) {
|
|
878
1005
|
logger.info("[sign] Generating P7b profile via hap-sign-tool...");
|
|
879
1006
|
const signaturesDir = path10.join(projectDir, "signatures");
|
|
880
1007
|
const profileTemplatePath = path10.join(signaturesDir, "UnsgnedReleasedProfileTemplate.json");
|
|
@@ -898,9 +1025,9 @@ function generateP7bFile(projectDir, paths, logger) {
|
|
|
898
1025
|
"-outFile",
|
|
899
1026
|
outputProfilePath,
|
|
900
1027
|
"-keyPwd",
|
|
901
|
-
|
|
1028
|
+
passwords.key,
|
|
902
1029
|
"-keystorePwd",
|
|
903
|
-
|
|
1030
|
+
passwords.store
|
|
904
1031
|
];
|
|
905
1032
|
try {
|
|
906
1033
|
execFileSync2("java", args, { stdio: "inherit" });
|
|
@@ -911,42 +1038,63 @@ function generateP7bFile(projectDir, paths, logger) {
|
|
|
911
1038
|
);
|
|
912
1039
|
}
|
|
913
1040
|
}
|
|
914
|
-
function
|
|
1041
|
+
function detectSigningConfigNames(projectDir) {
|
|
1042
|
+
const buildProfilePath = path10.join(projectDir, "build-profile.json5");
|
|
1043
|
+
if (!fs11.existsSync(buildProfilePath)) return ["default"];
|
|
1044
|
+
let parsed;
|
|
1045
|
+
try {
|
|
1046
|
+
parsed = JSON52.parse(fs11.readFileSync(buildProfilePath, "utf-8"));
|
|
1047
|
+
} catch {
|
|
1048
|
+
return ["default"];
|
|
1049
|
+
}
|
|
1050
|
+
const products = parsed.app?.products;
|
|
1051
|
+
if (!Array.isArray(products) || products.length === 0) return ["default"];
|
|
1052
|
+
const names = [];
|
|
1053
|
+
for (const product of products) {
|
|
1054
|
+
const name = product.signingConfig;
|
|
1055
|
+
if (typeof name === "string" && name.length > 0 && !names.includes(name)) {
|
|
1056
|
+
names.push(name);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return names.length > 0 ? names : ["default"];
|
|
1060
|
+
}
|
|
1061
|
+
function updateBuildProfile(projectDir, kind, logger, passwords) {
|
|
915
1062
|
logger.info("[sign] Writing signing configs into build-profile.json5...");
|
|
916
1063
|
const materialDir = path10.join(projectDir, "signatures", "material");
|
|
917
1064
|
const buildProfilePath = path10.join(projectDir, "build-profile.json5");
|
|
918
|
-
const encryptedStorePassword = encryptPwd(
|
|
919
|
-
const encryptedKeyPassword = encryptPwd(
|
|
1065
|
+
const encryptedStorePassword = encryptPwd(passwords.store, materialDir);
|
|
1066
|
+
const encryptedKeyPassword = encryptPwd(passwords.key, materialDir);
|
|
920
1067
|
let buildProfile = { app: {} };
|
|
921
|
-
if (
|
|
1068
|
+
if (fs11.existsSync(buildProfilePath)) {
|
|
922
1069
|
try {
|
|
923
|
-
buildProfile = JSON52.parse(
|
|
1070
|
+
buildProfile = JSON52.parse(fs11.readFileSync(buildProfilePath, "utf-8"));
|
|
924
1071
|
} catch (e) {
|
|
925
1072
|
throw new OniroError(`Error parsing ${buildProfilePath}: ${e.message}`, e);
|
|
926
1073
|
}
|
|
927
1074
|
}
|
|
1075
|
+
const { hapKeyAlias, certBasename } = SIGNING_KIND_CONFIG[kind];
|
|
1076
|
+
const configNames = detectSigningConfigNames(projectDir);
|
|
1077
|
+
logger.info(`[sign] Writing signingConfigs entries: ${configNames.join(", ")}`);
|
|
928
1078
|
buildProfile.app = buildProfile.app ?? {};
|
|
929
|
-
buildProfile.app.signingConfigs =
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
storeFile: "./signatures/OpenHarmony.p12"
|
|
940
|
-
}
|
|
1079
|
+
buildProfile.app.signingConfigs = configNames.map((name) => ({
|
|
1080
|
+
name,
|
|
1081
|
+
material: {
|
|
1082
|
+
certpath: `./signatures/${certBasename}`,
|
|
1083
|
+
storePassword: encryptedStorePassword,
|
|
1084
|
+
keyAlias: hapKeyAlias,
|
|
1085
|
+
keyPassword: encryptedKeyPassword,
|
|
1086
|
+
profile: "./signatures/app1-profile.p7b",
|
|
1087
|
+
signAlg: "SHA256withECDSA",
|
|
1088
|
+
storeFile: "./signatures/OpenHarmony.p12"
|
|
941
1089
|
}
|
|
942
|
-
|
|
943
|
-
|
|
1090
|
+
}));
|
|
1091
|
+
fs11.writeFileSync(buildProfilePath, JSON.stringify(buildProfile, null, 2));
|
|
944
1092
|
}
|
|
945
1093
|
function prepareMaterialDirectory(projectDir, logger) {
|
|
946
1094
|
logger.info("[sign] Generating fresh signing material...");
|
|
947
1095
|
const materialDir = path10.join(projectDir, "signatures", "material");
|
|
948
|
-
if (
|
|
949
|
-
|
|
1096
|
+
if (fs11.existsSync(materialDir)) {
|
|
1097
|
+
fs11.rmSync(materialDir, { recursive: true, force: true });
|
|
950
1098
|
}
|
|
951
1099
|
createMaterial(materialDir);
|
|
952
1100
|
}
|
|
@@ -970,7 +1118,7 @@ function generateSigningConfigs(options) {
|
|
|
970
1118
|
);
|
|
971
1119
|
}
|
|
972
1120
|
const sdkPath = path10.join(sdkHome, String(sdkVersion));
|
|
973
|
-
if (!
|
|
1121
|
+
if (!fs11.existsSync(sdkPath)) {
|
|
974
1122
|
throw new OniroError(`SDK path does not exist: ${sdkPath}`);
|
|
975
1123
|
}
|
|
976
1124
|
const paths = {
|
|
@@ -979,17 +1127,19 @@ function generateSigningConfigs(options) {
|
|
|
979
1127
|
profileCert: path10.join(sdkPath, "toolchains/lib/OpenHarmonyProfileRelease.pem"),
|
|
980
1128
|
unsignedProfileTemplate: path10.join(sdkPath, "toolchains/lib/UnsgnedReleasedProfileTemplate.json")
|
|
981
1129
|
};
|
|
1130
|
+
const kind = pickSigningKind(apl);
|
|
1131
|
+
const passwords = options.passwords ?? DEFAULT_PASSWORDS;
|
|
982
1132
|
logger.info("[sign] Starting signing configuration generation...");
|
|
983
|
-
copyFilesToProject(projectDir, paths, logger);
|
|
984
|
-
modifyProfileTemplate(projectDir, apl, appFeature, logger);
|
|
985
|
-
generateP7bFile(projectDir, paths, logger);
|
|
1133
|
+
copyFilesToProject(projectDir, paths, kind, logger, options.applicationCertPath);
|
|
1134
|
+
modifyProfileTemplate(projectDir, apl, appFeature, logger, { kind, acls: options.acls });
|
|
1135
|
+
generateP7bFile(projectDir, paths, logger, passwords);
|
|
986
1136
|
prepareMaterialDirectory(projectDir, logger);
|
|
987
|
-
updateBuildProfile(projectDir, logger);
|
|
1137
|
+
updateBuildProfile(projectDir, kind, logger, passwords);
|
|
988
1138
|
logger.info("[sign] Signing configuration generated successfully.");
|
|
989
1139
|
}
|
|
990
1140
|
|
|
991
1141
|
// src/emulator/install.ts
|
|
992
|
-
import * as
|
|
1142
|
+
import * as fs12 from "fs";
|
|
993
1143
|
import * as os7 from "os";
|
|
994
1144
|
import * as path11 from "path";
|
|
995
1145
|
async function installEmulator(opts) {
|
|
@@ -997,63 +1147,142 @@ async function installEmulator(opts) {
|
|
|
997
1147
|
const logger = opts.logger ?? noopLogger;
|
|
998
1148
|
const url = config.get("emulatorUrl", defaultPaths.emulatorUrl);
|
|
999
1149
|
const emulatorDir = getEmulatorDir(config);
|
|
1000
|
-
const tmpDir =
|
|
1150
|
+
const tmpDir = fs12.mkdtempSync(path11.join(os7.tmpdir(), "oniro-emulator-"));
|
|
1001
1151
|
const tmpZip = path11.join(tmpDir, "oniro_emulator.zip");
|
|
1002
1152
|
try {
|
|
1003
|
-
|
|
1153
|
+
fs12.mkdirSync(emulatorDir, { recursive: true });
|
|
1004
1154
|
progress?.report({ message: "Downloading emulator...", increment: 0 });
|
|
1005
1155
|
await downloadFile({ url, dest: tmpZip, progress, abortSignal, start: 0, range: 50 });
|
|
1006
1156
|
progress?.report({ message: "Extracting emulator...", increment: 0 });
|
|
1007
1157
|
await extractZipWithProgress({ zipPath: tmpZip, dest: emulatorDir, progress, start: 50, range: 45, logger });
|
|
1008
1158
|
const runSh = path11.join(emulatorDir, "images", "run.sh");
|
|
1009
|
-
if (
|
|
1010
|
-
|
|
1159
|
+
if (fs12.existsSync(runSh)) {
|
|
1160
|
+
fs12.chmodSync(runSh, 493);
|
|
1011
1161
|
}
|
|
1012
1162
|
progress?.report({ message: "Finalizing installation...", increment: 5 });
|
|
1013
1163
|
} finally {
|
|
1014
|
-
|
|
1164
|
+
fs12.rmSync(tmpDir, { recursive: true, force: true });
|
|
1015
1165
|
}
|
|
1016
1166
|
}
|
|
1017
1167
|
function isEmulatorInstalled(config) {
|
|
1018
|
-
|
|
1168
|
+
const imagesDir = path11.join(getEmulatorDir(config), "images");
|
|
1169
|
+
return fs12.existsSync(path11.join(imagesDir, "run.sh")) || fs12.existsSync(path11.join(imagesDir, "run.bat"));
|
|
1019
1170
|
}
|
|
1020
1171
|
function removeEmulator(config) {
|
|
1021
1172
|
const emulatorDir = getEmulatorDir(config);
|
|
1022
|
-
if (
|
|
1023
|
-
|
|
1173
|
+
if (fs12.existsSync(emulatorDir)) {
|
|
1174
|
+
fs12.rmSync(emulatorDir, { recursive: true, force: true });
|
|
1024
1175
|
}
|
|
1025
1176
|
}
|
|
1026
1177
|
|
|
1027
1178
|
// src/emulator/lifecycle.ts
|
|
1028
|
-
import * as
|
|
1179
|
+
import * as fs13 from "fs";
|
|
1029
1180
|
import * as os8 from "os";
|
|
1030
1181
|
import * as path12 from "path";
|
|
1031
|
-
import {
|
|
1032
|
-
|
|
1033
|
-
|
|
1182
|
+
import { spawn as spawn2 } from "child_process";
|
|
1183
|
+
|
|
1184
|
+
// src/hdc/exec.ts
|
|
1185
|
+
import { spawn } from "child_process";
|
|
1186
|
+
function runProcess(opts) {
|
|
1187
|
+
const { command, args, cwd, env, onOutput } = opts;
|
|
1188
|
+
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
1189
|
+
const { abortSignal } = opts;
|
|
1034
1190
|
return new Promise((resolve2, reject) => {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1191
|
+
if (abortSignal?.aborted) {
|
|
1192
|
+
reject(new CancelledError(`Command aborted before start: ${command}`));
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const child = spawn(command, [...args], { cwd, env, shell: false });
|
|
1196
|
+
let stdout = "";
|
|
1197
|
+
let stderr = "";
|
|
1198
|
+
let settled = false;
|
|
1199
|
+
let timedOut = false;
|
|
1200
|
+
const cleanup = () => {
|
|
1201
|
+
clearTimeout(timer);
|
|
1202
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
1203
|
+
};
|
|
1204
|
+
const onAbort = () => {
|
|
1205
|
+
if (settled) return;
|
|
1206
|
+
settled = true;
|
|
1207
|
+
cleanup();
|
|
1208
|
+
try {
|
|
1209
|
+
child.kill("SIGKILL");
|
|
1210
|
+
} catch {
|
|
1211
|
+
}
|
|
1212
|
+
reject(new CancelledError(`Command aborted: ${command} ${args.join(" ")}`));
|
|
1213
|
+
};
|
|
1214
|
+
const timer = setTimeout(() => {
|
|
1215
|
+
timedOut = true;
|
|
1216
|
+
try {
|
|
1217
|
+
child.kill("SIGKILL");
|
|
1218
|
+
} catch {
|
|
1043
1219
|
}
|
|
1220
|
+
}, timeoutMs);
|
|
1221
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1222
|
+
child.stdout.on("data", (d) => {
|
|
1223
|
+
const s = d.toString();
|
|
1224
|
+
stdout += s;
|
|
1225
|
+
onOutput?.(s, "stdout");
|
|
1226
|
+
});
|
|
1227
|
+
child.stderr.on("data", (d) => {
|
|
1228
|
+
const s = d.toString();
|
|
1229
|
+
stderr += s;
|
|
1230
|
+
onOutput?.(s, "stderr");
|
|
1044
1231
|
});
|
|
1232
|
+
child.on("error", (err) => {
|
|
1233
|
+
if (settled) return;
|
|
1234
|
+
settled = true;
|
|
1235
|
+
cleanup();
|
|
1236
|
+
reject(new OniroError(`Failed to spawn ${command}: ${err.message}`, err));
|
|
1237
|
+
});
|
|
1238
|
+
child.on("close", (code) => {
|
|
1239
|
+
if (settled) return;
|
|
1240
|
+
settled = true;
|
|
1241
|
+
cleanup();
|
|
1242
|
+
if (timedOut) {
|
|
1243
|
+
reject(
|
|
1244
|
+
new OniroError(
|
|
1245
|
+
`Command timed out after ${timeoutMs}ms: ${command} ${args.join(" ")}` + (stderr.trim() ? `
|
|
1246
|
+
${stderr.trim()}` : "")
|
|
1247
|
+
)
|
|
1248
|
+
);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
resolve2({ code: code ?? -1, stdout, stderr });
|
|
1252
|
+
});
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
function hdcExec(opts) {
|
|
1256
|
+
const hdc = getHdcPath(opts.config);
|
|
1257
|
+
const argv = opts.deviceSerial ? ["-t", opts.deviceSerial, ...opts.args] : [...opts.args];
|
|
1258
|
+
return runProcess({
|
|
1259
|
+
command: hdc,
|
|
1260
|
+
args: argv,
|
|
1261
|
+
timeoutMs: opts.timeoutMs,
|
|
1262
|
+
abortSignal: opts.abortSignal,
|
|
1263
|
+
onOutput: opts.onOutput,
|
|
1264
|
+
logger: opts.logger
|
|
1045
1265
|
});
|
|
1046
1266
|
}
|
|
1267
|
+
function shell(opts) {
|
|
1268
|
+
const { command, ...rest } = opts;
|
|
1269
|
+
return hdcExec({ ...rest, args: ["shell", command] });
|
|
1270
|
+
}
|
|
1271
|
+
function ensureOk(result, command) {
|
|
1272
|
+
if (result.code !== 0) {
|
|
1273
|
+
throw new CommandFailedError(command, result.code, result.stderr.trim());
|
|
1274
|
+
}
|
|
1275
|
+
return result;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/emulator/lifecycle.ts
|
|
1279
|
+
var PID_FILE = path12.join(os8.tmpdir(), "oniro_emulator.pid");
|
|
1047
1280
|
async function attemptHdcConnection(config, address = "127.0.0.1:55555", logger = noopLogger) {
|
|
1048
|
-
const hdc = getHdcPath(config);
|
|
1049
1281
|
try {
|
|
1050
|
-
await
|
|
1051
|
-
await
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
resolve2(stdout?.includes(address) ?? false);
|
|
1055
|
-
});
|
|
1056
|
-
});
|
|
1282
|
+
await hdcExec({ config, args: ["start", "-r"], logger });
|
|
1283
|
+
await hdcExec({ config, args: ["tconn", address], logger });
|
|
1284
|
+
const targets = await hdcExec({ config, args: ["list", "targets"], logger });
|
|
1285
|
+
return targets.stdout.includes(address);
|
|
1057
1286
|
} catch (err) {
|
|
1058
1287
|
logger.warn(`hdc connection attempt failed: ${err.message}`);
|
|
1059
1288
|
return false;
|
|
@@ -1066,18 +1295,18 @@ function resolveLauncher(imagesPath, headless, connect) {
|
|
|
1066
1295
|
if (headless) extra.push("--headless");
|
|
1067
1296
|
if (connect) extra.push("--connect", connect);
|
|
1068
1297
|
if (os8.platform() === "win32") {
|
|
1069
|
-
if (
|
|
1298
|
+
if (fs13.existsSync(runBat)) {
|
|
1070
1299
|
return { command: "cmd.exe", args: ["/c", runBat, ...extra], cwd: imagesPath };
|
|
1071
1300
|
}
|
|
1072
|
-
if (
|
|
1301
|
+
if (fs13.existsSync(runSh)) {
|
|
1073
1302
|
return { command: "bash", args: [runSh, ...extra], cwd: imagesPath };
|
|
1074
1303
|
}
|
|
1075
1304
|
throw new OniroError(`No emulator launcher (run.bat or run.sh) found in ${imagesPath}.`);
|
|
1076
1305
|
}
|
|
1077
|
-
if (
|
|
1306
|
+
if (fs13.existsSync(runSh)) {
|
|
1078
1307
|
return { command: "bash", args: [runSh, ...extra], cwd: imagesPath };
|
|
1079
1308
|
}
|
|
1080
|
-
if (
|
|
1309
|
+
if (fs13.existsSync(runBat)) {
|
|
1081
1310
|
return { command: "bash", args: [runBat, ...extra], cwd: imagesPath };
|
|
1082
1311
|
}
|
|
1083
1312
|
throw new OniroError(`No emulator launcher (run.sh or run.bat) found in ${imagesPath}.`);
|
|
@@ -1085,9 +1314,9 @@ function resolveLauncher(imagesPath, headless, connect) {
|
|
|
1085
1314
|
async function startEmulator(opts) {
|
|
1086
1315
|
const { config } = opts;
|
|
1087
1316
|
const logger = opts.logger ?? noopLogger;
|
|
1088
|
-
if (os8.platform() !== "win32" &&
|
|
1317
|
+
if (os8.platform() !== "win32" && fs13.existsSync(PID_FILE)) {
|
|
1089
1318
|
try {
|
|
1090
|
-
const pid = Number(
|
|
1319
|
+
const pid = Number(fs13.readFileSync(PID_FILE, "utf8").trim());
|
|
1091
1320
|
if (Number.isFinite(pid) && pid > 0) {
|
|
1092
1321
|
try {
|
|
1093
1322
|
process.kill(pid, 0);
|
|
@@ -1104,27 +1333,27 @@ async function startEmulator(opts) {
|
|
|
1104
1333
|
}
|
|
1105
1334
|
}
|
|
1106
1335
|
const imagesPath = path12.join(getEmulatorDir(config), "images");
|
|
1107
|
-
if (!
|
|
1336
|
+
if (!fs13.existsSync(imagesPath)) {
|
|
1108
1337
|
throw new OniroError(`Emulator images directory not found at ${imagesPath}. Run \`oniro-app emulator install\` first.`);
|
|
1109
1338
|
}
|
|
1110
1339
|
const launcher = resolveLauncher(imagesPath, opts.headless === true, opts.connect);
|
|
1111
1340
|
const logPath = opts.logFile ?? (os8.platform() === "win32" ? "NUL" : "/dev/null");
|
|
1112
|
-
const logFd =
|
|
1341
|
+
const logFd = fs13.openSync(logPath, "a");
|
|
1113
1342
|
let child;
|
|
1114
1343
|
try {
|
|
1115
|
-
child =
|
|
1344
|
+
child = spawn2(launcher.command, [...launcher.args], {
|
|
1116
1345
|
cwd: launcher.cwd,
|
|
1117
1346
|
detached: true,
|
|
1118
1347
|
stdio: ["ignore", logFd, logFd],
|
|
1119
1348
|
windowsHide: true
|
|
1120
1349
|
});
|
|
1121
1350
|
} finally {
|
|
1122
|
-
|
|
1351
|
+
fs13.closeSync(logFd);
|
|
1123
1352
|
}
|
|
1124
1353
|
if (!child.pid) {
|
|
1125
1354
|
throw new OniroError(`Failed to spawn launcher (${launcher.command} ${launcher.args.join(" ")}).`);
|
|
1126
1355
|
}
|
|
1127
|
-
|
|
1356
|
+
fs13.writeFileSync(PID_FILE, String(child.pid));
|
|
1128
1357
|
child.unref();
|
|
1129
1358
|
logger.info(`Emulator launched (PID ${child.pid}). Logs: ${logPath}`);
|
|
1130
1359
|
const earlyExit = new Promise((resolve2) => {
|
|
@@ -1171,24 +1400,24 @@ ${tail}` : ""}`);
|
|
|
1171
1400
|
}
|
|
1172
1401
|
function readTail(filePath, bytes) {
|
|
1173
1402
|
try {
|
|
1174
|
-
const stat =
|
|
1403
|
+
const stat = fs13.statSync(filePath);
|
|
1175
1404
|
const start = Math.max(0, stat.size - bytes);
|
|
1176
|
-
const fd =
|
|
1405
|
+
const fd = fs13.openSync(filePath, "r");
|
|
1177
1406
|
try {
|
|
1178
1407
|
const buf = Buffer.alloc(stat.size - start);
|
|
1179
|
-
|
|
1408
|
+
fs13.readSync(fd, buf, 0, buf.length, start);
|
|
1180
1409
|
return buf.toString("utf8");
|
|
1181
1410
|
} finally {
|
|
1182
|
-
|
|
1411
|
+
fs13.closeSync(fd);
|
|
1183
1412
|
}
|
|
1184
1413
|
} catch {
|
|
1185
1414
|
return "";
|
|
1186
1415
|
}
|
|
1187
1416
|
}
|
|
1188
1417
|
async function stopEmulator(logger = noopLogger) {
|
|
1189
|
-
if (
|
|
1418
|
+
if (fs13.existsSync(PID_FILE)) {
|
|
1190
1419
|
try {
|
|
1191
|
-
const pid = Number(
|
|
1420
|
+
const pid = Number(fs13.readFileSync(PID_FILE, "utf8").trim());
|
|
1192
1421
|
if (Number.isFinite(pid) && pid > 0) {
|
|
1193
1422
|
try {
|
|
1194
1423
|
process.kill(pid, "SIGTERM");
|
|
@@ -1201,27 +1430,194 @@ async function stopEmulator(logger = noopLogger) {
|
|
|
1201
1430
|
logger.warn(`Could not read PID file: ${err.message}`);
|
|
1202
1431
|
}
|
|
1203
1432
|
}
|
|
1204
|
-
const killCmd = os8.platform() === "win32" ? "taskkill /IM qemu-system-x86_64.exe /F" : "pkill -f qemu-system-x86_64";
|
|
1433
|
+
const [killCmd, ...killArgs] = os8.platform() === "win32" ? ["taskkill", "/IM", "qemu-system-x86_64.exe", "/F"] : ["pkill", "-f", "qemu-system-x86_64"];
|
|
1205
1434
|
try {
|
|
1206
|
-
await
|
|
1207
|
-
} catch {
|
|
1435
|
+
await runProcess({ command: killCmd, args: killArgs, logger });
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
logger.warn(`[emulator] could not run ${killCmd}: ${err.message}`);
|
|
1208
1438
|
}
|
|
1209
|
-
|
|
1439
|
+
fs13.rm(PID_FILE, { force: true }, (err) => {
|
|
1210
1440
|
if (err) logger.warn(`Could not remove PID file: ${err.message}`);
|
|
1211
1441
|
});
|
|
1212
1442
|
}
|
|
1213
1443
|
|
|
1444
|
+
// src/hdc/devices.ts
|
|
1445
|
+
var STATUS_TOKENS = /* @__PURE__ */ new Set([
|
|
1446
|
+
"Connected",
|
|
1447
|
+
"Offline",
|
|
1448
|
+
"Unauthorized",
|
|
1449
|
+
"Authorizing",
|
|
1450
|
+
"Disconnected",
|
|
1451
|
+
"Recovery"
|
|
1452
|
+
]);
|
|
1453
|
+
var CONNECTION_TOKENS = /* @__PURE__ */ new Set(["USB", "TCP", "UART", "BT", "UNKNOWN"]);
|
|
1454
|
+
function parseDeviceList(stdout) {
|
|
1455
|
+
const devices = [];
|
|
1456
|
+
for (const line of stdout.split("\n")) {
|
|
1457
|
+
const trimmed = line.trim();
|
|
1458
|
+
if (!trimmed || trimmed.startsWith("[Empty]")) continue;
|
|
1459
|
+
const cols = trimmed.split(/\s+/);
|
|
1460
|
+
const serial = cols[0];
|
|
1461
|
+
if (!serial) continue;
|
|
1462
|
+
const rest = cols.slice(1);
|
|
1463
|
+
const connection = rest.find((c) => CONNECTION_TOKENS.has(c));
|
|
1464
|
+
const status = rest.find((c) => STATUS_TOKENS.has(c)) ?? "Unknown";
|
|
1465
|
+
const info = { serial, status };
|
|
1466
|
+
if (connection) info.connection = connection;
|
|
1467
|
+
devices.push(info);
|
|
1468
|
+
}
|
|
1469
|
+
return devices;
|
|
1470
|
+
}
|
|
1471
|
+
async function listDevices(config, opts = {}) {
|
|
1472
|
+
const res = await hdcExec({
|
|
1473
|
+
config,
|
|
1474
|
+
args: ["list", "targets", "-v"],
|
|
1475
|
+
timeoutMs: opts.timeoutMs ?? 1e4,
|
|
1476
|
+
logger: opts.logger
|
|
1477
|
+
});
|
|
1478
|
+
return parseDeviceList(res.stdout);
|
|
1479
|
+
}
|
|
1480
|
+
async function selectDevice(config, serial, opts = {}) {
|
|
1481
|
+
if (serial && serial !== "auto") return serial;
|
|
1482
|
+
const envSerial = process.env.ONIRO_DEVICE_SERIAL || process.env.DEVICE_SERIAL;
|
|
1483
|
+
if (envSerial && envSerial !== "auto") return envSerial;
|
|
1484
|
+
const usable = (await listDevices(config, opts)).filter(
|
|
1485
|
+
(d) => d.status === "Connected" || d.status === "Unknown"
|
|
1486
|
+
);
|
|
1487
|
+
if (usable.length === 1) return usable[0].serial;
|
|
1488
|
+
if (usable.length === 0) {
|
|
1489
|
+
throw new OniroError("No connected device found. Connect a device/emulator or set ONIRO_DEVICE_SERIAL.");
|
|
1490
|
+
}
|
|
1491
|
+
throw new OniroError(
|
|
1492
|
+
`Multiple devices connected (${usable.map((d) => d.serial).join(", ")}). Set ONIRO_DEVICE_SERIAL to choose one.`
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/hdc/param.ts
|
|
1497
|
+
async function paramGet(config, key, opts = {}) {
|
|
1498
|
+
const res = await shell({
|
|
1499
|
+
config,
|
|
1500
|
+
command: `param get ${key}`,
|
|
1501
|
+
deviceSerial: opts.deviceSerial,
|
|
1502
|
+
timeoutMs: opts.timeoutMs ?? 1e4,
|
|
1503
|
+
logger: opts.logger
|
|
1504
|
+
});
|
|
1505
|
+
ensureOk(res, `hdc shell param get ${key}`);
|
|
1506
|
+
return res.stdout.trim();
|
|
1507
|
+
}
|
|
1508
|
+
async function paramSet(config, key, value, opts = {}) {
|
|
1509
|
+
const res = await shell({
|
|
1510
|
+
config,
|
|
1511
|
+
command: `param set ${key} ${value}`,
|
|
1512
|
+
deviceSerial: opts.deviceSerial,
|
|
1513
|
+
timeoutMs: opts.timeoutMs ?? 1e4,
|
|
1514
|
+
logger: opts.logger
|
|
1515
|
+
});
|
|
1516
|
+
ensureOk(res, `hdc shell param set ${key} ${value}`);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/hdc/files.ts
|
|
1520
|
+
async function sendFile(opts) {
|
|
1521
|
+
const res = await hdcExec({
|
|
1522
|
+
config: opts.config,
|
|
1523
|
+
args: ["file", "send", opts.local, opts.remote],
|
|
1524
|
+
deviceSerial: opts.deviceSerial,
|
|
1525
|
+
timeoutMs: opts.timeoutMs ?? 6e5,
|
|
1526
|
+
abortSignal: opts.abortSignal,
|
|
1527
|
+
onOutput: opts.onOutput,
|
|
1528
|
+
logger: opts.logger
|
|
1529
|
+
});
|
|
1530
|
+
ensureOk(res, `hdc file send ${opts.local} ${opts.remote}`);
|
|
1531
|
+
}
|
|
1532
|
+
async function recvFile(opts) {
|
|
1533
|
+
const res = await hdcExec({
|
|
1534
|
+
config: opts.config,
|
|
1535
|
+
args: ["file", "recv", opts.remote, opts.local],
|
|
1536
|
+
deviceSerial: opts.deviceSerial,
|
|
1537
|
+
timeoutMs: opts.timeoutMs ?? 6e5,
|
|
1538
|
+
abortSignal: opts.abortSignal,
|
|
1539
|
+
onOutput: opts.onOutput,
|
|
1540
|
+
logger: opts.logger
|
|
1541
|
+
});
|
|
1542
|
+
ensureOk(res, `hdc file recv ${opts.remote} ${opts.local}`);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/hdc/display.ts
|
|
1546
|
+
async function getDisplaySize(config, opts = {}) {
|
|
1547
|
+
const res = await shell({
|
|
1548
|
+
config,
|
|
1549
|
+
command: "hidumper -s 10 -a screen",
|
|
1550
|
+
deviceSerial: opts.deviceSerial,
|
|
1551
|
+
timeoutMs: opts.timeoutMs ?? 1e4,
|
|
1552
|
+
logger: opts.logger
|
|
1553
|
+
});
|
|
1554
|
+
if (res.code !== 0) return null;
|
|
1555
|
+
const m = res.stdout.match(/render resolution=(\d+)x(\d+)/);
|
|
1556
|
+
if (!m) return null;
|
|
1557
|
+
return { width: Number(m[1]), height: Number(m[2]) };
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// src/hdc/wait.ts
|
|
1561
|
+
function delay(ms, signal) {
|
|
1562
|
+
return new Promise((resolve2, reject) => {
|
|
1563
|
+
if (signal?.aborted) {
|
|
1564
|
+
reject(new CancelledError("Wait cancelled."));
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const onAbort = () => {
|
|
1568
|
+
cleanup();
|
|
1569
|
+
reject(new CancelledError("Wait cancelled."));
|
|
1570
|
+
};
|
|
1571
|
+
const cleanup = () => {
|
|
1572
|
+
clearTimeout(timer);
|
|
1573
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1574
|
+
};
|
|
1575
|
+
const timer = setTimeout(() => {
|
|
1576
|
+
cleanup();
|
|
1577
|
+
resolve2();
|
|
1578
|
+
}, ms);
|
|
1579
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
async function waitForCondition(opts) {
|
|
1583
|
+
const pollMs = opts.pollMs ?? 2e3;
|
|
1584
|
+
const heartbeatEvery = opts.heartbeatEvery ?? 6;
|
|
1585
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
1586
|
+
let attempt = 0;
|
|
1587
|
+
while (Date.now() < deadline) {
|
|
1588
|
+
if (opts.abortSignal?.aborted) throw new CancelledError("Wait cancelled.");
|
|
1589
|
+
attempt++;
|
|
1590
|
+
let ok = false;
|
|
1591
|
+
try {
|
|
1592
|
+
ok = await opts.probe();
|
|
1593
|
+
} catch {
|
|
1594
|
+
ok = false;
|
|
1595
|
+
}
|
|
1596
|
+
if (ok) return;
|
|
1597
|
+
if (opts.onHeartbeat && attempt % heartbeatEvery === 0) {
|
|
1598
|
+
opts.onHeartbeat(attempt, Math.max(0, deadline - Date.now()));
|
|
1599
|
+
}
|
|
1600
|
+
await delay(pollMs, opts.abortSignal);
|
|
1601
|
+
}
|
|
1602
|
+
throw new OniroError(opts.timeoutMessage ?? `Condition not met within ${opts.timeoutMs}ms.`);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/hdc/app.ts
|
|
1606
|
+
import * as fs15 from "fs";
|
|
1607
|
+
import * as path14 from "path";
|
|
1608
|
+
import { spawn as spawn3 } from "child_process";
|
|
1609
|
+
|
|
1214
1610
|
// src/hdc/project.ts
|
|
1215
|
-
import * as
|
|
1611
|
+
import * as fs14 from "fs";
|
|
1216
1612
|
import * as path13 from "path";
|
|
1217
1613
|
import JSON53 from "json5";
|
|
1218
1614
|
function readJson5(filePath) {
|
|
1219
|
-
const content =
|
|
1615
|
+
const content = fs14.readFileSync(filePath, "utf-8");
|
|
1220
1616
|
return JSON53.parse(content);
|
|
1221
1617
|
}
|
|
1222
1618
|
function getBundleName(projectDir) {
|
|
1223
1619
|
const appJsonPath = path13.join(projectDir, "AppScope", "app.json5");
|
|
1224
|
-
if (!
|
|
1620
|
+
if (!fs14.existsSync(appJsonPath)) {
|
|
1225
1621
|
throw new OniroError(`Could not find app.json5 at ${appJsonPath}`);
|
|
1226
1622
|
}
|
|
1227
1623
|
const appJson = readJson5(appJsonPath);
|
|
@@ -1230,59 +1626,72 @@ function getBundleName(projectDir) {
|
|
|
1230
1626
|
}
|
|
1231
1627
|
return appJson.app.bundleName;
|
|
1232
1628
|
}
|
|
1233
|
-
function getMainAbility(projectDir, moduleName = "entry") {
|
|
1629
|
+
function getMainAbility(projectDir, moduleName = "entry", abilityName) {
|
|
1234
1630
|
const moduleJsonPath = path13.join(projectDir, moduleName, "src", "main", "module.json5");
|
|
1235
|
-
if (!
|
|
1631
|
+
if (!fs14.existsSync(moduleJsonPath)) {
|
|
1236
1632
|
throw new OniroError(`Could not find module.json5 at ${moduleJsonPath}`);
|
|
1237
1633
|
}
|
|
1238
1634
|
const moduleJson = readJson5(moduleJsonPath);
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1635
|
+
const module = moduleJson.module ?? {};
|
|
1636
|
+
if (abilityName) {
|
|
1637
|
+
const match = module.abilities?.find((a) => a.name === abilityName);
|
|
1638
|
+
if (match?.name) return match.name;
|
|
1639
|
+
throw new OniroError(`Ability "${abilityName}" not found in module.json5 at ${moduleJsonPath}`);
|
|
1640
|
+
}
|
|
1641
|
+
if (module.mainElement) return module.mainElement;
|
|
1642
|
+
const abilities = module.abilities ?? [];
|
|
1643
|
+
const visible = abilities.find((a) => a.visible && a.name);
|
|
1644
|
+
if (visible?.name) return visible.name;
|
|
1645
|
+
const first = abilities.find((a) => a.name);
|
|
1646
|
+
if (first?.name) return first.name;
|
|
1647
|
+
throw new OniroError("mainElement not found in module.json5");
|
|
1243
1648
|
}
|
|
1244
1649
|
|
|
1245
1650
|
// src/hdc/app.ts
|
|
1246
|
-
import * as fs14 from "fs";
|
|
1247
|
-
import * as path14 from "path";
|
|
1248
|
-
import { exec as exec2, spawn as spawn2 } from "child_process";
|
|
1249
|
-
function execPromise2(cmd, logger) {
|
|
1250
|
-
return new Promise((resolve2, reject) => {
|
|
1251
|
-
exec2(cmd, (error, stdout, stderr) => {
|
|
1252
|
-
if (stdout?.trim()) logger.info(`[hdc] ${stdout.trim()}`);
|
|
1253
|
-
if (stderr?.trim()) logger.warn(`[hdc] ${stderr.trim()}`);
|
|
1254
|
-
if (error) {
|
|
1255
|
-
logger.error(`[hdc] ${error.message}`);
|
|
1256
|
-
reject(error);
|
|
1257
|
-
} else {
|
|
1258
|
-
resolve2();
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
1651
|
async function installApp(opts) {
|
|
1264
|
-
const logger = opts.logger ?? noopLogger;
|
|
1652
|
+
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
1265
1653
|
const relativeHapPath = opts.hapPath ?? opts.config.get("hapPath", defaultPaths.hapPath);
|
|
1266
1654
|
const hapPath = path14.isAbsolute(relativeHapPath) ? relativeHapPath : path14.join(opts.projectDir, relativeHapPath);
|
|
1267
|
-
if (!
|
|
1655
|
+
if (!fs15.existsSync(hapPath)) {
|
|
1268
1656
|
throw new OniroError(`HAP file not found at: ${hapPath}. Build and sign the app first.`);
|
|
1269
1657
|
}
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1658
|
+
const result = await hdcExec({ config: opts.config, args: ["install", hapPath], timeoutMs: 3e5 });
|
|
1659
|
+
if (result.stdout.trim()) logger.info(result.stdout.trim());
|
|
1660
|
+
if (result.stderr.trim()) logger.warn(result.stderr.trim());
|
|
1661
|
+
ensureOk(result, `hdc install ${hapPath}`);
|
|
1662
|
+
let bundleName = "";
|
|
1663
|
+
try {
|
|
1664
|
+
bundleName = getBundleName(opts.projectDir);
|
|
1665
|
+
} catch {
|
|
1666
|
+
}
|
|
1667
|
+
return { installed: true, bundleName, hapPath, output: `${result.stdout}${result.stderr}`.trim() };
|
|
1668
|
+
}
|
|
1669
|
+
function detectAaStartFailure(output) {
|
|
1670
|
+
return /failed to start ability|Error Code:\s*\d|Error Message:/i.test(output) ? output.trim() : null;
|
|
1272
1671
|
}
|
|
1273
1672
|
async function launchApp(opts) {
|
|
1274
|
-
const logger = opts.logger ?? noopLogger;
|
|
1673
|
+
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
1275
1674
|
const bundleName = getBundleName(opts.projectDir);
|
|
1276
|
-
const mainAbility = getMainAbility(opts.projectDir, opts.moduleName);
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1675
|
+
const mainAbility = getMainAbility(opts.projectDir, opts.moduleName, opts.abilityName);
|
|
1676
|
+
const result = await hdcExec({
|
|
1677
|
+
config: opts.config,
|
|
1678
|
+
args: ["shell", "aa", "start", "-a", mainAbility, "-b", bundleName]
|
|
1679
|
+
});
|
|
1680
|
+
if (result.stdout.trim()) logger.info(result.stdout.trim());
|
|
1681
|
+
if (result.stderr.trim()) logger.warn(result.stderr.trim());
|
|
1682
|
+
ensureOk(result, `hdc shell aa start -a ${mainAbility} -b ${bundleName}`);
|
|
1683
|
+
const failure = detectAaStartFailure(`${result.stdout}
|
|
1684
|
+
${result.stderr}`);
|
|
1685
|
+
if (failure) {
|
|
1686
|
+
throw new OniroError(`Failed to launch ${bundleName} (${mainAbility}): ${failure}`);
|
|
1687
|
+
}
|
|
1279
1688
|
}
|
|
1280
1689
|
function listRunningProcesses(config, options = {}) {
|
|
1281
1690
|
const logger = options.logger ?? noopLogger;
|
|
1282
1691
|
const timeoutMs = options.timeoutMs ?? 1e3;
|
|
1283
1692
|
const hdc = getHdcPath(config);
|
|
1284
1693
|
return new Promise((resolve2, reject) => {
|
|
1285
|
-
const proc =
|
|
1694
|
+
const proc = spawn3(hdc, ["track-jpid"]);
|
|
1286
1695
|
const processes = [];
|
|
1287
1696
|
let resolved = false;
|
|
1288
1697
|
proc.stdout.on("data", (data) => {
|
|
@@ -1336,14 +1745,395 @@ async function findAppProcessId(config, projectDir, logger) {
|
|
|
1336
1745
|
const result = await listRunningProcesses(config, { targetProcessName: bundleName, logger });
|
|
1337
1746
|
return result;
|
|
1338
1747
|
}
|
|
1748
|
+
async function findRunningProcess(opts) {
|
|
1749
|
+
const safe = opts.bundle.replace(/'/g, `'\\''`);
|
|
1750
|
+
const res = await shell({
|
|
1751
|
+
config: opts.config,
|
|
1752
|
+
command: `pidof '${safe}'`,
|
|
1753
|
+
deviceSerial: opts.deviceSerial,
|
|
1754
|
+
timeoutMs: opts.timeoutMs ?? 1e4,
|
|
1755
|
+
logger: opts.logger
|
|
1756
|
+
});
|
|
1757
|
+
const pid = res.stdout.trim().split(/\s+/).filter(Boolean)[0];
|
|
1758
|
+
if (res.code !== 0 || !pid || !/^\d+$/.test(pid)) return null;
|
|
1759
|
+
return { pid, name: opts.bundle };
|
|
1760
|
+
}
|
|
1761
|
+
async function uninstallApp(opts) {
|
|
1762
|
+
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
1763
|
+
const res = await hdcExec({
|
|
1764
|
+
config: opts.config,
|
|
1765
|
+
args: ["uninstall", opts.bundle],
|
|
1766
|
+
deviceSerial: opts.deviceSerial,
|
|
1767
|
+
timeoutMs: opts.timeoutMs ?? 12e4
|
|
1768
|
+
});
|
|
1769
|
+
if (res.stdout.trim()) logger.info(res.stdout.trim());
|
|
1770
|
+
if (res.stderr.trim()) logger.warn(res.stderr.trim());
|
|
1771
|
+
ensureOk(res, `hdc uninstall ${opts.bundle}`);
|
|
1772
|
+
}
|
|
1773
|
+
async function forceStop(opts) {
|
|
1774
|
+
const res = await shell({
|
|
1775
|
+
config: opts.config,
|
|
1776
|
+
command: `aa force-stop ${opts.bundle}`,
|
|
1777
|
+
deviceSerial: opts.deviceSerial,
|
|
1778
|
+
timeoutMs: opts.timeoutMs ?? 3e4,
|
|
1779
|
+
logger: opts.logger
|
|
1780
|
+
});
|
|
1781
|
+
ensureOk(res, `hdc shell aa force-stop ${opts.bundle}`);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// src/hdc/lifecycle.ts
|
|
1785
|
+
async function isReachable(config, deviceSerial) {
|
|
1786
|
+
try {
|
|
1787
|
+
const res = await shell({ config, command: "echo oniro_ping", deviceSerial, timeoutMs: 5e3 });
|
|
1788
|
+
return res.code === 0 && res.stdout.includes("oniro_ping");
|
|
1789
|
+
} catch {
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
var TCP_SERIAL_RE = /^.+:\d+$/;
|
|
1794
|
+
async function reconnectIfTcp(config, deviceSerial, logger) {
|
|
1795
|
+
if (!deviceSerial || !TCP_SERIAL_RE.test(deviceSerial)) return;
|
|
1796
|
+
try {
|
|
1797
|
+
await hdcExec({ config, args: ["tconn", deviceSerial], timeoutMs: 5e3, logger });
|
|
1798
|
+
} catch {
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
async function waitForBundle(opts) {
|
|
1802
|
+
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
1803
|
+
const timeoutMs = opts.timeoutMs ?? 18e4;
|
|
1804
|
+
await waitForCondition({
|
|
1805
|
+
probe: async () => {
|
|
1806
|
+
await reconnectIfTcp(opts.config, opts.deviceSerial, logger);
|
|
1807
|
+
return await findRunningProcess({ config: opts.config, bundle: opts.bundle, deviceSerial: opts.deviceSerial, timeoutMs: 8e3 }) !== null;
|
|
1808
|
+
},
|
|
1809
|
+
timeoutMs,
|
|
1810
|
+
pollMs: 2e3,
|
|
1811
|
+
abortSignal: opts.abortSignal,
|
|
1812
|
+
onHeartbeat: (attempt, remaining) => logger.info(`waiting for ${opts.bundle} (attempt ${attempt}, ${Math.floor(remaining / 1e3)}s left)`),
|
|
1813
|
+
timeoutMessage: `Bundle ${opts.bundle} did not start within ${timeoutMs}ms.`
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
async function waitForBoot(opts) {
|
|
1817
|
+
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
1818
|
+
const timeoutMs = opts.timeoutMs ?? 18e4;
|
|
1819
|
+
await waitForCondition({
|
|
1820
|
+
probe: async () => {
|
|
1821
|
+
await reconnectIfTcp(opts.config, opts.deviceSerial, logger);
|
|
1822
|
+
if (!await isReachable(opts.config, opts.deviceSerial)) return false;
|
|
1823
|
+
if (!opts.untilPidOf) return true;
|
|
1824
|
+
return await findRunningProcess({ config: opts.config, bundle: opts.untilPidOf, deviceSerial: opts.deviceSerial, timeoutMs: 8e3 }) !== null;
|
|
1825
|
+
},
|
|
1826
|
+
timeoutMs,
|
|
1827
|
+
pollMs: 2e3,
|
|
1828
|
+
abortSignal: opts.abortSignal,
|
|
1829
|
+
onHeartbeat: (attempt, remaining) => logger.info(`waiting for boot (attempt ${attempt}, ${Math.floor(remaining / 1e3)}s left)`),
|
|
1830
|
+
timeoutMessage: `Device did not boot within ${timeoutMs}ms.`
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
async function reboot(opts) {
|
|
1834
|
+
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
1835
|
+
const mode = opts.mode ?? "system";
|
|
1836
|
+
const timeoutMs = opts.timeoutMs ?? 18e4;
|
|
1837
|
+
logger.info(`Rebooting device (${mode})...`);
|
|
1838
|
+
try {
|
|
1839
|
+
if (mode === "system") {
|
|
1840
|
+
await paramSet(opts.config, "ohos.startup.powerctrl", "reboot", {
|
|
1841
|
+
deviceSerial: opts.deviceSerial,
|
|
1842
|
+
timeoutMs: 15e3,
|
|
1843
|
+
logger: opts.logger
|
|
1844
|
+
});
|
|
1845
|
+
} else {
|
|
1846
|
+
await shell({ config: opts.config, command: `reboot ${mode}`, deviceSerial: opts.deviceSerial, timeoutMs: 15e3 });
|
|
1847
|
+
}
|
|
1848
|
+
} catch (err) {
|
|
1849
|
+
logger.debug(`reboot command did not return cleanly: ${err.message}`);
|
|
1850
|
+
}
|
|
1851
|
+
if (mode !== "system" || !opts.waitForBundle) return;
|
|
1852
|
+
try {
|
|
1853
|
+
await waitForCondition({
|
|
1854
|
+
probe: async () => !await isReachable(opts.config, opts.deviceSerial),
|
|
1855
|
+
timeoutMs: Math.min(timeoutMs, 3e4),
|
|
1856
|
+
pollMs: 1500,
|
|
1857
|
+
abortSignal: opts.abortSignal,
|
|
1858
|
+
timeoutMessage: "device did not become unreachable"
|
|
1859
|
+
});
|
|
1860
|
+
} catch {
|
|
1861
|
+
}
|
|
1862
|
+
await waitForBundle({
|
|
1863
|
+
config: opts.config,
|
|
1864
|
+
bundle: opts.waitForBundle,
|
|
1865
|
+
deviceSerial: opts.deviceSerial,
|
|
1866
|
+
timeoutMs,
|
|
1867
|
+
abortSignal: opts.abortSignal,
|
|
1868
|
+
logger: opts.logger
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/hdc/screen.ts
|
|
1873
|
+
import * as fs16 from "fs";
|
|
1874
|
+
import * as os9 from "os";
|
|
1875
|
+
import * as path15 from "path";
|
|
1876
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1877
|
+
function tmpName(ext) {
|
|
1878
|
+
return path15.join(os9.tmpdir(), `oniro_screen_${randomBytes2(6).toString("hex")}.${ext}`);
|
|
1879
|
+
}
|
|
1880
|
+
async function takeScreenshot(opts) {
|
|
1881
|
+
const remote = "/data/local/tmp/oniro_screenshot.jpeg";
|
|
1882
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1883
|
+
const cap = await shell({ config: opts.config, command: `snapshot_display -f ${remote}`, deviceSerial: opts.deviceSerial, timeoutMs });
|
|
1884
|
+
ensureOk(cap, `snapshot_display -f ${remote}`);
|
|
1885
|
+
const local = tmpName("jpeg");
|
|
1886
|
+
try {
|
|
1887
|
+
await recvFile({ config: opts.config, remote, local, deviceSerial: opts.deviceSerial, timeoutMs });
|
|
1888
|
+
const pixels = await fs16.promises.readFile(local);
|
|
1889
|
+
const size = await getDisplaySize(opts.config, { deviceSerial: opts.deviceSerial }) ?? { width: 0, height: 0 };
|
|
1890
|
+
return { pixels, width: size.width, height: size.height };
|
|
1891
|
+
} finally {
|
|
1892
|
+
fs16.promises.unlink(local).catch(() => {
|
|
1893
|
+
});
|
|
1894
|
+
void shell({ config: opts.config, command: `rm -f ${remote}`, deviceSerial: opts.deviceSerial }).catch(() => {
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
async function captureBurst(opts) {
|
|
1899
|
+
const count = Math.max(1, Math.floor(opts.count));
|
|
1900
|
+
const remoteDir = `/data/local/tmp/oniro_burst_${randomBytes2(6).toString("hex")}`;
|
|
1901
|
+
const sleepSec = opts.intervalMs > 0 ? (opts.intervalMs / 1e3).toFixed(3) : "0";
|
|
1902
|
+
const loop = `mkdir -p ${remoteDir}; i=0; while [ $i -lt ${count} ]; do snapshot_display -f ${remoteDir}/f$i.jpeg; [ ${count} -gt 1 ] && sleep ${sleepSec}; i=$((i+1)); done`;
|
|
1903
|
+
const cap = await shell({ config: opts.config, command: loop, deviceSerial: opts.deviceSerial, timeoutMs: opts.timeoutMs ?? 12e4 });
|
|
1904
|
+
ensureOk(cap, "snapshot_display burst");
|
|
1905
|
+
const frames = [];
|
|
1906
|
+
try {
|
|
1907
|
+
for (let i = 0; i < count; i++) {
|
|
1908
|
+
const local = tmpName(`burst_${i}.jpeg`);
|
|
1909
|
+
try {
|
|
1910
|
+
await recvFile({ config: opts.config, remote: `${remoteDir}/f${i}.jpeg`, local, deviceSerial: opts.deviceSerial });
|
|
1911
|
+
frames.push(await fs16.promises.readFile(local));
|
|
1912
|
+
} finally {
|
|
1913
|
+
fs16.promises.unlink(local).catch(() => {
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
} finally {
|
|
1918
|
+
void shell({ config: opts.config, command: `rm -rf ${remoteDir}`, deviceSerial: opts.deviceSerial }).catch(() => {
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
return frames;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/hdc/input.ts
|
|
1925
|
+
function reqNum(value, name, type) {
|
|
1926
|
+
if (value === void 0 || Number.isNaN(value)) {
|
|
1927
|
+
throw new OniroError(`send input "${type}" requires a numeric ${name}.`);
|
|
1928
|
+
}
|
|
1929
|
+
return value;
|
|
1930
|
+
}
|
|
1931
|
+
function buildInputCommand(opts) {
|
|
1932
|
+
switch (opts.type) {
|
|
1933
|
+
case "click":
|
|
1934
|
+
case "doubleClick":
|
|
1935
|
+
case "longClick":
|
|
1936
|
+
return `uitest uiInput ${opts.type} ${reqNum(opts.pxX, "pxX", opts.type)} ${reqNum(opts.pxY, "pxY", opts.type)}`;
|
|
1937
|
+
case "swipe":
|
|
1938
|
+
case "drag":
|
|
1939
|
+
case "fling": {
|
|
1940
|
+
const base = `uitest uiInput ${opts.type} ${reqNum(opts.pxX, "pxX", opts.type)} ${reqNum(opts.pxY, "pxY", opts.type)} ${reqNum(opts.pxX2, "pxX2", opts.type)} ${reqNum(opts.pxY2, "pxY2", opts.type)}`;
|
|
1941
|
+
return opts.speed !== void 0 ? `${base} ${opts.speed}` : base;
|
|
1942
|
+
}
|
|
1943
|
+
case "keyEvent":
|
|
1944
|
+
if (opts.key === void 0 || opts.key === "") throw new OniroError('send input "keyEvent" requires a key.');
|
|
1945
|
+
return `uitest uiInput keyEvent ${opts.key}`;
|
|
1946
|
+
case "inputText": {
|
|
1947
|
+
const t = String(opts.text ?? "").replace(/'/g, `'\\''`);
|
|
1948
|
+
if (opts.pxX !== void 0 && opts.pxY !== void 0) {
|
|
1949
|
+
return `uitest uiInput inputText ${opts.pxX} ${opts.pxY} '${t}'`;
|
|
1950
|
+
}
|
|
1951
|
+
return `uitest uiInput text '${t}'`;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
async function sendInput(opts) {
|
|
1956
|
+
const cmd = buildInputCommand(opts);
|
|
1957
|
+
const res = await shell({ config: opts.config, command: cmd, deviceSerial: opts.deviceSerial, timeoutMs: 3e4, logger: opts.logger });
|
|
1958
|
+
ensureOk(res, `hdc shell ${cmd}`);
|
|
1959
|
+
}
|
|
1960
|
+
function buildGestureCommands(waypoints) {
|
|
1961
|
+
if (waypoints.length < 2) {
|
|
1962
|
+
throw new OniroError("sendGesture requires at least two waypoints.");
|
|
1963
|
+
}
|
|
1964
|
+
const cmds = [];
|
|
1965
|
+
for (let i = 1; i < waypoints.length; i++) {
|
|
1966
|
+
const a = waypoints[i - 1];
|
|
1967
|
+
const b = waypoints[i];
|
|
1968
|
+
const dt = b.t - a.t;
|
|
1969
|
+
const dist = Math.hypot(b.x - a.x, b.y - a.y);
|
|
1970
|
+
let cmd = `uitest uiInput drag ${a.x} ${a.y} ${b.x} ${b.y}`;
|
|
1971
|
+
if (dt > 0 && dist > 0) cmd += ` ${Math.round(dist / (dt / 1e3))}`;
|
|
1972
|
+
cmds.push(cmd);
|
|
1973
|
+
}
|
|
1974
|
+
return cmds;
|
|
1975
|
+
}
|
|
1976
|
+
function buildRawTouchCommand(events) {
|
|
1977
|
+
if (events.length === 0) throw new OniroError("sendRawTouch requires at least one event.");
|
|
1978
|
+
const ops = [];
|
|
1979
|
+
let prev = null;
|
|
1980
|
+
for (const ev of events) {
|
|
1981
|
+
if (prev && ev.t > prev.t) ops.push(`-i ${ev.t - prev.t}`);
|
|
1982
|
+
if (ev.type === "down") ops.push(`-d ${ev.x} ${ev.y}`);
|
|
1983
|
+
else if (ev.type === "up") ops.push(`-u ${ev.x} ${ev.y}`);
|
|
1984
|
+
else ops.push(`-m ${prev ? `${prev.x} ${prev.y} ` : ""}${ev.x} ${ev.y}`);
|
|
1985
|
+
prev = ev;
|
|
1986
|
+
}
|
|
1987
|
+
return `uinput -T ${ops.join(" ")}`;
|
|
1988
|
+
}
|
|
1989
|
+
function waypointsToRawEvents(waypoints, holdStartMs = 0, holdEndMs = 0) {
|
|
1990
|
+
if (waypoints.length < 2) throw new OniroError("sendGesture requires at least two waypoints.");
|
|
1991
|
+
const first = waypoints[0];
|
|
1992
|
+
const events = [{ type: "down", x: first.x, y: first.y, t: 0 }];
|
|
1993
|
+
for (let i = 1; i < waypoints.length; i++) {
|
|
1994
|
+
const w = waypoints[i];
|
|
1995
|
+
events.push({ type: "move", x: w.x, y: w.y, t: w.t + holdStartMs });
|
|
1996
|
+
}
|
|
1997
|
+
const last = waypoints[waypoints.length - 1];
|
|
1998
|
+
events.push({ type: "up", x: last.x, y: last.y, t: last.t + holdStartMs + holdEndMs });
|
|
1999
|
+
return events;
|
|
2000
|
+
}
|
|
2001
|
+
async function sendGesture(opts) {
|
|
2002
|
+
const wantsHold = (opts.holdStartMs ?? 0) > 0 || (opts.holdEndMs ?? 0) > 0;
|
|
2003
|
+
if (wantsHold) {
|
|
2004
|
+
await sendRawTouch({
|
|
2005
|
+
config: opts.config,
|
|
2006
|
+
events: waypointsToRawEvents(opts.waypoints, opts.holdStartMs, opts.holdEndMs),
|
|
2007
|
+
deviceSerial: opts.deviceSerial,
|
|
2008
|
+
logger: opts.logger
|
|
2009
|
+
});
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
for (const cmd of buildGestureCommands(opts.waypoints)) {
|
|
2013
|
+
const res = await shell({ config: opts.config, command: cmd, deviceSerial: opts.deviceSerial, timeoutMs: 3e4, logger: opts.logger });
|
|
2014
|
+
ensureOk(res, `hdc shell ${cmd}`);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
async function sendRawTouch(opts) {
|
|
2018
|
+
const cmd = buildRawTouchCommand(opts.events);
|
|
2019
|
+
const res = await shell({ config: opts.config, command: cmd, deviceSerial: opts.deviceSerial, timeoutMs: 3e4, logger: opts.logger });
|
|
2020
|
+
ensureOk(res, `hdc shell ${cmd}`);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// src/hdc/dump.ts
|
|
2024
|
+
import * as fs17 from "fs";
|
|
2025
|
+
import * as os10 from "os";
|
|
2026
|
+
import * as path16 from "path";
|
|
2027
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
2028
|
+
function parseBounds(s) {
|
|
2029
|
+
const m = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/.exec(s ?? "");
|
|
2030
|
+
if (!m) return null;
|
|
2031
|
+
return [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
|
|
2032
|
+
}
|
|
2033
|
+
var STRUCTURAL_TYPES = /* @__PURE__ */ new Set([
|
|
2034
|
+
"Stack",
|
|
2035
|
+
"Column",
|
|
2036
|
+
"Row",
|
|
2037
|
+
"Flex",
|
|
2038
|
+
"__Common__",
|
|
2039
|
+
"Grid",
|
|
2040
|
+
"GridItem",
|
|
2041
|
+
"List",
|
|
2042
|
+
"ListItem",
|
|
2043
|
+
"Scroll",
|
|
2044
|
+
"Swiper",
|
|
2045
|
+
"SwiperContent",
|
|
2046
|
+
"Navigator",
|
|
2047
|
+
"Tabs",
|
|
2048
|
+
"TabContent",
|
|
2049
|
+
"RelativeContainer",
|
|
2050
|
+
"WaterFlow",
|
|
2051
|
+
"FlowItem"
|
|
2052
|
+
]);
|
|
2053
|
+
var FLAG_MAP = [
|
|
2054
|
+
["clickable", "click"],
|
|
2055
|
+
["checkable", "check"],
|
|
2056
|
+
["checked", "checked"],
|
|
2057
|
+
["selected", "sel"],
|
|
2058
|
+
["focused", "focus"],
|
|
2059
|
+
["scrollable", "scroll"],
|
|
2060
|
+
["longClickable", "longClick"]
|
|
2061
|
+
];
|
|
2062
|
+
function pruneLayout(node, display) {
|
|
2063
|
+
const a = node.attributes ?? {};
|
|
2064
|
+
const px = parseBounds(a.bounds);
|
|
2065
|
+
const out = {};
|
|
2066
|
+
if (px && display) {
|
|
2067
|
+
const w = display.width || 1;
|
|
2068
|
+
const h = display.height || 1;
|
|
2069
|
+
out.b = [
|
|
2070
|
+
+(px[0] / w).toFixed(3),
|
|
2071
|
+
+(px[1] / h).toFixed(3),
|
|
2072
|
+
+(px[2] / w).toFixed(3),
|
|
2073
|
+
+(px[3] / h).toFixed(3)
|
|
2074
|
+
];
|
|
2075
|
+
out.c = [+((px[0] + px[2]) / 2 / w).toFixed(3), +((px[1] + px[3]) / 2 / h).toFixed(3)];
|
|
2076
|
+
}
|
|
2077
|
+
if (a.type && !STRUCTURAL_TYPES.has(a.type)) out.type = a.type;
|
|
2078
|
+
if (a.text) out.text = a.text;
|
|
2079
|
+
if (a.description) out.desc = a.description;
|
|
2080
|
+
if (a.id) out.id = a.id;
|
|
2081
|
+
if (a.key && a.key !== a.id) out.key = a.key;
|
|
2082
|
+
if (a.bundleName) out.bundle = a.bundleName;
|
|
2083
|
+
for (const [attr, short] of FLAG_MAP) {
|
|
2084
|
+
if (a[attr] === "true") out[short] = true;
|
|
2085
|
+
}
|
|
2086
|
+
const children = [];
|
|
2087
|
+
for (const c of node.children ?? []) {
|
|
2088
|
+
const pc = pruneLayout(c, display);
|
|
2089
|
+
if (pc) {
|
|
2090
|
+
if (Array.isArray(pc)) children.push(...pc);
|
|
2091
|
+
else children.push(pc);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
const hasSignal = Boolean(
|
|
2095
|
+
out.text || out.desc || out.id || out.key || out.click || out.scroll || out.check || out.longClick || out.bundle || out.type && out.type !== "root"
|
|
2096
|
+
);
|
|
2097
|
+
if (!hasSignal && children.length === 0) return null;
|
|
2098
|
+
if (!hasSignal) return children.length === 1 ? children[0] : children;
|
|
2099
|
+
if (children.length) out.children = children;
|
|
2100
|
+
return out;
|
|
2101
|
+
}
|
|
2102
|
+
async function dumpLayout(opts) {
|
|
2103
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2104
|
+
const bundleArg = opts.bundleName ? ` -b '${opts.bundleName.replace(/'/g, `'\\''`)}'` : "";
|
|
2105
|
+
const dump = await shell({ config: opts.config, command: `uitest dumpLayout${bundleArg}`, deviceSerial: opts.deviceSerial, timeoutMs });
|
|
2106
|
+
ensureOk(dump, "uitest dumpLayout");
|
|
2107
|
+
const m = dump.stdout.match(/saved to:\s*(\S+)/i);
|
|
2108
|
+
if (!m) throw new OniroError(`Could not parse dumpLayout output:
|
|
2109
|
+
${dump.stdout}`);
|
|
2110
|
+
const remote = m[1];
|
|
2111
|
+
const local = path16.join(os10.tmpdir(), `oniro_layout_${randomBytes3(6).toString("hex")}.json`);
|
|
2112
|
+
try {
|
|
2113
|
+
await recvFile({ config: opts.config, remote, local, deviceSerial: opts.deviceSerial, timeoutMs });
|
|
2114
|
+
const parsed = JSON.parse(await fs17.promises.readFile(local, "utf8"));
|
|
2115
|
+
const display = await getDisplaySize(opts.config, { deviceSerial: opts.deviceSerial });
|
|
2116
|
+
if (opts.prune === false) {
|
|
2117
|
+
return { display, tree: parsed };
|
|
2118
|
+
}
|
|
2119
|
+
const prunedRaw = pruneLayout(parsed, display);
|
|
2120
|
+
const tree = Array.isArray(prunedRaw) ? { children: prunedRaw } : prunedRaw ?? { children: [] };
|
|
2121
|
+
return { display, tree };
|
|
2122
|
+
} finally {
|
|
2123
|
+
fs17.promises.unlink(local).catch(() => {
|
|
2124
|
+
});
|
|
2125
|
+
void shell({ config: opts.config, command: `rm -f ${remote}`, deviceSerial: opts.deviceSerial }).catch(() => {
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
1339
2129
|
|
|
1340
2130
|
// src/hdc/hilog.ts
|
|
1341
|
-
import { spawn as
|
|
2131
|
+
import { spawn as spawn4 } from "child_process";
|
|
1342
2132
|
function setHilogLevel(opts) {
|
|
1343
2133
|
const logger = opts.logger ?? noopLogger;
|
|
1344
2134
|
const hdc = getHdcPath(opts.config);
|
|
1345
2135
|
return new Promise((resolve2, reject) => {
|
|
1346
|
-
const child =
|
|
2136
|
+
const child = spawn4(hdc, ["shell", "hilog", "-b", opts.level]);
|
|
1347
2137
|
let stderr = "";
|
|
1348
2138
|
child.stderr.on("data", (chunk) => {
|
|
1349
2139
|
stderr += chunk.toString();
|
|
@@ -1365,11 +2155,16 @@ function setHilogLevel(opts) {
|
|
|
1365
2155
|
}
|
|
1366
2156
|
function streamHilog(opts) {
|
|
1367
2157
|
const hdc = getHdcPath(opts.config);
|
|
1368
|
-
const args = [
|
|
2158
|
+
const args = [];
|
|
2159
|
+
if (opts.deviceSerial) args.push("-t", opts.deviceSerial);
|
|
2160
|
+
args.push("shell", "hilog");
|
|
1369
2161
|
if (opts.processId && opts.processId.trim() !== "") {
|
|
1370
2162
|
args.push("-P", opts.processId.trim());
|
|
1371
2163
|
}
|
|
1372
|
-
|
|
2164
|
+
if (opts.domain !== void 0 && `${opts.domain}` !== "") {
|
|
2165
|
+
args.push("-D", `${opts.domain}`);
|
|
2166
|
+
}
|
|
2167
|
+
return spawn4(hdc, args);
|
|
1373
2168
|
}
|
|
1374
2169
|
var HILOG_LINE_RE = /^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([DIWEF])\s+(\S+):\s+(.*)$/;
|
|
1375
2170
|
function parseHilogLine(line) {
|
|
@@ -1384,30 +2179,194 @@ function parseHilogLine(line) {
|
|
|
1384
2179
|
message: match[6]
|
|
1385
2180
|
};
|
|
1386
2181
|
}
|
|
2182
|
+
function entryText(e) {
|
|
2183
|
+
return `${e.tag}: ${e.message}`;
|
|
2184
|
+
}
|
|
2185
|
+
function dedupEntries(entries) {
|
|
2186
|
+
const out = [];
|
|
2187
|
+
let lastKey = "";
|
|
2188
|
+
for (const e of entries) {
|
|
2189
|
+
const key = `${e.level}|${e.tag}|${e.message}`;
|
|
2190
|
+
if (key === lastKey) continue;
|
|
2191
|
+
out.push(e);
|
|
2192
|
+
lastKey = key;
|
|
2193
|
+
}
|
|
2194
|
+
return out;
|
|
2195
|
+
}
|
|
2196
|
+
async function resolveProcessId(config, bundle, deviceSerial) {
|
|
2197
|
+
if (!bundle) return void 0;
|
|
2198
|
+
const proc = await findRunningProcess({ config, bundle, deviceSerial });
|
|
2199
|
+
return proc?.pid;
|
|
2200
|
+
}
|
|
2201
|
+
function waitForLog(opts) {
|
|
2202
|
+
return new Promise((resolve2, reject) => {
|
|
2203
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
2204
|
+
let settled = false;
|
|
2205
|
+
let child = null;
|
|
2206
|
+
let reconnectTimer;
|
|
2207
|
+
let buf = "";
|
|
2208
|
+
const cleanup = () => {
|
|
2209
|
+
clearTimeout(timer);
|
|
2210
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
2211
|
+
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
2212
|
+
if (child) {
|
|
2213
|
+
try {
|
|
2214
|
+
child.kill("SIGKILL");
|
|
2215
|
+
} catch {
|
|
2216
|
+
}
|
|
2217
|
+
child = null;
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
const finish = (entry) => {
|
|
2221
|
+
if (settled) return;
|
|
2222
|
+
settled = true;
|
|
2223
|
+
cleanup();
|
|
2224
|
+
resolve2(entry);
|
|
2225
|
+
};
|
|
2226
|
+
const fail = (err) => {
|
|
2227
|
+
if (settled) return;
|
|
2228
|
+
settled = true;
|
|
2229
|
+
cleanup();
|
|
2230
|
+
reject(err);
|
|
2231
|
+
};
|
|
2232
|
+
const onAbort = () => fail(new CancelledError("waitForLog cancelled."));
|
|
2233
|
+
const timer = setTimeout(
|
|
2234
|
+
() => fail(new OniroError(`No log line matched ${String(opts.pattern)} within ${opts.timeoutMs}ms.`)),
|
|
2235
|
+
opts.timeoutMs
|
|
2236
|
+
);
|
|
2237
|
+
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
2238
|
+
const spawnStream = async () => {
|
|
2239
|
+
if (settled) return;
|
|
2240
|
+
const processId = await resolveProcessId(opts.config, opts.bundle, opts.deviceSerial);
|
|
2241
|
+
if (settled) return;
|
|
2242
|
+
const stream = streamHilog({ config: opts.config, processId, domain: opts.domain, deviceSerial: opts.deviceSerial });
|
|
2243
|
+
child = stream;
|
|
2244
|
+
stream.stdout.on("data", (d) => {
|
|
2245
|
+
buf += d.toString();
|
|
2246
|
+
let idx;
|
|
2247
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
2248
|
+
const line = buf.slice(0, idx);
|
|
2249
|
+
buf = buf.slice(idx + 1);
|
|
2250
|
+
const entry = parseHilogLine(line);
|
|
2251
|
+
if (entry && opts.pattern.test(entryText(entry))) {
|
|
2252
|
+
finish(entry);
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
const onEnd = () => {
|
|
2258
|
+
if (settled || child !== stream) return;
|
|
2259
|
+
child = null;
|
|
2260
|
+
if (Date.now() < deadline) {
|
|
2261
|
+
reconnectTimer = setTimeout(() => void spawnStream(), 1e3);
|
|
2262
|
+
}
|
|
2263
|
+
};
|
|
2264
|
+
stream.once("close", onEnd);
|
|
2265
|
+
stream.once("error", onEnd);
|
|
2266
|
+
};
|
|
2267
|
+
void spawnStream();
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
function watchLog(opts) {
|
|
2271
|
+
return new Promise((resolve2, reject) => {
|
|
2272
|
+
const matches = [];
|
|
2273
|
+
let settled = false;
|
|
2274
|
+
let child = null;
|
|
2275
|
+
let buf = "";
|
|
2276
|
+
const cleanup = () => {
|
|
2277
|
+
clearTimeout(timer);
|
|
2278
|
+
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
2279
|
+
if (child) {
|
|
2280
|
+
try {
|
|
2281
|
+
child.kill("SIGKILL");
|
|
2282
|
+
} catch {
|
|
2283
|
+
}
|
|
2284
|
+
child = null;
|
|
2285
|
+
}
|
|
2286
|
+
};
|
|
2287
|
+
const done = () => {
|
|
2288
|
+
if (settled) return;
|
|
2289
|
+
settled = true;
|
|
2290
|
+
cleanup();
|
|
2291
|
+
resolve2(opts.dedup === false ? matches : dedupEntries(matches));
|
|
2292
|
+
};
|
|
2293
|
+
const onAbort = () => {
|
|
2294
|
+
if (settled) return;
|
|
2295
|
+
settled = true;
|
|
2296
|
+
cleanup();
|
|
2297
|
+
reject(new CancelledError("watchLog cancelled."));
|
|
2298
|
+
};
|
|
2299
|
+
const timer = setTimeout(done, opts.durationMs);
|
|
2300
|
+
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
2301
|
+
void (async () => {
|
|
2302
|
+
const processId = await resolveProcessId(opts.config, opts.bundle, opts.deviceSerial);
|
|
2303
|
+
if (settled) return;
|
|
2304
|
+
const stream = streamHilog({ config: opts.config, processId, domain: opts.domain, deviceSerial: opts.deviceSerial });
|
|
2305
|
+
child = stream;
|
|
2306
|
+
stream.stdout.on("data", (d) => {
|
|
2307
|
+
buf += d.toString();
|
|
2308
|
+
let idx;
|
|
2309
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
2310
|
+
const line = buf.slice(0, idx);
|
|
2311
|
+
buf = buf.slice(idx + 1);
|
|
2312
|
+
const entry = parseHilogLine(line);
|
|
2313
|
+
if (entry && opts.pattern.test(entryText(entry))) matches.push(entry);
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
})();
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
async function dumpLog(opts) {
|
|
2320
|
+
let pipe = "hilog -x";
|
|
2321
|
+
if (opts.domain !== void 0 && `${opts.domain}` !== "") pipe += ` -D ${opts.domain}`;
|
|
2322
|
+
if (opts.bundle) {
|
|
2323
|
+
const proc = await findRunningProcess({ config: opts.config, bundle: opts.bundle, deviceSerial: opts.deviceSerial });
|
|
2324
|
+
if (!proc) return [];
|
|
2325
|
+
pipe += ` | grep -E '\\b${proc.pid}\\b'`;
|
|
2326
|
+
}
|
|
2327
|
+
if (opts.grep) {
|
|
2328
|
+
const safeGrep = opts.grep.replace(/'/g, `'\\''`);
|
|
2329
|
+
pipe += ` | grep -E '${safeGrep}'`;
|
|
2330
|
+
}
|
|
2331
|
+
if (opts.lines && opts.lines > 0) pipe += ` | tail -n ${Math.floor(opts.lines)}`;
|
|
2332
|
+
const res = await shell({
|
|
2333
|
+
config: opts.config,
|
|
2334
|
+
command: pipe,
|
|
2335
|
+
deviceSerial: opts.deviceSerial,
|
|
2336
|
+
timeoutMs: opts.timeoutMs ?? 3e4,
|
|
2337
|
+
logger: opts.logger
|
|
2338
|
+
});
|
|
2339
|
+
return res.stdout.split("\n").map((line) => parseHilogLine(line)).filter((e) => e !== null);
|
|
2340
|
+
}
|
|
1387
2341
|
|
|
1388
2342
|
// src/build/runHvigorw.ts
|
|
1389
|
-
import * as
|
|
1390
|
-
import * as
|
|
1391
|
-
import { spawn as
|
|
2343
|
+
import * as fs18 from "fs";
|
|
2344
|
+
import * as path17 from "path";
|
|
2345
|
+
import { spawn as spawn5 } from "child_process";
|
|
2346
|
+
function buildHvigorwArgs(opts) {
|
|
2347
|
+
const args = [opts.task ?? "assembleHap", "--mode", "module"];
|
|
2348
|
+
args.push("-p", `product=${opts.product ?? "default"}`);
|
|
2349
|
+
if (opts.module) args.push("-p", `module=${opts.module}`);
|
|
2350
|
+
if (opts.buildMode) args.push("-p", `buildMode=${opts.buildMode}`);
|
|
2351
|
+
args.push("--stacktrace", "--no-daemon");
|
|
2352
|
+
if (opts.parallel === false) args.push("--no-parallel");
|
|
2353
|
+
if (opts.extraArgs?.length) args.push(...opts.extraArgs);
|
|
2354
|
+
return args;
|
|
2355
|
+
}
|
|
1392
2356
|
function runHvigorw(opts) {
|
|
1393
2357
|
const { config, projectDir } = opts;
|
|
1394
2358
|
const logger = opts.logger ?? noopLogger;
|
|
1395
|
-
if (!
|
|
2359
|
+
if (!fs18.existsSync(path17.join(projectDir, "build-profile.json5"))) {
|
|
1396
2360
|
throw new OniroError(`Not an OpenHarmony project: ${projectDir} (build-profile.json5 not found).`);
|
|
1397
2361
|
}
|
|
1398
|
-
if (!
|
|
2362
|
+
if (!fs18.existsSync(getCmdToolsPath(config))) {
|
|
1399
2363
|
throw new CmdToolsNotInstalledError(getCmdToolsPath(config));
|
|
1400
2364
|
}
|
|
1401
2365
|
const hvigorw = getHvigorwPath(config, projectDir);
|
|
1402
|
-
const args =
|
|
1403
|
-
args.push("-p", `product=${opts.product ?? "default"}`);
|
|
1404
|
-
if (opts.module) args.push("-p", `module=${opts.module}`);
|
|
1405
|
-
if (opts.buildMode) args.push("-p", `buildMode=${opts.buildMode}`);
|
|
1406
|
-
args.push("--stacktrace", "--no-parallel", "--no-daemon");
|
|
1407
|
-
if (opts.extraArgs?.length) args.push(...opts.extraArgs);
|
|
2366
|
+
const args = buildHvigorwArgs(opts);
|
|
1408
2367
|
if (process.platform !== "win32") {
|
|
1409
2368
|
try {
|
|
1410
|
-
|
|
2369
|
+
fs18.chmodSync(hvigorw, 493);
|
|
1411
2370
|
} catch {
|
|
1412
2371
|
}
|
|
1413
2372
|
}
|
|
@@ -1416,8 +2375,25 @@ function runHvigorw(opts) {
|
|
|
1416
2375
|
OHOS_BASE_SDK_HOME: getOhosBaseSdkHome(config)
|
|
1417
2376
|
};
|
|
1418
2377
|
return new Promise((resolve2, reject) => {
|
|
2378
|
+
if (opts.abortSignal?.aborted) {
|
|
2379
|
+
reject(new CancelledError("hvigorw build cancelled."));
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
1419
2382
|
logger.info(`[build] ${hvigorw} ${args.join(" ")}`);
|
|
1420
|
-
const child =
|
|
2383
|
+
const child = spawn5(hvigorw, args, { cwd: projectDir, env, shell: false });
|
|
2384
|
+
let settled = false;
|
|
2385
|
+
const cleanup = () => opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
2386
|
+
const onAbort = () => {
|
|
2387
|
+
if (settled) return;
|
|
2388
|
+
settled = true;
|
|
2389
|
+
cleanup();
|
|
2390
|
+
try {
|
|
2391
|
+
child.kill("SIGKILL");
|
|
2392
|
+
} catch {
|
|
2393
|
+
}
|
|
2394
|
+
reject(new CancelledError("hvigorw build cancelled."));
|
|
2395
|
+
};
|
|
2396
|
+
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1421
2397
|
child.stdout.on("data", (chunk) => {
|
|
1422
2398
|
const text = chunk.toString();
|
|
1423
2399
|
opts.onOutput?.(text, "stdout");
|
|
@@ -1428,8 +2404,16 @@ function runHvigorw(opts) {
|
|
|
1428
2404
|
opts.onOutput?.(text, "stderr");
|
|
1429
2405
|
logger.warn(text.trimEnd());
|
|
1430
2406
|
});
|
|
1431
|
-
child.on("error", (err) =>
|
|
2407
|
+
child.on("error", (err) => {
|
|
2408
|
+
if (settled) return;
|
|
2409
|
+
settled = true;
|
|
2410
|
+
cleanup();
|
|
2411
|
+
reject(new OniroError(`Failed to start hvigorw: ${err.message}`, err));
|
|
2412
|
+
});
|
|
1432
2413
|
child.on("close", (code) => {
|
|
2414
|
+
if (settled) return;
|
|
2415
|
+
settled = true;
|
|
2416
|
+
cleanup();
|
|
1433
2417
|
const exitCode = code ?? 1;
|
|
1434
2418
|
if (exitCode === 0) resolve2({ exitCode });
|
|
1435
2419
|
else reject(new OniroError(`hvigorw exited with code ${exitCode}`));
|
|
@@ -1437,6 +2421,137 @@ function runHvigorw(opts) {
|
|
|
1437
2421
|
});
|
|
1438
2422
|
}
|
|
1439
2423
|
|
|
2424
|
+
// src/build/discoverHaps.ts
|
|
2425
|
+
import * as fs19 from "fs";
|
|
2426
|
+
import * as path18 from "path";
|
|
2427
|
+
function moduleOf(projectDir, hapPath) {
|
|
2428
|
+
const rel = path18.relative(projectDir, hapPath);
|
|
2429
|
+
const first = rel.split(path18.sep)[0];
|
|
2430
|
+
return first && first !== ".." ? first : "(root)";
|
|
2431
|
+
}
|
|
2432
|
+
async function discoverHaps(opts) {
|
|
2433
|
+
const { projectDir } = opts;
|
|
2434
|
+
const maxDepth = opts.maxDepth ?? 8;
|
|
2435
|
+
const result = {};
|
|
2436
|
+
const walk = async (dir, depth) => {
|
|
2437
|
+
if (depth > maxDepth) return;
|
|
2438
|
+
const entries = await fs19.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2439
|
+
for (const e of entries) {
|
|
2440
|
+
const full = path18.join(dir, e.name);
|
|
2441
|
+
if (e.isDirectory()) {
|
|
2442
|
+
if (e.name === "node_modules" || e.name === "oh_modules" || e.name === ".git") continue;
|
|
2443
|
+
await walk(full, depth + 1);
|
|
2444
|
+
} else if (e.isFile() && e.name.endsWith(".hap")) {
|
|
2445
|
+
const moduleName = moduleOf(projectDir, full);
|
|
2446
|
+
const bucket = result[moduleName] ??= { signed: [], unsigned: [] };
|
|
2447
|
+
if (e.name.includes("unsigned")) bucket.unsigned.push(full);
|
|
2448
|
+
else if (e.name.includes("signed")) bucket.signed.push(full);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
await walk(projectDir, 0);
|
|
2453
|
+
return result;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// src/build/ohpm.ts
|
|
2457
|
+
import * as fs20 from "fs";
|
|
2458
|
+
import * as path19 from "path";
|
|
2459
|
+
function runOhpm(opts) {
|
|
2460
|
+
return runProcess({
|
|
2461
|
+
command: getOhpmPath(opts.config),
|
|
2462
|
+
args: [...opts.args],
|
|
2463
|
+
cwd: opts.projectDir,
|
|
2464
|
+
timeoutMs: opts.timeoutMs ?? 6e5,
|
|
2465
|
+
abortSignal: opts.abortSignal,
|
|
2466
|
+
onOutput: opts.onOutput,
|
|
2467
|
+
logger: opts.logger
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
async function ensureOhModules(opts) {
|
|
2471
|
+
if (fs20.existsSync(path19.join(opts.projectDir, "oh_modules"))) {
|
|
2472
|
+
return { installed: false };
|
|
2473
|
+
}
|
|
2474
|
+
const res = await runOhpm({
|
|
2475
|
+
config: opts.config,
|
|
2476
|
+
projectDir: opts.projectDir,
|
|
2477
|
+
args: ["install", "--all"],
|
|
2478
|
+
abortSignal: opts.abortSignal,
|
|
2479
|
+
onOutput: opts.onOutput,
|
|
2480
|
+
logger: opts.logger
|
|
2481
|
+
});
|
|
2482
|
+
ensureOk(res, "ohpm install --all");
|
|
2483
|
+
return { installed: true };
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// src/build/codelinter.ts
|
|
2487
|
+
var FINDING_RE = /^(.*?):(\d+):(?:\d+:)?\s*(error|warning|warn|info)\b[:\s-]*(.*)$/i;
|
|
2488
|
+
var TRAILING_CODE_RE = /[([]([\w@/.\-]+)[)\]]\s*$/;
|
|
2489
|
+
function parseCodelinterFindings(output) {
|
|
2490
|
+
const findings = [];
|
|
2491
|
+
for (const raw of output.split("\n")) {
|
|
2492
|
+
const m = FINDING_RE.exec(raw.trim());
|
|
2493
|
+
if (!m) continue;
|
|
2494
|
+
let message = m[4].trim();
|
|
2495
|
+
let code = "";
|
|
2496
|
+
const codeMatch = TRAILING_CODE_RE.exec(message);
|
|
2497
|
+
if (codeMatch) {
|
|
2498
|
+
code = codeMatch[1];
|
|
2499
|
+
message = message.slice(0, codeMatch.index).trim();
|
|
2500
|
+
}
|
|
2501
|
+
findings.push({ file: m[1].trim(), line: Number(m[2]), severity: m[3].toLowerCase(), code, message });
|
|
2502
|
+
}
|
|
2503
|
+
return findings;
|
|
2504
|
+
}
|
|
2505
|
+
async function runCodelinter(opts) {
|
|
2506
|
+
const res = await runProcess({
|
|
2507
|
+
command: getCmdToolsBin(opts.config, "codelinter"),
|
|
2508
|
+
args: opts.files?.length ? [...opts.files] : [],
|
|
2509
|
+
cwd: opts.projectDir,
|
|
2510
|
+
timeoutMs: opts.timeoutMs ?? 6e5,
|
|
2511
|
+
abortSignal: opts.abortSignal,
|
|
2512
|
+
logger: opts.logger
|
|
2513
|
+
});
|
|
2514
|
+
const raw = `${res.stdout}${res.stderr}`;
|
|
2515
|
+
return { code: res.code, findings: parseCodelinterFindings(raw), raw };
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// src/build/buildHap.ts
|
|
2519
|
+
async function buildHap(opts) {
|
|
2520
|
+
const logger = scopedLogger(opts.logger ?? noopLogger, "build");
|
|
2521
|
+
const warnings = [];
|
|
2522
|
+
const start = Date.now();
|
|
2523
|
+
if (opts.autoInstallDeps !== false) {
|
|
2524
|
+
const r = await ensureOhModules({
|
|
2525
|
+
config: opts.config,
|
|
2526
|
+
projectDir: opts.projectDir,
|
|
2527
|
+
abortSignal: opts.abortSignal,
|
|
2528
|
+
onOutput: opts.onOutput,
|
|
2529
|
+
logger: opts.logger
|
|
2530
|
+
});
|
|
2531
|
+
if (r.installed) logger.info("ran ohpm install --all (oh_modules was missing)");
|
|
2532
|
+
}
|
|
2533
|
+
const { exitCode } = await runHvigorw({
|
|
2534
|
+
config: opts.config,
|
|
2535
|
+
projectDir: opts.projectDir,
|
|
2536
|
+
product: opts.product,
|
|
2537
|
+
module: opts.module,
|
|
2538
|
+
buildMode: opts.mode,
|
|
2539
|
+
task: opts.task,
|
|
2540
|
+
parallel: opts.parallel,
|
|
2541
|
+
abortSignal: opts.abortSignal,
|
|
2542
|
+
onOutput: opts.onOutput,
|
|
2543
|
+
logger: opts.logger
|
|
2544
|
+
});
|
|
2545
|
+
const discoveredHaps = await discoverHaps({ projectDir: opts.projectDir });
|
|
2546
|
+
const anySigned = Object.values(discoveredHaps).some((m) => m.signed.length > 0);
|
|
2547
|
+
if (!anySigned) {
|
|
2548
|
+
warnings.push(
|
|
2549
|
+
"No signed HAPs found \u2014 configure signingConfigs in build-profile.json5 (unsigned HAPs are rejected by most devices)."
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
return { exitCode, durationMs: Date.now() - start, discoveredHaps, warnings };
|
|
2553
|
+
}
|
|
2554
|
+
|
|
1440
2555
|
// src/project/validators.ts
|
|
1441
2556
|
function isValidProjectName(name) {
|
|
1442
2557
|
if (!name) return false;
|
|
@@ -1448,21 +2563,21 @@ function isValidBundleName(bundleName) {
|
|
|
1448
2563
|
}
|
|
1449
2564
|
|
|
1450
2565
|
// src/project/templates.ts
|
|
1451
|
-
import * as
|
|
1452
|
-
import * as
|
|
2566
|
+
import * as fs21 from "fs";
|
|
2567
|
+
import * as path20 from "path";
|
|
1453
2568
|
function toHumanTemplateName(folderName) {
|
|
1454
2569
|
return folderName.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
1455
2570
|
}
|
|
1456
2571
|
function listTemplates(templateRoot) {
|
|
1457
|
-
if (!
|
|
1458
|
-
const entries =
|
|
2572
|
+
if (!fs21.existsSync(templateRoot)) return [];
|
|
2573
|
+
const entries = fs21.readdirSync(templateRoot, { withFileTypes: true });
|
|
1459
2574
|
return entries.filter((e) => e.isDirectory()).map((e) => {
|
|
1460
|
-
const dir =
|
|
1461
|
-
const metaPath =
|
|
2575
|
+
const dir = path20.join(templateRoot, e.name);
|
|
2576
|
+
const metaPath = path20.join(dir, "template.json");
|
|
1462
2577
|
let meta = {};
|
|
1463
|
-
if (
|
|
2578
|
+
if (fs21.existsSync(metaPath)) {
|
|
1464
2579
|
try {
|
|
1465
|
-
meta = JSON.parse(
|
|
2580
|
+
meta = JSON.parse(fs21.readFileSync(metaPath, "utf8"));
|
|
1466
2581
|
} catch {
|
|
1467
2582
|
}
|
|
1468
2583
|
}
|
|
@@ -1477,57 +2592,57 @@ function listTemplates(templateRoot) {
|
|
|
1477
2592
|
function validateTemplateLayout(templateDir, defaultModuleName) {
|
|
1478
2593
|
const required = [
|
|
1479
2594
|
"build-profile.json5",
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
2595
|
+
path20.join("AppScope", "app.json5"),
|
|
2596
|
+
path20.join(defaultModuleName, "src", "main", "module.json5"),
|
|
2597
|
+
path20.join(defaultModuleName, "oh-package.json5"),
|
|
1483
2598
|
"hvigorfile.ts"
|
|
1484
2599
|
];
|
|
1485
|
-
return required.filter((rel) => !
|
|
2600
|
+
return required.filter((rel) => !fs21.existsSync(path20.join(templateDir, rel)));
|
|
1486
2601
|
}
|
|
1487
2602
|
|
|
1488
2603
|
// src/project/createScaffold.ts
|
|
1489
|
-
import * as
|
|
1490
|
-
import * as
|
|
2604
|
+
import * as fs23 from "fs";
|
|
2605
|
+
import * as path21 from "path";
|
|
1491
2606
|
|
|
1492
2607
|
// src/project/jsonHelpers.ts
|
|
1493
|
-
import * as
|
|
2608
|
+
import * as fs22 from "fs";
|
|
1494
2609
|
import JSON54 from "json5";
|
|
1495
2610
|
function readJson5File(filePath) {
|
|
1496
|
-
return JSON54.parse(
|
|
2611
|
+
return JSON54.parse(fs22.readFileSync(filePath, "utf8"));
|
|
1497
2612
|
}
|
|
1498
2613
|
function writeJson5File(filePath, value) {
|
|
1499
|
-
|
|
2614
|
+
fs22.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
1500
2615
|
}
|
|
1501
2616
|
function readJsonFile(filePath) {
|
|
1502
|
-
return JSON.parse(
|
|
2617
|
+
return JSON.parse(fs22.readFileSync(filePath, "utf8"));
|
|
1503
2618
|
}
|
|
1504
2619
|
function writeJsonFile(filePath, value) {
|
|
1505
|
-
|
|
2620
|
+
fs22.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
1506
2621
|
}
|
|
1507
2622
|
|
|
1508
2623
|
// src/project/createScaffold.ts
|
|
1509
2624
|
var IGNORED_DIRS = /* @__PURE__ */ new Set(["oh_modules", "node_modules", "build", ".hvigor"]);
|
|
1510
2625
|
async function pathExists(p) {
|
|
1511
2626
|
try {
|
|
1512
|
-
await
|
|
2627
|
+
await fs23.promises.access(p);
|
|
1513
2628
|
return true;
|
|
1514
2629
|
} catch {
|
|
1515
2630
|
return false;
|
|
1516
2631
|
}
|
|
1517
2632
|
}
|
|
1518
2633
|
async function copyDirRecursive(srcDir, destDir) {
|
|
1519
|
-
await
|
|
1520
|
-
const entries = await
|
|
2634
|
+
await fs23.promises.mkdir(destDir, { recursive: true });
|
|
2635
|
+
const entries = await fs23.promises.readdir(srcDir, { withFileTypes: true });
|
|
1521
2636
|
for (const entry of entries) {
|
|
1522
|
-
const src =
|
|
1523
|
-
const dest =
|
|
2637
|
+
const src = path21.join(srcDir, entry.name);
|
|
2638
|
+
const dest = path21.join(destDir, entry.name);
|
|
1524
2639
|
if (entry.isDirectory()) {
|
|
1525
2640
|
await copyDirRecursive(src, dest);
|
|
1526
2641
|
} else if (entry.isSymbolicLink()) {
|
|
1527
2642
|
continue;
|
|
1528
2643
|
} else if (entry.isFile()) {
|
|
1529
2644
|
if (entry.name === "template.json") continue;
|
|
1530
|
-
await
|
|
2645
|
+
await fs23.promises.copyFile(src, dest);
|
|
1531
2646
|
}
|
|
1532
2647
|
}
|
|
1533
2648
|
}
|
|
@@ -1535,9 +2650,9 @@ async function normalizeJson5ToJson(projectDir, logger) {
|
|
|
1535
2650
|
const stack = [projectDir];
|
|
1536
2651
|
while (stack.length > 0) {
|
|
1537
2652
|
const currentDir = stack.pop();
|
|
1538
|
-
const entries = await
|
|
2653
|
+
const entries = await fs23.promises.readdir(currentDir, { withFileTypes: true });
|
|
1539
2654
|
for (const entry of entries) {
|
|
1540
|
-
const fullPath =
|
|
2655
|
+
const fullPath = path21.join(currentDir, entry.name);
|
|
1541
2656
|
if (entry.isDirectory()) {
|
|
1542
2657
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
1543
2658
|
stack.push(fullPath);
|
|
@@ -1559,23 +2674,23 @@ function toJavaPropertiesPath(p) {
|
|
|
1559
2674
|
function createOrUpdateLocalProperties(projectDir, sdkDir) {
|
|
1560
2675
|
const content = `sdk.dir=${toJavaPropertiesPath(sdkDir)}
|
|
1561
2676
|
`;
|
|
1562
|
-
|
|
2677
|
+
fs23.writeFileSync(path21.join(projectDir, "local.properties"), content, "utf8");
|
|
1563
2678
|
}
|
|
1564
2679
|
function renameIfExists(fromPath, toPath) {
|
|
1565
|
-
if (
|
|
1566
|
-
|
|
2680
|
+
if (fs23.existsSync(fromPath) && fromPath !== toPath) {
|
|
2681
|
+
fs23.renameSync(fromPath, toPath);
|
|
1567
2682
|
}
|
|
1568
2683
|
}
|
|
1569
2684
|
function updateTemplateConfigs(projectDir, args, sdkBaseDir) {
|
|
1570
|
-
const appJsonPath =
|
|
1571
|
-
if (
|
|
2685
|
+
const appJsonPath = path21.join(projectDir, "AppScope", "app.json5");
|
|
2686
|
+
if (fs23.existsSync(appJsonPath)) {
|
|
1572
2687
|
const appJson = readJson5File(appJsonPath);
|
|
1573
2688
|
appJson.app = appJson.app ?? {};
|
|
1574
2689
|
appJson.app.bundleName = args.bundleName;
|
|
1575
2690
|
writeJson5File(appJsonPath, appJson);
|
|
1576
2691
|
}
|
|
1577
|
-
const stringsPath =
|
|
1578
|
-
if (
|
|
2692
|
+
const stringsPath = path21.join(projectDir, "AppScope", "resources", "base", "element", "string.json");
|
|
2693
|
+
if (fs23.existsSync(stringsPath)) {
|
|
1579
2694
|
const strings = readJsonFile(stringsPath);
|
|
1580
2695
|
if (Array.isArray(strings.string)) {
|
|
1581
2696
|
const appName = strings.string.find((s) => s.name === "app_name");
|
|
@@ -1583,8 +2698,8 @@ function updateTemplateConfigs(projectDir, args, sdkBaseDir) {
|
|
|
1583
2698
|
}
|
|
1584
2699
|
writeJsonFile(stringsPath, strings);
|
|
1585
2700
|
}
|
|
1586
|
-
const buildProfilePath =
|
|
1587
|
-
if (
|
|
2701
|
+
const buildProfilePath = path21.join(projectDir, "build-profile.json5");
|
|
2702
|
+
if (fs23.existsSync(buildProfilePath)) {
|
|
1588
2703
|
const buildProfile = readJson5File(buildProfilePath);
|
|
1589
2704
|
buildProfile.app = buildProfile.app ?? {};
|
|
1590
2705
|
if (Array.isArray(buildProfile.app.products) && buildProfile.app.products.length > 0) {
|
|
@@ -1597,23 +2712,23 @@ function updateTemplateConfigs(projectDir, args, sdkBaseDir) {
|
|
|
1597
2712
|
}
|
|
1598
2713
|
writeJson5File(buildProfilePath, buildProfile);
|
|
1599
2714
|
}
|
|
1600
|
-
const moduleJsonPath =
|
|
1601
|
-
if (
|
|
2715
|
+
const moduleJsonPath = path21.join(projectDir, args.moduleName, "src", "main", "module.json5");
|
|
2716
|
+
if (fs23.existsSync(moduleJsonPath)) {
|
|
1602
2717
|
const moduleJson = readJson5File(moduleJsonPath);
|
|
1603
2718
|
moduleJson.module = moduleJson.module ?? {};
|
|
1604
2719
|
moduleJson.module.name = args.moduleName;
|
|
1605
2720
|
writeJson5File(moduleJsonPath, moduleJson);
|
|
1606
2721
|
}
|
|
1607
|
-
const moduleOhPackagePath =
|
|
1608
|
-
if (
|
|
2722
|
+
const moduleOhPackagePath = path21.join(projectDir, args.moduleName, "oh-package.json5");
|
|
2723
|
+
if (fs23.existsSync(moduleOhPackagePath)) {
|
|
1609
2724
|
const pkg = readJson5File(moduleOhPackagePath);
|
|
1610
2725
|
pkg.name = args.moduleName;
|
|
1611
2726
|
writeJson5File(moduleOhPackagePath, pkg);
|
|
1612
2727
|
}
|
|
1613
|
-
const vscodeDir =
|
|
1614
|
-
|
|
2728
|
+
const vscodeDir = path21.join(projectDir, ".vscode");
|
|
2729
|
+
fs23.mkdirSync(vscodeDir, { recursive: true });
|
|
1615
2730
|
const hapPath = `${args.moduleName}/build/default/outputs/default/${args.moduleName}-default-signed.hap`;
|
|
1616
|
-
writeJsonFile(
|
|
2731
|
+
writeJsonFile(path21.join(vscodeDir, "settings.json"), {
|
|
1617
2732
|
"oniro.hapPath": hapPath,
|
|
1618
2733
|
"files.associations": { "*.json5": "jsonc" }
|
|
1619
2734
|
});
|
|
@@ -1630,7 +2745,7 @@ async function createScaffold(opts) {
|
|
|
1630
2745
|
if (!opts.location || !await pathExists(opts.location)) {
|
|
1631
2746
|
throw new OniroError(`Location does not exist: ${opts.location}`);
|
|
1632
2747
|
}
|
|
1633
|
-
const templateDir =
|
|
2748
|
+
const templateDir = path21.join(opts.templateRoot, opts.templateId);
|
|
1634
2749
|
if (!await pathExists(templateDir)) {
|
|
1635
2750
|
throw new OniroError(`Template not found: ${templateDir}`);
|
|
1636
2751
|
}
|
|
@@ -1643,17 +2758,17 @@ async function createScaffold(opts) {
|
|
|
1643
2758
|
throw new OniroError(`Template '${opts.templateId}' is missing required files:
|
|
1644
2759
|
- ${missing.join("\n- ")}`);
|
|
1645
2760
|
}
|
|
1646
|
-
const projectDir =
|
|
2761
|
+
const projectDir = path21.join(opts.location, opts.projectName);
|
|
1647
2762
|
if (await pathExists(projectDir)) {
|
|
1648
2763
|
if (!opts.overwrite) {
|
|
1649
2764
|
throw new OniroError(`Destination already exists: ${projectDir}. Pass overwrite: true to replace it.`);
|
|
1650
2765
|
}
|
|
1651
|
-
await
|
|
2766
|
+
await fs23.promises.rm(projectDir, { recursive: true, force: true });
|
|
1652
2767
|
}
|
|
1653
2768
|
logger.info(`[create] Scaffolding ${opts.templateId} at ${projectDir}`);
|
|
1654
2769
|
await copyDirRecursive(templateDir, projectDir);
|
|
1655
2770
|
if (moduleName !== defaultModuleName) {
|
|
1656
|
-
renameIfExists(
|
|
2771
|
+
renameIfExists(path21.join(projectDir, defaultModuleName), path21.join(projectDir, moduleName));
|
|
1657
2772
|
}
|
|
1658
2773
|
updateTemplateConfigs(
|
|
1659
2774
|
projectDir,
|
|
@@ -1669,8 +2784,225 @@ async function createScaffold(opts) {
|
|
|
1669
2784
|
return { projectDir };
|
|
1670
2785
|
}
|
|
1671
2786
|
|
|
2787
|
+
// src/project/listModules.ts
|
|
2788
|
+
import * as fs24 from "fs";
|
|
2789
|
+
import * as path22 from "path";
|
|
2790
|
+
function listModules(opts) {
|
|
2791
|
+
const buildProfile = path22.join(opts.projectDir, "build-profile.json5");
|
|
2792
|
+
if (!fs24.existsSync(buildProfile)) {
|
|
2793
|
+
throw new OniroError(`build-profile.json5 not found at ${buildProfile}`);
|
|
2794
|
+
}
|
|
2795
|
+
const parsed = readJson5File(buildProfile);
|
|
2796
|
+
return (parsed.modules ?? []).filter((m) => Boolean(m.name)).map((m) => ({
|
|
2797
|
+
name: m.name,
|
|
2798
|
+
srcPath: m.srcPath ?? `./${m.name}`,
|
|
2799
|
+
targets: (m.targets ?? []).map((t) => t.name).filter((n) => Boolean(n))
|
|
2800
|
+
}));
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// src/project/abilities.ts
|
|
2804
|
+
import * as fs25 from "fs";
|
|
2805
|
+
import * as path23 from "path";
|
|
2806
|
+
function listAbilities(opts) {
|
|
2807
|
+
const moduleName = opts.moduleName ?? "entry";
|
|
2808
|
+
const moduleJsonPath = path23.join(opts.projectDir, moduleName, "src", "main", "module.json5");
|
|
2809
|
+
if (!fs25.existsSync(moduleJsonPath)) {
|
|
2810
|
+
throw new OniroError(`Could not find module.json5 at ${moduleJsonPath}`);
|
|
2811
|
+
}
|
|
2812
|
+
const parsed = readJson5File(moduleJsonPath);
|
|
2813
|
+
return (parsed.module?.abilities ?? []).filter((a) => Boolean(a.name)).map((a) => ({ name: a.name, type: a.type, visible: a.visible, srcEntry: a.srcEntry }));
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// src/project/prepareSigning.ts
|
|
2817
|
+
import * as fs26 from "fs";
|
|
2818
|
+
import * as path24 from "path";
|
|
2819
|
+
function hasSigningArtifacts(dir) {
|
|
2820
|
+
if (!fs26.existsSync(path24.join(dir, "signatures"))) return false;
|
|
2821
|
+
const buildProfile = path24.join(dir, "build-profile.json5");
|
|
2822
|
+
if (!fs26.existsSync(buildProfile)) return false;
|
|
2823
|
+
try {
|
|
2824
|
+
const parsed = readJson5File(buildProfile);
|
|
2825
|
+
return Array.isArray(parsed.app?.signingConfigs) && parsed.app.signingConfigs.length > 0;
|
|
2826
|
+
} catch {
|
|
2827
|
+
return false;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
function prepareSigning(opts) {
|
|
2831
|
+
if (hasSigningArtifacts(opts.projectDir)) {
|
|
2832
|
+
return { source: "present" };
|
|
2833
|
+
}
|
|
2834
|
+
generateSigningConfigs({
|
|
2835
|
+
projectDir: opts.projectDir,
|
|
2836
|
+
sdkHome: getOhosBaseSdkHome(opts.config),
|
|
2837
|
+
apl: opts.apl,
|
|
2838
|
+
appFeature: opts.appFeature,
|
|
2839
|
+
acls: opts.acls,
|
|
2840
|
+
passwords: opts.passwords,
|
|
2841
|
+
logger: opts.logger
|
|
2842
|
+
});
|
|
2843
|
+
return { source: "fresh" };
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// src/device/hidumper.ts
|
|
2847
|
+
async function dumpScreen(opts) {
|
|
2848
|
+
const res = await shell({ config: opts.config, command: "hidumper -s 10 -a screen", deviceSerial: opts.deviceSerial, timeoutMs: opts.timeoutMs ?? 1e4, logger: opts.logger });
|
|
2849
|
+
if (res.code !== 0) return null;
|
|
2850
|
+
const m = res.stdout.match(/render resolution=(\d+)x(\d+)/);
|
|
2851
|
+
if (!m) return null;
|
|
2852
|
+
return { width: Number(m[1]), height: Number(m[2]), raw: res.stdout };
|
|
2853
|
+
}
|
|
2854
|
+
async function dumpWindow(opts) {
|
|
2855
|
+
const res = await shell({ config: opts.config, command: "hidumper -s WindowManagerService -a -a", deviceSerial: opts.deviceSerial, timeoutMs: opts.timeoutMs ?? 15e3, logger: opts.logger });
|
|
2856
|
+
return res.stdout;
|
|
2857
|
+
}
|
|
2858
|
+
async function dumpRenderService(opts) {
|
|
2859
|
+
const res = await shell({ config: opts.config, command: "hidumper -s RenderService -a allInfo", deviceSerial: opts.deviceSerial, timeoutMs: opts.timeoutMs ?? 15e3, logger: opts.logger });
|
|
2860
|
+
return res.stdout;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// src/device/info.ts
|
|
2864
|
+
var PROPS = [
|
|
2865
|
+
["model", "const.product.model"],
|
|
2866
|
+
["manufacturer", "const.product.manufacturer"],
|
|
2867
|
+
["brand", "const.product.brand"],
|
|
2868
|
+
["osFullName", "const.ohos.fullname"],
|
|
2869
|
+
["osReleaseType", "const.ohos.releasetype"],
|
|
2870
|
+
["sdkApiVersion", "const.ohos.apiversion"],
|
|
2871
|
+
["buildVersion", "const.product.software.version"]
|
|
2872
|
+
];
|
|
2873
|
+
async function getDeviceInfo(opts) {
|
|
2874
|
+
const info = { serial: opts.deviceSerial ?? "(default)", display: null };
|
|
2875
|
+
for (const [field, prop] of PROPS) {
|
|
2876
|
+
try {
|
|
2877
|
+
const value = await paramGet(opts.config, prop, { deviceSerial: opts.deviceSerial, timeoutMs: opts.timeoutMs ?? 1e4, logger: opts.logger });
|
|
2878
|
+
if (value) info[field] = value;
|
|
2879
|
+
} catch {
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
const screen = await dumpScreen({ config: opts.config, deviceSerial: opts.deviceSerial, logger: opts.logger });
|
|
2883
|
+
if (screen) info.display = { width: screen.width, height: screen.height };
|
|
2884
|
+
return info;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// src/install/diffHapAssets.ts
|
|
2888
|
+
import StreamZip2 from "node-stream-zip";
|
|
2889
|
+
function diffEntryNames(installed, next) {
|
|
2890
|
+
const installedSet = new Set(installed);
|
|
2891
|
+
const nextSet = new Set(next);
|
|
2892
|
+
return {
|
|
2893
|
+
addedAssetPaths: [...nextSet].filter((n) => !installedSet.has(n)).sort(),
|
|
2894
|
+
removedAssetPaths: [...installedSet].filter((n) => !nextSet.has(n)).sort()
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
async function listFileEntries(hapPath) {
|
|
2898
|
+
const zip = new StreamZip2.async({ file: hapPath });
|
|
2899
|
+
try {
|
|
2900
|
+
const entries = await zip.entries();
|
|
2901
|
+
return Object.values(entries).filter((e) => !e.isDirectory).map((e) => e.name);
|
|
2902
|
+
} finally {
|
|
2903
|
+
await zip.close();
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
async function diffHapAssets(opts) {
|
|
2907
|
+
const [installed, next] = await Promise.all([listFileEntries(opts.installedHap), listFileEntries(opts.newHap)]);
|
|
2908
|
+
return diffEntryNames(installed, next);
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
// src/install/apply.ts
|
|
2912
|
+
import * as fs27 from "fs";
|
|
2913
|
+
import * as path25 from "path";
|
|
2914
|
+
function decideInstallMethod(s) {
|
|
2915
|
+
if (!s.signInfoInconsistent) return "replace";
|
|
2916
|
+
if (s.isSystemBundle && !s.allowUninstall) return "refuse";
|
|
2917
|
+
return "uninstall-install";
|
|
2918
|
+
}
|
|
2919
|
+
function decideReboot(s) {
|
|
2920
|
+
if (s.assetsChanged) return { reboot: true, reason: "asset-cache" };
|
|
2921
|
+
if (s.isSystemBundle && s.preInstallPid !== null && s.postInstallPid === s.preInstallPid) {
|
|
2922
|
+
return { reboot: true, reason: "pid-unchanged" };
|
|
2923
|
+
}
|
|
2924
|
+
return { reboot: false, reason: "none" };
|
|
2925
|
+
}
|
|
2926
|
+
var SIGN_INFO_INCONSISTENT = /9568332|sign.*info.*inconsistent/i;
|
|
2927
|
+
async function pidOf(config, bundle, deviceSerial) {
|
|
2928
|
+
const proc = await findRunningProcess({ config, bundle, deviceSerial });
|
|
2929
|
+
return proc ? Number(proc.pid) : null;
|
|
2930
|
+
}
|
|
2931
|
+
async function resolveHapPath(opts) {
|
|
2932
|
+
let hapPath = opts.hapPath;
|
|
2933
|
+
if (hapPath) {
|
|
2934
|
+
if (!path25.isAbsolute(hapPath) && opts.projectDir) hapPath = path25.join(opts.projectDir, hapPath);
|
|
2935
|
+
} else {
|
|
2936
|
+
if (!opts.projectDir) throw new OniroError("applyChanges requires hapPath or projectDir.");
|
|
2937
|
+
const haps = await discoverHaps({ projectDir: opts.projectDir });
|
|
2938
|
+
if (opts.module) {
|
|
2939
|
+
const signed = haps[opts.module]?.signed ?? [];
|
|
2940
|
+
if (signed.length === 0) throw new OniroError(`No signed HAP for module ${opts.module}. Build first.`);
|
|
2941
|
+
hapPath = signed[0];
|
|
2942
|
+
} else {
|
|
2943
|
+
const withSigned = Object.entries(haps).filter(([, m]) => m.signed.length > 0);
|
|
2944
|
+
if (withSigned.length === 0) throw new OniroError("No signed HAP found. Build first.");
|
|
2945
|
+
if (withSigned.length > 1) {
|
|
2946
|
+
throw new OniroError(
|
|
2947
|
+
`Multiple modules have signed HAPs (${withSigned.map(([k]) => k).join(", ")}). Pass module to choose.`
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
hapPath = withSigned[0][1].signed[0];
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
if (!fs27.existsSync(hapPath)) throw new OniroError(`HAP not found at ${hapPath}.`);
|
|
2954
|
+
return hapPath;
|
|
2955
|
+
}
|
|
2956
|
+
async function applyChanges(opts) {
|
|
2957
|
+
const logger = scopedLogger(opts.logger ?? noopLogger, "apply");
|
|
2958
|
+
const { config, bundle, deviceSerial } = opts;
|
|
2959
|
+
const isSystemBundle = opts.isSystemBundle ?? false;
|
|
2960
|
+
const allowUninstall = opts.allowUninstall ?? false;
|
|
2961
|
+
const hapPath = await resolveHapPath(opts);
|
|
2962
|
+
let assetsChanged = false;
|
|
2963
|
+
if (opts.installedHapPath && fs27.existsSync(opts.installedHapPath)) {
|
|
2964
|
+
try {
|
|
2965
|
+
const diff = await diffHapAssets({ installedHap: opts.installedHapPath, newHap: hapPath });
|
|
2966
|
+
assetsChanged = diff.addedAssetPaths.length > 0 || diff.removedAssetPaths.length > 0;
|
|
2967
|
+
} catch (err) {
|
|
2968
|
+
logger.debug(`asset diff skipped: ${err.message}`);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
const preInstallPid = await pidOf(config, bundle, deviceSerial);
|
|
2972
|
+
const replaceRes = await hdcExec({ config, args: ["install", "-r", hapPath], deviceSerial, timeoutMs: 3e5 });
|
|
2973
|
+
const signInfoInconsistent = SIGN_INFO_INCONSISTENT.test(`${replaceRes.stdout}${replaceRes.stderr}`);
|
|
2974
|
+
const decision = decideInstallMethod({ signInfoInconsistent, isSystemBundle, allowUninstall });
|
|
2975
|
+
let installMethod;
|
|
2976
|
+
if (decision === "refuse") {
|
|
2977
|
+
throw new OniroError(
|
|
2978
|
+
`Refusing to reinstall system bundle ${bundle}: signing certs are inconsistent (9568332). Align the signing certs first \u2014 uninstalling a system bundle can brick the device. Pass allowUninstall to override.`
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
if (decision === "uninstall-install") {
|
|
2982
|
+
logger.warn(`sign-info inconsistent for ${bundle}; uninstalling then installing fresh.`);
|
|
2983
|
+
await uninstallApp({ config, bundle, deviceSerial });
|
|
2984
|
+
ensureOk(await hdcExec({ config, args: ["install", hapPath], deviceSerial, timeoutMs: 3e5 }), `hdc install ${hapPath}`);
|
|
2985
|
+
installMethod = "uninstall-install";
|
|
2986
|
+
} else {
|
|
2987
|
+
ensureOk(replaceRes, `hdc install -r ${hapPath}`);
|
|
2988
|
+
installMethod = "replace";
|
|
2989
|
+
}
|
|
2990
|
+
let postInstallPid = await pidOf(config, bundle, deviceSerial);
|
|
2991
|
+
const rebootDecision = decideReboot({ assetsChanged, isSystemBundle, preInstallPid, postInstallPid });
|
|
2992
|
+
let method = installMethod;
|
|
2993
|
+
let cacheCleared = false;
|
|
2994
|
+
if (rebootDecision.reboot) {
|
|
2995
|
+
logger.info(`rebooting (${rebootDecision.reason}) to apply changes to ${bundle}.`);
|
|
2996
|
+
await reboot({ config, waitForBundle: bundle, deviceSerial, abortSignal: opts.abortSignal, logger: opts.logger });
|
|
2997
|
+
postInstallPid = await pidOf(config, bundle, deviceSerial);
|
|
2998
|
+
method = "reboot";
|
|
2999
|
+
cacheCleared = rebootDecision.reason === "asset-cache";
|
|
3000
|
+
}
|
|
3001
|
+
return { method, preInstallPid, postInstallPid, replaced: installMethod === "replace", cacheCleared };
|
|
3002
|
+
}
|
|
3003
|
+
|
|
1672
3004
|
// src/index.ts
|
|
1673
|
-
var VERSION = "0.
|
|
3005
|
+
var VERSION = "0.7.0";
|
|
1674
3006
|
export {
|
|
1675
3007
|
ALL_SDKS,
|
|
1676
3008
|
APL_VALUES,
|
|
@@ -1678,32 +3010,59 @@ export {
|
|
|
1678
3010
|
CancelledError,
|
|
1679
3011
|
ChecksumMismatchError,
|
|
1680
3012
|
CmdToolsNotInstalledError,
|
|
3013
|
+
CommandFailedError,
|
|
1681
3014
|
OHOS_URL_BASE,
|
|
1682
3015
|
OniroError,
|
|
1683
3016
|
SdkNotInstalledError,
|
|
1684
3017
|
UnsupportedPlatformError,
|
|
1685
3018
|
VERSION,
|
|
3019
|
+
applyChanges,
|
|
1686
3020
|
attemptHdcConnection,
|
|
3021
|
+
buildGestureCommands,
|
|
3022
|
+
buildHap,
|
|
3023
|
+
buildHvigorwArgs,
|
|
3024
|
+
buildInputCommand,
|
|
3025
|
+
buildRawTouchCommand,
|
|
3026
|
+
captureBurst,
|
|
1687
3027
|
consoleLogger,
|
|
1688
3028
|
createMaterial,
|
|
1689
3029
|
createScaffold,
|
|
3030
|
+
decideInstallMethod,
|
|
3031
|
+
decideReboot,
|
|
1690
3032
|
decryptPwd,
|
|
3033
|
+
dedupEntries,
|
|
1691
3034
|
defaultContext,
|
|
1692
3035
|
defaultPaths,
|
|
3036
|
+
detectAaStartFailure,
|
|
1693
3037
|
detectProjectSdkVersion,
|
|
3038
|
+
detectSigningConfigNames,
|
|
3039
|
+
diffEntryNames,
|
|
3040
|
+
diffHapAssets,
|
|
3041
|
+
discoverHaps,
|
|
1694
3042
|
downloadAndInstallSdk,
|
|
1695
3043
|
downloadFile,
|
|
3044
|
+
dumpLayout,
|
|
3045
|
+
dumpLog,
|
|
3046
|
+
dumpRenderService,
|
|
3047
|
+
dumpScreen,
|
|
3048
|
+
dumpWindow,
|
|
1696
3049
|
encryptPwd,
|
|
3050
|
+
ensureOhModules,
|
|
3051
|
+
ensureOk,
|
|
1697
3052
|
extractTarball,
|
|
1698
3053
|
extractZipWithProgress,
|
|
1699
3054
|
findAppProcessId,
|
|
1700
3055
|
findCmdToolsSourceDir,
|
|
3056
|
+
findRunningProcess,
|
|
3057
|
+
forceStop,
|
|
1701
3058
|
generateSigningConfigs,
|
|
1702
3059
|
getBundleName,
|
|
1703
3060
|
getCmdToolsBin,
|
|
1704
3061
|
getCmdToolsDownloadUrl,
|
|
1705
3062
|
getCmdToolsPath,
|
|
1706
3063
|
getCmdToolsStatus,
|
|
3064
|
+
getDeviceInfo,
|
|
3065
|
+
getDisplaySize,
|
|
1707
3066
|
getEmulatorDir,
|
|
1708
3067
|
getHdcPath,
|
|
1709
3068
|
getHvigorwPath,
|
|
@@ -1711,10 +3070,12 @@ export {
|
|
|
1711
3070
|
getKey,
|
|
1712
3071
|
getMainAbility,
|
|
1713
3072
|
getOhosBaseSdkHome,
|
|
3073
|
+
getOhpmPath,
|
|
1714
3074
|
getOsFolder,
|
|
1715
3075
|
getSdkFilename,
|
|
1716
3076
|
getSdkRootDir,
|
|
1717
3077
|
getSupportedSdksForUi,
|
|
3078
|
+
hdcExec,
|
|
1718
3079
|
installApp,
|
|
1719
3080
|
installCmdTools,
|
|
1720
3081
|
installEmulator,
|
|
@@ -1723,25 +3084,55 @@ export {
|
|
|
1723
3084
|
isValidBundleName,
|
|
1724
3085
|
isValidProjectName,
|
|
1725
3086
|
launchApp,
|
|
3087
|
+
listAbilities,
|
|
3088
|
+
listDevices,
|
|
3089
|
+
listModules,
|
|
1726
3090
|
listRunningProcesses,
|
|
1727
3091
|
listTemplates,
|
|
1728
3092
|
nonInteractivePrompter,
|
|
1729
3093
|
noopLogger,
|
|
1730
3094
|
noopProgress,
|
|
3095
|
+
paramGet,
|
|
3096
|
+
paramSet,
|
|
3097
|
+
parseBounds,
|
|
3098
|
+
parseCodelinterFindings,
|
|
3099
|
+
parseDeviceList,
|
|
1731
3100
|
parseHilogLine,
|
|
3101
|
+
pickSigningKind,
|
|
3102
|
+
prepareSigning,
|
|
3103
|
+
pruneLayout,
|
|
1732
3104
|
readJson5File,
|
|
1733
3105
|
readJsonFile,
|
|
3106
|
+
reboot,
|
|
3107
|
+
recvFile,
|
|
1734
3108
|
removeCmdTools,
|
|
1735
3109
|
removeEmulator,
|
|
1736
3110
|
removeSdk,
|
|
3111
|
+
runCodelinter,
|
|
1737
3112
|
runHvigorw,
|
|
3113
|
+
runOhpm,
|
|
3114
|
+
runProcess,
|
|
3115
|
+
scopedLogger,
|
|
3116
|
+
selectDevice,
|
|
3117
|
+
sendFile,
|
|
3118
|
+
sendGesture,
|
|
3119
|
+
sendInput,
|
|
3120
|
+
sendRawTouch,
|
|
1738
3121
|
setHilogLevel,
|
|
3122
|
+
shell,
|
|
1739
3123
|
startEmulator,
|
|
1740
3124
|
staticConfig,
|
|
1741
3125
|
stopEmulator,
|
|
1742
3126
|
streamHilog,
|
|
3127
|
+
takeScreenshot,
|
|
3128
|
+
uninstallApp,
|
|
1743
3129
|
validateTemplateLayout,
|
|
1744
3130
|
verifySha256,
|
|
3131
|
+
waitForBoot,
|
|
3132
|
+
waitForBundle,
|
|
3133
|
+
waitForCondition,
|
|
3134
|
+
waitForLog,
|
|
3135
|
+
watchLog,
|
|
1745
3136
|
writeJson5File,
|
|
1746
3137
|
writeJsonFile
|
|
1747
3138
|
};
|