-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat(linux): embed AppImage update information and provide .zsync delta updates (#1021) #1033
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| import { execFileSync } from "node:child_process"; | ||
| import { | ||
| closeSync, | ||
| existsSync, | ||
| openSync, | ||
| readdirSync, | ||
| readSync, | ||
| statSync, | ||
| writeSync, | ||
| } from "node:fs"; | ||
| import path from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const projectRoot = process.cwd(); | ||
| const releaseRoot = path.join(projectRoot, "release"); | ||
|
|
||
| const DEFAULT_OWNER = "webadderallorg"; | ||
| const DEFAULT_REPO = "Recordly"; | ||
|
|
||
| function findElfUpdInfoSection(filePath) { | ||
| let fd; | ||
| try { | ||
| fd = openSync(filePath, "r"); | ||
| const headerBuf = Buffer.alloc(64); | ||
| readSync(fd, headerBuf, 0, 64, 0); | ||
|
|
||
| if (headerBuf.toString("utf8", 0, 4) !== "\x7fELF") { | ||
| return null; | ||
| } | ||
|
|
||
| const is64Bit = headerBuf[4] === 2; | ||
| if (!is64Bit) { | ||
| return null; | ||
| } | ||
|
|
||
| const e_shoff = Number(headerBuf.readBigUInt64LE(40)); | ||
| const e_shentsize = headerBuf.readUInt16LE(58); | ||
| const e_shnum = headerBuf.readUInt16LE(60); | ||
| const e_shstrndx = headerBuf.readUInt16LE(62); | ||
|
|
||
| if (e_shnum === 0 || e_shentsize === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| const shTableBuf = Buffer.alloc(e_shentsize * e_shnum); | ||
| readSync(fd, shTableBuf, 0, shTableBuf.length, e_shoff); | ||
|
|
||
| const strtabHeaderOffset = e_shstrndx * e_shentsize; | ||
| const strtabOffset = Number(shTableBuf.readBigUInt64LE(strtabHeaderOffset + 24)); | ||
| const strtabSize = Number(shTableBuf.readBigUInt64LE(strtabHeaderOffset + 32)); | ||
|
|
||
| const strtabBuf = Buffer.alloc(strtabSize); | ||
| readSync(fd, strtabBuf, 0, strtabSize, strtabOffset); | ||
|
|
||
| for (let i = 0; i < e_shnum; i++) { | ||
| const off = i * e_shentsize; | ||
| const nameIdx = shTableBuf.readUInt32LE(off); | ||
| const nameEnd = strtabBuf.indexOf(0, nameIdx); | ||
| const name = strtabBuf.toString("utf8", nameIdx, nameEnd === -1 ? undefined : nameEnd); | ||
|
|
||
| if (name === ".upd_info") { | ||
| return { | ||
| offset: Number(shTableBuf.readBigUInt64LE(off + 24)), | ||
| size: Number(shTableBuf.readBigUInt64LE(off + 32)), | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } finally { | ||
| if (fd !== undefined) { | ||
| closeSync(fd); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export function embedUpdateInfoInAppImage(filePath, updateInfoString) { | ||
| const section = findElfUpdInfoSection(filePath); | ||
| if (!section) { | ||
| console.warn(`[appimage-updateinfo] Warning: .upd_info section not found in ${filePath}`); | ||
| return false; | ||
| } | ||
|
|
||
| const updateInfoBuffer = Buffer.from(updateInfoString, "utf8"); | ||
| if (updateInfoBuffer.length >= section.size) { | ||
| throw new Error( | ||
| `Update info string too long (${updateInfoBuffer.length} bytes, max ${section.size - 1} bytes)`, | ||
| ); | ||
| } | ||
|
|
||
| const padded = Buffer.alloc(section.size); | ||
| updateInfoBuffer.copy(padded, 0); | ||
|
|
||
| let fd; | ||
| try { | ||
| fd = openSync(filePath, "r+"); | ||
| writeSync(fd, padded, 0, padded.length, section.offset); | ||
| console.log( | ||
| `[appimage-updateinfo] Embedded update information into ${path.basename(filePath)} (${updateInfoString})`, | ||
| ); | ||
| return true; | ||
| } finally { | ||
| if (fd !== undefined) { | ||
| closeSync(fd); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export function generateZsyncFile(appImagePath, zsyncOutputPath) { | ||
| const appImageFileName = path.basename(appImagePath); | ||
| try { | ||
| execFileSync( | ||
| "zsyncmake", | ||
| ["-u", appImageFileName, "-o", zsyncOutputPath, appImagePath], | ||
| { stdio: "inherit" }, | ||
| ); | ||
| console.log(`[appimage-updateinfo] Generated zsync control file: ${zsyncOutputPath}`); | ||
| return true; | ||
| } catch (err) { | ||
| console.warn( | ||
| `[appimage-updateinfo] zsyncmake failed or is not available. Please install 'zsync' to generate delta files: ${err.message}`, | ||
| ); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| function processReleaseAppImages() { | ||
| if (!existsSync(releaseRoot)) { | ||
| console.log("[appimage-updateinfo] Release directory does not exist. Skipping."); | ||
| return; | ||
| } | ||
|
|
||
| const appImageFiles = readdirSync(releaseRoot) | ||
| .filter((f) => f.endsWith(".AppImage")) | ||
| .map((f) => path.join(releaseRoot, f)) | ||
| .filter((f) => statSync(f).isFile()); | ||
|
|
||
| if (appImageFiles.length === 0) { | ||
| console.log("[appimage-updateinfo] No .AppImage files found in release directory."); | ||
| return; | ||
| } | ||
|
|
||
| const owner = process.env.GITHUB_REPOSITORY_OWNER || DEFAULT_OWNER; | ||
| const repo = process.env.GITHUB_REPOSITORY?.split("/")[1] || DEFAULT_REPO; | ||
|
|
||
| let hasFailures = false; | ||
|
|
||
| for (const appImagePath of appImageFiles) { | ||
| const fileName = path.basename(appImagePath); | ||
| const zsyncFileName = `${fileName}.zsync`; | ||
| const zsyncOutputPath = path.join(releaseRoot, zsyncFileName); | ||
|
|
||
| const updateInfoString = | ||
| process.env.APPIMAGE_UPDATE_INFO || | ||
| `gh-releases-zsync|${owner}|${repo}|latest|${zsyncFileName}`; | ||
|
|
||
| const embedded = embedUpdateInfoInAppImage(appImagePath, updateInfoString); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- changed files ---'
git diff --name-only 18884285b11b3603fc4ccede89add40e0e4a9bd6 9b4a39d3e7a46daa4f959c07ef9d29f44efd3567
printf '%s\n' '--- target script ---'
cat -n scripts/embed-appimage-updateinfo.mjs
printf '%s\n' '--- workflow references ---'
rg -n -C 8 'embed-appimage|electron-builder|latest-linux\.yml|zsyncmake|release/' .github scripts package.json electron-builder.yml 2>/dev/null || trueRepository: webadderallorg/Recordly Length of output: 41665 🏁 Script executed: pwd; git diff --stat 18884285b11b3603fc4ccede89add40e0e4a9bd6 9b4a39d3e7a46daa4f959c07ef9d29f44efd3567; sed -n '120,190p' scripts/embed-appimage-updateinfo.mjs; rg -n -C 6 'embed-appimage|electron-builder|latest-linux\.yml|zsyncmake' .github scripts package.json 2>/dev/null || trueRepository: webadderallorg/Recordly Length of output: 24782 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- electron-builder configuration ---'
cat -n electron-builder.json5
printf '%s\n' '--- package lock versions ---'
rg -n -m 5 '"electron-builder"|"electron-updater"' package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock 2>/dev/null || true
printf '%s\n' '--- metadata and checksum helpers ---'
cat -n scripts/write-release-checksums.mjs
rg -n -C 8 'latest-linux|sha512|sha256|checksum|metadata' scripts .github/workflows/release.yml .github/workflows/build.yml electron-builder.json5Repository: webadderallorg/Recordly Length of output: 41039 Update
🤖 Prompt for AI Agents |
||
| if (!embedded) { | ||
| hasFailures = true; | ||
| } | ||
|
|
||
| const zsyncGenerated = generateZsyncFile(appImagePath, zsyncOutputPath); | ||
| if (!zsyncGenerated) { | ||
| hasFailures = true; | ||
| } | ||
| } | ||
|
|
||
| if (hasFailures && process.env.CI) { | ||
| console.error( | ||
| "[appimage-updateinfo] Error: Failed to embed update information or generate zsync in CI environment.", | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { | ||
| processReleaseAppImages(); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.