Skip to content

Commit 5581e36

Browse files
beautyfreeclaude
andauthored
v0.2.3: run AppImage runtime patch after electron-builder, not in hook (#11)
v0.2.2's Linux build showed that electron-builder's `afterAllArtifactBuild` hook fires BEFORE `latest-linux.yml` gets written — so our hook patched the AppImage bytes but never found the YAML to update, leaving the sha512 in the manifest pointing at the original (pre-patch) file. electron-updater would have rejected the download on signature mismatch. Fix: demote the hook to a standalone CLI script and run it from the `dist:linux` npm script AFTER `electron-builder` has finished producing everything it writes (AppImage + latest-linux.yml both present on disk). "dist:linux": "electron-vite build && electron-builder --linux && node scripts/patch-appimage-runtime.mjs artifacts" Script logic is identical — download static runtime once, splice it over the original runtime prefix of each .AppImage in the given dir, recompute sha512/size, rewrite matching entries in latest-linux.yml. Just runs at the right time now. Removed `afterAllArtifactBuild` from electron-builder.yml since the script is no longer wired there. Version bump to 0.2.3. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cf26a2e commit 5581e36

3 files changed

Lines changed: 42 additions & 34 deletions

File tree

electron-builder.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ extraResources:
2626
asarUnpack:
2727
- "**/*.node"
2828

29-
# After every platform finishes, run post-processing. Currently: swap the
30-
# stock AppImage runtime for the static one from AppImage/type2-runtime so
31-
# the AppImage works on distros without libfuse2 (CachyOS and other Arch
32-
# spin-offs). Also recomputes sha512/size in latest-linux.yml.
33-
afterAllArtifactBuild: scripts/patch-appimage-runtime.mjs
34-
3529
# Notarization: electron-builder 26+ handles it natively when the expected env
3630
# vars are present (APPLE_API_KEY, APPLE_API_KEY_ID, APPLE_API_ISSUER for the
3731
# API-key flow; APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID as

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "skiller",
3-
"version": "0.2.2",
3+
"version": "0.2.3",
44
"description": "A desktop app for managing AI agent skills across Claude Code, Cursor, Copilot, Gemini CLI, and more",
55
"license": "MIT",
66
"author": {
@@ -21,7 +21,7 @@
2121
"typecheck": "tsc --noEmit && tsc -p tsconfig.node.json --noEmit",
2222
"dist:mac": "electron-vite build && electron-builder --mac && bash scripts/repack-dmg.sh",
2323
"dist:win": "electron-vite build && electron-builder --win",
24-
"dist:linux": "electron-vite build && electron-builder --linux"
24+
"dist:linux": "electron-vite build && electron-builder --linux && node scripts/patch-appimage-runtime.mjs artifacts"
2525
},
2626
"dependencies": {
2727
"@base-ui/react": "^1.3.0",

scripts/patch-appimage-runtime.mjs

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
1+
#!/usr/bin/env node
12
/**
2-
* electron-builder `afterAllArtifactBuild` hook.
3+
* Standalone CLI — patches every `*.AppImage` in the given directory to use
4+
* the static runtime from AppImage/type2-runtime, then rewrites matching
5+
* entries in `latest-linux.yml` so electron-updater's sha512 check still
6+
* passes.
37
*
4-
* Swaps the ELF runtime at the head of every produced AppImage with the
5-
* static runtime from https://github.com/AppImage/type2-runtime. That runtime
6-
* ships with squashfuse compiled in, so the resulting AppImage launches on
7-
* systems that have libfuse2, libfuse3, or no FUSE at all — which matters
8-
* most for Arch-based distros (CachyOS, Manjaro, EndeavourOS) where libfuse2
9-
* is not preinstalled.
8+
* Usage:
9+
* node scripts/patch-appimage-runtime.mjs <artifacts-dir>
1010
*
11-
* AppImage layout (type 2):
12-
* [ ELF runtime (~180 KB) | squashfs FS image ('hsqs' magic) ]
11+
* Designed to run AFTER `electron-builder --linux` has finished producing
12+
* both the AppImage and the update manifest (electron-builder's own
13+
* `afterAllArtifactBuild` hook fires before the manifest is written, so we
14+
* invoke this at the `dist:linux` npm-script level instead).
1315
*
14-
* We locate the squashfs magic, drop everything before it, prepend the new
15-
* runtime. Then we recompute sha512 + size and patch `latest-linux.yml` so
16-
* electron-updater's signature check keeps passing.
16+
* Why we patch: the stock AppImage runtime electron-builder ships dynamically
17+
* links against libfuse.so.2. Arch-based distros (CachyOS, Manjaro,
18+
* EndeavourOS) ship fuse3 by default, so double-clicking a stock AppImage
19+
* there fails with "dlopen failed for libfuse.so.2". The static runtime
20+
* bundles squashfuse, falls back to extract-and-run where kernel FUSE is
21+
* unavailable, and works on every distro.
1722
*/
1823
import { createHash } from "node:crypto";
1924
import { existsSync } from "node:fs";
20-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
21-
import { basename, dirname, join } from "node:path";
25+
import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
26+
import { basename, dirname, join, resolve } from "node:path";
2227
import YAML from "yaml";
2328

2429
const STATIC_RUNTIME_URL =
@@ -30,9 +35,7 @@ const RUNTIME_CACHE_PATH = join(RUNTIME_CACHE_DIR, "appimage-runtime-x86_64");
3035
const SQUASHFS_MAGIC = Buffer.from([0x68, 0x73, 0x71, 0x73]);
3136

3237
async function downloadStaticRuntime() {
33-
if (existsSync(RUNTIME_CACHE_PATH)) {
34-
return RUNTIME_CACHE_PATH;
35-
}
38+
if (existsSync(RUNTIME_CACHE_PATH)) return RUNTIME_CACHE_PATH;
3639
await mkdir(RUNTIME_CACHE_DIR, { recursive: true });
3740
console.log(
3841
`[patch-appimage] downloading static runtime from ${STATIC_RUNTIME_URL}`,
@@ -75,7 +78,7 @@ async function patchAppImage(appImagePath) {
7578
async function updateLatestLinuxYaml(ymlPath, appImageName, patched) {
7679
if (!existsSync(ymlPath)) {
7780
console.log(
78-
`[patch-appimage] ${basename(ymlPath)} not found — skipping manifest update`,
81+
`[patch-appimage] ${basename(ymlPath)} not found — skipping manifest update (auto-update will not work on Linux)`,
7982
);
8083
return;
8184
}
@@ -109,20 +112,31 @@ async function updateLatestLinuxYaml(ymlPath, appImageName, patched) {
109112
}
110113
}
111114

112-
export default async function afterAllArtifactBuild(buildResult) {
113-
const appImages = (buildResult?.artifactPaths ?? []).filter((p) =>
114-
p.endsWith(".AppImage"),
115-
);
115+
async function main() {
116+
const dir = resolve(process.argv[2] ?? "artifacts");
117+
if (!existsSync(dir)) {
118+
console.log(
119+
`[patch-appimage] directory ${dir} does not exist — nothing to do`,
120+
);
121+
return;
122+
}
123+
const entries = await readdir(dir);
124+
const appImages = entries
125+
.filter((e) => e.endsWith(".AppImage"))
126+
.map((e) => join(dir, e));
116127
if (appImages.length === 0) {
117-
return [];
128+
console.log(`[patch-appimage] no .AppImage files in ${dir}`);
129+
return;
118130
}
119131

120132
for (const appImagePath of appImages) {
121133
const patched = await patchAppImage(appImagePath);
122134
const ymlPath = join(dirname(appImagePath), "latest-linux.yml");
123135
await updateLatestLinuxYaml(ymlPath, basename(appImagePath), patched);
124136
}
125-
126-
// Don't advertise additional artifacts — we mutated existing ones in place.
127-
return [];
128137
}
138+
139+
void main().catch((err) => {
140+
console.error("[patch-appimage] FAILED:", err);
141+
process.exit(1);
142+
});

0 commit comments

Comments
 (0)