@mirinjs/cli 0.1.0-alpha.1 → 0.1.0-alpha.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/package.json +5 -5
- package/src/artifacts.ts +12 -5
- package/src/build.ts +165 -6
- package/src/bundle-linux.ts +234 -0
- package/src/dev.ts +64 -8
- package/src/dmg.ts +29 -1
- package/src/icon-win.ts +210 -39
- package/src/index.ts +46 -3
- package/src/installer-inno.ts +130 -0
- package/src/package-linux.ts +334 -0
- package/src/release.ts +104 -38
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux distributable packages for a mirin app — **AppImage**, **.deb**, **.rpm** —
|
|
3
|
+
* built from the assembled Linux app folder (buildLinuxBundle's output). This is the
|
|
4
|
+
* Linux analog of dmg.ts (macOS) and installer-inno.ts / installer-win.ts (Windows):
|
|
5
|
+
* `mirin build` (with packaging requested) and `mirin release` call it after the app
|
|
6
|
+
* folder is assembled.
|
|
7
|
+
*
|
|
8
|
+
* The app folder is a flat, relocatable payload — the host binary, libcef.so + the
|
|
9
|
+
* CEF runtime, and `resources/`, all resolved at runtime from an `$ORIGIN` rpath and
|
|
10
|
+
* `dirname(process.execPath)` (host.ts). Every package therefore ships the whole
|
|
11
|
+
* folder verbatim under one prefix and points a launcher at the *real* host binary
|
|
12
|
+
* inside it (never a bare PATH name — the host derives everything from its own dir):
|
|
13
|
+
*
|
|
14
|
+
* - deb / rpm: payload → `/opt/<id>/`, a `/usr/bin/<bin>` wrapper `exec`s
|
|
15
|
+
* `/opt/<id>/<App>`, plus `/usr/share/applications/<id>.desktop` and a
|
|
16
|
+
* `/usr/share/icons/hicolor/256x256/apps/<id>.png`.
|
|
17
|
+
* - AppImage: payload → `AppDir/usr/lib/<id>/`, top-level `AppRun` `exec`s it,
|
|
18
|
+
* `<id>.desktop` + `<id>.png` (+ `.DirIcon`) at the AppDir root, packed by
|
|
19
|
+
* appimagetool.
|
|
20
|
+
*
|
|
21
|
+
* Tooling (resolved via $MIRIN_FPM / $MIRIN_APPIMAGETOOL, then PATH, then a
|
|
22
|
+
* conventional fallback — see resolveTool):
|
|
23
|
+
* - **fpm** builds both deb and rpm in pure Ruby (no dpkg/rpmbuild on the host).
|
|
24
|
+
* - **appimagetool** packs the AppImage (invoked with `--appimage-extract-and-run`
|
|
25
|
+
* so it works on hosts without FUSE).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { $ } from "bun";
|
|
29
|
+
import {
|
|
30
|
+
cpSync,
|
|
31
|
+
mkdirSync,
|
|
32
|
+
rmSync,
|
|
33
|
+
writeFileSync,
|
|
34
|
+
chmodSync,
|
|
35
|
+
existsSync,
|
|
36
|
+
symlinkSync,
|
|
37
|
+
statSync,
|
|
38
|
+
} from "node:fs";
|
|
39
|
+
import { join } from "node:path";
|
|
40
|
+
import { homedir } from "node:os";
|
|
41
|
+
import { desktopEntry, resolveLinuxDesktopIconPng } from "./bundle-linux.ts";
|
|
42
|
+
import type { LinuxConfig, LinuxPackageFormat } from "mirinjs";
|
|
43
|
+
|
|
44
|
+
export interface LinuxPackageInput {
|
|
45
|
+
/** The assembled Linux app folder (build/<App>): the whole relocatable payload. */
|
|
46
|
+
appDir: string;
|
|
47
|
+
/** App display name; also the host-binary stem inside `appDir`. */
|
|
48
|
+
appName: string;
|
|
49
|
+
/** Reverse-DNS app id, e.g. "dev.netko.anko" (install prefix + `.desktop`/icon name). */
|
|
50
|
+
bundleId: string;
|
|
51
|
+
version: string;
|
|
52
|
+
/** Publisher / company (deb/rpm vendor + maintainer fallback). */
|
|
53
|
+
publisher: string;
|
|
54
|
+
/** Directory the artifacts are written into. */
|
|
55
|
+
outDir: string;
|
|
56
|
+
projectDir: string;
|
|
57
|
+
/** Abs path to the app icon source (`.png` / `.iconset`); resolved to a PNG. */
|
|
58
|
+
icon?: string;
|
|
59
|
+
/** Resolved Linux config (maintainer, depends, category, …). */
|
|
60
|
+
options: LinuxConfig;
|
|
61
|
+
/** Which formats to build. */
|
|
62
|
+
formats: LinuxPackageFormat[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LinuxPackageResult {
|
|
66
|
+
format: LinuxPackageFormat;
|
|
67
|
+
path: string;
|
|
68
|
+
size: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** CEF/GTK runtime deps on Debian/Ubuntu package naming (best-effort; overridable). */
|
|
72
|
+
const DEFAULT_DEB_DEPENDS = [
|
|
73
|
+
"libgtk-3-0", "libnss3", "libnspr4", "libasound2", "libx11-6", "libxcomposite1",
|
|
74
|
+
"libxdamage1", "libxext6", "libxfixes3", "libxrandr2", "libgbm1", "libxkbcommon0",
|
|
75
|
+
"libpango-1.0-0", "libcairo2", "libatk1.0-0", "libatk-bridge2.0-0", "libcups2",
|
|
76
|
+
"libdrm2", "libdbus-1-3",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/** CEF/GTK runtime deps on RPM (Fedora/RHEL/openSUSE) package naming. */
|
|
80
|
+
const DEFAULT_RPM_DEPENDS = [
|
|
81
|
+
"gtk3", "nss", "nspr", "alsa-lib", "libX11", "libXcomposite", "libXdamage",
|
|
82
|
+
"libXext", "libXfixes", "libXrandr", "mesa-libgbm", "libxkbcommon", "pango",
|
|
83
|
+
"cairo", "atk", "at-spi2-atk", "cups-libs", "libdrm", "dbus-libs",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve an external tool path: an explicit `$ENV` override (if it exists), then
|
|
88
|
+
* PATH, then the first existing `fallback`. Throws a clear, actionable error when
|
|
89
|
+
* none resolve (no user-specific absolute path is baked into committed code).
|
|
90
|
+
*/
|
|
91
|
+
function resolveTool(bin: string, envVar: string, fallbacks: string[]): string {
|
|
92
|
+
const fromEnv = process.env[envVar];
|
|
93
|
+
if (fromEnv && existsSync(fromEnv)) return fromEnv;
|
|
94
|
+
const onPath = Bun.which(bin);
|
|
95
|
+
if (onPath) return onPath;
|
|
96
|
+
for (const f of fallbacks) if (existsSync(f)) return f;
|
|
97
|
+
throw new Error(
|
|
98
|
+
`[mirin] "${bin}" not found — install it and put it on PATH, or set $${envVar} ` +
|
|
99
|
+
`to its absolute path.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `~/.local/share/gem/ruby/<ver>/bin` (RubyGems user install dir), if resolvable. */
|
|
104
|
+
function gemUserBin(): string | undefined {
|
|
105
|
+
const ruby = Bun.which("ruby");
|
|
106
|
+
if (!ruby) return undefined;
|
|
107
|
+
try {
|
|
108
|
+
const dir = Bun.spawnSync([ruby, "-e", "print Gem.user_dir"]).stdout.toString().trim();
|
|
109
|
+
return dir ? join(dir, "bin") : undefined;
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveFpm(): string {
|
|
116
|
+
const fallbacks: string[] = [];
|
|
117
|
+
const gemBin = gemUserBin();
|
|
118
|
+
if (gemBin) fallbacks.push(join(gemBin, "fpm"));
|
|
119
|
+
return resolveTool("fpm", "MIRIN_FPM", fallbacks);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveAppimagetool(): string {
|
|
123
|
+
return resolveTool("appimagetool", "MIRIN_APPIMAGETOOL", [
|
|
124
|
+
join(homedir(), ".local", "bin", "appimagetool"),
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A Debian/RPM-friendly package name: lowercase, alnum + `.-+`, no leading sigil. */
|
|
129
|
+
function shortName(input: string): string {
|
|
130
|
+
return (
|
|
131
|
+
input.toLowerCase().replace(/[^a-z0-9.\-+]/g, "-").replace(/^[.\-+]+/, "") || "app"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const size = (p: string) => statSync(p).size;
|
|
136
|
+
|
|
137
|
+
/** Build every requested Linux package; returns each artifact's path + size. */
|
|
138
|
+
export async function buildLinuxPackages(
|
|
139
|
+
input: LinuxPackageInput,
|
|
140
|
+
): Promise<LinuxPackageResult[]> {
|
|
141
|
+
mkdirSync(input.outDir, { recursive: true });
|
|
142
|
+
const binName = input.options.binName ?? shortName(input.bundleId.split(".").pop() ?? input.appName);
|
|
143
|
+
const results: LinuxPackageResult[] = [];
|
|
144
|
+
|
|
145
|
+
// deb + rpm share one staged filesystem tree (payload under /opt, launcher,
|
|
146
|
+
// .desktop, icon). Build it once and reuse.
|
|
147
|
+
const needFsTree = input.formats.includes("deb") || input.formats.includes("rpm");
|
|
148
|
+
let fsRoot: string | undefined;
|
|
149
|
+
try {
|
|
150
|
+
if (needFsTree) fsRoot = stageFsTree(input, binName);
|
|
151
|
+
if (input.formats.includes("deb")) results.push(await buildFpm(input, binName, fsRoot!, "deb"));
|
|
152
|
+
if (input.formats.includes("rpm")) results.push(await buildFpm(input, binName, fsRoot!, "rpm"));
|
|
153
|
+
if (input.formats.includes("appimage")) results.push(await buildAppImage(input, binName));
|
|
154
|
+
} finally {
|
|
155
|
+
if (fsRoot) rmSync(fsRoot, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Stage the on-disk filesystem layout deb/rpm install: the whole payload under
|
|
162
|
+
* `/opt/<id>/`, a `/usr/bin/<bin>` wrapper `exec`ing the real host binary, the
|
|
163
|
+
* `.desktop` entry, and a hicolor icon. Returns the staging root (fpm's `-C` chdir).
|
|
164
|
+
*/
|
|
165
|
+
function stageFsTree(input: LinuxPackageInput, binName: string): string {
|
|
166
|
+
const root = join(input.outDir, `.pkgroot-${process.pid}`);
|
|
167
|
+
rmSync(root, { recursive: true, force: true });
|
|
168
|
+
|
|
169
|
+
// Payload → /opt/<id>/…
|
|
170
|
+
const prefix = join(root, "opt", input.bundleId);
|
|
171
|
+
mkdirSync(prefix, { recursive: true });
|
|
172
|
+
cpSync(input.appDir, prefix, { recursive: true });
|
|
173
|
+
|
|
174
|
+
// /usr/bin/<bin>: a wrapper that exec's the real binary so process.execPath (and
|
|
175
|
+
// thus the host's core/resources/helper resolution) points into /opt/<id>.
|
|
176
|
+
const binDir = join(root, "usr", "bin");
|
|
177
|
+
mkdirSync(binDir, { recursive: true });
|
|
178
|
+
const launcher = join(binDir, binName);
|
|
179
|
+
writeFileSync(launcher, `#!/bin/sh\nexec "/opt/${input.bundleId}/${input.appName}" "$@"\n`);
|
|
180
|
+
chmodSync(launcher, 0o755);
|
|
181
|
+
|
|
182
|
+
// /usr/share/applications/<id>.desktop — Icon= is the theme name (<id>), resolved
|
|
183
|
+
// from the hicolor icon below; Exec= is the /usr/bin launcher on PATH.
|
|
184
|
+
const appsDir = join(root, "usr", "share", "applications");
|
|
185
|
+
mkdirSync(appsDir, { recursive: true });
|
|
186
|
+
writeFileSync(join(appsDir, `${input.bundleId}.desktop`), buildDesktop(input, binName));
|
|
187
|
+
|
|
188
|
+
stageHicolorIcon(root, input);
|
|
189
|
+
return root;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Install the app icon at `usr/share/icons/hicolor/256x256/apps/<id>.png` under `root`. */
|
|
193
|
+
function stageHicolorIcon(root: string, input: LinuxPackageInput): void {
|
|
194
|
+
const png = input.icon ? resolveLinuxDesktopIconPng(input.icon) : undefined;
|
|
195
|
+
if (!png) return;
|
|
196
|
+
const dir = join(root, "usr", "share", "icons", "hicolor", "256x256", "apps");
|
|
197
|
+
mkdirSync(dir, { recursive: true });
|
|
198
|
+
cpSync(png, join(dir, `${input.bundleId}.png`));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The `.desktop` body shared by deb/rpm/AppImage (Exec = launcher name, Icon = <id>). */
|
|
202
|
+
function buildDesktop(input: LinuxPackageInput, binName: string): string {
|
|
203
|
+
return desktopEntry({
|
|
204
|
+
name: input.appName,
|
|
205
|
+
exec: binName,
|
|
206
|
+
iconPng: input.bundleId,
|
|
207
|
+
wmClass: input.bundleId,
|
|
208
|
+
categories: input.options.category ?? "Utility",
|
|
209
|
+
comment: input.options.description,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Build a .deb or .rpm from the staged tree via fpm. */
|
|
214
|
+
async function buildFpm(
|
|
215
|
+
input: LinuxPackageInput,
|
|
216
|
+
binName: string,
|
|
217
|
+
fsRoot: string,
|
|
218
|
+
kind: "deb" | "rpm",
|
|
219
|
+
): Promise<LinuxPackageResult> {
|
|
220
|
+
const fpm = resolveFpm();
|
|
221
|
+
const arch = kind === "deb" ? "amd64" : "x86_64";
|
|
222
|
+
const fileName =
|
|
223
|
+
kind === "deb"
|
|
224
|
+
? `${binName}_${input.version}_${arch}.deb`
|
|
225
|
+
: `${binName}-${input.version}-1.${arch}.rpm`;
|
|
226
|
+
const out = join(input.outDir, fileName);
|
|
227
|
+
rmSync(out, { force: true });
|
|
228
|
+
|
|
229
|
+
const depends =
|
|
230
|
+
kind === "deb"
|
|
231
|
+
? input.options.debDepends ?? DEFAULT_DEB_DEPENDS
|
|
232
|
+
: input.options.rpmDepends ?? DEFAULT_RPM_DEPENDS;
|
|
233
|
+
|
|
234
|
+
const args: string[] = [
|
|
235
|
+
"-s", "dir",
|
|
236
|
+
"-t", kind,
|
|
237
|
+
"-C", fsRoot,
|
|
238
|
+
"--package", out,
|
|
239
|
+
"--name", binName,
|
|
240
|
+
"--version", input.version,
|
|
241
|
+
"--iteration", "1",
|
|
242
|
+
"--architecture", arch,
|
|
243
|
+
"--maintainer", input.options.maintainer ?? input.publisher,
|
|
244
|
+
"--vendor", input.publisher,
|
|
245
|
+
"--description", input.options.description ?? input.appName,
|
|
246
|
+
];
|
|
247
|
+
if (input.options.homepage) args.push("--url", input.options.homepage);
|
|
248
|
+
if (input.options.license) args.push("--license", input.options.license);
|
|
249
|
+
for (const d of depends) args.push("--depends", d);
|
|
250
|
+
// Package the whole staged tree ('.' relative to the -C chdir).
|
|
251
|
+
args.push(".");
|
|
252
|
+
|
|
253
|
+
console.log(`[mirin] building ${kind} → ${fileName}`);
|
|
254
|
+
const res = await $`${fpm} ${args}`.nothrow().quiet();
|
|
255
|
+
if (res.exitCode !== 0 || !existsSync(out)) {
|
|
256
|
+
throw new Error(`fpm (${kind}) failed (exit ${res.exitCode}):\n${res.stderr.toString()}`);
|
|
257
|
+
}
|
|
258
|
+
return { format: kind, path: out, size: size(out) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Build an AppDir and pack it into a single-file AppImage via appimagetool. */
|
|
262
|
+
async function buildAppImage(
|
|
263
|
+
input: LinuxPackageInput,
|
|
264
|
+
binName: string,
|
|
265
|
+
): Promise<LinuxPackageResult> {
|
|
266
|
+
const appimagetool = resolveAppimagetool();
|
|
267
|
+
const appDirRoot = join(input.outDir, `.AppDir-${process.pid}`);
|
|
268
|
+
rmSync(appDirRoot, { recursive: true, force: true });
|
|
269
|
+
|
|
270
|
+
// Payload → AppDir/usr/lib/<id>/ so $ORIGIN resolves within it.
|
|
271
|
+
const payload = join(appDirRoot, "usr", "lib", input.bundleId);
|
|
272
|
+
mkdirSync(payload, { recursive: true });
|
|
273
|
+
cpSync(input.appDir, payload, { recursive: true });
|
|
274
|
+
|
|
275
|
+
// AppRun: exec the real host binary (resolves core/resources/helper from its dir).
|
|
276
|
+
const appRun = join(appDirRoot, "AppRun");
|
|
277
|
+
writeFileSync(
|
|
278
|
+
appRun,
|
|
279
|
+
[
|
|
280
|
+
"#!/bin/sh",
|
|
281
|
+
'HERE="$(dirname "$(readlink -f "$0")")"',
|
|
282
|
+
`exec "$HERE/usr/lib/${input.bundleId}/${input.appName}" "$@"`,
|
|
283
|
+
"",
|
|
284
|
+
].join("\n"),
|
|
285
|
+
);
|
|
286
|
+
chmodSync(appRun, 0o755);
|
|
287
|
+
|
|
288
|
+
// .desktop at the AppDir root (appimagetool reads it) + the standard location.
|
|
289
|
+
const desktop = buildDesktop(input, binName);
|
|
290
|
+
writeFileSync(join(appDirRoot, `${input.bundleId}.desktop`), desktop);
|
|
291
|
+
const appsDir = join(appDirRoot, "usr", "share", "applications");
|
|
292
|
+
mkdirSync(appsDir, { recursive: true });
|
|
293
|
+
writeFileSync(join(appsDir, `${input.bundleId}.desktop`), desktop);
|
|
294
|
+
|
|
295
|
+
// Icon: <id>.png + .DirIcon at the root, plus the hicolor copy.
|
|
296
|
+
const png = input.icon ? resolveLinuxDesktopIconPng(input.icon) : undefined;
|
|
297
|
+
if (png) {
|
|
298
|
+
cpSync(png, join(appDirRoot, `${input.bundleId}.png`));
|
|
299
|
+
try {
|
|
300
|
+
symlinkSync(`${input.bundleId}.png`, join(appDirRoot, ".DirIcon"));
|
|
301
|
+
} catch {
|
|
302
|
+
/* best-effort: some filesystems disallow symlinks */
|
|
303
|
+
}
|
|
304
|
+
stageHicolorIcon(appDirRoot, input);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const fileName = `${input.appName}-${input.version}-x86_64.AppImage`;
|
|
308
|
+
const out = join(input.outDir, fileName);
|
|
309
|
+
rmSync(out, { force: true });
|
|
310
|
+
|
|
311
|
+
console.log(`[mirin] building AppImage → ${fileName}`);
|
|
312
|
+
// --appimage-extract-and-run: run appimagetool without FUSE. ARCH is required by
|
|
313
|
+
// appimagetool; NO_STRIP avoids stripping the Bun-compiled host (which would break it).
|
|
314
|
+
const res = await $`${appimagetool} --appimage-extract-and-run ${appDirRoot} ${out}`
|
|
315
|
+
.env({ ...process.env, ARCH: "x86_64", NO_STRIP: "1" })
|
|
316
|
+
.nothrow()
|
|
317
|
+
.quiet();
|
|
318
|
+
rmSync(appDirRoot, { recursive: true, force: true });
|
|
319
|
+
if (res.exitCode !== 0 || !existsSync(out)) {
|
|
320
|
+
throw new Error(`appimagetool failed (exit ${res.exitCode}):\n${res.stderr.toString()}`);
|
|
321
|
+
}
|
|
322
|
+
chmodSync(out, 0o755);
|
|
323
|
+
return { format: "appimage", path: out, size: size(out) };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Resolve the effective set of formats from config + an optional CLI/caller override. */
|
|
327
|
+
export function resolveLinuxFormats(
|
|
328
|
+
linux: boolean | LinuxConfig | undefined,
|
|
329
|
+
override?: LinuxPackageFormat[],
|
|
330
|
+
): LinuxPackageFormat[] {
|
|
331
|
+
if (override && override.length) return override;
|
|
332
|
+
const fromConfig = typeof linux === "object" ? linux.formats : undefined;
|
|
333
|
+
return fromConfig && fromConfig.length ? fromConfig : ["appimage", "deb", "rpm"];
|
|
334
|
+
}
|
package/src/release.ts
CHANGED
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
|
|
19
19
|
import { $ } from "bun";
|
|
20
20
|
import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
21
|
-
import { join } from "node:path";
|
|
21
|
+
import { join, basename } from "node:path";
|
|
22
22
|
import { tmpdir } from "node:os";
|
|
23
23
|
import { build } from "./build.ts";
|
|
24
24
|
import { buildDmg, notarizeAndStaple, type DmgOptions } from "./dmg.ts";
|
|
25
25
|
import { buildNsisInstaller, hasMakensis } from "./installer-win.ts";
|
|
26
|
+
import { buildInnoInstaller, hasInno } from "./installer-inno.ts";
|
|
27
|
+
import { buildLinuxPackages, resolveLinuxFormats } from "./package-linux.ts";
|
|
26
28
|
import { loadCodec } from "mirinjs/codec";
|
|
27
29
|
|
|
28
30
|
const sha256File = (path: string) =>
|
|
@@ -36,13 +38,14 @@ export async function release(projectDir = process.cwd()): Promise<number> {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const isWindows = process.platform === "win32";
|
|
39
|
-
const
|
|
41
|
+
const isLinux = process.platform === "linux";
|
|
42
|
+
const platform = isWindows ? "win32" : isLinux ? "linux" : "darwin";
|
|
40
43
|
const arch = process.arch; // "arm64" | "x64"
|
|
41
44
|
const prefix = `${result.channel}-${platform}-${arch}`;
|
|
42
45
|
const safeName = result.appName.replace(/[^A-Za-z0-9._-]/g, "");
|
|
43
46
|
const base = result.baseUrl.replace(/\/$/, "");
|
|
44
|
-
// The packaged unit: a flat app folder on Windows, an `.app` bundle on macOS.
|
|
45
|
-
const appArtifact = isWindows ? result.appName : `${result.appName}.app`;
|
|
47
|
+
// The packaged unit: a flat app folder on Windows/Linux, an `.app` bundle on macOS.
|
|
48
|
+
const appArtifact = isWindows || isLinux ? result.appName : `${result.appName}.app`;
|
|
46
49
|
|
|
47
50
|
const buildDir = join(projectDir, "build");
|
|
48
51
|
const outDir = join(buildDir, "release");
|
|
@@ -52,7 +55,7 @@ export async function release(projectDir = process.cwd()): Promise<number> {
|
|
|
52
55
|
// Notarize + staple the .app (Developer ID, macOS) before packing, when
|
|
53
56
|
// credentials are present. The .tar.zst / patch updater bundles are made from
|
|
54
57
|
// this signed, stapled .app, so the updater swaps in an already-notarized app.
|
|
55
|
-
if (!isWindows) await notarizeAndStaple(result.app);
|
|
58
|
+
if (!isWindows && !isLinux) await notarizeAndStaple(result.app);
|
|
56
59
|
|
|
57
60
|
// Load the codec (zstd/bsdiff) from the *bundled* core, not the raw build output:
|
|
58
61
|
// on Windows mirin_core.dll imports libcef.dll. The DLL loader resolves imports
|
|
@@ -130,29 +133,44 @@ export async function release(projectDir = process.cwd()): Promise<number> {
|
|
|
130
133
|
let installerName: string | undefined;
|
|
131
134
|
let installerSize = 0;
|
|
132
135
|
if (isWindows) {
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// portable .zip
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
136
|
+
// A real installer (Program Files / per-user install, Start Menu + Desktop
|
|
137
|
+
// shortcuts, uninstaller, Add/Remove Programs): Inno Setup (modern wizard)
|
|
138
|
+
// preferred, then NSIS, then a portable .zip if neither toolchain is present.
|
|
139
|
+
const setupName = `${prefix}-${safeName}-setup.exe`;
|
|
140
|
+
const installerArgs = {
|
|
141
|
+
appDir: result.app,
|
|
142
|
+
appName: result.appName,
|
|
143
|
+
exeName: `${result.appName}.exe`,
|
|
144
|
+
version: result.version,
|
|
145
|
+
bundleId: result.bundleId,
|
|
146
|
+
outDir,
|
|
147
|
+
fileName: setupName,
|
|
148
|
+
projectDir: result.projectDir,
|
|
149
|
+
};
|
|
150
|
+
const innoWanted = result.inno !== false;
|
|
151
|
+
const nsisWanted = result.nsis !== false;
|
|
152
|
+
const innoOpts = typeof result.inno === "object" ? result.inno : {};
|
|
153
|
+
const nsisOpts = typeof result.nsis === "object" ? result.nsis : {};
|
|
154
|
+
let exePath: string | undefined;
|
|
155
|
+
if (innoWanted && hasInno()) {
|
|
156
|
+
exePath = await buildInnoInstaller({
|
|
157
|
+
...installerArgs,
|
|
158
|
+
options: { ...innoOpts, publisher: innoOpts.publisher ?? result.publisher },
|
|
149
159
|
});
|
|
160
|
+
} else if (nsisWanted && (await hasMakensis())) {
|
|
161
|
+
exePath = await buildNsisInstaller({
|
|
162
|
+
...installerArgs,
|
|
163
|
+
options: { ...nsisOpts, publisher: nsisOpts.publisher ?? result.publisher },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (exePath) {
|
|
167
|
+
installerName = setupName;
|
|
150
168
|
installerSize = readFileSync(exePath).byteLength;
|
|
151
169
|
} else {
|
|
152
|
-
if (
|
|
170
|
+
if (innoWanted || nsisWanted) {
|
|
153
171
|
console.warn(
|
|
154
|
-
"[mirin release]
|
|
155
|
-
"Install
|
|
172
|
+
"[mirin release] no installer toolchain found — shipping the portable .zip. " +
|
|
173
|
+
"Install Inno Setup (`scoop install inno-setup`) or NSIS (`scoop install nsis`).",
|
|
156
174
|
);
|
|
157
175
|
}
|
|
158
176
|
installerName = `${prefix}-${safeName}.zip`;
|
|
@@ -163,21 +181,69 @@ export async function release(projectDir = process.cwd()): Promise<number> {
|
|
|
163
181
|
await $`tar -a -cf ${zipPath} -C ${buildDir} ${appArtifact}`;
|
|
164
182
|
installerSize = readFileSync(zipPath).byteLength;
|
|
165
183
|
}
|
|
184
|
+
} else if (isLinux) {
|
|
185
|
+
// Linux: the first-install convenience is a set of distributable packages
|
|
186
|
+
// (AppImage + .deb + .rpm), the analog of the macOS .dmg / Windows installer.
|
|
187
|
+
// Never fail the whole release over a packaging step — warn and ship the rest.
|
|
188
|
+
if (result.linux !== false) {
|
|
189
|
+
try {
|
|
190
|
+
const formats = resolveLinuxFormats(result.linux);
|
|
191
|
+
console.log(`[mirin release] building Linux packages (${formats.join(", ")})…`);
|
|
192
|
+
const pkgs = await buildLinuxPackages({
|
|
193
|
+
appDir: result.app,
|
|
194
|
+
appName: result.appName,
|
|
195
|
+
bundleId: result.bundleId,
|
|
196
|
+
version: result.version,
|
|
197
|
+
publisher: result.publisher,
|
|
198
|
+
outDir,
|
|
199
|
+
projectDir: result.projectDir,
|
|
200
|
+
icon: result.icon,
|
|
201
|
+
options: typeof result.linux === "object" ? result.linux : {},
|
|
202
|
+
formats,
|
|
203
|
+
});
|
|
204
|
+
// The AppImage doubles as the primary "installer" line in the summary.
|
|
205
|
+
const appImage = pkgs.find((p) => p.format === "appimage") ?? pkgs[0];
|
|
206
|
+
if (appImage) {
|
|
207
|
+
installerName = basename(appImage.path);
|
|
208
|
+
installerSize = appImage.size;
|
|
209
|
+
}
|
|
210
|
+
for (const p of pkgs) {
|
|
211
|
+
if (p !== appImage) console.log(` ${basename(p.path)} (${(p.size / 1e6).toFixed(1)} MB)`);
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.warn(
|
|
215
|
+
`\n[mirin release] ⚠️ Linux packaging failed: ${e instanceof Error ? e.message : e}\n` +
|
|
216
|
+
`[mirin release] shipping the updater bundle + manifest WITHOUT packages.\n`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
166
220
|
} else if (result.dmg !== false) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
221
|
+
// The DMG is the first-install convenience; the updater bundle + manifest (the
|
|
222
|
+
// critical artifacts) are already built. Never fail the whole release over a
|
|
223
|
+
// flaky installer step — warn loudly and ship the rest.
|
|
224
|
+
const dmgName = `${prefix}-${safeName}.dmg`;
|
|
225
|
+
console.log(`[mirin release] building installer → ${dmgName}`);
|
|
226
|
+
try {
|
|
227
|
+
const options: DmgOptions = typeof result.dmg === "object" ? result.dmg : {};
|
|
228
|
+
const dmgPath = await buildDmg({
|
|
229
|
+
app: result.app,
|
|
230
|
+
appName: result.appName,
|
|
231
|
+
outDir,
|
|
232
|
+
fileName: dmgName,
|
|
233
|
+
options,
|
|
234
|
+
projectDir: result.projectDir,
|
|
235
|
+
signIdentity: result.signIdentity,
|
|
236
|
+
});
|
|
237
|
+
await notarizeAndStaple(dmgPath); // no-op without notary credentials
|
|
238
|
+
installerName = dmgName;
|
|
239
|
+
installerSize = readFileSync(dmgPath).byteLength;
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.warn(
|
|
242
|
+
`\n[mirin release] ⚠️ DMG build failed: ${e instanceof Error ? e.message : e}\n` +
|
|
243
|
+
`[mirin release] shipping the updater bundle + manifest WITHOUT a .dmg installer.\n`,
|
|
244
|
+
);
|
|
245
|
+
rmSync(join(outDir, dmgName), { force: true });
|
|
246
|
+
}
|
|
181
247
|
}
|
|
182
248
|
|
|
183
249
|
const mb = (n: number) => (n / 1e6).toFixed(1);
|