Add production .rpm packaging and dnf setup; share runtime staging
Template tests / tests (pull_request) Failing after 33s
Template tests / tests (pull_request) Failing after 33s
Phase 3 of the improvement plan (PR 9 of the sequence): the dnf/Fedora packaging half, in separate Linux-specific files, mirroring the apt/.deb work. - packaging/linux/common/stage-runtime.sh: shared runtime-only payload staging extracted from the .deb builder so the two package formats never drift. It copies app code + a fixed Electron runtime + production npm deps (npm ls --omit=dev), guards against dev-dep leaks, and fails without node_modules. The .deb builder now delegates to it. - packaging/linux/fedora/stepforge.spec: runtime Requires (nss, nspr, gtk3, …), Recommends (xinput, portal, pipewire), %license, and a %post that makes chrome-sandbox setuid and refreshes desktop/MIME/icon caches. No compilation — it packages the prebuilt BuildRoot. - packaging/linux/fedora/package.sh: stages the shared payload, substitutes version/maintainer into the spec, and runs rpmbuild against the prebuilt BuildRoot with detected arch. Requires rpmbuild + node_modules; emits an .rpm + sha256. - scripts/linux/dnf/install-runtime-deps.sh and install-build-deps.sh: separate runtime vs build dependency sets for dnf (runtime installs no build tools). - docs/linux/dnf.md: Fedora/RHEL install + build guide, distinct from apt.md. Tests: packaging-linux.test.js gains Fedora/dnf structural checks (spec Requires + MPL-2.0 + %license + sandbox setup, builder shares staging + requires rpmbuild, dnf build/runtime dep separation, shared staging never copies the whole dev tree). tests/integration/linux/package-rpm.test.sh builds a real .rpm and asserts runtime-only contents (honest skip when rpmbuild is absent, as on this apt host). Verified: 13 packaging unit tests pass; the refactored .deb builder still produces a valid runtime-only package (integration test green); the .rpm integration test skips cleanly where rpmbuild is unavailable. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -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"))"
|
||||
@@ -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
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user