chore: checkpoint classroom 0.1.5 stable
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
dist/
|
dist/
|
||||||
|
repo/
|
||||||
packaging/root/
|
packaging/root/
|
||||||
*.deb
|
*.deb
|
||||||
|
|||||||
+1
-4
@@ -7,10 +7,7 @@ set $mod Mod4
|
|||||||
output * bg #202020 solid_color
|
output * bg #202020 solid_color
|
||||||
|
|
||||||
# Waybar
|
# Waybar
|
||||||
exec_always --no-startup-id pkill waybar
|
exec --no-startup-id waybar -c /etc/ekklesia-classroom/waybar/config.jsonc -s /etc/ekklesia-classroom/waybar/style.css
|
||||||
exec_always --no-startup-id waybar \
|
|
||||||
-c /etc/ekklesia-classroom/waybar/config.jsonc \
|
|
||||||
-s /etc/ekklesia-classroom/waybar/style.css
|
|
||||||
|
|
||||||
# Keep the interface intentionally minimal.
|
# Keep the interface intentionally minimal.
|
||||||
# Additional classroom controls are added explicitly by Ekklesia Classroom.
|
# Additional classroom controls are added explicitly by Ekklesia Classroom.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Package: ekklesia-classroom
|
||||||
|
Version: @VERSION@
|
||||||
|
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
|
||||||
|
Description: Ekklesia managed classroom environment
|
||||||
|
A minimal Wayland classroom environment for Ekklesia-managed church computers.
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if ! id classroom >/dev/null 2>&1; then
|
||||||
|
adduser \
|
||||||
|
--disabled-password \
|
||||||
|
--gecos "" \
|
||||||
|
classroom
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > /home/classroom/.bash_profile <<'PROFILE'
|
||||||
|
if [ -z "$WAYLAND_DISPLAY" ] && [ "${XDG_VTNR:-}" = "1" ]; then
|
||||||
|
exec sway -c /etc/ekklesia-classroom/sway/config
|
||||||
|
fi
|
||||||
|
PROFILE
|
||||||
|
|
||||||
|
chown classroom:classroom /home/classroom/.bash_profile
|
||||||
|
chmod 0644 /home/classroom/.bash_profile
|
||||||
|
|
||||||
|
chmod 0755 /usr/local/bin/classroom-browser
|
||||||
|
|
||||||
|
if [ ! -f /etc/ekklesia-classroom/device.conf ]; then
|
||||||
|
cat > /etc/ekklesia-classroom/device.conf <<'CONFIG'
|
||||||
|
# Ekklesia Classroom device configuration
|
||||||
|
|
||||||
|
UPDATE_URL=https://updates.riverofglorymn.org
|
||||||
|
CHANNEL=stable
|
||||||
|
CONFIG
|
||||||
|
|
||||||
|
chmod 0644 /etc/ekklesia-classroom/device.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
|
||||||
|
systemctl enable ekklesia-classroom-update.timer || true
|
||||||
|
systemctl start ekklesia-classroom-update.timer || true
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+210
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import fcntl
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
CONFIG = "/etc/ekklesia-classroom/device.conf"
|
||||||
|
LOCKFILE = "/run/ekklesia-classroom-update.lock"
|
||||||
|
|
||||||
|
|
||||||
|
def log(message):
|
||||||
|
print(f"[ekklesia-classroom-update] {message}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def read_config():
|
||||||
|
config = {
|
||||||
|
"UPDATE_URL": "https://updates.riverofglorymn.org",
|
||||||
|
"CHANNEL": "stable",
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.path.exists(CONFIG):
|
||||||
|
with open(CONFIG, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
config[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def installed_version():
|
||||||
|
result = subprocess.run(
|
||||||
|
["dpkg-query", "-W", "-f=${Version}", "ekklesia-classroom"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return "0.0.0"
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def version_is_newer(candidate, current):
|
||||||
|
result = subprocess.run(
|
||||||
|
["dpkg", "--compare-versions", candidate, "gt", current]
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_json(url):
|
||||||
|
request = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
headers={"User-Agent": "Ekklesia-Classroom-Updater/0.1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
return json.load(response)
|
||||||
|
|
||||||
|
|
||||||
|
def download(url, destination):
|
||||||
|
request = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
headers={"User-Agent": "Ekklesia-Classroom-Updater/0.1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
|
with open(destination, "wb") as f:
|
||||||
|
while True:
|
||||||
|
chunk = response.read(1024 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
|
||||||
|
def sha256(path):
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
while True:
|
||||||
|
chunk = f.read(1024 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
digest.update(chunk)
|
||||||
|
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-reboot",
|
||||||
|
action="store_true",
|
||||||
|
help="Install the update but do not reboot automatically",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--check",
|
||||||
|
action="store_true",
|
||||||
|
help="Check for an update without installing it",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
with open(LOCKFILE, "w", encoding="utf-8") as lock:
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
except BlockingIOError:
|
||||||
|
log("Another update is already running.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
config = read_config()
|
||||||
|
|
||||||
|
base_url = config["UPDATE_URL"].rstrip("/") + "/"
|
||||||
|
channel = config["CHANNEL"]
|
||||||
|
manifest_url = urljoin(base_url, f"{channel}/manifest.json")
|
||||||
|
|
||||||
|
current = installed_version()
|
||||||
|
|
||||||
|
log(f"Current version: {current}")
|
||||||
|
log(f"Channel: {channel}")
|
||||||
|
log(f"Checking: {manifest_url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest = fetch_json(manifest_url)
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"Unable to fetch update manifest: {exc}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
candidate = manifest["version"]
|
||||||
|
|
||||||
|
log(f"Available version: {candidate}")
|
||||||
|
|
||||||
|
if not version_is_newer(candidate, current):
|
||||||
|
log("Already up to date.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.check:
|
||||||
|
log(f"Update available: {current} -> {candidate}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
package_url = urljoin(base_url, manifest["package"].lstrip("/"))
|
||||||
|
expected_hash = manifest["sha256"].lower()
|
||||||
|
|
||||||
|
os.makedirs("/var/cache/ekklesia-classroom", exist_ok=True)
|
||||||
|
|
||||||
|
package_path = os.path.join(
|
||||||
|
"/var/cache/ekklesia-classroom",
|
||||||
|
os.path.basename(manifest["package"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
temporary_path = package_path + ".download"
|
||||||
|
|
||||||
|
log(f"Downloading {package_url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
download(package_url, temporary_path)
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"Download failed: {exc}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
actual_hash = sha256(temporary_path)
|
||||||
|
|
||||||
|
if actual_hash != expected_hash:
|
||||||
|
log("SHA-256 verification FAILED.")
|
||||||
|
log(f"Expected: {expected_hash}")
|
||||||
|
log(f"Received: {actual_hash}")
|
||||||
|
os.unlink(temporary_path)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
log("SHA-256 verification passed.")
|
||||||
|
|
||||||
|
os.replace(temporary_path, package_path)
|
||||||
|
|
||||||
|
log(f"Installing {candidate}")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"apt-get",
|
||||||
|
"install",
|
||||||
|
"-y",
|
||||||
|
package_path,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
log("Package installation failed.")
|
||||||
|
return result.returncode
|
||||||
|
|
||||||
|
log(f"Update to {candidate} installed successfully.")
|
||||||
|
|
||||||
|
if manifest.get("requires_reboot", False):
|
||||||
|
if args.no_reboot:
|
||||||
|
log("Update requires reboot, but --no-reboot was specified.")
|
||||||
|
else:
|
||||||
|
log("Update requires reboot. Rebooting now.")
|
||||||
|
subprocess.run(["systemctl", "reboot"])
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Ekklesia Classroom Update
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/ekklesia-classroom-update
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Nightly Ekklesia Classroom Update Check
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 03:00:00
|
||||||
|
RandomizedDelaySec=30m
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Executable
+83
@@ -0,0 +1,83 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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 UI configuration.
|
||||||
|
install -Dm644 \
|
||||||
|
"$ROOT/config/sway/config" \
|
||||||
|
"$STAGE/etc/ekklesia-classroom/sway/config"
|
||||||
|
|
||||||
|
install -Dm644 \
|
||||||
|
"$ROOT/config/waybar/config.jsonc" \
|
||||||
|
"$STAGE/etc/ekklesia-classroom/waybar/config.jsonc"
|
||||||
|
|
||||||
|
install -Dm644 \
|
||||||
|
"$ROOT/config/waybar/style.css" \
|
||||||
|
"$STAGE/etc/ekklesia-classroom/waybar/style.css"
|
||||||
|
|
||||||
|
# Classroom commands.
|
||||||
|
install -Dm755 \
|
||||||
|
"$ROOT/scripts/classroom-browser" \
|
||||||
|
"$STAGE/usr/local/bin/classroom-browser"
|
||||||
|
|
||||||
|
install -Dm755 \
|
||||||
|
"$ROOT/scripts/ekklesia-classroom-update" \
|
||||||
|
"$STAGE/usr/local/bin/ekklesia-classroom-update"
|
||||||
|
|
||||||
|
# System configuration.
|
||||||
|
install -Dm644 \
|
||||||
|
"$ROOT/systemd/autologin/autologin.conf" \
|
||||||
|
"$STAGE/etc/systemd/system/[email protected]/autologin.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"
|
||||||
|
|
||||||
|
printf '%s\n' "$VERSION" > "$ROOT/VERSION"
|
||||||
|
|
||||||
|
dpkg-deb \
|
||||||
|
--root-owner-group \
|
||||||
|
--build "$STAGE" "$OUTPUT"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Built:"
|
||||||
|
echo "$OUTPUT"
|
||||||
|
sha256sum "$OUTPUT"
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
current_version="$(cat VERSION)"
|
||||||
|
|
||||||
|
if [[ $# -eq 1 ]]; then
|
||||||
|
version="$1"
|
||||||
|
else
|
||||||
|
IFS='.' read -r major minor patch <<< "$current_version"
|
||||||
|
version="${major}.${minor}.$((patch + 1))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
package_name="ekklesia-classroom_${version}_amd64.deb"
|
||||||
|
package_path="$ROOT/dist/$package_name"
|
||||||
|
|
||||||
|
echo "Current version: $current_version"
|
||||||
|
echo "New canary: $version"
|
||||||
|
echo
|
||||||
|
|
||||||
|
"$ROOT/tools/build-release.sh" "$version"
|
||||||
|
|
||||||
|
hash="$(sha256sum "$package_path" | awk '{print $1}')"
|
||||||
|
|
||||||
|
mkdir -p "$ROOT/repo/packages" "$ROOT/repo/canary"
|
||||||
|
|
||||||
|
cp "$package_path" "$ROOT/repo/packages/$package_name"
|
||||||
|
|
||||||
|
cat > "$ROOT/repo/canary/manifest.json" <<EOF
|
||||||
|
{
|
||||||
|
"version": "$version",
|
||||||
|
"package": "/packages/$package_name",
|
||||||
|
"sha256": "$hash",
|
||||||
|
"requires_reboot": true
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Publishing package..."
|
||||||
|
|
||||||
|
rsync -av \
|
||||||
|
-e ssh \
|
||||||
|
"$ROOT/repo/packages/$package_name" \
|
||||||
|
classroom-control:/srv/ekklesia/classroom-updates/packages/
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Publishing canary manifest..."
|
||||||
|
|
||||||
|
# Manifest deliberately goes LAST.
|
||||||
|
rsync -av \
|
||||||
|
-e ssh \
|
||||||
|
"$ROOT/repo/canary/manifest.json" \
|
||||||
|
classroom-control:/srv/ekklesia/classroom-updates/canary/manifest.json
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Published Ekklesia Classroom $version to CANARY"
|
||||||
|
echo "https://updates.riverofglorymn.org/canary/manifest.json"
|
||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if [[ $# -eq 1 ]]; then
|
||||||
|
version="$1"
|
||||||
|
|
||||||
|
package_name="ekklesia-classroom_${version}_amd64.deb"
|
||||||
|
package_path="$ROOT/repo/packages/$package_name"
|
||||||
|
|
||||||
|
if [[ ! -f "$package_path" ]]; then
|
||||||
|
echo "Package does not exist locally:"
|
||||||
|
echo "$package_path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
hash="$(sha256sum "$package_path" | awk '{print $1}')"
|
||||||
|
|
||||||
|
mkdir -p "$ROOT/repo/stable"
|
||||||
|
|
||||||
|
cat > "$ROOT/repo/stable/manifest.json" <<EOF
|
||||||
|
{
|
||||||
|
"version": "$version",
|
||||||
|
"package": "/packages/$package_name",
|
||||||
|
"sha256": "$hash",
|
||||||
|
"requires_reboot": true
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
if [[ ! -f "$ROOT/repo/canary/manifest.json" ]]; then
|
||||||
|
echo "No local canary manifest exists."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ROOT/repo/stable"
|
||||||
|
cp "$ROOT/repo/canary/manifest.json" "$ROOT/repo/stable/manifest.json"
|
||||||
|
|
||||||
|
version="$(python3 -c \
|
||||||
|
'import json; print(json.load(open("repo/stable/manifest.json"))["version"])')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Promoting $version to STABLE"
|
||||||
|
|
||||||
|
rsync -av \
|
||||||
|
-e ssh \
|
||||||
|
"$ROOT/repo/stable/manifest.json" \
|
||||||
|
classroom-control:/srv/ekklesia/classroom-updates/stable/manifest.json
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Ekklesia Classroom $version is now STABLE"
|
||||||
|
echo "https://updates.riverofglorymn.org/stable/manifest.json"
|
||||||
Reference in New Issue
Block a user