diff --git a/docs/provisioning.md b/docs/provisioning.md new file mode 100644 index 0000000..55654c3 --- /dev/null +++ b/docs/provisioning.md @@ -0,0 +1,58 @@ +# Provisioning + +Some things a classroom needs cannot be installed by the classroom +package itself: they need the network, and they need the apt lock that +dpkg is holding for the whole of the package's `postinst`. + +`ekklesia-classroom-provision.service` runs after every boot and does +them. It is a `oneshot` unit that nothing else waits on, so it never +holds the classroom up, and **it is not allowed to fail the boot**: a +machine with no DVD codecs is still a working classroom. + +The package `postinst` enables the service but deliberately does not +start it. Every OTA update reboots the moment the install finishes, and +an apt run killed by that reboot would leave dpkg half-configured. The +next boot is a few seconds away and is a clean place to work. + +To provision without waiting for a reboot: + +``` +sudo classroom-provision +``` + +Each step checks whether it is already done and returns immediately if +so. A provisioned machine costs nothing; a machine that was offline the +first time gets another go at the next boot. + +``` +journalctl -u ekklesia-classroom-provision +``` + +## Google Chrome + +Chrome is not in Debian and a classroom is not usable without it, so a +fresh machine installs it itself rather than waiting for someone to do it +by hand on every box. + +`classroom-chrome-setup` adds Google's signing key to +`/etc/apt/keyrings/google-chrome.gpg` and their repository to +`/etc/apt/sources.list.d/google-chrome.list` — the same path Google's own +package manages, so the two cannot become two copies of the same +repository — then installs `google-chrome-stable`. Using the repository +rather than a one-off `.deb` means Chrome keeps getting security updates. + +Run it by hand with `sudo classroom-chrome-setup`. It returns +immediately if Chrome is already installed. + +The Chrome policies in `/etc/opt/chrome/policies/` are shipped by the +classroom package and apply as soon as Chrome exists. + +## DVD codecs + +Only for rooms with `DVD_ENABLED=true`. See [dvd.md](dvd.md). + +## What provisioning does not do + +It never upgrades or reconfigures anything already installed, and it +never touches a repository it did not add. Classroom software updates +are the OTA channel's job, not this. diff --git a/docs/testing.md b/docs/testing.md index 7aee7dc..2ee4e80 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -11,6 +11,14 @@ before promoting to stable with `./update_stable.sh`. - [ ] Dock appears, centred - [ ] Clock appears, bottom right +## Provisioning + +- [ ] `journalctl -u ekklesia-classroom-provision` shows a clean run +- [ ] Google Chrome is installed on a machine that never had it +- [ ] A second boot does not reinstall anything +- [ ] A machine with no network still boots to a working shell, and + provisioning succeeds at a later boot once it is online + ## UI scale - [ ] `UI_SCALE=1.0` looks exactly as it did before scaling existed @@ -114,7 +122,8 @@ With `DVD_ENABLED=true`: - [ ] VLC opens fullscreen, dark, with no menu bar, no orange cone and no sway title bar - [ ] The fullscreen controller still appears on mouse movement -- [ ] Commercial CSS disc, after `classroom-dvd-css-setup` +- [ ] Commercial CSS disc plays without anyone having run codec setup + by hand ## OTA diff --git a/packaging/debian/control.template b/packaging/debian/control.template index 3a81e89..f30e41b 100644 --- a/packaging/debian/control.template +++ b/packaging/debian/control.template @@ -4,7 +4,7 @@ Section: misc Priority: optional Architecture: amd64 Maintainer: Ekklesia -Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups, python3-gi, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, adwaita-icon-theme, libnotify-bin +Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups, python3-gi, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, adwaita-icon-theme, libnotify-bin, curl, gnupg, ca-certificates Recommends: vlc Description: Ekklesia managed classroom environment A minimal Wayland classroom environment for Ekklesia-managed church computers. diff --git a/packaging/debian/postinst b/packaging/debian/postinst index db8e168..29d8a02 100755 --- a/packaging/debian/postinst +++ b/packaging/debian/postinst @@ -23,6 +23,8 @@ chmod 0755 /usr/local/bin/classroom-dvd chmod 0755 /usr/local/bin/classroom-dvd-css-setup chmod 0755 /usr/local/bin/classroom-notify-test chmod 0755 /usr/local/bin/classroom-notifications +chmod 0755 /usr/local/bin/classroom-chrome-setup +chmod 0755 /usr/local/bin/classroom-provision # Reading the optical drive needs group cdrom. Takes effect at the next # login, which the OTA reboot provides. @@ -73,4 +75,17 @@ systemctl daemon-reload || true systemctl enable ekklesia-classroom-update.timer || true systemctl start ekklesia-classroom-update.timer || true +# Google Chrome, and the DVD codecs when the room has a drive, are +# fetched by the provisioning service at the next boot. +# +# Deliberately enabled but not started here. It cannot run during this +# script anyway, because dpkg holds the apt lock throughout. Starting it +# straight afterwards would race the reboot that an OTA update performs +# the moment this install finishes, and an apt run killed by that reboot +# leaves dpkg half-configured. Every OTA reboots, so waiting for the next +# boot costs nothing and cannot corrupt anything. +# +# A failure there must never fail this installation. +systemctl enable ekklesia-classroom-provision.service || true + exit 0 diff --git a/scripts/classroom-chrome-setup b/scripts/classroom-chrome-setup new file mode 100755 index 0000000..956cf11 --- /dev/null +++ b/scripts/classroom-chrome-setup @@ -0,0 +1,89 @@ +#!/bin/bash +set -uo pipefail + +# Installs Google Chrome from Google's own APT repository. +# +# Chrome is not in Debian, and a classroom is not usable without it, so +# a fresh machine provisions it itself rather than waiting for someone to +# install it by hand on every box. Using the repository rather than a +# one-off .deb means Chrome keeps getting security updates. +# +# sudo classroom-chrome-setup +# +# Idempotent: once Chrome is installed this returns immediately. + +KEYRING=/etc/apt/keyrings/google-chrome.gpg + +# The same path Google's own package manages, so the two cannot end up +# as two copies of the same repository. +LIST=/etc/apt/sources.list.d/google-chrome.list + +REPOSITORY="https://dl.google.com/linux/chrome/deb/" +KEY_URL="https://dl.google.com/linux/linux_signing_key.pub" + +log() { + echo "[classroom-chrome-setup] $1" +} + +if command -v google-chrome >/dev/null 2>&1; then + log "Google Chrome is already installed; nothing to do" + exit 0 +fi + +if [[ "$(id -u)" -ne 0 ]]; then + log "must be run as root: sudo classroom-chrome-setup" + exit 1 +fi + +install -d -m 0755 /etc/apt/keyrings + +if [[ ! -s "$KEYRING" ]]; then + log "fetching Google's signing key" + + if ! curl -fsS --retry 3 --max-time 60 "$KEY_URL" | + gpg --dearmor --batch --yes --output "$KEYRING.new"; then + rm -f "$KEYRING.new" + log "could not fetch Google's signing key; is this machine online?" + exit 1 + fi + + chmod 0644 "$KEYRING.new" + mv "$KEYRING.new" "$KEYRING" +fi + +# Google's own package migrates whatever we write here into a deb822 +# /etc/apt/sources.list.d/google-chrome.sources of its own, so look for +# the repository everywhere rather than only in the file we would write. +# Otherwise a machine that has had Chrome removed would end up with two +# copies of the same repository. +if ! grep -rqsF "dl.google.com/linux/chrome" \ + /etc/apt/sources.list /etc/apt/sources.list.d/; then + log "adding the Google Chrome repository" + printf 'deb [arch=amd64 signed-by=%s] %s stable main\n' \ + "$KEYRING" "$REPOSITORY" > "$LIST" + chmod 0644 "$LIST" +else + log "the Google Chrome repository is already configured" +fi + +export DEBIAN_FRONTEND=noninteractive + +# The OTA install or the update timer may hold the lock; wait it out +# rather than failing the whole provisioning run. +APT=(apt-get -o DPkg::Lock::Timeout=600) + +log "updating package lists" + +if ! "${APT[@]}" update; then + log "apt-get update failed; is this machine online?" + exit 1 +fi + +log "installing google-chrome-stable" + +if ! "${APT[@]}" install -y google-chrome-stable; then + log "could not install Google Chrome" + exit 1 +fi + +log "Google Chrome installed" diff --git a/scripts/classroom-dvd-css-setup b/scripts/classroom-dvd-css-setup index d81e141..6000e76 100755 --- a/scripts/classroom-dvd-css-setup +++ b/scripts/classroom-dvd-css-setup @@ -1,61 +1,413 @@ -#!/bin/bash -set -euo pipefail +#!/usr/bin/env python3 -# One-time bootstrap for encrypted (commercial) DVD-Video playback. -# -# Most retail DVDs are CSS encrypted. Debian cannot ship the decryption -# library, so libdvdcss has to be built on the machine from the contrib -# package libdvd-pkg. This is deliberately NOT run from the package -# postinst: it needs the network, it compiles a package, and it must not -# run while dpkg already holds the apt lock during an OTA update. -# -# Run this once, as root, on any classroom that needs commercial discs: -# -# sudo classroom-dvd-css-setup -# -# Unencrypted discs (church curriculum, home-burned video) play without it. +"""Enable encrypted (commercial) DVD-Video playback. -if [[ "$(id -u)" -ne 0 ]]; then - echo "classroom-dvd-css-setup must be run as root." >&2 - exit 1 -fi +Most retail discs are CSS encrypted. Debian cannot ship the decryption +library, so libdvdcss has to be built on the machine from the contrib +package libdvd-pkg. That needs the contrib component enabled, which this +does for either APT source format: -if [[ -e /usr/lib/x86_64-linux-gnu/libdvdcss.so.2 ]]; then - echo "libdvdcss is already installed. Nothing to do." - exit 0 -fi + classic /etc/apt/sources.list and /etc/apt/sources.list.d/*.list + deb http://deb.debian.org/debian/ trixie main non-free-firmware -echo "Enabling the Debian contrib component..." + deb822 /etc/apt/sources.list.d/*.sources + Components: main non-free-firmware -if ! grep -rqs 'contrib' /etc/apt/sources.list /etc/apt/sources.list.d/; then - if [[ -f /etc/apt/sources.list.d/debian.sources ]]; then - sed -i 's/^\(Components:.*\)$/\1 contrib/' \ - /etc/apt/sources.list.d/debian.sources - else - echo "Could not find a Debian sources file to add contrib to." >&2 - echo "Add 'contrib' manually and re-run this script." >&2 - exit 1 - fi -fi +Only existing Debian entries are edited, and only by adding the missing +component, so running this repeatedly cannot produce a duplicate +repository or a duplicate component. -echo "Installing libdvd-pkg..." + sudo classroom-dvd-css-setup # set this machine up now + classroom-dvd-css-setup --status # report without changing anything -export DEBIAN_FRONTEND=noninteractive +Unencrypted discs — church curriculum, home-burned video — play without +any of this. +""" -apt-get update -apt-get install -y libdvd-pkg +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path -echo "Building libdvdcss..." +sys.path.insert(0, "/usr/local/lib/ekklesia-classroom") +sys.path.insert( + 0, + os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib"), +) -# libdvd-pkg downloads and builds libdvdcss during reconfigure. -dpkg-reconfigure -f noninteractive libdvd-pkg +from classroom_config import enabled, read_device_config # noqa: E402 -if [[ -e /usr/lib/x86_64-linux-gnu/libdvdcss.so.2 ]]; then - echo - echo "libdvdcss installed. Commercial DVDs should now play." - echo "Test with an actual disc before relying on it." -else - echo - echo "libdvdcss was not installed. Commercial discs will not play." >&2 - exit 1 -fi +# Tests point this at a fixture tree instead of the live system. +APT_ROOT = Path(os.environ.get("CLASSROOM_APT_ROOT", "/")) + +COMPONENT = "contrib" + +# A Debian entry is one served from debian.org or from a path with a +# /debian segment, which covers local mirrors. Deliberately narrow: the +# Google Chrome repository must never be touched. +DEBIAN_URI = re.compile( + r"^\w+://[^/]*debian\.org(/|$)|^\w+://[^/]*/[^ ]*\bdebian\b", + re.IGNORECASE, +) + +CLASSIC_LINE = re.compile( + r"^(?P\s*)" + r"(?Pdeb|deb-src)" + r"(?P\s+\[[^\]]*\])?" + r"(?P\s+)" + r"(?P[^#]*?)" + r"(?P\s*#.*)?$" +) + + +def log(message): + print(f"[classroom-dvd-css-setup] {message}", flush=True) + + +def sources_list(): + return APT_ROOT / "etc/apt/sources.list" + + +def sources_directory(): + return APT_ROOT / "etc/apt/sources.list.d" + + +def is_debian(uri): + return bool(DEBIAN_URI.match(uri)) + + +def rewrite_classic(text): + """Add contrib to Debian entries in a classic sources.list. + + Returns the new text, whether anything changed, and whether a Debian + entry already carried the component. + """ + lines = text.splitlines(keepends=True) + changed = False + present = False + + for index, line in enumerate(lines): + ending = "\n" if line.endswith("\n") else "" + match = CLASSIC_LINE.match(line.rstrip("\n")) + + if not match: + continue + + fields = match.group("body").split() + + # uri, suite, then one or more components. A flat repository + # (no components) has nothing to add. + if len(fields) < 3: + continue + + uri = fields[0] + components = fields[2:] + + if not is_debian(uri): + continue + + if COMPONENT in components: + present = True + continue + + if "main" not in components: + continue + + components.append(COMPONENT) + + lines[index] = "".join( + [ + match.group("indent"), + match.group("type"), + match.group("options") or "", + match.group("gap"), + " ".join([uri, fields[1]] + components), + match.group("comment") or "", + ending, + ] + ) + changed = True + + return "".join(lines), changed, present + + +def rewrite_deb822(text): + """Add contrib to the Components of Debian stanzas in a .sources file. + + Returns the new text, whether anything changed, and whether a Debian + stanza already carried the component. + """ + lines = text.splitlines(keepends=True) + changed = False + present = False + + # Stanza boundaries are blank lines. + stanza_start = 0 + stanzas = [] + + for index, line in enumerate(lines + ["\n"]): + if line.strip(): + continue + + if index > stanza_start: + stanzas.append((stanza_start, index)) + + stanza_start = index + 1 + + for start, end in stanzas: + uris = None + components_index = None + + for index in range(start, end): + field, separator, value = lines[index].partition(":") + + if not separator: + continue + + key = field.strip().lower() + + if key == "uris": + uris = value.split() + elif key == "components": + components_index = index + + if components_index is None or not uris: + continue + + if not any(is_debian(uri) for uri in uris): + continue + + field, _, value = lines[components_index].partition(":") + components = value.split() + + if COMPONENT in components: + present = True + continue + + if "main" not in components: + continue + + components.append(COMPONENT) + + ending = "\n" if lines[components_index].endswith("\n") else "" + lines[components_index] = f"{field}: {' '.join(components)}{ending}" + changed = True + + return "".join(lines), changed, present + + +def source_files(): + """Every APT source file, newest format first.""" + found = [] + + directory = sources_directory() + + if directory.is_dir(): + found.extend(sorted(directory.glob("*.sources"))) + found.extend(sorted(directory.glob("*.list"))) + + if sources_list().is_file(): + found.append(sources_list()) + + return found + + +def enable_contrib(): + """Enable contrib in the existing Debian sources. Idempotent.""" + touched = [] + + for path in source_files(): + try: + original = path.read_text() + except OSError as error: + log(f"skipping {path}: {error}") + continue + + if path.suffix == ".sources": + updated, changed, _ = rewrite_deb822(original) + else: + updated, changed, _ = rewrite_classic(original) + + if not changed: + continue + + path.write_text(updated) + touched.append(path) + + if touched: + for path in touched: + log(f"enabled {COMPONENT} in {path}") + else: + log(f"{COMPONENT} was already enabled") + + return touched + + +def contrib_available(): + """True when some Debian entry already carries the contrib component.""" + for path in source_files(): + try: + text = path.read_text() + except OSError: + continue + + if path.suffix == ".sources": + _, _, present = rewrite_deb822(text) + else: + _, _, present = rewrite_classic(text) + + if present: + return True + + return False + + +def libdvdcss_installed(): + # ldconfig lives in /sbin, which is not on an ordinary user's PATH. + for candidate in ("/sbin/ldconfig", "/usr/sbin/ldconfig", "ldconfig"): + try: + result = subprocess.run( + [candidate, "-p"], + capture_output=True, + text=True, + ) + except OSError: + continue + + return "libdvdcss.so.2" in result.stdout + + return any( + Path(directory, "libdvdcss.so.2").exists() + for directory in ("/usr/lib/x86_64-linux-gnu", "/usr/lib", "/lib") + ) + + +def apt(*arguments): + environment = dict(os.environ) + environment["DEBIAN_FRONTEND"] = "noninteractive" + + return subprocess.run( + [ + "apt-get", + # An OTA install or the update timer may hold the lock. + # Waiting is far better than failing the provisioning run. + "-o", + "DPkg::Lock::Timeout=600", + *arguments, + ], + env=environment, + ).returncode + + +def preseed(): + """Answer libdvd-pkg's debconf questions so nothing blocks on a prompt.""" + answers = "\n".join( + [ + "libdvd-pkg libdvd-pkg/build boolean true", + "libdvd-pkg libdvd-pkg/post-invoke_hook-install boolean true", + "libdvd-pkg libdvd-pkg/post-invoke_hook-remove boolean true", + ] + ) + + subprocess.run( + ["debconf-set-selections"], + input=answers + "\n", + text=True, + ) + + +def install(): + log("updating package lists") + + if apt("update") != 0: + log("apt-get update failed; is this machine online?") + return 1 + + preseed() + + log("installing libdvd-pkg") + + if apt("install", "-y", "libdvd-pkg") != 0: + log("could not install libdvd-pkg") + return 1 + + log("building libdvdcss") + + environment = dict(os.environ) + environment["DEBIAN_FRONTEND"] = "noninteractive" + + subprocess.run( + ["dpkg-reconfigure", "-f", "noninteractive", "libdvd-pkg"], + env=environment, + ) + + if not libdvdcss_installed(): + log("libdvdcss was not built; commercial discs will not play") + return 1 + + log("libdvdcss installed; commercial discs should now play") + return 0 + + +def status(): + print(f"contrib enabled: {contrib_available()}") + print(f"libdvdcss: {'installed' if libdvdcss_installed() else 'missing'}") + + config = read_device_config() + print(f"DVD_ENABLED: {enabled(config, 'DVD_ENABLED', False)}") + + return 0 if libdvdcss_installed() else 1 + + +def main(): + parser = argparse.ArgumentParser( + description="Enable commercial DVD playback on this classroom.", + ) + parser.add_argument( + "--status", + action="store_true", + help="Report the current state without changing anything", + ) + parser.add_argument( + "--enable-contrib-only", + action="store_true", + help="Only enable the contrib component, install nothing", + ) + parser.add_argument( + "--if-enabled", + action="store_true", + help="Do nothing unless DVD_ENABLED is set for this classroom", + ) + arguments = parser.parse_args() + + if arguments.status: + return status() + + if arguments.if_enabled: + if not enabled(read_device_config(), "DVD_ENABLED", False): + log("DVD support is not enabled on this classroom; nothing to do") + return 0 + + # Only the real system needs privilege. Pointed at a fixture tree + # through CLASSROOM_APT_ROOT, this is just a file rewrite. + if APT_ROOT == Path("/") and os.geteuid() != 0: + log("must be run as root: sudo classroom-dvd-css-setup") + return 1 + + if arguments.enable_contrib_only: + enable_contrib() + return 0 + + if libdvdcss_installed(): + log("libdvdcss is already installed; nothing to do") + return 0 + + enable_contrib() + + return install() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/classroom-provision b/scripts/classroom-provision new file mode 100755 index 0000000..6dd8986 --- /dev/null +++ b/scripts/classroom-provision @@ -0,0 +1,41 @@ +#!/bin/bash +set -uo pipefail + +# Finishes setting a classroom machine up: the things that need the +# network and the apt lock, and so cannot run from the package postinst. +# +# Run from ekklesia-classroom-provision.service at every boot. Each step +# checks whether it is already done and returns immediately if so, so a +# provisioned machine costs nothing, and a machine that was offline the +# first time gets another go at the next boot. +# +# Nothing here is allowed to fail the boot. A classroom with no DVD +# codecs is still a working classroom. + +log() { + echo "[classroom-provision] $1" +} + +log "starting" + +if /usr/local/bin/classroom-chrome-setup; then + : +else + log "WARNING: Google Chrome is not installed. The Browser button will" + log "WARNING: not work until this succeeds. Retrying at the next boot," + log "WARNING: or run: sudo classroom-chrome-setup" +fi + +# DVD codecs are only fetched for rooms that actually have a drive. +if /usr/local/bin/classroom-dvd-css-setup --if-enabled; then + : +else + log "WARNING: commercial DVD support is not set up. Unencrypted discs" + log "WARNING: still play. Retrying at the next boot, or run:" + log "WARNING: sudo classroom-dvd-css-setup" +fi + +log "finished" + +# Provisioning is best-effort by design. +exit 0 diff --git a/systemd/provision/ekklesia-classroom-provision.service b/systemd/provision/ekklesia-classroom-provision.service new file mode 100644 index 0000000..78ea0b7 --- /dev/null +++ b/systemd/provision/ekklesia-classroom-provision.service @@ -0,0 +1,15 @@ +[Unit] +Description=Ekklesia Classroom provisioning +Documentation=file:///usr/local/bin/classroom-provision +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/classroom-provision +# Building libdvdcss from source takes a while on classroom hardware. +TimeoutStartSec=1800 +RemainAfterExit=no + +[Install] +WantedBy=multi-user.target diff --git a/tests/test-classroom-dvd-css-setup.sh b/tests/test-classroom-dvd-css-setup.sh new file mode 100755 index 0000000..650bb5c --- /dev/null +++ b/tests/test-classroom-dvd-css-setup.sh @@ -0,0 +1,215 @@ +#!/bin/bash +set -uo pipefail + +# Tests contrib enabling against fixture APT trees: classic sources.list, +# deb822 .sources, contrib already present, and repeated runs. +# +# tests/test-classroom-dvd-css-setup.sh + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPT="$ROOT/scripts/classroom-dvd-css-setup" + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +passed=0 +failed=0 + +check() { + local label="$1" expected="$2" actual="$3" + + if [[ "$actual" == "$expected" ]]; then + echo " ok $label" + passed=$((passed + 1)) + else + echo " FAIL $label" + echo " expected: $expected" + echo " got: $actual" + failed=$((failed + 1)) + fi +} + +# A fixture APT tree. Prints the root. +fixture() { + local name="$1" + local root="$WORK/$name" + + rm -rf "$root" + mkdir -p "$root/etc/apt/sources.list.d" + + echo "$root" +} + +enable_contrib() { + CLASSROOM_APT_ROOT="$1" python3 "$SCRIPT" --enable-contrib-only 2>&1 +} + +# ------------------------------------------------- 1. classic sources.list + +echo "1. classic /etc/apt/sources.list" + +root="$(fixture classic)" +cat > "$root/etc/apt/sources.list" <<'LIST' +# Debian Trixie +deb http://deb.debian.org/debian/ trixie main non-free-firmware +deb-src http://deb.debian.org/debian/ trixie main non-free-firmware + +deb http://security.debian.org/debian-security trixie-security main non-free-firmware +deb http://deb.debian.org/debian/ trixie-updates main non-free-firmware +LIST + +enable_contrib "$root" > /dev/null + +check "binary entry gains contrib" \ + "deb http://deb.debian.org/debian/ trixie main non-free-firmware contrib" \ + "$(sed -n '2p' "$root/etc/apt/sources.list")" +check "source entry gains contrib" \ + "deb-src http://deb.debian.org/debian/ trixie main non-free-firmware contrib" \ + "$(sed -n '3p' "$root/etc/apt/sources.list")" +check "security entry gains contrib" \ + "deb http://security.debian.org/debian-security trixie-security main non-free-firmware contrib" \ + "$(sed -n '5p' "$root/etc/apt/sources.list")" +check "comment line untouched" "# Debian Trixie" "$(sed -n '1p' "$root/etc/apt/sources.list")" +check "line count unchanged" "6" "$(wc -l < "$root/etc/apt/sources.list")" +check "contrib appears once per line" "0" \ + "$(grep -c 'contrib.*contrib' "$root/etc/apt/sources.list")" + +# ------------------------------------------------------------ 2. deb822 + +echo "2. deb822 .sources" + +root="$(fixture deb822)" +cat > "$root/etc/apt/sources.list.d/debian.sources" <<'SOURCES' +Types: deb deb-src +URIs: http://deb.debian.org/debian/ +Suites: trixie trixie-updates +Components: main non-free-firmware +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg + +Types: deb +URIs: http://security.debian.org/debian-security/ +Suites: trixie-security +Components: main non-free-firmware +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg +SOURCES + +enable_contrib "$root" > /dev/null + +check "first stanza gains contrib" \ + "Components: main non-free-firmware contrib" \ + "$(sed -n '4p' "$root/etc/apt/sources.list.d/debian.sources")" +check "second stanza gains contrib" \ + "Components: main non-free-firmware contrib" \ + "$(sed -n '10p' "$root/etc/apt/sources.list.d/debian.sources")" +check "other fields untouched" \ + "URIs: http://deb.debian.org/debian/" \ + "$(sed -n '2p' "$root/etc/apt/sources.list.d/debian.sources")" +check "stanza separator kept" "" \ + "$(sed -n '6p' "$root/etc/apt/sources.list.d/debian.sources")" + +# ------------------------------------------------ 3. contrib already there + +echo "3. contrib already enabled" + +root="$(fixture already)" +cat > "$root/etc/apt/sources.list" <<'LIST' +deb http://deb.debian.org/debian/ trixie main contrib non-free-firmware +LIST +cat > "$root/etc/apt/sources.list.d/extra.sources" <<'SOURCES' +Types: deb +URIs: http://deb.debian.org/debian/ +Suites: trixie +Components: main contrib +SOURCES + +before_list="$(cat "$root/etc/apt/sources.list")" +before_sources="$(cat "$root/etc/apt/sources.list.d/extra.sources")" + +out="$(enable_contrib "$root")" + +check "reports nothing to do" "[classroom-dvd-css-setup] contrib was already enabled" "$out" +check "sources.list byte-identical" "$before_list" "$(cat "$root/etc/apt/sources.list")" +check ".sources byte-identical" "$before_sources" "$(cat "$root/etc/apt/sources.list.d/extra.sources")" + +# ------------------------------------------------------- 4. repeated runs + +echo "4. repeated runs are idempotent" + +root="$(fixture repeat)" +cat > "$root/etc/apt/sources.list" <<'LIST' +deb http://deb.debian.org/debian/ trixie main non-free-firmware +LIST +cat > "$root/etc/apt/sources.list.d/debian.sources" <<'SOURCES' +Types: deb +URIs: http://deb.debian.org/debian/ +Suites: trixie +Components: main +SOURCES + +enable_contrib "$root" > /dev/null +first_list="$(cat "$root/etc/apt/sources.list")" +first_sources="$(cat "$root/etc/apt/sources.list.d/debian.sources")" + +for _ in 1 2 3; do + enable_contrib "$root" > /dev/null +done + +check "sources.list unchanged after 4 runs" "$first_list" "$(cat "$root/etc/apt/sources.list")" +check ".sources unchanged after 4 runs" "$first_sources" "$(cat "$root/etc/apt/sources.list.d/debian.sources")" +check "exactly one contrib in sources.list" "1" \ + "$(grep -o 'contrib' "$root/etc/apt/sources.list" | wc -l)" +check "exactly one contrib in .sources" "1" \ + "$(grep -o 'contrib' "$root/etc/apt/sources.list.d/debian.sources" | wc -l)" +check "no repository was added" "1" "$(wc -l < "$root/etc/apt/sources.list")" + +# ------------------------------------------- 5. third-party repos untouched + +echo "5. third-party repositories are left alone" + +root="$(fixture thirdparty)" +cat > "$root/etc/apt/sources.list" <<'LIST' +deb http://deb.debian.org/debian/ trixie main +LIST +chrome='deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main' +echo "$chrome" > "$root/etc/apt/sources.list.d/google-chrome.list" + +enable_contrib "$root" > /dev/null + +check "chrome repository untouched" "$chrome" \ + "$(cat "$root/etc/apt/sources.list.d/google-chrome.list")" +check "debian entry still updated" \ + "deb http://deb.debian.org/debian/ trixie main contrib" \ + "$(cat "$root/etc/apt/sources.list")" + +# ---------------------------------------------------- 6. awkward real lines + +echo "6. options, comments and flat repositories" + +root="$(fixture awkward)" +cat > "$root/etc/apt/sources.list" <<'LIST' +deb [arch=amd64 signed-by=/usr/share/keyrings/debian-archive-keyring.gpg] http://deb.debian.org/debian/ trixie main + deb http://deb.debian.org/debian/ trixie main # indented, with a comment +deb http://deb.debian.org/debian/ ./ +# deb http://deb.debian.org/debian/ trixie main +LIST + +enable_contrib "$root" > /dev/null + +check "bracketed options preserved" \ + "deb [arch=amd64 signed-by=/usr/share/keyrings/debian-archive-keyring.gpg] http://deb.debian.org/debian/ trixie main contrib" \ + "$(sed -n '1p' "$root/etc/apt/sources.list")" +check "indent and trailing comment preserved" \ + " deb http://deb.debian.org/debian/ trixie main contrib # indented, with a comment" \ + "$(sed -n '2p' "$root/etc/apt/sources.list")" +check "flat repository untouched" \ + "deb http://deb.debian.org/debian/ ./" \ + "$(sed -n '3p' "$root/etc/apt/sources.list")" +check "commented-out entry untouched" \ + "# deb http://deb.debian.org/debian/ trixie main" \ + "$(sed -n '4p' "$root/etc/apt/sources.list")" + +# ------------------------------------------------------------- summary + +echo +echo "passed: $passed failed: $failed" +[[ "$failed" -eq 0 ]] diff --git a/tools/build-release.sh b/tools/build-release.sh index 4c231a1..6c7baf1 100755 --- a/tools/build-release.sh +++ b/tools/build-release.sh @@ -117,6 +117,14 @@ install -Dm755 \ "$ROOT/scripts/classroom-notifications" \ "$STAGE/usr/local/bin/classroom-notifications" +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" + # Shared helpers imported by the classroom commands. install -Dm644 \ "$ROOT/lib/classroom_config.py" \ @@ -171,6 +179,10 @@ 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/config/icons/dvd.svg" \ "$STAGE/etc/ekklesia-classroom/icons/dvd.svg"