@lynxship/cli 0.1.8 → 0.1.11
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 +175 -19
- package/dist/android-toolchain.d.ts +23 -0
- package/dist/android-toolchain.d.ts.map +1 -0
- package/dist/android-toolchain.js +319 -0
- package/dist/artifact-build.d.ts +17 -0
- package/dist/artifact-build.d.ts.map +1 -0
- package/dist/artifact-build.js +56 -0
- package/dist/artifact-name.d.ts +1 -1
- package/dist/artifact-name.d.ts.map +1 -1
- package/dist/autolink.d.ts +3 -2
- package/dist/autolink.d.ts.map +1 -1
- package/dist/autolink.js +130 -6
- package/dist/bundle-build.d.ts +2 -0
- package/dist/bundle-build.d.ts.map +1 -1
- package/dist/bundle-build.js +11 -3
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/desktop-build.d.ts +22 -0
- package/dist/desktop-build.d.ts.map +1 -0
- package/dist/desktop-build.js +161 -0
- package/dist/desktop-signing.d.ts +14 -0
- package/dist/desktop-signing.d.ts.map +1 -0
- package/dist/desktop-signing.js +200 -0
- package/dist/dev-qr.d.ts +3 -0
- package/dist/dev-qr.d.ts.map +1 -0
- package/dist/dev-qr.js +12 -0
- package/dist/guidance.d.ts.map +1 -1
- package/dist/guidance.js +113 -4
- package/dist/harmony-build.d.ts +22 -0
- package/dist/harmony-build.d.ts.map +1 -0
- package/dist/harmony-build.js +194 -0
- package/dist/index.js +359 -74
- package/dist/ios-toolchain.d.ts +19 -0
- package/dist/ios-toolchain.d.ts.map +1 -0
- package/dist/ios-toolchain.js +303 -0
- package/dist/lynx-devtool.d.ts +18 -0
- package/dist/lynx-devtool.d.ts.map +1 -0
- package/dist/lynx-devtool.js +125 -0
- package/dist/process-runner.d.ts +11 -0
- package/dist/process-runner.d.ts.map +1 -1
- package/dist/process-runner.js +29 -0
- package/dist/prompt.d.ts +1 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +6 -0
- package/dist/remote.d.ts +3 -3
- package/dist/remote.d.ts.map +1 -1
- package/dist/target-toolchain.d.ts +17 -0
- package/dist/target-toolchain.d.ts.map +1 -0
- package/dist/target-toolchain.js +122 -0
- package/dist/ui/components.d.ts +1 -0
- package/dist/ui/components.d.ts.map +1 -1
- package/dist/ui/components.js +7 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +4 -1
- package/dist/web-build.d.ts +16 -0
- package/dist/web-build.d.ts.map +1 -0
- package/dist/web-build.js +116 -0
- package/package.json +3 -3
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { captureProcess, commandExists } from "./process-runner.js";
|
|
5
|
+
async function manifest(root) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function hasConfiguredCertificate(configuration) {
|
|
14
|
+
if (process.platform === "win32") {
|
|
15
|
+
const windows = configuration.win;
|
|
16
|
+
if (windows?.signAndEditExecutable === false)
|
|
17
|
+
return "missing";
|
|
18
|
+
if (process.env.WIN_CSC_LINK ||
|
|
19
|
+
process.env.CSC_LINK ||
|
|
20
|
+
windows?.certificateFile ||
|
|
21
|
+
windows?.certificateSubjectName)
|
|
22
|
+
return "configured";
|
|
23
|
+
return "missing";
|
|
24
|
+
}
|
|
25
|
+
if (process.platform === "darwin") {
|
|
26
|
+
const mac = configuration.mac;
|
|
27
|
+
if (process.env.CSC_LINK ||
|
|
28
|
+
process.env.CSC_NAME ||
|
|
29
|
+
mac?.identity ||
|
|
30
|
+
mac?.certificateFile)
|
|
31
|
+
return "configured";
|
|
32
|
+
return "missing";
|
|
33
|
+
}
|
|
34
|
+
return "configured";
|
|
35
|
+
}
|
|
36
|
+
export async function inspectDesktopSigning(root) {
|
|
37
|
+
if (process.platform !== "win32" && process.platform !== "darwin")
|
|
38
|
+
return {
|
|
39
|
+
status: "not-required",
|
|
40
|
+
value: `${process.platform} desktop packaging does not use Authenticode or Apple code signing`,
|
|
41
|
+
fix: "Use the target operating system's distribution signing policy before publishing.",
|
|
42
|
+
};
|
|
43
|
+
const configuration = (await manifest(root)).build ?? {};
|
|
44
|
+
if (process.platform === "win32" &&
|
|
45
|
+
configuration.win?.signAndEditExecutable === false)
|
|
46
|
+
return {
|
|
47
|
+
status: "disabled",
|
|
48
|
+
value: "Windows executable signing is disabled in Electron Builder",
|
|
49
|
+
fix: "Remove build.win.signAndEditExecutable=false and configure WIN_CSC_LINK/CSC_KEY_PASSWORD or a certificateSubjectName.",
|
|
50
|
+
};
|
|
51
|
+
const configured = hasConfiguredCertificate(configuration);
|
|
52
|
+
if (configured === "configured")
|
|
53
|
+
return {
|
|
54
|
+
status: "configured",
|
|
55
|
+
value: process.platform === "win32"
|
|
56
|
+
? "Windows Authenticode certificate input detected"
|
|
57
|
+
: "Apple desktop signing input detected",
|
|
58
|
+
fix: "The final artifact signature will be verified after packaging.",
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
status: process.platform === "darwin" ? "unknown" : "missing",
|
|
62
|
+
value: process.platform === "win32"
|
|
63
|
+
? "Windows Authenticode certificate not configured"
|
|
64
|
+
: "Apple desktop signing identity not explicitly configured",
|
|
65
|
+
fix: process.platform === "win32"
|
|
66
|
+
? "Configure WIN_CSC_LINK or CSC_LINK and CSC_KEY_PASSWORD, or set build.win.certificateSubjectName, then rerun lynxship doctor --platform desktop."
|
|
67
|
+
: "Configure CSC_LINK/CSC_NAME or the mac.identity in Electron Builder; the final build must pass codesign verification.",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function verifyWindowsArtifact(artifactPath) {
|
|
71
|
+
const windowsPowerShell = process.env.SystemRoot
|
|
72
|
+
? join(process.env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
73
|
+
: "powershell.exe";
|
|
74
|
+
const powershell = commandExists(windowsPowerShell)
|
|
75
|
+
? windowsPowerShell
|
|
76
|
+
: commandExists("powershell.exe")
|
|
77
|
+
? "powershell.exe"
|
|
78
|
+
: commandExists("pwsh")
|
|
79
|
+
? "pwsh"
|
|
80
|
+
: undefined;
|
|
81
|
+
if (!powershell)
|
|
82
|
+
return {
|
|
83
|
+
signed: false,
|
|
84
|
+
status: "unavailable",
|
|
85
|
+
detail: "PowerShell is unavailable; Authenticode could not be verified.",
|
|
86
|
+
};
|
|
87
|
+
const result = await captureProcess(powershell, [
|
|
88
|
+
"-NoProfile",
|
|
89
|
+
"-NonInteractive",
|
|
90
|
+
"-Command",
|
|
91
|
+
"if (-not (Get-Command Get-AuthenticodeSignature -ErrorAction SilentlyContinue)) { Write-Output 'UNAVAILABLE'; exit 2 }; $signature = Get-AuthenticodeSignature -LiteralPath $env:LYNXSHIP_ARTIFACT_PATH; Write-Output $signature.Status; if ($signature.Status -ne 'Valid') { exit 1 }",
|
|
92
|
+
], {
|
|
93
|
+
cwd: dirname(artifactPath),
|
|
94
|
+
env: {
|
|
95
|
+
...process.env,
|
|
96
|
+
LYNXSHIP_ARTIFACT_PATH: artifactPath,
|
|
97
|
+
...(process.env.SystemRoot
|
|
98
|
+
? {
|
|
99
|
+
PSModulePath: [
|
|
100
|
+
join(process.env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "Modules"),
|
|
101
|
+
process.env.ProgramFiles
|
|
102
|
+
? join(process.env.ProgramFiles, "WindowsPowerShell", "Modules")
|
|
103
|
+
: undefined,
|
|
104
|
+
]
|
|
105
|
+
.filter((value) => Boolean(value))
|
|
106
|
+
.join(";"),
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const detail = (result.stdout || result.stderr).trim().split(/\r?\n/).at(-1);
|
|
112
|
+
if (result.code === 0)
|
|
113
|
+
return { signed: true, status: "signed", detail: detail || "Valid" };
|
|
114
|
+
if (result.code === 2 ||
|
|
115
|
+
/(?:couldnotautoload|commandnotfound|not recognized|unavailable)/i.test(detail ?? ""))
|
|
116
|
+
return {
|
|
117
|
+
signed: false,
|
|
118
|
+
status: "unavailable",
|
|
119
|
+
detail: "PowerShell Authenticode support is unavailable on this machine.",
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
signed: false,
|
|
123
|
+
status: "unsigned",
|
|
124
|
+
detail: detail || "Authenticode signature is not valid.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function verifyMacArtifact(artifactPath) {
|
|
128
|
+
const extension = extname(artifactPath).toLowerCase();
|
|
129
|
+
let appPath = extension === ".app" ? artifactPath : undefined;
|
|
130
|
+
let mountPath;
|
|
131
|
+
try {
|
|
132
|
+
if (extension === ".dmg") {
|
|
133
|
+
if (!commandExists("hdiutil"))
|
|
134
|
+
return {
|
|
135
|
+
signed: false,
|
|
136
|
+
status: "unavailable",
|
|
137
|
+
detail: "hdiutil is unavailable; the DMG application could not be inspected.",
|
|
138
|
+
};
|
|
139
|
+
mountPath = await mkdtemp(join(tmpdir(), "lynxship-dmg-"));
|
|
140
|
+
const mounted = await captureProcess("hdiutil", [
|
|
141
|
+
"attach",
|
|
142
|
+
artifactPath,
|
|
143
|
+
"-nobrowse",
|
|
144
|
+
"-readonly",
|
|
145
|
+
"-mountpoint",
|
|
146
|
+
mountPath,
|
|
147
|
+
], { cwd: dirname(artifactPath) });
|
|
148
|
+
if (mounted.code !== 0)
|
|
149
|
+
return {
|
|
150
|
+
signed: false,
|
|
151
|
+
status: "unsigned",
|
|
152
|
+
detail: (mounted.stderr || mounted.stdout).trim() ||
|
|
153
|
+
"The DMG could not be mounted for signature verification.",
|
|
154
|
+
};
|
|
155
|
+
const entries = await readdir(mountPath, { withFileTypes: true });
|
|
156
|
+
const app = entries.find((entry) => entry.isDirectory() && entry.name.toLowerCase().endsWith(".app"));
|
|
157
|
+
appPath = app ? join(mountPath, app.name) : undefined;
|
|
158
|
+
}
|
|
159
|
+
if (!appPath || !commandExists("codesign"))
|
|
160
|
+
return {
|
|
161
|
+
signed: false,
|
|
162
|
+
status: "unavailable",
|
|
163
|
+
detail: "No verifiable macOS .app bundle was found in the Desktop artifact.",
|
|
164
|
+
};
|
|
165
|
+
const result = await captureProcess("codesign", ["--verify", "--deep", "--strict", appPath], { cwd: dirname(appPath) });
|
|
166
|
+
return result.code === 0
|
|
167
|
+
? {
|
|
168
|
+
signed: true,
|
|
169
|
+
status: "signed",
|
|
170
|
+
detail: "codesign verification passed",
|
|
171
|
+
}
|
|
172
|
+
: {
|
|
173
|
+
signed: false,
|
|
174
|
+
status: "unsigned",
|
|
175
|
+
detail: (result.stderr || result.stdout).trim() ||
|
|
176
|
+
"codesign verification failed",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
if (mountPath) {
|
|
181
|
+
await captureProcess("hdiutil", ["detach", mountPath, "-force"], {
|
|
182
|
+
cwd: dirname(artifactPath),
|
|
183
|
+
}).catch(() => undefined);
|
|
184
|
+
await rm(mountPath, { recursive: true, force: true }).catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export async function verifyDesktopArtifactSignature(artifactPath) {
|
|
189
|
+
const absoluteArtifact = resolve(artifactPath);
|
|
190
|
+
if (process.platform === "win32" &&
|
|
191
|
+
extname(absoluteArtifact).toLowerCase() === ".exe")
|
|
192
|
+
return verifyWindowsArtifact(absoluteArtifact);
|
|
193
|
+
if (process.platform === "darwin")
|
|
194
|
+
return verifyMacArtifact(absoluteArtifact);
|
|
195
|
+
return {
|
|
196
|
+
signed: false,
|
|
197
|
+
status: "not-required",
|
|
198
|
+
detail: "No platform code-signature verification is required for this target.",
|
|
199
|
+
};
|
|
200
|
+
}
|
package/dist/dev-qr.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-qr.d.ts","sourceRoot":"","sources":["../src/dev-qr.ts"],"names":[],"mappings":"AAMA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMpE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D"}
|
package/dist/dev-qr.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const URL_PATTERN = /https?:\/\/[^\s<>"'`]+/g;
|
|
2
|
+
function cleanUrl(value) {
|
|
3
|
+
return value.replace(/[),.;!?]+$/u, "");
|
|
4
|
+
}
|
|
5
|
+
export function extractDevServerUrl(line) {
|
|
6
|
+
const urls = (line.match(URL_PATTERN) ?? []).map(cleanUrl);
|
|
7
|
+
return (urls.find((url) => url.includes("fullscreen=true")) ??
|
|
8
|
+
urls.find((url) => url.includes(".lynx.bundle")));
|
|
9
|
+
}
|
|
10
|
+
export function shouldPrintDevServerQr(line) {
|
|
11
|
+
return /(?:default:|fullscreen=true|\bready\b)/iu.test(line);
|
|
12
|
+
}
|
package/dist/guidance.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guidance.d.ts","sourceRoot":"","sources":["../src/guidance.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"guidance.d.ts","sourceRoot":"","sources":["../src/guidance.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAiXD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAiG5D"}
|
package/dist/guidance.js
CHANGED
|
@@ -51,10 +51,10 @@ const guidance = {
|
|
|
51
51
|
commands: [
|
|
52
52
|
"lynxship dev",
|
|
53
53
|
"lynxship android host init --application-id com.example.myapp",
|
|
54
|
+
"lynxship build --platform android --application-id com.example.myapp --profile production",
|
|
54
55
|
"lynxship doctor --platform android",
|
|
55
|
-
"lynxship build --platform android --profile production",
|
|
56
56
|
],
|
|
57
|
-
note: "
|
|
57
|
+
note: "Interactive build creates a missing android/ host after asking for the application ID. CI must pass --application-id. Existing android/ directories are never overwritten.",
|
|
58
58
|
},
|
|
59
59
|
ANDROID_HOST_EXISTS: {
|
|
60
60
|
commands: ["lynxship doctor --platform android"],
|
|
@@ -90,6 +90,99 @@ const guidance = {
|
|
|
90
90
|
commands: ["lynxship doctor --platform android", "adb devices"],
|
|
91
91
|
note: "Install Android SDK Platform-Tools and connect or start a device.",
|
|
92
92
|
},
|
|
93
|
+
ANDROID_TOOLCHAIN_REQUIRED: {
|
|
94
|
+
commands: [
|
|
95
|
+
"lynxship doctor --platform android",
|
|
96
|
+
"lynxship doctor --platform android --fix",
|
|
97
|
+
"lynxship build --platform android --profile production",
|
|
98
|
+
],
|
|
99
|
+
note: "The doctor detects the project AGP/Gradle contract and the required JDK, Android SDK packages and Build Tools. --fix installs only missing SDK packages after confirmation.",
|
|
100
|
+
},
|
|
101
|
+
WEB_CONFIGURATION_REQUIRED: {
|
|
102
|
+
commands: [
|
|
103
|
+
"lynxship doctor --platform web",
|
|
104
|
+
"lynxship build --platform web --profile production",
|
|
105
|
+
],
|
|
106
|
+
note: "Configure environments.web in lynx.config.* or provide the project's build:web script.",
|
|
107
|
+
},
|
|
108
|
+
WEB_BUNDLE_MISSING: {
|
|
109
|
+
commands: [
|
|
110
|
+
"lynxship doctor --platform web",
|
|
111
|
+
"lynxship build --platform web --profile production",
|
|
112
|
+
],
|
|
113
|
+
note: "The official Web output is dist/*.web.bundle; check the Rspeedy Web environment and artifact path.",
|
|
114
|
+
},
|
|
115
|
+
HARMONY_HOST_REQUIRED: {
|
|
116
|
+
commands: [
|
|
117
|
+
"lynxship doctor --platform harmony",
|
|
118
|
+
"lynxship build --platform harmony --profile production",
|
|
119
|
+
],
|
|
120
|
+
note: "Use the official Lynx Harmony host; LynxShip does not invent a fake HAP host.",
|
|
121
|
+
},
|
|
122
|
+
HARMONY_TOOLCHAIN_REQUIRED: {
|
|
123
|
+
commands: [
|
|
124
|
+
"lynxship doctor --platform harmony",
|
|
125
|
+
"lynxship build --platform harmony --profile production",
|
|
126
|
+
],
|
|
127
|
+
note: "Install the DevEco/OpenHarmony SDK, expose ohpm and use the host's pinned hvigorw wrapper.",
|
|
128
|
+
},
|
|
129
|
+
HARMONY_SIGN_TOOL_REQUIRED: {
|
|
130
|
+
commands: [
|
|
131
|
+
"lynxship doctor --platform harmony",
|
|
132
|
+
"lynxship build --platform harmony --profile production",
|
|
133
|
+
],
|
|
134
|
+
note: "Set LYNXSHIP_HAP_SIGN_TOOL to the official hap-sign-tool.jar or configure build.<profile>.harmony.signTool.",
|
|
135
|
+
},
|
|
136
|
+
HARMONY_HDC_REQUIRED: {
|
|
137
|
+
commands: [
|
|
138
|
+
"lynxship doctor --platform harmony",
|
|
139
|
+
"lynxship run --platform harmony --device <device-id>",
|
|
140
|
+
"lynxship logs --platform harmony --device <device-id>",
|
|
141
|
+
],
|
|
142
|
+
note: "Install the OpenHarmony SDK platform tools and make hdc available on PATH.",
|
|
143
|
+
},
|
|
144
|
+
DESKTOP_HOST_REQUIRED: {
|
|
145
|
+
commands: [
|
|
146
|
+
"lynxship doctor --platform desktop",
|
|
147
|
+
"lynxship build --platform desktop --profile production",
|
|
148
|
+
],
|
|
149
|
+
note: "Use the official Lynxtron host and its electron-builder configuration or pack script.",
|
|
150
|
+
},
|
|
151
|
+
DESKTOP_ARTIFACT_MISSING: {
|
|
152
|
+
commands: [
|
|
153
|
+
"lynxship doctor --platform desktop",
|
|
154
|
+
"lynxship build --platform desktop --profile production",
|
|
155
|
+
],
|
|
156
|
+
note: "Configure the Lynxtron pack script and set build.<profile>.desktop.artifact when output is not unique.",
|
|
157
|
+
},
|
|
158
|
+
DESKTOP_SIGNING_REQUIRED: {
|
|
159
|
+
commands: [
|
|
160
|
+
"lynxship doctor --platform desktop",
|
|
161
|
+
"lynxship build --platform desktop --profile production",
|
|
162
|
+
"lynxship build --platform desktop --profile production --no-upload --allow-unsigned",
|
|
163
|
+
],
|
|
164
|
+
note: "Production and uploaded Desktop artifacts must pass Windows Authenticode or Apple code-signature verification. Use --allow-unsigned only for local packaging tests together with --no-upload; it can never publish an unsigned artifact.",
|
|
165
|
+
},
|
|
166
|
+
TARGET_RUN_UNSUPPORTED: {
|
|
167
|
+
commands: [
|
|
168
|
+
"lynxship preview",
|
|
169
|
+
"lynxship run --platform android --artifact <apk>",
|
|
170
|
+
"lynxship run --platform harmony --artifact <hap>",
|
|
171
|
+
],
|
|
172
|
+
note: "Web and desktop artifacts use their target runtime or operating-system installer; only device targets are installed by this command.",
|
|
173
|
+
},
|
|
174
|
+
TARGET_LOGS_UNSUPPORTED: {
|
|
175
|
+
commands: [
|
|
176
|
+
"lynxship dev",
|
|
177
|
+
"lynxship logs --platform android",
|
|
178
|
+
"lynxship logs --platform harmony",
|
|
179
|
+
],
|
|
180
|
+
note: "Web and desktop logs come from their runtime tooling; LynxShip streams native device logs only.",
|
|
181
|
+
},
|
|
182
|
+
CLI_DOCTOR_FIX_PLATFORM: {
|
|
183
|
+
commands: ["lynxship doctor --platform android --fix"],
|
|
184
|
+
note: "The guided repair currently installs Android SDK packages only; JDK and Android Studio remain explicit developer installations.",
|
|
185
|
+
},
|
|
93
186
|
LYNX_BUNDLE_MISSING: {
|
|
94
187
|
commands: ["lynxship dev", "lynxship build --platform android"],
|
|
95
188
|
note: "Build the Lynx bundle with the project's configured package manager.",
|
|
@@ -98,10 +191,10 @@ const guidance = {
|
|
|
98
191
|
commands: [
|
|
99
192
|
"lynxship dev",
|
|
100
193
|
"lynxship ios host init --bundle-identifier com.example.myapp",
|
|
194
|
+
"lynxship build --platform ios --bundle-identifier com.example.myapp --profile production",
|
|
101
195
|
"lynxship doctor --platform ios",
|
|
102
|
-
"lynxship build --platform ios --profile production",
|
|
103
196
|
],
|
|
104
|
-
note: "
|
|
197
|
+
note: "Interactive build creates a missing ios/ host after asking for the bundle identifier. CI must pass --bundle-identifier. Existing ios/ directories are never overwritten.",
|
|
105
198
|
},
|
|
106
199
|
IOS_HOST_EXISTS: {
|
|
107
200
|
commands: ["lynxship doctor --platform ios"],
|
|
@@ -122,6 +215,14 @@ const guidance = {
|
|
|
122
215
|
IOS_XCRUN_REQUIRED: {
|
|
123
216
|
commands: ["xcode-select --install", "lynxship doctor --platform ios"],
|
|
124
217
|
},
|
|
218
|
+
IOS_TOOLCHAIN_REQUIRED: {
|
|
219
|
+
commands: [
|
|
220
|
+
"lynxship doctor --platform ios",
|
|
221
|
+
"lynxship ios host init --bundle-identifier com.example.myapp",
|
|
222
|
+
"lynxship build --platform ios --profile production",
|
|
223
|
+
],
|
|
224
|
+
note: "The iOS doctor checks macOS, Xcode, xcrun, CocoaPods, Xcode settings, Apple signing identities, provisioning and export options without printing certificate contents.",
|
|
225
|
+
},
|
|
125
226
|
IOS_COCOAPODS_REQUIRED: {
|
|
126
227
|
commands: ["brew install cocoapods", "lynxship build --platform ios"],
|
|
127
228
|
note: "CocoaPods is required when the iOS host contains a Podfile.",
|
|
@@ -254,6 +355,14 @@ const guidance = {
|
|
|
254
355
|
],
|
|
255
356
|
note: "Check the Apple certificate, provisioning profile, team and export options.",
|
|
256
357
|
},
|
|
358
|
+
CLI_DEVTOOL_COMMAND: {
|
|
359
|
+
commands: [
|
|
360
|
+
"lynxship devtool doctor --platform android",
|
|
361
|
+
"lynxship trace doctor --platform android",
|
|
362
|
+
"lynxship recorder doctor --platform android",
|
|
363
|
+
],
|
|
364
|
+
note: "Lynx Trace and Recorder are operated from Lynx DevTool Desktop after the native -dev runtime is integrated.",
|
|
365
|
+
},
|
|
257
366
|
};
|
|
258
367
|
export function guidanceForError(error) {
|
|
259
368
|
const code = error.code;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type BuildJob } from "@lynxship/contracts";
|
|
2
|
+
import type { BuildProfile } from "./config.js";
|
|
3
|
+
interface HarmonyBuildOptions {
|
|
4
|
+
root: string;
|
|
5
|
+
profile: BuildProfile;
|
|
6
|
+
uploadArtifacts?: boolean;
|
|
7
|
+
skipBundleBuild?: boolean;
|
|
8
|
+
quiet?: boolean;
|
|
9
|
+
onEvent?: (message: string) => void;
|
|
10
|
+
onProgress?: (value?: number, label?: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function hasHarmonyHost(root: string): boolean;
|
|
13
|
+
export declare function harmonyToolchain(root: string): {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
wrapper?: string;
|
|
16
|
+
ohpm: boolean;
|
|
17
|
+
hdc: boolean;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function runRealHarmonyBuild(job: BuildJob, options: HarmonyBuildOptions): Promise<BuildJob>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=harmony-build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harmony-build.d.ts","sourceRoot":"","sources":["../src/harmony-build.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAeD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAC9C,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAcA;AAmID,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAkGnB"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { copyFile, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { assert } from "@lynxship/contracts";
|
|
6
|
+
import { transitionBuild } from "@lynxship/build-orchestrator";
|
|
7
|
+
import { publishBuiltArtifact } from "./artifact-build.js";
|
|
8
|
+
import { buildLynxBundle } from "./bundle-build.js";
|
|
9
|
+
import { commandExists, runProcess } from "./process-runner.js";
|
|
10
|
+
function harmonyRoot(root) {
|
|
11
|
+
return join(root, "harmony");
|
|
12
|
+
}
|
|
13
|
+
function wrapper(root) {
|
|
14
|
+
const directory = harmonyRoot(root);
|
|
15
|
+
const candidates = process.platform === "win32"
|
|
16
|
+
? [join(directory, "hvigorw.bat"), join(directory, "hvigorw")]
|
|
17
|
+
: [join(directory, "hvigorw"), join(directory, "hvigorw.sh")];
|
|
18
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
19
|
+
}
|
|
20
|
+
export function hasHarmonyHost(root) {
|
|
21
|
+
return Boolean(wrapper(root) &&
|
|
22
|
+
existsSync(join(harmonyRoot(root), "hvigorfile.ts")) &&
|
|
23
|
+
existsSync(join(harmonyRoot(root), "build-profile.json5")) &&
|
|
24
|
+
existsSync(join(harmonyRoot(root), "oh-package.json5")));
|
|
25
|
+
}
|
|
26
|
+
export function harmonyToolchain(root) {
|
|
27
|
+
const projectWrapper = wrapper(root);
|
|
28
|
+
const ohpm = commandExists("ohpm");
|
|
29
|
+
const hdc = commandExists("hdc");
|
|
30
|
+
return {
|
|
31
|
+
ok: Boolean(projectWrapper && ohpm),
|
|
32
|
+
wrapper: projectWrapper,
|
|
33
|
+
ohpm,
|
|
34
|
+
hdc,
|
|
35
|
+
message: projectWrapper && ohpm
|
|
36
|
+
? "Hvigor and ohpm detected"
|
|
37
|
+
: "Harmony host requires the project hvigorw wrapper and ohpm from DevEco Studio",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function syncBundle(root, profile) {
|
|
41
|
+
const dist = join(root, "dist");
|
|
42
|
+
const entries = await readdir(dist, { withFileTypes: true }).catch(() => []);
|
|
43
|
+
const bundles = entries
|
|
44
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".lynx.bundle"))
|
|
45
|
+
.map((entry) => entry.name);
|
|
46
|
+
assert(bundles.length > 0, "LYNX_BUNDLE_MISSING", `No .lynx.bundle was found in ${dist}. Check the Rspeedy output configuration.`);
|
|
47
|
+
const target = resolve(root, profile.harmony?.bundleDir ?? "harmony/entry/src/main/resources/rawfile");
|
|
48
|
+
await mkdir(target, { recursive: true });
|
|
49
|
+
for (const bundle of bundles)
|
|
50
|
+
await copyFile(join(dist, bundle), join(target, bundle));
|
|
51
|
+
return bundles;
|
|
52
|
+
}
|
|
53
|
+
async function findHap(root, configured) {
|
|
54
|
+
if (configured) {
|
|
55
|
+
const file = resolve(root, configured);
|
|
56
|
+
assert(existsSync(file), "HARMONY_ARTIFACT_MISSING", `Configured HAP was not found: ${file}`);
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
const result = [];
|
|
60
|
+
async function visit(directory) {
|
|
61
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (entry.name === ".hvigor" || entry.name === "node_modules")
|
|
64
|
+
continue;
|
|
65
|
+
const file = join(directory, entry.name);
|
|
66
|
+
if (entry.isDirectory())
|
|
67
|
+
await visit(file);
|
|
68
|
+
else if (entry.isFile() && entry.name.endsWith(".hap"))
|
|
69
|
+
result.push(file);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await visit(harmonyRoot(root));
|
|
73
|
+
const signed = result.filter((file) => !file.toLowerCase().includes("unsigned"));
|
|
74
|
+
assert(signed.length === 1, "HARMONY_ARTIFACT_AMBIGUOUS", signed.length === 0
|
|
75
|
+
? "Hvigor produced no signed HAP. Configure Harmony signing in build-profile.json5 or set build.<profile>.harmony.artifact."
|
|
76
|
+
: "Hvigor produced multiple signed HAP files. Set build.<profile>.harmony.artifact explicitly.");
|
|
77
|
+
return signed[0];
|
|
78
|
+
}
|
|
79
|
+
function signToolPath(root, configured) {
|
|
80
|
+
const candidates = [
|
|
81
|
+
configured ? resolve(root, configured) : undefined,
|
|
82
|
+
process.env.LYNXSHIP_HAP_SIGN_TOOL,
|
|
83
|
+
process.env.DEVECO_HAP_SIGN_TOOL,
|
|
84
|
+
process.env.HOS_SDK_HOME
|
|
85
|
+
? join(process.env.HOS_SDK_HOME, "toolchains", "lib", "hap-sign-tool.jar")
|
|
86
|
+
: undefined,
|
|
87
|
+
process.env.DEVECO_SDK_HOME
|
|
88
|
+
? join(process.env.DEVECO_SDK_HOME, "toolchains", "lib", "hap-sign-tool.jar")
|
|
89
|
+
: undefined,
|
|
90
|
+
join(homedir(), ".ohos", "sdk", "default", "openharmony", "toolchains", "hap-sign-tool.jar"),
|
|
91
|
+
].filter((value) => Boolean(value));
|
|
92
|
+
return candidates.find((value) => existsSync(value));
|
|
93
|
+
}
|
|
94
|
+
async function verifySignedHap(root, artifact, configured) {
|
|
95
|
+
const tool = signToolPath(root, configured);
|
|
96
|
+
assert(tool, "HARMONY_SIGN_TOOL_REQUIRED", "The official hap-sign-tool.jar is required to verify a signed HAP. Set LYNXSHIP_HAP_SIGN_TOOL or build.<profile>.harmony.signTool.");
|
|
97
|
+
assert(commandExists("java"), "HARMONY_JAVA_REQUIRED", "Java is required to run hap-sign-tool.jar.");
|
|
98
|
+
const directory = await mkdtemp(join(tmpdir(), "lynxship-hap-verify-"));
|
|
99
|
+
try {
|
|
100
|
+
await runProcess("java", [
|
|
101
|
+
"-jar",
|
|
102
|
+
tool,
|
|
103
|
+
"verify-app",
|
|
104
|
+
"-inFile",
|
|
105
|
+
artifact,
|
|
106
|
+
"-outCertchain",
|
|
107
|
+
join(directory, "certchain.pem"),
|
|
108
|
+
"-outProfile",
|
|
109
|
+
join(directory, "profile.p7b"),
|
|
110
|
+
], { cwd: root, quiet: true });
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await rm(directory, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function runRealHarmonyBuild(job, options) {
|
|
117
|
+
const toolchain = harmonyToolchain(options.root);
|
|
118
|
+
assert(hasHarmonyHost(options.root), "HARMONY_HOST_REQUIRED", "No complete HarmonyOS host was found. Add harmony/hvigorw, hvigorfile.ts, build-profile.json5 and oh-package.json5 from an official Lynx Harmony host.");
|
|
119
|
+
assert(toolchain.ok, "HARMONY_TOOLCHAIN_REQUIRED", toolchain.message);
|
|
120
|
+
const projectWrapper = toolchain.wrapper;
|
|
121
|
+
const uploadArtifacts = options.uploadArtifacts ?? true;
|
|
122
|
+
const step = (message, value) => {
|
|
123
|
+
options.onEvent?.(message);
|
|
124
|
+
options.onProgress?.(value, message);
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
transitionBuild(job, "uploading_source", "HarmonyOS source prepared");
|
|
128
|
+
if (!options.skipBundleBuild) {
|
|
129
|
+
step("Building Lynx bundle with Rspeedy…", 10);
|
|
130
|
+
await buildLynxBundle(options.root, {
|
|
131
|
+
quiet: options.quiet,
|
|
132
|
+
onOutput: options.onEvent,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
step("Syncing bundle into the HarmonyOS HAP resources…", 20);
|
|
136
|
+
await syncBundle(options.root, options.profile);
|
|
137
|
+
transitionBuild(job, "queued", "HarmonyOS build queued locally");
|
|
138
|
+
transitionBuild(job, "provisioning", "HarmonyOS Hvigor toolchain selected");
|
|
139
|
+
transitionBuild(job, "installing_dependencies", "HarmonyOS dependencies selected");
|
|
140
|
+
step("Installing HarmonyOS dependencies with ohpm…", 30);
|
|
141
|
+
await runProcess("ohpm", ["install"], {
|
|
142
|
+
cwd: harmonyRoot(options.root),
|
|
143
|
+
quiet: options.quiet,
|
|
144
|
+
onOutput: options.onEvent,
|
|
145
|
+
});
|
|
146
|
+
transitionBuild(job, "building", "Hvigor HAP task started");
|
|
147
|
+
const task = options.profile.harmony?.task ?? "assembleHap";
|
|
148
|
+
step(`Running real Hvigor task ${task}…`, 45);
|
|
149
|
+
const harmony = options.profile.harmony;
|
|
150
|
+
const mode = harmony?.mode ?? "module";
|
|
151
|
+
const args = [
|
|
152
|
+
"--no-daemon",
|
|
153
|
+
"--mode",
|
|
154
|
+
mode,
|
|
155
|
+
"-p",
|
|
156
|
+
`product=${harmony?.product ?? "default"}`,
|
|
157
|
+
];
|
|
158
|
+
if (mode === "module")
|
|
159
|
+
args.push("-p", `module=${harmony?.module ?? "entry@default"}`);
|
|
160
|
+
args.push("-p", `buildMode=${harmony?.buildMode ?? "release"}`);
|
|
161
|
+
args.push(task);
|
|
162
|
+
await runProcess(projectWrapper, args, {
|
|
163
|
+
cwd: harmonyRoot(options.root),
|
|
164
|
+
quiet: options.quiet,
|
|
165
|
+
onOutput: options.onEvent,
|
|
166
|
+
});
|
|
167
|
+
const artifact = await findHap(options.root, options.profile.harmony?.artifact);
|
|
168
|
+
step("Verifying signed HarmonyOS HAP…", 75);
|
|
169
|
+
await verifySignedHap(options.root, artifact, options.profile.harmony?.signTool);
|
|
170
|
+
return publishBuiltArtifact({
|
|
171
|
+
root: options.root,
|
|
172
|
+
job,
|
|
173
|
+
platform: "harmony",
|
|
174
|
+
artifactPath: artifact,
|
|
175
|
+
extension: "hap",
|
|
176
|
+
contentType: "application/octet-stream",
|
|
177
|
+
uploadArtifacts,
|
|
178
|
+
verificationMessage: "Signed HAP verified with the official hap-sign-tool",
|
|
179
|
+
quiet: options.quiet,
|
|
180
|
+
onEvent: options.onEvent,
|
|
181
|
+
onProgress: options.onProgress,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (!["success", "failed", "canceled", "timed_out"].includes(job.state))
|
|
186
|
+
transitionBuild(job, "failed", error instanceof Error ? error.message : "HarmonyOS build failed");
|
|
187
|
+
job.logs.push({
|
|
188
|
+
level: "error",
|
|
189
|
+
message: error instanceof Error ? error.message : "HarmonyOS build failed",
|
|
190
|
+
at: new Date().toISOString(),
|
|
191
|
+
});
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|