@mirinjs/cli 0.0.1-alpha.9 → 0.1.0-alpha.0
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 -3
- package/src/artifacts.ts +27 -8
- package/src/build.ts +101 -24
- package/src/bundle-win.ts +118 -0
- package/src/bundle.ts +159 -14
- package/src/dev.ts +100 -24
- package/src/dmg.ts +236 -0
- package/src/extras.ts +61 -0
- package/src/icon-win.ts +81 -0
- package/src/index.ts +8 -1
- package/src/release.ts +165 -0
- package/src/temps.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirinjs/cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
4
|
"description": "CLI for mirin apps: dev, build, init.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
"bun": ">=1.2.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"create-mirinjs": "0.0
|
|
30
|
+
"create-mirinjs": "0.1.0-alpha.0",
|
|
31
|
+
"mirinjs": "0.1.0-alpha.0"
|
|
31
32
|
},
|
|
32
33
|
"optionalDependencies": {
|
|
33
|
-
"@mirinjs/darwin-arm64": "0.0
|
|
34
|
+
"@mirinjs/darwin-arm64": "0.1.0-alpha.0",
|
|
35
|
+
"@mirinjs/win32-x64": "0.1.0-alpha.0"
|
|
34
36
|
},
|
|
35
37
|
"publishConfig": {
|
|
36
38
|
"access": "public"
|
package/src/artifacts.ts
CHANGED
|
@@ -49,8 +49,8 @@ export async function resolveArtifacts(opts: { release: boolean }): Promise<Arti
|
|
|
49
49
|
await $`cargo build -p mirin-core -p mirin-helper ${flags}`.cwd(REPO_ROOT);
|
|
50
50
|
const target = join(REPO_ROOT, "target", profile);
|
|
51
51
|
return {
|
|
52
|
-
coreDylib: join(target,
|
|
53
|
-
helperBin: join(target,
|
|
52
|
+
coreDylib: join(target, coreFileName()),
|
|
53
|
+
helperBin: join(target, helperFileName()),
|
|
54
54
|
hostEntry: join(REPO_ROOT, "packages", "mirin", "src", "host.ts"),
|
|
55
55
|
cefPath,
|
|
56
56
|
};
|
|
@@ -58,17 +58,30 @@ export async function resolveArtifacts(opts: { release: boolean }): Promise<Arti
|
|
|
58
58
|
|
|
59
59
|
const nativeDir = resolveNativeDir();
|
|
60
60
|
return {
|
|
61
|
-
coreDylib: join(nativeDir,
|
|
62
|
-
helperBin: join(nativeDir,
|
|
61
|
+
coreDylib: join(nativeDir, coreFileName()),
|
|
62
|
+
helperBin: join(nativeDir, helperFileName()),
|
|
63
63
|
hostEntry: resolvePackageFile("mirinjs/host"),
|
|
64
64
|
cefPath,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** The native core library file name for the host platform (MSVC has no `lib` prefix). */
|
|
69
|
+
function coreFileName(): string {
|
|
70
|
+
return process.platform === "win32" ? "mirin_core.dll" : "libmirin_core.dylib";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The CEF subprocess binary name for the host platform. */
|
|
74
|
+
function helperFileName(): string {
|
|
75
|
+
return process.platform === "win32" ? "mirin-helper.exe" : "mirin-helper";
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
function assertSupportedPlatform(): void {
|
|
69
|
-
|
|
79
|
+
const supported =
|
|
80
|
+
(process.platform === "darwin" && process.arch === "arm64") ||
|
|
81
|
+
(process.platform === "win32" && process.arch === "x64");
|
|
82
|
+
if (!supported) {
|
|
70
83
|
throw new Error(
|
|
71
|
-
`mirin alpha supports macOS arm64
|
|
84
|
+
`mirin alpha supports macOS arm64 and Windows x64 (got ${process.platform}/${process.arch}).`,
|
|
72
85
|
);
|
|
73
86
|
}
|
|
74
87
|
}
|
|
@@ -99,10 +112,16 @@ function cliVersion(): string {
|
|
|
99
112
|
return pkg.version as string;
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
/** A file/dir that, if present in a CEF dir, means the distribution is unpacked.
|
|
116
|
+
* macOS ships the framework bundle; Windows ships a flat dir with libcef.dll. */
|
|
117
|
+
function cefMarker(): string {
|
|
118
|
+
return process.platform === "win32" ? "libcef.dll" : "Chromium Embedded Framework.framework";
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
async function ensureCef(): Promise<string> {
|
|
103
122
|
if (IN_REPO) {
|
|
104
123
|
const vendor = join(REPO_ROOT, "vendor", "cef");
|
|
105
|
-
if (existsSync(join(vendor,
|
|
124
|
+
if (existsSync(join(vendor, cefMarker()))) return vendor;
|
|
106
125
|
console.error(
|
|
107
126
|
"[mirin] vendor/cef missing — run `bun scripts/fetch-cef.ts` in the monorepo first.",
|
|
108
127
|
);
|
|
@@ -111,7 +130,7 @@ async function ensureCef(): Promise<string> {
|
|
|
111
130
|
|
|
112
131
|
const version = cliVersion();
|
|
113
132
|
const cacheDir = join(homedir(), ".mirinjs", "cef", `${version}-${platformTag()}`);
|
|
114
|
-
if (existsSync(join(cacheDir,
|
|
133
|
+
if (existsSync(join(cacheDir, cefMarker()))) return cacheDir;
|
|
115
134
|
|
|
116
135
|
mkdirSync(cacheDir, { recursive: true });
|
|
117
136
|
const asset = `cef-${platformTag()}.tar.gz`;
|
package/src/build.ts
CHANGED
|
@@ -16,23 +16,65 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { $ } from "bun";
|
|
19
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
19
|
+
import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { buildAppBundle } from "./bundle.ts";
|
|
22
|
+
import { buildWindowsBundle } from "./bundle-win.ts";
|
|
22
23
|
import { resolveArtifacts } from "./artifacts.ts";
|
|
24
|
+
import { sweepBuildTemps } from "./temps.ts";
|
|
25
|
+
import { normalizeSidecars, compileWorkers } from "./extras.ts";
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
28
|
+
|
|
29
|
+
export interface BuildResult {
|
|
30
|
+
/** Path to the assembled .app. */
|
|
31
|
+
app: string;
|
|
32
|
+
appName: string;
|
|
33
|
+
bundleId: string;
|
|
34
|
+
/** App version (from the project's package.json). */
|
|
35
|
+
version: string;
|
|
36
|
+
/** Update channel (config.release.channel ?? "stable"). */
|
|
37
|
+
channel: string;
|
|
38
|
+
/** Update baseUrl, if `release` is configured. */
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
/** libmirin_core path (for the updater codec at release time). */
|
|
41
|
+
coreDylib: string;
|
|
42
|
+
/** Project root (so `mirin release` can resolve relative asset paths). */
|
|
43
|
+
projectDir: string;
|
|
44
|
+
/** DMG config from mirin.config.ts (`true`/object/`false`); default `true`. */
|
|
45
|
+
dmg: boolean | import("mirinjs").DmgConfig;
|
|
46
|
+
/** Codesign identity used for the bundle, if any (MIRIN_SIGN_IDENTITY). */
|
|
47
|
+
signIdentity?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Read the project's package.json version (the single source of app version). */
|
|
51
|
+
function appVersion(projectDir: string): string {
|
|
52
|
+
const pkgPath = join(projectDir, "package.json");
|
|
53
|
+
if (!existsSync(pkgPath)) return "0.0.0";
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version ?? "0.0.0";
|
|
56
|
+
} catch {
|
|
57
|
+
return "0.0.0";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function build(projectDir = process.cwd()): Promise<BuildResult> {
|
|
25
62
|
const outDir = join(projectDir, "build");
|
|
26
63
|
const work = join(projectDir, ".mirin");
|
|
27
64
|
mkdirSync(outDir, { recursive: true });
|
|
28
65
|
mkdirSync(work, { recursive: true });
|
|
66
|
+
sweepBuildTemps(projectDir);
|
|
29
67
|
|
|
30
68
|
const config = (await import(join(projectDir, "mirin.config.ts"))).default;
|
|
31
69
|
const appName: string = config.name ?? "Mirin App";
|
|
32
70
|
const bundleId: string = config.id ?? "dev.mirin.app";
|
|
33
71
|
const mainEntry = join(projectDir, config.main ?? "main/main.ts");
|
|
72
|
+
const version = appVersion(projectDir);
|
|
73
|
+
const channel: string = config.release?.channel ?? "stable";
|
|
74
|
+
const baseUrl: string | undefined = config.release?.baseUrl;
|
|
75
|
+
const dmg: boolean | import("mirinjs").DmgConfig = config.dmg ?? true;
|
|
34
76
|
|
|
35
|
-
console.log(`[mirin build] ${appName}`);
|
|
77
|
+
console.log(`[mirin build] ${appName} ${version}`);
|
|
36
78
|
|
|
37
79
|
// 1. production UI
|
|
38
80
|
console.log("[mirin build] vite build…");
|
|
@@ -43,32 +85,67 @@ export async function build(projectDir = process.cwd()): Promise<number> {
|
|
|
43
85
|
|
|
44
86
|
// 3 + 4. host + worker (minified)
|
|
45
87
|
console.log("[mirin build] compiling host + bundling main process…");
|
|
46
|
-
const
|
|
88
|
+
const signIdentity = process.env.MIRIN_SIGN_IDENTITY;
|
|
89
|
+
const hostExe = join(work, IS_WINDOWS ? "host-release.exe" : "host-release");
|
|
47
90
|
const workerJs = join(work, "worker.release.js");
|
|
48
91
|
await $`bun build --compile --minify ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(projectDir);
|
|
49
92
|
await $`bun build ${mainEntry} --target=bun --minify --outfile ${workerJs}`.cwd(projectDir);
|
|
50
93
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
// Extra assets: resolve sidecar binaries + compile any extra worker entries.
|
|
95
|
+
const sidecars = normalizeSidecars(projectDir, config.sidecars);
|
|
96
|
+
const extraWorkers = await compileWorkers(projectDir, config.workers, join(work, "workers"), true);
|
|
97
|
+
|
|
98
|
+
// 5. assemble (+ sign on macOS)
|
|
99
|
+
console.log(`[mirin build] assembling ${IS_WINDOWS ? "app folder" : ".app"}…`);
|
|
100
|
+
// version.json embeds the running app's update identity (read by app.updater).
|
|
101
|
+
// Only when `release` is configured — otherwise the app has no updater.
|
|
102
|
+
const versionJson = baseUrl
|
|
103
|
+
? JSON.stringify({ version, channel, baseUrl, name: appName, identifier: bundleId })
|
|
104
|
+
: undefined;
|
|
105
|
+
const resources = {
|
|
106
|
+
uiDir: join(projectDir, "dist"),
|
|
107
|
+
workerJs,
|
|
108
|
+
manifestJson: JSON.stringify({ windows: config.windows }),
|
|
109
|
+
versionJson,
|
|
110
|
+
workers: extraWorkers,
|
|
111
|
+
};
|
|
112
|
+
const { app } = IS_WINDOWS
|
|
113
|
+
? await buildWindowsBundle({
|
|
114
|
+
appName,
|
|
115
|
+
outDir,
|
|
116
|
+
hostExe,
|
|
117
|
+
coreDll: artifacts.coreDylib,
|
|
118
|
+
helperExe: artifacts.helperBin,
|
|
119
|
+
cefPath: artifacts.cefPath,
|
|
120
|
+
icon: config.icon ? join(projectDir, config.icon) : undefined,
|
|
121
|
+
resources: { ...resources, sidecars: sidecars.map((s) => ({ name: s.name, src: s.src })) },
|
|
122
|
+
})
|
|
123
|
+
: await buildAppBundle({
|
|
124
|
+
appName,
|
|
125
|
+
bundleId,
|
|
126
|
+
outDir,
|
|
127
|
+
hostExe,
|
|
128
|
+
coreDylib: artifacts.coreDylib,
|
|
129
|
+
helperBin: artifacts.helperBin,
|
|
130
|
+
cefPath: artifacts.cefPath,
|
|
131
|
+
version,
|
|
132
|
+
icon: config.icon ? join(projectDir, config.icon) : undefined,
|
|
133
|
+
signIdentity,
|
|
134
|
+
urlSchemes: config.urlSchemes,
|
|
135
|
+
resources: { ...resources, sidecars },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(`\n[mirin build] done → ${app}`);
|
|
139
|
+
return {
|
|
140
|
+
app,
|
|
55
141
|
appName,
|
|
56
142
|
bundleId,
|
|
57
|
-
|
|
58
|
-
|
|
143
|
+
version,
|
|
144
|
+
channel,
|
|
145
|
+
baseUrl,
|
|
59
146
|
coreDylib: artifacts.coreDylib,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
resources: {
|
|
65
|
-
uiDir: join(projectDir, "dist"),
|
|
66
|
-
workerJs,
|
|
67
|
-
manifestJson: JSON.stringify({ windows: config.windows }),
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
console.log(`\n[mirin build] done → ${app}`);
|
|
72
|
-
console.log(` open "${app}"`);
|
|
73
|
-
return 0;
|
|
147
|
+
projectDir,
|
|
148
|
+
dmg,
|
|
149
|
+
signIdentity,
|
|
150
|
+
};
|
|
74
151
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble a Windows app folder for a mirin app. Windows has no `.app` bundle and
|
|
3
|
+
* no codesign step in the MVP — just a flat directory the OS loader can resolve
|
|
4
|
+
* libcef.dll + the CEF runtime from, next to the host exe:
|
|
5
|
+
*
|
|
6
|
+
* <App>/
|
|
7
|
+
* <App>.exe compiled Bun host (process.execPath)
|
|
8
|
+
* mirin_core.dll loaded by the host via bun:ffi
|
|
9
|
+
* mirin-helper.exe CEF subprocess (browser_subprocess_path)
|
|
10
|
+
* libcef.dll, *.pak, icudtl.dat, v8_context_snapshot.bin, locales/, … (CEF runtime)
|
|
11
|
+
* resources/ (production only — dev serves from the Vite URL)
|
|
12
|
+
* ui/, worker.js, mirin.manifest.json, version.json, sidecars/, workers/
|
|
13
|
+
*
|
|
14
|
+
* The CEF subprocess name (`mirin-helper.exe`) and the `resources/` layout match
|
|
15
|
+
* what mirin-core derives at runtime (engine::derive_subprocess_path, host.ts).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
19
|
+
import { join, extname } from "node:path";
|
|
20
|
+
import { makeWindowsIcon } from "./icon-win.ts";
|
|
21
|
+
|
|
22
|
+
/** Source dir (vendor/cef on Windows) must contain libcef.dll. */
|
|
23
|
+
const CEF_MARKER = "libcef.dll";
|
|
24
|
+
|
|
25
|
+
/** CEF runtime files copied next to the host exe. Everything else in the Windows
|
|
26
|
+
* distribution (libcef.lib, include/, cmake/, archive.json, bootstrap*.exe) is
|
|
27
|
+
* build-time only and intentionally left out. */
|
|
28
|
+
const CEF_RUNTIME_EXTS = new Set([".dll", ".bin", ".pak", ".dat"]);
|
|
29
|
+
const CEF_RUNTIME_EXTRAS = ["vk_swiftshader_icd.json", "locales"];
|
|
30
|
+
|
|
31
|
+
export interface WinBundleOptions {
|
|
32
|
+
appName: string; // also the host exe stem → <appName>.exe
|
|
33
|
+
outDir: string;
|
|
34
|
+
hostExe: string; // compiled Bun host (.exe)
|
|
35
|
+
coreDll: string; // mirin_core.dll
|
|
36
|
+
helperExe: string; // mirin-helper.exe
|
|
37
|
+
cefPath: string; // dir containing the Windows CEF runtime (vendor/cef)
|
|
38
|
+
/** App icon source (.ico / .iconset / .png) → <App>/icon.ico, set as the window icon. */
|
|
39
|
+
icon?: string;
|
|
40
|
+
/** Production-only resources placed under <App>/resources. */
|
|
41
|
+
resources?: {
|
|
42
|
+
uiDir?: string; // Vite dist/ → resources/ui (served via app://ui)
|
|
43
|
+
workerJs?: string; // bundled main-process Worker → resources/worker.js
|
|
44
|
+
manifestJson?: string; // serialized manifest → resources/mirin.manifest.json
|
|
45
|
+
versionJson?: string; // serialized version.json → resources/version.json (updater)
|
|
46
|
+
sidecars?: { name: string; src: string }[]; // → resources/sidecars/<name>
|
|
47
|
+
workers?: Record<string, string>; // name → abs path → resources/workers/<name>.js
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Build the Windows app folder and return its path + the host exe to launch. */
|
|
52
|
+
export async function buildWindowsBundle(
|
|
53
|
+
opts: WinBundleOptions,
|
|
54
|
+
): Promise<{ app: string; exe: string }> {
|
|
55
|
+
const { appName, cefPath } = opts;
|
|
56
|
+
if (!existsSync(join(cefPath, CEF_MARKER))) {
|
|
57
|
+
throw new Error(`CEF runtime not found at ${cefPath} — run: bun scripts/fetch-cef.ts`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const app = join(opts.outDir, appName);
|
|
61
|
+
rmSync(app, { recursive: true, force: true });
|
|
62
|
+
mkdirSync(app, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const exe = join(app, `${appName}.exe`);
|
|
65
|
+
cpSync(opts.hostExe, exe);
|
|
66
|
+
cpSync(opts.coreDll, join(app, "mirin_core.dll"));
|
|
67
|
+
cpSync(opts.helperExe, join(app, "mirin-helper.exe"));
|
|
68
|
+
|
|
69
|
+
copyCefRuntime(cefPath, app);
|
|
70
|
+
|
|
71
|
+
// App icon → <App>/icon.ico, loaded at runtime by the core and set as the window
|
|
72
|
+
// icon (taskbar / Alt-Tab / title). Sits beside the exe so the core finds it.
|
|
73
|
+
if (opts.icon) makeWindowsIcon(opts.icon, join(app, "icon.ico"));
|
|
74
|
+
|
|
75
|
+
// Production resources (dev passes none — paths come from env + the Vite URL).
|
|
76
|
+
if (opts.resources) {
|
|
77
|
+
const res = join(app, "resources");
|
|
78
|
+
mkdirSync(res, { recursive: true });
|
|
79
|
+
if (opts.resources.uiDir) cpSync(opts.resources.uiDir, join(res, "ui"), { recursive: true });
|
|
80
|
+
if (opts.resources.workerJs) cpSync(opts.resources.workerJs, join(res, "worker.js"));
|
|
81
|
+
if (opts.resources.manifestJson != null) {
|
|
82
|
+
writeFileSync(join(res, "mirin.manifest.json"), opts.resources.manifestJson);
|
|
83
|
+
}
|
|
84
|
+
if (opts.resources.versionJson != null) {
|
|
85
|
+
writeFileSync(join(res, "version.json"), opts.resources.versionJson);
|
|
86
|
+
}
|
|
87
|
+
if (opts.resources.workers && Object.keys(opts.resources.workers).length) {
|
|
88
|
+
const workersDir = join(res, "workers");
|
|
89
|
+
mkdirSync(workersDir, { recursive: true });
|
|
90
|
+
for (const [name, src] of Object.entries(opts.resources.workers)) {
|
|
91
|
+
cpSync(src, join(workersDir, `${name}.js`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (opts.resources.sidecars?.length) {
|
|
95
|
+
const sidecarsDir = join(res, "sidecars");
|
|
96
|
+
mkdirSync(sidecarsDir, { recursive: true });
|
|
97
|
+
for (const sc of opts.resources.sidecars) {
|
|
98
|
+
if (!existsSync(sc.src)) throw new Error(`sidecar "${sc.name}" not found: ${sc.src}`);
|
|
99
|
+
cpSync(sc.src, join(sidecarsDir, sc.name));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { app, exe };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Copy the CEF runtime (dlls, paks, snapshot, icu data, locales) into `dest`. */
|
|
108
|
+
function copyCefRuntime(cefPath: string, dest: string): void {
|
|
109
|
+
for (const entry of readdirSync(cefPath)) {
|
|
110
|
+
if (CEF_RUNTIME_EXTS.has(extname(entry).toLowerCase())) {
|
|
111
|
+
cpSync(join(cefPath, entry), join(dest, entry));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const extra of CEF_RUNTIME_EXTRAS) {
|
|
115
|
+
const src = join(cefPath, extra);
|
|
116
|
+
if (existsSync(src)) cpSync(src, join(dest, extra), { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/bundle.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { $ } from "bun";
|
|
15
|
-
import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
15
|
+
import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync, readdirSync, chmodSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
|
|
18
18
|
const FRAMEWORK = "Chromium Embedded Framework.framework";
|
|
@@ -33,18 +33,57 @@ export interface BundleOptions {
|
|
|
33
33
|
coreDylib: string; // libmirin_core.dylib
|
|
34
34
|
helperBin: string; // compiled mirin-helper binary
|
|
35
35
|
cefPath: string; // dir containing the CEF framework (vendor/cef)
|
|
36
|
+
/** App version → Info.plist CFBundleShortVersionString/CFBundleVersion. */
|
|
37
|
+
version?: string;
|
|
36
38
|
/** App icon source: a .icns, a .iconset dir, or a square .png. Optional. */
|
|
37
39
|
icon?: string;
|
|
38
40
|
/** Codesign identity; "-" (default) is ad-hoc. Set to a Developer ID to ship. */
|
|
39
41
|
signIdentity?: string;
|
|
42
|
+
/** Custom URL schemes -> Info.plist CFBundleURLTypes (deep links). */
|
|
43
|
+
urlSchemes?: string[];
|
|
40
44
|
/** Production-only resources placed under Contents/Resources. */
|
|
41
45
|
resources?: {
|
|
42
46
|
uiDir?: string; // Vite dist/, copied to Resources/ui (served via app://ui)
|
|
43
47
|
workerJs?: string; // bundled main-process Worker entry -> Resources/worker.js
|
|
44
48
|
manifestJson?: string; // serialized manifest -> Resources/mirin.manifest.json
|
|
49
|
+
versionJson?: string; // serialized version.json -> Resources/version.json (updater)
|
|
50
|
+
/** Bundled sidecar binaries -> Resources/sidecars/<name> (copied + signed). */
|
|
51
|
+
sidecars?: SidecarBundle[];
|
|
52
|
+
/** Compiled extra-worker JS (name -> abs path) -> Resources/workers/<name>.js. */
|
|
53
|
+
workers?: Record<string, string>;
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
/** A sidecar binary to bundle: logical name, source path, hardened-runtime entitlements. */
|
|
58
|
+
export interface SidecarBundle {
|
|
59
|
+
name: string;
|
|
60
|
+
src: string;
|
|
61
|
+
entitlements: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Short entitlement name -> the full `com.apple.security.cs.*` key. */
|
|
65
|
+
const SIDECAR_ENTITLEMENT_KEYS: Record<string, string> = {
|
|
66
|
+
"allow-jit": "com.apple.security.cs.allow-jit",
|
|
67
|
+
"allow-unsigned-executable-memory": "com.apple.security.cs.allow-unsigned-executable-memory",
|
|
68
|
+
"disable-library-validation": "com.apple.security.cs.disable-library-validation",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function entitlementsPlist(names: string[]): string {
|
|
72
|
+
const keys = names
|
|
73
|
+
.map((n) => SIDECAR_ENTITLEMENT_KEYS[n])
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((k) => ` <key>${k}</key><true/>`)
|
|
76
|
+
.join("\n");
|
|
77
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
78
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
79
|
+
<plist version="1.0">
|
|
80
|
+
<dict>
|
|
81
|
+
${keys}
|
|
82
|
+
</dict>
|
|
83
|
+
</plist>
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
48
87
|
/** The 10 standard iconset renditions (point size + @1x/@2x pixel size). */
|
|
49
88
|
const ICONSET_RENDITIONS = [
|
|
50
89
|
{ name: "icon_16x16.png", px: 16 },
|
|
@@ -89,14 +128,18 @@ async function writeIcon(iconSrc: string, resources: string): Promise<string | u
|
|
|
89
128
|
return "icon";
|
|
90
129
|
}
|
|
91
130
|
|
|
92
|
-
type PlistValue = string | boolean |
|
|
131
|
+
type PlistValue = string | boolean | PlistValue[] | { [k: string]: PlistValue };
|
|
132
|
+
|
|
133
|
+
const xmlEscape = (s: string) =>
|
|
134
|
+
s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
93
135
|
|
|
94
136
|
function plist(entries: Record<string, PlistValue>): string {
|
|
95
137
|
const render = (v: PlistValue): string => {
|
|
96
138
|
if (typeof v === "boolean") return v ? "<true/>" : "<false/>";
|
|
97
|
-
if (typeof v === "string") return `<string>${v}</string>`;
|
|
139
|
+
if (typeof v === "string") return `<string>${xmlEscape(v)}</string>`;
|
|
140
|
+
if (Array.isArray(v)) return `<array>${v.map(render).join("")}</array>`;
|
|
98
141
|
const inner = Object.entries(v)
|
|
99
|
-
.map(([k, val]) => `<key>${k}</key
|
|
142
|
+
.map(([k, val]) => `<key>${k}</key>${render(val)}`)
|
|
100
143
|
.join("");
|
|
101
144
|
return `<dict>${inner}</dict>`;
|
|
102
145
|
};
|
|
@@ -136,6 +179,7 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
136
179
|
// Render the icon (if any) into Resources before writing the plist, so we
|
|
137
180
|
// only set CFBundleIconFile when an icon was actually produced.
|
|
138
181
|
const iconFile = opts.icon ? await writeIcon(opts.icon, join(contents, "Resources")) : undefined;
|
|
182
|
+
const version = opts.version ?? "0.0.1";
|
|
139
183
|
|
|
140
184
|
const info: Record<string, PlistValue> = {
|
|
141
185
|
CFBundleDevelopmentRegion: "en",
|
|
@@ -145,8 +189,8 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
145
189
|
CFBundleInfoDictionaryVersion: "6.0",
|
|
146
190
|
CFBundleName: appName,
|
|
147
191
|
CFBundlePackageType: "APPL",
|
|
148
|
-
CFBundleShortVersionString:
|
|
149
|
-
CFBundleVersion:
|
|
192
|
+
CFBundleShortVersionString: version,
|
|
193
|
+
CFBundleVersion: version,
|
|
150
194
|
LSMinimumSystemVersion: "13.0",
|
|
151
195
|
NSHighResolutionCapable: true,
|
|
152
196
|
NSSupportsAutomaticGraphicsSwitching: true,
|
|
@@ -154,6 +198,13 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
154
198
|
LSEnvironment: { MallocNanoZone: "0" },
|
|
155
199
|
};
|
|
156
200
|
if (iconFile) info.CFBundleIconFile = iconFile;
|
|
201
|
+
// Deep-link schemes: register this app as the macOS handler for app://-style
|
|
202
|
+
// URLs (e.g. anko://…). Delivered at runtime via app.on("open-url").
|
|
203
|
+
if (opts.urlSchemes?.length) {
|
|
204
|
+
info.CFBundleURLTypes = [
|
|
205
|
+
{ CFBundleURLName: bundleId, CFBundleURLSchemes: opts.urlSchemes },
|
|
206
|
+
];
|
|
207
|
+
}
|
|
157
208
|
|
|
158
209
|
writeFileSync(join(contents, "Info.plist"), plist(info));
|
|
159
210
|
|
|
@@ -173,6 +224,33 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
173
224
|
if (opts.resources?.manifestJson != null) {
|
|
174
225
|
writeFileSync(join(resources, "mirin.manifest.json"), opts.resources.manifestJson);
|
|
175
226
|
}
|
|
227
|
+
// version.json is the running app's self-knowledge for the updater. Written
|
|
228
|
+
// before the codesign loop below so the signature covers it.
|
|
229
|
+
if (opts.resources?.versionJson != null) {
|
|
230
|
+
writeFileSync(join(resources, "version.json"), opts.resources.versionJson);
|
|
231
|
+
}
|
|
232
|
+
// Extra Bun Worker bundles -> Resources/workers/<name>.js (resolveWorker()).
|
|
233
|
+
if (opts.resources?.workers && Object.keys(opts.resources.workers).length) {
|
|
234
|
+
const workersDir = join(resources, "workers");
|
|
235
|
+
mkdirSync(workersDir, { recursive: true });
|
|
236
|
+
for (const [name, src] of Object.entries(opts.resources.workers)) {
|
|
237
|
+
cpSync(src, join(workersDir, `${name}.js`));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Sidecar binaries -> Resources/sidecars/<name> (chmod +x; signed below). Paths
|
|
241
|
+
// collected here so the codesign loop can sign them inside-out with the app.
|
|
242
|
+
const sidecarDests: SidecarBundle[] = [];
|
|
243
|
+
if (opts.resources?.sidecars?.length) {
|
|
244
|
+
const sidecarsDir = join(resources, "sidecars");
|
|
245
|
+
mkdirSync(sidecarsDir, { recursive: true });
|
|
246
|
+
for (const sc of opts.resources.sidecars) {
|
|
247
|
+
if (!existsSync(sc.src)) throw new Error(`sidecar "${sc.name}" not found: ${sc.src}`);
|
|
248
|
+
const dest = join(sidecarsDir, sc.name);
|
|
249
|
+
cpSync(sc.src, dest);
|
|
250
|
+
chmodSync(dest, 0o755);
|
|
251
|
+
sidecarDests.push({ name: sc.name, src: dest, entitlements: sc.entitlements });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
176
254
|
|
|
177
255
|
for (const { suffix, id } of HELPER_TYPES) {
|
|
178
256
|
const name = `${appName} Helper${suffix}`;
|
|
@@ -189,8 +267,8 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
189
267
|
CFBundleInfoDictionaryVersion: "6.0",
|
|
190
268
|
CFBundleName: name,
|
|
191
269
|
CFBundlePackageType: "APPL",
|
|
192
|
-
CFBundleShortVersionString:
|
|
193
|
-
CFBundleVersion:
|
|
270
|
+
CFBundleShortVersionString: version,
|
|
271
|
+
CFBundleVersion: version,
|
|
194
272
|
LSMinimumSystemVersion: "13.0",
|
|
195
273
|
LSUIElement: "1",
|
|
196
274
|
NSHighResolutionCapable: true,
|
|
@@ -199,14 +277,81 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
199
277
|
);
|
|
200
278
|
}
|
|
201
279
|
|
|
202
|
-
// Sign inside-out
|
|
203
|
-
//
|
|
280
|
+
// Sign inside-out. Ad-hoc ("-") by default for local builds; pass a Developer
|
|
281
|
+
// ID to produce a distributable, notarizable app. Notarization requires the
|
|
282
|
+
// hardened runtime (--options runtime), a secure timestamp (--timestamp), and
|
|
283
|
+
// entitlements that let CEF + Bun JIT and load unsigned executable memory —
|
|
284
|
+
// without all three the Apple notary service returns "Invalid".
|
|
204
285
|
const identity = opts.signIdentity ?? "-";
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
286
|
+
const notarizable = identity !== "-";
|
|
287
|
+
const cef = join(frameworks, FRAMEWORK);
|
|
288
|
+
|
|
289
|
+
if (notarizable) {
|
|
290
|
+
// Mirrors the entitlement set Electrobun ships for Bun + CEF under hardened
|
|
291
|
+
// runtime (electrobun-reference/package/src/cli/index.ts).
|
|
292
|
+
const entitlements = join(opts.outDir, "_entitlements.plist");
|
|
293
|
+
writeFileSync(
|
|
294
|
+
entitlements,
|
|
295
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
296
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
297
|
+
<plist version="1.0">
|
|
298
|
+
<dict>
|
|
299
|
+
<key>com.apple.security.cs.allow-jit</key><true/>
|
|
300
|
+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
|
|
301
|
+
<key>com.apple.security.cs.disable-library-validation</key><true/>
|
|
302
|
+
</dict>
|
|
303
|
+
</plist>
|
|
304
|
+
`,
|
|
305
|
+
);
|
|
306
|
+
const sign = (path: string, ents = false) =>
|
|
307
|
+
ents
|
|
308
|
+
? $`codesign --force --timestamp --options runtime --entitlements ${entitlements} --sign ${identity} ${path}`.quiet()
|
|
309
|
+
: $`codesign --force --timestamp --options runtime --sign ${identity} ${path}`.quiet();
|
|
310
|
+
|
|
311
|
+
// 1. CEF's nested libraries, then 2. the framework bundle itself.
|
|
312
|
+
const cefLibs = join(cef, "Libraries");
|
|
313
|
+
if (existsSync(cefLibs)) {
|
|
314
|
+
for (const lib of readdirSync(cefLibs)) {
|
|
315
|
+
if (lib.endsWith(".dylib")) await sign(join(cefLibs, lib));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
await sign(cef);
|
|
319
|
+
// 3. our FFI core dylib.
|
|
320
|
+
await sign(join(macos, "libmirin_core.dylib"));
|
|
321
|
+
// 3b. sidecars: hardened runtime + timestamp; per-binary entitlements only
|
|
322
|
+
// when the spec asks (most CLIs need none — over-entitling is a smell).
|
|
323
|
+
for (const sc of sidecarDests) {
|
|
324
|
+
if (sc.entitlements.length) {
|
|
325
|
+
const ent = join(opts.outDir, `_sidecar_${sc.name}.plist`);
|
|
326
|
+
writeFileSync(ent, entitlementsPlist(sc.entitlements));
|
|
327
|
+
await $`codesign --force --timestamp --options runtime --entitlements ${ent} --sign ${identity} ${sc.src}`.quiet();
|
|
328
|
+
rmSync(ent, { force: true });
|
|
329
|
+
} else {
|
|
330
|
+
await sign(sc.src);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// 4. each helper: the inner executable, then the .app wrapper (entitlements
|
|
334
|
+
// on both — the renderer/GPU helpers are what actually JIT).
|
|
335
|
+
for (const { suffix } of HELPER_TYPES) {
|
|
336
|
+
const name = `${appName} Helper${suffix}`;
|
|
337
|
+
const helperApp = join(frameworks, `${name}.app`);
|
|
338
|
+
await sign(join(helperApp, "Contents", "MacOS", name), true);
|
|
339
|
+
await sign(helperApp, true);
|
|
340
|
+
}
|
|
341
|
+
// 5. finally the outer app.
|
|
342
|
+
await sign(app, true);
|
|
343
|
+
rmSync(entitlements, { force: true });
|
|
344
|
+
} else {
|
|
345
|
+
// Ad-hoc: enough to launch locally; not distributable or notarizable.
|
|
346
|
+
await $`codesign --force --sign ${identity} ${cef}`.quiet();
|
|
347
|
+
for (const sc of sidecarDests) {
|
|
348
|
+
await $`codesign --force --sign ${identity} ${sc.src}`.quiet();
|
|
349
|
+
}
|
|
350
|
+
for (const { suffix } of HELPER_TYPES) {
|
|
351
|
+
await $`codesign --force --sign ${identity} ${join(frameworks, `${appName} Helper${suffix}.app`)}`.quiet();
|
|
352
|
+
}
|
|
353
|
+
await $`codesign --force --sign ${identity} ${app}`.quiet();
|
|
208
354
|
}
|
|
209
|
-
await $`codesign --force --sign ${identity} ${app}`.quiet();
|
|
210
355
|
|
|
211
356
|
return { app, exe: join(macos, appName) };
|
|
212
357
|
}
|