@mirinjs/cli 0.1.0-alpha.14 → 0.1.0-alpha.15
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 +6 -6
- package/src/artifacts.ts +122 -13
- package/src/build.ts +85 -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} +79 -128
- 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 +126 -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
|
/**
|
|
@@ -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
|
|
|
@@ -328,7 +266,20 @@ export function resolveLinuxFormats(
|
|
|
328
266
|
linux: boolean | LinuxConfig | undefined,
|
|
329
267
|
override?: LinuxPackageFormat[],
|
|
330
268
|
): LinuxPackageFormat[] {
|
|
331
|
-
if (override && override.length) return override;
|
|
269
|
+
if (override && override.length) return validateLinuxFormats(override, "CLI override");
|
|
332
270
|
const fromConfig = typeof linux === "object" ? linux.formats : undefined;
|
|
333
|
-
return fromConfig && fromConfig.length
|
|
271
|
+
return fromConfig && fromConfig.length
|
|
272
|
+
? validateLinuxFormats(fromConfig, "mirin.config.ts linux.formats")
|
|
273
|
+
: [...LINUX_FORMATS];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function parseLinuxFormats(raw: string): LinuxPackageFormat[] {
|
|
277
|
+
const formats = raw
|
|
278
|
+
.split(",")
|
|
279
|
+
.map((s) => s.trim())
|
|
280
|
+
.filter(Boolean);
|
|
281
|
+
if (!formats.length) {
|
|
282
|
+
throw new Error("[mirin] --linux-target needs at least one format: appimage, deb, or rpm.");
|
|
283
|
+
}
|
|
284
|
+
return validateLinuxFormats(formats, "--linux-target");
|
|
334
285
|
}
|
|
@@ -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
|
+
}
|