@oniroproject/core 0.12.0 → 0.14.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 +26 -13
- package/dist/index.cjs +1227 -311
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +483 -27
- package/dist/index.d.ts +483 -27
- package/dist/index.js +1187 -311
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -89,14 +89,19 @@ var OniroError = class extends Error {
|
|
|
89
89
|
cause;
|
|
90
90
|
};
|
|
91
91
|
var SdkNotInstalledError = class extends OniroError {
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
/** `version` is the release that ships `api` (e.g. `6.0` for API 20), when one is known. */
|
|
93
|
+
constructor(api, expectedPath, version) {
|
|
94
|
+
super(
|
|
95
|
+
`SDK API ${api} is not installed at ${expectedPath}.` + (version ? ` Install it with \`oniro-app sdk install ${version}\`.` : "")
|
|
96
|
+
);
|
|
94
97
|
this.api = api;
|
|
95
98
|
this.expectedPath = expectedPath;
|
|
99
|
+
this.version = version;
|
|
96
100
|
this.name = "SdkNotInstalledError";
|
|
97
101
|
}
|
|
98
102
|
api;
|
|
99
103
|
expectedPath;
|
|
104
|
+
version;
|
|
100
105
|
};
|
|
101
106
|
var CmdToolsNotInstalledError = class extends OniroError {
|
|
102
107
|
constructor(expectedPath) {
|
|
@@ -372,6 +377,24 @@ function isProjectLocalHvigorwWorking(projectDir) {
|
|
|
372
377
|
import * as fs3 from "fs";
|
|
373
378
|
import * as path4 from "path";
|
|
374
379
|
import JSON5 from "json5";
|
|
380
|
+
function localPropertiesSdkDir(projectRoot) {
|
|
381
|
+
try {
|
|
382
|
+
const content = fs3.readFileSync(path4.join(projectRoot, "local.properties"), "utf-8");
|
|
383
|
+
const m = /^\s*sdk\.dir\s*[=:]\s*(.+?)\s*$/m.exec(content);
|
|
384
|
+
return m ? m[1].replace(/\\(.)/g, "$1") : void 0;
|
|
385
|
+
} catch {
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function findMissingProjectSdk(config, projectRoot) {
|
|
390
|
+
const api = detectProjectSdkVersion(projectRoot);
|
|
391
|
+
if (api === void 0) return null;
|
|
392
|
+
const expected = path4.join(getOhosBaseSdkHome(config), String(api));
|
|
393
|
+
if (fs3.existsSync(expected)) return null;
|
|
394
|
+
const localSdkDir = localPropertiesSdkDir(projectRoot);
|
|
395
|
+
if (localSdkDir && fs3.existsSync(path4.join(localSdkDir, String(api)))) return null;
|
|
396
|
+
return new SdkNotInstalledError(String(api), expected, ALL_SDKS.find((s) => s.api === String(api))?.version);
|
|
397
|
+
}
|
|
375
398
|
function detectProjectSdkVersion(projectRoot, logger = noopLogger) {
|
|
376
399
|
const buildProfilePath = path4.join(projectRoot, "build-profile.json5");
|
|
377
400
|
if (!fs3.existsSync(buildProfilePath)) {
|
|
@@ -557,14 +580,14 @@ var pipelineAsync = promisify(pipeline);
|
|
|
557
580
|
async function downloadFile(opts) {
|
|
558
581
|
const { url, dest, progress, abortSignal } = opts;
|
|
559
582
|
const proto = url.startsWith("https") ? https : http;
|
|
560
|
-
return new Promise((
|
|
583
|
+
return new Promise((resolve7, reject) => {
|
|
561
584
|
const s = Math.max(0, Math.min(100, opts.start ?? 0));
|
|
562
585
|
const r = Math.max(0, Math.min(100 - s, opts.range ?? 100));
|
|
563
586
|
let settled = false;
|
|
564
587
|
const done = (err) => {
|
|
565
588
|
if (settled) return;
|
|
566
589
|
settled = true;
|
|
567
|
-
err ? reject(err) :
|
|
590
|
+
err ? reject(err) : resolve7();
|
|
568
591
|
};
|
|
569
592
|
if (abortSignal?.aborted) {
|
|
570
593
|
done(new CancelledError("Download cancelled before start."));
|
|
@@ -1051,21 +1074,27 @@ async function installCmdTools(opts) {
|
|
|
1051
1074
|
removeTempWorkDir(tmpDir, tmpRoot);
|
|
1052
1075
|
}
|
|
1053
1076
|
}
|
|
1077
|
+
function detectCmdToolsFlavour(cmdToolsPath) {
|
|
1078
|
+
return fs11.existsSync(path12.join(cmdToolsPath, "sdk", "default", "hms")) ? "HarmonyOS" : "OpenHarmony";
|
|
1079
|
+
}
|
|
1054
1080
|
function getCmdToolsStatus(config) {
|
|
1055
1081
|
const cmdPath = getCmdToolsPath(config);
|
|
1082
|
+
const hdcPath = getHdcPath(config);
|
|
1083
|
+
const base = { path: cmdPath, hdc: { path: hdcPath, exists: fs11.existsSync(hdcPath) } };
|
|
1056
1084
|
const binDir = path12.join(cmdPath, "bin");
|
|
1057
1085
|
const candidates = os7.platform() === "win32" ? ["ohpm.exe", "ohpm.cmd", "ohpm.bat", "ohpm"] : ["ohpm"];
|
|
1058
1086
|
const bin = candidates.map((c) => path12.join(binDir, c)).find((p) => fs11.existsSync(p));
|
|
1059
1087
|
if (!bin) {
|
|
1060
|
-
return { installed: false, status: "Not installed" };
|
|
1088
|
+
return { installed: false, status: "Not installed", ...base };
|
|
1061
1089
|
}
|
|
1090
|
+
const installed = { installed: true, ...base, flavour: detectCmdToolsFlavour(cmdPath) };
|
|
1062
1091
|
try {
|
|
1063
1092
|
const versionFile = path12.join(cmdPath, "version.txt");
|
|
1064
1093
|
if (fs11.existsSync(versionFile)) {
|
|
1065
1094
|
const content = fs11.readFileSync(versionFile, "utf8");
|
|
1066
1095
|
for (const line of content.split(/\r?\n/)) {
|
|
1067
1096
|
const m = line.match(/^\s*#\s*Version:\s*(.+)$/);
|
|
1068
|
-
if (m?.[1]) return { installed
|
|
1097
|
+
if (m?.[1]) return { ...installed, status: `Installed (${m[1].trim()})` };
|
|
1069
1098
|
}
|
|
1070
1099
|
}
|
|
1071
1100
|
} catch {
|
|
@@ -1079,9 +1108,9 @@ function getCmdToolsStatus(config) {
|
|
|
1079
1108
|
});
|
|
1080
1109
|
if (res.error || res.status !== 0) throw res.error ?? new Error(`exit ${String(res.status)}`);
|
|
1081
1110
|
const version = res.stdout.trim();
|
|
1082
|
-
return { installed
|
|
1111
|
+
return { ...installed, status: `Installed (${version})` };
|
|
1083
1112
|
} catch {
|
|
1084
|
-
return { installed
|
|
1113
|
+
return { ...installed, status: "Installed (version unknown)" };
|
|
1085
1114
|
}
|
|
1086
1115
|
}
|
|
1087
1116
|
function isCmdToolsInstalled(config) {
|
|
@@ -1371,6 +1400,9 @@ function modifyProfileTemplate(projectDir, apl, appFeature, logger, options = {}
|
|
|
1371
1400
|
profileTemplate.acls = profileTemplate.acls ?? {};
|
|
1372
1401
|
profileTemplate.acls["allowed-acls"] = options.acls;
|
|
1373
1402
|
}
|
|
1403
|
+
if (options.appPrivilegeCapabilities !== void 0) {
|
|
1404
|
+
profileTemplate["app-privilege-capabilities"] = options.appPrivilegeCapabilities;
|
|
1405
|
+
}
|
|
1374
1406
|
fs13.writeFileSync(profileTemplatePath, JSON.stringify(profileTemplate, null, 2));
|
|
1375
1407
|
}
|
|
1376
1408
|
function generateP7bFile(projectDir, paths, logger, passwords) {
|
|
@@ -1460,6 +1492,12 @@ function updateBuildProfile(projectDir, kind, logger, passwords) {
|
|
|
1460
1492
|
storeFile: "./signatures/OpenHarmony.p12"
|
|
1461
1493
|
}
|
|
1462
1494
|
}));
|
|
1495
|
+
for (const product of buildProfile.app.products ?? []) {
|
|
1496
|
+
if (typeof product.signingConfig !== "string" || product.signingConfig === "") {
|
|
1497
|
+
product.signingConfig = configNames[0];
|
|
1498
|
+
logger.info(`[sign] Pointing a product with no signingConfig at "${configNames[0]}".`);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1463
1501
|
fs13.writeFileSync(buildProfilePath, JSON.stringify(buildProfile, null, 2));
|
|
1464
1502
|
}
|
|
1465
1503
|
function prepareMaterialDirectory(projectDir, logger) {
|
|
@@ -1491,7 +1529,7 @@ function generateSigningConfigs(options) {
|
|
|
1491
1529
|
}
|
|
1492
1530
|
const sdkPath = path14.join(sdkHome, String(sdkVersion));
|
|
1493
1531
|
if (!fs13.existsSync(sdkPath)) {
|
|
1494
|
-
throw new
|
|
1532
|
+
throw new SdkNotInstalledError(String(sdkVersion), sdkPath, ALL_SDKS.find((s) => s.api === String(sdkVersion))?.version);
|
|
1495
1533
|
}
|
|
1496
1534
|
const paths = {
|
|
1497
1535
|
signTool: path14.join(sdkPath, "toolchains/lib/hap-sign-tool.jar"),
|
|
@@ -1503,7 +1541,11 @@ function generateSigningConfigs(options) {
|
|
|
1503
1541
|
const passwords = options.passwords ?? DEFAULT_PASSWORDS;
|
|
1504
1542
|
logger.info("[sign] Starting signing configuration generation...");
|
|
1505
1543
|
copyFilesToProject(projectDir, paths, kind, logger, options.applicationCertPath);
|
|
1506
|
-
modifyProfileTemplate(projectDir, apl, appFeature, logger, {
|
|
1544
|
+
modifyProfileTemplate(projectDir, apl, appFeature, logger, {
|
|
1545
|
+
kind,
|
|
1546
|
+
acls: options.acls,
|
|
1547
|
+
appPrivilegeCapabilities: options.appPrivilegeCapabilities
|
|
1548
|
+
});
|
|
1507
1549
|
generateP7bFile(projectDir, paths, logger, passwords);
|
|
1508
1550
|
prepareMaterialDirectory(projectDir, logger);
|
|
1509
1551
|
updateBuildProfile(projectDir, kind, logger, passwords);
|
|
@@ -1570,7 +1612,7 @@ function runProcess(opts) {
|
|
|
1570
1612
|
const { command, args, cwd, env, onOutput } = opts;
|
|
1571
1613
|
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
1572
1614
|
const { abortSignal } = opts;
|
|
1573
|
-
return new Promise((
|
|
1615
|
+
return new Promise((resolve7, reject) => {
|
|
1574
1616
|
if (abortSignal?.aborted) {
|
|
1575
1617
|
reject(new CancelledError(`Command aborted before start: ${command}`));
|
|
1576
1618
|
return;
|
|
@@ -1635,21 +1677,41 @@ ${stderr.trim()}` : "")
|
|
|
1635
1677
|
);
|
|
1636
1678
|
return;
|
|
1637
1679
|
}
|
|
1638
|
-
|
|
1680
|
+
resolve7({ code: code ?? -1, stdout, stderr });
|
|
1639
1681
|
});
|
|
1640
1682
|
});
|
|
1641
1683
|
}
|
|
1642
|
-
function
|
|
1684
|
+
function resolveDeviceSerial(explicit, env = process.env) {
|
|
1685
|
+
if (explicit && explicit !== "auto") return explicit;
|
|
1686
|
+
const fromEnv = env.ONIRO_DEVICE_SERIAL || env.DEVICE_SERIAL;
|
|
1687
|
+
return fromEnv && fromEnv !== "auto" ? fromEnv : void 0;
|
|
1688
|
+
}
|
|
1689
|
+
function hdcTargetArgs(explicit) {
|
|
1690
|
+
const serial = resolveDeviceSerial(explicit);
|
|
1691
|
+
return serial ? ["-t", serial] : [];
|
|
1692
|
+
}
|
|
1693
|
+
var HOST_COMMANDS = /* @__PURE__ */ new Set(["list", "tconn", "start", "kill", "version", "checkserver"]);
|
|
1694
|
+
function hdcTargetArgsFor(args, explicit) {
|
|
1695
|
+
if (HOST_COMMANDS.has(args[0] ?? "")) return explicit ? ["-t", explicit] : [];
|
|
1696
|
+
return hdcTargetArgs(explicit);
|
|
1697
|
+
}
|
|
1698
|
+
function hdcClientFailure(output) {
|
|
1699
|
+
const first = output.split(/\r?\n/).find((l) => l.trim() !== "")?.trim() ?? "";
|
|
1700
|
+
return first.startsWith("[Fail]") ? first : null;
|
|
1701
|
+
}
|
|
1702
|
+
async function hdcExec(opts) {
|
|
1643
1703
|
const hdc = getHdcPath(opts.config);
|
|
1644
|
-
const
|
|
1645
|
-
return runProcess({
|
|
1704
|
+
const res = await runProcess({
|
|
1646
1705
|
command: hdc,
|
|
1647
|
-
args:
|
|
1706
|
+
args: [...hdcTargetArgsFor(opts.args, opts.deviceSerial), ...opts.args],
|
|
1648
1707
|
timeoutMs: opts.timeoutMs,
|
|
1649
1708
|
abortSignal: opts.abortSignal,
|
|
1650
1709
|
onOutput: opts.onOutput,
|
|
1651
1710
|
logger: opts.logger
|
|
1652
1711
|
});
|
|
1712
|
+
if (res.code === 0 && hdcClientFailure(`${res.stdout}
|
|
1713
|
+
${res.stderr}`)) return { ...res, code: 1 };
|
|
1714
|
+
return res;
|
|
1653
1715
|
}
|
|
1654
1716
|
function shell(opts) {
|
|
1655
1717
|
const { command, ...rest } = opts;
|
|
@@ -1657,10 +1719,15 @@ function shell(opts) {
|
|
|
1657
1719
|
}
|
|
1658
1720
|
function ensureOk(result, command) {
|
|
1659
1721
|
if (result.code !== 0) {
|
|
1660
|
-
throw new CommandFailedError(command, result.code, result.stderr.trim());
|
|
1722
|
+
throw new CommandFailedError(command, result.code, withTargetHint(result.stderr.trim() || result.stdout.trim()));
|
|
1661
1723
|
}
|
|
1662
1724
|
return result;
|
|
1663
1725
|
}
|
|
1726
|
+
function withTargetHint(detail) {
|
|
1727
|
+
if (!/need connect-key|Not match target/i.test(detail)) return detail;
|
|
1728
|
+
return `${detail}
|
|
1729
|
+
More than one target is connected, or the serial is unknown: pass --device <serial> or set ONIRO_DEVICE_SERIAL to one listed by \`oniro-app devices\`.`;
|
|
1730
|
+
}
|
|
1664
1731
|
var REMOTE_RC_RE = /__ONIRO_RC=(-?\d+)__/g;
|
|
1665
1732
|
function parseRemoteExit(stdout) {
|
|
1666
1733
|
let code = null;
|
|
@@ -1677,7 +1744,7 @@ async function shellChecked(opts) {
|
|
|
1677
1744
|
}
|
|
1678
1745
|
function ensureRemoteOk(result, command) {
|
|
1679
1746
|
if (result.code !== 0) {
|
|
1680
|
-
throw new CommandFailedError(command, result.code, result.stderr.trim() || result.stdout.trim());
|
|
1747
|
+
throw new CommandFailedError(command, result.code, withTargetHint(result.stderr.trim() || result.stdout.trim()));
|
|
1681
1748
|
}
|
|
1682
1749
|
return result;
|
|
1683
1750
|
}
|
|
@@ -1763,11 +1830,11 @@ async function startEmulator(opts) {
|
|
|
1763
1830
|
fs16.writeFileSync(PID_FILE, String(child.pid));
|
|
1764
1831
|
child.unref();
|
|
1765
1832
|
logger.info(`Emulator launched (PID ${child.pid}). Logs: ${logPath}`);
|
|
1766
|
-
const earlyExit = new Promise((
|
|
1767
|
-
child.once("exit", (code) =>
|
|
1768
|
-
child.once("error", () =>
|
|
1833
|
+
const earlyExit = new Promise((resolve7) => {
|
|
1834
|
+
child.once("exit", (code) => resolve7(code));
|
|
1835
|
+
child.once("error", () => resolve7(-1));
|
|
1769
1836
|
});
|
|
1770
|
-
const liveness = new Promise((
|
|
1837
|
+
const liveness = new Promise((resolve7) => setTimeout(() => resolve7("alive"), 2e3));
|
|
1771
1838
|
const outcome = await Promise.race([earlyExit, liveness]);
|
|
1772
1839
|
if (outcome !== "alive") {
|
|
1773
1840
|
const tail = readTail(logPath, 4e3);
|
|
@@ -1847,6 +1914,43 @@ async function stopEmulator(logger = noopLogger) {
|
|
|
1847
1914
|
if (err) logger.warn(`Could not remove PID file: ${err.message}`);
|
|
1848
1915
|
});
|
|
1849
1916
|
}
|
|
1917
|
+
async function isQemuRunning(logger) {
|
|
1918
|
+
if (os8.platform() !== "win32") {
|
|
1919
|
+
try {
|
|
1920
|
+
const pid = Number(fs16.readFileSync(PID_FILE, "utf8").trim());
|
|
1921
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
1922
|
+
process.kill(pid, 0);
|
|
1923
|
+
return true;
|
|
1924
|
+
}
|
|
1925
|
+
} catch {
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const [cmd, ...args] = os8.platform() === "win32" ? ["tasklist", "/FI", "IMAGENAME eq qemu-system-x86_64.exe", "/NH"] : ["pgrep", "-f", "qemu-system-x86_64"];
|
|
1929
|
+
try {
|
|
1930
|
+
const res = await runProcess({ command: cmd, args, timeoutMs: 1e4, logger });
|
|
1931
|
+
return os8.platform() === "win32" ? /qemu-system-x86_64/i.test(res.stdout) : res.code === 0;
|
|
1932
|
+
} catch {
|
|
1933
|
+
return false;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
async function getEmulatorStatus(config, opts = {}) {
|
|
1937
|
+
const logger = opts.logger ?? noopLogger;
|
|
1938
|
+
const address = opts.address ?? "127.0.0.1:55555";
|
|
1939
|
+
const dir = getEmulatorDir(config);
|
|
1940
|
+
let connected = false;
|
|
1941
|
+
try {
|
|
1942
|
+
const targets = await hdcExec({ config, args: ["list", "targets", "-v"], timeoutMs: 1e4, logger });
|
|
1943
|
+
connected = targets.stdout.split("\n").some((l) => l.trim().startsWith(address) && /\bConnected\b/.test(l));
|
|
1944
|
+
} catch {
|
|
1945
|
+
}
|
|
1946
|
+
return {
|
|
1947
|
+
installed: isEmulatorInstalled(config),
|
|
1948
|
+
dir,
|
|
1949
|
+
running: await isQemuRunning(logger),
|
|
1950
|
+
address,
|
|
1951
|
+
connected
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1850
1954
|
|
|
1851
1955
|
// src/hdc/devices.ts
|
|
1852
1956
|
var STATUS_TOKENS = /* @__PURE__ */ new Set([
|
|
@@ -1885,9 +1989,8 @@ async function listDevices(config, opts = {}) {
|
|
|
1885
1989
|
return parseDeviceList(res.stdout);
|
|
1886
1990
|
}
|
|
1887
1991
|
async function selectDevice(config, serial, opts = {}) {
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
if (envSerial && envSerial !== "auto") return envSerial;
|
|
1992
|
+
const resolved = resolveDeviceSerial(serial);
|
|
1993
|
+
if (resolved) return resolved;
|
|
1891
1994
|
const usable = (await listDevices(config, opts)).filter(
|
|
1892
1995
|
(d) => d.status === "Connected" || d.status === "Unknown"
|
|
1893
1996
|
);
|
|
@@ -1901,6 +2004,7 @@ async function selectDevice(config, serial, opts = {}) {
|
|
|
1901
2004
|
}
|
|
1902
2005
|
|
|
1903
2006
|
// src/hdc/param.ts
|
|
2007
|
+
var PARAM_MISSING_RE = /^Get parameter ".*" fail!/;
|
|
1904
2008
|
async function paramGet(config, key, opts = {}) {
|
|
1905
2009
|
const res = await shell({
|
|
1906
2010
|
config,
|
|
@@ -1910,7 +2014,30 @@ async function paramGet(config, key, opts = {}) {
|
|
|
1910
2014
|
logger: opts.logger
|
|
1911
2015
|
});
|
|
1912
2016
|
ensureOk(res, `hdc shell param get ${key}`);
|
|
1913
|
-
|
|
2017
|
+
const value = res.stdout.trim();
|
|
2018
|
+
if (PARAM_MISSING_RE.test(value)) throw new OniroError(`System parameter ${key} is not set: ${value}`);
|
|
2019
|
+
return value;
|
|
2020
|
+
}
|
|
2021
|
+
var PARAM_SEPARATOR = "__ONIRO_PARAM__";
|
|
2022
|
+
function parseParamValues(keys, stdout) {
|
|
2023
|
+
const parts = stdout.split(PARAM_SEPARATOR);
|
|
2024
|
+
const values = {};
|
|
2025
|
+
keys.forEach((key, i) => {
|
|
2026
|
+
const value = parts[i]?.trim();
|
|
2027
|
+
values[key] = value && !PARAM_MISSING_RE.test(value) ? value : void 0;
|
|
2028
|
+
});
|
|
2029
|
+
return values;
|
|
2030
|
+
}
|
|
2031
|
+
async function paramGetMany(config, keys, opts = {}) {
|
|
2032
|
+
const res = await shell({
|
|
2033
|
+
config,
|
|
2034
|
+
command: keys.map((k) => `param get ${k}; echo ${PARAM_SEPARATOR}`).join("; "),
|
|
2035
|
+
deviceSerial: opts.deviceSerial,
|
|
2036
|
+
timeoutMs: opts.timeoutMs ?? 1e4,
|
|
2037
|
+
logger: opts.logger
|
|
2038
|
+
});
|
|
2039
|
+
ensureOk(res, "hdc shell param get \u2026");
|
|
2040
|
+
return parseParamValues(keys, res.stdout);
|
|
1914
2041
|
}
|
|
1915
2042
|
async function paramSet(config, key, value, opts = {}) {
|
|
1916
2043
|
const res = await shell({
|
|
@@ -1924,29 +2051,66 @@ async function paramSet(config, key, value, opts = {}) {
|
|
|
1924
2051
|
}
|
|
1925
2052
|
|
|
1926
2053
|
// src/hdc/files.ts
|
|
2054
|
+
import * as path17 from "path";
|
|
2055
|
+
function normaliseLocalPath(local) {
|
|
2056
|
+
const abs = path17.resolve(local);
|
|
2057
|
+
const long = toLongPath(abs);
|
|
2058
|
+
return long !== abs ? long : path17.join(toLongPath(path17.dirname(abs)), path17.basename(abs));
|
|
2059
|
+
}
|
|
2060
|
+
function assertDevicePath(remote) {
|
|
2061
|
+
if (/^[A-Za-z]:[\\/]/.test(remote)) {
|
|
2062
|
+
throw new OniroError(
|
|
2063
|
+
`The device path ${remote} is a Windows path. Git Bash rewrites arguments that start with /: run the command with MSYS_NO_PATHCONV=1, or from PowerShell or cmd.`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
function appSandboxDir(bundle) {
|
|
2068
|
+
return `/data/app/el2/100/base/${bundle}`;
|
|
2069
|
+
}
|
|
2070
|
+
function shQuote(s) {
|
|
2071
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
2072
|
+
}
|
|
1927
2073
|
async function sendFile(opts) {
|
|
2074
|
+
assertDevicePath(opts.remote);
|
|
2075
|
+
const local = normaliseLocalPath(opts.local);
|
|
2076
|
+
const sandbox = opts.bundle ? appSandboxDir(opts.bundle) : void 0;
|
|
2077
|
+
const remote = sandbox && !opts.remote.startsWith("/") ? path17.posix.join(sandbox, opts.remote) : opts.remote;
|
|
2078
|
+
if (sandbox && !remote.startsWith(`${sandbox}/`)) {
|
|
2079
|
+
throw new OniroError(`${remote} is outside the sandbox of ${opts.bundle} (${sandbox}).`);
|
|
2080
|
+
}
|
|
2081
|
+
const device = { config: opts.config, deviceSerial: opts.deviceSerial, logger: opts.logger };
|
|
2082
|
+
if (sandbox) {
|
|
2083
|
+
const mkdir = `mkdir -p ${shQuote(path17.posix.dirname(remote))}`;
|
|
2084
|
+
ensureRemoteOk(await shellChecked({ ...device, command: mkdir }), `hdc shell ${mkdir}`);
|
|
2085
|
+
}
|
|
1928
2086
|
const res = await hdcExec({
|
|
1929
|
-
|
|
1930
|
-
args: ["file", "send",
|
|
1931
|
-
deviceSerial: opts.deviceSerial,
|
|
2087
|
+
...device,
|
|
2088
|
+
args: ["file", "send", local, remote],
|
|
1932
2089
|
timeoutMs: opts.timeoutMs ?? 6e5,
|
|
1933
2090
|
abortSignal: opts.abortSignal,
|
|
1934
|
-
onOutput: opts.onOutput
|
|
1935
|
-
logger: opts.logger
|
|
2091
|
+
onOutput: opts.onOutput
|
|
1936
2092
|
});
|
|
1937
|
-
ensureOk(res, `hdc file send ${
|
|
2093
|
+
ensureOk(res, `hdc file send ${local} ${remote}`);
|
|
2094
|
+
if (sandbox) {
|
|
2095
|
+
const top = path17.posix.join(sandbox, path17.posix.relative(sandbox, remote).split("/")[0]);
|
|
2096
|
+
const chown = `chown -R "$(stat -c %u:%g ${shQuote(sandbox)})" ${shQuote(top)}`;
|
|
2097
|
+
ensureRemoteOk(await shellChecked({ ...device, command: chown }), `hdc shell ${chown}`);
|
|
2098
|
+
}
|
|
2099
|
+
return remote;
|
|
1938
2100
|
}
|
|
1939
2101
|
async function recvFile(opts) {
|
|
2102
|
+
assertDevicePath(opts.remote);
|
|
2103
|
+
const local = normaliseLocalPath(opts.local);
|
|
1940
2104
|
const res = await hdcExec({
|
|
1941
2105
|
config: opts.config,
|
|
1942
|
-
args: ["file", "recv", opts.remote,
|
|
2106
|
+
args: ["file", "recv", opts.remote, local],
|
|
1943
2107
|
deviceSerial: opts.deviceSerial,
|
|
1944
2108
|
timeoutMs: opts.timeoutMs ?? 6e5,
|
|
1945
2109
|
abortSignal: opts.abortSignal,
|
|
1946
2110
|
onOutput: opts.onOutput,
|
|
1947
2111
|
logger: opts.logger
|
|
1948
2112
|
});
|
|
1949
|
-
ensureOk(res, `hdc file recv ${opts.remote} ${
|
|
2113
|
+
ensureOk(res, `hdc file recv ${opts.remote} ${local}`);
|
|
1950
2114
|
}
|
|
1951
2115
|
|
|
1952
2116
|
// src/hdc/display.ts
|
|
@@ -1966,7 +2130,7 @@ async function getDisplaySize(config, opts = {}) {
|
|
|
1966
2130
|
|
|
1967
2131
|
// src/hdc/wait.ts
|
|
1968
2132
|
function delay(ms, signal) {
|
|
1969
|
-
return new Promise((
|
|
2133
|
+
return new Promise((resolve7, reject) => {
|
|
1970
2134
|
if (signal?.aborted) {
|
|
1971
2135
|
reject(new CancelledError("Wait cancelled."));
|
|
1972
2136
|
return;
|
|
@@ -1981,7 +2145,7 @@ function delay(ms, signal) {
|
|
|
1981
2145
|
};
|
|
1982
2146
|
const timer = setTimeout(() => {
|
|
1983
2147
|
cleanup();
|
|
1984
|
-
|
|
2148
|
+
resolve7();
|
|
1985
2149
|
}, ms);
|
|
1986
2150
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1987
2151
|
});
|
|
@@ -2010,20 +2174,73 @@ async function waitForCondition(opts) {
|
|
|
2010
2174
|
}
|
|
2011
2175
|
|
|
2012
2176
|
// src/hdc/app.ts
|
|
2013
|
-
import * as
|
|
2177
|
+
import * as fs19 from "fs";
|
|
2178
|
+
import * as path20 from "path";
|
|
2179
|
+
|
|
2180
|
+
// src/build/discoverHaps.ts
|
|
2181
|
+
import * as fs17 from "fs";
|
|
2014
2182
|
import * as path18 from "path";
|
|
2183
|
+
function moduleOfHap(hapFileName) {
|
|
2184
|
+
const dash = hapFileName.indexOf("-");
|
|
2185
|
+
return dash > 0 ? hapFileName.slice(0, dash) : hapFileName.replace(/\.hap$/i, "");
|
|
2186
|
+
}
|
|
2187
|
+
async function discoverHaps(opts) {
|
|
2188
|
+
const { projectDir } = opts;
|
|
2189
|
+
const maxDepth = opts.maxDepth ?? 8;
|
|
2190
|
+
const result = {};
|
|
2191
|
+
const walk = async (dir, depth) => {
|
|
2192
|
+
if (depth > maxDepth) return;
|
|
2193
|
+
const entries = await fs17.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2194
|
+
for (const e of entries) {
|
|
2195
|
+
const full = path18.join(dir, e.name);
|
|
2196
|
+
if (e.isDirectory()) {
|
|
2197
|
+
if (e.name === "node_modules" || e.name === "oh_modules" || e.name === ".git") continue;
|
|
2198
|
+
await walk(full, depth + 1);
|
|
2199
|
+
} else if (e.isFile() && e.name.endsWith(".hap")) {
|
|
2200
|
+
const moduleName = moduleOfHap(e.name);
|
|
2201
|
+
const bucket = result[moduleName] ??= { signed: [], unsigned: [] };
|
|
2202
|
+
if (e.name.includes("unsigned")) bucket.unsigned.push(full);
|
|
2203
|
+
else if (e.name.includes("signed")) bucket.signed.push(full);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
await walk(projectDir, 0);
|
|
2208
|
+
const mtime = (p) => fs17.statSync(p).mtimeMs;
|
|
2209
|
+
for (const bucket of Object.values(result)) {
|
|
2210
|
+
bucket.signed.sort((a, b) => mtime(b) - mtime(a));
|
|
2211
|
+
bucket.unsigned.sort((a, b) => mtime(b) - mtime(a));
|
|
2212
|
+
}
|
|
2213
|
+
return result;
|
|
2214
|
+
}
|
|
2215
|
+
function pickHap(haps, module) {
|
|
2216
|
+
const pick = (name) => {
|
|
2217
|
+
const signed = haps[name]?.signed[0];
|
|
2218
|
+
if (signed) return { module: name, path: signed, signed: true };
|
|
2219
|
+
const unsigned = haps[name]?.unsigned[0];
|
|
2220
|
+
return unsigned ? { module: name, path: unsigned, signed: false } : null;
|
|
2221
|
+
};
|
|
2222
|
+
if (module) return pick(module);
|
|
2223
|
+
const names = Object.keys(haps);
|
|
2224
|
+
const withSigned = names.filter((n) => haps[n].signed.length > 0);
|
|
2225
|
+
const candidates = withSigned.length > 0 ? withSigned : names.filter((n) => haps[n].unsigned.length > 0);
|
|
2226
|
+
if (candidates.length === 0) return null;
|
|
2227
|
+
if (candidates.length > 1) {
|
|
2228
|
+
throw new OniroError(`Multiple modules have built HAPs (${candidates.join(", ")}). Pass a module to choose.`);
|
|
2229
|
+
}
|
|
2230
|
+
return pick(candidates[0]);
|
|
2231
|
+
}
|
|
2015
2232
|
|
|
2016
2233
|
// src/hdc/project.ts
|
|
2017
|
-
import * as
|
|
2018
|
-
import * as
|
|
2234
|
+
import * as fs18 from "fs";
|
|
2235
|
+
import * as path19 from "path";
|
|
2019
2236
|
import JSON53 from "json5";
|
|
2020
2237
|
function readJson5(filePath) {
|
|
2021
|
-
const content =
|
|
2238
|
+
const content = fs18.readFileSync(filePath, "utf-8");
|
|
2022
2239
|
return JSON53.parse(content);
|
|
2023
2240
|
}
|
|
2024
2241
|
function getBundleName(projectDir) {
|
|
2025
|
-
const appJsonPath =
|
|
2026
|
-
if (!
|
|
2242
|
+
const appJsonPath = path19.join(projectDir, "AppScope", "app.json5");
|
|
2243
|
+
if (!fs18.existsSync(appJsonPath)) {
|
|
2027
2244
|
throw new OniroError(`Could not find app.json5 at ${appJsonPath}`);
|
|
2028
2245
|
}
|
|
2029
2246
|
const appJson = readJson5(appJsonPath);
|
|
@@ -2033,8 +2250,8 @@ function getBundleName(projectDir) {
|
|
|
2033
2250
|
return appJson.app.bundleName;
|
|
2034
2251
|
}
|
|
2035
2252
|
function getMainAbility(projectDir, moduleName = "entry", abilityName) {
|
|
2036
|
-
const moduleJsonPath =
|
|
2037
|
-
if (!
|
|
2253
|
+
const moduleJsonPath = path19.join(projectDir, moduleName, "src", "main", "module.json5");
|
|
2254
|
+
if (!fs18.existsSync(moduleJsonPath)) {
|
|
2038
2255
|
throw new OniroError(`Could not find module.json5 at ${moduleJsonPath}`);
|
|
2039
2256
|
}
|
|
2040
2257
|
const moduleJson = readJson5(moduleJsonPath);
|
|
@@ -2054,13 +2271,67 @@ function getMainAbility(projectDir, moduleName = "entry", abilityName) {
|
|
|
2054
2271
|
}
|
|
2055
2272
|
|
|
2056
2273
|
// src/hdc/app.ts
|
|
2274
|
+
function detectInstallFailure(output) {
|
|
2275
|
+
return /\[Fail\]|msg:\s*error|error:\s*failed to install|code:\s*95\d{5}/i.test(output) ? output.trim() : null;
|
|
2276
|
+
}
|
|
2277
|
+
function isUnsignedHap(hapPath) {
|
|
2278
|
+
return path20.basename(hapPath).includes("unsigned");
|
|
2279
|
+
}
|
|
2280
|
+
function installFailureReason(output) {
|
|
2281
|
+
const code = /code:\s*(\d+)/i.exec(output);
|
|
2282
|
+
if (!code) return output.trim().split(/\r?\n/)[0] ?? "";
|
|
2283
|
+
const message = /error:\s*([^\r\n]+)/i.exec(output.slice(code.index + code[0].length))?.[1]?.trim().replace(/\.$/, "");
|
|
2284
|
+
return message ? `code ${code[1]}: ${message}` : `code ${code[1]}`;
|
|
2285
|
+
}
|
|
2286
|
+
function unsignedHapRejection(output, hap) {
|
|
2287
|
+
const name = path20.basename(hap.path);
|
|
2288
|
+
const reason = installFailureReason(output);
|
|
2289
|
+
const what = hap.fallback ? `No signed HAP was found, so the unsigned ${name} was tried, and the target rejected it (${reason}).` : `The target rejected the unsigned ${name} (${reason}).`;
|
|
2290
|
+
if (!/9568320|signature|pkcs7/i.test(reason)) return what;
|
|
2291
|
+
return `${what} This target installs only signed HAPs. Sign the project for it, build again, and install the signed HAP: \`oniro-app sign\` for an OpenHarmony device or the Oniro emulator, \`oniro-app sign --harmonyos\` for a HarmonyOS device.`;
|
|
2292
|
+
}
|
|
2293
|
+
function ensureInstallOk(result, command, hap) {
|
|
2294
|
+
const output = `${result.stdout}
|
|
2295
|
+
${result.stderr}`;
|
|
2296
|
+
if (hap && isUnsignedHap(hap.path) && /code:\s*95\d{5}/i.test(output)) {
|
|
2297
|
+
throw new OniroError(unsignedHapRejection(output, hap), new CommandFailedError(command, result.code || 1, output.trim()));
|
|
2298
|
+
}
|
|
2299
|
+
ensureOk(result, command);
|
|
2300
|
+
const failure = detectInstallFailure(output);
|
|
2301
|
+
if (failure) throw new CommandFailedError(command, 1, failure);
|
|
2302
|
+
return result;
|
|
2303
|
+
}
|
|
2304
|
+
function existingHapOrUnsigned(hapPath) {
|
|
2305
|
+
if (fs19.existsSync(hapPath)) return { path: hapPath };
|
|
2306
|
+
const sibling = hapPath.replace(/-signed\.hap$/i, "-unsigned.hap");
|
|
2307
|
+
return sibling !== hapPath && fs19.existsSync(sibling) ? { path: sibling, fallback: true } : void 0;
|
|
2308
|
+
}
|
|
2309
|
+
async function resolveInstallHap(opts, logger) {
|
|
2310
|
+
const warnIfFallback = (hap) => {
|
|
2311
|
+
if (hap.fallback) logger.warn(`No signed HAP found; installing the unsigned ${path20.basename(hap.path)}.`);
|
|
2312
|
+
return hap;
|
|
2313
|
+
};
|
|
2314
|
+
const configured = opts.hapPath ?? opts.config.get("hapPath", "");
|
|
2315
|
+
const relative3 = configured || defaultPaths.hapPath;
|
|
2316
|
+
const hapPath = path20.isAbsolute(relative3) ? relative3 : path20.join(opts.projectDir, relative3);
|
|
2317
|
+
if (configured) {
|
|
2318
|
+
const hap = existingHapOrUnsigned(hapPath);
|
|
2319
|
+
if (!hap) throw new OniroError(`HAP file not found at: ${hapPath}. Build the app first.`);
|
|
2320
|
+
return warnIfFallback(hap);
|
|
2321
|
+
}
|
|
2322
|
+
if (fs19.existsSync(hapPath)) return { path: hapPath };
|
|
2323
|
+
const haps = await discoverHaps({ projectDir: opts.projectDir });
|
|
2324
|
+
const anySigned = Object.values(haps).some((m) => m.signed.length > 0);
|
|
2325
|
+
const entryUnsigned = anySigned ? void 0 : existingHapOrUnsigned(hapPath);
|
|
2326
|
+
if (entryUnsigned) return warnIfFallback(entryUnsigned);
|
|
2327
|
+
const picked = pickHap(haps);
|
|
2328
|
+
if (!picked) throw new OniroError(`No HAP found under ${opts.projectDir}. Build the app first (\`oniro-app build\`).`);
|
|
2329
|
+
return warnIfFallback({ path: picked.path, fallback: !picked.signed });
|
|
2330
|
+
}
|
|
2057
2331
|
async function installApp(opts) {
|
|
2058
2332
|
const logger = scopedLogger(opts.logger ?? noopLogger, "hdc");
|
|
2059
|
-
const
|
|
2060
|
-
const hapPath =
|
|
2061
|
-
if (!fs18.existsSync(hapPath)) {
|
|
2062
|
-
throw new OniroError(`HAP file not found at: ${hapPath}. Build and sign the app first.`);
|
|
2063
|
-
}
|
|
2333
|
+
const hap = await resolveInstallHap(opts, logger);
|
|
2334
|
+
const hapPath = hap.path;
|
|
2064
2335
|
const result = await hdcExec({
|
|
2065
2336
|
config: opts.config,
|
|
2066
2337
|
args: ["install", hapPath],
|
|
@@ -2069,7 +2340,7 @@ async function installApp(opts) {
|
|
|
2069
2340
|
});
|
|
2070
2341
|
if (result.stdout.trim()) logger.info(result.stdout.trim());
|
|
2071
2342
|
if (result.stderr.trim()) logger.warn(result.stderr.trim());
|
|
2072
|
-
|
|
2343
|
+
ensureInstallOk(result, `hdc install ${hapPath}`, hap);
|
|
2073
2344
|
let bundleName = "";
|
|
2074
2345
|
try {
|
|
2075
2346
|
bundleName = getBundleName(opts.projectDir);
|
|
@@ -2077,6 +2348,18 @@ async function installApp(opts) {
|
|
|
2077
2348
|
}
|
|
2078
2349
|
return { installed: true, bundleName, hapPath, output: `${result.stdout}${result.stderr}`.trim() };
|
|
2079
2350
|
}
|
|
2351
|
+
async function isBundleInstalled(opts) {
|
|
2352
|
+
const safe = opts.bundle.replace(/'/g, `'\\''`);
|
|
2353
|
+
const res = await shell({
|
|
2354
|
+
config: opts.config,
|
|
2355
|
+
command: `bm dump -n '${safe}'`,
|
|
2356
|
+
deviceSerial: opts.deviceSerial,
|
|
2357
|
+
timeoutMs: opts.timeoutMs ?? 15e3,
|
|
2358
|
+
logger: opts.logger
|
|
2359
|
+
});
|
|
2360
|
+
ensureOk(res, `hdc shell bm dump -n ${opts.bundle}`);
|
|
2361
|
+
return res.stdout.includes("{") && !/failed to get information/i.test(res.stdout);
|
|
2362
|
+
}
|
|
2080
2363
|
function detectAaStartFailure(output) {
|
|
2081
2364
|
return /failed to start ability|Error Code:\s*\d|Error Message:/i.test(output) ? output.trim() : null;
|
|
2082
2365
|
}
|
|
@@ -2147,10 +2430,10 @@ function listRunningProcesses(config, options = {}) {
|
|
|
2147
2430
|
const logger = options.logger ?? noopLogger;
|
|
2148
2431
|
const timeoutMs = options.timeoutMs ?? 1e3;
|
|
2149
2432
|
const hdc = getHdcPath(config);
|
|
2150
|
-
return new Promise((
|
|
2433
|
+
return new Promise((resolve7, reject) => {
|
|
2151
2434
|
let proc;
|
|
2152
2435
|
try {
|
|
2153
|
-
proc = spawnCompat(hdc, ["track-jpid"]);
|
|
2436
|
+
proc = spawnCompat(hdc, [...hdcTargetArgs(options.deviceSerial), "track-jpid"]);
|
|
2154
2437
|
} catch (err) {
|
|
2155
2438
|
reject(new OniroError(`Failed to spawn ${hdc}: ${err instanceof Error ? err.message : String(err)}`, err));
|
|
2156
2439
|
return;
|
|
@@ -2167,7 +2450,7 @@ function listRunningProcesses(config, options = {}) {
|
|
|
2167
2450
|
if (options.targetProcessName && name === options.targetProcessName && !resolved) {
|
|
2168
2451
|
resolved = true;
|
|
2169
2452
|
killProcessTree(proc);
|
|
2170
|
-
|
|
2453
|
+
resolve7(pid);
|
|
2171
2454
|
return;
|
|
2172
2455
|
}
|
|
2173
2456
|
}
|
|
@@ -2188,7 +2471,7 @@ function listRunningProcesses(config, options = {}) {
|
|
|
2188
2471
|
if (options.targetProcessName) {
|
|
2189
2472
|
reject(new OniroError(`Could not find process for bundle: ${options.targetProcessName}`));
|
|
2190
2473
|
} else {
|
|
2191
|
-
|
|
2474
|
+
resolve7(processes);
|
|
2192
2475
|
}
|
|
2193
2476
|
}, timeoutMs);
|
|
2194
2477
|
proc.on("close", () => {
|
|
@@ -2198,7 +2481,7 @@ function listRunningProcesses(config, options = {}) {
|
|
|
2198
2481
|
if (options.targetProcessName) {
|
|
2199
2482
|
reject(new OniroError(`Could not find process for bundle: ${options.targetProcessName}`));
|
|
2200
2483
|
} else {
|
|
2201
|
-
|
|
2484
|
+
resolve7(processes);
|
|
2202
2485
|
}
|
|
2203
2486
|
});
|
|
2204
2487
|
});
|
|
@@ -2220,7 +2503,11 @@ async function findRunningProcess(opts) {
|
|
|
2220
2503
|
const pid = res.stdout.trim().split(/\s+/).filter(Boolean)[0];
|
|
2221
2504
|
if (res.code === 0 && pid && /^\d+$/.test(pid)) return { pid, name: opts.bundle };
|
|
2222
2505
|
try {
|
|
2223
|
-
const list = await listRunningProcesses(opts.config, {
|
|
2506
|
+
const list = await listRunningProcesses(opts.config, {
|
|
2507
|
+
timeoutMs: 1200,
|
|
2508
|
+
deviceSerial: opts.deviceSerial,
|
|
2509
|
+
logger: opts.logger
|
|
2510
|
+
});
|
|
2224
2511
|
if (Array.isArray(list)) {
|
|
2225
2512
|
const mine = (name) => name === opts.bundle || name.startsWith(`${opts.bundle}/`) || name.startsWith(`${opts.bundle}:`);
|
|
2226
2513
|
const match = list.find((p) => p.name === opts.bundle) ?? list.find((p) => mine(p.name));
|
|
@@ -2363,12 +2650,12 @@ async function reboot(opts) {
|
|
|
2363
2650
|
}
|
|
2364
2651
|
|
|
2365
2652
|
// src/hdc/screen.ts
|
|
2366
|
-
import * as
|
|
2653
|
+
import * as fs20 from "fs";
|
|
2367
2654
|
import * as os9 from "os";
|
|
2368
|
-
import * as
|
|
2655
|
+
import * as path21 from "path";
|
|
2369
2656
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2370
2657
|
function tmpName(ext) {
|
|
2371
|
-
return
|
|
2658
|
+
return path21.join(os9.tmpdir(), `oniro_screen_${randomBytes2(6).toString("hex")}.${ext}`);
|
|
2372
2659
|
}
|
|
2373
2660
|
async function takeScreenshot(opts) {
|
|
2374
2661
|
const remote = "/data/local/tmp/oniro_screenshot.jpeg";
|
|
@@ -2378,11 +2665,11 @@ async function takeScreenshot(opts) {
|
|
|
2378
2665
|
const local = tmpName("jpeg");
|
|
2379
2666
|
try {
|
|
2380
2667
|
await recvFile({ config: opts.config, remote, local, deviceSerial: opts.deviceSerial, timeoutMs });
|
|
2381
|
-
const pixels = await
|
|
2668
|
+
const pixels = await fs20.promises.readFile(local);
|
|
2382
2669
|
const size = await getDisplaySize(opts.config, { deviceSerial: opts.deviceSerial }) ?? { width: 0, height: 0 };
|
|
2383
2670
|
return { pixels, width: size.width, height: size.height };
|
|
2384
2671
|
} finally {
|
|
2385
|
-
|
|
2672
|
+
fs20.promises.unlink(local).catch(() => {
|
|
2386
2673
|
});
|
|
2387
2674
|
void shell({ config: opts.config, command: `rm -f ${remote}`, deviceSerial: opts.deviceSerial }).catch(() => {
|
|
2388
2675
|
});
|
|
@@ -2401,9 +2688,9 @@ async function captureBurst(opts) {
|
|
|
2401
2688
|
const local = tmpName(`burst_${i}.jpeg`);
|
|
2402
2689
|
try {
|
|
2403
2690
|
await recvFile({ config: opts.config, remote: `${remoteDir}/f${i}.jpeg`, local, deviceSerial: opts.deviceSerial });
|
|
2404
|
-
frames.push(await
|
|
2691
|
+
frames.push(await fs20.promises.readFile(local));
|
|
2405
2692
|
} finally {
|
|
2406
|
-
|
|
2693
|
+
fs20.promises.unlink(local).catch(() => {
|
|
2407
2694
|
});
|
|
2408
2695
|
}
|
|
2409
2696
|
}
|
|
@@ -2415,6 +2702,55 @@ async function captureBurst(opts) {
|
|
|
2415
2702
|
}
|
|
2416
2703
|
|
|
2417
2704
|
// src/hdc/input.ts
|
|
2705
|
+
var UITEST_KEY_NAMES = ["Back", "Home", "Power"];
|
|
2706
|
+
var KEY_IDS = {
|
|
2707
|
+
back: 2,
|
|
2708
|
+
home: 1,
|
|
2709
|
+
power: 18,
|
|
2710
|
+
volumeup: 16,
|
|
2711
|
+
volumedown: 17,
|
|
2712
|
+
up: 2012,
|
|
2713
|
+
down: 2013,
|
|
2714
|
+
left: 2014,
|
|
2715
|
+
right: 2015,
|
|
2716
|
+
alt: 2045,
|
|
2717
|
+
shift: 2047,
|
|
2718
|
+
tab: 2049,
|
|
2719
|
+
space: 2050,
|
|
2720
|
+
enter: 2054,
|
|
2721
|
+
delete: 2055,
|
|
2722
|
+
backspace: 2055,
|
|
2723
|
+
pageup: 2068,
|
|
2724
|
+
pagedown: 2069,
|
|
2725
|
+
escape: 2070,
|
|
2726
|
+
esc: 2070,
|
|
2727
|
+
forwarddelete: 2071,
|
|
2728
|
+
ctrl: 2072,
|
|
2729
|
+
meta: 2076,
|
|
2730
|
+
movehome: 2081,
|
|
2731
|
+
moveend: 2082
|
|
2732
|
+
};
|
|
2733
|
+
function keyId(name) {
|
|
2734
|
+
const k = name.trim().toLowerCase();
|
|
2735
|
+
if (/^\d+$/.test(k)) return Number(k);
|
|
2736
|
+
if (/^[a-z]$/.test(k)) return 2017 + (k.charCodeAt(0) - 97);
|
|
2737
|
+
const id = KEY_IDS[k];
|
|
2738
|
+
if (id === void 0) {
|
|
2739
|
+
throw new OniroError(
|
|
2740
|
+
`Unknown key "${name}". Use a numeric KeyCode id, a letter, or one of: ${Object.keys(KEY_IDS).join(", ")}. Combine up to three with "+", e.g. Ctrl+A.`
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
return id;
|
|
2744
|
+
}
|
|
2745
|
+
function resolveKeyEvent(key) {
|
|
2746
|
+
const spec = String(key).trim();
|
|
2747
|
+
if (spec === "") throw new OniroError('send input "keyEvent" requires a key.');
|
|
2748
|
+
const native = UITEST_KEY_NAMES.find((n) => n.toLowerCase() === spec.toLowerCase());
|
|
2749
|
+
if (native) return native;
|
|
2750
|
+
const parts = spec.includes("+") ? spec.split("+") : spec.split(/\s+/);
|
|
2751
|
+
if (parts.length > 3) throw new OniroError(`uitest injects at most three keys at once; got "${spec}".`);
|
|
2752
|
+
return parts.map(keyId).join(" ");
|
|
2753
|
+
}
|
|
2418
2754
|
function reqNum(value, name, type) {
|
|
2419
2755
|
if (value === void 0 || Number.isNaN(value)) {
|
|
2420
2756
|
throw new OniroError(`send input "${type}" requires a numeric ${name}.`);
|
|
@@ -2435,7 +2771,7 @@ function buildInputCommand(opts) {
|
|
|
2435
2771
|
}
|
|
2436
2772
|
case "keyEvent":
|
|
2437
2773
|
if (opts.key === void 0 || opts.key === "") throw new OniroError('send input "keyEvent" requires a key.');
|
|
2438
|
-
return `uitest uiInput keyEvent ${opts.key}`;
|
|
2774
|
+
return `uitest uiInput keyEvent ${resolveKeyEvent(opts.key)}`;
|
|
2439
2775
|
case "inputText": {
|
|
2440
2776
|
const t = String(opts.text ?? "").replace(/'/g, `'\\''`);
|
|
2441
2777
|
if (opts.pxX !== void 0 && opts.pxY !== void 0) {
|
|
@@ -2529,10 +2865,52 @@ async function sendRawTouch(opts) {
|
|
|
2529
2865
|
}
|
|
2530
2866
|
|
|
2531
2867
|
// src/hdc/dump.ts
|
|
2532
|
-
import * as
|
|
2868
|
+
import * as fs21 from "fs";
|
|
2533
2869
|
import * as os10 from "os";
|
|
2534
|
-
import * as
|
|
2870
|
+
import * as path22 from "path";
|
|
2535
2871
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
2872
|
+
|
|
2873
|
+
// src/hdc/ime.ts
|
|
2874
|
+
var INPUT_METHOD_SA_ID = 3703;
|
|
2875
|
+
var IME_BUNDLE_FALLBACK = /inputmethod|keyboard|(^|\.)ime(\.|$)/i;
|
|
2876
|
+
function parseImeBundles(output) {
|
|
2877
|
+
const current = [];
|
|
2878
|
+
const others = [];
|
|
2879
|
+
const entry = /\{[^{}]*"ime"\s*:\s*"([^"/]+)\/[^"]*"[^{}]*\}/g;
|
|
2880
|
+
for (const m of output.matchAll(entry)) {
|
|
2881
|
+
const bundle = m[1];
|
|
2882
|
+
const list = /"isCurrentIme"\s*:\s*"true"/.test(m[0]) ? current : others;
|
|
2883
|
+
if (!current.includes(bundle) && !others.includes(bundle)) list.push(bundle);
|
|
2884
|
+
}
|
|
2885
|
+
return [...current, ...others];
|
|
2886
|
+
}
|
|
2887
|
+
async function getImeBundles(config, opts = {}) {
|
|
2888
|
+
try {
|
|
2889
|
+
const res = await shell({
|
|
2890
|
+
config,
|
|
2891
|
+
command: `hidumper -s ${INPUT_METHOD_SA_ID} -a '-a'`,
|
|
2892
|
+
deviceSerial: opts.deviceSerial,
|
|
2893
|
+
timeoutMs: 1e4,
|
|
2894
|
+
logger: opts.logger
|
|
2895
|
+
});
|
|
2896
|
+
if (res.code === 0) return parseImeBundles(res.stdout);
|
|
2897
|
+
} catch (err) {
|
|
2898
|
+
opts.logger?.debug(`could not list input methods: ${err instanceof Error ? err.message : String(err)}`);
|
|
2899
|
+
}
|
|
2900
|
+
return [];
|
|
2901
|
+
}
|
|
2902
|
+
function findKeyboardWindow(tree, imeBundles) {
|
|
2903
|
+
const isIme = (bundle) => imeBundles.length > 0 ? imeBundles.includes(bundle) : IME_BUNDLE_FALLBACK.test(bundle);
|
|
2904
|
+
const stack = [tree];
|
|
2905
|
+
while (stack.length > 0) {
|
|
2906
|
+
const n = stack.shift();
|
|
2907
|
+
if (n.bundle && isIme(n.bundle)) return n;
|
|
2908
|
+
stack.push(...n.children ?? []);
|
|
2909
|
+
}
|
|
2910
|
+
return null;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// src/hdc/dump.ts
|
|
2536
2914
|
function parseBounds(s) {
|
|
2537
2915
|
const m = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/.exec(s ?? "");
|
|
2538
2916
|
if (!m) return null;
|
|
@@ -2567,7 +2945,46 @@ var FLAG_MAP = [
|
|
|
2567
2945
|
["scrollable", "scroll"],
|
|
2568
2946
|
["longClickable", "longClick"]
|
|
2569
2947
|
];
|
|
2570
|
-
function
|
|
2948
|
+
function colorAlpha(color) {
|
|
2949
|
+
const m = /^#([0-9a-f]{2})?[0-9a-f]{6}$/i.exec(color ?? "");
|
|
2950
|
+
if (!m) return 0;
|
|
2951
|
+
return m[1] === void 0 ? 255 : parseInt(m[1], 16);
|
|
2952
|
+
}
|
|
2953
|
+
function paintsFill(a) {
|
|
2954
|
+
if (a.visible === "false") return false;
|
|
2955
|
+
if (a.opacity !== void 0 && a.opacity !== "" && !(Number(a.opacity) > 0)) return false;
|
|
2956
|
+
return colorAlpha(a.backgroundColor) > 0;
|
|
2957
|
+
}
|
|
2958
|
+
function findCoveredNodes(root) {
|
|
2959
|
+
const order = [];
|
|
2960
|
+
const fills = [];
|
|
2961
|
+
const visit = (n) => {
|
|
2962
|
+
const a = n.attributes ?? {};
|
|
2963
|
+
const entry = { node: n, bounds: parseBounds(a.bounds), end: 0 };
|
|
2964
|
+
if (entry.bounds && paintsFill(a)) fills.push(order.length);
|
|
2965
|
+
order.push(entry);
|
|
2966
|
+
const kids = (n.children ?? []).map((c, i) => ({ c, i, z: Number(c.attributes?.zIndex) || 0 })).sort((p, q) => p.z - q.z || p.i - q.i);
|
|
2967
|
+
for (const k of kids) visit(k.c);
|
|
2968
|
+
entry.end = order.length;
|
|
2969
|
+
};
|
|
2970
|
+
visit(root);
|
|
2971
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2972
|
+
for (const p of order) {
|
|
2973
|
+
if (!p.bounds) continue;
|
|
2974
|
+
const cx = (p.bounds[0] + p.bounds[2]) / 2;
|
|
2975
|
+
const cy = (p.bounds[1] + p.bounds[3]) / 2;
|
|
2976
|
+
for (const j of fills) {
|
|
2977
|
+
if (j < p.end) continue;
|
|
2978
|
+
const q = order[j].bounds;
|
|
2979
|
+
if (cx >= q[0] && cx < q[2] && cy >= q[1] && cy < q[3]) {
|
|
2980
|
+
covered.add(p.node);
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
return covered;
|
|
2986
|
+
}
|
|
2987
|
+
function pruneLayout(node, display, covered) {
|
|
2571
2988
|
const a = node.attributes ?? {};
|
|
2572
2989
|
const px = parseBounds(a.bounds);
|
|
2573
2990
|
const out = {};
|
|
@@ -2585,28 +3002,125 @@ function pruneLayout(node, display) {
|
|
|
2585
3002
|
if (a.type && !STRUCTURAL_TYPES.has(a.type)) out.type = a.type;
|
|
2586
3003
|
if (a.text) out.text = a.text;
|
|
2587
3004
|
if (a.description) out.desc = a.description;
|
|
3005
|
+
if (a.hint) out.hint = a.hint;
|
|
2588
3006
|
if (a.id) out.id = a.id;
|
|
2589
3007
|
if (a.key && a.key !== a.id) out.key = a.key;
|
|
2590
3008
|
if (a.bundleName) out.bundle = a.bundleName;
|
|
2591
3009
|
for (const [attr, short] of FLAG_MAP) {
|
|
2592
3010
|
if (a[attr] === "true") out[short] = true;
|
|
2593
3011
|
}
|
|
3012
|
+
if (a.enabled === "false") out.disabled = true;
|
|
3013
|
+
if (covered?.has(node)) out.covered = true;
|
|
2594
3014
|
const children = [];
|
|
2595
3015
|
for (const c of node.children ?? []) {
|
|
2596
|
-
const pc = pruneLayout(c, display);
|
|
3016
|
+
const pc = pruneLayout(c, display, covered);
|
|
2597
3017
|
if (pc) {
|
|
2598
3018
|
if (Array.isArray(pc)) children.push(...pc);
|
|
2599
3019
|
else children.push(pc);
|
|
2600
3020
|
}
|
|
2601
3021
|
}
|
|
2602
3022
|
const hasSignal = Boolean(
|
|
2603
|
-
out.text || out.desc || out.id || out.key || out.click || out.scroll || out.check || out.longClick || out.bundle || out.type && out.type !== "root"
|
|
3023
|
+
out.text || out.desc || out.hint || out.id || out.key || out.click || out.scroll || out.check || out.longClick || out.bundle || out.type && out.type !== "root"
|
|
2604
3024
|
);
|
|
2605
3025
|
if (!hasSignal && children.length === 0) return null;
|
|
2606
3026
|
if (!hasSignal) return children.length === 1 ? children[0] : children;
|
|
2607
3027
|
if (children.length) out.children = children;
|
|
2608
3028
|
return out;
|
|
2609
3029
|
}
|
|
3030
|
+
function flattenLayout(tree) {
|
|
3031
|
+
const out = [];
|
|
3032
|
+
const visit = (n, window) => {
|
|
3033
|
+
const { children, ...rest } = n;
|
|
3034
|
+
const win = n.bundle ?? window;
|
|
3035
|
+
if (Object.keys(rest).length > 0) out.push({ ...rest, ...win ? { window: win } : {} });
|
|
3036
|
+
for (const c of children ?? []) visit(c, win);
|
|
3037
|
+
};
|
|
3038
|
+
visit(tree, void 0);
|
|
3039
|
+
return out;
|
|
3040
|
+
}
|
|
3041
|
+
function assertLayoutSelector(selector) {
|
|
3042
|
+
if (selector.text === void 0 && selector.id === void 0 && selector.type === void 0) {
|
|
3043
|
+
throw new OniroError("A layout selector needs a text, id, or type.");
|
|
3044
|
+
}
|
|
3045
|
+
if (selector.text !== void 0 && selector.text.trim() === "") {
|
|
3046
|
+
throw new OniroError("A layout selector text must not be blank.");
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
function withoutWindow(tree, bundle) {
|
|
3050
|
+
const children = (tree.children ?? []).filter((c) => c.bundle !== bundle).map((c) => withoutWindow(c, bundle));
|
|
3051
|
+
return tree.children ? { ...tree, children } : tree;
|
|
3052
|
+
}
|
|
3053
|
+
function findLayoutNodes(tree, selector) {
|
|
3054
|
+
assertLayoutSelector(selector);
|
|
3055
|
+
const type = selector.type?.toLowerCase();
|
|
3056
|
+
const nodes = flattenLayout(tree).filter(
|
|
3057
|
+
(n) => (type === void 0 || n.type?.toLowerCase() === type) && (selector.id === void 0 || n.id === selector.id || n.key === selector.id)
|
|
3058
|
+
);
|
|
3059
|
+
if (selector.text === void 0) return nodes;
|
|
3060
|
+
const want = selector.text.trim().toLowerCase();
|
|
3061
|
+
const labels = (n) => [n.text, n.desc, n.hint].filter((s) => !!s).map((s) => s.trim().toLowerCase());
|
|
3062
|
+
const exact = nodes.filter((n) => labels(n).some((l) => l === want));
|
|
3063
|
+
if (exact.length > 0) return exact;
|
|
3064
|
+
return nodes.filter((n) => labels(n).some((l) => l.includes(want)));
|
|
3065
|
+
}
|
|
3066
|
+
function describeSelector(s) {
|
|
3067
|
+
const parts = [];
|
|
3068
|
+
if (s.text !== void 0) parts.push(`text "${s.text}"`);
|
|
3069
|
+
if (s.id !== void 0) parts.push(`id "${s.id}"`);
|
|
3070
|
+
if (s.type !== void 0) parts.push(`type ${s.type}`);
|
|
3071
|
+
return parts.join(", ");
|
|
3072
|
+
}
|
|
3073
|
+
function describeLayoutNode(n) {
|
|
3074
|
+
const label = n.text ?? n.desc ?? (n.hint !== void 0 ? `hint: ${n.hint}` : void 0);
|
|
3075
|
+
const at = n.c ? ` at ${n.c[0].toFixed(3)},${n.c[1].toFixed(3)}` : "";
|
|
3076
|
+
return `${n.type ?? "node"}${label !== void 0 ? ` "${label}"` : ""}${n.id ? ` #${n.id}` : ""}${at}`;
|
|
3077
|
+
}
|
|
3078
|
+
var area = (b) => b ? (b[2] - b[0]) * (b[3] - b[1]) : 0;
|
|
3079
|
+
function enclosesTarget(outer, inner) {
|
|
3080
|
+
const b = outer.b;
|
|
3081
|
+
const c = inner.c;
|
|
3082
|
+
if (!b || !c) return false;
|
|
3083
|
+
return c[0] >= b[0] && c[0] <= b[2] && c[1] >= b[1] && c[1] <= b[3] && area(b) > area(inner.b);
|
|
3084
|
+
}
|
|
3085
|
+
var TargetNotFoundError = class extends OniroError {
|
|
3086
|
+
constructor(message, reason) {
|
|
3087
|
+
super(message);
|
|
3088
|
+
this.reason = reason;
|
|
3089
|
+
this.name = "TargetNotFoundError";
|
|
3090
|
+
}
|
|
3091
|
+
reason;
|
|
3092
|
+
};
|
|
3093
|
+
function pickTarget(tree, selector, index) {
|
|
3094
|
+
const matches = findLayoutNodes(tree, selector).filter((n) => n.c !== void 0);
|
|
3095
|
+
const what = describeSelector(selector);
|
|
3096
|
+
if (matches.length === 0) {
|
|
3097
|
+
throw new TargetNotFoundError(`No on-screen element matches ${what}. Run \`oniro-app dump layout --flat\` to see what is there.`, "none");
|
|
3098
|
+
}
|
|
3099
|
+
const visible = matches.filter((n) => !n.covered);
|
|
3100
|
+
if (visible.length === 0) {
|
|
3101
|
+
throw new TargetNotFoundError(
|
|
3102
|
+
`Found ${matches.length} element(s) matching ${what}, but each is covered by an overlay (dialog, keyboard, or another layer): ${matches.map(describeLayoutNode).join("; ")}.`,
|
|
3103
|
+
"covered"
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
const clickable = visible.filter((n) => n.click);
|
|
3107
|
+
const preferred = clickable.length > 0 ? clickable : visible;
|
|
3108
|
+
const candidates = preferred.filter((n) => !preferred.some((o) => o !== n && o.click && enclosesTarget(o, n)));
|
|
3109
|
+
if (index !== void 0) {
|
|
3110
|
+
const picked = candidates[index];
|
|
3111
|
+
if (!picked) {
|
|
3112
|
+
throw new TargetNotFoundError(`Index ${index} is out of range: ${candidates.length} element(s) match ${what}.`, "ambiguous");
|
|
3113
|
+
}
|
|
3114
|
+
return picked;
|
|
3115
|
+
}
|
|
3116
|
+
if (candidates.length > 1) {
|
|
3117
|
+
throw new TargetNotFoundError(
|
|
3118
|
+
`${candidates.length} elements match ${what}; pass an index to choose: ` + candidates.map((n, i) => `[${i}] ${describeLayoutNode(n)}`).join("; "),
|
|
3119
|
+
"ambiguous"
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
return candidates[0];
|
|
3123
|
+
}
|
|
2610
3124
|
async function dumpLayout(opts) {
|
|
2611
3125
|
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2612
3126
|
const bundleArg = opts.bundleName ? ` -b '${opts.bundleName.replace(/'/g, `'\\''`)}'` : "";
|
|
@@ -2616,33 +3130,193 @@ async function dumpLayout(opts) {
|
|
|
2616
3130
|
if (!m) throw new OniroError(`Could not parse dumpLayout output:
|
|
2617
3131
|
${dump.stdout}`);
|
|
2618
3132
|
const remote = m[1];
|
|
2619
|
-
const local =
|
|
3133
|
+
const local = path22.join(os10.tmpdir(), `oniro_layout_${randomBytes3(6).toString("hex")}.json`);
|
|
2620
3134
|
try {
|
|
2621
3135
|
await recvFile({ config: opts.config, remote, local, deviceSerial: opts.deviceSerial, timeoutMs });
|
|
2622
|
-
const parsed = JSON.parse(await
|
|
3136
|
+
const parsed = JSON.parse(await fs21.promises.readFile(local, "utf8"));
|
|
2623
3137
|
const display = await getDisplaySize(opts.config, { deviceSerial: opts.deviceSerial });
|
|
2624
3138
|
if (opts.prune === false) {
|
|
2625
3139
|
return { display, tree: parsed };
|
|
2626
3140
|
}
|
|
2627
|
-
const prunedRaw = pruneLayout(parsed, display);
|
|
3141
|
+
const prunedRaw = pruneLayout(parsed, display, findCoveredNodes(parsed));
|
|
2628
3142
|
const tree = Array.isArray(prunedRaw) ? { children: prunedRaw } : prunedRaw ?? { children: [] };
|
|
2629
|
-
return { display, tree };
|
|
3143
|
+
if (!opts.detectKeyboard || opts.bundleName) return { display, tree };
|
|
3144
|
+
const imeBundles = await getImeBundles(opts.config, { deviceSerial: opts.deviceSerial, logger: opts.logger });
|
|
3145
|
+
const kb = findKeyboardWindow(tree, imeBundles);
|
|
3146
|
+
const keyboard = kb ? { shown: true, bundle: kb.bundle, ...kb.b ? { b: kb.b } : {} } : { shown: false };
|
|
3147
|
+
return { display, tree, keyboard };
|
|
2630
3148
|
} finally {
|
|
2631
|
-
|
|
3149
|
+
fs21.promises.unlink(local).catch(() => {
|
|
2632
3150
|
});
|
|
2633
3151
|
void shell({ config: opts.config, command: `rm -f ${remote}`, deviceSerial: opts.deviceSerial }).catch(() => {
|
|
2634
3152
|
});
|
|
2635
3153
|
}
|
|
2636
3154
|
}
|
|
2637
3155
|
|
|
3156
|
+
// src/hdc/uiActions.ts
|
|
3157
|
+
var FOCUS_SETTLE_MS = 400;
|
|
3158
|
+
var KEYBOARD_SETTLE_MS = 400;
|
|
3159
|
+
var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
3160
|
+
function targetTree(layout) {
|
|
3161
|
+
const kb = layout.keyboard;
|
|
3162
|
+
return kb?.shown && kb.bundle ? withoutWindow(layout.tree, kb.bundle) : layout.tree;
|
|
3163
|
+
}
|
|
3164
|
+
function fractionToPixel(f, size) {
|
|
3165
|
+
return Math.min(size - 1, Math.round(f * size));
|
|
3166
|
+
}
|
|
3167
|
+
function pressBack(opts) {
|
|
3168
|
+
return sendInput({ config: opts.config, type: "keyEvent", key: "Back", deviceSerial: opts.deviceSerial, logger: opts.logger });
|
|
3169
|
+
}
|
|
3170
|
+
async function hideKeyboard(opts) {
|
|
3171
|
+
const { keyboard } = await dumpLayout({ ...opts, detectKeyboard: true });
|
|
3172
|
+
if (!keyboard?.shown) return false;
|
|
3173
|
+
await pressBack(opts);
|
|
3174
|
+
await sleep(KEYBOARD_SETTLE_MS);
|
|
3175
|
+
return true;
|
|
3176
|
+
}
|
|
3177
|
+
async function locateTarget(opts) {
|
|
3178
|
+
assertLayoutSelector(opts.selector);
|
|
3179
|
+
const dumpOpts = { config: opts.config, detectKeyboard: true, deviceSerial: opts.deviceSerial, logger: opts.logger };
|
|
3180
|
+
let layout = await dumpLayout(dumpOpts);
|
|
3181
|
+
if (!layout.display) throw new OniroError("Could not read the display size to locate the element.");
|
|
3182
|
+
let keyboardHidden = false;
|
|
3183
|
+
let node;
|
|
3184
|
+
let failure;
|
|
3185
|
+
try {
|
|
3186
|
+
node = pickTarget(targetTree(layout), opts.selector, opts.index);
|
|
3187
|
+
} catch (err) {
|
|
3188
|
+
if (!(err instanceof TargetNotFoundError) || err.reason === "ambiguous") throw err;
|
|
3189
|
+
failure = err;
|
|
3190
|
+
}
|
|
3191
|
+
if ((!node || !node.click) && layout.keyboard?.shown && opts.hideKeyboard !== false) {
|
|
3192
|
+
opts.logger?.debug("target not reachable while the keyboard is up; dismissing it and looking again");
|
|
3193
|
+
await pressBack(opts);
|
|
3194
|
+
await sleep(KEYBOARD_SETTLE_MS);
|
|
3195
|
+
keyboardHidden = true;
|
|
3196
|
+
layout = await dumpLayout(dumpOpts);
|
|
3197
|
+
if (!layout.display) throw new OniroError("Could not read the display size to locate the element.");
|
|
3198
|
+
node = pickTarget(targetTree(layout), opts.selector, opts.index);
|
|
3199
|
+
}
|
|
3200
|
+
if (!node) throw failure;
|
|
3201
|
+
if (!node.c) throw new OniroError("The element has no on-screen position.");
|
|
3202
|
+
const x2 = fractionToPixel(node.c[0], layout.display.width);
|
|
3203
|
+
const y = fractionToPixel(node.c[1], layout.display.height);
|
|
3204
|
+
return { node, x: x2, y, display: layout.display, keyboardHidden };
|
|
3205
|
+
}
|
|
3206
|
+
function fraction(value, name) {
|
|
3207
|
+
if (value === void 0) return void 0;
|
|
3208
|
+
if (!(value >= 0 && value <= 1)) throw new OniroError(`${name} must be a fraction between 0 and 1; got ${value}.`);
|
|
3209
|
+
return value;
|
|
3210
|
+
}
|
|
3211
|
+
function pair(a, b, names) {
|
|
3212
|
+
if (a === void 0 && b === void 0) return void 0;
|
|
3213
|
+
if (a === void 0 || b === void 0) throw new OniroError(`Give both ${names}.`);
|
|
3214
|
+
return [a, b];
|
|
3215
|
+
}
|
|
3216
|
+
async function performInput(opts) {
|
|
3217
|
+
const device = { config: opts.config, deviceSerial: opts.deviceSerial, logger: opts.logger };
|
|
3218
|
+
const startFx = pair(fraction(opts.fx, "--fx"), fraction(opts.fy, "--fy"), "--fx and --fy");
|
|
3219
|
+
const startPx = pair(opts.pxX, opts.pxY, "--x and --y");
|
|
3220
|
+
const endFx = pair(fraction(opts.fx2, "--fx2"), fraction(opts.fy2, "--fy2"), "--fx2 and --fy2");
|
|
3221
|
+
const endPx = pair(opts.pxX2, opts.pxY2, "--x2 and --y2");
|
|
3222
|
+
if ([opts.target, startFx, startPx].filter((s) => s !== void 0).length > 1) {
|
|
3223
|
+
throw new OniroError("Give the start point one way: a target, fractions (--fx/--fy), or pixels (--x/--y).");
|
|
3224
|
+
}
|
|
3225
|
+
if (endFx && endPx) throw new OniroError("Give the end point one way: fractions (--fx2/--fy2) or pixels (--x2/--y2).");
|
|
3226
|
+
let display;
|
|
3227
|
+
const toPx = async ([fx, fy]) => {
|
|
3228
|
+
display ??= await getDisplaySize(opts.config, { deviceSerial: opts.deviceSerial, logger: opts.logger });
|
|
3229
|
+
if (!display) throw new OniroError("Could not read the display size to convert fractions to pixels.");
|
|
3230
|
+
return [fractionToPixel(fx, display.width), fractionToPixel(fy, display.height)];
|
|
3231
|
+
};
|
|
3232
|
+
const result = { keyboardHidden: false };
|
|
3233
|
+
let start = startPx;
|
|
3234
|
+
if (opts.target) {
|
|
3235
|
+
const located = await locateTarget({ ...device, selector: opts.target, index: opts.targetIndex });
|
|
3236
|
+
display = located.display;
|
|
3237
|
+
start = [located.x, located.y];
|
|
3238
|
+
result.target = located.node;
|
|
3239
|
+
result.keyboardHidden = located.keyboardHidden;
|
|
3240
|
+
} else if (startFx) {
|
|
3241
|
+
start = await toPx(startFx);
|
|
3242
|
+
}
|
|
3243
|
+
const end = endFx ? await toPx(endFx) : endPx;
|
|
3244
|
+
if (start) [result.x, result.y] = start;
|
|
3245
|
+
const tapToFocus = async () => {
|
|
3246
|
+
if (!start) return;
|
|
3247
|
+
await sendInput({ ...device, type: "click", pxX: start[0], pxY: start[1] });
|
|
3248
|
+
await sleep(FOCUS_SETTLE_MS);
|
|
3249
|
+
};
|
|
3250
|
+
const clearFocused = async () => {
|
|
3251
|
+
await sendInput({ ...device, type: "keyEvent", key: "Ctrl+A" });
|
|
3252
|
+
await sendInput({ ...device, type: "keyEvent", key: "Delete" });
|
|
3253
|
+
};
|
|
3254
|
+
switch (opts.type) {
|
|
3255
|
+
case "hideKeyboard":
|
|
3256
|
+
result.keyboardHidden = await hideKeyboard(device) || result.keyboardHidden;
|
|
3257
|
+
return result;
|
|
3258
|
+
case "clearText":
|
|
3259
|
+
await tapToFocus();
|
|
3260
|
+
await clearFocused();
|
|
3261
|
+
break;
|
|
3262
|
+
case "inputText":
|
|
3263
|
+
if (opts.clear) {
|
|
3264
|
+
await tapToFocus();
|
|
3265
|
+
await clearFocused();
|
|
3266
|
+
await sendInput({ ...device, type: "inputText", text: opts.text });
|
|
3267
|
+
} else {
|
|
3268
|
+
await sendInput({ ...device, type: "inputText", pxX: start?.[0], pxY: start?.[1], text: opts.text });
|
|
3269
|
+
}
|
|
3270
|
+
break;
|
|
3271
|
+
default:
|
|
3272
|
+
await sendInput({
|
|
3273
|
+
...device,
|
|
3274
|
+
type: opts.type,
|
|
3275
|
+
pxX: start?.[0],
|
|
3276
|
+
pxY: start?.[1],
|
|
3277
|
+
pxX2: end?.[0],
|
|
3278
|
+
pxY2: end?.[1],
|
|
3279
|
+
speed: opts.speed,
|
|
3280
|
+
key: opts.key,
|
|
3281
|
+
text: opts.text
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
if (opts.hideKeyboard) result.keyboardHidden = await hideKeyboard(device) || result.keyboardHidden;
|
|
3285
|
+
return result;
|
|
3286
|
+
}
|
|
3287
|
+
async function waitForTarget(opts) {
|
|
3288
|
+
const { selector } = opts;
|
|
3289
|
+
assertLayoutSelector(selector);
|
|
3290
|
+
let found = null;
|
|
3291
|
+
await waitForCondition({
|
|
3292
|
+
probe: async () => {
|
|
3293
|
+
const layout = await dumpLayout({
|
|
3294
|
+
config: opts.config,
|
|
3295
|
+
detectKeyboard: true,
|
|
3296
|
+
deviceSerial: opts.deviceSerial,
|
|
3297
|
+
logger: opts.logger
|
|
3298
|
+
});
|
|
3299
|
+
found = findLayoutNodes(targetTree(layout), selector).find((n) => !n.covered) ?? null;
|
|
3300
|
+
return opts.gone ? found === null : found !== null;
|
|
3301
|
+
},
|
|
3302
|
+
timeoutMs: opts.timeoutMs,
|
|
3303
|
+
pollMs: opts.pollMs ?? 500,
|
|
3304
|
+
abortSignal: opts.abortSignal,
|
|
3305
|
+
logger: opts.logger,
|
|
3306
|
+
timeoutMessage: opts.gone ? `An element matching the selector was still on screen after ${opts.timeoutMs}ms.` : `No element matching the selector appeared within ${opts.timeoutMs}ms.`
|
|
3307
|
+
});
|
|
3308
|
+
return opts.gone ? null : found;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
2638
3311
|
// src/hdc/hilog.ts
|
|
3312
|
+
import * as fs22 from "fs";
|
|
2639
3313
|
function setHilogLevel(opts) {
|
|
2640
3314
|
const logger = opts.logger ?? noopLogger;
|
|
2641
3315
|
const hdc = getHdcPath(opts.config);
|
|
2642
|
-
return new Promise((
|
|
3316
|
+
return new Promise((resolve7, reject) => {
|
|
2643
3317
|
let child;
|
|
2644
3318
|
try {
|
|
2645
|
-
child = spawnCompat(hdc, ["shell", "hilog", "-b", opts.level]);
|
|
3319
|
+
child = spawnCompat(hdc, [...hdcTargetArgs(opts.deviceSerial), "shell", "hilog", "-b", opts.level]);
|
|
2646
3320
|
} catch (err) {
|
|
2647
3321
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2648
3322
|
logger.error(`[hilog] failed to spawn hdc: ${msg}`);
|
|
@@ -2659,7 +3333,7 @@ function setHilogLevel(opts) {
|
|
|
2659
3333
|
});
|
|
2660
3334
|
child.once("close", (code) => {
|
|
2661
3335
|
if (code === 0) {
|
|
2662
|
-
|
|
3336
|
+
resolve7();
|
|
2663
3337
|
} else {
|
|
2664
3338
|
const msg = stderr.trim() || `hdc shell hilog -b ${opts.level} exited with code ${code}`;
|
|
2665
3339
|
logger.error(`[hilog] ${msg}`);
|
|
@@ -2670,8 +3344,7 @@ function setHilogLevel(opts) {
|
|
|
2670
3344
|
}
|
|
2671
3345
|
function streamHilog(opts) {
|
|
2672
3346
|
const hdc = getHdcPath(opts.config);
|
|
2673
|
-
const args = [];
|
|
2674
|
-
if (opts.deviceSerial) args.push("-t", opts.deviceSerial);
|
|
3347
|
+
const args = [...hdcTargetArgs(opts.deviceSerial)];
|
|
2675
3348
|
args.push("shell", "hilog");
|
|
2676
3349
|
if (opts.processId && opts.processId.trim() !== "") {
|
|
2677
3350
|
args.push("-P", opts.processId.trim());
|
|
@@ -2714,7 +3387,7 @@ async function resolveProcessId(config, bundle, deviceSerial) {
|
|
|
2714
3387
|
return proc?.pid;
|
|
2715
3388
|
}
|
|
2716
3389
|
function waitForLog(opts) {
|
|
2717
|
-
return new Promise((
|
|
3390
|
+
return new Promise((resolve7, reject) => {
|
|
2718
3391
|
const deadline = Date.now() + opts.timeoutMs;
|
|
2719
3392
|
let settled = false;
|
|
2720
3393
|
let child = null;
|
|
@@ -2733,7 +3406,7 @@ function waitForLog(opts) {
|
|
|
2733
3406
|
if (settled) return;
|
|
2734
3407
|
settled = true;
|
|
2735
3408
|
cleanup();
|
|
2736
|
-
|
|
3409
|
+
resolve7(entry);
|
|
2737
3410
|
};
|
|
2738
3411
|
const fail = (err) => {
|
|
2739
3412
|
if (settled) return;
|
|
@@ -2780,7 +3453,7 @@ function waitForLog(opts) {
|
|
|
2780
3453
|
});
|
|
2781
3454
|
}
|
|
2782
3455
|
function watchLog(opts) {
|
|
2783
|
-
return new Promise((
|
|
3456
|
+
return new Promise((resolve7, reject) => {
|
|
2784
3457
|
const matches = [];
|
|
2785
3458
|
let settled = false;
|
|
2786
3459
|
let child = null;
|
|
@@ -2797,7 +3470,7 @@ function watchLog(opts) {
|
|
|
2797
3470
|
if (settled) return;
|
|
2798
3471
|
settled = true;
|
|
2799
3472
|
cleanup();
|
|
2800
|
-
|
|
3473
|
+
resolve7(opts.dedup === false ? matches : dedupEntries(matches));
|
|
2801
3474
|
};
|
|
2802
3475
|
const onAbort = () => {
|
|
2803
3476
|
if (settled) return;
|
|
@@ -2829,6 +3502,94 @@ function watchLog(opts) {
|
|
|
2829
3502
|
});
|
|
2830
3503
|
});
|
|
2831
3504
|
}
|
|
3505
|
+
async function captureLog(opts) {
|
|
3506
|
+
const logger = opts.logger ?? noopLogger;
|
|
3507
|
+
if (opts.clear) {
|
|
3508
|
+
const res = await shell({ config: opts.config, command: "hilog -r", deviceSerial: opts.deviceSerial, timeoutMs: 1e4, logger });
|
|
3509
|
+
ensureOk(res, "hdc shell hilog -r");
|
|
3510
|
+
}
|
|
3511
|
+
const fd = fs22.openSync(opts.file, "a");
|
|
3512
|
+
let lines = 0;
|
|
3513
|
+
let reconnects = 0;
|
|
3514
|
+
try {
|
|
3515
|
+
await new Promise((resolve7, reject) => {
|
|
3516
|
+
const deadline = opts.durationMs !== void 0 ? Date.now() + opts.durationMs : Infinity;
|
|
3517
|
+
let settled = false;
|
|
3518
|
+
let child = null;
|
|
3519
|
+
let reconnectTimer;
|
|
3520
|
+
let buf = "";
|
|
3521
|
+
let open = true;
|
|
3522
|
+
const write = (line) => {
|
|
3523
|
+
if (!open || opts.pattern && !opts.pattern.test(line)) return;
|
|
3524
|
+
fs22.writeSync(fd, `${line}
|
|
3525
|
+
`);
|
|
3526
|
+
lines++;
|
|
3527
|
+
};
|
|
3528
|
+
const cleanup = () => {
|
|
3529
|
+
if (timer) clearTimeout(timer);
|
|
3530
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
3531
|
+
opts.abortSignal?.removeEventListener("abort", finish);
|
|
3532
|
+
if (buf.trim()) write(buf.trimEnd());
|
|
3533
|
+
buf = "";
|
|
3534
|
+
open = false;
|
|
3535
|
+
if (child) {
|
|
3536
|
+
child.stdout.removeAllListeners("data");
|
|
3537
|
+
killProcessTree(child);
|
|
3538
|
+
child = null;
|
|
3539
|
+
}
|
|
3540
|
+
};
|
|
3541
|
+
function finish() {
|
|
3542
|
+
if (settled) return;
|
|
3543
|
+
settled = true;
|
|
3544
|
+
cleanup();
|
|
3545
|
+
resolve7();
|
|
3546
|
+
}
|
|
3547
|
+
const fail = (err) => {
|
|
3548
|
+
if (settled) return;
|
|
3549
|
+
settled = true;
|
|
3550
|
+
cleanup();
|
|
3551
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
3552
|
+
};
|
|
3553
|
+
const timer = opts.durationMs !== void 0 ? setTimeout(finish, opts.durationMs) : void 0;
|
|
3554
|
+
if (opts.abortSignal?.aborted) {
|
|
3555
|
+
finish();
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3558
|
+
opts.abortSignal?.addEventListener("abort", finish, { once: true });
|
|
3559
|
+
const spawnStream = () => {
|
|
3560
|
+
if (settled) return;
|
|
3561
|
+
const stream = streamHilog({ config: opts.config, domain: opts.domain, deviceSerial: opts.deviceSerial });
|
|
3562
|
+
child = stream;
|
|
3563
|
+
stream.stdout.on("data", (d) => {
|
|
3564
|
+
buf += d.toString();
|
|
3565
|
+
const parts = buf.split(/\r?\n/);
|
|
3566
|
+
buf = parts.pop() ?? "";
|
|
3567
|
+
for (const line of parts) write(line);
|
|
3568
|
+
});
|
|
3569
|
+
const onEnd = () => {
|
|
3570
|
+
if (settled || child !== stream) return;
|
|
3571
|
+
child = null;
|
|
3572
|
+
if (Date.now() >= deadline) return;
|
|
3573
|
+
reconnects++;
|
|
3574
|
+
logger.warn(`[hilog] stream ended; reconnecting (${reconnects}).`);
|
|
3575
|
+
reconnectTimer = setTimeout(() => {
|
|
3576
|
+
try {
|
|
3577
|
+
spawnStream();
|
|
3578
|
+
} catch (err) {
|
|
3579
|
+
fail(err);
|
|
3580
|
+
}
|
|
3581
|
+
}, 2e3);
|
|
3582
|
+
};
|
|
3583
|
+
stream.once("close", onEnd);
|
|
3584
|
+
stream.once("error", onEnd);
|
|
3585
|
+
};
|
|
3586
|
+
spawnStream();
|
|
3587
|
+
});
|
|
3588
|
+
} finally {
|
|
3589
|
+
fs22.closeSync(fd);
|
|
3590
|
+
}
|
|
3591
|
+
return { file: opts.file, lines, reconnects };
|
|
3592
|
+
}
|
|
2832
3593
|
async function dumpLog(opts) {
|
|
2833
3594
|
let pipe = "hilog -x";
|
|
2834
3595
|
if (opts.domain !== void 0 && `${opts.domain}` !== "") pipe += ` -D ${opts.domain}`;
|
|
@@ -2853,17 +3614,17 @@ async function dumpLog(opts) {
|
|
|
2853
3614
|
}
|
|
2854
3615
|
|
|
2855
3616
|
// src/build/runHvigorw.ts
|
|
2856
|
-
import * as
|
|
2857
|
-
import * as
|
|
3617
|
+
import * as fs24 from "fs";
|
|
3618
|
+
import * as path24 from "path";
|
|
2858
3619
|
|
|
2859
3620
|
// src/harmonyos/target.ts
|
|
2860
|
-
import * as
|
|
2861
|
-
import * as
|
|
3621
|
+
import * as fs23 from "fs";
|
|
3622
|
+
import * as path23 from "path";
|
|
2862
3623
|
import JSON54 from "json5";
|
|
2863
3624
|
function detectRuntimeOs(projectDir, productName) {
|
|
2864
3625
|
let products = [];
|
|
2865
3626
|
try {
|
|
2866
|
-
const parsed = JSON54.parse(
|
|
3627
|
+
const parsed = JSON54.parse(fs23.readFileSync(path23.join(projectDir, "build-profile.json5"), "utf-8"));
|
|
2867
3628
|
if (Array.isArray(parsed.app?.products)) products = parsed.app.products;
|
|
2868
3629
|
} catch {
|
|
2869
3630
|
return "OpenHarmony";
|
|
@@ -2904,7 +3665,7 @@ function runHvigorw(opts) {
|
|
|
2904
3665
|
const { config } = opts;
|
|
2905
3666
|
const projectDir = toLongPath(opts.projectDir);
|
|
2906
3667
|
const logger = opts.logger ?? noopLogger;
|
|
2907
|
-
if (!
|
|
3668
|
+
if (!fs24.existsSync(path24.join(projectDir, "build-profile.json5"))) {
|
|
2908
3669
|
throw new OniroError(`Not an OpenHarmony project: ${projectDir} (build-profile.json5 not found).`);
|
|
2909
3670
|
}
|
|
2910
3671
|
let hvigorw;
|
|
@@ -2916,24 +3677,24 @@ function runHvigorw(opts) {
|
|
|
2916
3677
|
hvigorw = resolved.startsWith(projectDir) ? resolved : findHarmonyOsTool(sdk, "hvigorw") ?? resolved;
|
|
2917
3678
|
logger.info(`[build] Building for HarmonyOS against the SDK at ${sdk.sdkPath}.`);
|
|
2918
3679
|
} else {
|
|
2919
|
-
if (!
|
|
3680
|
+
if (!fs24.existsSync(getCmdToolsPath(config))) {
|
|
2920
3681
|
throw new CmdToolsNotInstalledError(getCmdToolsPath(config));
|
|
2921
3682
|
}
|
|
2922
3683
|
sdkEnv = { OHOS_BASE_SDK_HOME: getOhosBaseSdkHome(config) };
|
|
2923
3684
|
hvigorw = getHvigorwPath(config, projectDir);
|
|
2924
3685
|
}
|
|
2925
|
-
if (!
|
|
3686
|
+
if (!fs24.existsSync(hvigorw)) {
|
|
2926
3687
|
throw new OniroError(`hvigorw wrapper not found at ${hvigorw}.`);
|
|
2927
3688
|
}
|
|
2928
3689
|
const args = buildHvigorwArgs(opts);
|
|
2929
3690
|
if (process.platform !== "win32") {
|
|
2930
3691
|
try {
|
|
2931
|
-
|
|
3692
|
+
fs24.chmodSync(hvigorw, 493);
|
|
2932
3693
|
} catch {
|
|
2933
3694
|
}
|
|
2934
3695
|
}
|
|
2935
3696
|
const env = { ...process.env, ...sdkEnv };
|
|
2936
|
-
return new Promise((
|
|
3697
|
+
return new Promise((resolve7, reject) => {
|
|
2937
3698
|
if (opts.abortSignal?.aborted) {
|
|
2938
3699
|
reject(new CancelledError("hvigorw build cancelled."));
|
|
2939
3700
|
return;
|
|
@@ -2977,46 +3738,15 @@ function runHvigorw(opts) {
|
|
|
2977
3738
|
settled = true;
|
|
2978
3739
|
cleanup();
|
|
2979
3740
|
const exitCode = code ?? 1;
|
|
2980
|
-
if (exitCode === 0)
|
|
3741
|
+
if (exitCode === 0) resolve7({ exitCode });
|
|
2981
3742
|
else reject(new OniroError(`hvigorw exited with code ${exitCode}`));
|
|
2982
3743
|
});
|
|
2983
3744
|
});
|
|
2984
3745
|
}
|
|
2985
3746
|
|
|
2986
|
-
// src/build/discoverHaps.ts
|
|
2987
|
-
import * as fs23 from "fs";
|
|
2988
|
-
import * as path23 from "path";
|
|
2989
|
-
function moduleOfHap(hapFileName) {
|
|
2990
|
-
const dash = hapFileName.indexOf("-");
|
|
2991
|
-
return dash > 0 ? hapFileName.slice(0, dash) : hapFileName.replace(/\.hap$/i, "");
|
|
2992
|
-
}
|
|
2993
|
-
async function discoverHaps(opts) {
|
|
2994
|
-
const { projectDir } = opts;
|
|
2995
|
-
const maxDepth = opts.maxDepth ?? 8;
|
|
2996
|
-
const result = {};
|
|
2997
|
-
const walk = async (dir, depth) => {
|
|
2998
|
-
if (depth > maxDepth) return;
|
|
2999
|
-
const entries = await fs23.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
3000
|
-
for (const e of entries) {
|
|
3001
|
-
const full = path23.join(dir, e.name);
|
|
3002
|
-
if (e.isDirectory()) {
|
|
3003
|
-
if (e.name === "node_modules" || e.name === "oh_modules" || e.name === ".git") continue;
|
|
3004
|
-
await walk(full, depth + 1);
|
|
3005
|
-
} else if (e.isFile() && e.name.endsWith(".hap")) {
|
|
3006
|
-
const moduleName = moduleOfHap(e.name);
|
|
3007
|
-
const bucket = result[moduleName] ??= { signed: [], unsigned: [] };
|
|
3008
|
-
if (e.name.includes("unsigned")) bucket.unsigned.push(full);
|
|
3009
|
-
else if (e.name.includes("signed")) bucket.signed.push(full);
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
};
|
|
3013
|
-
await walk(projectDir, 0);
|
|
3014
|
-
return result;
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
3747
|
// src/build/ohpm.ts
|
|
3018
|
-
import * as
|
|
3019
|
-
import * as
|
|
3748
|
+
import * as fs25 from "fs";
|
|
3749
|
+
import * as path25 from "path";
|
|
3020
3750
|
function runOhpm(opts) {
|
|
3021
3751
|
return runProcess({
|
|
3022
3752
|
command: opts.ohpmPath ?? getOhpmPath(opts.config),
|
|
@@ -3029,7 +3759,7 @@ function runOhpm(opts) {
|
|
|
3029
3759
|
});
|
|
3030
3760
|
}
|
|
3031
3761
|
async function ensureOhModules(opts) {
|
|
3032
|
-
if (
|
|
3762
|
+
if (fs25.existsSync(path25.join(opts.projectDir, "oh_modules"))) {
|
|
3033
3763
|
return { installed: false };
|
|
3034
3764
|
}
|
|
3035
3765
|
const res = await runOhpm({
|
|
@@ -3046,23 +3776,91 @@ async function ensureOhModules(opts) {
|
|
|
3046
3776
|
}
|
|
3047
3777
|
|
|
3048
3778
|
// src/build/codelinter.ts
|
|
3049
|
-
var FINDING_RE = /^(.*?):(\d+):(
|
|
3779
|
+
var FINDING_RE = /^(.*?):(\d+):(?:(\d+):)?\s*(error|warning|warn|suggestion|info)\b[:\s-]*(.*)$/i;
|
|
3050
3780
|
var TRAILING_CODE_RE = /[([]([\w@/.\-]+)[)\]]\s*$/;
|
|
3051
|
-
|
|
3781
|
+
var FILE_HEADER_RE = /^(.+\.\w+)\((\d+)\)$/;
|
|
3782
|
+
var ROW_RE = /^(\d+):(\d+)\s+(error|warning|warn|suggestion|info)\s+(.*)$/i;
|
|
3783
|
+
var RULE_RE = /^@?[\w.-]+(?:\/[\w.-]+)+$/;
|
|
3784
|
+
var SUMMARY_RE = /Defects:\s*(\d+);\s*Errors:\s*(\d+);\s*Warns:\s*(\d+);\s*Suggestions:\s*(\d+)/i;
|
|
3785
|
+
var NOTICE_RE = /does not match that of the project|error occurred during linting|incomplete report/i;
|
|
3786
|
+
var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
3787
|
+
function cleanCodelinterOutput(output) {
|
|
3788
|
+
return output.replace(ANSI_RE, "").split("\n").map((line) => (line.split("\r").filter((s) => s.trim() !== "").pop() ?? "").trimEnd()).join("\n");
|
|
3789
|
+
}
|
|
3790
|
+
function normaliseSeverity(s) {
|
|
3791
|
+
const lower = s.toLowerCase();
|
|
3792
|
+
return lower === "warn" ? "warning" : lower;
|
|
3793
|
+
}
|
|
3794
|
+
function parseCodelinterReport(output) {
|
|
3052
3795
|
const findings = [];
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3796
|
+
const notices = [];
|
|
3797
|
+
let summary = null;
|
|
3798
|
+
let file = "";
|
|
3799
|
+
for (const rawLine of cleanCodelinterOutput(output).split("\n")) {
|
|
3800
|
+
const line = rawLine.trim();
|
|
3801
|
+
if (!line) continue;
|
|
3802
|
+
const row = ROW_RE.exec(line);
|
|
3803
|
+
if (row) {
|
|
3804
|
+
if (!file) continue;
|
|
3805
|
+
const columns = row[4].trim().split(/\s{2,}/);
|
|
3806
|
+
const last = columns[columns.length - 1];
|
|
3807
|
+
const code = columns.length > 1 && RULE_RE.test(last) ? last : "";
|
|
3808
|
+
const message = (code ? columns.slice(0, -1) : columns).join(" ").trim();
|
|
3809
|
+
findings.push({
|
|
3810
|
+
file,
|
|
3811
|
+
line: Number(row[1]),
|
|
3812
|
+
column: Number(row[2]),
|
|
3813
|
+
severity: normaliseSeverity(row[3]),
|
|
3814
|
+
code,
|
|
3815
|
+
message
|
|
3816
|
+
});
|
|
3817
|
+
continue;
|
|
3818
|
+
}
|
|
3819
|
+
const header = FILE_HEADER_RE.exec(line);
|
|
3820
|
+
if (header) {
|
|
3821
|
+
file = header[1];
|
|
3822
|
+
continue;
|
|
3062
3823
|
}
|
|
3063
|
-
|
|
3824
|
+
const legacy = FINDING_RE.exec(line);
|
|
3825
|
+
if (legacy) {
|
|
3826
|
+
let message = legacy[5].trim();
|
|
3827
|
+
let code = "";
|
|
3828
|
+
const codeMatch = TRAILING_CODE_RE.exec(message);
|
|
3829
|
+
if (codeMatch) {
|
|
3830
|
+
code = codeMatch[1];
|
|
3831
|
+
message = message.slice(0, codeMatch.index).trim();
|
|
3832
|
+
}
|
|
3833
|
+
const finding = {
|
|
3834
|
+
file: legacy[1].trim(),
|
|
3835
|
+
line: Number(legacy[2]),
|
|
3836
|
+
severity: normaliseSeverity(legacy[4]),
|
|
3837
|
+
code,
|
|
3838
|
+
message
|
|
3839
|
+
};
|
|
3840
|
+
if (legacy[3]) finding.column = Number(legacy[3]);
|
|
3841
|
+
findings.push(finding);
|
|
3842
|
+
continue;
|
|
3843
|
+
}
|
|
3844
|
+
const totals = SUMMARY_RE.exec(line);
|
|
3845
|
+
if (totals) {
|
|
3846
|
+
summary = {
|
|
3847
|
+
defects: Number(totals[1]),
|
|
3848
|
+
errors: Number(totals[2]),
|
|
3849
|
+
warnings: Number(totals[3]),
|
|
3850
|
+
suggestions: Number(totals[4])
|
|
3851
|
+
};
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
if (/no defects found/i.test(line)) {
|
|
3855
|
+
summary = { defects: 0, errors: 0, warnings: 0, suggestions: 0 };
|
|
3856
|
+
continue;
|
|
3857
|
+
}
|
|
3858
|
+
if (NOTICE_RE.test(line)) notices.push(line);
|
|
3064
3859
|
}
|
|
3065
|
-
return findings;
|
|
3860
|
+
return { findings, summary, notices, incomplete: notices.length > 0 };
|
|
3861
|
+
}
|
|
3862
|
+
function parseCodelinterFindings(output) {
|
|
3863
|
+
return parseCodelinterReport(output).findings;
|
|
3066
3864
|
}
|
|
3067
3865
|
async function runCodelinter(opts) {
|
|
3068
3866
|
const res = await runProcess({
|
|
@@ -3074,16 +3872,28 @@ async function runCodelinter(opts) {
|
|
|
3074
3872
|
logger: opts.logger
|
|
3075
3873
|
});
|
|
3076
3874
|
const raw = `${res.stdout}${res.stderr}`;
|
|
3077
|
-
return { code: res.code,
|
|
3875
|
+
return { code: res.code, ...parseCodelinterReport(raw), raw: cleanCodelinterOutput(raw) };
|
|
3078
3876
|
}
|
|
3079
3877
|
|
|
3080
3878
|
// src/build/buildHap.ts
|
|
3879
|
+
var DIAGNOSIS_OUTPUT_LIMIT = 2e6;
|
|
3880
|
+
function diagnoseBuildFailure(output) {
|
|
3881
|
+
if (/is not exported from Kit|has no exported member .*@systemapi/i.test(output)) {
|
|
3882
|
+
return "The project calls system APIs (`@systemapi`), which the public SDK leaves out. Build against the full OpenHarmony SDK: unpack its components into <root>/<os>/<api> of a separate SDK root and set ONIRO_SDK_ROOT_DIR to that root. Installing such an app also needs privileged signing (`oniro-app sign --apl system_core --acls \u2026 --app-privilege-capabilities \u2026`).";
|
|
3883
|
+
}
|
|
3884
|
+
if (/00308018|SDK path does not exist|SDK license agreement is not accepted/i.test(output)) {
|
|
3885
|
+
return "hvigor cannot find the SDK the project compiles against: check `oniro-app sdk list` against compileSdkVersion.";
|
|
3886
|
+
}
|
|
3887
|
+
return void 0;
|
|
3888
|
+
}
|
|
3081
3889
|
async function buildHap(opts) {
|
|
3082
3890
|
const logger = scopedLogger(opts.logger ?? noopLogger, "build");
|
|
3083
3891
|
const warnings = [];
|
|
3084
3892
|
const start = Date.now();
|
|
3085
3893
|
const projectDir = toLongPath(opts.projectDir);
|
|
3086
3894
|
const isHarmonyOs = detectRuntimeOs(projectDir, opts.product) === "HarmonyOS";
|
|
3895
|
+
const missingSdk = isHarmonyOs ? null : findMissingProjectSdk(opts.config, projectDir);
|
|
3896
|
+
if (missingSdk) throw missingSdk;
|
|
3087
3897
|
if (opts.autoInstallDeps !== false) {
|
|
3088
3898
|
const harmonySdk = isHarmonyOs ? findHarmonyOsSdk({ config: opts.config, logger: opts.logger }) : null;
|
|
3089
3899
|
const r = await ensureOhModules({
|
|
@@ -3096,23 +3906,35 @@ async function buildHap(opts) {
|
|
|
3096
3906
|
});
|
|
3097
3907
|
if (r.installed) logger.info("ran ohpm install --all (oh_modules was missing)");
|
|
3098
3908
|
}
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3909
|
+
let output = "";
|
|
3910
|
+
let exitCode;
|
|
3911
|
+
try {
|
|
3912
|
+
({ exitCode } = await runHvigorw({
|
|
3913
|
+
config: opts.config,
|
|
3914
|
+
projectDir,
|
|
3915
|
+
product: opts.product,
|
|
3916
|
+
module: opts.module,
|
|
3917
|
+
buildMode: opts.mode,
|
|
3918
|
+
task: opts.task,
|
|
3919
|
+
parallel: opts.parallel,
|
|
3920
|
+
abortSignal: opts.abortSignal,
|
|
3921
|
+
onOutput: (chunk, stream) => {
|
|
3922
|
+
output = (output + chunk).slice(-DIAGNOSIS_OUTPUT_LIMIT);
|
|
3923
|
+
opts.onOutput?.(chunk, stream);
|
|
3924
|
+
},
|
|
3925
|
+
logger: opts.logger
|
|
3926
|
+
}));
|
|
3927
|
+
} catch (err) {
|
|
3928
|
+
const hint = diagnoseBuildFailure(output);
|
|
3929
|
+
if (hint && err instanceof OniroError) throw new OniroError(`${err.message}
|
|
3930
|
+
${hint}`, err);
|
|
3931
|
+
throw err;
|
|
3932
|
+
}
|
|
3111
3933
|
const discoveredHaps = await discoverHaps({ projectDir });
|
|
3112
3934
|
const anySigned = Object.values(discoveredHaps).some((m) => m.signed.length > 0);
|
|
3113
3935
|
if (!anySigned) {
|
|
3114
3936
|
warnings.push(
|
|
3115
|
-
isHarmonyOs ? "No signed HAPs found
|
|
3937
|
+
isHarmonyOs ? "No signed HAPs found. The HarmonyOS emulator installs the unsigned HAP as it is; for a device, run `oniro-app sign --harmonyos` to have AppGallery Connect issue a certificate and profile." : "No signed HAPs found: most OpenHarmony targets reject unsigned HAPs. Run `oniro-app sign` to generate local signing material, then build again."
|
|
3116
3938
|
);
|
|
3117
3939
|
}
|
|
3118
3940
|
return { exitCode, durationMs: Date.now() - start, discoveredHaps, warnings };
|
|
@@ -3129,21 +3951,21 @@ function isValidBundleName(bundleName) {
|
|
|
3129
3951
|
}
|
|
3130
3952
|
|
|
3131
3953
|
// src/project/templates.ts
|
|
3132
|
-
import * as
|
|
3133
|
-
import * as
|
|
3954
|
+
import * as fs26 from "fs";
|
|
3955
|
+
import * as path26 from "path";
|
|
3134
3956
|
function toHumanTemplateName(folderName) {
|
|
3135
3957
|
return folderName.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
3136
3958
|
}
|
|
3137
3959
|
function listTemplates(templateRoot) {
|
|
3138
|
-
if (!
|
|
3139
|
-
const entries =
|
|
3960
|
+
if (!fs26.existsSync(templateRoot)) return [];
|
|
3961
|
+
const entries = fs26.readdirSync(templateRoot, { withFileTypes: true });
|
|
3140
3962
|
return entries.filter((e) => e.isDirectory()).map((e) => {
|
|
3141
|
-
const dir =
|
|
3142
|
-
const metaPath =
|
|
3963
|
+
const dir = path26.join(templateRoot, e.name);
|
|
3964
|
+
const metaPath = path26.join(dir, "template.json");
|
|
3143
3965
|
let meta = {};
|
|
3144
|
-
if (
|
|
3966
|
+
if (fs26.existsSync(metaPath)) {
|
|
3145
3967
|
try {
|
|
3146
|
-
meta = JSON.parse(
|
|
3968
|
+
meta = JSON.parse(fs26.readFileSync(metaPath, "utf8"));
|
|
3147
3969
|
} catch {
|
|
3148
3970
|
}
|
|
3149
3971
|
}
|
|
@@ -3158,57 +3980,57 @@ function listTemplates(templateRoot) {
|
|
|
3158
3980
|
function validateTemplateLayout(templateDir, defaultModuleName) {
|
|
3159
3981
|
const required = [
|
|
3160
3982
|
"build-profile.json5",
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3983
|
+
path26.join("AppScope", "app.json5"),
|
|
3984
|
+
path26.join(defaultModuleName, "src", "main", "module.json5"),
|
|
3985
|
+
path26.join(defaultModuleName, "oh-package.json5"),
|
|
3164
3986
|
"hvigorfile.ts"
|
|
3165
3987
|
];
|
|
3166
|
-
return required.filter((rel) => !
|
|
3988
|
+
return required.filter((rel) => !fs26.existsSync(path26.join(templateDir, rel)));
|
|
3167
3989
|
}
|
|
3168
3990
|
|
|
3169
3991
|
// src/project/createScaffold.ts
|
|
3170
|
-
import * as
|
|
3171
|
-
import * as
|
|
3992
|
+
import * as fs28 from "fs";
|
|
3993
|
+
import * as path27 from "path";
|
|
3172
3994
|
|
|
3173
3995
|
// src/project/jsonHelpers.ts
|
|
3174
|
-
import * as
|
|
3996
|
+
import * as fs27 from "fs";
|
|
3175
3997
|
import JSON55 from "json5";
|
|
3176
3998
|
function readJson5File(filePath) {
|
|
3177
|
-
return JSON55.parse(
|
|
3999
|
+
return JSON55.parse(fs27.readFileSync(filePath, "utf8"));
|
|
3178
4000
|
}
|
|
3179
4001
|
function writeJson5File(filePath, value) {
|
|
3180
|
-
|
|
4002
|
+
fs27.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
3181
4003
|
}
|
|
3182
4004
|
function readJsonFile(filePath) {
|
|
3183
|
-
return JSON.parse(
|
|
4005
|
+
return JSON.parse(fs27.readFileSync(filePath, "utf8"));
|
|
3184
4006
|
}
|
|
3185
4007
|
function writeJsonFile(filePath, value) {
|
|
3186
|
-
|
|
4008
|
+
fs27.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
3187
4009
|
}
|
|
3188
4010
|
|
|
3189
4011
|
// src/project/createScaffold.ts
|
|
3190
4012
|
var IGNORED_DIRS = /* @__PURE__ */ new Set(["oh_modules", "node_modules", "build", ".hvigor"]);
|
|
3191
4013
|
async function pathExists(p) {
|
|
3192
4014
|
try {
|
|
3193
|
-
await
|
|
4015
|
+
await fs28.promises.access(p);
|
|
3194
4016
|
return true;
|
|
3195
4017
|
} catch {
|
|
3196
4018
|
return false;
|
|
3197
4019
|
}
|
|
3198
4020
|
}
|
|
3199
4021
|
async function copyDirRecursive(srcDir, destDir) {
|
|
3200
|
-
await
|
|
3201
|
-
const entries = await
|
|
4022
|
+
await fs28.promises.mkdir(destDir, { recursive: true });
|
|
4023
|
+
const entries = await fs28.promises.readdir(srcDir, { withFileTypes: true });
|
|
3202
4024
|
for (const entry of entries) {
|
|
3203
|
-
const src =
|
|
3204
|
-
const dest =
|
|
4025
|
+
const src = path27.join(srcDir, entry.name);
|
|
4026
|
+
const dest = path27.join(destDir, entry.name);
|
|
3205
4027
|
if (entry.isDirectory()) {
|
|
3206
4028
|
await copyDirRecursive(src, dest);
|
|
3207
4029
|
} else if (entry.isSymbolicLink()) {
|
|
3208
4030
|
continue;
|
|
3209
4031
|
} else if (entry.isFile()) {
|
|
3210
4032
|
if (entry.name === "template.json") continue;
|
|
3211
|
-
await
|
|
4033
|
+
await fs28.promises.copyFile(src, dest);
|
|
3212
4034
|
}
|
|
3213
4035
|
}
|
|
3214
4036
|
}
|
|
@@ -3216,9 +4038,9 @@ async function normalizeJson5ToJson(projectDir, logger) {
|
|
|
3216
4038
|
const stack = [projectDir];
|
|
3217
4039
|
while (stack.length > 0) {
|
|
3218
4040
|
const currentDir = stack.pop();
|
|
3219
|
-
const entries = await
|
|
4041
|
+
const entries = await fs28.promises.readdir(currentDir, { withFileTypes: true });
|
|
3220
4042
|
for (const entry of entries) {
|
|
3221
|
-
const fullPath =
|
|
4043
|
+
const fullPath = path27.join(currentDir, entry.name);
|
|
3222
4044
|
if (entry.isDirectory()) {
|
|
3223
4045
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3224
4046
|
stack.push(fullPath);
|
|
@@ -3240,23 +4062,23 @@ function toJavaPropertiesPath(p) {
|
|
|
3240
4062
|
function createOrUpdateLocalProperties(projectDir, sdkDir) {
|
|
3241
4063
|
const content = `sdk.dir=${toJavaPropertiesPath(sdkDir)}
|
|
3242
4064
|
`;
|
|
3243
|
-
|
|
4065
|
+
fs28.writeFileSync(path27.join(projectDir, "local.properties"), content, "utf8");
|
|
3244
4066
|
}
|
|
3245
4067
|
function renameIfExists(fromPath, toPath) {
|
|
3246
|
-
if (
|
|
3247
|
-
|
|
4068
|
+
if (fs28.existsSync(fromPath) && fromPath !== toPath) {
|
|
4069
|
+
fs28.renameSync(fromPath, toPath);
|
|
3248
4070
|
}
|
|
3249
4071
|
}
|
|
3250
4072
|
function updateTemplateConfigs(projectDir, args, sdkBaseDir) {
|
|
3251
|
-
const appJsonPath =
|
|
3252
|
-
if (
|
|
4073
|
+
const appJsonPath = path27.join(projectDir, "AppScope", "app.json5");
|
|
4074
|
+
if (fs28.existsSync(appJsonPath)) {
|
|
3253
4075
|
const appJson = readJson5File(appJsonPath);
|
|
3254
4076
|
appJson.app = appJson.app ?? {};
|
|
3255
4077
|
appJson.app.bundleName = args.bundleName;
|
|
3256
4078
|
writeJson5File(appJsonPath, appJson);
|
|
3257
4079
|
}
|
|
3258
|
-
const stringsPath =
|
|
3259
|
-
if (
|
|
4080
|
+
const stringsPath = path27.join(projectDir, "AppScope", "resources", "base", "element", "string.json");
|
|
4081
|
+
if (fs28.existsSync(stringsPath)) {
|
|
3260
4082
|
const strings = readJsonFile(stringsPath);
|
|
3261
4083
|
if (Array.isArray(strings.string)) {
|
|
3262
4084
|
const appName = strings.string.find((s) => s.name === "app_name");
|
|
@@ -3264,8 +4086,8 @@ function updateTemplateConfigs(projectDir, args, sdkBaseDir) {
|
|
|
3264
4086
|
}
|
|
3265
4087
|
writeJsonFile(stringsPath, strings);
|
|
3266
4088
|
}
|
|
3267
|
-
const buildProfilePath =
|
|
3268
|
-
if (
|
|
4089
|
+
const buildProfilePath = path27.join(projectDir, "build-profile.json5");
|
|
4090
|
+
if (fs28.existsSync(buildProfilePath)) {
|
|
3269
4091
|
const buildProfile = readJson5File(buildProfilePath);
|
|
3270
4092
|
buildProfile.app = buildProfile.app ?? {};
|
|
3271
4093
|
if (Array.isArray(buildProfile.app.products) && buildProfile.app.products.length > 0) {
|
|
@@ -3278,23 +4100,23 @@ function updateTemplateConfigs(projectDir, args, sdkBaseDir) {
|
|
|
3278
4100
|
}
|
|
3279
4101
|
writeJson5File(buildProfilePath, buildProfile);
|
|
3280
4102
|
}
|
|
3281
|
-
const moduleJsonPath =
|
|
3282
|
-
if (
|
|
4103
|
+
const moduleJsonPath = path27.join(projectDir, args.moduleName, "src", "main", "module.json5");
|
|
4104
|
+
if (fs28.existsSync(moduleJsonPath)) {
|
|
3283
4105
|
const moduleJson = readJson5File(moduleJsonPath);
|
|
3284
4106
|
moduleJson.module = moduleJson.module ?? {};
|
|
3285
4107
|
moduleJson.module.name = args.moduleName;
|
|
3286
4108
|
writeJson5File(moduleJsonPath, moduleJson);
|
|
3287
4109
|
}
|
|
3288
|
-
const moduleOhPackagePath =
|
|
3289
|
-
if (
|
|
4110
|
+
const moduleOhPackagePath = path27.join(projectDir, args.moduleName, "oh-package.json5");
|
|
4111
|
+
if (fs28.existsSync(moduleOhPackagePath)) {
|
|
3290
4112
|
const pkg = readJson5File(moduleOhPackagePath);
|
|
3291
4113
|
pkg.name = args.moduleName;
|
|
3292
4114
|
writeJson5File(moduleOhPackagePath, pkg);
|
|
3293
4115
|
}
|
|
3294
|
-
const vscodeDir =
|
|
3295
|
-
|
|
4116
|
+
const vscodeDir = path27.join(projectDir, ".vscode");
|
|
4117
|
+
fs28.mkdirSync(vscodeDir, { recursive: true });
|
|
3296
4118
|
const hapPath = `${args.moduleName}/build/default/outputs/default/${args.moduleName}-default-signed.hap`;
|
|
3297
|
-
writeJsonFile(
|
|
4119
|
+
writeJsonFile(path27.join(vscodeDir, "settings.json"), {
|
|
3298
4120
|
"oniro.hapPath": hapPath,
|
|
3299
4121
|
"files.associations": { "*.json5": "jsonc" }
|
|
3300
4122
|
});
|
|
@@ -3311,7 +4133,7 @@ async function createScaffold(opts) {
|
|
|
3311
4133
|
if (!opts.location || !await pathExists(opts.location)) {
|
|
3312
4134
|
throw new OniroError(`Location does not exist: ${opts.location}`);
|
|
3313
4135
|
}
|
|
3314
|
-
const templateDir =
|
|
4136
|
+
const templateDir = path27.join(opts.templateRoot, opts.templateId);
|
|
3315
4137
|
if (!await pathExists(templateDir)) {
|
|
3316
4138
|
throw new OniroError(`Template not found: ${templateDir}`);
|
|
3317
4139
|
}
|
|
@@ -3346,17 +4168,17 @@ async function createScaffold(opts) {
|
|
|
3346
4168
|
}
|
|
3347
4169
|
}
|
|
3348
4170
|
}
|
|
3349
|
-
const projectDir =
|
|
4171
|
+
const projectDir = path27.join(opts.location, opts.projectName);
|
|
3350
4172
|
if (await pathExists(projectDir)) {
|
|
3351
4173
|
if (!opts.overwrite) {
|
|
3352
4174
|
throw new OniroError(`Destination already exists: ${projectDir}. Pass overwrite: true to replace it.`);
|
|
3353
4175
|
}
|
|
3354
|
-
await
|
|
4176
|
+
await fs28.promises.rm(projectDir, { recursive: true, force: true });
|
|
3355
4177
|
}
|
|
3356
4178
|
logger.info(`[create] Scaffolding ${opts.templateId} at ${projectDir}`);
|
|
3357
4179
|
await copyDirRecursive(templateDir, projectDir);
|
|
3358
4180
|
if (moduleName !== defaultModuleName) {
|
|
3359
|
-
renameIfExists(
|
|
4181
|
+
renameIfExists(path27.join(projectDir, defaultModuleName), path27.join(projectDir, moduleName));
|
|
3360
4182
|
}
|
|
3361
4183
|
updateTemplateConfigs(
|
|
3362
4184
|
projectDir,
|
|
@@ -3373,11 +4195,11 @@ async function createScaffold(opts) {
|
|
|
3373
4195
|
}
|
|
3374
4196
|
|
|
3375
4197
|
// src/project/listModules.ts
|
|
3376
|
-
import * as
|
|
3377
|
-
import * as
|
|
4198
|
+
import * as fs29 from "fs";
|
|
4199
|
+
import * as path28 from "path";
|
|
3378
4200
|
function listModules(opts) {
|
|
3379
|
-
const buildProfile =
|
|
3380
|
-
if (!
|
|
4201
|
+
const buildProfile = path28.join(opts.projectDir, "build-profile.json5");
|
|
4202
|
+
if (!fs29.existsSync(buildProfile)) {
|
|
3381
4203
|
throw new OniroError(`build-profile.json5 not found at ${buildProfile}`);
|
|
3382
4204
|
}
|
|
3383
4205
|
const parsed = readJson5File(buildProfile);
|
|
@@ -3389,12 +4211,12 @@ function listModules(opts) {
|
|
|
3389
4211
|
}
|
|
3390
4212
|
|
|
3391
4213
|
// src/project/abilities.ts
|
|
3392
|
-
import * as
|
|
3393
|
-
import * as
|
|
4214
|
+
import * as fs30 from "fs";
|
|
4215
|
+
import * as path29 from "path";
|
|
3394
4216
|
function listAbilities(opts) {
|
|
3395
4217
|
const moduleName = opts.moduleName ?? "entry";
|
|
3396
|
-
const moduleJsonPath =
|
|
3397
|
-
if (!
|
|
4218
|
+
const moduleJsonPath = path29.join(opts.projectDir, moduleName, "src", "main", "module.json5");
|
|
4219
|
+
if (!fs30.existsSync(moduleJsonPath)) {
|
|
3398
4220
|
throw new OniroError(`Could not find module.json5 at ${moduleJsonPath}`);
|
|
3399
4221
|
}
|
|
3400
4222
|
const parsed = readJson5File(moduleJsonPath);
|
|
@@ -3402,12 +4224,12 @@ function listAbilities(opts) {
|
|
|
3402
4224
|
}
|
|
3403
4225
|
|
|
3404
4226
|
// src/project/prepareSigning.ts
|
|
3405
|
-
import * as
|
|
3406
|
-
import * as
|
|
4227
|
+
import * as fs31 from "fs";
|
|
4228
|
+
import * as path30 from "path";
|
|
3407
4229
|
function hasSigningArtifacts(dir) {
|
|
3408
|
-
if (!
|
|
3409
|
-
const buildProfile =
|
|
3410
|
-
if (!
|
|
4230
|
+
if (!fs31.existsSync(path30.join(dir, "signatures"))) return false;
|
|
4231
|
+
const buildProfile = path30.join(dir, "build-profile.json5");
|
|
4232
|
+
if (!fs31.existsSync(buildProfile)) return false;
|
|
3411
4233
|
try {
|
|
3412
4234
|
const parsed = readJson5File(buildProfile);
|
|
3413
4235
|
return Array.isArray(parsed.app?.signingConfigs) && parsed.app.signingConfigs.length > 0;
|
|
@@ -3425,6 +4247,7 @@ function prepareSigning(opts) {
|
|
|
3425
4247
|
apl: opts.apl,
|
|
3426
4248
|
appFeature: opts.appFeature,
|
|
3427
4249
|
acls: opts.acls,
|
|
4250
|
+
appPrivilegeCapabilities: opts.appPrivilegeCapabilities,
|
|
3428
4251
|
passwords: opts.passwords,
|
|
3429
4252
|
logger: opts.logger
|
|
3430
4253
|
});
|
|
@@ -3453,19 +4276,31 @@ var PROPS = [
|
|
|
3453
4276
|
["model", "const.product.model"],
|
|
3454
4277
|
["manufacturer", "const.product.manufacturer"],
|
|
3455
4278
|
["brand", "const.product.brand"],
|
|
4279
|
+
["osDistName", "const.product.os.dist.name"],
|
|
4280
|
+
["osDistVersion", "const.product.os.dist.version"],
|
|
3456
4281
|
["osFullName", "const.ohos.fullname"],
|
|
3457
4282
|
["osReleaseType", "const.ohos.releasetype"],
|
|
3458
4283
|
["sdkApiVersion", "const.ohos.apiversion"],
|
|
3459
|
-
["buildVersion", "const.product.software.version"]
|
|
4284
|
+
["buildVersion", "const.product.software.version"],
|
|
4285
|
+
["abi", "const.product.cpu.abilist"],
|
|
4286
|
+
["deviceType", "const.product.devicetype"]
|
|
3460
4287
|
];
|
|
3461
4288
|
async function getDeviceInfo(opts) {
|
|
3462
4289
|
const info = { serial: opts.deviceSerial ?? "(default)", display: null };
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
4290
|
+
try {
|
|
4291
|
+
const values = await paramGetMany(
|
|
4292
|
+
opts.config,
|
|
4293
|
+
PROPS.map(([, prop]) => prop),
|
|
4294
|
+
{ deviceSerial: opts.deviceSerial, timeoutMs: opts.timeoutMs ?? 1e4, logger: opts.logger }
|
|
4295
|
+
);
|
|
4296
|
+
for (const [field, prop] of PROPS) {
|
|
4297
|
+
const value = values[prop];
|
|
3466
4298
|
if (value) info[field] = value;
|
|
3467
|
-
} catch {
|
|
3468
4299
|
}
|
|
4300
|
+
} catch {
|
|
4301
|
+
}
|
|
4302
|
+
if (info.osDistName || info.osFullName) {
|
|
4303
|
+
info.osFamily = /harmonyos/i.test(info.osDistName ?? "") ? "HarmonyOS" : "OpenHarmony";
|
|
3469
4304
|
}
|
|
3470
4305
|
const screen = await dumpScreen({ config: opts.config, deviceSerial: opts.deviceSerial, logger: opts.logger });
|
|
3471
4306
|
if (screen) info.display = { width: screen.width, height: screen.height };
|
|
@@ -3497,8 +4332,8 @@ async function diffHapAssets(opts) {
|
|
|
3497
4332
|
}
|
|
3498
4333
|
|
|
3499
4334
|
// src/install/apply.ts
|
|
3500
|
-
import * as
|
|
3501
|
-
import * as
|
|
4335
|
+
import * as fs32 from "fs";
|
|
4336
|
+
import * as path31 from "path";
|
|
3502
4337
|
function decideInstallMethod(s) {
|
|
3503
4338
|
if (!s.signInfoInconsistent) return "replace";
|
|
3504
4339
|
if (s.isSystemBundle && !s.allowUninstall) return "refuse";
|
|
@@ -3516,39 +4351,31 @@ async function pidOf(config, bundle, deviceSerial) {
|
|
|
3516
4351
|
const proc = await findRunningProcess({ config, bundle, deviceSerial });
|
|
3517
4352
|
return proc ? Number(proc.pid) : null;
|
|
3518
4353
|
}
|
|
3519
|
-
async function
|
|
3520
|
-
let
|
|
3521
|
-
if (hapPath) {
|
|
3522
|
-
|
|
4354
|
+
async function resolveHap(opts, logger) {
|
|
4355
|
+
let hap;
|
|
4356
|
+
if (opts.hapPath) {
|
|
4357
|
+
const hapPath = !path31.isAbsolute(opts.hapPath) && opts.projectDir ? path31.join(opts.projectDir, opts.hapPath) : opts.hapPath;
|
|
4358
|
+
hap = existingHapOrUnsigned(hapPath);
|
|
4359
|
+
if (!hap) throw new OniroError(`HAP not found at ${hapPath}.`);
|
|
3523
4360
|
} else {
|
|
3524
4361
|
if (!opts.projectDir) throw new OniroError("applyChanges requires hapPath or projectDir.");
|
|
3525
|
-
const
|
|
3526
|
-
if (opts.module
|
|
3527
|
-
|
|
3528
|
-
if (signed.length === 0) throw new OniroError(`No signed HAP for module ${opts.module}. Build first.`);
|
|
3529
|
-
hapPath = signed[0];
|
|
3530
|
-
} else {
|
|
3531
|
-
const withSigned = Object.entries(haps).filter(([, m]) => m.signed.length > 0);
|
|
3532
|
-
if (withSigned.length === 0) throw new OniroError("No signed HAP found. Build first.");
|
|
3533
|
-
if (withSigned.length > 1) {
|
|
3534
|
-
throw new OniroError(
|
|
3535
|
-
`Multiple modules have signed HAPs (${withSigned.map(([k]) => k).join(", ")}). Pass module to choose.`
|
|
3536
|
-
);
|
|
3537
|
-
}
|
|
3538
|
-
hapPath = withSigned[0][1].signed[0];
|
|
3539
|
-
}
|
|
4362
|
+
const picked = pickHap(await discoverHaps({ projectDir: opts.projectDir }), opts.module);
|
|
4363
|
+
if (!picked) throw new OniroError(`No HAP found${opts.module ? ` for module ${opts.module}` : ""}. Build first.`);
|
|
4364
|
+
hap = { path: picked.path, fallback: !picked.signed };
|
|
3540
4365
|
}
|
|
3541
|
-
if (
|
|
3542
|
-
return
|
|
4366
|
+
if (hap.fallback) logger.warn(`No signed HAP found; installing the unsigned ${path31.basename(hap.path)}.`);
|
|
4367
|
+
return hap;
|
|
3543
4368
|
}
|
|
3544
4369
|
async function applyChanges(opts) {
|
|
3545
4370
|
const logger = scopedLogger(opts.logger ?? noopLogger, "apply");
|
|
3546
4371
|
const { config, bundle, deviceSerial } = opts;
|
|
3547
4372
|
const isSystemBundle = opts.isSystemBundle ?? false;
|
|
3548
4373
|
const allowUninstall = opts.allowUninstall ?? false;
|
|
3549
|
-
const
|
|
4374
|
+
const hap = await resolveHap(opts, logger);
|
|
4375
|
+
const hapPath = hap.path;
|
|
4376
|
+
logger.info(`installing ${hapPath}`);
|
|
3550
4377
|
let assetsChanged = false;
|
|
3551
|
-
if (opts.installedHapPath &&
|
|
4378
|
+
if (opts.installedHapPath && fs32.existsSync(opts.installedHapPath)) {
|
|
3552
4379
|
try {
|
|
3553
4380
|
const diff = await diffHapAssets({ installedHap: opts.installedHapPath, newHap: hapPath });
|
|
3554
4381
|
assetsChanged = diff.addedAssetPaths.length > 0 || diff.removedAssetPaths.length > 0;
|
|
@@ -3569,12 +4396,21 @@ async function applyChanges(opts) {
|
|
|
3569
4396
|
if (decision === "uninstall-install") {
|
|
3570
4397
|
logger.warn(`sign-info inconsistent for ${bundle}; uninstalling then installing fresh.`);
|
|
3571
4398
|
await uninstallApp({ config, bundle, deviceSerial });
|
|
3572
|
-
|
|
4399
|
+
ensureInstallOk(
|
|
4400
|
+
await hdcExec({ config, args: ["install", hapPath], deviceSerial, timeoutMs: 3e5 }),
|
|
4401
|
+
`hdc install ${hapPath}`,
|
|
4402
|
+
hap
|
|
4403
|
+
);
|
|
3573
4404
|
installMethod = "uninstall-install";
|
|
3574
4405
|
} else {
|
|
3575
|
-
|
|
4406
|
+
ensureInstallOk(replaceRes, `hdc install -r ${hapPath}`, hap);
|
|
3576
4407
|
installMethod = "replace";
|
|
3577
4408
|
}
|
|
4409
|
+
if (!await isBundleInstalled({ config, bundle, deviceSerial })) {
|
|
4410
|
+
throw new OniroError(
|
|
4411
|
+
`hdc reported ${path31.basename(hapPath)} as installed, but bm dump does not list ${bundle}. Check that the HAP was built from this project (its bundleName must match).`
|
|
4412
|
+
);
|
|
4413
|
+
}
|
|
3578
4414
|
let postInstallPid = await pidOf(config, bundle, deviceSerial);
|
|
3579
4415
|
const rebootDecision = decideReboot({ assetsChanged, isSystemBundle, preInstallPid, postInstallPid });
|
|
3580
4416
|
let method = installMethod;
|
|
@@ -3649,9 +4485,9 @@ var AUTH_PATHS = {
|
|
|
3649
4485
|
};
|
|
3650
4486
|
|
|
3651
4487
|
// src/harmonyos/auth/tokenStore.ts
|
|
3652
|
-
import * as
|
|
4488
|
+
import * as fs33 from "fs";
|
|
3653
4489
|
import * as os11 from "os";
|
|
3654
|
-
import * as
|
|
4490
|
+
import * as path32 from "path";
|
|
3655
4491
|
import * as crypto3 from "crypto";
|
|
3656
4492
|
var ALGORITHM = "aes-256-gcm";
|
|
3657
4493
|
var KEY_LENGTH = 32;
|
|
@@ -3687,7 +4523,7 @@ function permissionHint(dir) {
|
|
|
3687
4523
|
}
|
|
3688
4524
|
function ensureDir(dir) {
|
|
3689
4525
|
try {
|
|
3690
|
-
|
|
4526
|
+
fs33.mkdirSync(dir, { recursive: true, mode: DIR_MODE });
|
|
3691
4527
|
} catch (err) {
|
|
3692
4528
|
if (err.code === "EACCES") {
|
|
3693
4529
|
throw new OniroError(permissionHint(dir), err);
|
|
@@ -3698,17 +4534,17 @@ function ensureDir(dir) {
|
|
|
3698
4534
|
function createTokenStore(opts) {
|
|
3699
4535
|
const logger = opts.logger ?? noopLogger;
|
|
3700
4536
|
const authDir = opts.config.get("harmonyosAuthDir", defaultPaths.harmonyosAuthDir());
|
|
3701
|
-
const tokenPath =
|
|
3702
|
-
const keyPath =
|
|
4537
|
+
const tokenPath = path32.join(authDir, TOKEN_FILE);
|
|
4538
|
+
const keyPath = path32.join(os11.homedir(), ".local", "share", "oniro-app", "keys", KEY_FILE);
|
|
3703
4539
|
function loadOrCreateKey() {
|
|
3704
|
-
if (
|
|
3705
|
-
const existing =
|
|
4540
|
+
if (fs33.existsSync(keyPath)) {
|
|
4541
|
+
const existing = fs33.readFileSync(keyPath);
|
|
3706
4542
|
if (existing.length === KEY_LENGTH) return existing;
|
|
3707
4543
|
logger.warn("[harmonyos] The token key file was malformed; regenerating (you will need to log in again).");
|
|
3708
4544
|
}
|
|
3709
|
-
ensureDir(
|
|
4545
|
+
ensureDir(path32.dirname(keyPath));
|
|
3710
4546
|
const key = crypto3.randomBytes(KEY_LENGTH);
|
|
3711
|
-
|
|
4547
|
+
fs33.writeFileSync(keyPath, key, { mode: FILE_MODE });
|
|
3712
4548
|
return key;
|
|
3713
4549
|
}
|
|
3714
4550
|
return {
|
|
@@ -3718,12 +4554,12 @@ function createTokenStore(opts) {
|
|
|
3718
4554
|
const payload = Buffer.from(JSON.stringify(session), "utf8");
|
|
3719
4555
|
const blob = encrypt2(loadOrCreateKey(), payload);
|
|
3720
4556
|
ensureDir(authDir);
|
|
3721
|
-
|
|
4557
|
+
fs33.writeFileSync(tokenPath, JSON.stringify(blob, null, 2), { mode: FILE_MODE });
|
|
3722
4558
|
},
|
|
3723
4559
|
load() {
|
|
3724
|
-
if (!
|
|
4560
|
+
if (!fs33.existsSync(tokenPath)) return null;
|
|
3725
4561
|
try {
|
|
3726
|
-
const blob = JSON.parse(
|
|
4562
|
+
const blob = JSON.parse(fs33.readFileSync(tokenPath, "utf8"));
|
|
3727
4563
|
if (!isEncryptedBlob(blob)) return null;
|
|
3728
4564
|
const { jwtToken, siteId } = JSON.parse(decrypt2(loadOrCreateKey(), blob).toString("utf8"));
|
|
3729
4565
|
return typeof jwtToken === "string" && jwtToken && typeof siteId === "string" ? { jwtToken, siteId } : null;
|
|
@@ -3735,7 +4571,7 @@ function createTokenStore(opts) {
|
|
|
3735
4571
|
}
|
|
3736
4572
|
logger.debug(`[harmonyos] Discarding unreadable token at ${tokenPath}.`);
|
|
3737
4573
|
try {
|
|
3738
|
-
|
|
4574
|
+
fs33.rmSync(tokenPath, { force: true });
|
|
3739
4575
|
} catch {
|
|
3740
4576
|
}
|
|
3741
4577
|
return null;
|
|
@@ -3743,7 +4579,7 @@ function createTokenStore(opts) {
|
|
|
3743
4579
|
},
|
|
3744
4580
|
clear() {
|
|
3745
4581
|
try {
|
|
3746
|
-
|
|
4582
|
+
fs33.rmSync(tokenPath, { force: true });
|
|
3747
4583
|
} catch (err) {
|
|
3748
4584
|
throw new OniroError(`Failed to remove ${tokenPath}.`, err);
|
|
3749
4585
|
}
|
|
@@ -3857,8 +4693,8 @@ function isQuit(quit) {
|
|
|
3857
4693
|
async function startCallbackServer(opts) {
|
|
3858
4694
|
let resolveCallback;
|
|
3859
4695
|
let rejectCallback;
|
|
3860
|
-
const callback = new Promise((
|
|
3861
|
-
resolveCallback =
|
|
4696
|
+
const callback = new Promise((resolve7, reject) => {
|
|
4697
|
+
resolveCallback = resolve7;
|
|
3862
4698
|
rejectCallback = reject;
|
|
3863
4699
|
});
|
|
3864
4700
|
callback.catch(() => {
|
|
@@ -3917,9 +4753,9 @@ async function startCallbackServer(opts) {
|
|
|
3917
4753
|
}
|
|
3918
4754
|
});
|
|
3919
4755
|
server.keepAliveTimeout = 1;
|
|
3920
|
-
await new Promise((
|
|
4756
|
+
await new Promise((resolve7, reject) => {
|
|
3921
4757
|
server.once("error", (err) => reject(new OniroError("Failed to start the local login callback server.", err)));
|
|
3922
|
-
server.listen(0, "127.0.0.1",
|
|
4758
|
+
server.listen(0, "127.0.0.1", resolve7);
|
|
3923
4759
|
});
|
|
3924
4760
|
const port = server.address().port;
|
|
3925
4761
|
return {
|
|
@@ -3935,12 +4771,12 @@ async function startCallbackServer(opts) {
|
|
|
3935
4771
|
return Promise.race([callback, timeout]).finally(() => clearTimeout(timer));
|
|
3936
4772
|
},
|
|
3937
4773
|
stop() {
|
|
3938
|
-
return new Promise((
|
|
4774
|
+
return new Promise((resolve7) => {
|
|
3939
4775
|
server.closeAllConnections?.();
|
|
3940
|
-
const timer = setTimeout(
|
|
4776
|
+
const timer = setTimeout(resolve7, 100);
|
|
3941
4777
|
server.close(() => {
|
|
3942
4778
|
clearTimeout(timer);
|
|
3943
|
-
|
|
4779
|
+
resolve7();
|
|
3944
4780
|
});
|
|
3945
4781
|
});
|
|
3946
4782
|
}
|
|
@@ -3972,11 +4808,11 @@ function openBrowser(url) {
|
|
|
3972
4808
|
windowsHide: true
|
|
3973
4809
|
});
|
|
3974
4810
|
child.unref();
|
|
3975
|
-
return new Promise((
|
|
4811
|
+
return new Promise((resolve7, reject) => {
|
|
3976
4812
|
child.on("error", (err) => reject(new OniroError(`Failed to open a browser (${command}).`, err)));
|
|
3977
4813
|
child.on(
|
|
3978
4814
|
"exit",
|
|
3979
|
-
(code) => code === 0 ?
|
|
4815
|
+
(code) => code === 0 ? resolve7() : reject(new OniroError(`Browser launcher '${command}' exited with code ${code}.`))
|
|
3980
4816
|
);
|
|
3981
4817
|
});
|
|
3982
4818
|
}
|
|
@@ -4224,7 +5060,7 @@ function mapAgcError(operation, statusCode, reasonPhrase, body) {
|
|
|
4224
5060
|
}
|
|
4225
5061
|
|
|
4226
5062
|
// src/harmonyos/signing/profile.ts
|
|
4227
|
-
import * as
|
|
5063
|
+
import * as fs34 from "fs";
|
|
4228
5064
|
import { X509Certificate } from "crypto";
|
|
4229
5065
|
import forge from "node-forge";
|
|
4230
5066
|
function extractProfileJson(buffer) {
|
|
@@ -4256,7 +5092,7 @@ function extractProfileJson(buffer) {
|
|
|
4256
5092
|
function readProvisionProfile(profilePath) {
|
|
4257
5093
|
let raw;
|
|
4258
5094
|
try {
|
|
4259
|
-
const json = extractProfileJson(
|
|
5095
|
+
const json = extractProfileJson(fs34.readFileSync(profilePath));
|
|
4260
5096
|
if (!json) return null;
|
|
4261
5097
|
raw = JSON.parse(json);
|
|
4262
5098
|
} catch {
|
|
@@ -4285,7 +5121,7 @@ function parseCertificate(pem) {
|
|
|
4285
5121
|
}
|
|
4286
5122
|
function keystoreHoldsKey(p12Path, keyAlias, keyPwd, certificate) {
|
|
4287
5123
|
try {
|
|
4288
|
-
const der = forge.util.createBuffer(
|
|
5124
|
+
const der = forge.util.createBuffer(fs34.readFileSync(p12Path).toString("binary"));
|
|
4289
5125
|
const p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(der), keyPwd);
|
|
4290
5126
|
const certBag = forge.pki.oids.certBag ?? "1.2.840.113549.1.12.10.1.3";
|
|
4291
5127
|
const wanted = certificate.publicKey.export({ type: "spki", format: "der" });
|
|
@@ -4301,7 +5137,7 @@ function keystoreHoldsKey(p12Path, keyAlias, keyPwd, certificate) {
|
|
|
4301
5137
|
}
|
|
4302
5138
|
}
|
|
4303
5139
|
function verifySigningMaterial(opts) {
|
|
4304
|
-
const pems =
|
|
5140
|
+
const pems = fs34.readFileSync(opts.cerPath, "utf8").match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
4305
5141
|
const chain = (pems ?? []).map(parseCertificate);
|
|
4306
5142
|
const leaf = chain.find((c) => !c.ca) ?? chain.at(-1);
|
|
4307
5143
|
if (!leaf) throw new AgcError(SIGNING_ERRORS.CERT_INVALID);
|
|
@@ -4320,14 +5156,14 @@ function verifySigningMaterial(opts) {
|
|
|
4320
5156
|
|
|
4321
5157
|
// src/harmonyos/signing/autoSign.ts
|
|
4322
5158
|
import * as crypto9 from "crypto";
|
|
4323
|
-
import * as
|
|
4324
|
-
import * as
|
|
5159
|
+
import * as fs39 from "fs";
|
|
5160
|
+
import * as path38 from "path";
|
|
4325
5161
|
import JSON57 from "json5";
|
|
4326
5162
|
|
|
4327
5163
|
// src/harmonyos/signing/agc.ts
|
|
4328
5164
|
import * as crypto6 from "crypto";
|
|
4329
|
-
import * as
|
|
4330
|
-
import * as
|
|
5165
|
+
import * as fs35 from "fs";
|
|
5166
|
+
import * as path33 from "path";
|
|
4331
5167
|
var ENDPOINTS = {
|
|
4332
5168
|
CERT_LIST: "/api/cps/harmony-cert-manage/v1/cert/list",
|
|
4333
5169
|
CERT_ADD: "/api/cps/harmony-cert-manage/v1/cert/add",
|
|
@@ -4413,8 +5249,8 @@ function createAgcClient(http3, auth) {
|
|
|
4413
5249
|
if (target.sha256 && actual !== target.sha256.toLowerCase()) {
|
|
4414
5250
|
throw new ChecksumMismatchError(target.sha256, actual);
|
|
4415
5251
|
}
|
|
4416
|
-
|
|
4417
|
-
|
|
5252
|
+
fs35.mkdirSync(path33.dirname(dest), { recursive: true, mode: 448 });
|
|
5253
|
+
fs35.writeFileSync(dest, buffer, { mode: 384 });
|
|
4418
5254
|
}
|
|
4419
5255
|
};
|
|
4420
5256
|
}
|
|
@@ -4465,22 +5301,22 @@ async function registerDevices(agc, connected, logger) {
|
|
|
4465
5301
|
|
|
4466
5302
|
// src/harmonyos/signing/keystore.ts
|
|
4467
5303
|
import * as crypto7 from "crypto";
|
|
4468
|
-
import * as
|
|
4469
|
-
import * as
|
|
5304
|
+
import * as fs37 from "fs";
|
|
5305
|
+
import * as path35 from "path";
|
|
4470
5306
|
|
|
4471
5307
|
// src/harmonyos/java.ts
|
|
4472
|
-
import * as
|
|
4473
|
-
import * as
|
|
5308
|
+
import * as fs36 from "fs";
|
|
5309
|
+
import * as path34 from "path";
|
|
4474
5310
|
var JAVA_BIN = process.platform === "win32" ? "java.exe" : "java";
|
|
4475
5311
|
function findJava(sdk, env = process.env) {
|
|
4476
5312
|
const candidates = [
|
|
4477
|
-
env.JAVA_HOME &&
|
|
4478
|
-
sdk &&
|
|
5313
|
+
env.JAVA_HOME && path34.join(env.JAVA_HOME, "bin", JAVA_BIN),
|
|
5314
|
+
sdk && path34.join(sdk.installRoot, "jbr", "bin", JAVA_BIN),
|
|
4479
5315
|
// macOS: the JBR is itself a bundle inside Studio's Contents.
|
|
4480
|
-
sdk &&
|
|
4481
|
-
...(env.PATH ?? "").split(
|
|
5316
|
+
sdk && path34.join(sdk.installRoot, "jbr", "Contents", "Home", "bin", JAVA_BIN),
|
|
5317
|
+
...(env.PATH ?? "").split(path34.delimiter).map((dir) => dir && path34.join(dir, JAVA_BIN))
|
|
4482
5318
|
];
|
|
4483
|
-
const found = candidates.find((c) => !!c &&
|
|
5319
|
+
const found = candidates.find((c) => !!c && fs36.existsSync(c));
|
|
4484
5320
|
if (found) return found;
|
|
4485
5321
|
throw new OniroError(
|
|
4486
5322
|
"No Java runtime found. HarmonyOS signing runs the SDK's hap-sign-tool.jar, which needs one: install a JDK and set JAVA_HOME, or install DevEco Studio, whose bundled runtime is used automatically."
|
|
@@ -4494,13 +5330,13 @@ function redactArgs(args) {
|
|
|
4494
5330
|
return args.map((arg, i) => args[i - 1] === "-keyPwd" || args[i - 1] === "-keystorePwd" ? "******" : arg);
|
|
4495
5331
|
}
|
|
4496
5332
|
async function generateKeystoreAndCsr(opts) {
|
|
4497
|
-
const jar =
|
|
4498
|
-
if (!
|
|
5333
|
+
const jar = path35.join(opts.sdk.sdkPath, "default", "openharmony", "toolchains", "lib", "hap-sign-tool.jar");
|
|
5334
|
+
if (!fs37.existsSync(jar)) {
|
|
4499
5335
|
throw new OniroError(`hap-sign-tool.jar not found at ${jar}; the HarmonyOS SDK looks incomplete.`);
|
|
4500
5336
|
}
|
|
4501
5337
|
const java = findJava(opts.sdk);
|
|
4502
5338
|
const keyPwd = crypto7.randomBytes(12).toString("base64url").replaceAll(/[-_]/g, "");
|
|
4503
|
-
|
|
5339
|
+
fs37.mkdirSync(path35.dirname(opts.p12Path), { recursive: true, mode: 448 });
|
|
4504
5340
|
const run = async (what, args) => {
|
|
4505
5341
|
const argv = ["-jar", jar, ...args, "-keyAlias", KEY_ALIAS, "-keystoreFile", opts.p12Path, "-keystorePwd", keyPwd, "-keyPwd", keyPwd];
|
|
4506
5342
|
opts.logger.debug(`[harmonyos] ${java} ${redactArgs(argv).join(" ")}`);
|
|
@@ -4514,28 +5350,28 @@ ${detail}` : ""}`);
|
|
|
4514
5350
|
opts.logger.info("[harmonyos] Generating the local keystore and certificate signing request...");
|
|
4515
5351
|
await run("generate the key pair", ["generate-keypair", "-keyAlg", "ECC", "-keySize", "NIST-P-256"]);
|
|
4516
5352
|
await run("generate the CSR", ["generate-csr", "-subject", "CN=DebugKey", "-signAlg", SIGN_ALG, "-outFile", opts.csrPath]);
|
|
4517
|
-
return { keyPwd, csr:
|
|
5353
|
+
return { keyPwd, csr: fs37.readFileSync(opts.csrPath, "utf8") };
|
|
4518
5354
|
}
|
|
4519
5355
|
|
|
4520
5356
|
// src/harmonyos/signing/paths.ts
|
|
4521
5357
|
import * as crypto8 from "crypto";
|
|
4522
|
-
import * as
|
|
5358
|
+
import * as path36 from "path";
|
|
4523
5359
|
function sanitizeNamePart(raw) {
|
|
4524
5360
|
const safe = (raw?.trim() ?? "").replace(/[\\/:*?"<>|=.-]+/g, "_");
|
|
4525
5361
|
return safe.slice(0, 64) || "default";
|
|
4526
5362
|
}
|
|
4527
5363
|
function resolveSigningMaterialPaths(config, productName, projectRoot) {
|
|
4528
5364
|
const hash = crypto8.createHash("sha256").update(projectRoot, "utf8").digest("base64url").replaceAll(/[-_]/g, "");
|
|
4529
|
-
const base =
|
|
5365
|
+
const base = path36.join(
|
|
4530
5366
|
config.get("harmonyosSigningDir", defaultPaths.harmonyosSigningDir()),
|
|
4531
|
-
`${sanitizeNamePart(productName)}_${
|
|
5367
|
+
`${sanitizeNamePart(productName)}_${path36.basename(projectRoot)}_${hash}=`
|
|
4532
5368
|
);
|
|
4533
5369
|
return { p12Path: `${base}.p12`, csrPath: `${base}.csr`, cerPath: `${base}.cer`, profilePath: `${base}.p7b` };
|
|
4534
5370
|
}
|
|
4535
5371
|
|
|
4536
5372
|
// src/harmonyos/signing/project.ts
|
|
4537
|
-
import * as
|
|
4538
|
-
import * as
|
|
5373
|
+
import * as fs38 from "fs";
|
|
5374
|
+
import * as path37 from "path";
|
|
4539
5375
|
import JSON56 from "json5";
|
|
4540
5376
|
|
|
4541
5377
|
// src/harmonyos/signing/aclPermissionList.ts
|
|
@@ -4603,9 +5439,9 @@ var ACL_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
|
4603
5439
|
|
|
4604
5440
|
// src/harmonyos/signing/project.ts
|
|
4605
5441
|
function readRequestedPermissions(moduleConfigPath) {
|
|
4606
|
-
if (!
|
|
5442
|
+
if (!fs38.existsSync(moduleConfigPath)) return [];
|
|
4607
5443
|
try {
|
|
4608
|
-
const parsed = JSON56.parse(
|
|
5444
|
+
const parsed = JSON56.parse(fs38.readFileSync(moduleConfigPath, "utf-8"));
|
|
4609
5445
|
return (parsed.module?.requestPermissions ?? []).map((p) => p?.name).filter((n) => typeof n === "string" && n.length > 0);
|
|
4610
5446
|
} catch {
|
|
4611
5447
|
return [];
|
|
@@ -4614,10 +5450,10 @@ function readRequestedPermissions(moduleConfigPath) {
|
|
|
4614
5450
|
function collectAclPermissions(projectDir, logger = noopLogger) {
|
|
4615
5451
|
const requested = /* @__PURE__ */ new Set();
|
|
4616
5452
|
for (const module of listModules({ projectDir })) {
|
|
4617
|
-
const moduleRoot =
|
|
5453
|
+
const moduleRoot = path37.resolve(projectDir, module.srcPath);
|
|
4618
5454
|
for (const sourceSet of ["main", "ohosTest"]) {
|
|
4619
5455
|
for (const name of readRequestedPermissions(
|
|
4620
|
-
|
|
5456
|
+
path37.join(moduleRoot, "src", sourceSet, "module.json5")
|
|
4621
5457
|
)) {
|
|
4622
5458
|
requested.add(name);
|
|
4623
5459
|
}
|
|
@@ -4633,7 +5469,7 @@ function collectAclPermissions(projectDir, logger = noopLogger) {
|
|
|
4633
5469
|
// src/harmonyos/signing/autoSign.ts
|
|
4634
5470
|
function shouldRegenerate(input) {
|
|
4635
5471
|
if (input.force) return "--force was passed";
|
|
4636
|
-
const missing = Object.values(input.paths).find((file) => !
|
|
5472
|
+
const missing = Object.values(input.paths).find((file) => !fs39.existsSync(file));
|
|
4637
5473
|
if (missing) return `${missing} is missing`;
|
|
4638
5474
|
const config = input.buildProfile.app?.signingConfigs?.find((c) => c.name === input.productName);
|
|
4639
5475
|
if (config?.type !== "HarmonyOS" || config.material?.profile !== input.paths.profilePath) {
|
|
@@ -4672,16 +5508,16 @@ function writeSigningConfig(opts) {
|
|
|
4672
5508
|
for (const product of app.products ?? []) {
|
|
4673
5509
|
if (product.name === opts.productName) product.signingConfig = opts.productName;
|
|
4674
5510
|
}
|
|
4675
|
-
|
|
5511
|
+
fs39.writeFileSync(opts.buildProfilePath, JSON.stringify(opts.buildProfile, null, 2));
|
|
4676
5512
|
}
|
|
4677
5513
|
async function harmonyOsAutoSign(opts) {
|
|
4678
5514
|
const logger = opts.logger ?? noopLogger;
|
|
4679
5515
|
const productName = opts.productName ?? "default";
|
|
4680
|
-
const projectDir =
|
|
4681
|
-
const buildProfilePath =
|
|
5516
|
+
const projectDir = path38.resolve(opts.projectDir);
|
|
5517
|
+
const buildProfilePath = path38.join(projectDir, "build-profile.json5");
|
|
4682
5518
|
let buildProfile;
|
|
4683
5519
|
try {
|
|
4684
|
-
buildProfile = JSON57.parse(
|
|
5520
|
+
buildProfile = JSON57.parse(fs39.readFileSync(buildProfilePath, "utf-8"));
|
|
4685
5521
|
} catch (err) {
|
|
4686
5522
|
throw new OniroError(`Could not read ${buildProfilePath}: ${err.message}`, err);
|
|
4687
5523
|
}
|
|
@@ -4721,7 +5557,7 @@ async function harmonyOsAutoSign(opts) {
|
|
|
4721
5557
|
logger.info(`[harmonyos] Regenerating signing material: ${reason}.`);
|
|
4722
5558
|
const agc = createAgcClient(opts.http ?? createHttpClient({ logger }), auth);
|
|
4723
5559
|
const deviceIds = await registerDevices(agc, connected, logger);
|
|
4724
|
-
for (const file of Object.values(paths))
|
|
5560
|
+
for (const file of Object.values(paths)) fs39.rmSync(file, { force: true });
|
|
4725
5561
|
const certName = `oniro_debug_${auth.teamId.replace(/[\\/.:]/g, "")}.cer`;
|
|
4726
5562
|
const stale = (await agc.listCertificates()).find((c) => c.certName === certName);
|
|
4727
5563
|
if (stale) {
|
|
@@ -4752,10 +5588,10 @@ async function harmonyOsAutoSign(opts) {
|
|
|
4752
5588
|
try {
|
|
4753
5589
|
verifySigningMaterial({ ...paths, keyAlias: KEY_ALIAS, keyPwd });
|
|
4754
5590
|
} catch (err) {
|
|
4755
|
-
|
|
5591
|
+
fs39.rmSync(paths.profilePath, { force: true });
|
|
4756
5592
|
throw err;
|
|
4757
5593
|
}
|
|
4758
|
-
const materialDir =
|
|
5594
|
+
const materialDir = path38.join(path38.dirname(paths.p12Path), "material");
|
|
4759
5595
|
try {
|
|
4760
5596
|
getKey(materialDir);
|
|
4761
5597
|
} catch {
|
|
@@ -4767,7 +5603,7 @@ async function harmonyOsAutoSign(opts) {
|
|
|
4767
5603
|
}
|
|
4768
5604
|
|
|
4769
5605
|
// src/index.ts
|
|
4770
|
-
var VERSION = "0.
|
|
5606
|
+
var VERSION = "0.14.0";
|
|
4771
5607
|
export {
|
|
4772
5608
|
ALL_SDKS,
|
|
4773
5609
|
APL_VALUES,
|
|
@@ -4786,9 +5622,13 @@ export {
|
|
|
4786
5622
|
SIGNING_ERRORS,
|
|
4787
5623
|
SdkNotInstalledError,
|
|
4788
5624
|
TMP_DIR_HINT,
|
|
5625
|
+
TargetNotFoundError,
|
|
4789
5626
|
UnsupportedPlatformError,
|
|
4790
5627
|
VERSION,
|
|
5628
|
+
appSandboxDir,
|
|
4791
5629
|
applyChanges,
|
|
5630
|
+
assertDevicePath,
|
|
5631
|
+
assertLayoutSelector,
|
|
4792
5632
|
attemptHdcConnection,
|
|
4793
5633
|
buildCmdWrapper,
|
|
4794
5634
|
buildGestureCommands,
|
|
@@ -4798,6 +5638,8 @@ export {
|
|
|
4798
5638
|
buildInputCommand,
|
|
4799
5639
|
buildRawTouchCommand,
|
|
4800
5640
|
captureBurst,
|
|
5641
|
+
captureLog,
|
|
5642
|
+
cleanCodelinterOutput,
|
|
4801
5643
|
consoleLogger,
|
|
4802
5644
|
createHarmonyOsSession,
|
|
4803
5645
|
createInstallTempDir,
|
|
@@ -4811,10 +5653,14 @@ export {
|
|
|
4811
5653
|
dedupEntries,
|
|
4812
5654
|
defaultContext,
|
|
4813
5655
|
defaultPaths,
|
|
5656
|
+
describeLayoutNode,
|
|
4814
5657
|
detectAaStartFailure,
|
|
5658
|
+
detectCmdToolsFlavour,
|
|
5659
|
+
detectInstallFailure,
|
|
4815
5660
|
detectProjectSdkVersion,
|
|
4816
5661
|
detectRuntimeOs,
|
|
4817
5662
|
detectSigningConfigNames,
|
|
5663
|
+
diagnoseBuildFailure,
|
|
4818
5664
|
diffEntryNames,
|
|
4819
5665
|
diffHapAssets,
|
|
4820
5666
|
discoverHaps,
|
|
@@ -4827,17 +5673,24 @@ export {
|
|
|
4827
5673
|
dumpWindow,
|
|
4828
5674
|
encryptPwd,
|
|
4829
5675
|
ensureFreeSpace,
|
|
5676
|
+
ensureInstallOk,
|
|
4830
5677
|
ensureOhModules,
|
|
4831
5678
|
ensureOk,
|
|
4832
5679
|
ensureRemoteOk,
|
|
4833
5680
|
escapeCmdArgument,
|
|
4834
5681
|
escapeCmdCommand,
|
|
5682
|
+
existingHapOrUnsigned,
|
|
4835
5683
|
extractTarball,
|
|
4836
5684
|
extractZipWithProgress,
|
|
4837
5685
|
findAppProcessId,
|
|
4838
5686
|
findCmdToolsSourceDir,
|
|
5687
|
+
findCoveredNodes,
|
|
4839
5688
|
findHarmonyOsSdk,
|
|
5689
|
+
findKeyboardWindow,
|
|
5690
|
+
findLayoutNodes,
|
|
5691
|
+
findMissingProjectSdk,
|
|
4840
5692
|
findRunningProcess,
|
|
5693
|
+
flattenLayout,
|
|
4841
5694
|
forceStop,
|
|
4842
5695
|
formatBytes,
|
|
4843
5696
|
generateSigningConfigs,
|
|
@@ -4849,9 +5702,11 @@ export {
|
|
|
4849
5702
|
getDeviceInfo,
|
|
4850
5703
|
getDisplaySize,
|
|
4851
5704
|
getEmulatorDir,
|
|
5705
|
+
getEmulatorStatus,
|
|
4852
5706
|
getFreeSpaceBytes,
|
|
4853
5707
|
getHdcPath,
|
|
4854
5708
|
getHvigorwPath,
|
|
5709
|
+
getImeBundles,
|
|
4855
5710
|
getInstalledSdks,
|
|
4856
5711
|
getKey,
|
|
4857
5712
|
getMainAbility,
|
|
@@ -4863,14 +5718,21 @@ export {
|
|
|
4863
5718
|
getSupportedSdksForUi,
|
|
4864
5719
|
harmonyOsAutoSign,
|
|
4865
5720
|
harmonyOsSdkLabelForApi,
|
|
5721
|
+
hdcClientFailure,
|
|
4866
5722
|
hdcExec,
|
|
5723
|
+
hdcTargetArgs,
|
|
5724
|
+
hdcTargetArgsFor,
|
|
5725
|
+
hideKeyboard,
|
|
4867
5726
|
installApp,
|
|
4868
5727
|
installCmdTools,
|
|
4869
5728
|
installEmulator,
|
|
5729
|
+
installFailureReason,
|
|
5730
|
+
isBundleInstalled,
|
|
4870
5731
|
isCmdToolsInstalled,
|
|
4871
5732
|
isEmulatorInstalled,
|
|
4872
5733
|
isOutOfSpaceError,
|
|
4873
5734
|
isRamBackedPath,
|
|
5735
|
+
isUnsignedHap,
|
|
4874
5736
|
isValidBundleName,
|
|
4875
5737
|
isValidProjectName,
|
|
4876
5738
|
killProcessTree,
|
|
@@ -4880,19 +5742,28 @@ export {
|
|
|
4880
5742
|
listModules,
|
|
4881
5743
|
listRunningProcesses,
|
|
4882
5744
|
listTemplates,
|
|
5745
|
+
locateTarget,
|
|
4883
5746
|
needsCmdWrapper,
|
|
4884
5747
|
nonInteractivePrompter,
|
|
4885
5748
|
noopLogger,
|
|
4886
5749
|
noopProgress,
|
|
5750
|
+
normaliseLocalPath,
|
|
4887
5751
|
paramGet,
|
|
5752
|
+
paramGetMany,
|
|
4888
5753
|
paramSet,
|
|
4889
5754
|
parseBounds,
|
|
4890
5755
|
parseCodelinterFindings,
|
|
5756
|
+
parseCodelinterReport,
|
|
4891
5757
|
parseDeviceList,
|
|
4892
5758
|
parseHilogLine,
|
|
5759
|
+
parseImeBundles,
|
|
4893
5760
|
parseMainAbilityFromBmDump,
|
|
5761
|
+
parseParamValues,
|
|
4894
5762
|
parseRemoteExit,
|
|
5763
|
+
performInput,
|
|
5764
|
+
pickHap,
|
|
4895
5765
|
pickSigningKind,
|
|
5766
|
+
pickTarget,
|
|
4896
5767
|
prepareSigning,
|
|
4897
5768
|
pruneLayout,
|
|
4898
5769
|
readJson5File,
|
|
@@ -4905,6 +5776,8 @@ export {
|
|
|
4905
5776
|
removeSdk,
|
|
4906
5777
|
removeTempWorkDir,
|
|
4907
5778
|
requireHarmonyOsSdk,
|
|
5779
|
+
resolveDeviceSerial,
|
|
5780
|
+
resolveKeyEvent,
|
|
4908
5781
|
resolveTmpRoot,
|
|
4909
5782
|
runCodelinter,
|
|
4910
5783
|
runHvigorw,
|
|
@@ -4928,14 +5801,17 @@ export {
|
|
|
4928
5801
|
toLongPath,
|
|
4929
5802
|
toSpaceError,
|
|
4930
5803
|
uninstallApp,
|
|
5804
|
+
unsignedHapRejection,
|
|
4931
5805
|
validateTemplateLayout,
|
|
4932
5806
|
verifySha256,
|
|
4933
5807
|
waitForBoot,
|
|
4934
5808
|
waitForBundle,
|
|
4935
5809
|
waitForCondition,
|
|
4936
5810
|
waitForLog,
|
|
5811
|
+
waitForTarget,
|
|
4937
5812
|
watchLog,
|
|
4938
5813
|
waypointsToRawEvents,
|
|
5814
|
+
withoutWindow,
|
|
4939
5815
|
writeJson5File,
|
|
4940
5816
|
writeJsonFile
|
|
4941
5817
|
};
|