Add production .deb packaging, apt setup, desktop integration, and icons
Template tests / tests (pull_request) Failing after 31s
Template tests / tests (pull_request) Failing after 31s
Phase 3 of the improvement plan (PR 8 of the sequence): the apt/X11 packaging half of Linux support, in separate Linux-specific files. Replaces the old scripts/package-linux.sh, which the audit flagged as "not production packaging" (it copied the whole dev node_modules — including vulnerable build deps — plus docs/prompts/examples/audit files, hardcoded amd64, declared only xinput, lacked desktop/icon/MIME integration, and could build without node_modules). Production builder (packaging/linux/debian/package.sh): - Stages ONLY runtime files: app code, a fixed Electron runtime, and the production npm deps (enumerated via npm ls --omit=dev). Never copies the development node_modules; guards against electron-builder/app-builder-lib leaking in. Fails if node_modules is absent instead of shipping an unusable artifact. - Detects architecture (dpkg --print-architecture, x64/arm64) rather than hardcoding amd64. Generates DEBIAN/control from control.in with proper runtime Depends, real maintainer, and homepage. - Installs a desktop entry, hicolor icons (16–512), .sfgz/.sfglt MIME registration, the launcher, and the license. postinst makes chrome-sandbox setuid and refreshes desktop/MIME/icon caches; postrm cleans them. - Emits a .deb, a portable tarball that now INCLUDES /usr/bin/stepforge (the old tarball omitted it), and a sha256 sums file. Launcher (packaging/linux/common/launcher.sh): - Runs sandboxed; prefers the user-namespace sandbox, accepts a root-owned setuid helper, and otherwise refuses to launch with an actionable message. --no-sandbox requires an explicit STEPFORGE_ALLOW_NO_SANDBOX opt-in. Never installs anything at runtime. Setup (separate build vs runtime, apt only): - scripts/linux/apt/install-runtime-deps.sh (Chromium/Electron libs, X11 tools, portal/PipeWire) and install-build-deps.sh (dpkg-dev, fakeroot, xvfb). Runtime script installs no build tools. Assets: original StepForge icon — packaging/assets/stepforge.svg plus a generator (scripts/make-icons.js) that renders the PNG set with the repo's own rasterizer/PNG writer (no third-party art). npm run icons regenerates them. Wiring: package.json gains package:linux:deb / package:linux:rpm / icons; build-release.sh uses the production builder and requires node_modules; README points at the apt/dnf guides. Tests: tests/unit/packaging-linux.test.js (structural: files present in their separate locations, old script gone, valid desktop entry, templated arch + runtime Depends, launcher gates --no-sandbox, builder requires node_modules and guards dev-dep leaks, apt build/runtime dep separation, original icon set generates a valid PNG) runs in the normal suite; tests/integration/linux/package-deb.test.sh builds a real .deb and asserts the right files present and the dev tree / build tooling / app docs absent (honest skip only when dpkg-deb/node_modules are genuinely missing). Verified locally: 276 unit tests pass; the integration test builds and validates stepforge_0.3.2_amd64.deb; build-release E2E passes with the new production package. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# Integration test: build the production .deb and assert it is a real,
|
||||
# runtime-only package — the right files present, and the dev tree / build
|
||||
# tooling / app docs absent. A package is NOT accepted merely because
|
||||
# dpkg-deb produced a file.
|
||||
#
|
||||
# Honest skip policy: skip ONLY when the prerequisites are genuinely absent
|
||||
# (not apt-based, dpkg-deb 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 dpkg-deb >/dev/null 2>&1; then
|
||||
echo "package-deb SKIPPED: dpkg-deb not installed (not an apt-based build host)"
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -d "$ROOT_DIR/node_modules/electron/dist" ]; then
|
||||
echo "package-deb SKIPPED: node_modules/electron missing (run npm ci first)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
OUT_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$OUT_DIR"' EXIT
|
||||
|
||||
DEB="$(STEPFORGE_PACKAGE_DIR="$OUT_DIR" bash packaging/linux/debian/package.sh | head -1)"
|
||||
if [ ! -f "$DEB" ]; then
|
||||
echo "package-deb FAILED: builder did not produce a .deb" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fail() { echo "package-deb FAILED: $1" >&2; exit 1; }
|
||||
|
||||
listing="$(dpkg-deb -c "$DEB")"
|
||||
control="$(dpkg-deb -f "$DEB")"
|
||||
|
||||
# Required install items.
|
||||
for needle in \
|
||||
'./usr/bin/stepforge' \
|
||||
'./usr/share/applications/stepforge.desktop' \
|
||||
'./usr/share/mime/packages/stepforge.xml' \
|
||||
'./usr/share/icons/hicolor/256x256/apps/stepforge.png' \
|
||||
'./opt/stepforge/node_modules/electron/dist/electron' \
|
||||
'./opt/stepforge/app/main.js' \
|
||||
'./usr/share/doc/stepforge/copyright'; do
|
||||
echo "$listing" | grep -qF "$needle" || fail "missing packaged file: $needle"
|
||||
done
|
||||
|
||||
# The development node_modules / build tooling must NOT be present.
|
||||
for banned in \
|
||||
'node_modules/electron-builder' \
|
||||
'node_modules/app-builder-lib' \
|
||||
'node_modules/dmg-builder'; do
|
||||
echo "$listing" | grep -qF "$banned" && fail "build-only dependency leaked: $banned" || true
|
||||
done
|
||||
|
||||
# The app's own docs/prompts/examples must not be shipped.
|
||||
for banned in \
|
||||
'./opt/stepforge/docs/' \
|
||||
'./opt/stepforge/ai_prompts/' \
|
||||
'./opt/stepforge/examples/'; do
|
||||
echo "$listing" | grep -qF "$banned" && fail "app extra shipped: $banned" || true
|
||||
done
|
||||
|
||||
# Control metadata sanity.
|
||||
echo "$control" | grep -q '^Package: stepforge' || fail "control missing Package"
|
||||
echo "$control" | grep -q '^Depends:.*libnss3' || fail "control missing runtime Depends"
|
||||
echo "$control" | grep -Eq '^Architecture: (amd64|arm64)' || fail "control has no concrete Architecture"
|
||||
|
||||
# Sandbox is set up, not disabled: postinst makes chrome-sandbox setuid.
|
||||
dpkg-deb --info "$DEB" | grep -q 'postinst' || fail "no postinst maintainer script"
|
||||
|
||||
# The launcher must refuse an unsandboxed launch by default.
|
||||
grep -q 'STEPFORGE_ALLOW_NO_SANDBOX' packaging/linux/common/launcher.sh \
|
||||
|| fail "launcher does not gate --no-sandbox behind an explicit opt-in"
|
||||
|
||||
echo "package-deb OK ($(basename "$DEB"), $(du -h "$DEB" | cut -f1))"
|
||||
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..', '..');
|
||||
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
||||
const exists = (rel) => fs.existsSync(path.join(ROOT, rel));
|
||||
|
||||
// These are structural checks that run in the normal (cross-platform) unit
|
||||
// suite so the Linux packaging config can't rot silently; the actual .deb
|
||||
// build/inspection lives in tests/integration/linux/package-deb.test.sh.
|
||||
|
||||
test('Linux packaging files exist in their expected separate locations', () => {
|
||||
for (const f of [
|
||||
'packaging/linux/debian/package.sh',
|
||||
'packaging/linux/debian/control.in',
|
||||
'packaging/linux/common/stepforge.desktop',
|
||||
'packaging/linux/common/stepforge-mime.xml',
|
||||
'packaging/linux/common/launcher.sh',
|
||||
'scripts/linux/apt/install-runtime-deps.sh',
|
||||
'scripts/linux/apt/install-build-deps.sh',
|
||||
'docs/linux/apt.md',
|
||||
'tests/integration/linux/package-deb.test.sh',
|
||||
]) {
|
||||
assert.ok(exists(f), `expected ${f} to exist`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the old non-production package-linux.sh is gone', () => {
|
||||
assert.equal(exists('scripts/package-linux.sh'), false);
|
||||
});
|
||||
|
||||
test('the desktop entry is valid and app-branded', () => {
|
||||
const desktop = read('packaging/linux/common/stepforge.desktop');
|
||||
assert.match(desktop, /^\[Desktop Entry\]/);
|
||||
assert.match(desktop, /^Type=Application$/m);
|
||||
assert.match(desktop, /^Exec=stepforge %U$/m);
|
||||
assert.match(desktop, /^Icon=stepforge$/m);
|
||||
assert.match(desktop, /^Categories=.+;$/m);
|
||||
assert.match(desktop, /MimeType=application\/x-stepforge-guide/);
|
||||
});
|
||||
|
||||
test('the control template declares runtime deps, no hardcoded arch, real maintainer slot', () => {
|
||||
const control = read('packaging/linux/debian/control.in');
|
||||
assert.match(control, /^Architecture: @ARCH@$/m, 'arch must be templated, not hardcoded');
|
||||
assert.match(control, /^Depends:.*libnss3/m);
|
||||
assert.match(control, /@VERSION@/);
|
||||
assert.match(control, /@MAINTAINER@/);
|
||||
// The false "fully offline" wording must not reappear here.
|
||||
assert.doesNotMatch(control, /fully offline/i);
|
||||
});
|
||||
|
||||
test('the launcher refuses an unsandboxed launch unless explicitly opted in', () => {
|
||||
const launcher = read('packaging/linux/common/launcher.sh');
|
||||
assert.match(launcher, /STEPFORGE_ALLOW_NO_SANDBOX/);
|
||||
// It must not unconditionally exec with --no-sandbox.
|
||||
assert.doesNotMatch(launcher, /^exec .*--no-sandbox/m);
|
||||
// It never installs anything at runtime.
|
||||
assert.doesNotMatch(launcher, /npm (install|ci|rebuild)/);
|
||||
});
|
||||
|
||||
test('the package builder requires node_modules and guards against dev-dep leaks', () => {
|
||||
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, /dpkg --print-architecture/); // arch detected, not hardcoded
|
||||
assert.doesNotMatch(script, /cp -a "\$ROOT_DIR\/node_modules" /); // never copy the whole dev tree
|
||||
});
|
||||
|
||||
test('apt setup scripts target apt and keep build vs runtime deps separate', () => {
|
||||
const runtime = read('scripts/linux/apt/install-runtime-deps.sh');
|
||||
const build = read('scripts/linux/apt/install-build-deps.sh');
|
||||
assert.match(runtime, /apt-get/);
|
||||
assert.match(runtime, /libnss3/);
|
||||
assert.doesNotMatch(runtime, /dpkg-dev|fakeroot/, 'runtime script must not install build tools');
|
||||
assert.match(build, /dpkg-dev/);
|
||||
assert.match(build, /fakeroot/);
|
||||
});
|
||||
|
||||
test('an original icon set is generated (not a placeholder/third-party asset)', () => {
|
||||
assert.ok(exists('packaging/assets/stepforge.svg'));
|
||||
assert.ok(exists('scripts/make-icons.js'));
|
||||
// The generated PNGs are committed for packaging.
|
||||
for (const size of [16, 48, 256]) {
|
||||
assert.ok(exists(`packaging/assets/icons/stepforge-${size}.png`), `icon ${size} missing`);
|
||||
}
|
||||
// Regenerate the 256px icon and confirm the generator is deterministic and
|
||||
// produces a valid PNG (starts with the PNG signature).
|
||||
const { renderIcon } = require('../../scripts/make-icons');
|
||||
const { encodePng } = require('../../core/png');
|
||||
const png = encodePng(renderIcon(256));
|
||||
assert.deepEqual([...png.subarray(0, 4)], [0x89, 0x50, 0x4e, 0x47]);
|
||||
});
|
||||
Reference in New Issue
Block a user