#!/usr/bin/env bash # Build the meshbay-client package: Electron app + .desktop launcher. # # Usage: ./build-client.sh [staging-dir] [--arm64] # # Requires Node >= 22. If not in PATH, set NODEJS_DIR=/opt/nodejs or similar. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" STAGING="${1:-/tmp/meshbay-build}" ROOT="$STAGING/meshbay-client-root" CLIENT="$REPO/packages/meshbay-client" ARCH_FLAG="" ELECTRON_ARCH="x64" DEB_ARCH="amd64" for arg in "$@"; do if [ "$arg" = "--arm64" ]; then ARCH_FLAG="--arm64" ELECTRON_ARCH="arm64" DEB_ARCH="arm64" fi done # Node.js version check if [ -n "${NODEJS_DIR:-}" ]; then export PATH="$NODEJS_DIR/bin:$PATH" fi NODE_VER=$(node --version 2>/dev/null || echo "none") echo "==> meshbay-client (Node $NODE_VER, arch=$ELECTRON_ARCH)" if ! node --version >/dev/null 2>&1; then echo "!! Node.js not found. Set NODEJS_DIR or install Node >= 22." >&2 exit 1 fi NODE_MAJOR=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))") if [ "$NODE_MAJOR" -lt 22 ]; then echo "!! Node $NODE_VER is too old — need >= 22 for Electron" >&2 exit 1 fi rm -rf "$ROOT" mkdir -p "$ROOT" # --- Build the Electron app ----------------------------------------------- cd "$CLIENT" # --- Electron: always build against the latest release -------------------- # # Chromium CVEs are fixed in Electron releases, and a client built against an # old one ships those holes to every user. That is a certain harm; a build that # breaks on a new Electron is a repairable one. So this bumps to the latest on # every build and lets the build fail if it cannot cope — the failure is the # signal to fix, not a reason to stay behind. # # It writes package.json and package-lock.json, so the build leaves the repo # dirty on purpose: the new pin is meant to be committed. `npm audit` will not # tell you any of this — Chromium CVEs fixed in Electron do not reliably reach # the npm advisory database, which is why this check exists at all. echo " checking for a newer Electron" PINNED=$(node -p "require('./package-lock.json').packages['node_modules/electron'].version" 2>/dev/null || echo "unknown") LATEST=$(npm view electron version 2>/dev/null || echo "") if [ -z "$LATEST" ]; then echo " !! could not reach the npm registry — building against the pinned Electron $PINNED" >&2 elif [ "$LATEST" = "$PINNED" ]; then echo " Electron $PINNED is the latest" else echo "" echo " ==> Electron $PINNED -> $LATEST" echo "" npm install --save-dev --ignore-scripts "electron@$LATEST" 2>&1 | tail -2 # allowScripts pins an exact version; leave it matching so npm does not # start refusing a script a future Electron reintroduces. node -e " const fs = require('node:fs'); const p = JSON.parse(fs.readFileSync('package.json', 'utf8')); if (p.allowScripts) { for (const k of Object.keys(p.allowScripts)) { if (k.startsWith('electron@')) { delete p.allowScripts[k]; p.allowScripts['electron@$LATEST'] = true; } } fs.writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n'); } " echo " package.json and package-lock.json updated — commit them" fi echo " npm ci" npm ci --ignore-scripts 2>&1 | tail -3 echo " approving + installing electron" npm approve-scripts electron 2>/dev/null || true node node_modules/electron/install.js 2>&1 | tail -1 echo " syncing UI from hub package" npm run sync-ui 2>&1 | tail -1 echo " electron-builder --dir $ARCH_FLAG" npx electron-builder --dir --linux $ARCH_FLAG 2>&1 | tail -5 # Determine the unpacked output directory if [ "$ELECTRON_ARCH" = "arm64" ]; then UNPACKED="$CLIENT/dist/linux-arm64-unpacked" else UNPACKED="$CLIENT/dist/linux-unpacked" fi [ -d "$UNPACKED" ] || { echo "!! electron-builder output not found at $UNPACKED" >&2; exit 1; } # --- Assemble the package tree -------------------------------------------- echo " assembling /opt/meshbay-client/" mkdir -p "$ROOT/opt/meshbay-client" cp -a "$UNPACKED"/* "$ROOT/opt/meshbay-client/" # Rename the electron binary if [ -f "$ROOT/opt/meshbay-client/meshbay-client" ]; then mv "$ROOT/opt/meshbay-client/meshbay-client" "$ROOT/opt/meshbay-client/meshbay" elif [ -f "$ROOT/opt/meshbay-client/electron" ]; then mv "$ROOT/opt/meshbay-client/electron" "$ROOT/opt/meshbay-client/meshbay" fi # Symlink in PATH mkdir -p "$ROOT/usr/bin" ln -sf /opt/meshbay-client/meshbay "$ROOT/usr/bin/meshbay" # --- Desktop launcher ----------------------------------------------------- mkdir -p "$ROOT/usr/share/applications" cp "$REPO/packaging/desktop/meshbay.desktop" "$ROOT/usr/share/applications/" # --- Icons ---------------------------------------------------------------- ICON_SRC="$CLIENT/build/icon.png" if [ -f "$ICON_SRC" ]; then for size in 128 256 512; do ICON_DIR="$ROOT/usr/share/icons/hicolor/${size}x${size}/apps" mkdir -p "$ICON_DIR" if command -v convert >/dev/null 2>&1; then convert "$ICON_SRC" -resize "${size}x${size}" "$ICON_DIR/meshbay.png" elif command -v magick >/dev/null 2>&1; then magick "$ICON_SRC" -resize "${size}x${size}" "$ICON_DIR/meshbay.png" else # Fallback: copy the original at all sizes (better than no icon) cp "$ICON_SRC" "$ICON_DIR/meshbay.png" fi done echo " icons installed" else echo " !! icon.png not found at $ICON_SRC — no icons" >&2 fi # --- Firewall profile (Chromecast) ---------------------------------------- mkdir -p "$ROOT/usr/lib/firewalld/services" cp "$REPO/packaging/firewall/firewalld/meshbay-cast.xml" \ "$ROOT/usr/lib/firewalld/services/" echo "==> meshbay-client staging ready at $ROOT ($DEB_ARCH)" # `npm ci` above deletes node_modules wholesale and install.js re-extracts # Electron's dist, so a chrome-sandbox that had been made root-owned 4755 for # local development comes back 755, owned by whoever ran this build. Running # the app straight from node_modules then aborts outright — Chromium refuses # to run without its SUID helper rather than quietly dropping the sandbox, # which is the right call and a baffling one if you have not connected the # crash to a package build you ran minutes earlier. # # Said, not done: this script does not use sudo, and a build is not where a # setuid bit should be set behind someone's back. The *packaged* app is # unaffected — packaging/deb/meshbay-client/DEBIAN/postinst does it at # install time, which is where it belongs. SANDBOX="$CLIENT/node_modules/electron/dist/chrome-sandbox" if [ -e "$SANDBOX" ] && [ ! -u "$SANDBOX" ]; then echo echo " NOTE: this build reset $SANDBOX to $(stat -c '%a %U:%G' "$SANDBOX")." echo " Running Electron from node_modules needs it back:" echo " sudo chown root:root '$SANDBOX' && sudo chmod 4755 '$SANDBOX'" fi