Files
Classroom_Desktop/tools/build-release.sh
T
ekklesiaandClaude Opus 5 52301084a5 feat: an external-only display mode, chosen rather than inferred
The women's Bible study laptop is a fixed appliance: AC power, battery
out, lid shut, one television on HDMI. Its own panel should be off.

DISPLAY_MODE=external-only says so, and it is a setting rather than
something inferred from having a lid, because another laptop may want its
own screen or both. Nothing reads DEVICE_TYPE to decide it.

The built-in panel is identified by kscreen's output TYPE — 7 is Panel —
rather than by connector name. Matching "eDP-1" would be matching one
machine's spelling: eDP, LVDS and DSI are all internal, numbered
differently per driver and renamed between kernel versions. The names are
kept only as a fallback for a driver that reports no type, which some
virtual devices do.

One rule overrides the setting: never leave the machine with no screen. A
laptop asked for external-only whose television is off keeps its own
panel and says so in the journal. A machine nobody can see is one nobody
can fix standing in front of it, and it corrects itself the next time
this runs with the set on. Only the outputs that are not already right
are touched, so a television power cycle is not a stream of mode sets.

Applied at login from an autostart entry named to sort before the scale
one, so the scale lands on the displays this leaves enabled.

The lid itself needed no new code — logind ignores every lid event,
PowerDevil's lidAction is 0 in all three profiles with
triggerLidActionWhenExternalMonitorPresent=false, the profiles are
immutable and the classroom account cannot reach suspend through polkit.
That was all already true for towers. What was missing was a test saying
so, and there is one now, because it is the property the laptop depends
on and nothing would have noticed it regressing.

KNOWN LIMITATION, and it is written into docs/testing.md rather than left
to be discovered: the mode is applied at login, not continuously. If KWin
re-enables the internal panel when the television is power cycled, it
stays enabled until the next login. The lab has no lid and no HDMI
hotplug, so this cannot be settled here; the physical procedure says to
check it specifically, because the answer decides whether the follow-up
needs an event-driven applier like the audio policy's.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 21:17:06 -05:00

515 lines
17 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
if [[ $# -ne 1 ]]; then
echo "Usage: $0 VERSION"
exit 1
fi
VERSION="$1"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version: $VERSION"
echo "Expected MAJOR.MINOR.PATCH, for example 0.1.6"
exit 1
fi
STAGE="$ROOT/packaging/root"
OUTPUT="$ROOT/dist/ekklesia-classroom_${VERSION}_amd64.deb"
# Syntax check everything the package ships, by shebang, so this cannot
# drift out of date as classroom commands are added.
echo "Validating sources"
for script in "$ROOT/packaging/debian/postinst" "$ROOT"/scripts/* "$ROOT"/tests/*; do
[[ -f "$script" ]] || continue
case "$(head -n 1 "$script")" in
*python3*)
python3 -m py_compile "$script"
;;
*bash*|*bin/sh*)
bash -n "$script"
;;
esac
done
for module in "$ROOT"/lib/*.py; do
[[ -f "$module" ]] || continue
python3 -m py_compile "$module"
done
for policy in "$ROOT"/config/chrome/policies/*/*.json; do
[[ -f "$policy" ]] || continue
python3 -c 'import json, sys; json.load(open(sys.argv[1]))' "$policy"
done
find "$ROOT/scripts" "$ROOT/lib" "$ROOT/tests" \
-name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true
for test in "$ROOT"/tests/test-*.sh; do
[[ -x "$test" ]] || continue
echo "Running $(basename "$test")"
"$test" > /dev/null || {
echo "Tests failed. Run it directly to see why:"
echo " $test"
exit 1
}
done
echo "Building Ekklesia Classroom ${VERSION}"
# Always construct the package from clean canonical sources.
rm -rf "$STAGE"
mkdir -p "$STAGE/DEBIAN" "$ROOT/dist"
# Debian package metadata.
sed \
"s/^Version: @VERSION@$/Version: ${VERSION}/" \
"$ROOT/packaging/debian/control.template" \
> "$STAGE/DEBIAN/control"
install -m755 \
"$ROOT/packaging/debian/postinst" \
"$STAGE/DEBIAN/postinst"
# Classroom commands.
install -Dm755 \
"$ROOT/scripts/classroom-browser" \
"$STAGE/usr/local/bin/classroom-browser"
install -Dm755 \
"$ROOT/scripts/classroom-dvd" \
"$STAGE/usr/local/bin/classroom-dvd"
install -Dm755 \
"$ROOT/scripts/classroom-dvd-session" \
"$STAGE/usr/local/bin/classroom-dvd-session"
install -Dm755 \
"$ROOT/scripts/classroom-dvd-css-setup" \
"$STAGE/usr/local/bin/classroom-dvd-css-setup"
install -Dm755 \
"$ROOT/scripts/classroom-mqtt" \
"$STAGE/usr/local/bin/classroom-mqtt"
install -Dm755 \
"$ROOT/scripts/classroom-scale" \
"$STAGE/usr/local/bin/classroom-scale"
install -Dm755 \
"$ROOT/scripts/classroom-display-mode" \
"$STAGE/usr/local/bin/classroom-display-mode"
install -Dm755 \
"$ROOT/scripts/classroom-shutdown" \
"$STAGE/usr/local/bin/classroom-shutdown"
install -Dm755 \
"$ROOT/scripts/classroom-chrome-setup" \
"$STAGE/usr/local/bin/classroom-chrome-setup"
install -Dm755 \
"$ROOT/scripts/classroom-provision" \
"$STAGE/usr/local/bin/classroom-provision"
install -Dm755 \
"$ROOT/scripts/classroom-printer-setup" \
"$STAGE/usr/local/bin/classroom-printer-setup"
# An administrator's command. The church Wi-Fi has to be provisioned as a
# system connection or the classroom account can read its password.
install -Dm755 \
"$ROOT/scripts/classroom-wifi" \
"$STAGE/usr/local/bin/classroom-wifi"
# Shared helpers imported by the classroom commands.
install -Dm644 \
"$ROOT/lib/classroom_config.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_config.py"
install -Dm644 \
"$ROOT/lib/classroom_display.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_display.py"
install -Dm644 \
"$ROOT/lib/classroom_mqtt.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_mqtt.py"
install -Dm644 \
"$ROOT/lib/classroom_vlc.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_vlc.py"
install -Dm644 \
"$ROOT/config/wallpapers/classroom-calm.png" \
"$STAGE/usr/share/backgrounds/ekklesia/classroom-calm.png"
# Chrome enterprise policy: suppress first-run and sign-in nags, and pin
# Home to Wonder Ink. Nothing here restricts where teachers can browse.
install -Dm644 \
"$ROOT/config/chrome/policies/managed/ekklesia-classroom.json" \
"$STAGE/etc/opt/chrome/policies/managed/ekklesia-classroom.json"
install -Dm644 \
"$ROOT/config/chrome/policies/recommended/ekklesia-classroom.json" \
"$STAGE/etc/opt/chrome/policies/recommended/ekklesia-classroom.json"
install -Dm755 \
"$ROOT/scripts/ekklesia-classroom-update" \
"$STAGE/usr/local/bin/ekklesia-classroom-update"
# System configuration.
install -Dm644 \
"$ROOT/config/sddm/ekklesia-classroom.conf" \
"$STAGE/etc/sddm.conf.d/ekklesia-classroom.conf"
install -Dm644 \
"$ROOT/systemd/updater/ekklesia-classroom-update.service" \
"$STAGE/etc/systemd/system/ekklesia-classroom-update.service"
install -Dm644 \
"$ROOT/systemd/updater/ekklesia-classroom-update.timer" \
"$STAGE/etc/systemd/system/ekklesia-classroom-update.timer"
install -Dm644 \
"$ROOT/systemd/provision/ekklesia-classroom-provision.service" \
"$STAGE/etc/systemd/system/ekklesia-classroom-provision.service"
install -Dm644 \
"$ROOT/systemd/shutdown/ekklesia-classroom-shutdown.service" \
"$STAGE/etc/systemd/system/ekklesia-classroom-shutdown.service"
install -Dm644 \
"$ROOT/systemd/mqtt/ekklesia-classroom-mqtt.service" \
"$STAGE/etc/systemd/system/ekklesia-classroom-mqtt.service"
# The classroom desktop.
#
# Not a shell, and not a daemon: a Plasma session described in files Plasma
# already reads. The panel, the window behaviour, the notifications, the
# lockdown and the palette are all in here, which is why there is no longer
# a dock, a window helper, a notification wrapper or a compositor config in
# the list above.
# Not /etc/xdg: plasma-workspace already owns files by these names there.
# The classroom's directory goes in front of it in XDG_CONFIG_DIRS instead.
for setting in kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \
krunnerrc plasmanotifyrc powermanagementprofilesrc plasmarc \
plasmaparc; do
install -Dm644 \
"$ROOT/config/plasma/xdg/${setting}" \
"$STAGE/etc/ekklesia-classroom/xdg/${setting}"
done
# kdeglobals is composed rather than copied: the session policy plus the
# dark palette itself. Naming a colour scheme does not produce one, and a
# room whose user profile had never had a scheme applied came up light.
install -d "$STAGE/etc/ekklesia-classroom/xdg"
"$ROOT/tools/build-kdeglobals" > "$STAGE/etc/ekklesia-classroom/xdg/kdeglobals"
chmod 0644 "$STAGE/etc/ekklesia-classroom/xdg/kdeglobals"
install -Dm644 \
"$ROOT/config/environment/90-ekklesia-classroom.conf" \
"$STAGE/etc/environment.d/90-ekklesia-classroom.conf"
# The session shortcuts, and deliberately NOT in the xdg directory above.
#
# kglobalacceld reads ~/.config/kglobalshortcutsrc as a SimpleConfig: no
# cascade, no system directories. Measured — with this file in the
# cascade, kreadconfig6 reported the chord unbound while the shortcut
# server went on answering Ctrl+Alt+Del. classroom-desktop-reset copies
# this into the teacher's home before the shortcut server starts, the
# same way the panel is copied out of the package at every login.
install -Dm644 \
"$ROOT/config/plasma/kglobalshortcutsrc" \
"$STAGE/etc/ekklesia-classroom/kglobalshortcutsrc"
# The one that actually reaches a session started by a display manager.
# startplasma sources this directory; PAM does not read environment.d, so
# without this file none of the settings above are in the cascade at all
# and the room comes up as stock Plasma. Measured, on a real machine.
install -Dm644 \
"$ROOT/config/plasma/env/zz-ekklesia-classroom.sh" \
"$STAGE/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh"
# The power button does nothing. Two layers, because while a session is
# running PowerDevil holds logind's inhibitor and would overrule it.
install -Dm644 \
"$ROOT/config/systemd/logind-ekklesia-classroom.conf" \
"$STAGE/etc/systemd/logind.conf.d/ekklesia-classroom.conf"
for scheme in EkklesiaDark EkklesiaLight; do
install -Dm644 \
"$ROOT/config/plasma/colors/${scheme}.colors" \
"$STAGE/usr/share/color-schemes/${scheme}.colors"
done
LOOK="$STAGE/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop"
install -Dm644 \
"$ROOT/config/plasma/lookandfeel/metadata.json" \
"$LOOK/metadata.json"
install -Dm644 \
"$ROOT/config/plasma/lookandfeel/contents/defaults" \
"$LOOK/contents/defaults"
install -Dm644 \
"$ROOT/config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js" \
"$LOOK/contents/layouts/org.kde.plasma.desktop-layout.js"
# NO SHELL PACKAGE OF OUR OWN.
#
# Plasma 6.3 loads the volume display from the shell package and from
# nowhere else — the look and feel is not consulted, which was measured —
# so a shell package is the only supported way to replace it, and a
# classroom-sized one was built and tried. With it selected, plasmashell
# builds no panel at all: not the stock one, not ours. Measured twice, and
# the room's panel is deleted at every login on purpose, so every login
# would have come up with nothing on the screen.
#
# The volume display is Plasma's own instead. UI_SCALE makes it the right
# size on a television, which is what the room actually needed.
# The room's Plasma style: the card a notification and the volume display
# are drawn on. Only the pieces the classroom changes; everything else
# comes from Breeze, so this can never leave a popup undrawn.
STYLE="$STAGE/usr/share/plasma/desktoptheme/ekklesia"
install -Dm644 \
"$ROOT/config/plasma/desktoptheme/metadata.json" \
"$STYLE/metadata.json"
install -Dm644 \
"$ROOT/config/plasma/desktoptheme/dialogs/background.svg" \
"$STYLE/dialogs/background.svg"
for entry in "$ROOT"/config/plasma/applications/*.desktop; do
install -Dm644 "$entry" \
"$STAGE/usr/share/applications/$(basename "$entry")"
done
# Shadows Chrome's own entry. XDG_DATA_DIRS searches /usr/local/share first,
# so the panel matches a Chrome window to this launcher instead of to
# Chrome's, and the room gets one button rather than two.
for entry in "$ROOT"/config/plasma/applications-local/*.desktop; do
install -Dm644 "$entry" \
"$STAGE/usr/local/share/applications/$(basename "$entry")"
done
install -Dm644 \
"$ROOT/config/plasma/solid/ekklesia-dvd.desktop" \
"$STAGE/usr/share/solid/actions/ekklesia-dvd.desktop"
# Shadows the old notification daemon's D-Bus activation. Deactivating its
# systemd unit is not enough: D-Bus starts it on demand from a file
# mako-notifier owns, and the crash-recovery test on a migrated machine
# caught it doing exactly that while the panel was down.
install -Dm644 \
"$ROOT/config/dbus/fr.emersion.mako.service" \
"$STAGE/usr/local/share/dbus-1/services/fr.emersion.mako.service"
# Shadows Plasma's session screen. Ctrl+Alt+Delete asks D-Bus to start it;
# with this in front of plasma-workspace's own file there is nothing to
# start. See the file itself for why the unbound chord is not enough on
# its own.
install -Dm644 \
"$ROOT/config/dbus/org.kde.LogoutPrompt.service" \
"$STAGE/usr/local/share/dbus-1/services/org.kde.LogoutPrompt.service"
# Two marks the classroom draws itself, because nothing in Breeze says
# either thing: a lesson, and fetching a child's grown-up.
for icon in ekklesia-wonderink ekklesia-parent-call; do
install -Dm644 \
"$ROOT/config/icons/${icon}.svg" \
"$STAGE/usr/share/icons/hicolor/scalable/apps/${icon}.svg"
done
# The panel is the only way a teacher has of getting anywhere. Plasma gives
# up restarting it after three crashes in a minute; this does not.
install -Dm644 \
"$ROOT/systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf" \
"$STAGE/etc/systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf"
install -Dm644 \
"$ROOT/systemd/user/ekklesia-classroom-audio.service" \
"$STAGE/etc/systemd/user/ekklesia-classroom-audio.service"
install -Dm644 \
"$ROOT/systemd/user/ekklesia-classroom-desktop.service" \
"$STAGE/etc/systemd/user/ekklesia-classroom-desktop.service"
install -Dm755 \
"$ROOT/scripts/classroom-wonderink" \
"$STAGE/usr/local/bin/classroom-wonderink"
install -Dm755 \
"$ROOT/scripts/classroom-audio" \
"$STAGE/usr/local/bin/classroom-audio"
install -Dm755 \
"$ROOT/scripts/classroom-parent-call" \
"$STAGE/usr/local/bin/classroom-parent-call"
install -Dm755 \
"$ROOT/scripts/classroom-desktop-layout" \
"$STAGE/usr/local/bin/classroom-desktop-layout"
install -Dm755 \
"$ROOT/scripts/classroom-admin-terminal" \
"$STAGE/usr/local/bin/classroom-admin-terminal"
install -Dm755 \
"$ROOT/scripts/classroom-desktop-reset" \
"$STAGE/usr/local/bin/classroom-desktop-reset"
install -Dm755 \
"$ROOT/scripts/classroom-theme" \
"$STAGE/usr/local/bin/classroom-theme"
# Converges a machine that used to run something else. Called from
# postinst on every install, and by hand by a technician.
install -Dm755 \
"$ROOT/scripts/classroom-migrate" \
"$STAGE/usr/local/bin/classroom-migrate"
# One number, one place to ask for it.
install -Dm755 \
"$ROOT/scripts/classroom-ui-scale" \
"$STAGE/usr/local/bin/classroom-ui-scale"
# The room's display size: a hidden dialog behind a chord, a writer that
# asks for an administrator password, and the one thing that applies the
# answer at every login.
for command in classroom-display-settings classroom-set-scale; do
install -Dm755 \
"$ROOT/scripts/${command}" \
"$STAGE/usr/local/bin/${command}"
done
install -Dm644 \
"$ROOT/config/polkit/org.ekklesia.classroom.display.policy" \
"$STAGE/usr/share/polkit-1/actions/org.ekklesia.classroom.display.policy"
# The classroom user cannot suspend or hibernate the room. A vendor rule
# in /usr/share, not /etc, so a technician can override it by dropping a
# file of the same name into /etc/polkit-1/rules.d.
install -Dm644 \
"$ROOT/config/polkit/org.ekklesia.classroom.sleep.rules" \
"$STAGE/usr/share/polkit-1/rules.d/49-ekklesia-classroom-sleep.rules"
# The classroom user cannot be shown a saved Wi-Fi password. Same place
# and the same override route as the sleep rule above.
install -Dm644 \
"$ROOT/config/polkit/org.ekklesia.classroom.network.rules" \
"$STAGE/usr/share/polkit-1/rules.d/49-ekklesia-classroom-network.rules"
install -Dm644 \
"$ROOT/config/plasma/autostart/ekklesia-classroom-scale.desktop" \
"$STAGE/etc/xdg/autostart/ekklesia-classroom-scale.desktop"
# Which displays are on, decided before the scale is applied to them.
install -Dm644 \
"$ROOT/config/plasma/autostart/ekklesia-classroom-display.desktop" \
"$STAGE/etc/xdg/autostart/ekklesia-classroom-display.desktop"
# The panel's disc player follows this machine's hardware, and the
# detection has to have happened before a session starts.
install -Dm644 \
"$ROOT/systemd/layout/ekklesia-classroom-layout.service" \
"$STAGE/etc/systemd/system/ekklesia-classroom-layout.service"
install -Dm644 \
"$ROOT/config/udev/60-ekklesia-classroom-optical.rules" \
"$STAGE/etc/udev/rules.d/60-ekklesia-classroom-optical.rules"
install -Dm644 \
"$ROOT/lib/classroom_audio.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_audio.py"
install -Dm644 \
"$ROOT/lib/classroom_parent_call.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_parent_call.py"
# The notifier the MQTT service uses to put a church message on screen.
install -Dm755 \
"$ROOT/scripts/classroom-notify" \
"$STAGE/usr/local/bin/classroom-notify"
install -Dm755 \
"$ROOT/scripts/classroom-notify-test" \
"$STAGE/usr/local/bin/classroom-notify-test"
install -Dm644 \
"$ROOT/lib/classroom_optical.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_optical.py"
# The DVD overlay draws its own control bar as a layer-shell surface, so it
# still needs the icon and stylesheet helpers the dock used to share.
install -Dm644 \
"$ROOT/lib/classroom_icons.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_icons.py"
install -Dm644 \
"$ROOT/lib/classroom_style.py" \
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_style.py"
install -Dm644 \
"$ROOT/config/icons/dvd.svg" \
"$STAGE/etc/ekklesia-classroom/icons/dvd.svg"
install -Dm644 \
"$ROOT/config/overlay/style.css.in" \
"$STAGE/etc/ekklesia-classroom/overlay/style.css.in"
# DVD mode control bar.
for icon in media-play media-pause chapter-prev chapter-next \
disc-menu eject close; do
install -Dm644 \
"$ROOT/config/icons/${icon}.svg" \
"$STAGE/etc/ekklesia-classroom/icons/${icon}.svg"
done
printf '%s\n' "$VERSION" > "$ROOT/VERSION"
install -Dm644 \
"$ROOT/VERSION" \
"$STAGE/usr/local/lib/ekklesia-classroom/VERSION"
dpkg-deb \
--root-owner-group \
--build "$STAGE" "$OUTPUT"
echo
echo "Built:"
echo "$OUTPUT"
sha256sum "$OUTPUT"