Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def _try_termux_ultrafast_version() -> bool:
import argparse
import json
import shutil
import stat
import subprocess
from pathlib import Path
from typing import Optional
Expand Down Expand Up @@ -6929,6 +6930,33 @@ def _desktop_macos_relaunchable_fixup(desktop_dir: Path) -> None:
print(f" (warning: macOS relaunch fixup skipped: {exc})")


def _desktop_linux_sandbox_fixup(packaged_executable: Path) -> bool:
"""Configure Electron's Linux SUID sandbox helper when required."""
if sys.platform != "linux":
return True

sandbox = packaged_executable.parent / "chrome-sandbox"
if not sandbox.exists():
print(f"✗ Hermes Desktop is missing Electron's Linux sandbox helper: {sandbox}")
return False

sandbox_stat = sandbox.stat()
if sandbox_stat.st_uid == 0 and stat.S_IMODE(sandbox_stat.st_mode) == 0o4755:
return True
Comment on lines +6938 to +6945

sudo = shutil.which("sudo")
if not sudo:
print("✗ Hermes Desktop requires sudo to configure Electron's Linux sandbox helper.")
return False

print("→ Configuring Electron Linux sandbox helper (sudo required)...")
for command in ([sudo, "chown", "root:root", str(sandbox)], [sudo, "chmod", "4755", str(sandbox)]):
if subprocess.run(command, check=False).returncode != 0:
print(f"✗ Failed to configure Electron's Linux sandbox helper: {sandbox}")
return False
return True


def cmd_gui(args):
"""Build and launch the native Electron desktop GUI."""
desktop_dir = PROJECT_ROOT / "apps" / "desktop"
Expand Down Expand Up @@ -7038,6 +7066,9 @@ def cmd_gui(args):
print(" Expected an unpacked Electron app for the current OS.")
sys.exit(1)

if not _desktop_linux_sandbox_fixup(packaged_executable):
sys.exit(1)

print(f"→ Launching packaged Hermes Desktop: {packaged_executable}")
launch_result = subprocess.run([str(packaged_executable)], cwd=desktop_dir, env=env, check=False)
sys.exit(launch_result.returncode)
Expand Down
44 changes: 32 additions & 12 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2343,10 +2343,10 @@ postinstall_mode() {
fi
}

# Build apps/desktop into a launchable Hermes.app. Mirrors install.ps1's
# Build apps/desktop into a launchable native app. Mirrors install.ps1's
# Install-Desktop: a root-level npm install so the apps/* workspace resolves
# the desktop's own deps (Electron ~150MB), then `npm run pack`
# (electron-builder --dir) which emits release/mac*/Hermes.app. Only invoked
# (electron-builder --dir) which emits an unpacked app for the current OS. Only invoked
# via the 'desktop' stage / --include-desktop, which the Electron app's own
# first-launch bootstrap never requests (it must not rebuild itself).
install_desktop() {
Expand Down Expand Up @@ -2382,7 +2382,7 @@ install_desktop() {
log_success "Desktop workspace dependencies installed"

# 2. Build. `npm run pack` = tsc + vite build + electron-builder --dir,
# producing an unpacked release/mac*/Hermes.app. We disable signing
# producing an unpacked app for the current OS. We disable signing
# auto-discovery so electron-builder falls back to an ad-hoc signature
# instead of grabbing an unrelated Developer ID from the keychain; a
# real signed/notarized .dmg needs Apple credentials and is a separate
Expand All @@ -2395,21 +2395,41 @@ install_desktop() {
}

local app=""
local cand
for cand in \
"$desktop_dir/release/mac-arm64/Hermes.app" \
"$desktop_dir/release/mac/Hermes.app"; do
if [ -d "$cand" ]; then
app="$cand"
break
if [ "$OS" = "linux" ]; then
if [ -x "$desktop_dir/release/linux-unpacked/Hermes" ]; then
app="$desktop_dir/release/linux-unpacked/Hermes"
elif [ -x "$desktop_dir/release/linux-unpacked/hermes" ]; then
app="$desktop_dir/release/linux-unpacked/hermes"
fi
done
else
local cand
for cand in \
"$desktop_dir/release/mac-arm64/Hermes.app" \
"$desktop_dir/release/mac/Hermes.app"; do
if [ -d "$cand" ]; then
app="$cand"
break
fi
done
fi
if [ -z "$app" ]; then
log_error "Desktop build completed but no Hermes.app was found under $desktop_dir/release/"
log_error "Desktop build completed but no app was found under $desktop_dir/release/"
return 1
fi
log_success "Desktop app built: $app"

if [ "$OS" = "linux" ]; then
local sandbox="$desktop_dir/release/linux-unpacked/chrome-sandbox"
if [ "$(id -u)" -eq 0 ]; then
chown root:root "$sandbox" && chmod 4755 "$sandbox"
elif command -v sudo >/dev/null 2>&1; then
sudo chown root:root "$sandbox" && sudo chmod 4755 "$sandbox"
else
log_error "Cannot configure Electron sandbox helper without sudo: $sandbox"
return 1
fi
fi
Comment on lines +2421 to +2431

# macOS: make the locally-built (ad-hoc) app relaunchable after an in-place
# self-update. An ad-hoc bundle has no stable Designated Requirement, so a
# later in-place rebuild (new cdhash) plus the inherited quarantine flag
Expand Down
20 changes: 20 additions & 0 deletions tests/hermes_cli/test_gui_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ def test_gui_skip_build_launches_existing_packaged_app_without_npm(tmp_path, mon
assert mock_run.call_args.args[0] == [str(packaged_exe)]


def test_gui_linux_configures_sandbox_before_launch(tmp_path, monkeypatch):
root = _make_desktop_tree(tmp_path)
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
packaged_exe = _make_packaged_executable(root, monkeypatch, platform="linux")
sandbox = packaged_exe.parent / "chrome-sandbox"
sandbox.write_text("", encoding="utf-8")
sandbox.chmod(0o755)
ok = subprocess.CompletedProcess([], 0)

with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/sudo"), \
patch("hermes_cli.main.subprocess.run", return_value=ok) as mock_run, \
pytest.raises(SystemExit) as exc:
cli_main.cmd_gui(_ns(skip_build=True))

assert exc.value.code == 0
assert mock_run.call_args_list[0].args[0] == ["/usr/bin/sudo", "chown", "root:root", str(sandbox)]
assert mock_run.call_args_list[1].args[0] == ["/usr/bin/sudo", "chmod", "4755", str(sandbox)]
assert mock_run.call_args_list[2].args[0] == [str(packaged_exe)]


def test_gui_source_mode_uses_renderer_build_and_electron(tmp_path, monkeypatch):
root = _make_desktop_tree(tmp_path)
desktop_dir = root / "apps" / "desktop"
Expand Down