mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-19 08:33:56 -06:00
TWO CHANGES, together because they touch the same packager hunks. 1. THE DRIVER. The BrightSign package used to be MANUFACTURED. scripts/build-server-zip.sh dropped better-sqlite3 from package.json and then installed db/sqlite-compat.js into node_modules under that name, so every require resolved to the façade. It worked — and it shipped a database layer that no test had ever executed. That is the same shape as the TELEMETRY_COLLECTOR TDZ crash that took production down while 1676 tests and four CI jobs were green: a build-time rewrite cannot be tested by the build that performs it. db/sqlite-driver.js now decides at runtime: the native driver when it loads, the node:sqlite façade otherwise. One artifact, one code path, and — the point — both branches reachable from a test. ST_SQLITE_DRIVER=node runs the entire suite the way a player runs it, and a new CI job does exactly that on Node 24 with --omit=optional so the fallback is reached the same way it is on hardware, not by an env var alone. better-sqlite3 becomes an optionalDependency, so a host with no compiler installs cleanly and falls back rather than failing. preflight-deps stops trying to rebuild a native module on a host that has no toolchain and a working built-in driver — on a player that was a five-minute node-gyp failure ending in a server that never started. Asking for the native driver BY NAME (ST_SQLITE_DRIVER=better-sqlite3) still fails loudly, because a production box that has lost its native module is broken and should say so rather than quietly running something else. ⚠️ NODE 24 IN PRACTICE. node:sqlite is unflagged only from 23.4; on the 22.x line it needs --experimental-sqlite and on 20.x it does not exist. So the code probes rather than comparing versions, the player package pins engines >=24, and the built-in cases skip on the Node 20 CI job rather than failing there. Verified on Node 24, both drivers, full suite: better-sqlite3 1762 pass / 0 fail node:sqlite 1762 pass / 0 fail and the built payload resolves node:sqlite with no better-sqlite3 present at all. 2. THE LICENCE. The ffprobe/ffmpeg binaries added in the previous commit are LGPL 2.1 and statically linked, so the licence text has to travel WITH them — a link on a website is not the copy the licence asks to accompany the work. The packager now copies COPYING.LGPLv2.1 and a build README into bin/, and refuses to build if the licence is missing. legal/third-party.html gains an LGPL section with the written offer required by section 6 for static linking, and the exact configure line. It also drops Sharp, which that page still listed although #263 removed it, and names what actually does the image work now (jimp, @jsquash/webp, @jsquash/avif). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56
383 lines
20 KiB
Bash
Executable file
383 lines
20 KiB
Bash
Executable file
#!/bin/bash
|
|
# Build brightsign/autorun-server.zip — the ScreenTinker SERVER, packaged to run ON a player.
|
|
#
|
|
# scripts/build-server-zip.sh [-o path/to/autorun-server.zip]
|
|
#
|
|
# Drop it on the storage root of a BrightSign running OS 10 (Node 24) and power-cycle. autozip.brs
|
|
# unpacks it in place and autorun.brs launches the server with roNodeJs, painting a diagnostic
|
|
# screen: URL, uptime, memory, disk, database size and a tail of the server's own console.
|
|
#
|
|
# ⚠️ THE BUNDLE MUST CONTAIN NO NATIVE CODE. That is the whole reason this is buildable on an
|
|
# x86_64 laptop for an aarch64 player. The server reaches SQLite through node:sqlite — built into
|
|
# the Node that BrightSignOS 10 already ships — via server/db/sqlite-compat.js. better-sqlite3 was
|
|
# the last native dependency; with it gone there is nothing to cross-compile, no ABI to match and
|
|
# no node-gyp to fail on a device with three slow cores. This script VERIFIES that rather than
|
|
# trusting it: any .node binary in the staged tree is a hard error, because the failure it would
|
|
# otherwise cause happens on the player, at boot, with no console.
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
|
|
OUT=""
|
|
PAYLOAD_ONLY=0
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
# Build the PAYLOAD half: everything except the boot files, for BrightSignOS 10 where a large
|
|
# autorun.zip cannot be opened by the boot-time zip reader. scripts/build-server-boot-zip.sh
|
|
# builds the other half, and bs-payload-install.js fetches this one onto the device.
|
|
--payload) PAYLOAD_ONLY=1; shift ;;
|
|
-o|--out) OUT="${2:-}"; shift 2 ;;
|
|
-h|--help) sed -n '2,18p' "$0"; exit 0 ;;
|
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$OUT" ] || { [ "$PAYLOAD_ONLY" = 1 ] && OUT="brightsign/server-payload.zip" || OUT="brightsign/autorun-server.zip"; }
|
|
|
|
command -v zip >/dev/null || { echo "ERROR: 'zip' is not installed." >&2; exit 1; }
|
|
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
|
|
# ⚠️ UNTRACKED FILES DO NOT SHIP, AND THE FAILURE LANDS ON THE PLAYER.
|
|
#
|
|
# Staging is `git ls-files`, which is the right call - it keeps databases, uploads and .env out by
|
|
# construction. The cost is that a NEW file that has not been `git add`ed is silently omitted while
|
|
# every file that requires it ships happily. That is not hypothetical twice over: it happened to
|
|
# db/sqlite-compat.js, and then to lib/fsutil.js, which reached a player as
|
|
#
|
|
# Cannot find module '../lib/fsutil'
|
|
# Require stack: /storage/ssd/server/db/database.js
|
|
#
|
|
# after a 73MB download and a two-minute extraction. The specific guard below for sqlite-compat.js
|
|
# was written after the first one; this is the general form, so there is no third.
|
|
UNTRACKED="$(git ls-files --others --exclude-standard -- server frontend scripts docs shared brightsign \
|
|
| grep -E '\.(js|json|html|brs|css|sql)$' || true)"
|
|
if [ -n "$UNTRACKED" ]; then
|
|
echo "ERROR: these source files are not tracked by git and would NOT be packaged:" >&2
|
|
echo "$UNTRACKED" | sed 's/^/ /' >&2
|
|
echo " git add them (or remove them) before building." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo " staging server..."
|
|
mkdir -p "$STAGE/server"
|
|
|
|
# ⚠️ GIT DECIDES WHAT SHIPS. Not a hand-written exclude list.
|
|
#
|
|
# The first version of this used --exclude and it staged 291MB: server/db/remote_display.db (a real
|
|
# 33MB database), server/uploads (105MB of customer content), server/certs, and .env. Every one of
|
|
# those is already in .gitignore — the knowledge existed, the packager just was not using it. A
|
|
# manual list is also the wrong shape: it has to be updated every time someone adds a secret, and
|
|
# the failure mode is silent and shipped.
|
|
#
|
|
# `git ls-files` yields exactly the tracked source, so anything gitignored is excluded by
|
|
# construction and stays excluded as the repo grows. node_modules is installed fresh below rather
|
|
# than copied, so a developer's x86_64 better-sqlite3 cannot ride along either.
|
|
#
|
|
# THE SERVER IS NOT SELF-CONTAINED, so the package reproduces the repo layout rather than just
|
|
# server/. Discovered the hard way: it resolves ../frontend for the dashboard (config.js:34),
|
|
# ../scripts for the multi-tenancy migration, ../VERSION, ../brightsign to build the PLAYER package
|
|
# it serves at /api/brightsign/package, and ../docs. Ship server/ alone and it boots, migrates, and
|
|
# then dies on `Cannot find module '../../scripts/migrate-multitenancy'`.
|
|
#
|
|
# Deliberately NOT shipped: android/ (228MB of APK build), video/, Examples/, audit/, tizen/ —
|
|
# none of which the server reads at runtime.
|
|
for p in server frontend scripts docs shared brightsign VERSION package.json; do
|
|
git ls-files -z -- "$p" \
|
|
| grep -zZv -E '^server/(test|node_modules)/|^brightsign/media-tools/' \
|
|
| while IFS= read -r -d '' f; do
|
|
mkdir -p "$STAGE/$(dirname "$f")"
|
|
cp "$f" "$STAGE/$f"
|
|
done
|
|
done
|
|
|
|
if [ "$PAYLOAD_ONLY" = 0 ]; then
|
|
cp brightsign/server/autorun.brs "$STAGE/autorun.brs"
|
|
cp brightsign/server/bs-server-boot.js "$STAGE/bs-server-boot.js"
|
|
cp brightsign/server/server.env.example "$STAGE/server.env.example"
|
|
cp brightsign/server/node-server.html "$STAGE/node-server.html"
|
|
cp brightsign/autozip.brs "$STAGE/autozip.brs"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# Media tools, at the top level as bin/ — where stageMediaTools() in bs-server-boot.js looks
|
|
# (path.join(__dirname, 'bin', ...), and the payload installs INTO __dirname).
|
|
#
|
|
# PAYLOAD ONLY, deliberately. The boot zip is read by the OS's own zip reader at boot and a 73MB one
|
|
# failed outright with "ZipArchive error"; it is ~64KB and stays that way. These ride with the
|
|
# payload instead, which also means an update refreshes them.
|
|
#
|
|
# Shipped gzipped and stored (-0) rather than compressed, because they are already compressed —
|
|
# 3.2MB here against 6.7MB unpacked, which is what /tmp pays at runtime.
|
|
#
|
|
# ⚠️ A hard error rather than a warning. Both files are tracked, so absence means someone removed
|
|
# them, and the failure it would otherwise produce is a player that silently stops making video
|
|
# thumbnails - exactly the silent degradation this whole path was built to end.
|
|
if [ "$PAYLOAD_ONLY" = 1 ]; then
|
|
echo " staging media tools..."
|
|
mkdir -p "$STAGE/bin"
|
|
for tool in ffprobe ffmpeg; do
|
|
src="brightsign/media-tools/$tool.gz"
|
|
if [ ! -f "$src" ]; then
|
|
echo "ERROR: $src is missing — the payload would ship without video thumbnails." >&2
|
|
exit 1
|
|
fi
|
|
cp "$src" "$STAGE/bin/$tool.gz"
|
|
echo " bin/$tool.gz ($(du -h "$src" | cut -f1))"
|
|
done
|
|
# ⚠️ THE LICENCE TRAVELS WITH THE BINARIES. These are LGPL 2.1 and statically linked, so the
|
|
# package that carries them has to carry the licence text too — a link on a website is not the
|
|
# same thing as the copy the licence asks to accompany the work.
|
|
if [ ! -f brightsign/media-tools/COPYING.LGPLv2.1 ]; then
|
|
echo "ERROR: brightsign/media-tools/COPYING.LGPLv2.1 is missing — LGPL binaries cannot ship without it." >&2
|
|
exit 1
|
|
fi
|
|
cp brightsign/media-tools/COPYING.LGPLv2.1 "$STAGE/bin/COPYING.LGPLv2.1"
|
|
cp brightsign/media-tools/README.md "$STAGE/bin/README.md"
|
|
echo " bin/COPYING.LGPLv2.1 + bin/README.md"
|
|
fi
|
|
|
|
# The player has no compiler, so the package must not carry better-sqlite3.
|
|
#
|
|
# NOTHING IS REWRITTEN HERE ANY MORE. db/sqlite-driver.js chooses at RUNTIME - native if it loads,
|
|
# node:sqlite otherwise - so the payload is main's code, unmodified. It used to be manufactured: this
|
|
# script dropped the dependency and then installed db/sqlite-compat.js into node_modules UNDER THE
|
|
# NAME better-sqlite3, which worked and shipped a database layer no test had ever executed.
|
|
#
|
|
# All that is left is to not install the native module. It is an optionalDependency, so
|
|
# `npm install --omit=optional` leaves it out and the runtime fallback does the rest.
|
|
echo " omitting the native sqlite driver (runtime fallback handles it)..."
|
|
python3 - "$STAGE" <<'PY'
|
|
import json, os, sys
|
|
stage = sys.argv[1]
|
|
root = os.path.join(stage, 'server')
|
|
|
|
# The fallback is worthless without the shim and the chooser. Both are TRACKED source files, so if
|
|
# either is missing the staging step never saw it - which happened once because sqlite-compat.js had
|
|
# been written but not `git add`ed, and the only symptom was the server refusing to start with
|
|
# "Cannot find module './sqlite-compat.js'".
|
|
for required in ('sqlite-compat.js', 'sqlite-driver.js'):
|
|
if not os.path.exists(os.path.join(root, 'db', required)):
|
|
sys.exit("ERROR: server/db/%s is not in the staged tree - is it committed to git?" % required)
|
|
|
|
pkg = os.path.join(root, 'package.json')
|
|
d = json.load(open(pkg))
|
|
if 'better-sqlite3' in d.get('optionalDependencies', {}):
|
|
del d['optionalDependencies']['better-sqlite3']
|
|
if not d['optionalDependencies']:
|
|
del d['optionalDependencies']
|
|
print(" dropped better-sqlite3 (the player falls back to node:sqlite)")
|
|
if 'better-sqlite3' in d.get('dependencies', {}):
|
|
sys.exit("ERROR: better-sqlite3 is a hard dependency again - the player cannot build native code.")
|
|
# The player has Node 24, and node:sqlite is unflagged only from 23.4 - on 22.x it needs
|
|
# --experimental-sqlite and the fallback would not load. Say so, so an install on anything older
|
|
# fails loudly rather than at the first query.
|
|
d['engines'] = {'node': '>=24.0.0'}
|
|
json.dump(d, open(pkg, 'w'), indent=2)
|
|
PY
|
|
|
|
echo " installing production dependencies..."
|
|
( cd "$STAGE/server" && rm -f package-lock.json && npm install --omit=dev --omit=optional --no-audit --no-fund --silent )
|
|
|
|
# THE INVARIANT. A single .node file here means the package cannot run on the player, and the way
|
|
# you would find out is a boot loop on a box with no console.
|
|
# ⚠️ ESM-ONLY PACKAGES DO NOT WORK INSIDE THE WIDGET.
|
|
#
|
|
# The player runs the server inside an Electron roHtmlWidget, and Electron's module loader does NOT
|
|
# implement require(ESM) - plain Node 24 does, which is why this only ever fails on hardware and
|
|
# never in a local test. An ESM-only dependency therefore gets compiled as CommonJS and dies on its
|
|
# first `export` keyword:
|
|
#
|
|
# [boot] Migration FAILED: Failed to construct 'ContextifyScript': Invalid or unexpected token
|
|
#
|
|
# uuid 14 is the one that bites immediately (21 files import it, including the multi-tenancy
|
|
# migration that runs on first boot), and it is trivially replaceable: only `v4` is used anywhere,
|
|
# and crypto.randomUUID() produces exactly that. So it gets the same treatment as better-sqlite3 -
|
|
# a CommonJS package installed under the real name, after npm, so every require resolves to it.
|
|
# ⚠️ STRIP SHEBANGS. Electron's module loader does not remove them; Node's does.
|
|
#
|
|
# Node's CJS loader strips a leading #! before wrapping a file in the module function, precisely
|
|
# because a shebang is not valid JavaScript. Electron's loader does not, so the wrapper it compiles
|
|
# is "(function(){#!/usr/bin/env node ...})" and V8 refuses it:
|
|
#
|
|
# [boot] Migration FAILED: Failed to construct 'ContextifyScript': Invalid or unexpected token
|
|
#
|
|
# Note the error names no token - "#" is not one. An ESM file compiled as CJS reports
|
|
# "Unexpected token 'export'" instead, which is how these two are told apart; chasing the ESM
|
|
# explanation for this error cost a full boot cycle.
|
|
#
|
|
# scripts/migrate-multitenancy.js is required during first boot, so it fails every time. Eight other
|
|
# shipped scripts carry the same line and would fail whenever something requires them.
|
|
#
|
|
# Done HERE and not in the source: those files are meant to be executable (./scripts/reset-admin.js),
|
|
# and the shebang is correct everywhere except inside this widget. The line is replaced by a comment
|
|
# of the same length rather than deleted, so line numbers in stack traces still match the source.
|
|
echo " stripping shebangs (Electron does not strip them; Node does)..."
|
|
python3 - "$STAGE" <<'SHEBANG'
|
|
import io, os, sys
|
|
stage = sys.argv[1]
|
|
fixed = []
|
|
for root, dirs, files in os.walk(stage):
|
|
if 'node_modules' in root.split(os.sep):
|
|
continue
|
|
for name in files:
|
|
if not name.endswith('.js'):
|
|
continue
|
|
path = os.path.join(root, name)
|
|
with io.open(path, 'rb') as fh:
|
|
head = fh.read(2)
|
|
if head != b'#!':
|
|
continue
|
|
rest = fh.read()
|
|
data = b'#!' + rest
|
|
nl = data.index(b'\n') if b'\n' in data else len(data)
|
|
# '//' + the rest of the shebang keeps the byte count and the line count identical.
|
|
with io.open(path, 'wb') as fh:
|
|
fh.write(b'//' + data[2:nl] + data[nl:])
|
|
fixed.append(os.path.relpath(path, stage))
|
|
for f in sorted(fixed):
|
|
print(" " + f)
|
|
print(" %d file(s)" % len(fixed))
|
|
SHEBANG
|
|
|
|
echo " installing a CommonJS uuid shim (Electron cannot require ESM)..."
|
|
mkdir -p "$STAGE/server/node_modules/uuid"
|
|
printf '%s\n' \
|
|
'{' \
|
|
' "name": "uuid",' \
|
|
' "version": "0.0.0-cjs-shim",' \
|
|
' "description": "Not the real uuid - a CommonJS facade over crypto.randomUUID, because Electron cannot require the ESM-only uuid 14.",' \
|
|
' "main": "index.js"' \
|
|
'}' > "$STAGE/server/node_modules/uuid/package.json"
|
|
printf '%s\n' \
|
|
"const { randomUUID } = require('crypto');" \
|
|
'// crypto.randomUUID() IS a v4 UUID (RFC 4122, random). Only v4 is used in this codebase; the' \
|
|
'// others are present so an accidental import fails loudly rather than silently returning undefined.' \
|
|
'const v4 = () => randomUUID();' \
|
|
"const notImplemented = (name) => () => { throw new Error('uuid.' + name + ' is not available in the BrightSign build (CJS shim provides v4 only)'); };" \
|
|
'module.exports = { v4, default: { v4 },' \
|
|
" v1: notImplemented('v1'), v3: notImplemented('v3'), v5: notImplemented('v5')," \
|
|
" v6: notImplemented('v6'), v7: notImplemented('v7')," \
|
|
" validate: (s) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(s)) };" \
|
|
> "$STAGE/server/node_modules/uuid/index.js"
|
|
node -e "const u=require('$STAGE/server/node_modules/uuid'); const v=u.v4();
|
|
if(!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v)) { console.error('ERROR: uuid shim does not produce a v4 UUID: '+v); process.exit(1); }
|
|
if(!u.validate(v)) { console.error('ERROR: uuid shim validate() rejects its own output'); process.exit(1); }
|
|
console.log(' shim produces valid v4 UUIDs (' + v + ')');"
|
|
|
|
# What is still ESM-only, and therefore still a landmine on this platform? Report it rather than
|
|
# pretend it is handled - these fail only when their code path is first exercised on the player.
|
|
echo " remaining ESM-only packages (fine under Node, FAIL inside the widget when first required):"
|
|
# Fed by heredoc, not `node -e '...'`: the scan needs both quote characters and nesting them
|
|
# inside a single-quoted shell argument mangles them.
|
|
node - "$STAGE" <<'ESMSCAN'
|
|
const fs = require("fs"), path = require("path");
|
|
const root = path.join(process.argv[2], "server", "node_modules");
|
|
const out = [];
|
|
const scan = (dir, depth) => {
|
|
let entries = [];
|
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return; }
|
|
for (const d of entries) {
|
|
if (!d.isDirectory()) continue;
|
|
if (d.name.startsWith("@") && depth === 0) { scan(path.join(dir, d.name), 1); continue; }
|
|
const pj = path.join(dir, d.name, "package.json");
|
|
if (!fs.existsSync(pj)) continue;
|
|
let j;
|
|
try { j = JSON.parse(fs.readFileSync(pj, "utf8")); } catch (e) { continue; }
|
|
if (j.type !== "module") continue;
|
|
// A CommonJS entry point, or a "require" condition in exports, means require() still works.
|
|
if (j.main && !String(j.main).endsWith(".mjs")) continue;
|
|
if (JSON.stringify(j.exports || "").includes(JSON.stringify("require"))) continue;
|
|
out.push((j.name || d.name) + "@" + (j.version || "?"));
|
|
}
|
|
};
|
|
scan(root, 0);
|
|
if (!out.length) console.log(" none");
|
|
else out.sort().forEach((n) => console.log(" " + n));
|
|
ESMSCAN
|
|
|
|
echo " verifying the bundle is architecture-independent..."
|
|
if find "$STAGE" -name '*.node' -print -quit | grep -q .; then
|
|
echo "ERROR: native binaries in the bundle — this cannot run on the player:" >&2
|
|
find "$STAGE" -name '*.node' | sed 's/^/ /' >&2
|
|
exit 1
|
|
fi
|
|
# The native module must be ABSENT, not shimmed. If npm reinstalled it (a stray `dependencies`
|
|
# entry, a transitive requirement) the .node check above would usually catch it - but a
|
|
# prebuild-less install can leave the JS half on disk with no binary, which loads fine here and
|
|
# fails on the player at the first query.
|
|
if [ -e "$STAGE/server/node_modules/better-sqlite3" ]; then
|
|
echo "ERROR: better-sqlite3 is present in the bundle — the player has no compiler for it." >&2
|
|
exit 1
|
|
fi
|
|
for f in db/sqlite-driver.js db/sqlite-compat.js; do
|
|
if [ ! -f "$STAGE/server/$f" ]; then
|
|
echo "ERROR: server/$f missing — the runtime fallback to node:sqlite cannot work." >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
echo " no .node binaries, no better-sqlite3, node:sqlite fallback present — portable"
|
|
|
|
# THE SECOND INVARIANT, and the one that matters more. This package gets copied onto hardware that
|
|
# leaves the building. The first build of it contained a real 33MB customer database, 105MB of
|
|
# uploads, the TLS certs and .env — because the staging step used an exclude list instead of git.
|
|
# Verified rather than trusted, because "we removed it" is not a property, it is a memory.
|
|
# Verified, not assumed: one surviving shebang is one boot failure with a misleading error.
|
|
LEFTOVER="$(find "$STAGE" -name '*.js' -not -path '*/node_modules/*' -exec sh -c 'head -c2 "$1" | grep -q "#!" && echo "$1"' _ {} \; )"
|
|
if [ -n "$LEFTOVER" ]; then
|
|
echo "ERROR: shebangs remain; these will fail to compile inside the widget:" >&2
|
|
echo "$LEFTOVER" | sed 's/^/ /' >&2
|
|
exit 1
|
|
fi
|
|
echo " no shebangs remain in shipped scripts"
|
|
|
|
echo " verifying no data or secrets are bundled..."
|
|
LEAKS="$(find "$STAGE" \( -name '*.db' -o -name '*.db-wal' -o -name '*.db-shm' -o -name '.env' \
|
|
-o -name '.env.*' -o -name '*.pem' -o -name '*.key' -o -name '*.devbak' \) \
|
|
-not -path '*/node_modules/*' -print)"
|
|
if [ -n "$LEAKS" ]; then
|
|
echo "ERROR: the bundle contains data or secrets:" >&2
|
|
echo "$LEAKS" | sed 's/^/ /' >&2
|
|
exit 1
|
|
fi
|
|
for d in uploads certs data; do
|
|
if [ -d "$STAGE/server/$d" ] && [ -n "$(ls -A "$STAGE/server/$d" 2>/dev/null)" ]; then
|
|
echo "ERROR: server/$d is non-empty in the bundle — that is runtime state, not source." >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
echo " no databases, uploads, certs or .env"
|
|
|
|
mkdir -p "$(dirname "$OUT")"
|
|
rm -f "$OUT"
|
|
ABS_OUT="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")"
|
|
|
|
# -0 = STORED, for the same reason as the player package: BrightSign's deployment reported our
|
|
# first DEFLATED archive as invalid, and roBrightPackage documents "no compression" as the
|
|
# universally safe option. A compressed archive copies across perfectly and then fails to open.
|
|
( cd "$STAGE" && zip -q -r -X -0 "$ABS_OUT" . )
|
|
|
|
echo " built $OUT ($(du -h "$ABS_OUT" | cut -f1))"
|
|
|
|
# Listed ONCE into a variable. `unzip -l | grep -q` looks obvious and is a trap here: grep -q exits
|
|
# at the first match, unzip dies of SIGPIPE, and under `set -o pipefail` the pipeline reports
|
|
# failure — so a file that IS present is reported missing. With 9000+ entries it fires every time.
|
|
LISTING="$(unzip -l "$OUT")"
|
|
echo "$LISTING" | tail -1 | sed 's/^/ /'
|
|
REQUIRED="autorun.brs autozip.brs bs-server-boot.js node-server.html"
|
|
# The payload is verified on the thing the installer actually looks for before it commits the
|
|
# extraction. An archive that unpacks perfectly and lacks this is the failure worth catching here.
|
|
[ "$PAYLOAD_ONLY" = 1 ] && REQUIRED="server/server.js bin/ffprobe.gz bin/ffmpeg.gz bin/COPYING.LGPLv2.1"
|
|
for required in $REQUIRED; do
|
|
case "$LISTING" in
|
|
*" $required"*) ;;
|
|
*) echo "ERROR: $required missing from the archive" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
COMPRESSED="$(unzip -v "$OUT" | awk '$1 ~ /^[0-9]+$/ && $2 ~ /^[A-Za-z]/ && $2 != "Stored" {print $2}' | head -1)"
|
|
if [ -n "$COMPRESSED" ]; then
|
|
echo "ERROR: archive contains compressed members ($COMPRESSED); BrightSign needs it stored." >&2
|
|
exit 1
|
|
fi
|
|
echo " root-level layout verified, all members stored"
|