@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { LinuxPackageFormat } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/** CEF/GTK runtime deps on Debian/Ubuntu package naming (best-effort; overridable). */
|
|
4
|
+
export const DEFAULT_DEB_DEPENDS = [
|
|
5
|
+
"libgtk-3-0",
|
|
6
|
+
"libnss3",
|
|
7
|
+
"libnspr4",
|
|
8
|
+
"libasound2",
|
|
9
|
+
"libx11-6",
|
|
10
|
+
"libxcomposite1",
|
|
11
|
+
"libxdamage1",
|
|
12
|
+
"libxext6",
|
|
13
|
+
"libxfixes3",
|
|
14
|
+
"libxrandr2",
|
|
15
|
+
"libgbm1",
|
|
16
|
+
"libxkbcommon0",
|
|
17
|
+
"libpango-1.0-0",
|
|
18
|
+
"libcairo2",
|
|
19
|
+
"libatk1.0-0",
|
|
20
|
+
"libatk-bridge2.0-0",
|
|
21
|
+
"libcups2",
|
|
22
|
+
"libdrm2",
|
|
23
|
+
"libdbus-1-3",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** CEF/GTK runtime deps on RPM (Fedora/RHEL/openSUSE) package naming. */
|
|
27
|
+
export const DEFAULT_RPM_DEPENDS = [
|
|
28
|
+
"gtk3",
|
|
29
|
+
"nss",
|
|
30
|
+
"nspr",
|
|
31
|
+
"alsa-lib",
|
|
32
|
+
"libX11",
|
|
33
|
+
"libXcomposite",
|
|
34
|
+
"libXdamage",
|
|
35
|
+
"libXext",
|
|
36
|
+
"libXfixes",
|
|
37
|
+
"libXrandr",
|
|
38
|
+
"mesa-libgbm",
|
|
39
|
+
"libxkbcommon",
|
|
40
|
+
"pango",
|
|
41
|
+
"cairo",
|
|
42
|
+
"atk",
|
|
43
|
+
"at-spi2-atk",
|
|
44
|
+
"cups-libs",
|
|
45
|
+
"libdrm",
|
|
46
|
+
"dbus-libs",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const LINUX_FORMATS = [
|
|
50
|
+
"appimage",
|
|
51
|
+
"deb",
|
|
52
|
+
"rpm",
|
|
53
|
+
] as const satisfies readonly LinuxPackageFormat[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve an external tool path: an explicit `$ENV` override (if it exists), then
|
|
7
|
+
* PATH, then the first existing `fallback`. Throws a clear, actionable error when
|
|
8
|
+
* none resolve (no user-specific absolute path is baked into committed code).
|
|
9
|
+
*/
|
|
10
|
+
function resolveTool(bin: string, envVar: string, fallbacks: string[]): string {
|
|
11
|
+
const fromEnv = process.env[envVar];
|
|
12
|
+
if (fromEnv && existsSync(fromEnv)) return fromEnv;
|
|
13
|
+
const onPath = Bun.which(bin);
|
|
14
|
+
if (onPath) return onPath;
|
|
15
|
+
for (const f of fallbacks) if (existsSync(f)) return f;
|
|
16
|
+
throw new Error(
|
|
17
|
+
`[mirin] "${bin}" not found — install it and put it on PATH, or set $${envVar} ` +
|
|
18
|
+
"to its absolute path.",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `~/.local/share/gem/ruby/<ver>/bin` (RubyGems user install dir), if resolvable. */
|
|
23
|
+
function gemUserBin(): string | undefined {
|
|
24
|
+
const ruby = Bun.which("ruby");
|
|
25
|
+
if (!ruby) return undefined;
|
|
26
|
+
try {
|
|
27
|
+
const dir = Bun.spawnSync([ruby, "-e", "print Gem.user_dir"]).stdout.toString().trim();
|
|
28
|
+
return dir ? join(dir, "bin") : undefined;
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveFpm(): string {
|
|
35
|
+
const fallbacks: string[] = [];
|
|
36
|
+
const gemBin = gemUserBin();
|
|
37
|
+
if (gemBin) fallbacks.push(join(gemBin, "fpm"));
|
|
38
|
+
return resolveTool("fpm", "MIRIN_FPM", fallbacks);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveAppimagetool(): string {
|
|
42
|
+
return resolveTool("appimagetool", "MIRIN_APPIMAGETOOL", [
|
|
43
|
+
join(homedir(), ".local", "bin", "appimagetool"),
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { LinuxPackageFormat, LinuxPackageInput } from "../types.ts";
|
|
2
|
+
import { LINUX_FORMATS } from "./defaults.ts";
|
|
3
|
+
|
|
4
|
+
/** A Debian/RPM-friendly package name: lowercase, alnum + `.-+`, no leading sigil. */
|
|
5
|
+
function shortName(input: string): string {
|
|
6
|
+
return (
|
|
7
|
+
input
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9.\-+]/g, "-")
|
|
10
|
+
.replace(/^[.\-+]+/, "") || "app"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function safeLinuxPackageInput(input: LinuxPackageInput): LinuxPackageInput {
|
|
15
|
+
return {
|
|
16
|
+
...input,
|
|
17
|
+
appName: safeAppBinaryName(input.appName),
|
|
18
|
+
bundleId: safeLinuxIdentifier(input.bundleId),
|
|
19
|
+
version: safePackageVersion(input.version),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeLinuxIdentifier(value: string): string {
|
|
24
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) || value === "." || value === "..") {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`[mirin] invalid Linux app id "${value}" — use a reverse-DNS style id with letters, ` +
|
|
27
|
+
'digits, ".", "_" or "-".',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function safeAppBinaryName(value: string): string {
|
|
34
|
+
if (value.length === 0 || value === "." || value === ".." || /[\0\r\n/"\\`$]/.test(value)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[mirin] invalid Linux app name "${value}" — it becomes a binary path segment, ` +
|
|
37
|
+
"so it cannot contain path separators, quotes, shell expansion characters, or newlines.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safePackageVersion(value: string): string {
|
|
44
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9.+:~_-]*$/.test(value)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`[mirin] invalid Linux package version "${value}" — use only letters, digits, ".", "+", ":", "~", "_" or "-".`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function safePackageName(value: string): string {
|
|
53
|
+
return shortName(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validateLinuxFormats(
|
|
57
|
+
formats: readonly string[],
|
|
58
|
+
source: string,
|
|
59
|
+
): LinuxPackageFormat[] {
|
|
60
|
+
const allowed = new Set<string>(LINUX_FORMATS);
|
|
61
|
+
const out: LinuxPackageFormat[] = [];
|
|
62
|
+
for (const format of formats) {
|
|
63
|
+
if (!allowed.has(format)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`[mirin] invalid Linux package format "${format}" in ${source}; expected appimage, deb, or rpm.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!out.includes(format as LinuxPackageFormat)) out.push(format as LinuxPackageFormat);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
@@ -25,136 +25,55 @@
|
|
|
25
25
|
* so it works on hosts without FUSE).
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { $ } from "bun";
|
|
29
28
|
import {
|
|
29
|
+
chmodSync,
|
|
30
30
|
cpSync,
|
|
31
|
+
existsSync,
|
|
31
32
|
mkdirSync,
|
|
32
33
|
rmSync,
|
|
33
|
-
writeFileSync,
|
|
34
|
-
chmodSync,
|
|
35
|
-
existsSync,
|
|
36
|
-
symlinkSync,
|
|
37
34
|
statSync,
|
|
35
|
+
symlinkSync,
|
|
36
|
+
writeFileSync,
|
|
38
37
|
} from "node:fs";
|
|
39
38
|
import { join } from "node:path";
|
|
40
|
-
import {
|
|
41
|
-
import { desktopEntry, resolveLinuxDesktopIconPng } from "
|
|
42
|
-
import
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
}
|
|
39
|
+
import { $ } from "bun";
|
|
40
|
+
import { desktopEntry, resolveLinuxDesktopIconPng } from "../../bundle/linux/index.ts";
|
|
41
|
+
import { DEFAULT_DEB_DEPENDS, DEFAULT_RPM_DEPENDS, LINUX_FORMATS } from "./lib/defaults.ts";
|
|
42
|
+
import { resolveAppimagetool, resolveFpm } from "./lib/tools.ts";
|
|
43
|
+
import { safeLinuxPackageInput, safePackageName, validateLinuxFormats } from "./lib/validation.ts";
|
|
44
|
+
import type {
|
|
45
|
+
LinuxConfig,
|
|
46
|
+
LinuxPackageFormat,
|
|
47
|
+
LinuxPackageInput,
|
|
48
|
+
LinuxPackageResult,
|
|
49
|
+
} from "./types.ts";
|
|
134
50
|
|
|
135
51
|
const size = (p: string) => statSync(p).size;
|
|
136
52
|
|
|
137
53
|
/** Build every requested Linux package; returns each artifact's path + size. */
|
|
138
|
-
export async function buildLinuxPackages(
|
|
139
|
-
input
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
54
|
+
export async function buildLinuxPackages(input: LinuxPackageInput): Promise<LinuxPackageResult[]> {
|
|
55
|
+
const safeInput = safeLinuxPackageInput(input);
|
|
56
|
+
mkdirSync(safeInput.outDir, { recursive: true });
|
|
57
|
+
const binName = safePackageName(
|
|
58
|
+
safeInput.options.binName ?? safeInput.bundleId.split(".").pop() ?? safeInput.appName,
|
|
59
|
+
);
|
|
145
60
|
// deb + rpm share one staged filesystem tree (payload under /opt, launcher,
|
|
146
|
-
// .desktop, icon). Build it once and reuse.
|
|
147
|
-
|
|
61
|
+
// .desktop, icon). Build it once and reuse. Package compressors only read the
|
|
62
|
+
// staged tree, so all requested formats can run concurrently.
|
|
63
|
+
const needFsTree = safeInput.formats.includes("deb") || safeInput.formats.includes("rpm");
|
|
148
64
|
let fsRoot: string | undefined;
|
|
149
65
|
try {
|
|
150
|
-
if (needFsTree) fsRoot = stageFsTree(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
66
|
+
if (needFsTree) fsRoot = stageFsTree(safeInput, binName);
|
|
67
|
+
return await Promise.all(
|
|
68
|
+
safeInput.formats.map((format) =>
|
|
69
|
+
format === "appimage"
|
|
70
|
+
? buildAppImage(safeInput, binName)
|
|
71
|
+
: buildFpm(safeInput, binName, fsRoot!, format),
|
|
72
|
+
),
|
|
73
|
+
);
|
|
154
74
|
} finally {
|
|
155
75
|
if (fsRoot) rmSync(fsRoot, { recursive: true, force: true });
|
|
156
76
|
}
|
|
157
|
-
return results;
|
|
158
77
|
}
|
|
159
78
|
|
|
160
79
|
/**
|
|
@@ -218,7 +137,7 @@ async function buildFpm(
|
|
|
218
137
|
kind: "deb" | "rpm",
|
|
219
138
|
): Promise<LinuxPackageResult> {
|
|
220
139
|
const fpm = resolveFpm();
|
|
221
|
-
const arch = kind
|
|
140
|
+
const arch = linuxPackageArch(kind);
|
|
222
141
|
const fileName =
|
|
223
142
|
kind === "deb"
|
|
224
143
|
? `${binName}_${input.version}_${arch}.deb`
|
|
@@ -228,25 +147,44 @@ async function buildFpm(
|
|
|
228
147
|
|
|
229
148
|
const depends =
|
|
230
149
|
kind === "deb"
|
|
231
|
-
? input.options.debDepends ?? DEFAULT_DEB_DEPENDS
|
|
232
|
-
: input.options.rpmDepends ?? DEFAULT_RPM_DEPENDS;
|
|
150
|
+
? (input.options.debDepends ?? DEFAULT_DEB_DEPENDS)
|
|
151
|
+
: (input.options.rpmDepends ?? DEFAULT_RPM_DEPENDS);
|
|
233
152
|
|
|
234
153
|
const args: string[] = [
|
|
235
|
-
"-s",
|
|
236
|
-
"
|
|
237
|
-
"-
|
|
238
|
-
|
|
239
|
-
"
|
|
240
|
-
|
|
241
|
-
"--
|
|
242
|
-
|
|
243
|
-
"--
|
|
244
|
-
|
|
245
|
-
"--
|
|
154
|
+
"-s",
|
|
155
|
+
"dir",
|
|
156
|
+
"-t",
|
|
157
|
+
kind,
|
|
158
|
+
"-C",
|
|
159
|
+
fsRoot,
|
|
160
|
+
"--package",
|
|
161
|
+
out,
|
|
162
|
+
"--name",
|
|
163
|
+
binName,
|
|
164
|
+
"--version",
|
|
165
|
+
input.version,
|
|
166
|
+
"--iteration",
|
|
167
|
+
"1",
|
|
168
|
+
"--architecture",
|
|
169
|
+
arch,
|
|
170
|
+
"--maintainer",
|
|
171
|
+
input.options.maintainer ?? input.publisher,
|
|
172
|
+
"--vendor",
|
|
173
|
+
input.publisher,
|
|
174
|
+
"--description",
|
|
175
|
+
input.options.description ?? input.appName,
|
|
246
176
|
];
|
|
247
177
|
if (input.options.homepage) args.push("--url", input.options.homepage);
|
|
248
178
|
if (input.options.license) args.push("--license", input.options.license);
|
|
249
179
|
for (const d of depends) args.push("--depends", d);
|
|
180
|
+
// CEF dominates the payload. Level 3 avoids spending minutes chasing small
|
|
181
|
+
// gains while retaining gzip compatibility across Debian and RPM families.
|
|
182
|
+
args.push(
|
|
183
|
+
kind === "deb" ? "--deb-compression" : "--rpm-compression",
|
|
184
|
+
kind === "deb" ? "gz" : "gzip",
|
|
185
|
+
kind === "deb" ? "--deb-compression-level" : "--rpm-compression-level",
|
|
186
|
+
"3",
|
|
187
|
+
);
|
|
250
188
|
// Package the whole staged tree ('.' relative to the -C chdir).
|
|
251
189
|
args.push(".");
|
|
252
190
|
|
|
@@ -304,7 +242,8 @@ async function buildAppImage(
|
|
|
304
242
|
stageHicolorIcon(appDirRoot, input);
|
|
305
243
|
}
|
|
306
244
|
|
|
307
|
-
const
|
|
245
|
+
const arch = linuxPackageArch("appimage");
|
|
246
|
+
const fileName = `${input.appName}-${input.version}-${arch}.AppImage`;
|
|
308
247
|
const out = join(input.outDir, fileName);
|
|
309
248
|
rmSync(out, { force: true });
|
|
310
249
|
|
|
@@ -312,7 +251,7 @@ async function buildAppImage(
|
|
|
312
251
|
// --appimage-extract-and-run: run appimagetool without FUSE. ARCH is required by
|
|
313
252
|
// appimagetool; NO_STRIP avoids stripping the Bun-compiled host (which would break it).
|
|
314
253
|
const res = await $`${appimagetool} --appimage-extract-and-run ${appDirRoot} ${out}`
|
|
315
|
-
.env({ ...process.env, ARCH:
|
|
254
|
+
.env({ ...process.env, ARCH: arch, NO_STRIP: "1" })
|
|
316
255
|
.nothrow()
|
|
317
256
|
.quiet();
|
|
318
257
|
rmSync(appDirRoot, { recursive: true, force: true });
|
|
@@ -323,12 +262,32 @@ async function buildAppImage(
|
|
|
323
262
|
return { format: "appimage", path: out, size: size(out) };
|
|
324
263
|
}
|
|
325
264
|
|
|
265
|
+
function linuxPackageArch(kind: "appimage" | "deb" | "rpm"): string {
|
|
266
|
+
if (process.arch === "arm64") {
|
|
267
|
+
return kind === "deb" ? "arm64" : "aarch64";
|
|
268
|
+
}
|
|
269
|
+
return kind === "deb" ? "amd64" : "x86_64";
|
|
270
|
+
}
|
|
271
|
+
|
|
326
272
|
/** Resolve the effective set of formats from config + an optional CLI/caller override. */
|
|
327
273
|
export function resolveLinuxFormats(
|
|
328
274
|
linux: boolean | LinuxConfig | undefined,
|
|
329
275
|
override?: LinuxPackageFormat[],
|
|
330
276
|
): LinuxPackageFormat[] {
|
|
331
|
-
if (override && override.length) return override;
|
|
277
|
+
if (override && override.length) return validateLinuxFormats(override, "CLI override");
|
|
332
278
|
const fromConfig = typeof linux === "object" ? linux.formats : undefined;
|
|
333
|
-
return fromConfig && fromConfig.length
|
|
279
|
+
return fromConfig && fromConfig.length
|
|
280
|
+
? validateLinuxFormats(fromConfig, "mirin.config.ts linux.formats")
|
|
281
|
+
: [...LINUX_FORMATS];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function parseLinuxFormats(raw: string): LinuxPackageFormat[] {
|
|
285
|
+
const formats = raw
|
|
286
|
+
.split(",")
|
|
287
|
+
.map((s) => s.trim())
|
|
288
|
+
.filter(Boolean);
|
|
289
|
+
if (!formats.length) {
|
|
290
|
+
throw new Error("[mirin] --linux-target needs at least one format: appimage, deb, or rpm.");
|
|
291
|
+
}
|
|
292
|
+
return validateLinuxFormats(formats, "--linux-target");
|
|
334
293
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { LinuxConfig, LinuxPackageFormat } from "mirinjs";
|
|
2
|
+
|
|
3
|
+
export type { LinuxConfig, LinuxPackageFormat };
|
|
4
|
+
|
|
5
|
+
export interface LinuxPackageInput {
|
|
6
|
+
/** The assembled Linux app folder (build/<App>): the whole relocatable payload. */
|
|
7
|
+
appDir: string;
|
|
8
|
+
/** App display name; also the host-binary stem inside `appDir`. */
|
|
9
|
+
appName: string;
|
|
10
|
+
/** Reverse-DNS app id, e.g. "dev.netko.anko" (install prefix + `.desktop`/icon name). */
|
|
11
|
+
bundleId: string;
|
|
12
|
+
version: string;
|
|
13
|
+
/** Publisher / company (deb/rpm vendor + maintainer fallback). */
|
|
14
|
+
publisher: string;
|
|
15
|
+
/** Directory the artifacts are written into. */
|
|
16
|
+
outDir: string;
|
|
17
|
+
projectDir: string;
|
|
18
|
+
/** Abs path to the app icon source (`.png` / `.iconset`); resolved to a PNG. */
|
|
19
|
+
icon?: string;
|
|
20
|
+
/** Resolved Linux config (maintainer, depends, category, ...). */
|
|
21
|
+
options: LinuxConfig;
|
|
22
|
+
/** Which formats to build. */
|
|
23
|
+
formats: LinuxPackageFormat[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LinuxPackageResult {
|
|
27
|
+
format: LinuxPackageFormat;
|
|
28
|
+
path: string;
|
|
29
|
+
size: number;
|
|
30
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { rmSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
import type { BuildResult } from "../build.ts";
|
|
5
|
+
import { buildDmg, type DmgOptions, notarizeAndStaple } from "../dmg.ts";
|
|
6
|
+
import { buildInnoInstaller, hasInno } from "../installer-inno.ts";
|
|
7
|
+
import { buildNsisInstaller, hasMakensis } from "../installer-win.ts";
|
|
8
|
+
import { buildLinuxPackages, resolveLinuxFormats } from "../package/linux/index.ts";
|
|
9
|
+
|
|
10
|
+
export interface ReleaseInstallerResult {
|
|
11
|
+
installerName?: string;
|
|
12
|
+
installerSize: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ReleaseInstallerInput {
|
|
16
|
+
result: BuildResult;
|
|
17
|
+
buildDir: string;
|
|
18
|
+
outDir: string;
|
|
19
|
+
appArtifact: string;
|
|
20
|
+
prefix: string;
|
|
21
|
+
safeName: string;
|
|
22
|
+
isWindows: boolean;
|
|
23
|
+
isLinux: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Build first-install artifacts independently from updater compression and deltas. */
|
|
27
|
+
export async function buildReleaseInstaller(
|
|
28
|
+
input: ReleaseInstallerInput,
|
|
29
|
+
): Promise<ReleaseInstallerResult> {
|
|
30
|
+
const { result, isWindows, isLinux } = input;
|
|
31
|
+
if (isWindows) return buildWindowsInstaller(input);
|
|
32
|
+
if (isLinux) return buildLinuxInstallers(input);
|
|
33
|
+
if (result.dmg !== false) return buildMacInstaller(input);
|
|
34
|
+
return { installerSize: 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function buildWindowsInstaller(
|
|
38
|
+
input: ReleaseInstallerInput,
|
|
39
|
+
): Promise<ReleaseInstallerResult> {
|
|
40
|
+
const { result, buildDir, outDir, appArtifact, prefix, safeName } = input;
|
|
41
|
+
const setupName = `${prefix}-${safeName}-setup.exe`;
|
|
42
|
+
const installerArgs = {
|
|
43
|
+
appDir: result.app,
|
|
44
|
+
appName: result.appName,
|
|
45
|
+
exeName: `${result.appName}.exe`,
|
|
46
|
+
version: result.version,
|
|
47
|
+
bundleId: result.bundleId,
|
|
48
|
+
outDir,
|
|
49
|
+
fileName: setupName,
|
|
50
|
+
projectDir: result.projectDir,
|
|
51
|
+
};
|
|
52
|
+
const innoWanted = result.inno !== false;
|
|
53
|
+
const nsisWanted = result.nsis !== false;
|
|
54
|
+
const innoOpts = typeof result.inno === "object" ? result.inno : {};
|
|
55
|
+
const nsisOpts = typeof result.nsis === "object" ? result.nsis : {};
|
|
56
|
+
let exePath: string | undefined;
|
|
57
|
+
|
|
58
|
+
if (innoWanted && hasInno()) {
|
|
59
|
+
exePath = await buildInnoInstaller({
|
|
60
|
+
...installerArgs,
|
|
61
|
+
options: { ...innoOpts, publisher: innoOpts.publisher ?? result.publisher },
|
|
62
|
+
});
|
|
63
|
+
} else if (nsisWanted && (await hasMakensis())) {
|
|
64
|
+
exePath = await buildNsisInstaller({
|
|
65
|
+
...installerArgs,
|
|
66
|
+
options: { ...nsisOpts, publisher: nsisOpts.publisher ?? result.publisher },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (exePath) return { installerName: setupName, installerSize: statSync(exePath).size };
|
|
71
|
+
if (innoWanted || nsisWanted) {
|
|
72
|
+
console.warn(
|
|
73
|
+
"[mirin release] no installer toolchain found — shipping the portable .zip. " +
|
|
74
|
+
"Install Inno Setup (`scoop install inno-setup`) or NSIS (`scoop install nsis`).",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const installerName = `${prefix}-${safeName}.zip`;
|
|
79
|
+
const zipPath = join(outDir, installerName);
|
|
80
|
+
console.log(`[mirin release] zipping → ${installerName}`);
|
|
81
|
+
rmSync(zipPath, { force: true });
|
|
82
|
+
await $`tar -a -cf ${zipPath} -C ${buildDir} ${appArtifact}`;
|
|
83
|
+
return { installerName, installerSize: statSync(zipPath).size };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function buildLinuxInstallers(input: ReleaseInstallerInput): Promise<ReleaseInstallerResult> {
|
|
87
|
+
const { result, outDir } = input;
|
|
88
|
+
if (result.linux === false) return { installerSize: 0 };
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const formats = resolveLinuxFormats(result.linux);
|
|
92
|
+
console.log(`[mirin release] building Linux packages (${formats.join(", ")})…`);
|
|
93
|
+
const packages = await buildLinuxPackages({
|
|
94
|
+
appDir: result.app,
|
|
95
|
+
appName: result.appName,
|
|
96
|
+
bundleId: result.bundleId,
|
|
97
|
+
version: result.version,
|
|
98
|
+
publisher: result.publisher,
|
|
99
|
+
outDir,
|
|
100
|
+
projectDir: result.projectDir,
|
|
101
|
+
icon: result.icon,
|
|
102
|
+
options: typeof result.linux === "object" ? result.linux : {},
|
|
103
|
+
formats,
|
|
104
|
+
});
|
|
105
|
+
const primary = packages.find((artifact) => artifact.format === "appimage") ?? packages[0];
|
|
106
|
+
for (const artifact of packages) {
|
|
107
|
+
if (artifact !== primary) {
|
|
108
|
+
console.log(` ${basename(artifact.path)} (${(artifact.size / 1e6).toFixed(1)} MB)`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return primary
|
|
112
|
+
? { installerName: basename(primary.path), installerSize: primary.size }
|
|
113
|
+
: { installerSize: 0 };
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`\n[mirin release] Linux packaging failed: ${error instanceof Error ? error.message : error}\n` +
|
|
117
|
+
"[mirin release] shipping the updater bundle + manifest WITHOUT packages.\n",
|
|
118
|
+
);
|
|
119
|
+
return { installerSize: 0 };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function buildMacInstaller(input: ReleaseInstallerInput): Promise<ReleaseInstallerResult> {
|
|
124
|
+
const { result, outDir, prefix, safeName } = input;
|
|
125
|
+
const installerName = `${prefix}-${safeName}.dmg`;
|
|
126
|
+
console.log(`[mirin release] building installer → ${installerName}`);
|
|
127
|
+
try {
|
|
128
|
+
const options: DmgOptions = typeof result.dmg === "object" ? result.dmg : {};
|
|
129
|
+
const path = await buildDmg({
|
|
130
|
+
app: result.app,
|
|
131
|
+
appName: result.appName,
|
|
132
|
+
outDir,
|
|
133
|
+
fileName: installerName,
|
|
134
|
+
options,
|
|
135
|
+
projectDir: result.projectDir,
|
|
136
|
+
signIdentity: result.signIdentity,
|
|
137
|
+
});
|
|
138
|
+
await notarizeAndStaple(path);
|
|
139
|
+
return { installerName, installerSize: statSync(path).size };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.warn(
|
|
142
|
+
`\n[mirin release] DMG build failed: ${error instanceof Error ? error.message : error}\n` +
|
|
143
|
+
"[mirin release] shipping the updater bundle + manifest WITHOUT a .dmg installer.\n",
|
|
144
|
+
);
|
|
145
|
+
rmSync(join(outDir, installerName), { force: true });
|
|
146
|
+
return { installerSize: 0 };
|
|
147
|
+
}
|
|
148
|
+
}
|