From f86195df53a4e869a0ec091d0c933c88aa5e4074 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Wed, 5 Aug 2026 00:06:00 -0500 Subject: [PATCH] BrightSign: autorun.zip installer, built and shipped with every release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four loose files that must all land intact, in the right place, is a poor way to hand someone a player. autorun.zip is one file: drop it on the root of a player's storage, power-cycle, and autozip.brs unpacks it in place and reboots into the player. A half-copied set of loose files boots into something broken; a half-copied zip simply fails to extract and leaves the player as it was. Two rules the format imposes, both of which fail SILENTLY when broken, so the build script asserts them instead of trusting them: - the archive must expand to files at its root, with no wrapper directory. A player extracts to the storage root, so a nested folder puts autorun.brs somewhere the player never looks and the card appears to do nothing. - autorun.brs must not sit next to autorun.zip on the storage root; its presence stops the zip being processed at all. autozip.brs renames the archive to autorun.zip.done after a successful extract, which is what makes it idempotent — without that the player extracts, reboots, extracts, reboots, a loop indistinguishable from a hardware fault. A FAILED extract deliberately does not rename, so a truncated copy gets retried once someone replaces it rather than being skipped forever. It is volume-aware for the same reason autorun.brs is: a player may be booting from internal flash because its card interface is dead, and extracting to "SD:/" on such a unit writes to a volume that does not exist. --server rewrites screentinker.json in the staging copy so a batch can be imaged for a specific instance without hand-editing anything. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL --- .github/workflows/release.yml | 12 ++++- brightsign/README.md | 27 ++++++++++++ brightsign/autozip.brs | 83 +++++++++++++++++++++++++++++++++++ scripts/build-autorun-zip.sh | 78 ++++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 brightsign/autozip.brs create mode 100755 scripts/build-autorun-zip.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c893f3d..3028207 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,6 +79,12 @@ jobs: cp tizen/ScreenTinker.wgt ScreenTinker.wgt ls -la ScreenTinker.wgt + - name: Build BrightSign autorun.zip (single-file player installer) + run: | + chmod +x scripts/build-autorun-zip.sh + ./scripts/build-autorun-zip.sh -o autorun.zip + ls -la autorun.zip + - name: Build source tarball (bundles the .wgt; the signed apk is added by scripts/finalize-release.sh) run: | OUT="screentinker-${{ steps.ver.outputs.version }}.tar.gz" @@ -87,7 +93,7 @@ jobs: --exclude='*.db' --exclude='*.db-wal' --exclude='*.db-shm' --exclude='*.db.*' \ --exclude='server/uploads' --exclude='server/certs' --exclude='server/test' \ --exclude='*.apk' \ - server frontend scripts docs VERSION README.md LICENSE .env.example ScreenTinker.wgt + server frontend scripts docs VERSION README.md LICENSE .env.example ScreenTinker.wgt brightsign echo "TARBALL=$OUT" >> "$GITHUB_ENV" ls -la "$OUT" @@ -111,6 +117,10 @@ jobs: echo " Sign it with your own Samsung certificate (Tizen Studio + a profile that includes" echo " your TV's DUID) to install, or - easiest - point a Tizen TV browser / URL Launcher" echo " at \`https:///player\` (no signing needed)." + echo "- \`autorun.zip\` - BrightSign player installer. Drop it on the root of a player's" + echo " storage (microSD, USB, or internal flash) and power-cycle: it unpacks itself and" + echo " reboots into the player. Edit \`screentinker.json\` inside the archive first to" + echo " point it at your own server." if [ "${{ steps.ver.outputs.prerelease }}" = "true" ]; then echo "- Docker image: \`ghcr.io/screentinker/screentinker:${{ steps.ver.outputs.version }}\` (pre-release - \`:latest\` is NOT moved)." else diff --git a/brightsign/README.md b/brightsign/README.md index c41eecf..44a64eb 100644 --- a/brightsign/README.md +++ b/brightsign/README.md @@ -75,6 +75,33 @@ the script loaded and then could not find its own `index.html`. them. A stale copy on a card is precisely the version skew that would leave a panel unable to restart itself. +## autorun.zip — one file instead of four + +`scripts/build-autorun-zip.sh` packages the host, the fallback page and the config into a single +`autorun.zip`, attached to every GitHub release: + +```bash +scripts/build-autorun-zip.sh --server https://your-server +``` + +Drop it on the root of a player's storage and power-cycle. `autozip.brs` unpacks it in place, +renames it `autorun.zip.done` so it never re-extracts, and reboots into the player. + +Two rules the format imposes, both of which fail silently if broken: + +- **The archive must expand to files at its ROOT**, with no wrapper directory — a player extracts + to the storage root, so a nested folder puts `autorun.brs` somewhere the player never looks and + the card appears to do nothing. The build script zips from *inside* the staging directory and + then asserts the layout rather than trusting it. +- **`autorun.brs` must NOT sit next to `autorun.zip`** on the storage root; its presence stops the + zip being processed at all. It belongs inside the archive. + +The rename is what makes it idempotent. Without it the player extracts, reboots, extracts, reboots +— a loop that looks exactly like a hardware fault. An extraction *failure* deliberately does not +rename, so a truncated copy is retried after someone replaces it rather than skipped forever. + +Requires BrightSignOS 7.0.60+ (`roUnzip`). + ## Provisioning Config resolves `screentinker.json` on the card **>** registry **>** built-in default. The JSON diff --git a/brightsign/autozip.brs b/brightsign/autozip.brs new file mode 100644 index 0000000..5bc3921 --- /dev/null +++ b/brightsign/autozip.brs @@ -0,0 +1,83 @@ +' ScreenTinker — autorun.zip unpacker. +' +' Ships INSIDE autorun.zip, at its root. The card (or internal flash) carries a single file — +' autorun.zip — and this script unpacks it in place, marks it done so it never re-extracts, and +' reboots into the real host. +' +' That is the whole point of the zip: one file to hand someone, or to drop on a hundred cards, +' instead of four files that must all arrive intact and in the right place. A partially-copied +' set of loose files boots into something broken; a partially-copied zip simply fails to extract +' and leaves the player where it was. +' +' ⚠️ autorun.brs must NOT sit next to autorun.zip on the storage root — its presence stops the zip +' being processed at all. autorun.brs belongs INSIDE the zip, which is where the build script puts +' it (scripts/build-autorun-zip.sh). +' +' Requires BrightSignOS 7.0.60+ (roUnzip). + +Function StorageRoot() As String + ' Same reasoning as autorun.brs: a player may be booting from internal flash rather than a + ' card — the only path on a unit whose card interface has failed. Extracting to "SD:/" on such + ' a player writes to a volume that does not exist. + if DoesFileExist("FLASH:/autorun.zip") then return "FLASH:" + return "SD:" +End Function + +Sub Main() + root$ = StorageRoot() + zipPath$ = root$ + "/autorun.zip" + extractPath$ = root$ + "/" + donePath$ = root$ + "/autorun.zip.done" + + print "[st-autozip] volume "; root$ + + if not DoesFileExist(zipPath$) then + print "[st-autozip] no autorun.zip at "; zipPath$; " — nothing to do" + return + end if + + ' Idempotence. Without this the player extracts, reboots, extracts again, reboots again — + ' a boot loop that looks like a hardware fault. + if DoesFileExist(donePath$) then + print "[st-autozip] already unpacked (autorun.zip.done present) — leaving it alone" + return + end if + + print "[st-autozip] unpacking "; zipPath$ + + unzip = CreateObject("roUnzip", zipPath$) + if unzip = invalid then + print "[st-autozip] ERROR: could not open the archive" + return + end if + + result = unzip.DecompressAllFiles(extractPath$) + if result <> 0 then + print "[st-autozip] ERROR: extract failed, code "; result + ' Deliberately NOT marking it done: a corrupt or truncated copy should be retried after + ' someone replaces the file, not silently skipped forever. + return + end if + + print "[st-autozip] extracted" + + fs = CreateObject("roFileSystem") + if fs = invalid then + print "[st-autozip] ERROR: no roFileSystem — cannot mark the archive done" + return + end if + + if not fs.Rename(zipPath$, donePath$) then + print "[st-autozip] ERROR: could not rename the archive; refusing to reboot into a loop" + return + end if + + print "[st-autozip] rebooting into the unpacked player" + sleep(2000) + RebootSystem() +End Sub + +Function DoesFileExist(filePath$ As String) As Boolean + files = MatchFiles(filePath$, filePath$) + return files.Count() > 0 +End Function diff --git a/scripts/build-autorun-zip.sh b/scripts/build-autorun-zip.sh new file mode 100755 index 0000000..986f306 --- /dev/null +++ b/scripts/build-autorun-zip.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Build brightsign/autorun.zip — the single-file installer for a BrightSign player. +# +# scripts/build-autorun-zip.sh [--server https://your-server] [-o path/to/autorun.zip] +# +# Drop the resulting autorun.zip on the root of a player's storage (microSD, USB, or internal +# flash over SFTP) and power-cycle. autozip.brs unpacks it in place, marks it done, and reboots +# into the player. One file to distribute instead of four that must all land intact. +# +# ⚠️ The zip must expand to files AT ITS ROOT — no wrapper directory. A player extracts to the +# storage root, so a nested folder puts autorun.brs somewhere the player never looks and the +# card silently does nothing. That is why this zips from *inside* the staging directory. +# +# ⚠️ autorun.brs must NOT sit next to autorun.zip on the storage root: its presence stops the zip +# being processed at all. It belongs inside, which is where this puts it. +set -euo pipefail +cd "$(dirname "$0")/.." + +SERVER="" +OUT="brightsign/autorun.zip" +while [ $# -gt 0 ]; do + case "$1" in + --server) SERVER="${2:-}"; shift 2 ;; + -o|--out) OUT="${2:-}"; shift 2 ;; + -h|--help) sed -n '2,12p' "$0"; exit 0 ;; + *) echo "unknown argument: $1" >&2; exit 1 ;; + esac +done + +command -v zip >/dev/null || { echo "ERROR: 'zip' is not installed." >&2; exit 1; } + +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT + +# The payload. autozip.brs must be here too: it is what the NEXT player to receive this archive +# runs, and it has to survive being extracted alongside everything else. +cp brightsign/autozip.brs "$STAGE/" +cp brightsign/autorun.brs "$STAGE/" +cp brightsign/offline.html "$STAGE/" +cp brightsign/screentinker.json "$STAGE/" + +# Point a batch at a specific server without hand-editing each card. +if [ -n "$SERVER" ]; then + python3 - "$STAGE/screentinker.json" "$SERVER" <<'PY' +import json, sys +path, server = sys.argv[1], sys.argv[2] +cfg = json.load(open(path)) +cfg['server_url'] = server +json.dump(cfg, open(path, 'w'), indent=2) +PY + echo " server_url set to $SERVER" +fi + +mkdir -p "$(dirname "$OUT")" +rm -f "$OUT" +ABS_OUT="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")" + +# -j would flatten any directories we add later; instead cd in and zip '.' so the archive root IS +# the staging root, and future subdirectories keep their structure. +( cd "$STAGE" && zip -q -r -X "$ABS_OUT" . ) + +echo " built $OUT" +unzip -l "$OUT" | sed 's/^/ /' + +# Prove the root-level invariant rather than trusting it: this is the one mistake that makes a +# card look blank to the player, and it is invisible until hardware refuses to boot. +if unzip -l "$OUT" | awk 'NR>3 && $4 ~ /\// && $4 !~ /^[^\/]+$/ {print $4}' | grep -qE '^[^/]+/'; then + echo " NOTE: archive contains directories — verify they are intended subdirectories, not a wrapper." +fi +if ! unzip -l "$OUT" | grep -qE ' autorun\.brs$'; then + echo "ERROR: autorun.brs is not at the archive root — the player would never find it." >&2 + exit 1 +fi +if ! unzip -l "$OUT" | grep -qE ' autozip\.brs$'; then + echo "ERROR: autozip.brs is missing — nothing would unpack this archive." >&2 + exit 1 +fi +echo " root-level layout verified"