@mirinjs/cli 0.0.1-alpha.11 → 0.0.1-alpha.13
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 +4 -3
- package/src/build.ts +3 -1
- package/src/bundle.ts +59 -7
- package/src/release.ts +98 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirinjs/cli",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.13",
|
|
4
4
|
"description": "CLI for mirin apps: dev, build, init.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
"bun": ">=1.2.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"create-mirinjs": "0.0.1-alpha.
|
|
30
|
+
"create-mirinjs": "0.0.1-alpha.13",
|
|
31
|
+
"mirinjs": "0.0.1-alpha.13"
|
|
31
32
|
},
|
|
32
33
|
"optionalDependencies": {
|
|
33
|
-
"@mirinjs/darwin-arm64": "0.0.1-alpha.
|
|
34
|
+
"@mirinjs/darwin-arm64": "0.0.1-alpha.13"
|
|
34
35
|
},
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public"
|
package/src/build.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface BuildResult {
|
|
|
33
33
|
channel: string;
|
|
34
34
|
/** Update baseUrl, if `release` is configured. */
|
|
35
35
|
baseUrl?: string;
|
|
36
|
+
/** libmirin_core path (for the updater codec at release time). */
|
|
37
|
+
coreDylib: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/** Read the project's package.json version (the single source of app version). */
|
|
@@ -106,5 +108,5 @@ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
|
|
|
106
108
|
|
|
107
109
|
console.log(`\n[mirin build] done → ${app}`);
|
|
108
110
|
console.log(` open "${app}"`);
|
|
109
|
-
return { app, appName, bundleId, version, channel, baseUrl };
|
|
111
|
+
return { app, appName, bundleId, version, channel, baseUrl, coreDylib: artifacts.coreDylib };
|
|
110
112
|
}
|
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 } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
|
|
18
18
|
const FRAMEWORK = "Chromium Embedded Framework.framework";
|
|
@@ -208,14 +208,66 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
208
208
|
);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
// Sign inside-out
|
|
212
|
-
//
|
|
211
|
+
// Sign inside-out. Ad-hoc ("-") by default for local builds; pass a Developer
|
|
212
|
+
// ID to produce a distributable, notarizable app. Notarization requires the
|
|
213
|
+
// hardened runtime (--options runtime), a secure timestamp (--timestamp), and
|
|
214
|
+
// entitlements that let CEF + Bun JIT and load unsigned executable memory —
|
|
215
|
+
// without all three the Apple notary service returns "Invalid".
|
|
213
216
|
const identity = opts.signIdentity ?? "-";
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
const notarizable = identity !== "-";
|
|
218
|
+
const cef = join(frameworks, FRAMEWORK);
|
|
219
|
+
|
|
220
|
+
if (notarizable) {
|
|
221
|
+
// Mirrors the entitlement set Electrobun ships for Bun + CEF under hardened
|
|
222
|
+
// runtime (electrobun-reference/package/src/cli/index.ts).
|
|
223
|
+
const entitlements = join(opts.outDir, "_entitlements.plist");
|
|
224
|
+
writeFileSync(
|
|
225
|
+
entitlements,
|
|
226
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
227
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
228
|
+
<plist version="1.0">
|
|
229
|
+
<dict>
|
|
230
|
+
<key>com.apple.security.cs.allow-jit</key><true/>
|
|
231
|
+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
|
|
232
|
+
<key>com.apple.security.cs.disable-library-validation</key><true/>
|
|
233
|
+
</dict>
|
|
234
|
+
</plist>
|
|
235
|
+
`,
|
|
236
|
+
);
|
|
237
|
+
const sign = (path: string, ents = false) =>
|
|
238
|
+
ents
|
|
239
|
+
? $`codesign --force --timestamp --options runtime --entitlements ${entitlements} --sign ${identity} ${path}`.quiet()
|
|
240
|
+
: $`codesign --force --timestamp --options runtime --sign ${identity} ${path}`.quiet();
|
|
241
|
+
|
|
242
|
+
// 1. CEF's nested libraries, then 2. the framework bundle itself.
|
|
243
|
+
const cefLibs = join(cef, "Libraries");
|
|
244
|
+
if (existsSync(cefLibs)) {
|
|
245
|
+
for (const lib of readdirSync(cefLibs)) {
|
|
246
|
+
if (lib.endsWith(".dylib")) await sign(join(cefLibs, lib));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
await sign(cef);
|
|
250
|
+
// 3. our FFI core dylib.
|
|
251
|
+
await sign(join(macos, "libmirin_core.dylib"));
|
|
252
|
+
// 4. each helper: the inner executable, then the .app wrapper (entitlements
|
|
253
|
+
// on both — the renderer/GPU helpers are what actually JIT).
|
|
254
|
+
for (const { suffix } of HELPER_TYPES) {
|
|
255
|
+
const name = `${appName} Helper${suffix}`;
|
|
256
|
+
const helperApp = join(frameworks, `${name}.app`);
|
|
257
|
+
await sign(join(helperApp, "Contents", "MacOS", name), true);
|
|
258
|
+
await sign(helperApp, true);
|
|
259
|
+
}
|
|
260
|
+
// 5. finally the outer app.
|
|
261
|
+
await sign(app, true);
|
|
262
|
+
rmSync(entitlements, { force: true });
|
|
263
|
+
} else {
|
|
264
|
+
// Ad-hoc: enough to launch locally; not distributable or notarizable.
|
|
265
|
+
await $`codesign --force --sign ${identity} ${cef}`.quiet();
|
|
266
|
+
for (const { suffix } of HELPER_TYPES) {
|
|
267
|
+
await $`codesign --force --sign ${identity} ${join(frameworks, `${appName} Helper${suffix}.app`)}`.quiet();
|
|
268
|
+
}
|
|
269
|
+
await $`codesign --force --sign ${identity} ${app}`.quiet();
|
|
217
270
|
}
|
|
218
|
-
await $`codesign --force --sign ${identity} ${app}`.quiet();
|
|
219
271
|
|
|
220
272
|
return { app, exe: join(macos, appName) };
|
|
221
273
|
}
|
package/src/release.ts
CHANGED
|
@@ -2,46 +2,49 @@
|
|
|
2
2
|
* `mirin release` — build the app and emit flat-named update artifacts.
|
|
3
3
|
*
|
|
4
4
|
* Produces, under `build/release/`:
|
|
5
|
-
* {channel}-{platform}-{arch}-update.json
|
|
6
|
-
* {channel}-{platform}-{arch}-{Name}.app.tar.
|
|
5
|
+
* {channel}-{platform}-{arch}-update.json the manifest the app polls
|
|
6
|
+
* {channel}-{platform}-{arch}-{Name}.app.tar.zst the full bundle (fallback)
|
|
7
|
+
* {channel}-{platform}-{arch}-{prevVersion}.patch delta from the previous release
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* The bundle is a zstd-compressed tar of the whole signed `.app`. Identity is the
|
|
10
|
+
* SHA-256 of the *uncompressed* tar (`tarHash`), so a delta patch can reconstruct
|
|
11
|
+
* it exactly. When the previous release is reachable at `baseUrl`, a bsdiff patch
|
|
12
|
+
* (prev → this) is generated; the app applies it instead of re-downloading the
|
|
13
|
+
* whole bundle, falling back to the full bundle whenever a patch isn't usable.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the whole `.app` and relaunches. (A signed/notarized `.app` must be replaced
|
|
15
|
-
* whole — never modified in place — so the artifact is the entire bundle.)
|
|
15
|
+
* Names are flat (no folders) so they upload as-is to GitHub Releases, S3/R2, or
|
|
16
|
+
* any static host. Channels coexist because the channel is part of every name.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { $ } from "bun";
|
|
19
|
-
import { mkdirSync, rmSync, readFileSync } from "node:fs";
|
|
20
|
+
import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
20
21
|
import { join } from "node:path";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
21
23
|
import { build } from "./build.ts";
|
|
24
|
+
import { loadCodec } from "mirinjs/codec";
|
|
25
|
+
|
|
26
|
+
const sha256File = (path: string) =>
|
|
27
|
+
new Bun.CryptoHasher("sha256").update(readFileSync(path)).digest("hex");
|
|
22
28
|
|
|
23
29
|
export async function release(projectDir = process.cwd()): Promise<number> {
|
|
24
30
|
const result = await build(projectDir);
|
|
25
31
|
if (!result.baseUrl) {
|
|
26
|
-
console.error(
|
|
27
|
-
"[mirin release] no `release.baseUrl` in mirin.config.ts — nothing to publish.",
|
|
28
|
-
);
|
|
32
|
+
console.error("[mirin release] no `release.baseUrl` in mirin.config.ts — nothing to publish.");
|
|
29
33
|
return 1;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
const platform = "darwin";
|
|
33
37
|
const arch = process.arch; // "arm64" | "x64"
|
|
34
38
|
const prefix = `${result.channel}-${platform}-${arch}`;
|
|
35
|
-
// Sanitize the app name for a URL-safe artifact filename (spaces break hosts).
|
|
36
39
|
const safeName = result.appName.replace(/[^A-Za-z0-9._-]/g, "");
|
|
40
|
+
const base = result.baseUrl.replace(/\/$/, "");
|
|
37
41
|
|
|
38
42
|
const buildDir = join(projectDir, "build");
|
|
39
43
|
const outDir = join(buildDir, "release");
|
|
40
44
|
rmSync(outDir, { recursive: true, force: true });
|
|
41
45
|
mkdirSync(outDir, { recursive: true });
|
|
42
46
|
|
|
43
|
-
//
|
|
44
|
-
// end-user machines), when notary credentials are present in the environment.
|
|
47
|
+
// Optional notarize + staple (Developer ID) before packing, when configured.
|
|
45
48
|
const apple = process.env.MIRIN_NOTARY_APPLE_ID;
|
|
46
49
|
const pw = process.env.MIRIN_NOTARY_PASSWORD;
|
|
47
50
|
const team = process.env.MIRIN_NOTARY_TEAM_ID;
|
|
@@ -49,37 +52,101 @@ export async function release(projectDir = process.cwd()): Promise<number> {
|
|
|
49
52
|
console.log("[mirin release] notarizing (this can take a few minutes)…");
|
|
50
53
|
const zip = join(buildDir, "_notarize.zip");
|
|
51
54
|
await $`ditto -c -k --keepParent ${result.app} ${zip}`;
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
// `notarytool submit --wait` exits 0 even when the result is "Invalid", so
|
|
56
|
+
// parse the JSON status ourselves and surface the notary log on rejection —
|
|
57
|
+
// otherwise the only symptom is a confusing `stapler` failure downstream.
|
|
58
|
+
const out =
|
|
59
|
+
await $`xcrun notarytool submit ${zip} --apple-id ${apple} --password ${pw} --team-id ${team} --wait --output-format json`.text();
|
|
54
60
|
rmSync(zip, { force: true });
|
|
61
|
+
let sub: { id?: string; status?: string } = {};
|
|
62
|
+
try {
|
|
63
|
+
sub = JSON.parse(out);
|
|
64
|
+
} catch {
|
|
65
|
+
console.error(out);
|
|
66
|
+
}
|
|
67
|
+
if (sub.status !== "Accepted") {
|
|
68
|
+
console.error(`[mirin release] notarization ${sub.status ?? "failed"} (id: ${sub.id ?? "?"})`);
|
|
69
|
+
if (sub.id) {
|
|
70
|
+
const log =
|
|
71
|
+
await $`xcrun notarytool log ${sub.id} --apple-id ${apple} --password ${pw} --team-id ${team}`
|
|
72
|
+
.text()
|
|
73
|
+
.catch(() => "");
|
|
74
|
+
if (log) console.error(log);
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`notarization not accepted: ${sub.status ?? "unknown"}`);
|
|
77
|
+
}
|
|
78
|
+
await $`xcrun stapler staple ${result.app}`;
|
|
55
79
|
}
|
|
56
80
|
|
|
57
|
-
const
|
|
58
|
-
const tarPath = join(outDir, tarName);
|
|
81
|
+
const codec = loadCodec(result.coreDylib);
|
|
59
82
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
await $`tar -
|
|
83
|
+
// Uncompressed tar — the identity + diff/patch basis (BSD tar keeps symlinks).
|
|
84
|
+
const newTar = join(outDir, "_new.tar");
|
|
85
|
+
await $`tar -cf ${newTar} -C ${buildDir} ${`${result.appName}.app`}`;
|
|
86
|
+
const tarHash = sha256File(newTar);
|
|
87
|
+
|
|
88
|
+
// Full bundle: zstd(newTar).
|
|
89
|
+
const bundleName = `${prefix}-${safeName}.app.tar.zst`;
|
|
90
|
+
const bundlePath = join(outDir, bundleName);
|
|
91
|
+
console.log(`[mirin release] compressing → ${bundleName}`);
|
|
92
|
+
codec.compress(newTar, bundlePath, 19);
|
|
93
|
+
const bundleSha = sha256File(bundlePath);
|
|
94
|
+
const bundleSize = readFileSync(bundlePath).byteLength;
|
|
95
|
+
|
|
96
|
+
// Delta patch vs the previous release (if reachable). Best-effort.
|
|
97
|
+
const patches: Array<{ fromVersion: string; url: string; sha256: string; size: number }> = [];
|
|
98
|
+
try {
|
|
99
|
+
const prevRes = await fetch(`${base}/${prefix}-update.json?t=${Date.now()}`, { redirect: "follow" });
|
|
100
|
+
if (prevRes.ok) {
|
|
101
|
+
const prev = (await prevRes.json()) as { version: string; bundle?: { url: string } };
|
|
102
|
+
if (prev.version && prev.version !== result.version && prev.bundle?.url) {
|
|
103
|
+
console.log(`[mirin release] generating delta ${prev.version} → ${result.version}…`);
|
|
104
|
+
const tmp = join(tmpdir(), `mirin-release-${Date.now()}`);
|
|
105
|
+
mkdirSync(tmp, { recursive: true });
|
|
106
|
+
const prevZst = join(tmp, "prev.tar.zst");
|
|
107
|
+
const dl = await fetch(`${base}/${prev.bundle.url}`, { redirect: "follow" });
|
|
108
|
+
if (!dl.ok || !dl.body) throw new Error(`prev bundle ${dl.status}`);
|
|
109
|
+
await Bun.write(prevZst, await dl.arrayBuffer());
|
|
110
|
+
const prevTar = join(tmp, "prev.tar");
|
|
111
|
+
codec.decompress(prevZst, prevTar);
|
|
112
|
+
const rawPatch = join(tmp, "patch.bin");
|
|
113
|
+
codec.diff(prevTar, newTar, rawPatch); // bsdiff
|
|
114
|
+
const patchName = `${prefix}-${prev.version}.patch`;
|
|
115
|
+
const patchPath = join(outDir, patchName);
|
|
116
|
+
codec.compress(rawPatch, patchPath, 19);
|
|
117
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
118
|
+
patches.push({
|
|
119
|
+
fromVersion: prev.version,
|
|
120
|
+
url: patchName,
|
|
121
|
+
sha256: sha256File(patchPath),
|
|
122
|
+
size: readFileSync(patchPath).byteLength,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.warn(`[mirin release] skipping delta patch: ${e instanceof Error ? e.message : e}`);
|
|
128
|
+
}
|
|
63
129
|
|
|
64
|
-
|
|
65
|
-
const sha256 = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
130
|
+
rmSync(newTar, { force: true });
|
|
66
131
|
|
|
67
132
|
const manifest = {
|
|
68
133
|
version: result.version,
|
|
69
134
|
channel: result.channel,
|
|
70
135
|
platform,
|
|
71
136
|
arch,
|
|
72
|
-
|
|
73
|
-
sha256,
|
|
74
|
-
|
|
137
|
+
tarHash,
|
|
138
|
+
bundle: { url: bundleName, sha256: bundleSha, size: bundleSize },
|
|
139
|
+
patches,
|
|
75
140
|
};
|
|
76
141
|
const manifestName = `${prefix}-update.json`;
|
|
77
142
|
await Bun.write(join(outDir, manifestName), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
78
143
|
|
|
79
|
-
const mb = (
|
|
144
|
+
const mb = (n: number) => (n / 1e6).toFixed(1);
|
|
80
145
|
console.log(`\n[mirin release] done → build/release/`);
|
|
81
146
|
console.log(` ${manifestName}`);
|
|
82
|
-
console.log(` ${
|
|
83
|
-
console.log(
|
|
147
|
+
console.log(` ${bundleName} (${mb(bundleSize)} MB)`);
|
|
148
|
+
for (const p of patches) console.log(` ${p.url} (${mb(p.size)} MB delta from ${p.fromVersion})`);
|
|
149
|
+
console.log(`\nUpload all of build/release/ to: ${result.baseUrl}`);
|
|
150
|
+
if (existsSync(join(outDir, "_new.tar"))) rmSync(join(outDir, "_new.tar"), { force: true });
|
|
84
151
|
return 0;
|
|
85
152
|
}
|