@mirinjs/cli 0.1.0-alpha.2 → 0.1.0-alpha.20
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/package.json +8 -5
- package/src/artifacts.ts +165 -26
- package/src/build.ts +234 -43
- package/src/bundle/linux/app.ts +275 -0
- package/src/bundle/linux/index.ts +1 -0
- package/src/{bundle.ts → bundle/macos/app.ts} +65 -23
- package/src/bundle/macos/index.ts +1 -0
- package/src/bundle/shared/cef-locales.ts +91 -0
- package/src/{bundle-win.ts → bundle/windows/app.ts} +10 -6
- package/src/bundle/windows/index.ts +1 -0
- package/src/check.ts +271 -0
- package/src/dev.ts +213 -35
- package/src/dmg.ts +22 -10
- package/src/doctor.ts +227 -0
- package/src/extras.ts +52 -13
- package/src/icons/macos/appearance.ts +96 -0
- package/src/icons/macos/index.ts +1 -0
- package/src/icons/windows/icon.ts +255 -0
- package/src/icons/windows/index.ts +1 -0
- package/src/index.ts +109 -7
- package/src/installer-inno.ts +141 -0
- package/src/installer-win.ts +17 -11
- package/src/package/linux/index.ts +2 -0
- package/src/package/linux/lib/defaults.ts +53 -0
- package/src/package/linux/lib/tools.ts +45 -0
- package/src/package/linux/lib/validation.ts +71 -0
- package/src/package/linux/package.ts +293 -0
- package/src/package/linux/types.ts +30 -0
- package/src/release/installers.ts +148 -0
- package/src/release/previous.ts +105 -0
- package/src/release.ts +166 -116
- package/src/shared/driver.ts +240 -0
- package/src/shared/inspector.ts +64 -0
- package/src/shared/report.ts +69 -0
- package/src/shared/scenario.ts +58 -0
- package/src/shared/session.ts +153 -0
- package/src/skill.ts +51 -0
- package/src/icon-win.ts +0 -81
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirinjs/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
4
|
"description": "CLI for mirin apps: dev, build, init.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,12 +27,15 @@
|
|
|
27
27
|
"bun": ">=1.2.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"create-mirinjs": "0.1.0-alpha.
|
|
31
|
-
"mirinjs": "0.1.0-alpha.
|
|
30
|
+
"create-mirinjs": "0.1.0-alpha.20",
|
|
31
|
+
"mirinjs": "0.1.0-alpha.20"
|
|
32
32
|
},
|
|
33
33
|
"optionalDependencies": {
|
|
34
|
-
"@mirinjs/darwin-arm64": "0.1.0-alpha.
|
|
35
|
-
"@mirinjs/win32-x64": "0.1.0-alpha.
|
|
34
|
+
"@mirinjs/darwin-arm64": "0.1.0-alpha.20",
|
|
35
|
+
"@mirinjs/win32-x64": "0.1.0-alpha.20",
|
|
36
|
+
"@mirinjs/win32-arm64": "0.1.0-alpha.20",
|
|
37
|
+
"@mirinjs/linux-arm64": "0.1.0-alpha.20",
|
|
38
|
+
"@mirinjs/linux-x64": "0.1.0-alpha.20"
|
|
36
39
|
},
|
|
37
40
|
"publishConfig": {
|
|
38
41
|
"access": "public"
|
package/src/artifacts.ts
CHANGED
|
@@ -9,13 +9,23 @@
|
|
|
9
9
|
* entry from the `mirin` package, and a CEF framework downloaded once from
|
|
10
10
|
* the matching GitHub Release into `~/.mirinjs/cef/<version>`. No Rust needed.
|
|
11
11
|
*
|
|
12
|
-
* Alpha supports macOS arm64
|
|
12
|
+
* Alpha supports macOS arm64, Windows x64/arm64, and Linux x64/arm64.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
|
|
15
|
+
import {
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
mkdtempSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
readlinkSync,
|
|
22
|
+
renameSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
} from "node:fs";
|
|
17
25
|
import { homedir, tmpdir } from "node:os";
|
|
18
|
-
import { dirname, join, resolve } from "node:path";
|
|
26
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
27
|
+
import { $ } from "bun";
|
|
28
|
+
import { createReporter, type Reporter } from "./shared/report.ts";
|
|
19
29
|
|
|
20
30
|
const REPO_SLUG = "Netko-Labs/mirin";
|
|
21
31
|
|
|
@@ -26,6 +36,8 @@ const IN_REPO = existsSync(join(REPO_ROOT, "crates", "mirin-core"));
|
|
|
26
36
|
export interface Artifacts {
|
|
27
37
|
/** libmirin_core.dylib — dlopened by the host. */
|
|
28
38
|
coreDylib: string;
|
|
39
|
+
/** Standalone updater codec used by `mirin release`. */
|
|
40
|
+
codecBin: string;
|
|
29
41
|
/** mirin-helper — the CEF subprocess binary. */
|
|
30
42
|
helperBin: string;
|
|
31
43
|
/** host entry (TS/JS) compiled with `bun build --compile`. */
|
|
@@ -38,18 +50,33 @@ export function isInRepo(): boolean {
|
|
|
38
50
|
return IN_REPO;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
/** The in-repo CEF distribution. `present` checks the framework marker, not just
|
|
54
|
+
* the directory: an interrupted `fetch-cef.ts` leaves an empty one behind. */
|
|
55
|
+
export function vendoredCef(): { path: string; present: boolean } {
|
|
56
|
+
const path = join(REPO_ROOT, "vendor", "cef");
|
|
57
|
+
return { path, present: existsSync(join(path, cefMarker())) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function resolveArtifacts(opts: {
|
|
61
|
+
release: boolean;
|
|
62
|
+
/** Where build chatter goes. Defaults to human mode, which prints it. */
|
|
63
|
+
reporter?: Reporter;
|
|
64
|
+
}): Promise<Artifacts> {
|
|
42
65
|
assertSupportedPlatform();
|
|
43
|
-
const
|
|
66
|
+
const reporter = opts.reporter ?? createReporter(false);
|
|
67
|
+
const cefPath = await ensureCef(reporter);
|
|
44
68
|
|
|
45
69
|
if (IN_REPO) {
|
|
46
70
|
const profile = opts.release ? "release" : "debug";
|
|
47
|
-
|
|
71
|
+
reporter.info(`[mirin] building native core + helpers (${profile})…`);
|
|
48
72
|
const flags = opts.release ? ["--release"] : [];
|
|
49
|
-
await
|
|
73
|
+
await reporter.build(
|
|
74
|
+
$`cargo build -p mirin-codec -p mirin-core -p mirin-helper ${flags}`.cwd(REPO_ROOT),
|
|
75
|
+
);
|
|
50
76
|
const target = join(REPO_ROOT, "target", profile);
|
|
51
77
|
return {
|
|
52
78
|
coreDylib: join(target, coreFileName()),
|
|
79
|
+
codecBin: join(target, codecFileName()),
|
|
53
80
|
helperBin: join(target, helperFileName()),
|
|
54
81
|
hostEntry: join(REPO_ROOT, "packages", "mirin", "src", "host.ts"),
|
|
55
82
|
cefPath,
|
|
@@ -59,6 +86,7 @@ export async function resolveArtifacts(opts: { release: boolean }): Promise<Arti
|
|
|
59
86
|
const nativeDir = resolveNativeDir();
|
|
60
87
|
return {
|
|
61
88
|
coreDylib: join(nativeDir, coreFileName()),
|
|
89
|
+
codecBin: join(nativeDir, codecFileName()),
|
|
62
90
|
helperBin: join(nativeDir, helperFileName()),
|
|
63
91
|
hostEntry: resolvePackageFile("mirinjs/host"),
|
|
64
92
|
cefPath,
|
|
@@ -67,7 +95,9 @@ export async function resolveArtifacts(opts: { release: boolean }): Promise<Arti
|
|
|
67
95
|
|
|
68
96
|
/** The native core library file name for the host platform (MSVC has no `lib` prefix). */
|
|
69
97
|
function coreFileName(): string {
|
|
70
|
-
|
|
98
|
+
if (process.platform === "win32") return "mirin_core.dll";
|
|
99
|
+
if (process.platform === "linux") return "libmirin_core.so";
|
|
100
|
+
return "libmirin_core.dylib";
|
|
71
101
|
}
|
|
72
102
|
|
|
73
103
|
/** The CEF subprocess binary name for the host platform. */
|
|
@@ -75,13 +105,19 @@ function helperFileName(): string {
|
|
|
75
105
|
return process.platform === "win32" ? "mirin-helper.exe" : "mirin-helper";
|
|
76
106
|
}
|
|
77
107
|
|
|
108
|
+
function codecFileName(): string {
|
|
109
|
+
return process.platform === "win32" ? "mirin-codec.exe" : "mirin-codec";
|
|
110
|
+
}
|
|
111
|
+
|
|
78
112
|
function assertSupportedPlatform(): void {
|
|
79
113
|
const supported =
|
|
80
114
|
(process.platform === "darwin" && process.arch === "arm64") ||
|
|
81
|
-
(process.platform === "win32" && process.arch === "x64")
|
|
115
|
+
(process.platform === "win32" && (process.arch === "x64" || process.arch === "arm64")) ||
|
|
116
|
+
(process.platform === "linux" && (process.arch === "x64" || process.arch === "arm64"));
|
|
82
117
|
if (!supported) {
|
|
83
118
|
throw new Error(
|
|
84
|
-
|
|
119
|
+
"mirin alpha supports macOS arm64, Windows x64/arm64, and Linux x64/arm64 " +
|
|
120
|
+
`(got ${process.platform}/${process.arch}).`,
|
|
85
121
|
);
|
|
86
122
|
}
|
|
87
123
|
}
|
|
@@ -97,7 +133,7 @@ function resolveNativeDir(): string {
|
|
|
97
133
|
} catch {
|
|
98
134
|
throw new Error(
|
|
99
135
|
`mirin: prebuilt native package "${pkg}" is not installed. Run \`bun install\` ` +
|
|
100
|
-
|
|
136
|
+
"(it is an optional dependency of @mirinjs/cli for your platform).",
|
|
101
137
|
);
|
|
102
138
|
}
|
|
103
139
|
}
|
|
@@ -113,41 +149,144 @@ function cliVersion(): string {
|
|
|
113
149
|
}
|
|
114
150
|
|
|
115
151
|
/** A file/dir that, if present in a CEF dir, means the distribution is unpacked.
|
|
116
|
-
* macOS ships the framework bundle; Windows ships a flat dir with libcef.dll
|
|
152
|
+
* macOS ships the framework bundle; Windows ships a flat dir with libcef.dll;
|
|
153
|
+
* Linux ships a flat dir with libcef.so. */
|
|
117
154
|
function cefMarker(): string {
|
|
118
|
-
|
|
155
|
+
if (process.platform === "win32") return "libcef.dll";
|
|
156
|
+
if (process.platform === "linux") return "libcef.so";
|
|
157
|
+
return "Chromium Embedded Framework.framework";
|
|
119
158
|
}
|
|
120
159
|
|
|
121
|
-
async function ensureCef(): Promise<string> {
|
|
160
|
+
async function ensureCef(reporter: Reporter): Promise<string> {
|
|
122
161
|
if (IN_REPO) {
|
|
123
|
-
const vendor =
|
|
124
|
-
if (
|
|
162
|
+
const vendor = vendoredCef();
|
|
163
|
+
if (vendor.present) return vendor.path;
|
|
125
164
|
console.error(
|
|
126
165
|
"[mirin] vendor/cef missing — run `bun scripts/fetch-cef.ts` in the monorepo first.",
|
|
127
166
|
);
|
|
128
|
-
return vendor;
|
|
167
|
+
return vendor.path;
|
|
129
168
|
}
|
|
130
169
|
|
|
131
170
|
const version = cliVersion();
|
|
132
|
-
const
|
|
171
|
+
const cacheRoot = join(homedir(), ".mirinjs", "cef");
|
|
172
|
+
const cacheDir = join(cacheRoot, `${version}-${platformTag()}`);
|
|
133
173
|
if (existsSync(join(cacheDir, cefMarker()))) return cacheDir;
|
|
134
174
|
|
|
135
|
-
mkdirSync(
|
|
175
|
+
mkdirSync(cacheRoot, { recursive: true });
|
|
136
176
|
const asset = `cef-${platformTag()}.tar.gz`;
|
|
137
177
|
const url = `https://github.com/${REPO_SLUG}/releases/download/v${version}/${asset}`;
|
|
138
|
-
|
|
178
|
+
reporter.info(`[mirin] downloading CEF for ${platformTag()} (one-time, ~hundreds of MB)…`);
|
|
139
179
|
|
|
140
180
|
// Download with curl, not `Bun.write(file, await fetch(url))`: the latter
|
|
141
181
|
// pins a core at 100% CPU and never completes on large gzip responses
|
|
142
182
|
// (the streaming write path). curl is fast and ubiquitous.
|
|
143
|
-
const
|
|
183
|
+
const downloadDir = mkdtempSync(join(tmpdir(), "mirin-cef-download-"));
|
|
184
|
+
const stagingDir = mkdtempSync(join(cacheRoot, ".staging-"));
|
|
185
|
+
const archive = join(downloadDir, asset);
|
|
186
|
+
const checksum = `${archive}.sha256`;
|
|
187
|
+
const checksumUrl = `${url}.sha256`;
|
|
188
|
+
const httpsOnly = "=https";
|
|
144
189
|
try {
|
|
145
|
-
await $`curl -
|
|
146
|
-
await $`
|
|
190
|
+
await $`curl --fail --show-error --location --retry 3 --proto ${httpsOnly} --proto-redir ${httpsOnly} -o ${checksum} ${checksumUrl}`;
|
|
191
|
+
await $`curl --fail --show-error --location --retry 3 --proto ${httpsOnly} --proto-redir ${httpsOnly} -o ${archive} ${url}`;
|
|
192
|
+
await verifyFileSha256(archive, readFileSync(checksum, "utf8"));
|
|
193
|
+
const listing = await $`tar -tzf ${archive}`.quiet().text();
|
|
194
|
+
validateArchiveEntries(listing);
|
|
195
|
+
await $`tar -xzf ${archive} -C ${stagingDir}`;
|
|
196
|
+
validateExtractedSymlinks(stagingDir);
|
|
197
|
+
if (!existsSync(join(stagingDir, cefMarker()))) {
|
|
198
|
+
throw new Error(`CEF archive is missing ${cefMarker()}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Another process may have populated the cache while this one downloaded.
|
|
202
|
+
if (existsSync(join(cacheDir, cefMarker()))) return cacheDir;
|
|
203
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
204
|
+
renameSync(stagingDir, cacheDir);
|
|
147
205
|
} catch (err) {
|
|
148
|
-
rmSync(archive, { force: true });
|
|
149
206
|
throw new Error(`mirin: failed to download CEF from ${url} (${err}).`);
|
|
207
|
+
} finally {
|
|
208
|
+
rmSync(downloadDir, { recursive: true, force: true });
|
|
209
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
150
210
|
}
|
|
151
|
-
rmSync(archive, { force: true });
|
|
152
211
|
return cacheDir;
|
|
153
212
|
}
|
|
213
|
+
|
|
214
|
+
/** Reject absolute paths and parent traversal before a downloaded tar is extracted. */
|
|
215
|
+
export function validateArchiveEntries(listing: string): void {
|
|
216
|
+
for (const rawEntry of listing.split(/\r?\n/)) {
|
|
217
|
+
if (rawEntry.length === 0) continue;
|
|
218
|
+
const entry = archivePath(rawEntry);
|
|
219
|
+
if (entry == null) throw new Error(`unsafe CEF archive entry: ${rawEntry}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Verify a downloaded release asset before parsing or extracting it. */
|
|
224
|
+
export async function verifyFileSha256(path: string, checksum: string): Promise<void> {
|
|
225
|
+
const expected = checksum.trim().toLowerCase();
|
|
226
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) {
|
|
227
|
+
throw new Error("invalid CEF SHA-256 checksum");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
231
|
+
for await (const chunk of Bun.file(path).stream()) {
|
|
232
|
+
hasher.update(chunk);
|
|
233
|
+
}
|
|
234
|
+
const actual = hasher.digest("hex");
|
|
235
|
+
if (actual !== expected) {
|
|
236
|
+
throw new Error(`CEF SHA-256 mismatch (expected ${expected}, got ${actual})`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Reject extracted symlinks whose lexical target escapes the staging directory. */
|
|
241
|
+
export function validateExtractedSymlinks(root: string): void {
|
|
242
|
+
const absoluteRoot = resolve(root);
|
|
243
|
+
const pending = [absoluteRoot];
|
|
244
|
+
|
|
245
|
+
while (pending.length > 0) {
|
|
246
|
+
const directory = pending.pop();
|
|
247
|
+
if (!directory) continue;
|
|
248
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
249
|
+
const path = join(directory, entry.name);
|
|
250
|
+
if (entry.isSymbolicLink()) {
|
|
251
|
+
const target = readlinkSync(path);
|
|
252
|
+
const resolvedTarget = resolve(dirname(path), target);
|
|
253
|
+
const fromRoot = relative(absoluteRoot, resolvedTarget);
|
|
254
|
+
if (
|
|
255
|
+
isAbsolute(target) ||
|
|
256
|
+
fromRoot === ".." ||
|
|
257
|
+
fromRoot.startsWith(`..${sep}`) ||
|
|
258
|
+
isAbsolute(fromRoot)
|
|
259
|
+
) {
|
|
260
|
+
throw new Error(`CEF archive symlink escapes its cache: ${path} -> ${target}`);
|
|
261
|
+
}
|
|
262
|
+
} else if (entry.isDirectory()) {
|
|
263
|
+
pending.push(path);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function archivePath(raw: string): string | null {
|
|
270
|
+
let path = raw.replaceAll("\\", "/");
|
|
271
|
+
while (path.startsWith("./")) path = path.slice(2);
|
|
272
|
+
if (path.length === 0 || path === ".") return "";
|
|
273
|
+
if (path.startsWith("/") || /^[A-Za-z]:/.test(path) || hasControlCharacter(path)) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const parts: string[] = [];
|
|
278
|
+
for (const part of path.split("/")) {
|
|
279
|
+
if (part.length === 0 || part === ".") continue;
|
|
280
|
+
if (part === "..") return null;
|
|
281
|
+
parts.push(part);
|
|
282
|
+
}
|
|
283
|
+
return parts.length > 0 ? parts.join("/") : "";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function hasControlCharacter(value: string): boolean {
|
|
287
|
+
for (let index = 0; index < value.length; index++) {
|
|
288
|
+
const code = value.charCodeAt(index);
|
|
289
|
+
if (code <= 0x1f || code === 0x7f) return true;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
package/src/build.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `mirin build` — package a standalone, signed .app (docs/macos-mvp.md).
|
|
3
3
|
*
|
|
4
|
-
* 1. vite build
|
|
5
|
-
* 2.
|
|
6
|
-
* 3.
|
|
7
|
-
* 4. bundle Worker (minified) → Resources/worker.js
|
|
8
|
-
* 5. assemble + sign the .app → dist/ui, manifest, dylib, helpers, CEF
|
|
4
|
+
* 1. vite build + native artifact resolution run in parallel.
|
|
5
|
+
* 2. compile host + bundle Workers run in parallel.
|
|
6
|
+
* 3. assemble + sign the app from the completed artifacts.
|
|
9
7
|
*
|
|
10
8
|
* The result runs with no env and no dev server: the host resolves everything
|
|
11
9
|
* from inside the bundle, and webviews load their `app://` URLs served from
|
|
@@ -15,16 +13,102 @@
|
|
|
15
13
|
* ID to produce a distributable, notarizable app.
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
|
-
import {
|
|
19
|
-
import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
17
|
import { join } from "node:path";
|
|
21
|
-
import {
|
|
22
|
-
import { buildWindowsBundle } from "./bundle-win.ts";
|
|
18
|
+
import { $ } from "bun";
|
|
23
19
|
import { resolveArtifacts } from "./artifacts.ts";
|
|
20
|
+
import { buildLinuxBundle } from "./bundle/linux/index.ts";
|
|
21
|
+
import { buildAppBundle } from "./bundle/macos/index.ts";
|
|
22
|
+
import { normalizeCefLocales } from "./bundle/shared/cef-locales.ts";
|
|
23
|
+
import { buildWindowsBundle } from "./bundle/windows/index.ts";
|
|
24
|
+
import { compileWorkers, normalizeSidecars } from "./extras.ts";
|
|
25
|
+
import { makeWindowsIcon } from "./icons/windows/index.ts";
|
|
26
|
+
import { buildLinuxPackages, resolveLinuxFormats } from "./package/linux/index.ts";
|
|
24
27
|
import { sweepBuildTemps } from "./temps.ts";
|
|
25
|
-
import { normalizeSidecars, compileWorkers } from "./extras.ts";
|
|
26
28
|
|
|
27
29
|
const IS_WINDOWS = process.platform === "win32";
|
|
30
|
+
const IS_LINUX = process.platform === "linux";
|
|
31
|
+
|
|
32
|
+
/** A PE FILEVERSION (`x.y.z.w`) from a semver string (pre-release suffix dropped). */
|
|
33
|
+
function winFileVersion(v: string): string {
|
|
34
|
+
const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
35
|
+
return m ? `${m[1]}.${m[2]}.${m[3]}.0` : "0.0.0.0";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Flip a PE exe from the CONSOLE subsystem (3) to GUI (2) so Windows allocates
|
|
40
|
+
* **no console window** when it's launched from a shortcut. Bun's
|
|
41
|
+
* `--windows-hide-console` only hides the console at runtime (a brief flash);
|
|
42
|
+
* GUI subsystem prevents it entirely — the equivalent of Electrobun's
|
|
43
|
+
* `exe.subsystem = .Windows`. The optional-header Subsystem field is at
|
|
44
|
+
* `e_lfanew + 24 (PE sig + COFF) + 68`.
|
|
45
|
+
*/
|
|
46
|
+
function patchPeToGuiSubsystem(exePath: string): void {
|
|
47
|
+
const buf = readFileSync(exePath);
|
|
48
|
+
const peOff = buf.readUInt32LE(0x3c);
|
|
49
|
+
if (buf.readUInt32LE(peOff) !== 0x0000_4550) return; // not "PE\0\0"
|
|
50
|
+
const subsystemOff = peOff + 92;
|
|
51
|
+
if (buf.readUInt16LE(subsystemOff) === 3 /* CONSOLE */) {
|
|
52
|
+
buf.writeUInt16LE(2 /* GUI */, subsystemOff);
|
|
53
|
+
writeFileSync(exePath, buf);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Stamp a Bun-compiled Windows exe with the app icon + version metadata via
|
|
59
|
+
* rcedit. Bun's `--windows-icon` is a silent no-op on Windows hosts, so without
|
|
60
|
+
* this the exe keeps Bun's icon — which the Start Menu / Desktop shortcut, the
|
|
61
|
+
* taskbar, and Explorer all show. Uses a single largest-size icon: rcedit's
|
|
62
|
+
* multi-size replacement silently fails on Bun's appended-payload exes, while a
|
|
63
|
+
* single 256 entry takes and Windows downscales it cleanly for smaller views.
|
|
64
|
+
* Best-effort — warns (doesn't fail the build) if rcedit isn't installed.
|
|
65
|
+
*/
|
|
66
|
+
async function brandWindowsExe(
|
|
67
|
+
exe: string,
|
|
68
|
+
opts: {
|
|
69
|
+
work: string;
|
|
70
|
+
appName: string;
|
|
71
|
+
version: string;
|
|
72
|
+
/** Absolute path to the icon source. */
|
|
73
|
+
icon?: string;
|
|
74
|
+
publisher: string;
|
|
75
|
+
},
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
if (!Bun.which("rcedit")) {
|
|
78
|
+
console.warn(
|
|
79
|
+
"[mirin build] rcedit not found — the Windows exe keeps Bun's default icon. " +
|
|
80
|
+
"Install it: `scoop install rcedit` (or `choco install rcedit`).",
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const args: string[] = [];
|
|
85
|
+
if (opts.icon) {
|
|
86
|
+
const ico = makeWindowsIcon(opts.icon, join(opts.work, "exe-icon.ico"), {
|
|
87
|
+
onlyLargest: true,
|
|
88
|
+
});
|
|
89
|
+
if (ico) args.push("--set-icon", ico);
|
|
90
|
+
}
|
|
91
|
+
const fv = winFileVersion(opts.version);
|
|
92
|
+
args.push(
|
|
93
|
+
"--set-version-string",
|
|
94
|
+
"ProductName",
|
|
95
|
+
opts.appName,
|
|
96
|
+
"--set-version-string",
|
|
97
|
+
"FileDescription",
|
|
98
|
+
opts.appName,
|
|
99
|
+
"--set-version-string",
|
|
100
|
+
"CompanyName",
|
|
101
|
+
opts.publisher,
|
|
102
|
+
"--set-file-version",
|
|
103
|
+
fv,
|
|
104
|
+
"--set-product-version",
|
|
105
|
+
fv,
|
|
106
|
+
);
|
|
107
|
+
const res = await $`rcedit ${exe} ${args}`.nothrow();
|
|
108
|
+
if (res.exitCode !== 0) {
|
|
109
|
+
console.warn(`[mirin build] rcedit failed (exit ${res.exitCode}) — exe branding skipped.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
28
112
|
|
|
29
113
|
export interface BuildResult {
|
|
30
114
|
/** Path to the assembled .app. */
|
|
@@ -37,20 +121,37 @@ export interface BuildResult {
|
|
|
37
121
|
channel: string;
|
|
38
122
|
/** Update baseUrl, if `release` is configured. */
|
|
39
123
|
baseUrl?: string;
|
|
124
|
+
/** Markdown release notes embedded in the updater manifest, if configured. */
|
|
125
|
+
releaseNotes?: string;
|
|
40
126
|
/** libmirin_core path (for the updater codec at release time). */
|
|
41
127
|
coreDylib: string;
|
|
128
|
+
/** Standalone updater codec executable used at release time. */
|
|
129
|
+
codecBin: string;
|
|
42
130
|
/** Project root (so `mirin release` can resolve relative asset paths). */
|
|
43
131
|
projectDir: string;
|
|
44
132
|
/** DMG config from mirin.config.ts (`true`/object/`false`); default `true`. */
|
|
45
133
|
dmg: boolean | import("mirinjs").DmgConfig;
|
|
46
134
|
/** NSIS installer config (Windows) — `true`/object/`false`; default `true`. */
|
|
47
135
|
nsis: boolean | import("mirinjs").NsisConfig;
|
|
136
|
+
/** Inno Setup installer config (Windows) — preferred over NSIS; default `true`. */
|
|
137
|
+
inno: boolean | import("mirinjs").InnoConfig;
|
|
138
|
+
/** Linux packaging config (`mirin.config.ts` `linux`) — `true`/object/`false`; default `true`. */
|
|
139
|
+
linux: boolean | import("mirinjs").LinuxConfig;
|
|
140
|
+
/** Publisher / company name (config.publisher ?? appName). */
|
|
141
|
+
publisher: string;
|
|
142
|
+
/** Absolute path to the app icon source (config.icon), if set — for Linux packaging. */
|
|
143
|
+
icon?: string;
|
|
48
144
|
/** Codesign identity used for the bundle, if any (MIRIN_SIGN_IDENTITY). */
|
|
49
145
|
signIdentity?: string;
|
|
50
146
|
}
|
|
51
147
|
|
|
52
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* The app version: `$MIRIN_APP_VERSION` (release-pipeline override) wins, otherwise
|
|
150
|
+
* the project's package.json `version` (the single source of truth).
|
|
151
|
+
*/
|
|
53
152
|
function appVersion(projectDir: string): string {
|
|
153
|
+
const override = process.env.MIRIN_APP_VERSION;
|
|
154
|
+
if (override) return override;
|
|
54
155
|
const pkgPath = join(projectDir, "package.json");
|
|
55
156
|
if (!existsSync(pkgPath)) return "0.0.0";
|
|
56
157
|
try {
|
|
@@ -60,7 +161,19 @@ function appVersion(projectDir: string): string {
|
|
|
60
161
|
}
|
|
61
162
|
}
|
|
62
163
|
|
|
63
|
-
export
|
|
164
|
+
export interface BuildOptions {
|
|
165
|
+
/** Override the app version (else `$MIRIN_APP_VERSION` / package.json). */
|
|
166
|
+
version?: string;
|
|
167
|
+
/** On Linux, also emit distributable packages (AppImage/deb/rpm) after assembling. */
|
|
168
|
+
packageLinux?: boolean;
|
|
169
|
+
/** Restrict Linux packaging to these formats (else config.linux.formats / all). */
|
|
170
|
+
linuxFormats?: import("mirinjs").LinuxPackageFormat[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function build(
|
|
174
|
+
projectDir = process.cwd(),
|
|
175
|
+
opts: BuildOptions = {},
|
|
176
|
+
): Promise<BuildResult> {
|
|
64
177
|
const outDir = join(projectDir, "build");
|
|
65
178
|
const work = join(projectDir, ".mirin");
|
|
66
179
|
mkdirSync(outDir, { recursive: true });
|
|
@@ -71,35 +184,61 @@ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
|
|
|
71
184
|
const appName: string = config.name ?? "Mirin App";
|
|
72
185
|
const bundleId: string = config.id ?? "dev.mirin.app";
|
|
73
186
|
const mainEntry = join(projectDir, config.main ?? "main/main.ts");
|
|
74
|
-
const version = appVersion(projectDir);
|
|
187
|
+
const version = opts.version ?? appVersion(projectDir);
|
|
75
188
|
const channel: string = config.release?.channel ?? "stable";
|
|
76
189
|
const baseUrl: string | undefined = config.release?.baseUrl;
|
|
190
|
+
const releaseNotes: string | undefined = config.release?.notes;
|
|
77
191
|
const dmg: boolean | import("mirinjs").DmgConfig = config.dmg ?? true;
|
|
78
192
|
const nsis: boolean | import("mirinjs").NsisConfig = config.nsis ?? true;
|
|
193
|
+
const inno: boolean | import("mirinjs").InnoConfig = config.inno ?? true;
|
|
194
|
+
const linux: boolean | import("mirinjs").LinuxConfig = config.linux ?? true;
|
|
195
|
+
const publisher: string = config.publisher ?? appName;
|
|
196
|
+
const cefLocales = normalizeCefLocales(config.cef?.locales);
|
|
197
|
+
const appIconPath: string | undefined = config.icon ? join(projectDir, config.icon) : undefined;
|
|
79
198
|
|
|
80
199
|
console.log(`[mirin build] ${appName} ${version}`);
|
|
81
200
|
|
|
82
|
-
//
|
|
83
|
-
console.log("[mirin build]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
201
|
+
// Production UI and native artifacts do not share outputs, so overlap them.
|
|
202
|
+
console.log("[mirin build] building UI + native artifacts…");
|
|
203
|
+
const [, artifacts] = await Promise.all([
|
|
204
|
+
$`bunx vite build`.cwd(projectDir),
|
|
205
|
+
resolveArtifacts({ release: true }),
|
|
206
|
+
]);
|
|
88
207
|
|
|
89
|
-
//
|
|
90
|
-
console.log("[mirin build] compiling host + bundling
|
|
208
|
+
// Host compilation, main Worker bundling, and extra Worker bundling are independent.
|
|
209
|
+
console.log("[mirin build] compiling host + bundling workers…");
|
|
91
210
|
const signIdentity = process.env.MIRIN_SIGN_IDENTITY;
|
|
92
211
|
const hostExe = join(work, IS_WINDOWS ? "host-release.exe" : "host-release");
|
|
93
212
|
const workerJs = join(work, "worker.release.js");
|
|
94
|
-
await $`bun build --compile --minify ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(projectDir);
|
|
95
|
-
await $`bun build ${mainEntry} --target=bun --minify --outfile ${workerJs}`.cwd(projectDir);
|
|
96
|
-
|
|
97
|
-
// Extra assets: resolve sidecar binaries + compile any extra worker entries.
|
|
98
213
|
const sidecars = normalizeSidecars(projectDir, config.sidecars);
|
|
99
|
-
const
|
|
214
|
+
const hostBuild = (async (): Promise<void> => {
|
|
215
|
+
// Bun's Windows arm64 build currently has no bun:ffi/TinyCC runtime. The
|
|
216
|
+
// win32-arm64 Mirin package therefore carries an x64 compatibility payload,
|
|
217
|
+
// which Windows 11 ARM executes through its built-in x64 emulation layer.
|
|
218
|
+
const target = IS_WINDOWS && process.arch === "arm64" ? ["--target=bun-windows-x64"] : [];
|
|
219
|
+
await $`bun build --compile --minify ${target} ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(
|
|
220
|
+
projectDir,
|
|
221
|
+
);
|
|
222
|
+
if (IS_WINDOWS) {
|
|
223
|
+
patchPeToGuiSubsystem(hostExe);
|
|
224
|
+
await brandWindowsExe(hostExe, {
|
|
225
|
+
work,
|
|
226
|
+
appName,
|
|
227
|
+
version,
|
|
228
|
+
icon: appIconPath,
|
|
229
|
+
publisher,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
})();
|
|
233
|
+
const workerBuild = $`bun build ${mainEntry} --target=bun --minify --outfile ${workerJs}`.cwd(
|
|
234
|
+
projectDir,
|
|
235
|
+
);
|
|
236
|
+
const extraWorkersBuild = compileWorkers(projectDir, config.workers, join(work, "workers"), true);
|
|
237
|
+
const [, , extraWorkers] = await Promise.all([hostBuild, workerBuild, extraWorkersBuild]);
|
|
100
238
|
|
|
101
239
|
// 5. assemble (+ sign on macOS)
|
|
102
|
-
|
|
240
|
+
const artifactKind = IS_WINDOWS ? "app folder" : IS_LINUX ? "app folder" : ".app";
|
|
241
|
+
console.log(`[mirin build] assembling ${artifactKind}…`);
|
|
103
242
|
// version.json embeds the running app's update identity (read by app.updater).
|
|
104
243
|
// Only when `release` is configured — otherwise the app has no updater.
|
|
105
244
|
const versionJson = baseUrl
|
|
@@ -108,7 +247,13 @@ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
|
|
|
108
247
|
const resources = {
|
|
109
248
|
uiDir: join(projectDir, "dist"),
|
|
110
249
|
workerJs,
|
|
111
|
-
manifestJson: JSON.stringify({
|
|
250
|
+
manifestJson: JSON.stringify({
|
|
251
|
+
id: bundleId,
|
|
252
|
+
windows: config.windows,
|
|
253
|
+
singleInstance: config.singleInstance,
|
|
254
|
+
// Lets a diagnostic build opt in via `devtools.production`; otherwise fully off.
|
|
255
|
+
devtools: config.devtools,
|
|
256
|
+
}),
|
|
112
257
|
versionJson,
|
|
113
258
|
workers: extraWorkers,
|
|
114
259
|
};
|
|
@@ -120,25 +265,65 @@ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
|
|
|
120
265
|
coreDll: artifacts.coreDylib,
|
|
121
266
|
helperExe: artifacts.helperBin,
|
|
122
267
|
cefPath: artifacts.cefPath,
|
|
123
|
-
|
|
268
|
+
cefLocales,
|
|
269
|
+
icon: appIconPath,
|
|
124
270
|
resources: { ...resources, sidecars: sidecars.map((s) => ({ name: s.name, src: s.src })) },
|
|
125
271
|
})
|
|
126
|
-
:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
272
|
+
: IS_LINUX
|
|
273
|
+
? await buildLinuxBundle({
|
|
274
|
+
appName,
|
|
275
|
+
outDir,
|
|
276
|
+
hostExe,
|
|
277
|
+
coreDll: artifacts.coreDylib,
|
|
278
|
+
helperExe: artifacts.helperBin,
|
|
279
|
+
cefPath: artifacts.cefPath,
|
|
280
|
+
cefLocales,
|
|
281
|
+
icon: appIconPath,
|
|
282
|
+
resources: {
|
|
283
|
+
...resources,
|
|
284
|
+
sidecars: sidecars.map((s) => ({ name: s.name, src: s.src })),
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
: await buildAppBundle({
|
|
288
|
+
appName,
|
|
289
|
+
bundleId,
|
|
290
|
+
outDir,
|
|
291
|
+
hostExe,
|
|
292
|
+
coreDylib: artifacts.coreDylib,
|
|
293
|
+
helperBin: artifacts.helperBin,
|
|
294
|
+
cefPath: artifacts.cefPath,
|
|
295
|
+
cefLocales,
|
|
296
|
+
version,
|
|
297
|
+
icon: appIconPath,
|
|
298
|
+
signIdentity,
|
|
299
|
+
urlSchemes: config.urlSchemes,
|
|
300
|
+
resources: { ...resources, sidecars },
|
|
301
|
+
});
|
|
140
302
|
|
|
141
303
|
console.log(`\n[mirin build] done → ${app}`);
|
|
304
|
+
|
|
305
|
+
// Linux: optionally emit distributable packages (AppImage/deb/rpm) straight from
|
|
306
|
+
// `mirin build`. `mirin release` does its own packaging into build/release, so it
|
|
307
|
+
// never sets `packageLinux` — this path only runs for a direct build request.
|
|
308
|
+
if (IS_LINUX && opts.packageLinux && linux !== false) {
|
|
309
|
+
const formats = resolveLinuxFormats(linux, opts.linuxFormats);
|
|
310
|
+
console.log(`[mirin build] packaging Linux artifacts (${formats.join(", ")})…`);
|
|
311
|
+
const pkgs = await buildLinuxPackages({
|
|
312
|
+
appDir: app,
|
|
313
|
+
appName,
|
|
314
|
+
bundleId,
|
|
315
|
+
version,
|
|
316
|
+
publisher,
|
|
317
|
+
outDir,
|
|
318
|
+
projectDir,
|
|
319
|
+
icon: appIconPath,
|
|
320
|
+
options: typeof linux === "object" ? linux : {},
|
|
321
|
+
formats,
|
|
322
|
+
});
|
|
323
|
+
const mb = (n: number) => (n / 1e6).toFixed(1);
|
|
324
|
+
for (const p of pkgs) console.log(` ${p.path} (${mb(p.size)} MB)`);
|
|
325
|
+
}
|
|
326
|
+
|
|
142
327
|
return {
|
|
143
328
|
app,
|
|
144
329
|
appName,
|
|
@@ -146,10 +331,16 @@ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
|
|
|
146
331
|
version,
|
|
147
332
|
channel,
|
|
148
333
|
baseUrl,
|
|
334
|
+
releaseNotes,
|
|
149
335
|
coreDylib: artifacts.coreDylib,
|
|
336
|
+
codecBin: artifacts.codecBin,
|
|
150
337
|
projectDir,
|
|
151
338
|
dmg,
|
|
152
339
|
nsis,
|
|
340
|
+
inno,
|
|
341
|
+
linux,
|
|
342
|
+
publisher,
|
|
343
|
+
icon: appIconPath,
|
|
153
344
|
signIdentity,
|
|
154
345
|
};
|
|
155
346
|
}
|