diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 5bf6dc9..638b80b 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -41,6 +41,17 @@ The build script automatically detects your distribution and selects the appropr | Arch Linux | `.AppImage` (via AUR) | yay/paru | | Other | `.AppImage` | - | +## Build Environment Variables + +The build pulls the Electron prebuilt binary from `github.com/electron/electron/releases` via `@electron/get`. Two upstream environment variables let you redirect that fetch: + +- `ELECTRON_MIRROR` — base URL to fetch Electron releases from instead of GitHub. Useful for mirrors or local proxies. Example: `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/`. +- `ELECTRON_CUSTOM_DIR` — overrides the path segment after the mirror. Defaults to `v{version}`. + +The cache location is fixed at `~/.cache/electron/` (resolved by `@electron/get` via `envPaths`) and is reused across builds. `ELECTRON_CACHE` is **not** read by `@electron/get` — set `ELECTRON_MIRROR` if you need to avoid the public CDN. + +The pinned Electron version lives in `scripts/setup/dependencies.sh` (`electron_version`) and must match `build-reference/app-extracted/package.json` — the upstream Claude Desktop `app.asar` is built against a specific Electron major and running a different one is unsupported. + ## Installing the Built Package ### For .deb packages (Debian/Ubuntu) diff --git a/scripts/setup/dependencies.sh b/scripts/setup/dependencies.sh index 659db05..3f39425 100644 --- a/scripts/setup/dependencies.sh +++ b/scripts/setup/dependencies.sh @@ -198,6 +198,13 @@ setup_nodejs() { setup_electron_asar() { section_header 'Electron & Asar Handling' + # Pin Electron to the exact version upstream Claude Desktop ships + # (build-reference/app-extracted/package.json). The shipped app.asar + # binds to specific V8/NAPI ABI, Chromium pairing, and node-pty + # native surface — running a different Electron major against this + # asar is unsupported. Bump when upstream bumps. + local electron_version='41.5.0' + echo "Ensuring local Electron and Asar installation in $work_dir..." cd "$work_dir" || exit 1 @@ -214,17 +221,34 @@ setup_electron_asar() { [[ ! -f $asar_bin_path ]] && echo 'Asar binary not found.' && install_needed=true if [[ $install_needed == true ]]; then - echo "Installing Electron and Asar locally into $work_dir..." - # Pin to electron 41.x: electron@42.0.0 (2026-05-06) dropped the - # postinstall that fetches the prebuilt binary into dist/, leaving - # node_modules/electron/dist absent and the build aborting (#584). - # A durable fix using @electron/get is tracked separately. - if ! npm install --no-save 'electron@^41' @electron/asar; then + echo "Installing electron@${electron_version} and Asar locally into $work_dir..." + if ! npm install --no-save \ + "electron@${electron_version}" @electron/asar @electron/get extract-zip; then echo 'Failed to install Electron and/or Asar locally.' >&2 cd "$project_root" || exit 1 exit 1 fi echo 'Electron and Asar installation command finished.' + + # electron@42+ no longer ships a postinstall script that fetches + # the prebuilt binary into dist/. If npm didn't populate it, fetch + # the matching binary explicitly via @electron/get. See #584. + # Retry once on transient CDN failures (503, network drops). + if [[ ! -d $electron_dist_path ]]; then + echo 'Electron postinstall did not populate dist/; fetching binary explicitly...' + local fetch_attempts=0 + while ! node "$project_root/scripts/setup/fetch-electron-binary.js"; do + fetch_attempts=$((fetch_attempts + 1)) + if (( fetch_attempts >= 2 )); then + echo 'Failed to fetch Electron binary via @electron/get after 2 attempts.' >&2 + echo 'For air-gapped or mirrored builds set ELECTRON_MIRROR or ELECTRON_CUSTOM_DIR; see docs/BUILDING.md.' >&2 + cd "$project_root" || exit 1 + exit 1 + fi + echo "Retrying Electron binary fetch (attempt $((fetch_attempts + 1))/2)..." + sleep 2 + done + fi else echo 'Local Electron distribution and Asar binary already present.' fi diff --git a/scripts/setup/fetch-electron-binary.js b/scripts/setup/fetch-electron-binary.js new file mode 100644 index 0000000..63b10a9 --- /dev/null +++ b/scripts/setup/fetch-electron-binary.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node +// Fetches the Electron prebuilt binary into node_modules/electron/dist/. +// +// electron@42.0.0 (2026-05-06) removed the postinstall script that +// historically populated dist/ during `npm install`. This helper restores +// that behavior using @electron/get + extract-zip, so the rest of the +// build pipeline (which depends on the dist/ layout) keeps working. +// +// Run from the directory containing node_modules/electron. Reads the +// installed electron version from its package.json and downloads the +// matching binary for the host platform/arch. +// +// See: https://github.com/aaddrick/claude-desktop-debian/issues/584 + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { createRequire } = require('node:module'); + +async function main() { + const cwd = process.cwd(); + const electronModuleDir = path.join(cwd, 'node_modules', 'electron'); + const distDir = path.join(electronModuleDir, 'dist'); + + if (!fs.existsSync(electronModuleDir)) { + throw new Error( + `Electron module not found at ${electronModuleDir}; ` + + "run 'npm install electron' first.", + ); + } + + const pkgPath = path.join(electronModuleDir, 'package.json'); + const { version } = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + if (!version) { + throw new Error(`Could not read version from ${pkgPath}`); + } + + const platform = 'linux'; + // node's process.arch values map cleanly to electron release archs, + // except 'arm' which electron publishes as 'armv7l'. + const arch = process.arch === 'arm' ? 'armv7l' : process.arch; + + const supportedArchs = ['x64', 'arm64', 'armv7l', 'ia32']; + if (!supportedArchs.includes(arch)) { + throw new Error( + `Unsupported architecture: ${arch}. ` + + `Electron publishes Linux binaries for ${supportedArchs.join(', ')}.`, + ); + } + + // Resolve @electron/get and extract-zip from the work-dir's + // node_modules. The script lives at scripts/setup/ so a plain + // require() walks up from there and never sees work_dir/. + const workDirRequire = createRequire(path.join(cwd, 'package.json')); + const { downloadArtifact } = workDirRequire('@electron/get'); + const extractZip = workDirRequire('extract-zip'); + + console.log(`Fetching electron@${version} for ${platform}-${arch}...`); + const zipPath = await downloadArtifact({ + version, + platform, + arch, + artifactName: 'electron', + }); + + console.log(`Extracting ${zipPath} into ${distDir}`); + fs.mkdirSync(distDir, { recursive: true }); + await extractZip(zipPath, { dir: distDir }); + + const electronBin = path.join(distDir, 'electron'); + if (fs.existsSync(electronBin)) { + fs.chmodSync(electronBin, 0o755); + } + + console.log('Electron binary fetched and extracted successfully.'); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : err); + process.exit(1); +});