@mirinjs/cli 0.0.1-alpha.8 → 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/src/release.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mirin release` — build the app and emit flat-named update artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Produces, under `build/release/`:
|
|
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
|
|
8
|
+
*
|
|
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.
|
|
14
|
+
*
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { $ } from "bun";
|
|
20
|
+
import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { build } from "./build.ts";
|
|
24
|
+
import { buildDmg, notarizeAndStaple, type DmgOptions } from "./dmg.ts";
|
|
25
|
+
import { loadCodec } from "mirinjs/codec";
|
|
26
|
+
|
|
27
|
+
const sha256File = (path: string) =>
|
|
28
|
+
new Bun.CryptoHasher("sha256").update(readFileSync(path)).digest("hex");
|
|
29
|
+
|
|
30
|
+
export async function release(projectDir = process.cwd()): Promise<number> {
|
|
31
|
+
const result = await build(projectDir);
|
|
32
|
+
if (!result.baseUrl) {
|
|
33
|
+
console.error("[mirin release] no `release.baseUrl` in mirin.config.ts — nothing to publish.");
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isWindows = process.platform === "win32";
|
|
38
|
+
const platform = isWindows ? "win32" : "darwin";
|
|
39
|
+
const arch = process.arch; // "arm64" | "x64"
|
|
40
|
+
const prefix = `${result.channel}-${platform}-${arch}`;
|
|
41
|
+
const safeName = result.appName.replace(/[^A-Za-z0-9._-]/g, "");
|
|
42
|
+
const base = result.baseUrl.replace(/\/$/, "");
|
|
43
|
+
// The packaged unit: a flat app folder on Windows, an `.app` bundle on macOS.
|
|
44
|
+
const appArtifact = isWindows ? result.appName : `${result.appName}.app`;
|
|
45
|
+
|
|
46
|
+
const buildDir = join(projectDir, "build");
|
|
47
|
+
const outDir = join(buildDir, "release");
|
|
48
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
49
|
+
mkdirSync(outDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
// Notarize + staple the .app (Developer ID, macOS) before packing, when
|
|
52
|
+
// credentials are present. The .tar.zst / patch updater bundles are made from
|
|
53
|
+
// this signed, stapled .app, so the updater swaps in an already-notarized app.
|
|
54
|
+
if (!isWindows) await notarizeAndStaple(result.app);
|
|
55
|
+
|
|
56
|
+
// Load the codec (zstd/bsdiff) from the *bundled* core, not the raw build output:
|
|
57
|
+
// on Windows mirin_core.dll imports libcef.dll. The DLL loader resolves imports
|
|
58
|
+
// from PATH (and the loader exe's dir), not the dll's own dir, so prepend the
|
|
59
|
+
// bundle dir to PATH so libcef.dll is found. (On macOS the framework loads at
|
|
60
|
+
// runtime, so result.coreDylib works directly.)
|
|
61
|
+
const codecCore = isWindows ? join(result.app, "mirin_core.dll") : result.coreDylib;
|
|
62
|
+
if (isWindows) process.env.PATH = `${result.app};${process.env.PATH ?? ""}`;
|
|
63
|
+
const codec = loadCodec(codecCore);
|
|
64
|
+
|
|
65
|
+
// Uncompressed tar — the identity + diff/patch basis (BSD tar keeps symlinks).
|
|
66
|
+
const newTar = join(outDir, "_new.tar");
|
|
67
|
+
await $`tar -cf ${newTar} -C ${buildDir} ${appArtifact}`;
|
|
68
|
+
const tarHash = sha256File(newTar);
|
|
69
|
+
|
|
70
|
+
// Full bundle: zstd(newTar).
|
|
71
|
+
const bundleName = `${prefix}-${safeName}${isWindows ? "" : ".app"}.tar.zst`;
|
|
72
|
+
const bundlePath = join(outDir, bundleName);
|
|
73
|
+
console.log(`[mirin release] compressing → ${bundleName}`);
|
|
74
|
+
codec.compress(newTar, bundlePath, 19);
|
|
75
|
+
const bundleSha = sha256File(bundlePath);
|
|
76
|
+
const bundleSize = readFileSync(bundlePath).byteLength;
|
|
77
|
+
|
|
78
|
+
// Delta patch vs the previous release (if reachable). Best-effort.
|
|
79
|
+
const patches: Array<{ fromVersion: string; url: string; sha256: string; size: number }> = [];
|
|
80
|
+
try {
|
|
81
|
+
const prevRes = await fetch(`${base}/${prefix}-update.json?t=${Date.now()}`, { redirect: "follow" });
|
|
82
|
+
if (prevRes.ok) {
|
|
83
|
+
const prev = (await prevRes.json()) as { version: string; bundle?: { url: string } };
|
|
84
|
+
if (prev.version && prev.version !== result.version && prev.bundle?.url) {
|
|
85
|
+
console.log(`[mirin release] generating delta ${prev.version} → ${result.version}…`);
|
|
86
|
+
const tmp = join(tmpdir(), `mirin-release-${Date.now()}`);
|
|
87
|
+
mkdirSync(tmp, { recursive: true });
|
|
88
|
+
const prevZst = join(tmp, "prev.tar.zst");
|
|
89
|
+
const dl = await fetch(`${base}/${prev.bundle.url}`, { redirect: "follow" });
|
|
90
|
+
if (!dl.ok || !dl.body) throw new Error(`prev bundle ${dl.status}`);
|
|
91
|
+
await Bun.write(prevZst, await dl.arrayBuffer());
|
|
92
|
+
const prevTar = join(tmp, "prev.tar");
|
|
93
|
+
codec.decompress(prevZst, prevTar);
|
|
94
|
+
const rawPatch = join(tmp, "patch.bin");
|
|
95
|
+
codec.diff(prevTar, newTar, rawPatch); // bsdiff
|
|
96
|
+
const patchName = `${prefix}-${prev.version}.patch`;
|
|
97
|
+
const patchPath = join(outDir, patchName);
|
|
98
|
+
codec.compress(rawPatch, patchPath, 19);
|
|
99
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
100
|
+
patches.push({
|
|
101
|
+
fromVersion: prev.version,
|
|
102
|
+
url: patchName,
|
|
103
|
+
sha256: sha256File(patchPath),
|
|
104
|
+
size: readFileSync(patchPath).byteLength,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.warn(`[mirin release] skipping delta patch: ${e instanceof Error ? e.message : e}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
rmSync(newTar, { force: true });
|
|
113
|
+
|
|
114
|
+
const manifest = {
|
|
115
|
+
version: result.version,
|
|
116
|
+
channel: result.channel,
|
|
117
|
+
platform,
|
|
118
|
+
arch,
|
|
119
|
+
tarHash,
|
|
120
|
+
bundle: { url: bundleName, sha256: bundleSha, size: bundleSize },
|
|
121
|
+
patches,
|
|
122
|
+
};
|
|
123
|
+
const manifestName = `${prefix}-update.json`;
|
|
124
|
+
await Bun.write(join(outDir, manifestName), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
125
|
+
|
|
126
|
+
// Distributable installer for first-time installs (the updater uses the
|
|
127
|
+
// .tar.zst/patch): a drag-to-Applications .dmg on macOS, a .zip of the app
|
|
128
|
+
// folder on Windows.
|
|
129
|
+
let installerName: string | undefined;
|
|
130
|
+
let installerSize = 0;
|
|
131
|
+
if (isWindows) {
|
|
132
|
+
installerName = `${prefix}-${safeName}.zip`;
|
|
133
|
+
console.log(`[mirin release] zipping installer → ${installerName}`);
|
|
134
|
+
const zipPath = join(outDir, installerName);
|
|
135
|
+
rmSync(zipPath, { force: true });
|
|
136
|
+
// bsdtar (Windows `tar`) creates a .zip from the extension with `-a`.
|
|
137
|
+
await $`tar -a -cf ${zipPath} -C ${buildDir} ${appArtifact}`;
|
|
138
|
+
installerSize = readFileSync(zipPath).byteLength;
|
|
139
|
+
} else if (result.dmg !== false) {
|
|
140
|
+
installerName = `${prefix}-${safeName}.dmg`;
|
|
141
|
+
console.log(`[mirin release] building installer → ${installerName}`);
|
|
142
|
+
const options: DmgOptions = typeof result.dmg === "object" ? result.dmg : {};
|
|
143
|
+
const dmgPath = await buildDmg({
|
|
144
|
+
app: result.app,
|
|
145
|
+
appName: result.appName,
|
|
146
|
+
outDir,
|
|
147
|
+
fileName: installerName,
|
|
148
|
+
options,
|
|
149
|
+
projectDir: result.projectDir,
|
|
150
|
+
signIdentity: result.signIdentity,
|
|
151
|
+
});
|
|
152
|
+
await notarizeAndStaple(dmgPath); // no-op without notary credentials
|
|
153
|
+
installerSize = readFileSync(dmgPath).byteLength;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const mb = (n: number) => (n / 1e6).toFixed(1);
|
|
157
|
+
console.log(`\n[mirin release] done → build/release/`);
|
|
158
|
+
console.log(` ${manifestName}`);
|
|
159
|
+
console.log(` ${bundleName} (${mb(bundleSize)} MB)`);
|
|
160
|
+
for (const p of patches) console.log(` ${p.url} (${mb(p.size)} MB delta from ${p.fromVersion})`);
|
|
161
|
+
if (installerName) console.log(` ${installerName} (${mb(installerSize)} MB installer)`);
|
|
162
|
+
console.log(`\nUpload all of build/release/ to: ${result.baseUrl}`);
|
|
163
|
+
if (existsSync(join(outDir, "_new.tar"))) rmSync(join(outDir, "_new.tar"), { force: true });
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
package/src/temps.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove `bun build --compile` scratch files (`.*.bun-build`) from a directory.
|
|
3
|
+
*
|
|
4
|
+
* `bun build --compile` writes these temp files into the working directory and
|
|
5
|
+
* normally cleans them up — but a hard-killed build leaves them behind, where
|
|
6
|
+
* they accumulate in the project root. The dev/build commands sweep them first.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readdirSync, rmSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
export function sweepBuildTemps(dir: string): void {
|
|
13
|
+
let entries: string[];
|
|
14
|
+
try {
|
|
15
|
+
entries = readdirSync(dir);
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const name of entries) {
|
|
20
|
+
if (name.endsWith(".bun-build")) {
|
|
21
|
+
rmSync(join(dir, name), { force: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|