mirror of
https://github.com/aaddrick/claude-desktop-debian.git
synced 2026-05-17 08:36:35 +03:00
Compare commits
29 Commits
feature/58
...
3c43aef219
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c43aef219 | ||
|
|
43f1d71109 | ||
|
|
f3553ad6d3 | ||
|
|
7b416df933 | ||
|
|
f686705350 | ||
|
|
b017c72e8f | ||
|
|
25abb00e61 | ||
|
|
d632fdb253 | ||
|
|
04f9a18b69 | ||
|
|
16f1bc8be1 | ||
|
|
56d22ca97a | ||
|
|
c9df9e2f2d | ||
|
|
5a9c7eb00c | ||
|
|
57cfab8c37 | ||
|
|
8796aa2c82 | ||
|
|
b0be17dd36 | ||
|
|
3b86003a6b | ||
|
|
b23e0aea12 | ||
|
|
755f283431 | ||
|
|
cf085711f2 | ||
|
|
fa8f3441c0 | ||
|
|
3db7866e69 | ||
|
|
d5a4104684 | ||
|
|
15813ca11f | ||
|
|
429d191f77 | ||
|
|
ab5636ef29 | ||
|
|
0d67646d21 | ||
|
|
cf64b78611 | ||
|
|
f7c4daeb89 |
@@ -260,6 +260,13 @@ Special thanks to:
|
||||
- **[sirfaber](https://github.com/sirfaber)** for fixing the `$`-in-minified-identifier breakage of cowork Patch 2b (vm module assignment) and Patch 6 step 2 (retry-delay auto-launch) on Claude Desktop 1.5354.0 (#555)
|
||||
- **[ProfFlow](https://github.com/ProfFlow)** for re-fixing the RPM repodata signing regression by appending `!` to the keyid passed to `gpg --default-key`, forcing `repomd.xml` to be signed by the primary key instead of the auto-selected signing subkey (#566)
|
||||
- **[jslatten](https://github.com/jslatten)** for fixing the KDE Plasma Wayland launcher-grouping bug by setting `pkg.desktopName` in the packaged `app.asar`'s `package.json`, format-conditional so deb/rpm get `claude-desktop.desktop` and AppImage gets `io.github.aaddrick.claude-desktop-debian.desktop` (#562)
|
||||
- **[JoshuaVlantis](https://github.com/JoshuaVlantis)**
|
||||
- RPM `chrome-sandbox` SUID via `%attr(4755, ...)` instead of a `%post` chmod scriptlet so the bit survives `--noscripts` and layered images (#539)
|
||||
- `autoUpdater` no-op Proxy on Linux that defends against future feed activation, with a thenable allowlist masking `then`/`catch`/`finally`/`Symbol.toPrimitive`/`Symbol.iterator` to `undefined` (#567)
|
||||
- Failing loudly on `npm install node-pty` failures instead of silently shipping the upstream Windows binaries, plus auto-installing `gcc`/`g++`/`make`/`python3` on minimal build environments (#401)
|
||||
- **[Hayao0819](https://github.com/Hayao0819)** for diagnosing the upstream `titleBarStyle:""` → `titleBarStyle:"hiddenInset"` migration that broke the About window render on GNOME/X11 and contributing the `isPopupWindow()` match extension (#481, #489)
|
||||
- **[michelsfun](https://github.com/michelsfun)** for reporting the cowork `ENAMETOOLONG` failure on eCryptfs-encrypted home directories with detailed `--doctor` output that pinpointed the short-NAME_MAX filesystem as the cause (#590)
|
||||
- **[proffalken](https://github.com/proffalken)** for the LUKS-volume + `pam_mount` workaround documented in `TROUBLESHOOTING.md`, restoring cowork support on legacy eCryptfs-encrypted home directories (#590)
|
||||
|
||||
## Sponsorship
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -228,6 +228,128 @@ TMPDIR=~/.config/Claude/tmp claude-desktop
|
||||
|
||||
Or add `TMPDIR=%h/.config/Claude/tmp` to the `Exec=` line in your `.desktop` file.
|
||||
|
||||
### Cowork: ENAMETOOLONG on encrypted home (eCryptfs)
|
||||
|
||||
Cowork sessions can fail with an opaque `ENAMETOOLONG` error when
|
||||
`$HOME` is on a filesystem with a short filename limit. The common
|
||||
case is **eCryptfs** — the legacy "encrypted home" option on older
|
||||
Ubuntu and Linux Mint installs, which caps individual filenames at
|
||||
143 chars because of filename-encryption overhead. Standard
|
||||
filesystems (ext4, btrfs, xfs, zfs) cap at 255 chars and are fine.
|
||||
|
||||
**Why it happens:** Claude Code creates one directory per session
|
||||
under `~/.claude/projects/`, named after the sanitized host CWD. For
|
||||
cowork sessions the host CWD is the deeply nested outputs dir under
|
||||
`~/.config/Claude/local-agent-mode-sessions/<accountId>/<orgId>/local_<uuid>/outputs`,
|
||||
which sanitizes to ~180 chars — fits ext4 but exceeds the eCryptfs
|
||||
143-char ceiling.
|
||||
|
||||
**Diagnosis:** `claude-desktop --doctor` detects this automatically
|
||||
and emits a `[WARN] Filename limit: NAME_MAX=143…` line, plus an
|
||||
eCryptfs-specific hint when the filesystem type matches. You can
|
||||
also check by hand:
|
||||
|
||||
```bash
|
||||
df -T $HOME # look for type "ecryptfs"
|
||||
getconf NAME_MAX $HOME # eCryptfs reports 143; ext4 reports 255
|
||||
```
|
||||
|
||||
**Workaround:** move Claude's data onto a separate LUKS-encrypted
|
||||
ext4 volume (NAME_MAX = 255) and symlink the original paths back.
|
||||
`~/.claude/` is the critical one — that's where Claude Code creates
|
||||
the long-named per-session dirs that overflow the limit — and
|
||||
`~/.config/Claude/` plus `~/.cache/claude-desktop-debian/` are
|
||||
relocated alongside it so all Claude state lives on the same volume.
|
||||
This keeps the data encrypted at rest while sidestepping the
|
||||
eCryptfs filename-length cap.
|
||||
|
||||
```bash
|
||||
# 1. Create a 2 GB LUKS container
|
||||
sudo dd if=/dev/urandom of=/opt/claude-secure.img bs=1M count=2048 \
|
||||
status=progress
|
||||
sudo cryptsetup luksFormat /opt/claude-secure.img
|
||||
sudo cryptsetup open /opt/claude-secure.img claude-secure
|
||||
sudo mkfs.ext4 /dev/mapper/claude-secure
|
||||
|
||||
# 2. Mount and move Claude's data in
|
||||
sudo mkdir -p /mnt/claude-secure
|
||||
sudo mount /dev/mapper/claude-secure /mnt/claude-secure
|
||||
sudo chown "$USER:$USER" /mnt/claude-secure
|
||||
|
||||
mv ~/.config/Claude /mnt/claude-secure/Claude-config
|
||||
mv ~/.cache/claude-desktop-debian /mnt/claude-secure/claude-cache
|
||||
# ~/.claude may not exist yet on a fresh install — create the target
|
||||
# either way so the symlink below resolves.
|
||||
if [ -e ~/.claude ]; then
|
||||
mv ~/.claude /mnt/claude-secure/claude-home
|
||||
else
|
||||
mkdir -p /mnt/claude-secure/claude-home
|
||||
fi
|
||||
|
||||
ln -s /mnt/claude-secure/Claude-config ~/.config/Claude
|
||||
ln -s /mnt/claude-secure/claude-cache ~/.cache/claude-desktop-debian
|
||||
ln -s /mnt/claude-secure/claude-home ~/.claude
|
||||
|
||||
# 3. Verify the filename limit and the symlinks
|
||||
getconf NAME_MAX /mnt/claude-secure # should print 255
|
||||
mountpoint /mnt/claude-secure # confirms the volume is mounted
|
||||
readlink ~/.claude # /mnt/claude-secure/claude-home
|
||||
readlink ~/.config/Claude # /mnt/claude-secure/Claude-config
|
||||
```
|
||||
|
||||
**If you've set `CLAUDE_CONFIG_DIR`** (or otherwise reconfigured
|
||||
Claude Code to use a directory other than `~/.claude/`), the
|
||||
`~/.claude` symlink above doesn't apply — adapt the path to wherever
|
||||
your Claude Code config actually lives. The constraint is the same:
|
||||
the directory tree where Claude Code creates per-session project
|
||||
dirs must sit on a filesystem with `NAME_MAX` ≥ ~200.
|
||||
|
||||
**Auto-mount at login** with `pam_mount` so the volume unlocks
|
||||
without a manual `cryptsetup open`:
|
||||
|
||||
```bash
|
||||
sudo apt install libpam-mount
|
||||
```
|
||||
|
||||
Add a `<volume>` entry to `/etc/security/pam_mount.conf.xml`
|
||||
(replace `YOUR_USERNAME` with your login name):
|
||||
|
||||
```xml
|
||||
<volume user="YOUR_USERNAME" fstype="crypt"
|
||||
path="/opt/claude-secure.img"
|
||||
mountpoint="/mnt/claude-secure"
|
||||
options="" />
|
||||
```
|
||||
|
||||
`libpam-mount` registers itself with `/etc/pam.d/common-auth` and
|
||||
`/etc/pam.d/common-session` automatically on install.
|
||||
|
||||
**Notes:**
|
||||
- Tested on Linux Mint with LightDM as the display manager.
|
||||
- **LUKS passphrase tradeoff:** for `pam_mount` to unlock silently
|
||||
at login the LUKS passphrase must match your login password. That
|
||||
means one compromise unlocks both your session and the encrypted
|
||||
volume — equivalent to the threat surface eCryptfs already had,
|
||||
but worth a deliberate choice. Use a distinct LUKS passphrase if
|
||||
you'd rather be prompted on each unlock.
|
||||
- **Confidentiality posture vs eCryptfs.** The LUKS image lives at
|
||||
`/opt/claude-secure.img`, outside `$HOME` and outside whatever
|
||||
encryption envelope eCryptfs gives you. If `pam_mount` ever fails
|
||||
silently — wrong passphrase, mount race at login, profile error —
|
||||
Claude won't start (the symlink targets won't exist), so writes
|
||||
fail loudly rather than landing on plaintext disk. Verify with
|
||||
`mountpoint /mnt/claude-secure` after login if you're unsure.
|
||||
- 2 GB is a conservative starting size; the Claude config
|
||||
directory can exceed 500 MB once cowork session history
|
||||
accumulates. Resize if needed.
|
||||
- This is a system-wide change that affects login flow — review
|
||||
the pam_mount config against your threat model before applying.
|
||||
|
||||
Credit: reported with detailed `--doctor` output by
|
||||
[@michelsfun](https://github.com/michelsfun); LUKS-volume workaround
|
||||
contributed by [@proffalken](https://github.com/proffalken) in
|
||||
[#590](https://github.com/aaddrick/claude-desktop-debian/issues/590).
|
||||
|
||||
### Authentication Errors (401)
|
||||
|
||||
If you encounter recurring "API Error: 401" messages after periods of inactivity, the cached OAuth token may need to be cleared. This is an upstream application issue reported in [#156](https://github.com/aaddrick/claude-desktop-debian/issues/156).
|
||||
|
||||
12
flake.lock
generated
12
flake.lock
generated
@@ -5,11 +5,11 @@
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1777678872,
|
||||
"narHash": "sha256-EPIFsulyon7Z1vLQq5Fk64GR8L7cQsT+IPhcsukVbgk=",
|
||||
"lastModified": 1777988971,
|
||||
"narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "5250617bffd85403b14dbf43c3870e7f255d2c16",
|
||||
"rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -20,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1777826146,
|
||||
"narHash": "sha256-wQ/iN5Zp5VIa3ebBibijPnLyKhor+xEbDy4d0goa9Zs=",
|
||||
"lastModified": 1778274207,
|
||||
"narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "73c703c22422b8951895a960959dbbaca7296492",
|
||||
"rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
}:
|
||||
let
|
||||
pname = "claude-desktop";
|
||||
version = "1.6259.0";
|
||||
version = "1.7196.0";
|
||||
|
||||
srcs = {
|
||||
x86_64-linux = fetchurl {
|
||||
url = "https://downloads.claude.ai/releases/win32/x64/1.6259.0/Claude-dc89db3be9b2bc795e0fda0ea3738b035a76ed46.exe";
|
||||
hash = "sha256-eNbLYQcyzHSWzTgDxyEubjPn92dggzoh+rnFsoyYD18=";
|
||||
url = "https://downloads.claude.ai/releases/win32/x64/1.7196.0/Claude-2dbd7802ab037cbb97d77be1a063241009b5e598.exe";
|
||||
hash = "sha256-7VVQQsj+1lPdYluQT8lYnzG3ab9yB9+zz8eN05PWCUA=";
|
||||
};
|
||||
aarch64-linux = fetchurl {
|
||||
url = "https://downloads.claude.ai/releases/win32/arm64/1.6259.0/Claude-dc89db3be9b2bc795e0fda0ea3738b035a76ed46.exe";
|
||||
hash = "sha256-yNX/2G5JvwvWi6L/TXvjbM0EnMmHol1AiGsdsCkZdxM=";
|
||||
url = "https://downloads.claude.ai/releases/win32/arm64/1.7196.0/Claude-2dbd7802ab037cbb97d77be1a063241009b5e598.exe";
|
||||
hash = "sha256-/ttSMNGfTkG0BmaIuGvZWd7pMm+Fk1CtfbSMG8NBsgo=";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -436,6 +436,56 @@ JSEOF
|
||||
fi
|
||||
}
|
||||
|
||||
# Diagnose short-filename-limit filesystems that break cowork session
|
||||
# initialization. Claude Code creates a per-session directory under
|
||||
# ~/.claude/projects/ whose name is the sanitized host CWD — for cowork
|
||||
# sessions that flattens to ~180 chars (the host CWD is the deeply
|
||||
# nested outputs dir under ~/.config/Claude/local-agent-mode-sessions/
|
||||
# <accountId>/<orgId>/local_<uuid>/outputs). On filesystems with a
|
||||
# short NAME_MAX — eCryptfs caps at 143 due to filename-encryption
|
||||
# overhead — that mkdir fails with ENAMETOOLONG and the session never
|
||||
# starts. Standard fs (ext4/btrfs/xfs/zfs) cap at 255 and are fine. See
|
||||
# #590.
|
||||
_doctor_check_filename_limit() {
|
||||
# Walk up from ~/.claude/projects to the first dir that exists so
|
||||
# getconf has something to query on a fresh install where the tree
|
||||
# hasn't been created yet. $HOME is the floor — stop there rather
|
||||
# than crossing into /.
|
||||
local probe_dir="$HOME/.claude/projects"
|
||||
while [[ ! -d $probe_dir ]]; do
|
||||
probe_dir=$(dirname "$probe_dir")
|
||||
[[ $probe_dir == "$HOME" || $probe_dir == / ]] && break
|
||||
done
|
||||
[[ -d $probe_dir ]] || return 0
|
||||
|
||||
local name_max
|
||||
name_max=$(getconf NAME_MAX "$probe_dir" 2>/dev/null) || return 0
|
||||
[[ $name_max =~ ^[0-9]+$ ]] || return 0
|
||||
|
||||
((name_max >= 200)) && return 0
|
||||
|
||||
_warn "Filename limit: NAME_MAX=$name_max on $probe_dir (< 200)"
|
||||
_info \
|
||||
'Cowork sessions create project-dir names up to ~180 chars' \
|
||||
'under ~/.claude/projects/; short limits cause ENAMETOOLONG'
|
||||
_info 'when Claude Code initializes a session inside cowork (#590).'
|
||||
|
||||
local fs_type
|
||||
fs_type=$(df --output=fstype "$probe_dir" 2>/dev/null \
|
||||
| awk 'NR==2 {print $1}')
|
||||
if [[ $fs_type == 'ecryptfs' ]]; then
|
||||
_info \
|
||||
'Detected eCryptfs (legacy Ubuntu/Mint encrypted home,' \
|
||||
'NAME_MAX=143 due to filename-encryption overhead).'
|
||||
_info \
|
||||
'Workaround: move ~/.config/Claude onto a separate' \
|
||||
'LUKS-encrypted ext4 volume (NAME_MAX=255) and symlink it'
|
||||
_info \
|
||||
'back. See docs/TROUBLESHOOTING.md "Cowork: ENAMETOOLONG' \
|
||||
'on encrypted home (eCryptfs)" for the worked steps.'
|
||||
fi
|
||||
}
|
||||
|
||||
# Surface a warning when systemd-coredump shows N+ recent Electron
|
||||
# crashes. The most common cause on Linux is the GPU process FATAL
|
||||
# exhaustion tracked in #583 — workaround for affected users is the
|
||||
@@ -947,6 +997,10 @@ print(len(servers))
|
||||
# Custom bwrap mount configuration
|
||||
_doctor_check_bwrap_mounts
|
||||
|
||||
# Short NAME_MAX on the host's ~/.claude tree (eCryptfs etc.)
|
||||
# blocks cowork session init with ENAMETOOLONG — see #590.
|
||||
_doctor_check_filename_limit
|
||||
|
||||
# -- Orphaned cowork daemon --
|
||||
# Uses the same live-UI detection as cleanup_orphaned_cowork_daemon
|
||||
# above: a live UI is an Electron main process on app.asar that is
|
||||
|
||||
@@ -81,15 +81,19 @@ const CLOSE_TO_TRAY = process.platform === 'linux'
|
||||
&& process.env.CLAUDE_QUIT_ON_CLOSE !== '1';
|
||||
console.log(`[Frame Fix] Close-to-tray: ${CLOSE_TO_TRAY ? 'on' : 'off'}`);
|
||||
|
||||
// Detect if a window intends to be frameless (popup/Quick Entry/About)
|
||||
// Quick Entry: titleBarStyle:"", skipTaskbar:true, transparent:true, resizable:false
|
||||
// About: titleBarStyle:"", skipTaskbar:true, resizable:false
|
||||
// Main: titleBarStyle:"", titleBarOverlay:false(linux), resizable (has minWidth)
|
||||
// The main window has minWidth set; popups do not.
|
||||
// Detect if a window intends to be frameless (popup/Quick Entry/About).
|
||||
// Window kinds — see build-reference/app-extracted/.vite/build/index.js:
|
||||
// Quick Entry: titleBarStyle:"hidden", frame:false (caught early)
|
||||
// About: titleBarStyle:"hiddenInset", no minWidth, no parent
|
||||
// Main: titleBarStyle:"hidden", minWidth:600
|
||||
// Hardware Buddy: titleBarStyle:"hiddenInset", parent set (child modal — keep frame)
|
||||
// minWidth excludes Main; the `parent` key excludes Hardware Buddy. About
|
||||
// went from "" to "hiddenInset" upstream, so the test matches either.
|
||||
function isPopupWindow(options) {
|
||||
if (!options) return false;
|
||||
if (options.frame === false) return true;
|
||||
if (options.titleBarStyle === '' && !options.minWidth) return true;
|
||||
if ('parent' in options) return false;
|
||||
if ((options.titleBarStyle === '' || options.titleBarStyle === 'hiddenInset') && !options.minWidth) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -117,6 +121,28 @@ const LINUX_CSS = `
|
||||
}
|
||||
`;
|
||||
|
||||
// autoUpdater no-op: every property access returns a chainable function
|
||||
// so `.on(...).once(...).setFeedURL(...).checkForUpdates()` is harmless.
|
||||
// `getFeedURL` returns '' so any code that inspects the URL gets a
|
||||
// well-typed empty string rather than undefined. `then`/`catch`/`finally`
|
||||
// and `Symbol.toPrimitive`/`Symbol.iterator` resolve to `undefined` so the
|
||||
// Proxy is not mistaken for a thenable (which would call chainNoop as
|
||||
// `then(resolve, reject)` and never resolve — silent await hang) or
|
||||
// asked to coerce to a primitive. Writes land on the target but are
|
||||
// shadowed by the get-trap. Defined once and reused across all
|
||||
// require('electron') calls. Linux-only; macOS/Windows still see the
|
||||
// real autoUpdater. See #567.
|
||||
const autoUpdaterNoop = new Proxy({}, {
|
||||
get(_target, prop) {
|
||||
if (prop === 'getFeedURL') return () => '';
|
||||
if (prop === 'then' || prop === 'catch' || prop === 'finally'
|
||||
|| prop === Symbol.toPrimitive || prop === Symbol.iterator) {
|
||||
return undefined;
|
||||
}
|
||||
return function chainNoop() { return autoUpdaterNoop; };
|
||||
},
|
||||
});
|
||||
|
||||
// Build the patched BrowserWindow class and Menu interceptor once,
|
||||
// on first require('electron'), then reuse via Proxy on every access.
|
||||
let PatchedBrowserWindow = null;
|
||||
@@ -741,6 +767,23 @@ X-GNOME-Autostart-enabled=true
|
||||
}
|
||||
});
|
||||
}
|
||||
if (prop === 'autoUpdater' && process.platform === 'linux') {
|
||||
// Force autoUpdater into a no-op on Linux. Upstream's bundled
|
||||
// app code sets a feed URL of api.anthropic.com/api/desktop/linux/...
|
||||
// when app.isPackaged is true (we set ELECTRON_FORCE_IS_PACKAGED=true
|
||||
// unconditionally). Today this is a happy accident: Electron's Linux
|
||||
// autoUpdater is unimplemented and logs "AutoUpdater is not supported
|
||||
// on Linux", so the calls no-op. If a future Electron implements it,
|
||||
// every install would start hitting that feed and would either 404
|
||||
// or — worse — receive content the install wasn't prepared for.
|
||||
// .deb/.rpm/AppImage updates flow through the OS package manager
|
||||
// (or AppImageUpdate); the Anthropic feed has no Linux artifacts.
|
||||
// We replace the entire autoUpdater object with a Proxy that
|
||||
// no-ops every method and returns chainable stubs for EventEmitter
|
||||
// calls so listener registration in the bundled code is harmless.
|
||||
// See #567.
|
||||
return autoUpdaterNoop;
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -233,14 +233,6 @@ install -Dm 755 $staging_dir/claude-desktop %{buildroot}/usr/bin/claude-desktop
|
||||
# Update desktop database for MIME types
|
||||
update-desktop-database /usr/share/applications &> /dev/null || true
|
||||
|
||||
# Set correct permissions for chrome-sandbox
|
||||
SANDBOX_PATH="/usr/lib/$package_name/node_modules/electron/dist/chrome-sandbox"
|
||||
if [ -f "\$SANDBOX_PATH" ]; then
|
||||
echo "Setting chrome-sandbox permissions..."
|
||||
chown root:root "\$SANDBOX_PATH" || echo "Warning: Failed to chown chrome-sandbox"
|
||||
chmod 4755 "\$SANDBOX_PATH" || echo "Warning: Failed to chmod chrome-sandbox"
|
||||
fi
|
||||
|
||||
%postun
|
||||
# Update desktop database after removal
|
||||
update-desktop-database /usr/share/applications &> /dev/null || true
|
||||
@@ -248,6 +240,7 @@ update-desktop-database /usr/share/applications &> /dev/null || true
|
||||
%files
|
||||
%defattr(-, root, root, 0755)
|
||||
%attr(755, root, root) /usr/bin/claude-desktop
|
||||
%attr(4755, root, root) /usr/lib/$package_name/node_modules/electron/dist/chrome-sandbox
|
||||
/usr/lib/$package_name
|
||||
/usr/share/applications/claude-desktop.desktop
|
||||
/usr/share/icons/hicolor/*/apps/claude-desktop.png
|
||||
|
||||
@@ -807,12 +807,27 @@ install_node_pty() {
|
||||
echo '{"name":"node-pty-build","version":"1.0.0","private":true}' > package.json
|
||||
|
||||
echo 'Installing node-pty (this compiles native module)...'
|
||||
if npm install node-pty 2>&1; then
|
||||
echo 'node-pty installed successfully'
|
||||
pty_src_dir="$node_pty_build_dir/node_modules/node-pty"
|
||||
else
|
||||
echo 'Failed to install node-pty - terminal features may not work'
|
||||
# Fail loudly on npm install failure rather than warn-and-continue.
|
||||
# The previous behavior silently dropped pty_src_dir, skipped the
|
||||
# entire copy block, and shipped the upstream Windows node-pty
|
||||
# binaries (the #401 failure mode). check_dependencies should now
|
||||
# install gcc/g++/make/python3 before we get here, so this branch
|
||||
# is the last line of defense for build-tool gaps that auto-install
|
||||
# couldn't fix (unknown distro, broken package mirror, etc.).
|
||||
if ! npm install node-pty 2>&1; then
|
||||
echo "Error: 'npm install node-pty' failed." >&2
|
||||
echo 'node-pty has a native module compiled via node-gyp;' >&2
|
||||
echo 'this usually means the build environment lacks a C/C++' >&2
|
||||
echo 'compiler, make, or python3.' >&2
|
||||
echo '' >&2
|
||||
echo 'Install build tools and re-run:' >&2
|
||||
echo ' Debian/Ubuntu: sudo apt install build-essential python3' >&2
|
||||
echo ' Fedora/RHEL: sudo dnf install gcc gcc-c++ make python3' >&2
|
||||
cd "$project_root" || exit 1
|
||||
exit 1
|
||||
fi
|
||||
echo 'node-pty installed successfully'
|
||||
pty_src_dir="$node_pty_build_dir/node_modules/node-pty"
|
||||
fi
|
||||
|
||||
if [[ -n $pty_src_dir && -d $pty_src_dir ]]; then
|
||||
|
||||
@@ -22,32 +22,51 @@ check_dependencies() {
|
||||
rpm) all_deps="$all_deps rpmbuild" ;;
|
||||
esac
|
||||
|
||||
# node-pty has a native C++ module compiled via node-gyp during
|
||||
# `npm install`. Without gcc/g++/make/python3 the install silently
|
||||
# emits a warning, leaves pty_src_dir empty, and the build ends up
|
||||
# shipping the upstream Windows binaries (the #401 failure mode).
|
||||
# Skip when --node-pty-dir is set (Nix and explicit overrides bring
|
||||
# their own pre-built node-pty).
|
||||
if [[ -z ${node_pty_dir:-} ]]; then
|
||||
all_deps="$all_deps gcc g++ make python3"
|
||||
fi
|
||||
|
||||
# Command-to-package mappings per distro family
|
||||
declare -A debian_pkgs=(
|
||||
[p7zip]='p7zip-full' [wget]='wget' [wrestool]='icoutils'
|
||||
[icotool]='icoutils' [convert]='imagemagick'
|
||||
[dpkg-deb]='dpkg-dev' [rpmbuild]='rpm'
|
||||
[gcc]='build-essential' [g++]='build-essential'
|
||||
[make]='build-essential' [python3]='python3'
|
||||
)
|
||||
declare -A rpm_pkgs=(
|
||||
[p7zip]='p7zip p7zip-plugins' [wget]='wget' [wrestool]='icoutils'
|
||||
[icotool]='icoutils' [convert]='ImageMagick'
|
||||
[dpkg-deb]='dpkg' [rpmbuild]='rpm-build'
|
||||
[gcc]='gcc' [g++]='gcc-c++'
|
||||
[make]='make' [python3]='python3'
|
||||
)
|
||||
|
||||
local cmd
|
||||
local cmd pkg
|
||||
for cmd in $all_deps; do
|
||||
if ! check_command "$cmd"; then
|
||||
case "$distro_family" in
|
||||
debian)
|
||||
deps_to_install="$deps_to_install ${debian_pkgs[$cmd]}"
|
||||
;;
|
||||
rpm)
|
||||
deps_to_install="$deps_to_install ${rpm_pkgs[$cmd]}"
|
||||
;;
|
||||
debian) pkg="${debian_pkgs[$cmd]}" ;;
|
||||
rpm) pkg="${rpm_pkgs[$cmd]}" ;;
|
||||
*)
|
||||
echo "Warning: Cannot auto-install '$cmd' on unknown distro. Please install manually." >&2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
# Several commands map to the same package (gcc/g++/make
|
||||
# -> build-essential, wrestool/icotool -> icoutils). Skip
|
||||
# if the package is already queued so the log line stays
|
||||
# readable.
|
||||
case " $deps_to_install " in
|
||||
*" $pkg "*) ;;
|
||||
*) deps_to_install="$deps_to_install $pkg" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -198,6 +217,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,13 +240,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..."
|
||||
if ! npm install --no-save electron @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
|
||||
|
||||
@@ -24,15 +24,15 @@ detect_architecture() {
|
||||
|
||||
case "$raw_arch" in
|
||||
x86_64)
|
||||
claude_download_url='https://downloads.claude.ai/releases/win32/x64/1.6259.0/Claude-dc89db3be9b2bc795e0fda0ea3738b035a76ed46.exe'
|
||||
claude_exe_sha256='78d6cb610732cc7496cd3803c7212e6e33e7f76760833a21fab9c5b28c980f5f'
|
||||
claude_download_url='https://downloads.claude.ai/releases/win32/x64/1.7196.0/Claude-2dbd7802ab037cbb97d77be1a063241009b5e598.exe'
|
||||
claude_exe_sha256='ed555042c8fed653dd625b904fc9589f31b769bf7207dfb3cfc78dd393d60940'
|
||||
architecture='amd64'
|
||||
claude_exe_filename='Claude-Setup-x64.exe'
|
||||
echo 'Configured for amd64 (x86_64) build.'
|
||||
;;
|
||||
aarch64)
|
||||
claude_download_url='https://downloads.claude.ai/releases/win32/arm64/1.6259.0/Claude-dc89db3be9b2bc795e0fda0ea3738b035a76ed46.exe'
|
||||
claude_exe_sha256='c8d5ffd86e49bf0bd68ba2ff4d7be36ccd049cc987a25d40886b1db029197713'
|
||||
claude_download_url='https://downloads.claude.ai/releases/win32/arm64/1.7196.0/Claude-2dbd7802ab037cbb97d77be1a063241009b5e598.exe'
|
||||
claude_exe_sha256='fedb5230d19f4e41b4066688b86bd959dee9326f859350ad7db48c1bc341b20a'
|
||||
architecture='arm64'
|
||||
claude_exe_filename='Claude-Setup-arm64.exe'
|
||||
echo 'Configured for arm64 (aarch64) build.'
|
||||
|
||||
82
scripts/setup/fetch-electron-binary.js
Normal file
82
scripts/setup/fetch-electron-binary.js
Normal file
@@ -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);
|
||||
});
|
||||
@@ -320,3 +320,107 @@ Wed 2026-05-06 08:00:21 EDT 130375 1000 1000 SIGTRAP present /usr/lib/claude-des
|
||||
[[ $output == *'Recent Electron crashes: 1'* ]]
|
||||
[[ $output == *'may be from other Electron apps'* ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# _doctor_check_filename_limit: NAME_MAX probe + eCryptfs hint (#590)
|
||||
# =============================================================================
|
||||
|
||||
# Install a getconf shim that emits $1 on stdout. Empty $1 → shim exits 1
|
||||
# so callers can test the "getconf failed" path.
|
||||
_install_getconf_shim() {
|
||||
mkdir -p "$TEST_TMP/bin"
|
||||
local value="$1"
|
||||
if [[ -z $value ]]; then
|
||||
cat > "$TEST_TMP/bin/getconf" <<'SHIM'
|
||||
#!/usr/bin/env bash
|
||||
exit 1
|
||||
SHIM
|
||||
else
|
||||
cat > "$TEST_TMP/bin/getconf" <<SHIM
|
||||
#!/usr/bin/env bash
|
||||
echo ${value}
|
||||
SHIM
|
||||
fi
|
||||
chmod +x "$TEST_TMP/bin/getconf"
|
||||
export PATH="$TEST_TMP/bin:$PATH"
|
||||
}
|
||||
|
||||
# Install a df shim that emits a single-column fstype listing matching
|
||||
# the `df --output=fstype` shape the helper relies on. Empty $1 → shim
|
||||
# exits 1 so callers can test the "df failed" path.
|
||||
_install_df_shim() {
|
||||
mkdir -p "$TEST_TMP/bin"
|
||||
local fstype="$1"
|
||||
if [[ -z $fstype ]]; then
|
||||
cat > "$TEST_TMP/bin/df" <<'SHIM'
|
||||
#!/usr/bin/env bash
|
||||
exit 1
|
||||
SHIM
|
||||
else
|
||||
cat > "$TEST_TMP/bin/df" <<SHIM
|
||||
#!/usr/bin/env bash
|
||||
cat <<'OUT'
|
||||
Type
|
||||
${fstype}
|
||||
OUT
|
||||
SHIM
|
||||
fi
|
||||
chmod +x "$TEST_TMP/bin/df"
|
||||
export PATH="$TEST_TMP/bin:$PATH"
|
||||
}
|
||||
|
||||
@test "_doctor_check_filename_limit: silent when NAME_MAX >= 200" {
|
||||
_install_getconf_shim '255'
|
||||
run _doctor_check_filename_limit
|
||||
[[ $status -eq 0 ]]
|
||||
[[ -z $output ]]
|
||||
}
|
||||
|
||||
@test "_doctor_check_filename_limit: warns when NAME_MAX < 200" {
|
||||
_install_getconf_shim '143'
|
||||
_install_df_shim 'ext4'
|
||||
run _doctor_check_filename_limit
|
||||
[[ $status -eq 0 ]]
|
||||
[[ $output == *'[WARN]'* ]]
|
||||
[[ $output == *'NAME_MAX=143'* ]]
|
||||
[[ $output == *'#590'* ]]
|
||||
# Non-ecryptfs fs: no LUKS hint
|
||||
[[ $output != *'eCryptfs'* ]]
|
||||
[[ $output != *'LUKS'* ]]
|
||||
}
|
||||
|
||||
@test "_doctor_check_filename_limit: eCryptfs adds LUKS workaround hint" {
|
||||
_install_getconf_shim '143'
|
||||
_install_df_shim 'ecryptfs'
|
||||
run _doctor_check_filename_limit
|
||||
[[ $status -eq 0 ]]
|
||||
[[ $output == *'[WARN]'* ]]
|
||||
[[ $output == *'NAME_MAX=143'* ]]
|
||||
[[ $output == *'eCryptfs'* ]]
|
||||
[[ $output == *'LUKS'* ]]
|
||||
}
|
||||
|
||||
@test "_doctor_check_filename_limit: silent on non-numeric getconf output" {
|
||||
_install_getconf_shim 'undefined'
|
||||
run _doctor_check_filename_limit
|
||||
[[ $status -eq 0 ]]
|
||||
[[ -z $output ]]
|
||||
}
|
||||
|
||||
@test "_doctor_check_filename_limit: silent when getconf fails" {
|
||||
_install_getconf_shim ''
|
||||
run _doctor_check_filename_limit
|
||||
[[ $status -eq 0 ]]
|
||||
[[ -z $output ]]
|
||||
}
|
||||
|
||||
@test "_doctor_check_filename_limit: df failure suppresses eCryptfs hint, keeps warn" {
|
||||
_install_getconf_shim '143'
|
||||
_install_df_shim ''
|
||||
run _doctor_check_filename_limit
|
||||
[[ $status -eq 0 ]]
|
||||
[[ $output == *'[WARN]'* ]]
|
||||
[[ $output == *'NAME_MAX=143'* ]]
|
||||
[[ $output != *'eCryptfs'* ]]
|
||||
[[ $output != *'LUKS'* ]]
|
||||
}
|
||||
|
||||
@@ -38,6 +38,14 @@ assert_executable() {
|
||||
fi
|
||||
}
|
||||
|
||||
assert_setuid() {
|
||||
if [[ -u $1 ]]; then
|
||||
pass "Setuid bit set: $1"
|
||||
else
|
||||
fail "Setuid bit not set: $1"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local file="$1" pattern="$2" desc="${3:-}"
|
||||
if grep -q "$pattern" "$file" 2>/dev/null; then
|
||||
|
||||
@@ -41,9 +41,14 @@ electron_path='/usr/lib/claude-desktop/node_modules/electron/dist/electron'
|
||||
assert_file_exists "$electron_path"
|
||||
assert_executable "$electron_path"
|
||||
|
||||
# chrome-sandbox
|
||||
assert_file_exists \
|
||||
'/usr/lib/claude-desktop/node_modules/electron/dist/chrome-sandbox'
|
||||
# chrome-sandbox: setuid bit must be set by the rpm spec's %files
|
||||
# %attr(4755, ...) entry, not by a %post chmod (#539). The check
|
||||
# guards against any regression that strips the suid bit — including
|
||||
# (but not limited to) reverting to a %post chmod, which silently
|
||||
# no-ops if the scriptlet is skipped (--noscripts, layered images).
|
||||
chrome_sandbox='/usr/lib/claude-desktop/node_modules/electron/dist/chrome-sandbox'
|
||||
assert_file_exists "$chrome_sandbox"
|
||||
assert_setuid "$chrome_sandbox"
|
||||
|
||||
# --- Desktop entry validation ---
|
||||
desktop_file='/usr/share/applications/claude-desktop.desktop'
|
||||
|
||||
Reference in New Issue
Block a user