@mirinjs/cli 0.0.1-alpha.11 → 0.0.1-alpha.12
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/release.ts +75 -29
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.12",
|
|
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.12",
|
|
31
|
+
"mirinjs": "0.0.1-alpha.12"
|
|
31
32
|
},
|
|
32
33
|
"optionalDependencies": {
|
|
33
|
-
"@mirinjs/darwin-arm64": "0.0.1-alpha.
|
|
34
|
+
"@mirinjs/darwin-arm64": "0.0.1-alpha.12"
|
|
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/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;
|
|
@@ -54,32 +57,75 @@ export async function release(projectDir = process.cwd()): Promise<number> {
|
|
|
54
57
|
rmSync(zip, { force: true });
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
const
|
|
58
|
-
|
|
60
|
+
const codec = loadCodec(result.coreDylib);
|
|
61
|
+
|
|
62
|
+
// Uncompressed tar — the identity + diff/patch basis (BSD tar keeps symlinks).
|
|
63
|
+
const newTar = join(outDir, "_new.tar");
|
|
64
|
+
await $`tar -cf ${newTar} -C ${buildDir} ${`${result.appName}.app`}`;
|
|
65
|
+
const tarHash = sha256File(newTar);
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
// Full bundle: zstd(newTar).
|
|
68
|
+
const bundleName = `${prefix}-${safeName}.app.tar.zst`;
|
|
69
|
+
const bundlePath = join(outDir, bundleName);
|
|
70
|
+
console.log(`[mirin release] compressing → ${bundleName}`);
|
|
71
|
+
codec.compress(newTar, bundlePath, 19);
|
|
72
|
+
const bundleSha = sha256File(bundlePath);
|
|
73
|
+
const bundleSize = readFileSync(bundlePath).byteLength;
|
|
74
|
+
|
|
75
|
+
// Delta patch vs the previous release (if reachable). Best-effort.
|
|
76
|
+
const patches: Array<{ fromVersion: string; url: string; sha256: string; size: number }> = [];
|
|
77
|
+
try {
|
|
78
|
+
const prevRes = await fetch(`${base}/${prefix}-update.json?t=${Date.now()}`, { redirect: "follow" });
|
|
79
|
+
if (prevRes.ok) {
|
|
80
|
+
const prev = (await prevRes.json()) as { version: string; bundle?: { url: string } };
|
|
81
|
+
if (prev.version && prev.version !== result.version && prev.bundle?.url) {
|
|
82
|
+
console.log(`[mirin release] generating delta ${prev.version} → ${result.version}…`);
|
|
83
|
+
const tmp = join(tmpdir(), `mirin-release-${Date.now()}`);
|
|
84
|
+
mkdirSync(tmp, { recursive: true });
|
|
85
|
+
const prevZst = join(tmp, "prev.tar.zst");
|
|
86
|
+
const dl = await fetch(`${base}/${prev.bundle.url}`, { redirect: "follow" });
|
|
87
|
+
if (!dl.ok || !dl.body) throw new Error(`prev bundle ${dl.status}`);
|
|
88
|
+
await Bun.write(prevZst, await dl.arrayBuffer());
|
|
89
|
+
const prevTar = join(tmp, "prev.tar");
|
|
90
|
+
codec.decompress(prevZst, prevTar);
|
|
91
|
+
const rawPatch = join(tmp, "patch.bin");
|
|
92
|
+
codec.diff(prevTar, newTar, rawPatch); // bsdiff
|
|
93
|
+
const patchName = `${prefix}-${prev.version}.patch`;
|
|
94
|
+
const patchPath = join(outDir, patchName);
|
|
95
|
+
codec.compress(rawPatch, patchPath, 19);
|
|
96
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
97
|
+
patches.push({
|
|
98
|
+
fromVersion: prev.version,
|
|
99
|
+
url: patchName,
|
|
100
|
+
sha256: sha256File(patchPath),
|
|
101
|
+
size: readFileSync(patchPath).byteLength,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.warn(`[mirin release] skipping delta patch: ${e instanceof Error ? e.message : e}`);
|
|
107
|
+
}
|
|
63
108
|
|
|
64
|
-
|
|
65
|
-
const sha256 = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
109
|
+
rmSync(newTar, { force: true });
|
|
66
110
|
|
|
67
111
|
const manifest = {
|
|
68
112
|
version: result.version,
|
|
69
113
|
channel: result.channel,
|
|
70
114
|
platform,
|
|
71
115
|
arch,
|
|
72
|
-
|
|
73
|
-
sha256,
|
|
74
|
-
|
|
116
|
+
tarHash,
|
|
117
|
+
bundle: { url: bundleName, sha256: bundleSha, size: bundleSize },
|
|
118
|
+
patches,
|
|
75
119
|
};
|
|
76
120
|
const manifestName = `${prefix}-update.json`;
|
|
77
121
|
await Bun.write(join(outDir, manifestName), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
78
122
|
|
|
79
|
-
const mb = (
|
|
123
|
+
const mb = (n: number) => (n / 1e6).toFixed(1);
|
|
80
124
|
console.log(`\n[mirin release] done → build/release/`);
|
|
81
125
|
console.log(` ${manifestName}`);
|
|
82
|
-
console.log(` ${
|
|
83
|
-
console.log(
|
|
126
|
+
console.log(` ${bundleName} (${mb(bundleSize)} MB)`);
|
|
127
|
+
for (const p of patches) console.log(` ${p.url} (${mb(p.size)} MB delta from ${p.fromVersion})`);
|
|
128
|
+
console.log(`\nUpload all of build/release/ to: ${result.baseUrl}`);
|
|
129
|
+
if (existsSync(join(outDir, "_new.tar"))) rmSync(join(outDir, "_new.tar"), { force: true });
|
|
84
130
|
return 0;
|
|
85
131
|
}
|