The helper at scripts/setup/fetch-electron-binary.js was bare
`require('@electron/get')`. Node resolves that relative to the
script's directory and walks up — so it searches
$project_root/scripts/setup/node_modules, $project_root/scripts/
node_modules, $project_root/node_modules. None of those have
@electron/get; the package is installed in $work_dir/node_modules
by setup_electron_asar at install time.
On the current electron@41.5.0 pin this is dormant — the upstream
postinstall populates dist/, the helper short-circuits, and the
broken require() never runs. Caught by validating the helper path
against an electron@42 sandbox build, which is exactly the scenario
the helper exists to handle.
Fix: createRequire(path.join(cwd, 'package.json')) so module
resolution is anchored at work_dir. The temporary package.json that
setup_electron_asar writes is always present at this point.
Co-Authored-By: Claude <claude@anthropic.com>
The dependency map sends gcc/g++/make all to build-essential and
wrestool/icotool both to icoutils. The previous loop appended once
per missing command, so a clean machine printed:
System dependencies needed: p7zip-full wget icoutils icoutils
imagemagick build-essential build-essential build-essential python3
apt dedupes internally so the install worked, but the log line read
as if the script was broken. Skip the append when the package is
already in deps_to_install; the bordered substring match handles
both build-essential and icoutils with a single rule.
Verified in a clean debian:stable-slim container with no build tools
present: build-essential and icoutils now appear exactly once in
each invocation.
Addresses review feedback on #401.
Re-add the exact Electron version pin that #586 had, hoisted to a
named local in scripts/setup/dependencies.sh so the next bump is
intentional. Upstream Claude Desktop's app.asar binds to a specific
V8/NAPI ABI, Chromium pairing, and node-pty native surface; running
a different Electron major against it is unsupported and was the
reproducibility hole left when this PR dropped the ^41 pin.
Also:
- Wrap node fetch-electron-binary.js in a 2-attempt retry so a
transient 503 from electron's CDN doesn't red the whole build.
- Whitelist supported archs (x64, arm64, armv7l, ia32) with an
actionable error in fetch-electron-binary.js, instead of 404'ing
on electron's release server for exotic-arch hosts.
- Document ELECTRON_MIRROR / ELECTRON_CUSTOM_DIR in docs/BUILDING.md
and clarify that ELECTRON_CACHE is NOT honored by @electron/get
(the original PR body's claim was wrong).
Refs #584. Addresses self-review feedback on #587.
Co-Authored-By: Claude <claude@anthropic.com>
Two complementary changes that close the silent-failure path surfaced
during testing for #401.
1. install_node_pty: when `npm install node-pty` fails, abort the build
with an explicit error message that names the likely cause (missing
C/C++ toolchain or python3) and the per-distro fix command. Previously
the function printed a one-line warning and continued, which left
pty_src_dir empty, skipped the entire copy block, and shipped the
upstream Windows node-pty binaries unchanged — exactly the failure
mode in #401.
2. check_dependencies: add gcc, g++, make, python3 to the dependency
check so the build environment is auto-provisioned before
install_node_pty runs (build-essential on Debian/Ubuntu, gcc /
gcc-c++ / make / python3 on Fedora). Skipped when --node-pty-dir
is set (Nix and explicit overrides bring their own pre-built node-pty).
Together: the dep check makes the failure rare, and the install_node_pty
abort makes the rare failure obvious instead of silent.
Verified two ways:
- Isolated test: forced npm to a fake binary that exits 1, called
install_node_pty, confirmed exit code 1, error message contains the
Debian/Fedora install hints, and the staging copy block did not run.
- Full deb build: ran build.sh in an ubuntu:24.04 container with no
pre-installed compilers (only ca-certificates, wget, sudo, nodejs,
npm). check_dependencies ran `apt install ... build-essential`
automatically; npm install node-pty subsequently compiled cleanly;
shipped pty.node is ELF 64-bit LSB shared object, x86-64.
The hotfix in #586 pinned electron to ^41 because electron@42.0.0
removed the postinstall that fetches the prebuilt binary into
node_modules/electron/dist/. That left us tethered to electron 41.x
and would re-break whenever 41 ages out of npm or the upstream
behavior shifts again.
This change does the binary fetch ourselves so we no longer depend on
electron's postinstall:
* New scripts/setup/fetch-electron-binary.js uses @electron/get to
download the matching prebuilt binary for the host platform/arch
and extract-zip to unpack it into dist/. Reads the version from
the installed electron's package.json so it always matches what
npm resolved.
* setup_electron_asar runs the helper only when npm install leaves
dist/ missing. If electron later restores the postinstall, this
is a no-op.
* Drops the 'electron@^41' pin so we follow latest electron again.
* @electron/get honors ELECTRON_MIRROR / ELECTRON_CACHE, so users
behind proxies/mirrors keep working without further changes.
Refs #584
Co-Authored-By: Claude <claude@anthropic.com>
electron@42.0.0 (published 2026-05-06) removed the postinstall script
that downloads the prebuilt binary into node_modules/electron/dist.
Without dist/ the existence check in setup_electron_asar fails and the
build aborts with "Failed to find Electron distribution directory".
Pin to electron@^41 as a hotfix to unblock source builds. A durable
fix using @electron/get to fetch the binary explicitly will land
separately so we no longer depend on electron's postinstall behavior.
Refs #584
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
Splits the 2124-line build.sh into a 318-line orchestrator plus
16 topical modules, grouped so CODEOWNERS can assign per-subsystem
reviewers:
scripts/_common.sh shared shell utilities
scripts/setup/ host detection, deps, download
scripts/patches/ regex patches on minified JS
_common.sh extract_electron_variable etc.
app-asar.sh wrapper injection
titlebar.sh
tray.sh menu handler + icon selection
quick-window.sh
claude-code.sh
cowork.sh cowork linux patching (largest)
scripts/staging/ post-patch file staging
build.sh now sources each module in dependency order and retains
only run_packaging, cleanup_build, print_next_steps, and main.
All globals stay at the top of build.sh and are read by sourced
modules; each module's header documents which globals it reads and
mutates (implicit-contract documentation).
This is a pure-move refactor. Function bodies were copied verbatim
— verified by byte-identical diff of the function set vs the
pre-split build.sh (34 functions, all present with identical bodies).
Note: .github/workflows/shellcheck.yml may benefit from a '-x' flag
so shellcheck follows the new '# shellcheck source=' directives, but
that CI tweak is left as a separate concern.
Co-Authored-By: Claude <claude@anthropic.com>