Skip to content

Commit ef4e6cd

Browse files
committed
unbreak macOS Apple Silicon install, atomic Linux install, real version smoke test
Three independently-motivated bug fixes in scripts/install.sh: 1. macOS Apple Silicon: AMFI rejects the release Mach-O at exec The lightpanda-aarch64-macos asset has a linker-signed ad-hoc signature without a CMS blob (`codesign -dv` shows flags=0x20002(adhoc,linker-signed)). AMFI on Apple Silicon enforces stricter requirements outside trusted prefixes; from /tmp or ~/.local/bin the kernel rejects it as 'Unrecoverable CT signature issue, bailing out' and SIGKILLs at exec. Re-signing locally with `codesign -s -` adds a CMS blob but AMFI then rejects with error -423 (adhoc signed by unknown chain). Verified empirically: same byte-identical Mach-O runs from /opt/homebrew/Cellar/lightpanda/.../bin/lightpanda but is SIGKILLed from /tmp/. Copying the broken curl-downloaded bytes INTO the brew path makes them run. So the trust is path-based, not byte-based. The Lightpanda brew formula does NOT build from source (verified via `brew cat`) — it just downloads the same Mach-O and runs `bin.install` to place it at /opt/homebrew/Cellar/.../bin/lightpanda, which AMFI exempts from the strict ad-hoc signature check. Reproducing on M3 Pro / macOS 14.x; should be uniform across Apple Silicon but not directly tested on M1/M2/M4. Intel macOS keeps the GitHub Release Mach-O path — Apple Silicon's stricter AMFI enforcement is arm64-specific and I don't have an Intel Mac to test. 2. Failed reruns destroy existing working binary SKILL.md documents reruns as the update path, but curl -L -o "$INSTALL_DIR/$BINARY_NAME" overwrites the install target during download. Atomic install via mktemp + verify + smoke-test + mv preserves the existing binary on any failure. 3. --version smoke test silently accepts broken binaries Lightpanda's CLI has no --version flag; running it logs '$msg=exit err=UnknownCommand' and exits 1. The current 2>/dev/null + --help fallback masks this, and the head -1 pipe at the end means it succeeds on broken binaries that produce no output. Use the 'version' subcommand and capture stderr to surface real diagnostics like 'GLIBC_2.32 not found' on failure. Smaller adjacent changes, each tightly motivated by the three above: - set -euo pipefail (was set -e); curl -fSL (was -sL/-L). pipefail + -f propagate curl HTTP errors through curl | jq, replacing the misleading 'could not retrieve checksum' message. - chmod 0755 instead of chmod a+x. Required because fix #2 uses mktemp (0600); chmod a+x on top yields 0711 (owner-only readable). - Apple Silicon PATH-shadow warning. A user upgrading from a pre- this-PR version still has the rejected ~/.local/bin/lightpanda (the linker-signed Mach-O) which shadows the new working brew binary in PATH — without the warning they still get SIGKILL after a successful brew install. - LIGHTPANDA_DIR warning on Apple Silicon (brew owns the install path; the variable is silently ignored without this). - SKILL.md Install section: per-OS callout describing the flow split. Better long-term fix (out of scope for this PR): upstream-side, the build pipeline producing the GitHub Release assets should apply a proper ad-hoc signature with a CMS blob (or, better, Developer ID + notarization). The release Mach-O would then run from anywhere and this workaround wouldn't be needed. Intentionally not included (offered as follow-ups): - Optional $GITHUB_TOKEN for API quota - PATH-shadow check on the shared download flow - Upstream-side signing fix (separate repo)
1 parent bc562f3 commit ef4e6cd

2 files changed

Lines changed: 102 additions & 22 deletions

File tree

SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ Prefer the built-in Web Search tool when it is available and sufficient for your
2525
bash scripts/install.sh
2626
```
2727

28-
Lightpanda is available on Linux and macOS only. Windows is supported via WSL2.
28+
Lightpanda is available on Linux and macOS only. Windows is supported via WSL2. The script picks the right method per OS:
29+
- **macOS, Apple Silicon** — installs via the upstream Homebrew tap (`brew install lightpanda-io/browser/lightpanda`). Requires Homebrew. The nightly Mach-O binaries from GitHub Releases get SIGKILLed by the kernel at exec on Apple Silicon despite shipping with an ad-hoc / linker signature, so the script delegates to the tap, which builds from source.
30+
- **macOS Intel + Linux** — downloads the nightly binary from GitHub Releases, verifies its SHA-256, smoke-tests it, and atomically installs to `$HOME/.local/bin/lightpanda` (override with `LIGHTPANDA_DIR=...`). On rerun, the existing binary is left in place if any step fails.
2931

3032
The binary is a nightly build that evolves quickly. If you encounter crashes or issues, run `scripts/install.sh` again to update to the latest version (max once per day).
3133

scripts/install.sh

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#!/bin/bash
22
# Lightpanda Setup Script
3-
# Run once to install Lightpanda browser
3+
# Run to install or update Lightpanda. macOS arm64 → Homebrew tap;
4+
# macOS x86_64 + Linux → GitHub Release download + atomic install.
45

5-
set -e
6+
# pipefail catches a failing curl in `curl | jq` that jq would otherwise swallow.
7+
set -euo pipefail
68

7-
INSTALL_DIR="${LIGHTPANDA_DIR:-$HOME/.local/bin}"
89
BINARY_NAME="lightpanda"
910

1011
echo "=== Lightpanda Setup ==="
11-
echo "Install directory: $INSTALL_DIR"
1212

1313
# Detect OS and architecture
1414
OS="$(uname -s)"
@@ -32,10 +32,59 @@ case "$OS" in
3232
Darwin)
3333
case "$ARCH" in
3434
x86_64)
35+
# Intel Macs: the Mach-O release asset runs fine, so
36+
# use the same download/verify/atomic-mv path as Linux.
3537
DOWNLOAD_URL="https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-macos"
3638
;;
3739
arm64|aarch64)
38-
DOWNLOAD_URL="https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos"
40+
# Apple Silicon: AMFI rejects the release Mach-O's linker-signed signature
41+
# (no CMS blob) when it's executed from arbitrary user paths like /tmp or
42+
# ~/.local/bin — the binary gets SIGKILLed at exec with kernel log
43+
# "AMFI: ... Unrecoverable CT signature issue, bailing out.". The brew tap's
44+
# bin.install puts the same byte-identical binary at /opt/homebrew/Cellar/...,
45+
# which AMFI exempts from the strict ad-hoc signature check, so it runs.
46+
# (Re-signing locally with `codesign -s -` doesn't help — AMFI then rejects
47+
# with error -423 "adhoc signed or signed by an unknown certificate chain".)
48+
if ! command -v brew &> /dev/null; then
49+
echo "ERROR: Apple Silicon install requires Homebrew (https://brew.sh)."
50+
echo "Install Homebrew, then re-run. Or: brew install lightpanda-io/browser/lightpanda"
51+
exit 1
52+
fi
53+
if [ -n "${LIGHTPANDA_DIR:-}" ]; then
54+
echo "Note: LIGHTPANDA_DIR is ignored on Apple Silicon — brew owns the install path."
55+
fi
56+
echo "Detected: $OS $ARCH — installing via Homebrew tap."
57+
# `install` is a no-op when current; `upgrade` keeps reruns fresh. Both exit 0 in the
58+
# up-to-date case and non-zero on real errors, so `set -e` propagates genuine failures.
59+
brew install lightpanda-io/browser/lightpanda
60+
brew upgrade lightpanda-io/browser/lightpanda
61+
# Anchor on `brew --prefix` (general, not keg-specific) so a stale ~/.local/bin/lightpanda
62+
# from a pre-this-PR run can't shadow the brew binary undetected.
63+
BREW_PREFIX="$(brew --prefix)"
64+
BIN_PATH="$BREW_PREFIX/bin/$BINARY_NAME"
65+
[ -x "$BIN_PATH" ] || { echo "ERROR: brew install completed but $BIN_PATH is missing. Try: brew link --overwrite lightpanda"; exit 1; }
66+
# Hard-fail smoke test — brew can ship a binary that crashes at exec; surface stderr.
67+
DARWIN_SMOKE_STDERR=$(mktemp)
68+
trap 'rm -f "$DARWIN_SMOKE_STDERR"' EXIT
69+
if ! VERSION_OUT=$("$BIN_PATH" version 2>"$DARWIN_SMOKE_STDERR") || [ -z "$VERSION_OUT" ]; then
70+
echo "ERROR: Homebrew-installed lightpanda failed its version smoke test."
71+
[ -s "$DARWIN_SMOKE_STDERR" ] && { echo " Binary stderr:"; sed 's/^/ /' "$DARWIN_SMOKE_STDERR"; }
72+
echo " Try: brew reinstall lightpanda-io/browser/lightpanda"
73+
exit 1
74+
fi
75+
echo ""
76+
echo "Lightpanda installed via Homebrew: $VERSION_OUT"
77+
echo "Binary location: $BIN_PATH"
78+
# Warn about PATH-shadow: a pre-this-PR run left the SIGKILL Mach-O at ~/.local/bin/lightpanda;
79+
# without this check the user still runs the broken one despite brew installing a working binary.
80+
PATH_RESOLVED="$(command -v lightpanda 2>/dev/null || true)"
81+
if [ -n "$PATH_RESOLVED" ] && [ "$PATH_RESOLVED" != "$BIN_PATH" ]; then
82+
echo ""
83+
echo "WARNING: another 'lightpanda' is earlier in your PATH: $PATH_RESOLVED"
84+
echo "Remove it or reorder PATH so $BREW_PREFIX/bin takes precedence."
85+
fi
86+
echo "Run with: lightpanda serve --host 127.0.0.1 --port 9222"
87+
exit 0
3988
;;
4089
*)
4190
echo "ERROR: Unsupported architecture: $ARCH"
@@ -50,7 +99,10 @@ case "$OS" in
5099
;;
51100
esac
52101

102+
# --- Shared download flow (Linux + Intel macOS) ---
103+
INSTALL_DIR="${LIGHTPANDA_DIR:-$HOME/.local/bin}"
53104
echo "Detected: $OS $ARCH"
105+
echo "Install directory: $INSTALL_DIR"
54106
echo "Download URL: $DOWNLOAD_URL"
55107

56108
# Check for required tools
@@ -75,9 +127,12 @@ mkdir -p "$INSTALL_DIR"
75127
# Determine asset name from URL
76128
ASSET_NAME="${DOWNLOAD_URL##*/}"
77129

78-
# Fetch expected SHA256 digest from GitHub release API
130+
# Fetch expected SHA256 digest from GitHub release API. -f makes curl
131+
# exit non-zero on HTTP errors (4xx/5xx) so set -e + pipefail catch
132+
# them; without it curl exits 0 on 503 and jq fails on the error body
133+
# with a misleading "could not retrieve checksum" message.
79134
echo "Fetching expected checksum from GitHub API..."
80-
EXPECTED_DIGEST=$(curl -sL "https://api.github.com/repos/lightpanda-io/browser/releases/tags/nightly" \
135+
EXPECTED_DIGEST=$(curl -fSL "https://api.github.com/repos/lightpanda-io/browser/releases/tags/nightly" \
81136
| jq -r --arg name "$ASSET_NAME" '.assets[] | select(.name == $name) | .digest')
82137

83138
if [ -z "$EXPECTED_DIGEST" ] || [ "$EXPECTED_DIGEST" = "null" ]; then
@@ -89,28 +144,57 @@ fi
89144
EXPECTED_SHA256="${EXPECTED_DIGEST#sha256:}"
90145
echo "Expected SHA256: $EXPECTED_SHA256"
91146

92-
# Download binary
93-
echo "Downloading Lightpanda..."
94-
curl -L -o "$INSTALL_DIR/$BINARY_NAME" "$DOWNLOAD_URL"
147+
# Download to a temp file in the same directory, verify, smoke-test,
148+
# THEN atomically `mv` into place. SKILL.md documents reruns as the
149+
# update path, so a failed download must not destroy a working binary.
150+
TMP_BINARY=$(mktemp "$INSTALL_DIR/.${BINARY_NAME}.XXXXXX")
151+
SMOKE_STDERR=$(mktemp)
152+
trap 'rm -f "$TMP_BINARY" "$SMOKE_STDERR"' EXIT
153+
154+
echo "Downloading Lightpanda to $TMP_BINARY..."
155+
curl -fSL -o "$TMP_BINARY" "$DOWNLOAD_URL"
95156

96157
# Verify checksum
97158
echo "Verifying checksum..."
98159
if command -v sha256sum &> /dev/null; then
99-
ACTUAL_SHA256=$(sha256sum "$INSTALL_DIR/$BINARY_NAME" | awk '{print $1}')
160+
ACTUAL_SHA256=$(sha256sum "$TMP_BINARY" | awk '{print $1}')
100161
else
101-
ACTUAL_SHA256=$(shasum -a 256 "$INSTALL_DIR/$BINARY_NAME" | awk '{print $1}')
162+
ACTUAL_SHA256=$(shasum -a 256 "$TMP_BINARY" | awk '{print $1}')
102163
fi
103164

104165
if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then
105166
echo "ERROR: Checksum verification FAILED!"
106167
echo " Expected: $EXPECTED_SHA256"
107168
echo " Actual: $ACTUAL_SHA256"
108-
rm -f "$INSTALL_DIR/$BINARY_NAME"
169+
echo " Existing binary (if any) at $INSTALL_DIR/$BINARY_NAME was NOT replaced."
109170
exit 1
110171
fi
111172

112173
echo "Checksum verified OK."
113-
chmod a+x "$INSTALL_DIR/$BINARY_NAME"
174+
# 0755 (conventional executable mode) rather than `chmod a+x` over
175+
# mktemp's 0600, which would yield 0711 (owner-only readable).
176+
chmod 0755 "$TMP_BINARY"
177+
178+
# Smoke-test BEFORE replacing. Use the `version` subcommand — the
179+
# `--version` flag exits 1 with `UnknownCommand`, so it would falsely
180+
# reject a working binary. Capturing stdout to a variable means the
181+
# `if` exit status is the binary's exit status (no pipe to mask it).
182+
echo "Smoke-testing the downloaded binary..."
183+
if ! VERSION_OUT=$("$TMP_BINARY" version 2>"$SMOKE_STDERR") || [ -z "$VERSION_OUT" ]; then
184+
echo "ERROR: Downloaded binary failed to run (\`version\` subcommand returned no output or non-zero exit)."
185+
echo " This may indicate an incompatible system, missing libc, or a corrupted release asset."
186+
if [ -s "$SMOKE_STDERR" ]; then
187+
echo " Binary stderr:"
188+
sed 's/^/ /' "$SMOKE_STDERR"
189+
fi
190+
echo " Existing binary (if any) at $INSTALL_DIR/$BINARY_NAME was NOT replaced."
191+
exit 1
192+
fi
193+
194+
# `mv` within the same filesystem is atomic.
195+
mv "$TMP_BINARY" "$INSTALL_DIR/$BINARY_NAME"
196+
# Leave the EXIT trap registered so $SMOKE_STDERR still gets cleaned;
197+
# `rm -f` on the moved-away $TMP_BINARY is a silent no-op.
114198

115199
# Check if install directory is in PATH
116200
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
@@ -120,14 +204,8 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
120204
echo " export PATH=\"\$PATH:$INSTALL_DIR\""
121205
fi
122206

123-
# Test installation
124207
echo ""
125-
echo "Testing Lightpanda..."
126-
if "$INSTALL_DIR/$BINARY_NAME" --version 2>/dev/null || "$INSTALL_DIR/$BINARY_NAME" --help 2>/dev/null | head -1; then
127-
echo "Lightpanda installed successfully!"
128-
else
129-
echo "Binary installed at: $INSTALL_DIR/$BINARY_NAME"
130-
fi
208+
echo "Lightpanda installed successfully: ${VERSION_OUT:-OK}"
131209

132210
echo ""
133211
echo "=== Setup Complete ==="

0 commit comments

Comments
 (0)