@mirinjs/cli 0.1.0-alpha.14 → 0.1.0-alpha.16
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 -6
- package/src/artifacts.ts +124 -15
- package/src/build.ts +89 -66
- package/src/{bundle-linux.ts → bundle/linux/app.ts} +54 -13
- package/src/bundle/linux/index.ts +1 -0
- package/src/{bundle.ts → bundle/macos/app.ts} +17 -6
- 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/dev.ts +49 -34
- package/src/dmg.ts +22 -10
- package/src/extras.ts +45 -8
- package/src/{icon-win.ts → icons/windows/icon.ts} +6 -3
- package/src/icons/windows/index.ts +1 -0
- package/src/index.ts +8 -7
- package/src/installer-inno.ts +20 -9
- 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.ts → package/linux/package.ts} +90 -131
- 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 +127 -157
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.16",
|
|
4
4
|
"description": "CLI for mirin apps: dev, build, init.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,13 +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.16",
|
|
31
|
+
"mirinjs": "0.1.0-alpha.16"
|
|
32
32
|
},
|
|
33
33
|
"optionalDependencies": {
|
|
34
|
-
"@mirinjs/darwin-arm64": "0.1.0-alpha.
|
|
35
|
-
"@mirinjs/win32-x64": "0.1.0-alpha.
|
|
36
|
-
"@mirinjs/
|
|
34
|
+
"@mirinjs/darwin-arm64": "0.1.0-alpha.16",
|
|
35
|
+
"@mirinjs/win32-x64": "0.1.0-alpha.16",
|
|
36
|
+
"@mirinjs/win32-arm64": "0.1.0-alpha.16",
|
|
37
|
+
"@mirinjs/linux-arm64": "0.1.0-alpha.16",
|
|
38
|
+
"@mirinjs/linux-x64": "0.1.0-alpha.16"
|
|
37
39
|
},
|
|
38
40
|
"publishConfig": {
|
|
39
41
|
"access": "public"
|
package/src/artifacts.ts
CHANGED
|
@@ -9,13 +9,22 @@
|
|
|
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";
|
|
19
28
|
|
|
20
29
|
const REPO_SLUG = "Netko-Labs/mirin";
|
|
21
30
|
|
|
@@ -80,11 +89,11 @@ function helperFileName(): string {
|
|
|
80
89
|
function assertSupportedPlatform(): void {
|
|
81
90
|
const supported =
|
|
82
91
|
(process.platform === "darwin" && process.arch === "arm64") ||
|
|
83
|
-
(process.platform === "win32" && process.arch === "x64") ||
|
|
84
|
-
(process.platform === "linux" && process.arch === "x64");
|
|
92
|
+
(process.platform === "win32" && (process.arch === "x64" || process.arch === "arm64")) ||
|
|
93
|
+
(process.platform === "linux" && (process.arch === "x64" || process.arch === "arm64"));
|
|
85
94
|
if (!supported) {
|
|
86
95
|
throw new Error(
|
|
87
|
-
|
|
96
|
+
"mirin alpha supports macOS arm64, Windows x64/arm64, and Linux x64/arm64 " +
|
|
88
97
|
`(got ${process.platform}/${process.arch}).`,
|
|
89
98
|
);
|
|
90
99
|
}
|
|
@@ -101,7 +110,7 @@ function resolveNativeDir(): string {
|
|
|
101
110
|
} catch {
|
|
102
111
|
throw new Error(
|
|
103
112
|
`mirin: prebuilt native package "${pkg}" is not installed. Run \`bun install\` ` +
|
|
104
|
-
|
|
113
|
+
"(it is an optional dependency of @mirinjs/cli for your platform).",
|
|
105
114
|
);
|
|
106
115
|
}
|
|
107
116
|
}
|
|
@@ -136,10 +145,11 @@ async function ensureCef(): Promise<string> {
|
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
const version = cliVersion();
|
|
139
|
-
const
|
|
148
|
+
const cacheRoot = join(homedir(), ".mirinjs", "cef");
|
|
149
|
+
const cacheDir = join(cacheRoot, `${version}-${platformTag()}`);
|
|
140
150
|
if (existsSync(join(cacheDir, cefMarker()))) return cacheDir;
|
|
141
151
|
|
|
142
|
-
mkdirSync(
|
|
152
|
+
mkdirSync(cacheRoot, { recursive: true });
|
|
143
153
|
const asset = `cef-${platformTag()}.tar.gz`;
|
|
144
154
|
const url = `https://github.com/${REPO_SLUG}/releases/download/v${version}/${asset}`;
|
|
145
155
|
console.log(`[mirin] downloading CEF for ${platformTag()} (one-time, ~hundreds of MB)…`);
|
|
@@ -147,14 +157,113 @@ async function ensureCef(): Promise<string> {
|
|
|
147
157
|
// Download with curl, not `Bun.write(file, await fetch(url))`: the latter
|
|
148
158
|
// pins a core at 100% CPU and never completes on large gzip responses
|
|
149
159
|
// (the streaming write path). curl is fast and ubiquitous.
|
|
150
|
-
const
|
|
160
|
+
const downloadDir = mkdtempSync(join(tmpdir(), "mirin-cef-download-"));
|
|
161
|
+
const stagingDir = mkdtempSync(join(cacheRoot, ".staging-"));
|
|
162
|
+
const archive = join(downloadDir, asset);
|
|
163
|
+
const checksum = `${archive}.sha256`;
|
|
164
|
+
const checksumUrl = `${url}.sha256`;
|
|
165
|
+
const httpsOnly = "=https";
|
|
151
166
|
try {
|
|
152
|
-
await $`curl -
|
|
153
|
-
await $`
|
|
167
|
+
await $`curl --fail --show-error --location --retry 3 --proto ${httpsOnly} --proto-redir ${httpsOnly} -o ${checksum} ${checksumUrl}`;
|
|
168
|
+
await $`curl --fail --show-error --location --retry 3 --proto ${httpsOnly} --proto-redir ${httpsOnly} -o ${archive} ${url}`;
|
|
169
|
+
await verifyFileSha256(archive, readFileSync(checksum, "utf8"));
|
|
170
|
+
const listing = await $`tar -tzf ${archive}`.quiet().text();
|
|
171
|
+
validateArchiveEntries(listing);
|
|
172
|
+
await $`tar -xzf ${archive} -C ${stagingDir}`;
|
|
173
|
+
validateExtractedSymlinks(stagingDir);
|
|
174
|
+
if (!existsSync(join(stagingDir, cefMarker()))) {
|
|
175
|
+
throw new Error(`CEF archive is missing ${cefMarker()}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Another process may have populated the cache while this one downloaded.
|
|
179
|
+
if (existsSync(join(cacheDir, cefMarker()))) return cacheDir;
|
|
180
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
181
|
+
renameSync(stagingDir, cacheDir);
|
|
154
182
|
} catch (err) {
|
|
155
|
-
rmSync(archive, { force: true });
|
|
156
183
|
throw new Error(`mirin: failed to download CEF from ${url} (${err}).`);
|
|
184
|
+
} finally {
|
|
185
|
+
rmSync(downloadDir, { recursive: true, force: true });
|
|
186
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
157
187
|
}
|
|
158
|
-
rmSync(archive, { force: true });
|
|
159
188
|
return cacheDir;
|
|
160
189
|
}
|
|
190
|
+
|
|
191
|
+
/** Reject absolute paths and parent traversal before a downloaded tar is extracted. */
|
|
192
|
+
export function validateArchiveEntries(listing: string): void {
|
|
193
|
+
for (const rawEntry of listing.split(/\r?\n/)) {
|
|
194
|
+
if (rawEntry.length === 0) continue;
|
|
195
|
+
const entry = archivePath(rawEntry);
|
|
196
|
+
if (entry == null) throw new Error(`unsafe CEF archive entry: ${rawEntry}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Verify a downloaded release asset before parsing or extracting it. */
|
|
201
|
+
export async function verifyFileSha256(path: string, checksum: string): Promise<void> {
|
|
202
|
+
const expected = checksum.trim().toLowerCase();
|
|
203
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) {
|
|
204
|
+
throw new Error("invalid CEF SHA-256 checksum");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
208
|
+
for await (const chunk of Bun.file(path).stream()) {
|
|
209
|
+
hasher.update(chunk);
|
|
210
|
+
}
|
|
211
|
+
const actual = hasher.digest("hex");
|
|
212
|
+
if (actual !== expected) {
|
|
213
|
+
throw new Error(`CEF SHA-256 mismatch (expected ${expected}, got ${actual})`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Reject extracted symlinks whose lexical target escapes the staging directory. */
|
|
218
|
+
export function validateExtractedSymlinks(root: string): void {
|
|
219
|
+
const absoluteRoot = resolve(root);
|
|
220
|
+
const pending = [absoluteRoot];
|
|
221
|
+
|
|
222
|
+
while (pending.length > 0) {
|
|
223
|
+
const directory = pending.pop();
|
|
224
|
+
if (!directory) continue;
|
|
225
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
226
|
+
const path = join(directory, entry.name);
|
|
227
|
+
if (entry.isSymbolicLink()) {
|
|
228
|
+
const target = readlinkSync(path);
|
|
229
|
+
const resolvedTarget = resolve(dirname(path), target);
|
|
230
|
+
const fromRoot = relative(absoluteRoot, resolvedTarget);
|
|
231
|
+
if (
|
|
232
|
+
isAbsolute(target) ||
|
|
233
|
+
fromRoot === ".." ||
|
|
234
|
+
fromRoot.startsWith(`..${sep}`) ||
|
|
235
|
+
isAbsolute(fromRoot)
|
|
236
|
+
) {
|
|
237
|
+
throw new Error(`CEF archive symlink escapes its cache: ${path} -> ${target}`);
|
|
238
|
+
}
|
|
239
|
+
} else if (entry.isDirectory()) {
|
|
240
|
+
pending.push(path);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function archivePath(raw: string): string | null {
|
|
247
|
+
let path = raw.replaceAll("\\", "/");
|
|
248
|
+
while (path.startsWith("./")) path = path.slice(2);
|
|
249
|
+
if (path.length === 0 || path === ".") return "";
|
|
250
|
+
if (path.startsWith("/") || /^[A-Za-z]:/.test(path) || hasControlCharacter(path)) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const parts: string[] = [];
|
|
255
|
+
for (const part of path.split("/")) {
|
|
256
|
+
if (part.length === 0 || part === ".") continue;
|
|
257
|
+
if (part === "..") return null;
|
|
258
|
+
parts.push(part);
|
|
259
|
+
}
|
|
260
|
+
return parts.length > 0 ? parts.join("/") : "";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function hasControlCharacter(value: string): boolean {
|
|
264
|
+
for (let index = 0; index < value.length; index++) {
|
|
265
|
+
const code = value.charCodeAt(index);
|
|
266
|
+
if (code <= 0x1f || code === 0x7f) return true;
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}
|
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,17 +13,18 @@
|
|
|
15
13
|
* ID to produce a distributable, notarizable app.
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
|
-
import {
|
|
19
|
-
import { mkdirSync, rmSync, readFileSync, writeFileSync, 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";
|
|
23
|
-
import { buildLinuxBundle } from "./bundle-linux.ts";
|
|
24
|
-
import { buildLinuxPackages, resolveLinuxFormats } from "./package-linux.ts";
|
|
25
|
-
import { makeWindowsIcon } from "./icon-win.ts";
|
|
18
|
+
import { $ } from "bun";
|
|
26
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";
|
|
27
27
|
import { sweepBuildTemps } from "./temps.ts";
|
|
28
|
-
import { normalizeSidecars, compileWorkers } from "./extras.ts";
|
|
29
28
|
|
|
30
29
|
const IS_WINDOWS = process.platform === "win32";
|
|
31
30
|
const IS_LINUX = process.platform === "linux";
|
|
@@ -91,11 +90,19 @@ async function brandWindowsExe(
|
|
|
91
90
|
}
|
|
92
91
|
const fv = winFileVersion(opts.version);
|
|
93
92
|
args.push(
|
|
94
|
-
"--set-version-string",
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
"--set-
|
|
98
|
-
"
|
|
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,
|
|
99
106
|
);
|
|
100
107
|
const res = await $`rcedit ${exe} ${args}`.nothrow();
|
|
101
108
|
if (res.exitCode !== 0) {
|
|
@@ -114,6 +121,8 @@ export interface BuildResult {
|
|
|
114
121
|
channel: string;
|
|
115
122
|
/** Update baseUrl, if `release` is configured. */
|
|
116
123
|
baseUrl?: string;
|
|
124
|
+
/** Markdown release notes embedded in the updater manifest, if configured. */
|
|
125
|
+
releaseNotes?: string;
|
|
117
126
|
/** libmirin_core path (for the updater codec at release time). */
|
|
118
127
|
coreDylib: string;
|
|
119
128
|
/** Project root (so `mirin release` can resolve relative asset paths). */
|
|
@@ -176,43 +185,50 @@ export async function build(
|
|
|
176
185
|
const version = opts.version ?? appVersion(projectDir);
|
|
177
186
|
const channel: string = config.release?.channel ?? "stable";
|
|
178
187
|
const baseUrl: string | undefined = config.release?.baseUrl;
|
|
188
|
+
const releaseNotes: string | undefined = config.release?.notes;
|
|
179
189
|
const dmg: boolean | import("mirinjs").DmgConfig = config.dmg ?? true;
|
|
180
190
|
const nsis: boolean | import("mirinjs").NsisConfig = config.nsis ?? true;
|
|
181
191
|
const inno: boolean | import("mirinjs").InnoConfig = config.inno ?? true;
|
|
182
192
|
const linux: boolean | import("mirinjs").LinuxConfig = config.linux ?? true;
|
|
183
193
|
const publisher: string = config.publisher ?? appName;
|
|
194
|
+
const cefLocales = normalizeCefLocales(config.cef?.locales);
|
|
184
195
|
|
|
185
196
|
console.log(`[mirin build] ${appName} ${version}`);
|
|
186
197
|
|
|
187
|
-
//
|
|
188
|
-
console.log("[mirin build]
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
// Production UI and native artifacts do not share outputs, so overlap them.
|
|
199
|
+
console.log("[mirin build] building UI + native artifacts…");
|
|
200
|
+
const [, artifacts] = await Promise.all([
|
|
201
|
+
$`bunx vite build`.cwd(projectDir),
|
|
202
|
+
resolveArtifacts({ release: true }),
|
|
203
|
+
]);
|
|
193
204
|
|
|
194
|
-
//
|
|
195
|
-
console.log("[mirin build] compiling host + bundling
|
|
205
|
+
// Host compilation, main Worker bundling, and extra Worker bundling are independent.
|
|
206
|
+
console.log("[mirin build] compiling host + bundling workers…");
|
|
196
207
|
const signIdentity = process.env.MIRIN_SIGN_IDENTITY;
|
|
197
208
|
const hostExe = join(work, IS_WINDOWS ? "host-release.exe" : "host-release");
|
|
198
209
|
const workerJs = join(work, "worker.release.js");
|
|
199
|
-
await $`bun build --compile --minify ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(projectDir);
|
|
200
|
-
if (IS_WINDOWS) {
|
|
201
|
-
patchPeToGuiSubsystem(hostExe);
|
|
202
|
-
await brandWindowsExe(hostExe, {
|
|
203
|
-
projectDir,
|
|
204
|
-
work,
|
|
205
|
-
appName,
|
|
206
|
-
version,
|
|
207
|
-
icon: config.icon,
|
|
208
|
-
publisher,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
await $`bun build ${mainEntry} --target=bun --minify --outfile ${workerJs}`.cwd(projectDir);
|
|
212
|
-
|
|
213
|
-
// Extra assets: resolve sidecar binaries + compile any extra worker entries.
|
|
214
210
|
const sidecars = normalizeSidecars(projectDir, config.sidecars);
|
|
215
|
-
const
|
|
211
|
+
const hostBuild = (async (): Promise<void> => {
|
|
212
|
+
await $`bun build --compile --minify ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(
|
|
213
|
+
projectDir,
|
|
214
|
+
);
|
|
215
|
+
if (IS_WINDOWS) {
|
|
216
|
+
patchPeToGuiSubsystem(hostExe);
|
|
217
|
+
await brandWindowsExe(hostExe, {
|
|
218
|
+
projectDir,
|
|
219
|
+
work,
|
|
220
|
+
appName,
|
|
221
|
+
version,
|
|
222
|
+
icon: config.icon,
|
|
223
|
+
publisher,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
const workerBuild = $`bun build ${mainEntry} --target=bun --minify --outfile ${workerJs}`.cwd(
|
|
228
|
+
projectDir,
|
|
229
|
+
);
|
|
230
|
+
const extraWorkersBuild = compileWorkers(projectDir, config.workers, join(work, "workers"), true);
|
|
231
|
+
const [, , extraWorkers] = await Promise.all([hostBuild, workerBuild, extraWorkersBuild]);
|
|
216
232
|
|
|
217
233
|
// 5. assemble (+ sign on macOS)
|
|
218
234
|
const artifactKind = IS_WINDOWS ? "app folder" : IS_LINUX ? "app folder" : ".app";
|
|
@@ -241,34 +257,40 @@ export async function build(
|
|
|
241
257
|
coreDll: artifacts.coreDylib,
|
|
242
258
|
helperExe: artifacts.helperBin,
|
|
243
259
|
cefPath: artifacts.cefPath,
|
|
260
|
+
cefLocales,
|
|
244
261
|
icon: config.icon ? join(projectDir, config.icon) : undefined,
|
|
245
262
|
resources: { ...resources, sidecars: sidecars.map((s) => ({ name: s.name, src: s.src })) },
|
|
246
263
|
})
|
|
247
264
|
: IS_LINUX
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
265
|
+
? await buildLinuxBundle({
|
|
266
|
+
appName,
|
|
267
|
+
outDir,
|
|
268
|
+
hostExe,
|
|
269
|
+
coreDll: artifacts.coreDylib,
|
|
270
|
+
helperExe: artifacts.helperBin,
|
|
271
|
+
cefPath: artifacts.cefPath,
|
|
272
|
+
cefLocales,
|
|
273
|
+
icon: config.icon ? join(projectDir, config.icon) : undefined,
|
|
274
|
+
resources: {
|
|
275
|
+
...resources,
|
|
276
|
+
sidecars: sidecars.map((s) => ({ name: s.name, src: s.src })),
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
: await buildAppBundle({
|
|
280
|
+
appName,
|
|
281
|
+
bundleId,
|
|
282
|
+
outDir,
|
|
283
|
+
hostExe,
|
|
284
|
+
coreDylib: artifacts.coreDylib,
|
|
285
|
+
helperBin: artifacts.helperBin,
|
|
286
|
+
cefPath: artifacts.cefPath,
|
|
287
|
+
cefLocales,
|
|
288
|
+
version,
|
|
289
|
+
icon: config.icon ? join(projectDir, config.icon) : undefined,
|
|
290
|
+
signIdentity,
|
|
291
|
+
urlSchemes: config.urlSchemes,
|
|
292
|
+
resources: { ...resources, sidecars },
|
|
293
|
+
});
|
|
272
294
|
|
|
273
295
|
console.log(`\n[mirin build] done → ${app}`);
|
|
274
296
|
|
|
@@ -301,6 +323,7 @@ export async function build(
|
|
|
301
323
|
version,
|
|
302
324
|
channel,
|
|
303
325
|
baseUrl,
|
|
326
|
+
releaseNotes,
|
|
304
327
|
coreDylib: artifacts.coreDylib,
|
|
305
328
|
projectDir,
|
|
306
329
|
dmg,
|
|
@@ -18,8 +18,18 @@
|
|
|
18
18
|
* LD_LIBRARY_PATH set on the host (dev.ts) or an rpath at build time.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
22
|
-
|
|
21
|
+
import {
|
|
22
|
+
chmodSync,
|
|
23
|
+
cpSync,
|
|
24
|
+
existsSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
rmSync,
|
|
28
|
+
statSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from "node:fs";
|
|
31
|
+
import { basename, extname, join } from "node:path";
|
|
32
|
+
import { copyFlatCefLocales } from "../shared/cef-locales.ts";
|
|
23
33
|
|
|
24
34
|
/** Source dir (vendor/cef on Linux) must contain libcef.so. */
|
|
25
35
|
const CEF_MARKER = "libcef.so";
|
|
@@ -29,7 +39,7 @@ const CEF_MARKER = "libcef.so";
|
|
|
29
39
|
* build-time only and intentionally left out. `.so` covers libcef/libEGL/
|
|
30
40
|
* libGLESv2/libvk_swiftshader; the extras list catches the non-extension names. */
|
|
31
41
|
const CEF_RUNTIME_EXTS = new Set([".so", ".bin", ".pak", ".dat"]);
|
|
32
|
-
const CEF_RUNTIME_EXTRAS = ["
|
|
42
|
+
const CEF_RUNTIME_EXTRAS = ["vk_swiftshader_icd.json", "libvulkan.so.1", "chrome-sandbox"];
|
|
33
43
|
|
|
34
44
|
export interface LinuxBundleOptions {
|
|
35
45
|
appName: string; // also the host binary stem → <appName>
|
|
@@ -38,6 +48,8 @@ export interface LinuxBundleOptions {
|
|
|
38
48
|
coreDll: string; // libmirin_core.so
|
|
39
49
|
helperExe: string; // mirin-helper
|
|
40
50
|
cefPath: string; // dir containing the Linux CEF runtime (vendor/cef)
|
|
51
|
+
/** Production CEF locale allowlist. Undefined keeps every locale. */
|
|
52
|
+
cefLocales?: string[];
|
|
41
53
|
/** App icon source (.png / .iconset); resolved to a PNG at resources/icon.png for
|
|
42
54
|
* the window's `_NET_WM_ICON` (taskbar/dock). */
|
|
43
55
|
icon?: string;
|
|
@@ -70,13 +82,13 @@ export function resolveLinuxIconPng(icon: string): string | undefined {
|
|
|
70
82
|
// a 256×256 icon (≈ 256 KB) can exceed it and get dropped. Fall back to the
|
|
71
83
|
// largest available under that ceiling (by pixels parsed from the iconset name).
|
|
72
84
|
const preferred = ["icon_128x128.png", "icon_64x64.png", "icon_256x256.png"];
|
|
73
|
-
const
|
|
85
|
+
const MaxSide = 128;
|
|
74
86
|
const pick =
|
|
75
87
|
preferred.find((p) => pngs.includes(p)) ??
|
|
76
88
|
pngs
|
|
77
89
|
.slice()
|
|
78
90
|
.sort((a, b) => iconsetPixels(b) - iconsetPixels(a))
|
|
79
|
-
.find((p) => iconsetPixels(p) <=
|
|
91
|
+
.find((p) => iconsetPixels(p) <= MaxSide * MaxSide) ??
|
|
80
92
|
pngs.sort((a, b) => iconsetPixels(a) - iconsetPixels(b))[0];
|
|
81
93
|
return pick ? join(icon, pick) : undefined;
|
|
82
94
|
}
|
|
@@ -119,21 +131,49 @@ export function desktopEntry(opts: {
|
|
|
119
131
|
/** Optional Comment= (tooltip / description) line. */
|
|
120
132
|
comment?: string;
|
|
121
133
|
}): string {
|
|
122
|
-
const lines = ["[Desktop Entry]", "Type=Application", `Name=${opts.name}`];
|
|
123
|
-
if (opts.comment) lines.push(`Comment=${opts.comment}`);
|
|
134
|
+
const lines = ["[Desktop Entry]", "Type=Application", `Name=${desktopValue("Name", opts.name)}`];
|
|
135
|
+
if (opts.comment) lines.push(`Comment=${desktopValue("Comment", opts.comment)}`);
|
|
124
136
|
lines.push(
|
|
125
|
-
`Exec=${opts.exec}`,
|
|
126
|
-
`Icon=${opts.iconPng}`,
|
|
127
|
-
`StartupWMClass=${opts.wmClass}`,
|
|
137
|
+
`Exec=${desktopExec(opts.exec)}`,
|
|
138
|
+
`Icon=${desktopValue("Icon", opts.iconPng)}`,
|
|
139
|
+
`StartupWMClass=${desktopValue("StartupWMClass", opts.wmClass)}`,
|
|
128
140
|
"Terminal=false",
|
|
129
141
|
);
|
|
130
142
|
if (opts.categories) {
|
|
131
|
-
lines.push(`Categories=${opts.categories
|
|
143
|
+
lines.push(`Categories=${desktopCategories(opts.categories)}`);
|
|
132
144
|
}
|
|
133
145
|
lines.push("");
|
|
134
146
|
return lines.join("\n");
|
|
135
147
|
}
|
|
136
148
|
|
|
149
|
+
function desktopValue(field: string, value: string): string {
|
|
150
|
+
assertDesktopScalar(field, value);
|
|
151
|
+
return value.replace(/\\/g, "\\\\").replace(/\t/g, "\\t");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function desktopExec(value: string): string {
|
|
155
|
+
assertDesktopScalar("Exec", value);
|
|
156
|
+
if (/^[A-Za-z0-9._/+:-]+$/.test(value)) return value;
|
|
157
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function desktopCategories(value: string): string {
|
|
161
|
+
const normalized = value.endsWith(";") ? value : `${value};`;
|
|
162
|
+
assertDesktopScalar("Categories", normalized);
|
|
163
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]*(;[A-Za-z0-9][A-Za-z0-9-]*)*;$/.test(normalized)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`[mirin] invalid Linux desktop category "${value}" — use freedesktop category names separated by semicolons.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return normalized;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function assertDesktopScalar(field: string, value: string): void {
|
|
172
|
+
if (value.length === 0 || /[\0\r\n]/.test(value)) {
|
|
173
|
+
throw new Error(`[mirin] invalid Linux desktop ${field} value.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
137
177
|
/** Effective pixel count encoded in an iconset filename (`icon_256x256@2x.png` → 512²). */
|
|
138
178
|
function iconsetPixels(name: string): number {
|
|
139
179
|
const m = basename(name).match(/(\d+)x(\d+)(@(\d+)x)?/);
|
|
@@ -163,7 +203,7 @@ export async function buildLinuxBundle(
|
|
|
163
203
|
cpSync(opts.helperExe, helper);
|
|
164
204
|
chmodSync(helper, 0o755);
|
|
165
205
|
|
|
166
|
-
copyCefRuntime(cefPath, app);
|
|
206
|
+
copyCefRuntime(cefPath, app, opts.cefLocales);
|
|
167
207
|
|
|
168
208
|
// The window taskbar/dock icon (`_NET_WM_ICON`) is set by the core from a PNG at
|
|
169
209
|
// `resources/icon.png`; host.ts points the init config's `icon_path` there in prod.
|
|
@@ -210,7 +250,7 @@ export async function buildLinuxBundle(
|
|
|
210
250
|
}
|
|
211
251
|
|
|
212
252
|
/** Copy the CEF runtime (shared libs, paks, snapshot, icu data, locales) into `dest`. */
|
|
213
|
-
function copyCefRuntime(cefPath: string, dest: string): void {
|
|
253
|
+
function copyCefRuntime(cefPath: string, dest: string, locales: string[] | undefined): void {
|
|
214
254
|
for (const entry of readdirSync(cefPath)) {
|
|
215
255
|
if (CEF_RUNTIME_EXTS.has(extname(entry).toLowerCase())) {
|
|
216
256
|
cpSync(join(cefPath, entry), join(dest, entry));
|
|
@@ -220,6 +260,7 @@ function copyCefRuntime(cefPath: string, dest: string): void {
|
|
|
220
260
|
const src = join(cefPath, extra);
|
|
221
261
|
if (existsSync(src)) cpSync(src, join(dest, extra), { recursive: true });
|
|
222
262
|
}
|
|
263
|
+
copyFlatCefLocales(join(cefPath, "locales"), join(dest, "locales"), locales);
|
|
223
264
|
// chrome-sandbox works setuid-root (mode 4755) or, on kernels with unprivileged
|
|
224
265
|
// user namespaces, via the namespace sandbox with no special perms. We can't
|
|
225
266
|
// setuid without root here; leave it executable and let CEF pick the userns path.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./app.ts";
|
|
@@ -11,9 +11,18 @@
|
|
|
11
11
|
* "<exe> Helper (Renderer).app/Contents/MacOS/<exe> Helper (Renderer)".
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
chmodSync,
|
|
16
|
+
cpSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
} from "node:fs";
|
|
16
23
|
import { join } from "node:path";
|
|
24
|
+
import { $ } from "bun";
|
|
25
|
+
import { pruneMacCefLocales } from "../shared/cef-locales.ts";
|
|
17
26
|
|
|
18
27
|
const FRAMEWORK = "Chromium Embedded Framework.framework";
|
|
19
28
|
|
|
@@ -33,6 +42,8 @@ export interface BundleOptions {
|
|
|
33
42
|
coreDylib: string; // libmirin_core.dylib
|
|
34
43
|
helperBin: string; // compiled mirin-helper binary
|
|
35
44
|
cefPath: string; // dir containing the CEF framework (vendor/cef)
|
|
45
|
+
/** Production CEF locale allowlist. Undefined keeps every locale. */
|
|
46
|
+
cefLocales?: string[];
|
|
36
47
|
/** App version → Info.plist CFBundleShortVersionString/CFBundleVersion. */
|
|
37
48
|
version?: string;
|
|
38
49
|
/** App icon source: a .icns, a .iconset dir, or a square .png. Optional. */
|
|
@@ -201,17 +212,17 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
201
212
|
// Deep-link schemes: register this app as the macOS handler for app://-style
|
|
202
213
|
// URLs (e.g. anko://…). Delivered at runtime via app.on("open-url").
|
|
203
214
|
if (opts.urlSchemes?.length) {
|
|
204
|
-
info.CFBundleURLTypes = [
|
|
205
|
-
{ CFBundleURLName: bundleId, CFBundleURLSchemes: opts.urlSchemes },
|
|
206
|
-
];
|
|
215
|
+
info.CFBundleURLTypes = [{ CFBundleURLName: bundleId, CFBundleURLSchemes: opts.urlSchemes }];
|
|
207
216
|
}
|
|
208
217
|
|
|
209
218
|
writeFileSync(join(contents, "Info.plist"), plist(info));
|
|
210
219
|
|
|
211
|
-
|
|
220
|
+
const bundledFramework = join(frameworks, FRAMEWORK);
|
|
221
|
+
cpSync(join(cefPath, FRAMEWORK), bundledFramework, {
|
|
212
222
|
recursive: true,
|
|
213
223
|
verbatimSymlinks: true,
|
|
214
224
|
});
|
|
225
|
+
pruneMacCefLocales(bundledFramework, opts.cefLocales);
|
|
215
226
|
|
|
216
227
|
// Production resources: the served UI, the Worker bundle, and the manifest.
|
|
217
228
|
const resources = join(contents, "Resources");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./app.ts";
|