diff --git a/docs/linux/dnf.md b/docs/linux/dnf.md new file mode 100644 index 0000000..bbf55e5 --- /dev/null +++ b/docs/linux/dnf.md @@ -0,0 +1,63 @@ +# StepForge on dnf-based Linux (Fedora / RHEL) + +This is the setup and packaging guide for **dnf-based** distributions. Debian, +Ubuntu, and other apt-based systems have a separate guide: +[apt.md](apt.md). + +## Install from the .rpm + +```bash +sudo dnf install ./stepforge--1..rpm +``` + +dnf pulls the required runtime libraries automatically (they are declared as +`Requires`). The package installs: + +- the app and a fixed Electron runtime under `/opt/stepforge`, +- the `stepforge` launcher at `/usr/bin/stepforge`, +- a desktop entry, icons, and `.sfgz`/`.sfglt` file associations. + +Launch it from your application menu or run `stepforge`. + +### Sandbox + +The launcher runs **sandboxed**. On most modern kernels the Chromium +user-namespace sandbox works out of the box; the package's `%post` also makes +the setuid `chrome-sandbox` helper usable as a fallback. StepForge will **not** +silently launch unsandboxed. + +## Install from the portable tarball + +The portable tarball (same one shipped for apt systems) includes the +`/usr/bin/stepforge` launcher. Install the runtime libraries first: + +```bash +bash scripts/linux/dnf/install-runtime-deps.sh +tar -xzf stepforge__linux-x64.tar.gz +./usr/bin/stepforge +``` + +## Capture capabilities on dnf systems + +- **X11**: full per-click capture with an accurate marker (needs `xinput`). +- **Wayland** (Fedora's default): screen capture via the XDG Desktop Portal + + PipeWire; the portal asks permission once per recording. Per-click capture + with coordinates is not exposed by Wayland, so recording uses a global + hotkey or interval trigger. StepForge reports the active trigger honestly. + +Open Settings → Diagnostics in the app to see the detected session type, +portal/PipeWire status, and the active capture profile. + +## Build the .rpm yourself + +```bash +bash scripts/linux/dnf/install-build-deps.sh # rpm-build, rpmdevtools, Xvfb, … +nvm install && nvm use # pinned Node 22 (see .nvmrc) +npm ci +npm run package:linux:rpm # -> build/artifacts/*.rpm + sha256 +``` + +The builder stages **only** runtime files (shared with the `.deb` builder via +`packaging/linux/common/stage-runtime.sh`): the app code, a fixed Electron +runtime, and production npm dependencies. It never copies the development +`node_modules` and fails if `node_modules` is missing. diff --git a/packaging/linux/common/stage-runtime.sh b/packaging/linux/common/stage-runtime.sh new file mode 100755 index 0000000..788c5b9 --- /dev/null +++ b/packaging/linux/common/stage-runtime.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Shared staging for the Linux packages. Populates $STAGE_ROOT with the FHS +# layout common to the .deb and .rpm: a pruned runtime-only /opt/stepforge, the +# launcher, desktop entry, icons, MIME registration, and the license. +# +# Distro-specific metadata (Depends vs Requires) and the packaging step +# (dpkg-deb vs rpmbuild) stay in the per-distro builders. This file only +# assembles the payload so the two never drift. +# +# Usage: ROOT_DIR= STAGE_ROOT= bash stage-runtime.sh +set -euo pipefail + +: "${ROOT_DIR:?ROOT_DIR must be set}" +: "${STAGE_ROOT:?STAGE_ROOT must be set}" + +# A packaged app must contain a fixed runtime; never install at build time and +# never ship without node_modules. +if [ ! -d "$ROOT_DIR/node_modules/electron/dist" ]; then + echo "error: node_modules/electron is missing. Run 'npm ci' before packaging." >&2 + exit 1 +fi + +APP_DIR="$STAGE_ROOT/opt/stepforge" +mkdir -p "$APP_DIR/node_modules" \ + "$STAGE_ROOT/usr/bin" \ + "$STAGE_ROOT/usr/share/applications" \ + "$STAGE_ROOT/usr/share/mime/packages" + +# --- application code (runtime only) ---------------------------------------- +for item in app core exporters package.json package-lock.json; do + cp -a "$ROOT_DIR/$item" "$APP_DIR/$item" +done + +# --- runtime node_modules ---------------------------------------------------- +# The fixed Electron runtime (needed at runtime even though it is a dev dep): +cp -a "$ROOT_DIR/node_modules/electron" "$APP_DIR/node_modules/electron" +# Production npm dependencies (tesseract.js + language data + transitive): +while IFS= read -r dep; do + [ -n "$dep" ] || continue + rel="${dep#"$ROOT_DIR"/}" + [ "$rel" != "$dep" ] || continue + [ -d "$dep" ] || continue + mkdir -p "$APP_DIR/$(dirname "$rel")" + cp -a "$dep" "$APP_DIR/$rel" +done < <(cd "$ROOT_DIR" && npm ls --omit=dev --all --parseable 2>/dev/null | tail -n +2) + +# Guard: the development-only packaging toolchain must not have leaked in. +if [ -d "$APP_DIR/node_modules/electron-builder" ] || [ -d "$APP_DIR/node_modules/app-builder-lib" ]; then + echo "error: build-only dependency leaked into the package payload." >&2 + exit 1 +fi + +# --- launcher ---------------------------------------------------------------- +install -m 0755 "$ROOT_DIR/packaging/linux/common/launcher.sh" "$STAGE_ROOT/usr/bin/stepforge" + +# --- desktop entry, icons, MIME --------------------------------------------- +install -m 0644 "$ROOT_DIR/packaging/linux/common/stepforge.desktop" "$STAGE_ROOT/usr/share/applications/stepforge.desktop" +install -m 0644 "$ROOT_DIR/packaging/linux/common/stepforge-mime.xml" "$STAGE_ROOT/usr/share/mime/packages/stepforge.xml" +for size in 16 32 48 64 128 256 512; do + icon="$ROOT_DIR/packaging/assets/icons/stepforge-${size}.png" + [ -f "$icon" ] || continue + dest="$STAGE_ROOT/usr/share/icons/hicolor/${size}x${size}/apps" + mkdir -p "$dest" + install -m 0644 "$icon" "$dest/stepforge.png" +done diff --git a/packaging/linux/debian/package.sh b/packaging/linux/debian/package.sh index 59503eb..40dd10f 100755 --- a/packaging/linux/debian/package.sh +++ b/packaging/linux/debian/package.sh @@ -25,59 +25,13 @@ case "$DEB_ARCH" in *) NODE_ARCH="$DEB_ARCH" ;; esac -# A packaged app must contain a fixed runtime; never install at build time from -# within the package step, and never ship without node_modules. -if [ ! -d "$ROOT_DIR/node_modules/electron/dist" ]; then - echo "error: node_modules/electron is missing. Run 'npm ci' before packaging." >&2 - exit 1 -fi - WORK_DIR="$(mktemp -d "${OUT_DIR%/}/.deb.XXXXXX")" trap 'rm -rf "$WORK_DIR"' EXIT -APP_DIR="$WORK_DIR/opt/stepforge" -mkdir -p "$APP_DIR" "$WORK_DIR/usr/bin" "$WORK_DIR/DEBIAN" -mkdir -p "$WORK_DIR/usr/share/applications" -mkdir -p "$WORK_DIR/usr/share/mime/packages" -mkdir -p "$WORK_DIR/usr/share/doc/stepforge" -# --- application code (runtime only) ---------------------------------------- -for item in app core exporters package.json package-lock.json; do - cp -a "$ROOT_DIR/$item" "$APP_DIR/$item" -done +# Stage the shared runtime-only payload (fails if node_modules is missing). +ROOT_DIR="$ROOT_DIR" STAGE_ROOT="$WORK_DIR" bash "$ROOT_DIR/packaging/linux/common/stage-runtime.sh" -# --- runtime node_modules ---------------------------------------------------- -# The fixed Electron runtime (needed at runtime even though it is a dev dep): -mkdir -p "$APP_DIR/node_modules" -cp -a "$ROOT_DIR/node_modules/electron" "$APP_DIR/node_modules/electron" -# Production npm dependencies (tesseract.js + language data + transitive): -while IFS= read -r dep; do - [ -n "$dep" ] || continue - rel="${dep#"$ROOT_DIR"/}" - [ "$rel" != "$dep" ] || continue # only paths under the repo - [ -d "$dep" ] || continue - mkdir -p "$APP_DIR/$(dirname "$rel")" - cp -a "$dep" "$APP_DIR/$rel" -done < <(npm ls --omit=dev --all --parseable 2>/dev/null | tail -n +2) - -# Guard: the development-only packaging toolchain must not have leaked in. -if [ -d "$APP_DIR/node_modules/electron-builder" ] || [ -d "$APP_DIR/node_modules/app-builder-lib" ]; then - echo "error: build-only dependency leaked into the package payload." >&2 - exit 1 -fi - -# --- launcher ---------------------------------------------------------------- -install -m 0755 "$ROOT_DIR/packaging/linux/common/launcher.sh" "$WORK_DIR/usr/bin/stepforge" - -# --- desktop entry, icons, MIME --------------------------------------------- -install -m 0644 "$ROOT_DIR/packaging/linux/common/stepforge.desktop" "$WORK_DIR/usr/share/applications/stepforge.desktop" -install -m 0644 "$ROOT_DIR/packaging/linux/common/stepforge-mime.xml" "$WORK_DIR/usr/share/mime/packages/stepforge.xml" -for size in 16 32 48 64 128 256 512; do - icon="$ROOT_DIR/packaging/assets/icons/stepforge-${size}.png" - [ -f "$icon" ] || continue - dest="$WORK_DIR/usr/share/icons/hicolor/${size}x${size}/apps" - mkdir -p "$dest" - install -m 0644 "$icon" "$dest/stepforge.png" -done +mkdir -p "$WORK_DIR/DEBIAN" "$WORK_DIR/usr/share/doc/stepforge" # --- license + docs pointer -------------------------------------------------- if [ -f "$ROOT_DIR/LICENSE" ]; then diff --git a/packaging/linux/fedora/package.sh b/packaging/linux/fedora/package.sh new file mode 100755 index 0000000..8842ce1 --- /dev/null +++ b/packaging/linux/fedora/package.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Build a production StepForge .rpm from a pruned, runtime-only tree, mirroring +# the .deb builder. Stages the shared payload via common/stage-runtime.sh, then +# packages it with rpmbuild against a prebuilt BuildRoot (no compilation). +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +cd "$ROOT_DIR" + +VERSION="$(node -p "require('./package.json').version")" +MAINTAINER="${STEPFORGE_MAINTAINER:-StepForge }" +OUT_DIR="${STEPFORGE_PACKAGE_DIR:-$ROOT_DIR/build/artifacts}" +mkdir -p "$OUT_DIR" + +if ! command -v rpmbuild >/dev/null 2>&1; then + echo "error: rpmbuild is not installed. Run scripts/linux/dnf/install-build-deps.sh" >&2 + exit 1 +fi + +# RPM arch label from the host. +RPM_ARCH="$(rpm --eval '%{_arch}' 2>/dev/null || uname -m)" + +BUILD_ROOT="$(mktemp -d "${OUT_DIR%/}/.rpm.XXXXXX")" +trap 'rm -rf "$BUILD_ROOT"' EXIT +STAGE="$BUILD_ROOT/buildroot" +mkdir -p "$STAGE" + +# Shared runtime-only payload (fails if node_modules is missing). +ROOT_DIR="$ROOT_DIR" STAGE_ROOT="$STAGE" bash "$ROOT_DIR/packaging/linux/common/stage-runtime.sh" + +# License into the RPM's conventional location. +mkdir -p "$STAGE/usr/share/licenses/stepforge" +if [ -f "$ROOT_DIR/LICENSE" ]; then + install -m 0644 "$ROOT_DIR/LICENSE" "$STAGE/usr/share/licenses/stepforge/LICENSE" +elif [ -f "$ROOT_DIR/docs/LICENSE" ]; then + install -m 0644 "$ROOT_DIR/docs/LICENSE" "$STAGE/usr/share/licenses/stepforge/LICENSE" +else + # rpmbuild %license requires the file to exist; write a pointer if absent. + echo "See project LICENSE." > "$STAGE/usr/share/licenses/stepforge/LICENSE" +fi + +# Materialize the spec with version/maintainer substituted. +SPEC="$BUILD_ROOT/stepforge.spec" +sed -e "s/@VERSION@/$VERSION/" -e "s#@MAINTAINER@#$MAINTAINER#" \ + "$ROOT_DIR/packaging/linux/fedora/stepforge.spec" > "$SPEC" + +rpmbuild -bb \ + --define "_topdir $BUILD_ROOT/rpmbuild" \ + --define "_rpmdir $OUT_DIR" \ + --define "_build_id_links none" \ + --buildroot "$STAGE" \ + --target "$RPM_ARCH" \ + "$SPEC" >/dev/null + +# rpmbuild writes to $OUT_DIR//.rpm — surface the final path. +RPM_FILE="$(find "$OUT_DIR" -name "stepforge-${VERSION}-1*.${RPM_ARCH}.rpm" -newer "$SPEC" | head -1)" +if [ -z "$RPM_FILE" ]; then + RPM_FILE="$(find "$OUT_DIR" -name "stepforge-${VERSION}-1*.rpm" | head -1)" +fi +[ -n "$RPM_FILE" ] || { echo "error: rpmbuild did not produce an .rpm" >&2; exit 1; } + +# Checksum. +( cd "$(dirname "$RPM_FILE")" && sha256sum "$(basename "$RPM_FILE")" > "$(basename "$RPM_FILE").sha256" ) + +echo "$RPM_FILE" diff --git a/packaging/linux/fedora/stepforge.spec b/packaging/linux/fedora/stepforge.spec new file mode 100644 index 0000000..dbce9e1 --- /dev/null +++ b/packaging/linux/fedora/stepforge.spec @@ -0,0 +1,69 @@ +# StepForge RPM spec. The payload is prebuilt into a staging BuildRoot by +# packaging/linux/fedora/package.sh (which stages a pruned runtime tree), so +# this spec only packages and declares metadata — it does not compile. +# +# Placeholders @VERSION@ / @MAINTAINER@ are substituted by package.sh. + +%global debug_package %{nil} +%global __brp_check_rpaths %{nil} +%define _build_id_links none + +Name: stepforge +Version: @VERSION@ +Release: 1%{?dist} +Summary: Local-first step-by-step guide capture and export tool + +License: MPL-2.0 +URL: https://github.com/Twest2/StepForge + +# Runtime shared libraries (Chromium/Electron) + capture integration. +Requires: nss +Requires: nspr +Requires: atk +Requires: at-spi2-atk +Requires: cups-libs +Requires: gtk3 +Requires: mesa-libgbm +Requires: alsa-lib +Requires: libxkbcommon +Recommends: xinput +Recommends: xdg-desktop-portal +Recommends: pipewire + +# The payload is architecture-specific (bundles the Electron binary). +%description +StepForge captures step-by-step workflows as screenshots, lets you annotate +and describe each step, and exports to Markdown, PDF, DOCX, PPTX, HTML, and +more. Local-first: no telemetry, with an optional user-configured local AI +integration. This package bundles a fixed Electron runtime and only +production dependencies; it does not install anything at runtime. + +%files +/opt/stepforge +/usr/bin/stepforge +/usr/share/applications/stepforge.desktop +/usr/share/mime/packages/stepforge.xml +/usr/share/icons/hicolor/*/apps/stepforge.png +%license /usr/share/licenses/stepforge/LICENSE + +%post +# Make the Chromium setuid sandbox helper usable so the app launches sandboxed. +HELPER=/opt/stepforge/node_modules/electron/dist/chrome-sandbox +if [ -e "$HELPER" ]; then + chown root:root "$HELPER" || true + chmod 4755 "$HELPER" || true +fi +if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q /usr/share/applications || true; fi +if command -v update-mime-database >/dev/null 2>&1; then update-mime-database /usr/share/mime || true; fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -q /usr/share/icons/hicolor || true; fi + +%postun +if [ "$1" = 0 ]; then + if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q /usr/share/applications || true; fi + if command -v update-mime-database >/dev/null 2>&1; then update-mime-database /usr/share/mime || true; fi + if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -q /usr/share/icons/hicolor || true; fi +fi + +%changelog +* Fri Jul 03 2026 @MAINTAINER@ - @VERSION@-1 +- Production runtime-only package (pruned tree, fixed Electron runtime). diff --git a/scripts/linux/dnf/install-build-deps.sh b/scripts/linux/dnf/install-build-deps.sh new file mode 100755 index 0000000..ddc8f97 --- /dev/null +++ b/scripts/linux/dnf/install-build-deps.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Install the BUILD toolchain for producing StepForge packages on dnf-based +# systems (Fedora/RHEL). For DEVELOPERS/packagers only; never shipped inside +# the end-user package. +set -euo pipefail + +if ! command -v dnf >/dev/null 2>&1; then + echo "This script is for dnf-based systems (Fedora/RHEL)." >&2 + exit 1 +fi + +SUDO="" +if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi + +PACKAGES=( + rpm-build rpmdevtools # build the .rpm + desktop-file-utils # validate the .desktop entry + ca-certificates # npm ci over https + xorg-x11-server-Xvfb # headless smoke test under Xvfb +) + +echo "Installing StepForge build dependencies via dnf..." +$SUDO dnf install -y "${PACKAGES[@]}" + +cat <<'MSG' +Done. Also install the pinned Node toolchain (see .nvmrc — Node 22.12+): + nvm install && nvm use # or another Node 22 LTS install method +Then, from the repo root: + npm ci + npm run package:linux:rpm +MSG diff --git a/scripts/linux/dnf/install-runtime-deps.sh b/scripts/linux/dnf/install-runtime-deps.sh new file mode 100755 index 0000000..eba695f --- /dev/null +++ b/scripts/linux/dnf/install-runtime-deps.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Install the RUNTIME libraries StepForge needs on dnf-based systems (Fedora, +# RHEL/derivatives). These are the shared libraries the packaged Electron +# runtime links against, plus the X11/portal integration used for capture. +# For END USERS installing from the tarball; the .rpm declares the same set as +# Requires so dnf pulls them automatically. +set -euo pipefail + +if ! command -v dnf >/dev/null 2>&1; then + echo "This script is for dnf-based systems (Fedora/RHEL). Use the apt script on Debian/Ubuntu." >&2 + exit 1 +fi + +SUDO="" +if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi + +PACKAGES=( + # Chromium/Electron shared libraries + nss nspr atk at-spi2-atk at-spi2-core cups-libs libdrm + gtk3 mesa-libgbm alsa-lib libxkbcommon + libXcomposite libXdamage libXfixes libXrandr libxshmfence + # X11 per-click capture (marker-accurate) — X11 sessions only + xorg-x11-server-utils xinput + # Wayland screen-share via the XDG portal + PipeWire + xdg-desktop-portal pipewire +) + +echo "Installing StepForge runtime dependencies via dnf..." +# Some package names differ across Fedora releases; install best-effort so one +# missing optional name doesn't abort the whole set. +$SUDO dnf install -y "${PACKAGES[@]}" || { + echo "Some packages were unavailable; retrying individually..." >&2 + for pkg in "${PACKAGES[@]}"; do $SUDO dnf install -y "$pkg" || echo " (skipped: $pkg)" >&2; done +} +echo "Done. StepForge runtime dependencies are installed." diff --git a/tests/integration/linux/package-rpm.test.sh b/tests/integration/linux/package-rpm.test.sh new file mode 100644 index 0000000..dc24df5 --- /dev/null +++ b/tests/integration/linux/package-rpm.test.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Integration test: build the production .rpm and assert it is a real, +# runtime-only package. Honest skip policy: skip ONLY when the prerequisites +# are genuinely absent (rpmbuild missing or node_modules not installed). Once +# we build, any structural failure fails the test. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +cd "$ROOT_DIR" + +if ! command -v rpmbuild >/dev/null 2>&1; then + echo "package-rpm SKIPPED: rpmbuild not installed (not a dnf-based build host)" + exit 0 +fi +if [ ! -d "$ROOT_DIR/node_modules/electron/dist" ]; then + echo "package-rpm SKIPPED: node_modules/electron missing (run npm ci first)" + exit 0 +fi + +OUT_DIR="$(mktemp -d)" +trap 'rm -rf "$OUT_DIR"' EXIT + +RPM="$(STEPFORGE_PACKAGE_DIR="$OUT_DIR" bash packaging/linux/fedora/package.sh | tail -1)" +[ -f "$RPM" ] || { echo "package-rpm FAILED: builder produced no .rpm" >&2; exit 1; } + +fail() { echo "package-rpm FAILED: $1" >&2; exit 1; } + +listing="$(rpm -qlp "$RPM" 2>/dev/null)" + +for needle in \ + '/usr/bin/stepforge' \ + '/usr/share/applications/stepforge.desktop' \ + '/usr/share/mime/packages/stepforge.xml' \ + '/opt/stepforge/node_modules/electron/dist/electron' \ + '/opt/stepforge/app/main.js'; do + echo "$listing" | grep -qF "$needle" || fail "missing packaged file: $needle" +done +echo "$listing" | grep -q '/usr/share/icons/hicolor/256x256/apps/stepforge.png' || fail "missing 256px icon" + +# No dev tree / build tooling / app docs. +for banned in 'electron-builder' '/opt/stepforge/docs/' '/opt/stepforge/ai_prompts/' '/opt/stepforge/examples/'; do + echo "$listing" | grep -qF "$banned" && fail "unexpected payload: $banned" || true +done + +# Metadata sanity. +rpm -qip "$RPM" 2>/dev/null | grep -q '^Name *: stepforge' || fail "rpm Name is not stepforge" +rpm -qp --requires "$RPM" 2>/dev/null | grep -q '^nss' || fail "rpm does not Require nss" + +echo "package-rpm OK ($(basename "$RPM"))" diff --git a/tests/unit/packaging-linux.test.js b/tests/unit/packaging-linux.test.js index ed25940..e988bfc 100644 --- a/tests/unit/packaging-linux.test.js +++ b/tests/unit/packaging-linux.test.js @@ -33,6 +33,55 @@ test('the old non-production package-linux.sh is gone', () => { assert.equal(exists('scripts/package-linux.sh'), false); }); +test('Fedora/dnf packaging files exist in their own separate locations', () => { + for (const f of [ + 'packaging/linux/fedora/package.sh', + 'packaging/linux/fedora/stepforge.spec', + 'packaging/linux/common/stage-runtime.sh', + 'scripts/linux/dnf/install-runtime-deps.sh', + 'scripts/linux/dnf/install-build-deps.sh', + 'docs/linux/dnf.md', + 'tests/integration/linux/package-rpm.test.sh', + ]) { + assert.ok(exists(f), `expected ${f} to exist`); + } +}); + +test('the RPM spec declares runtime Requires, license, and MPL-2.0', () => { + const spec = read('packaging/linux/fedora/stepforge.spec'); + assert.match(spec, /^License:\s+MPL-2\.0$/m); + assert.match(spec, /^Requires:\s+nss$/m); + assert.match(spec, /%license/); + assert.match(spec, /chrome-sandbox/); // %post makes the sandbox helper setuid + assert.match(spec, /@VERSION@/); + assert.doesNotMatch(spec, /fully offline/i); +}); + +test('the rpm builder shares staging and requires rpmbuild + node_modules', () => { + const script = read('packaging/linux/fedora/package.sh'); + assert.match(script, /stage-runtime\.sh/); + assert.match(script, /rpmbuild/); + assert.match(script, /rpm --eval '%\{_arch\}'|uname -m/); // arch detected + assert.doesNotMatch(script, /cp -a "\$ROOT_DIR\/node_modules" /); +}); + +test('dnf setup scripts target dnf and keep build vs runtime deps separate', () => { + const runtime = read('scripts/linux/dnf/install-runtime-deps.sh'); + const build = read('scripts/linux/dnf/install-build-deps.sh'); + assert.match(runtime, /dnf install/); + assert.match(runtime, /nss/); + assert.doesNotMatch(runtime, /rpm-build|rpmdevtools/, 'runtime script must not install build tools'); + assert.match(build, /rpm-build/); +}); + +test('the shared staging never copies the whole dev node_modules', () => { + const staging = read('packaging/linux/common/stage-runtime.sh'); + assert.match(staging, /npm ls --omit=dev/); + assert.match(staging, /electron-builder/); // leak guard + assert.doesNotMatch(staging, /cp -a "\$ROOT_DIR\/node_modules" "\$APP_DIR/); + assert.match(staging, /node_modules\/electron\/dist/); // requires the runtime +}); + test('the desktop entry is valid and app-branded', () => { const desktop = read('packaging/linux/common/stepforge.desktop'); assert.match(desktop, /^\[Desktop Entry\]/); @@ -62,11 +111,9 @@ test('the launcher refuses an unsandboxed launch unless explicitly opted in', () assert.doesNotMatch(launcher, /npm (install|ci|rebuild)/); }); -test('the package builder requires node_modules and guards against dev-dep leaks', () => { +test('the deb builder detects arch and delegates to shared staging', () => { const script = read('packaging/linux/debian/package.sh'); - assert.match(script, /node_modules\/electron\/dist/); - assert.match(script, /npm ls --omit=dev/); - assert.match(script, /electron-builder/); // the leak guard references it + assert.match(script, /stage-runtime\.sh/); // runtime-only staging is shared assert.match(script, /dpkg --print-architecture/); // arch detected, not hardcoded assert.doesNotMatch(script, /cp -a "\$ROOT_DIR\/node_modules" /); // never copy the whole dev tree });