A manager-run 'update apply' refreshes code/apps/WebUI but CANNOT rewrite the root-owned footprint (helpers/wrapper/uninstall/unit/sudoers) — that immutability is the de-sudo boundary. Previously a release that changed those would silently leave them stale. Make it explicit: - init.sh: footprint_version=1 constant, baked at install into /usr/local/lib/libreportal/.footprint_version (root:root 0644) by initRootHelpers. Bump it whenever a root component changes. - make_release.sh: publishes footprint_version in latest.json. - fetch.sh: lpInstalledFootprintVersion (marker) + lpReleaseLatestFootprint (manifest). - check_update.sh: 'update apply' REFUSES when the release's footprint_version exceeds the installed one, directing to a root re-install (which fetches + re-bakes everything atomically). No half-applied updates. - webui_system_update.sh: badge sets footprint_update_needed + clears can_update so the WebUI won't offer a one-click apply for a footprint-bumping release. - docs/DEVELOPMENT.md: the bump rule + the footprint exception explained. Verified: manifest carries footprint_version; drift decision correct both ways (no marker/older -> needs re-install; equal -> no drift). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
6.9 KiB
LibrePortal — Development & Releases
How to run a development copy, cut stable/edge releases, and test them before they go out. For installing/using LibrePortal, see USER.md.
Mental model (read this first)
Install modes — CFG_INSTALL_MODE decides where the code comes from:
| Mode | Source | Use |
|---|---|---|
release (default) |
a checksum-verified .tar.gz over HTTPS |
end users / stable |
git |
git clone of the repo |
contributors tracking a branch |
local |
a copy of a local folder | hacking on the code on the box |
Three roots (each relocatable at install, then fixed):
<system> (manager-owned: configs/db/logs/install) · <containers> (container
user: app data) · <backups> (container user: repos). Defaults /libreportal-*.
Two users: the manager (sudo_user_name, default libreportal) owns the
control plane and runs the runtime; the container user (CFG_DOCKER_INSTALL_USER,
default dockerinstall) owns app data + runs rootless Docker. Genuine-root actions
go through fixed, root-owned helpers in /usr/local/lib/libreportal/ (paths +
manager name are baked into them at install — never read from runtime config).
Key files:
init.sh— the installer (self-contained; creates users/folders/helpers, bakes things).install.sh— the thin bootstrap (download+verify+extract a release, then run init.sh).scripts/source/paths.sh— resolves the three roots + manager user.scripts/source/fetch.sh—lpFetchRelease/lpVersionGt(runtime fetch + version compare).scripts/release/make_release.sh— builds release artifacts.VERSION— the single source of the version number.
Run a development copy
From a clone of the repo, on a throwaway Debian/Ubuntu host (install is destructive — it creates system users and dirs):
Local mode (install from the working tree — best for hacking):
sudo ./init.sh --random-password --local init
# custom locations work in dev too:
sudo ./init.sh --random-password --local --system-dir=/srv/lp --manager-user=lpadmin init
Git mode (track a branch):
# via the bootstrap:
sudo ./install.sh --git-url=https://example.com/you/LibrePortal.git \
--git-user=USER --git-token=TOKEN
# or directly: ./init.sh init <password> <git_user> <git_token> <git_url> true git
Iterating: re-run the installer to redeploy after changes (local mode re-copies the tree). To wipe and start over:
sudo ./init.sh uninstall # removes the three roots + users + footprint
sudo ./init.sh --skip-docker-images uninstall # keep the Docker layer for a fast reinstall
This repo's CI/hook setup may auto-deploy on commit (commit on a branch → auto-merge → redeploy). That's environment-specific; the commands above are the portable way to stand up and refresh a dev box.
Cut a release (stable or edge)
- Bump the version in
VERSION(semver, e.g.0.2.0→0.3.0). Commit it. - Build the artifact (uses
git archive, so it ships only committed files and honours.gitattributes export-ignore—scripts/unused,site,docs,.claude, the release tooling, etc. never ship):
Produces, underscripts/release/make_release.sh stable # or: edge [git-ref]dist/<channel>/:libreportal-<version>.tar.gz— the releaselibreportal-<version>.tar.gz.sha256— its checksumlatest.json—{ version, channel, url, sha256, notes }(the channel pointer)
- Publish by copying
dist/<channel>/*to the host so they're served athttps://get.libreportal.org/<channel>/…(Phase E — thegetlibreportalcontainer; not built yet).latest.jsonis what makes a version "the latest".
Channels: stable is the default users get; edge is for early/testing
builds. Same tooling, different <channel>. To promote an edge build to stable,
rebuild with make_release.sh stable at that ref (or copy its artifacts into the
stable/ path and update stable/latest.json).
Test a release locally before publishing
No hosting needed — serve dist/ and point an install at it:
# build, then serve the artifacts
scripts/release/make_release.sh stable
( cd dist && python3 -m http.server 8000 )
# on a throwaway host, install from your local server:
LP_RELEASE_BASE_URL=http://<your-ip>:8000 \
sudo ./install.sh --channel=stable --system-dir=/libreportal-system
LP_RELEASE_BASE_URL overrides the release host everywhere (installer, updater,
recovery). Quick non-destructive checks:
# fetch + verify + stage only (no install):
LP_RELEASE_BASE_URL=http://<ip>:8000 ./install.sh --dry-run
# tamper with the tarball and confirm it's refused:
echo x >> dist/stable/libreportal-*.tar.gz
LP_RELEASE_BASE_URL=http://<ip>:8000 ./install.sh --dry-run # => CHECKSUM MISMATCH
How updates work (so you can reason about them)
In release mode the WebUI badge + libreportal update apply compare the local
VERSION against the channel's latest.json (lpVersionGt); if newer, they
lpFetchRelease the new tarball (verified) and redeploy. Because the install tree
is code only (configs/logs/backups live in the other roots), the update just
replaces it — no backup/restore dance. git/local modes keep their existing
git-based update path.
The footprint exception (important). update apply runs as the manager, and
the manager is deliberately forbidden from rewriting the root-owned footprint
(the helpers in /usr/local/lib/libreportal/, the CLI wrapper, the uninstall
launcher, the systemd unit, the sudoers) — that immutability is the de-sudo
security boundary. So a manager-run update can refresh code/apps/WebUI, but not
those. To track when an update touches them, init.sh carries a footprint_version
integer, baked at install into /usr/local/lib/libreportal/.footprint_version and
published in latest.json. When the channel's footprint_version exceeds the
installed one, the updater refuses the WebUI apply and the badge flags
footprint_update_needed — the user re-runs the installer as root
(curl … install.sh | sudo bash), which fetches and re-bakes the footprint
atomically. (Re-running the installer is idempotent.)
➡️ BUMP footprint_version in init.sh whenever you change anything in
scripts/system/*, the CLI wrapper, the uninstall launcher, the systemd unit, or
the sudoers. Forgetting it means those root components silently stay stale until
the next full reinstall.
Conventions
- Versioning: semver in
VERSION. Bump before building;latest.jsoncarries it. - New runtime script? Add it under
scripts/<area>/…and runscripts/source/files/generate_arrays.sh runso it's sourced (build/standalone tooling underscripts/releaseandscripts/systemis intentionally excluded). - Don't make the OS footprint (
/etc/*,/usr/local/*) relocatable — it's fixed by design for the privilege model.