@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,91 @@
|
|
|
1
|
+
import { cpSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const CEF_LOCALE = /^[A-Za-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})*$/;
|
|
5
|
+
const MAC_VARIANTS = new Set(["FEMININE", "MASCULINE", "NEUTER"]);
|
|
6
|
+
|
|
7
|
+
/** Validate and normalize an optional CEF locale allowlist. */
|
|
8
|
+
export function normalizeCefLocales(locales: unknown): string[] | undefined {
|
|
9
|
+
if (locales == null) return undefined;
|
|
10
|
+
if (!Array.isArray(locales) || locales.length === 0) {
|
|
11
|
+
throw new Error("[mirin] cef.locales must contain at least one locale.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const seen = new Set<string>();
|
|
15
|
+
const normalized: string[] = [];
|
|
16
|
+
for (const value of locales) {
|
|
17
|
+
if (typeof value !== "string" || !CEF_LOCALE.test(value)) {
|
|
18
|
+
throw new Error(`[mirin] invalid CEF locale ${JSON.stringify(value)}.`);
|
|
19
|
+
}
|
|
20
|
+
const locale = value.replaceAll("_", "-");
|
|
21
|
+
const key = locale.toLowerCase();
|
|
22
|
+
if (!seen.has(key)) {
|
|
23
|
+
seen.add(key);
|
|
24
|
+
normalized.push(locale);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Copy selected Windows/Linux `locales/<locale>.pak` files, or every locale when unset. */
|
|
31
|
+
export function copyFlatCefLocales(
|
|
32
|
+
source: string,
|
|
33
|
+
destination: string,
|
|
34
|
+
locales: string[] | undefined,
|
|
35
|
+
): void {
|
|
36
|
+
if (!locales) {
|
|
37
|
+
cpSync(source, destination, { recursive: true });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const entries = readdirSync(source, { withFileTypes: true }).filter(
|
|
42
|
+
(entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".pak"),
|
|
43
|
+
);
|
|
44
|
+
const byLocale = new Map(
|
|
45
|
+
entries.map((entry) => [localeKey(entry.name.slice(0, -".pak".length)), entry.name]),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
mkdirSync(destination, { recursive: true });
|
|
49
|
+
for (const locale of locales) {
|
|
50
|
+
const file = byLocale.get(localeKey(locale));
|
|
51
|
+
if (!file) throw new Error(`[mirin] CEF locale "${locale}" is not available in ${source}.`);
|
|
52
|
+
cpSync(join(source, file), join(destination, file));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Remove unselected macOS `.lproj` locale directories from a copied CEF framework. */
|
|
57
|
+
export function pruneMacCefLocales(framework: string, locales: string[] | undefined): void {
|
|
58
|
+
if (!locales) return;
|
|
59
|
+
|
|
60
|
+
const resources = join(framework, "Resources");
|
|
61
|
+
const localeDirs = readdirSync(resources, { withFileTypes: true })
|
|
62
|
+
.filter((entry) => entry.isDirectory() && entry.name.endsWith(".lproj"))
|
|
63
|
+
.map((entry) => entry.name);
|
|
64
|
+
const available = new Set(localeDirs.map((name) => name.slice(0, -".lproj".length)));
|
|
65
|
+
const keep = new Set<string>();
|
|
66
|
+
|
|
67
|
+
for (const locale of locales) {
|
|
68
|
+
const requested = locale.replaceAll("-", "_");
|
|
69
|
+
const language = requested.split("_", 1)[0] ?? requested;
|
|
70
|
+
const root = available.has(requested)
|
|
71
|
+
? requested
|
|
72
|
+
: available.has(language)
|
|
73
|
+
? language
|
|
74
|
+
: undefined;
|
|
75
|
+
if (!root) throw new Error(`[mirin] CEF locale "${locale}" is not available in ${resources}.`);
|
|
76
|
+
|
|
77
|
+
keep.add(`${root}.lproj`);
|
|
78
|
+
for (const variant of MAC_VARIANTS) {
|
|
79
|
+
const name = `${root}_${variant}.lproj`;
|
|
80
|
+
if (localeDirs.includes(name)) keep.add(name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const directory of localeDirs) {
|
|
85
|
+
if (!keep.has(directory)) rmSync(join(resources, directory), { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function localeKey(locale: string): string {
|
|
90
|
+
return locale.replaceAll("_", "-").toLowerCase();
|
|
91
|
+
}
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
* what mirin-core derives at runtime (engine::derive_subprocess_path, host.ts).
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { cpSync,
|
|
19
|
-
import {
|
|
20
|
-
import { makeWindowsIcon } from "
|
|
18
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { extname, join } from "node:path";
|
|
20
|
+
import { makeWindowsIcon } from "../../icons/windows/index.ts";
|
|
21
|
+
import { copyFlatCefLocales } from "../shared/cef-locales.ts";
|
|
21
22
|
|
|
22
23
|
/** Source dir (vendor/cef on Windows) must contain libcef.dll. */
|
|
23
24
|
const CEF_MARKER = "libcef.dll";
|
|
@@ -26,7 +27,7 @@ const CEF_MARKER = "libcef.dll";
|
|
|
26
27
|
* distribution (libcef.lib, include/, cmake/, archive.json, bootstrap*.exe) is
|
|
27
28
|
* build-time only and intentionally left out. */
|
|
28
29
|
const CEF_RUNTIME_EXTS = new Set([".dll", ".bin", ".pak", ".dat"]);
|
|
29
|
-
const CEF_RUNTIME_EXTRAS = ["vk_swiftshader_icd.json"
|
|
30
|
+
const CEF_RUNTIME_EXTRAS = ["vk_swiftshader_icd.json"];
|
|
30
31
|
|
|
31
32
|
export interface WinBundleOptions {
|
|
32
33
|
appName: string; // also the host exe stem → <appName>.exe
|
|
@@ -35,6 +36,8 @@ export interface WinBundleOptions {
|
|
|
35
36
|
coreDll: string; // mirin_core.dll
|
|
36
37
|
helperExe: string; // mirin-helper.exe
|
|
37
38
|
cefPath: string; // dir containing the Windows CEF runtime (vendor/cef)
|
|
39
|
+
/** Production CEF locale allowlist. Undefined keeps every locale. */
|
|
40
|
+
cefLocales?: string[];
|
|
38
41
|
/** App icon source (.ico / .iconset / .png) → <App>/icon.ico, set as the window icon. */
|
|
39
42
|
icon?: string;
|
|
40
43
|
/** Production-only resources placed under <App>/resources. */
|
|
@@ -66,7 +69,7 @@ export async function buildWindowsBundle(
|
|
|
66
69
|
cpSync(opts.coreDll, join(app, "mirin_core.dll"));
|
|
67
70
|
cpSync(opts.helperExe, join(app, "mirin-helper.exe"));
|
|
68
71
|
|
|
69
|
-
copyCefRuntime(cefPath, app);
|
|
72
|
+
copyCefRuntime(cefPath, app, opts.cefLocales);
|
|
70
73
|
|
|
71
74
|
// App icon → <App>/icon.ico, loaded at runtime by the core and set as the window
|
|
72
75
|
// icon (taskbar / Alt-Tab / title). Sits beside the exe so the core finds it.
|
|
@@ -105,7 +108,7 @@ export async function buildWindowsBundle(
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/** Copy the CEF runtime (dlls, paks, snapshot, icu data, locales) into `dest`. */
|
|
108
|
-
function copyCefRuntime(cefPath: string, dest: string): void {
|
|
111
|
+
function copyCefRuntime(cefPath: string, dest: string, locales: string[] | undefined): void {
|
|
109
112
|
for (const entry of readdirSync(cefPath)) {
|
|
110
113
|
if (CEF_RUNTIME_EXTS.has(extname(entry).toLowerCase())) {
|
|
111
114
|
cpSync(join(cefPath, entry), join(dest, entry));
|
|
@@ -115,4 +118,5 @@ function copyCefRuntime(cefPath: string, dest: string): void {
|
|
|
115
118
|
const src = join(cefPath, extra);
|
|
116
119
|
if (existsSync(src)) cpSync(src, join(dest, extra), { recursive: true });
|
|
117
120
|
}
|
|
121
|
+
copyFlatCefLocales(join(cefPath, "locales"), join(dest, "locales"), locales);
|
|
118
122
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./app.ts";
|
package/src/dev.ts
CHANGED
|
@@ -8,25 +8,26 @@
|
|
|
8
8
|
* 5. launch the app pointed at the Vite URL, with RPC injected into the webview
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import { mkdirSync, rmSync, symlinkSync, existsSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
13
12
|
import { createServer } from "node:net";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { $ } from "bun";
|
|
16
|
+
import { resolveArtifacts } from "./artifacts.ts";
|
|
17
17
|
import {
|
|
18
18
|
buildLinuxBundle,
|
|
19
|
-
resolveLinuxIconPng,
|
|
20
|
-
resolveLinuxDesktopIconPng,
|
|
21
19
|
desktopEntry,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
resolveLinuxDesktopIconPng,
|
|
21
|
+
resolveLinuxIconPng,
|
|
22
|
+
} from "./bundle/linux/index.ts";
|
|
23
|
+
import { buildAppBundle } from "./bundle/macos/index.ts";
|
|
24
|
+
import { buildWindowsBundle } from "./bundle/windows/index.ts";
|
|
25
25
|
|
|
26
26
|
const IS_WINDOWS = process.platform === "win32";
|
|
27
27
|
const IS_LINUX = process.platform === "linux";
|
|
28
|
+
|
|
29
|
+
import { compileWorkers, normalizeSidecars } from "./extras.ts";
|
|
28
30
|
import { sweepBuildTemps } from "./temps.ts";
|
|
29
|
-
import { normalizeSidecars, compileWorkers } from "./extras.ts";
|
|
30
31
|
|
|
31
32
|
/** Vite's default port. `mirin dev` probes upward from here for a free one, so a
|
|
32
33
|
* second dev session (or anything already on 5173) doesn't collide. */
|
|
@@ -87,26 +88,26 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
|
|
|
87
88
|
icon: iconSrc,
|
|
88
89
|
})
|
|
89
90
|
: IS_LINUX
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
91
|
+
? await buildLinuxBundle({
|
|
92
|
+
appName,
|
|
93
|
+
outDir: work,
|
|
94
|
+
hostExe,
|
|
95
|
+
coreDll: artifacts.coreDylib,
|
|
96
|
+
helperExe: artifacts.helperBin,
|
|
97
|
+
cefPath: artifacts.cefPath,
|
|
98
|
+
icon: iconSrc,
|
|
99
|
+
})
|
|
100
|
+
: await buildAppBundle({
|
|
101
|
+
appName,
|
|
102
|
+
bundleId,
|
|
103
|
+
outDir: work,
|
|
104
|
+
hostExe,
|
|
105
|
+
coreDylib: artifacts.coreDylib,
|
|
106
|
+
helperBin: artifacts.helperBin,
|
|
107
|
+
cefPath: artifacts.cefPath,
|
|
108
|
+
icon: iconSrc,
|
|
109
|
+
urlSchemes: config.urlSchemes,
|
|
110
|
+
});
|
|
110
111
|
|
|
111
112
|
// Linux: install a dev `.desktop` entry so cosmic's dock can resolve the app's
|
|
112
113
|
// icon. cosmic matches a running X11 window's `WM_CLASS` (which the core sets to
|
|
@@ -138,7 +139,17 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
|
|
|
138
139
|
const devUrl = `http://127.0.0.1:${port}`;
|
|
139
140
|
console.log(`[mirin dev] starting Vite dev server on ${devUrl}…`);
|
|
140
141
|
const vite = Bun.spawn(
|
|
141
|
-
[
|
|
142
|
+
[
|
|
143
|
+
"bunx",
|
|
144
|
+
"vite",
|
|
145
|
+
"--clearScreen",
|
|
146
|
+
"false",
|
|
147
|
+
"--host",
|
|
148
|
+
"127.0.0.1",
|
|
149
|
+
"--port",
|
|
150
|
+
String(port),
|
|
151
|
+
"--strictPort",
|
|
152
|
+
],
|
|
142
153
|
{
|
|
143
154
|
cwd: projectDir,
|
|
144
155
|
stdio: ["ignore", "inherit", "inherit"],
|
|
@@ -155,13 +166,17 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
|
|
|
155
166
|
MIRIN_CORE: IS_WINDOWS
|
|
156
167
|
? join(app, "mirin_core.dll")
|
|
157
168
|
: IS_LINUX
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
? join(app, "libmirin_core.so")
|
|
170
|
+
: join(app, "Contents", "MacOS", "libmirin_core.dylib"),
|
|
160
171
|
// Linux: the flat app dir holds libcef.so + libmirin_core.so; put it on the
|
|
161
172
|
// loader path so the host's dlopen and libmirin_core's NEEDED libcef.so (and
|
|
162
173
|
// the spawned mirin-helper) all resolve. (macOS uses the framework loader;
|
|
163
174
|
// Windows the exe-dir search — neither needs this.)
|
|
164
|
-
...(IS_LINUX
|
|
175
|
+
...(IS_LINUX
|
|
176
|
+
? {
|
|
177
|
+
LD_LIBRARY_PATH: `${app}${process.env.LD_LIBRARY_PATH ? ":" + process.env.LD_LIBRARY_PATH : ""}`,
|
|
178
|
+
}
|
|
179
|
+
: {}),
|
|
165
180
|
MIRIN_WORKER: workerJs,
|
|
166
181
|
MIRIN_DEV_URL: devUrl,
|
|
167
182
|
MIRIN_MANIFEST_JSON: JSON.stringify({ id: bundleId, windows: config.windows }),
|
package/src/dmg.ts
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
* stapled so a fresh download opens without a Gatekeeper prompt.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
import { cpSync, mkdirSync, rmSync, symlinkSync, existsSync } from "node:fs";
|
|
20
|
-
import { join, basename, isAbsolute } from "node:path";
|
|
18
|
+
import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
21
19
|
import { tmpdir } from "node:os";
|
|
20
|
+
import { basename, isAbsolute, join } from "node:path";
|
|
21
|
+
import { $ } from "bun";
|
|
22
22
|
|
|
23
23
|
export interface DmgOptions {
|
|
24
24
|
volumeName?: string;
|
|
@@ -148,12 +148,20 @@ export async function buildDmg(input: BuildDmgInput): Promise<string> {
|
|
|
148
148
|
let built = false;
|
|
149
149
|
if (wantsLayout(options)) {
|
|
150
150
|
try {
|
|
151
|
-
await buildLaidOutDmg({
|
|
151
|
+
await buildLaidOutDmg({
|
|
152
|
+
staging,
|
|
153
|
+
vol,
|
|
154
|
+
format,
|
|
155
|
+
dmgPath,
|
|
156
|
+
options,
|
|
157
|
+
projectDir,
|
|
158
|
+
appFile: basename(app),
|
|
159
|
+
});
|
|
152
160
|
built = true;
|
|
153
161
|
} catch (e) {
|
|
154
162
|
console.warn(
|
|
155
163
|
`[mirin release] DMG layout failed (${e instanceof Error ? e.message : e}); ` +
|
|
156
|
-
|
|
164
|
+
"falling back to a plain DMG.",
|
|
157
165
|
);
|
|
158
166
|
rmSync(dmgPath, { force: true });
|
|
159
167
|
}
|
|
@@ -190,9 +198,14 @@ async function buildLaidOutDmg(args: {
|
|
|
190
198
|
const { staging, vol, format, dmgPath, options, projectDir, appFile } = args;
|
|
191
199
|
const win = options.windowSize ?? { width: 640, height: 400 };
|
|
192
200
|
const iconSize = options.iconSize ?? 128;
|
|
193
|
-
const appPos = options.appPosition ?? {
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
const appPos = options.appPosition ?? {
|
|
202
|
+
x: Math.round(win.width * 0.25),
|
|
203
|
+
y: Math.round(win.height * 0.5),
|
|
204
|
+
};
|
|
205
|
+
const appsPos = options.applicationsPosition ?? {
|
|
206
|
+
x: Math.round(win.width * 0.75),
|
|
207
|
+
y: Math.round(win.height * 0.5),
|
|
208
|
+
};
|
|
196
209
|
|
|
197
210
|
// Stage a background image (if any) under a hidden folder Finder can reference.
|
|
198
211
|
let bgFile: string | undefined;
|
|
@@ -214,8 +227,7 @@ async function buildLaidOutDmg(args: {
|
|
|
214
227
|
await $`hdiutil create -volname ${vol} -srcfolder ${staging} -fs HFS+ -format UDRW -ov ${rw}`.quiet();
|
|
215
228
|
|
|
216
229
|
// Attach without auto-opening a Finder window; parse the real mountpoint.
|
|
217
|
-
const attach =
|
|
218
|
-
await $`hdiutil attach ${rw} -readwrite -noverify -noautoopen`.text();
|
|
230
|
+
const attach = await $`hdiutil attach ${rw} -readwrite -noverify -noautoopen`.text();
|
|
219
231
|
const mount = attach
|
|
220
232
|
.split("\n")
|
|
221
233
|
.map((l) => l.trim())
|
package/src/extras.ts
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* place so the two CLI paths stay in sync.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
10
|
import { $ } from "bun";
|
|
9
|
-
import { join } from "node:path";
|
|
10
11
|
|
|
11
|
-
/** Config shapes (mirrors packages/mirin/src/config.ts; CLI can't import the runtime). */
|
|
12
|
+
/** Config shapes (mirrors packages/mirin/src/config/index.ts; CLI can't import the runtime). */
|
|
12
13
|
export type SidecarConfig = Record<string, string | { bin: string; entitlements?: string[] }>;
|
|
13
14
|
export type WorkersConfig = Record<string, string>;
|
|
14
15
|
|
|
@@ -21,16 +22,47 @@ export interface NormalizedSidecar {
|
|
|
21
22
|
entitlements: string[];
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
const SAFE_EXTRA_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extra asset names become bundle filenames, so keep them as path segments. This
|
|
29
|
+
* prevents config keys such as "../tool" from escaping resources/sidecars or
|
|
30
|
+
* resources/workers in dev and production bundles.
|
|
31
|
+
*/
|
|
32
|
+
export function safeExtraAssetName(name: string, label: string): string {
|
|
33
|
+
if (!SAFE_EXTRA_NAME.test(name) || name === "." || name === "..") {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`[mirin] invalid ${label} "${name}" — use letters, digits, ".", "_" or "-", ` +
|
|
36
|
+
"and do not include path separators.",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function projectRelativePath(projectDir: string, file: string, label: string): string {
|
|
43
|
+
if (isAbsolute(file)) {
|
|
44
|
+
throw new Error(`[mirin] ${label} must be relative to the project root: ${file}`);
|
|
45
|
+
}
|
|
46
|
+
const root = resolve(projectDir);
|
|
47
|
+
const full = resolve(root, file);
|
|
48
|
+
const rel = relative(root, full);
|
|
49
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
50
|
+
throw new Error(`[mirin] ${label} escapes the project root: ${file}`);
|
|
51
|
+
}
|
|
52
|
+
return full;
|
|
53
|
+
}
|
|
54
|
+
|
|
24
55
|
/** Resolve `config.sidecars` to absolute source paths. */
|
|
25
56
|
export function normalizeSidecars(
|
|
26
57
|
projectDir: string,
|
|
27
58
|
sidecars: SidecarConfig | undefined,
|
|
28
59
|
): NormalizedSidecar[] {
|
|
29
60
|
return Object.entries(sidecars ?? {}).map(([name, value]) => {
|
|
61
|
+
const safeName = safeExtraAssetName(name, "sidecar name");
|
|
30
62
|
const spec = typeof value === "string" ? { bin: value } : value;
|
|
31
63
|
return {
|
|
32
|
-
name,
|
|
33
|
-
src:
|
|
64
|
+
name: safeName,
|
|
65
|
+
src: projectRelativePath(projectDir, spec.bin, `sidecar "${safeName}" path`),
|
|
34
66
|
entitlements: spec.entitlements ?? [],
|
|
35
67
|
};
|
|
36
68
|
});
|
|
@@ -46,16 +78,21 @@ export async function compileWorkers(
|
|
|
46
78
|
outDir: string,
|
|
47
79
|
minify: boolean,
|
|
48
80
|
): Promise<Record<string, string>> {
|
|
81
|
+
mkdirSync(outDir, { recursive: true });
|
|
49
82
|
const out: Record<string, string> = {};
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
83
|
+
const compiled = Object.entries(workers ?? {}).map(async ([name, entry]) => {
|
|
84
|
+
const safeName = safeExtraAssetName(name, "worker name");
|
|
85
|
+
const js = join(outDir, `${safeName}.js`);
|
|
86
|
+
const src = projectRelativePath(projectDir, entry, `worker "${safeName}" entry`);
|
|
53
87
|
if (minify) {
|
|
54
88
|
await $`bun build ${src} --target=bun --minify --outfile ${js}`.cwd(projectDir);
|
|
55
89
|
} else {
|
|
56
90
|
await $`bun build ${src} --target=bun --outfile ${js}`.cwd(projectDir);
|
|
57
91
|
}
|
|
58
|
-
|
|
92
|
+
return { name: safeName, js };
|
|
93
|
+
});
|
|
94
|
+
for (const worker of await Promise.all(compiled)) {
|
|
95
|
+
out[worker.name] = worker.js;
|
|
59
96
|
}
|
|
60
97
|
return out;
|
|
61
98
|
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { inflateSync } from "node:zlib";
|
|
12
11
|
import { join } from "node:path";
|
|
12
|
+
import { inflateSync } from "node:zlib";
|
|
13
13
|
|
|
14
14
|
/** Standard Windows icon sizes; intermediate sizes are downscaled from these by
|
|
15
15
|
* the shell, so the 256 entry keeps large views crisp. */
|
|
@@ -57,7 +57,8 @@ function decodePng(buf: Buffer): { width: number; height: number; rgba: Buffer }
|
|
|
57
57
|
if (bitDepth !== 8 || interlace !== 0) {
|
|
58
58
|
throw new Error(`unsupported PNG (depth ${bitDepth}, interlace ${interlace})`);
|
|
59
59
|
}
|
|
60
|
-
const channels =
|
|
60
|
+
const channels =
|
|
61
|
+
colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
|
|
61
62
|
if (!channels) throw new Error(`unsupported color type ${colorType}`);
|
|
62
63
|
|
|
63
64
|
const raw = inflateSync(Buffer.concat(idat));
|
|
@@ -239,7 +240,9 @@ export function makeWindowsIcon(
|
|
|
239
240
|
entries.push({ size: width, data: dibEntry(width, rgba), png: false });
|
|
240
241
|
}
|
|
241
242
|
} catch (e) {
|
|
242
|
-
console.warn(
|
|
243
|
+
console.warn(
|
|
244
|
+
`[mirin] icon: cannot decode ${iconSrc} (${e instanceof Error ? e.message : e})`,
|
|
245
|
+
);
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./icon.ts";
|
package/src/index.ts
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { scaffold } from "create-mirinjs";
|
|
8
8
|
import { build } from "./build.ts";
|
|
9
|
+
import { dev } from "./dev.ts";
|
|
10
|
+
import { parseLinuxFormats } from "./package/linux/index.ts";
|
|
9
11
|
import { release } from "./release.ts";
|
|
10
|
-
import { scaffold } from "create-mirinjs";
|
|
11
|
-
import type { LinuxPackageFormat } from "mirinjs";
|
|
12
12
|
|
|
13
13
|
/** Minimal flag parser: `--k=v`, `--k v`, and boolean `--k`; the rest are positionals. */
|
|
14
|
-
function parseArgs(argv: string[]): {
|
|
14
|
+
function parseArgs(argv: string[]): {
|
|
15
|
+
positionals: string[];
|
|
16
|
+
opts: Record<string, string | boolean>;
|
|
17
|
+
} {
|
|
15
18
|
const opts: Record<string, string | boolean> = {};
|
|
16
19
|
const positionals: string[] = [];
|
|
17
20
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -65,9 +68,7 @@ switch (command) {
|
|
|
65
68
|
case "build": {
|
|
66
69
|
const version = typeof opts.version === "string" ? opts.version : undefined;
|
|
67
70
|
const target = typeof opts["linux-target"] === "string" ? opts["linux-target"] : undefined;
|
|
68
|
-
const linuxFormats = target
|
|
69
|
-
? (target.split(",").map((s) => s.trim()).filter(Boolean) as LinuxPackageFormat[])
|
|
70
|
-
: undefined;
|
|
71
|
+
const linuxFormats = target ? parseLinuxFormats(target) : undefined;
|
|
71
72
|
const packageLinux = opts.linux === true || linuxFormats != null;
|
|
72
73
|
await build(process.cwd(), { version, packageLinux, linuxFormats });
|
|
73
74
|
process.exit(0);
|
package/src/installer-inno.ts
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* back to NSIS / the portable `.zip` when it's absent.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { $ } from "bun";
|
|
14
13
|
import { existsSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
-
import {
|
|
14
|
+
import { isAbsolute, join } from "node:path";
|
|
15
|
+
import { $ } from "bun";
|
|
16
16
|
import type { InnoConfig } from "mirinjs";
|
|
17
|
-
import { makeWindowsIcon } from "./
|
|
17
|
+
import { makeWindowsIcon } from "./icons/windows/index.ts";
|
|
18
18
|
|
|
19
19
|
export interface BuildInnoInput {
|
|
20
20
|
appDir: string; // the assembled app folder (build/<App>)
|
|
@@ -39,7 +39,8 @@ const v = (s: string) => s.replace(/\{/g, "{{");
|
|
|
39
39
|
|
|
40
40
|
/** Build the installer with `iscc`, returning the setup.exe path. */
|
|
41
41
|
export async function buildInnoInstaller(input: BuildInnoInput): Promise<string> {
|
|
42
|
-
const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } =
|
|
42
|
+
const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } =
|
|
43
|
+
input;
|
|
43
44
|
const out = join(outDir, fileName);
|
|
44
45
|
rmSync(out, { force: true });
|
|
45
46
|
if (!fileName.endsWith(".exe")) throw new Error("inno: fileName must end with .exe");
|
|
@@ -53,7 +54,8 @@ export async function buildInnoInstaller(input: BuildInnoInput): Promise<string>
|
|
|
53
54
|
const changeDir = options.allowChangeInstallDir !== false && !minimal;
|
|
54
55
|
const publisher = options.publisher ?? appName;
|
|
55
56
|
const installDir =
|
|
56
|
-
options.installDir ??
|
|
57
|
+
options.installDir ??
|
|
58
|
+
(perMachine ? "{autopf}\\" + appName : "{localappdata}\\Programs\\" + appName);
|
|
57
59
|
|
|
58
60
|
// Setup icon: an explicit .ico, else the app's bundled icon.ico.
|
|
59
61
|
let icon: string | undefined;
|
|
@@ -80,7 +82,10 @@ export async function buildInnoInstaller(input: BuildInnoInput): Promise<string>
|
|
|
80
82
|
L.push(`PrivilegesRequired=${perMachine ? "admin" : "lowest"}`);
|
|
81
83
|
L.push(`OutputDir=${outDir}`);
|
|
82
84
|
L.push(`OutputBaseFilename=${baseName}`);
|
|
83
|
-
|
|
85
|
+
// CEF is the bulk of the installer. Normal compression plus two independent
|
|
86
|
+
// LZMA2 blocks is substantially faster on CI with a modest size tradeoff.
|
|
87
|
+
L.push("Compression=lzma2/normal");
|
|
88
|
+
L.push("LZMANumBlockThreads=2");
|
|
84
89
|
L.push("SolidCompression=yes");
|
|
85
90
|
L.push("ArchitecturesAllowed=x64compatible");
|
|
86
91
|
L.push("ArchitecturesInstallIn64BitMode=x64compatible");
|
|
@@ -96,17 +101,23 @@ export async function buildInnoInstaller(input: BuildInnoInput): Promise<string>
|
|
|
96
101
|
if (options.include) L.push("", "; --- user include ---", options.include, "");
|
|
97
102
|
|
|
98
103
|
L.push("", "[Files]");
|
|
99
|
-
L.push(
|
|
104
|
+
L.push(
|
|
105
|
+
`Source: "${appDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs`,
|
|
106
|
+
);
|
|
100
107
|
|
|
101
108
|
if (desktop) {
|
|
102
109
|
L.push("", "[Tasks]");
|
|
103
|
-
L.push(
|
|
110
|
+
L.push(
|
|
111
|
+
`Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional:"`,
|
|
112
|
+
);
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
L.push("", "[Icons]");
|
|
107
116
|
if (startMenu) L.push(`Name: "{group}\\${v(appName)}"; Filename: "{app}\\${exeName}"`);
|
|
108
117
|
if (desktop) {
|
|
109
|
-
L.push(
|
|
118
|
+
L.push(
|
|
119
|
+
`Name: "{autodesktop}\\${v(appName)}"; Filename: "{app}\\${exeName}"; Tasks: desktopicon`,
|
|
120
|
+
);
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
if (runAfter) {
|
package/src/installer-win.ts
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* falls back to the portable `.zip` when it's absent.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { $ } from "bun";
|
|
16
15
|
import { existsSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
-
import {
|
|
16
|
+
import { isAbsolute, join } from "node:path";
|
|
17
|
+
import { $ } from "bun";
|
|
18
18
|
import type { NsisConfig } from "mirinjs";
|
|
19
|
-
import { makeWindowsIcon } from "./
|
|
19
|
+
import { makeWindowsIcon } from "./icons/windows/index.ts";
|
|
20
20
|
|
|
21
21
|
export interface BuildNsisInput {
|
|
22
22
|
/** The assembled app folder (build/<App>). */
|
|
@@ -50,7 +50,8 @@ const resolve = (projectDir: string, p: string) => (isAbsolute(p) ? p : join(pro
|
|
|
50
50
|
|
|
51
51
|
/** Build the installer with `makensis`, returning the setup.exe path. */
|
|
52
52
|
export async function buildNsisInstaller(input: BuildNsisInput): Promise<string> {
|
|
53
|
-
const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } =
|
|
53
|
+
const { appDir, appName, exeName, version, bundleId, outDir, fileName, options, projectDir } =
|
|
54
|
+
input;
|
|
54
55
|
const out = join(outDir, fileName);
|
|
55
56
|
rmSync(out, { force: true });
|
|
56
57
|
|
|
@@ -63,12 +64,16 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
|
|
|
63
64
|
const publisher = options.publisher ?? appName;
|
|
64
65
|
const root = perMachine ? "HKLM" : "HKCU";
|
|
65
66
|
const installDir =
|
|
66
|
-
options.installDir ??
|
|
67
|
+
options.installDir ??
|
|
68
|
+
(perMachine ? `$PROGRAMFILES64\\${appName}` : `$LOCALAPPDATA\\Programs\\${appName}`);
|
|
67
69
|
|
|
68
70
|
// Installer/uninstaller icon: an explicit .ico, else the app's bundled icon.ico.
|
|
69
71
|
let icon: string | undefined;
|
|
70
72
|
if (options.installerIcon) {
|
|
71
|
-
icon = makeWindowsIcon(
|
|
73
|
+
icon = makeWindowsIcon(
|
|
74
|
+
resolve(projectDir, options.installerIcon),
|
|
75
|
+
join(outDir, "_installer.ico"),
|
|
76
|
+
);
|
|
72
77
|
} else if (existsSync(join(appDir, "icon.ico"))) {
|
|
73
78
|
icon = join(appDir, "icon.ico");
|
|
74
79
|
}
|
|
@@ -85,7 +90,7 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
|
|
|
85
90
|
L.push(`OutFile "${nsis(out)}"`);
|
|
86
91
|
L.push(`InstallDir "${installDir}"`);
|
|
87
92
|
L.push(`RequestExecutionLevel ${perMachine ? "admin" : "user"}`);
|
|
88
|
-
L.push(
|
|
93
|
+
L.push("!define MUI_ABORTWARNING");
|
|
89
94
|
if (icon) {
|
|
90
95
|
L.push(`!define MUI_ICON "${nsis(icon)}"`);
|
|
91
96
|
L.push(`!define MUI_UNICON "${nsis(icon)}"`);
|
|
@@ -127,14 +132,15 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
|
|
|
127
132
|
// Estimated size for Add/Remove Programs.
|
|
128
133
|
L.push(` \${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2`);
|
|
129
134
|
L.push(` IntFmt $0 "0x%08X" $0`);
|
|
130
|
-
const reg = (name: string, val: string) =>
|
|
135
|
+
const reg = (name: string, val: string) =>
|
|
136
|
+
L.push(` WriteRegStr ${root} "${key}" "${name}" "${val}"`);
|
|
131
137
|
reg("DisplayName", nsis(appName));
|
|
132
138
|
reg("DisplayVersion", nsis(version));
|
|
133
139
|
reg("Publisher", nsis(publisher));
|
|
134
140
|
reg("DisplayIcon", `$INSTDIR\\${nsis(exeName)}`);
|
|
135
141
|
reg("InstallLocation", "$INSTDIR");
|
|
136
|
-
reg("UninstallString",
|
|
137
|
-
reg("QuietUninstallString",
|
|
142
|
+
reg("UninstallString", "$INSTDIR\\Uninstall.exe");
|
|
143
|
+
reg("QuietUninstallString", "$INSTDIR\\Uninstall.exe /S");
|
|
138
144
|
L.push(` WriteRegDWORD ${root} "${key}" "NoModify" 1`);
|
|
139
145
|
L.push(` WriteRegDWORD ${root} "${key}" "NoRepair" 1`);
|
|
140
146
|
L.push(` WriteRegDWORD ${root} "${key}" "EstimatedSize" $0`);
|
|
@@ -142,7 +148,7 @@ export async function buildNsisInstaller(input: BuildNsisInput): Promise<string>
|
|
|
142
148
|
L.push("SectionEnd");
|
|
143
149
|
|
|
144
150
|
// Uninstall section.
|
|
145
|
-
L.push(
|
|
151
|
+
L.push('Section "Uninstall"');
|
|
146
152
|
L.push(` SetShellVarContext ${ctx}`);
|
|
147
153
|
if (desktop) L.push(` Delete "$DESKTOP\\${nsis(appName)}.lnk"`);
|
|
148
154
|
if (startMenu) {
|