Release 0.1.37 deployment prep
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"PasswordManagerEnabled": false,
|
||||
"PasswordLeakDetectionEnabled": false,
|
||||
"BackgroundModeEnabled": false,
|
||||
"DisablePrintPreview": true,
|
||||
|
||||
"CookiesSessionOnlyForUrls": [
|
||||
"[*.]wonderink.org"
|
||||
|
||||
+2
-3
@@ -13,9 +13,8 @@ DVD_ENABLED=true
|
||||
Then restart the session (reboot, or restart `classroom-dock`).
|
||||
|
||||
With `DVD_ENABLED=false`, or the key absent, the DVD button is not built
|
||||
at all and no drive polling happens. Older endpoints get a commented
|
||||
`DVD_ENABLED=false` appended on upgrade so the option is discoverable,
|
||||
which leaves their behaviour unchanged.
|
||||
at all and no drive polling happens. The LIVE placeholder is controlled
|
||||
independently by `LIVE_ENABLED`; see [live.md](live.md).
|
||||
|
||||
## Behaviour
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Sanctuary LIVE dock capability
|
||||
|
||||
Sanctuary live viewing is not implemented. The existing placeholder can be
|
||||
shown on development devices or hidden on ordinary classrooms with
|
||||
`/etc/ekklesia-classroom/device.conf`:
|
||||
|
||||
```
|
||||
LIVE_ENABLED=false
|
||||
```
|
||||
|
||||
With `LIVE_ENABLED=false`, or when the key is absent, the LIVE button is not
|
||||
built at all, so it leaves no empty space and starts no live-specific work.
|
||||
|
||||
With `LIVE_ENABLED=true`, the current LIVE icon and placeholder notification
|
||||
appear unchanged. This flag does not enable playback or start a streaming
|
||||
service.
|
||||
|
||||
Older endpoints receive one commented `LIVE_ENABLED=false` setting on
|
||||
upgrade. Existing device configuration values and comments are preserved.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Classroom networking
|
||||
|
||||
Ethernet remains the normal classroom connection. The package also installs
|
||||
and enables NetworkManager so an administrator can add Wi-Fi when a room needs
|
||||
it; there is no classroom Wi-Fi screen and no network name or password in the
|
||||
package.
|
||||
|
||||
## Add Wi-Fi
|
||||
|
||||
From a terminal or SSH session, run:
|
||||
|
||||
```
|
||||
sudo nmtui
|
||||
```
|
||||
|
||||
Choose **Activate a connection**, select the church Wi-Fi network, enter its
|
||||
password, and activate it. NetworkManager saves the connection as a system
|
||||
connection and reconnects it automatically after reboot.
|
||||
|
||||
Useful checks:
|
||||
|
||||
```
|
||||
nmcli device status
|
||||
nmcli connection show
|
||||
systemctl status NetworkManager
|
||||
```
|
||||
|
||||
There is nothing to configure on a computer without Wi-Fi hardware.
|
||||
NetworkManager starts normally, sees no Wi-Fi device, and Ethernet continues
|
||||
to work.
|
||||
|
||||
## Existing Ethernet configuration
|
||||
|
||||
Do not remove or rewrite `/etc/network/interfaces` as part of this procedure.
|
||||
Debian Trixie's NetworkManager configuration uses the `ifupdown` plugin with
|
||||
`managed=false`, so any Ethernet interface declared there remains under
|
||||
ifupdown's control. NetworkManager manages other interfaces, including Wi-Fi.
|
||||
This preserves an existing static or ifupdown Ethernet setup.
|
||||
|
||||
The package disables `NetworkManager-wait-online.service`; no classroom
|
||||
feature is allowed to hold boot waiting for Wi-Fi or Internet access. Saved
|
||||
connections still reconnect automatically.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Classroom printing
|
||||
|
||||
The package installs and enables CUPS, its client/filter stack, and Debian's
|
||||
compressed Foomatic PPD database. Chrome is configured to open the system
|
||||
print dialog, which shows the queues configured in CUPS.
|
||||
|
||||
The target Ricoh Aficio MP C3502 supports PCL 5c and PCL 6 as standard;
|
||||
PostScript 3 is optional. Debian Trixie does not provide a model-specific
|
||||
C3502 PPD in the installed driver set, so the classroom helper uses Trixie's
|
||||
recommended generic color PCL 6/PCL XL driver. This works with the printer's
|
||||
standard language and does not require a vendor binary.
|
||||
|
||||
## Configure the church color printer
|
||||
|
||||
Give the helper the printer's IP address or DNS name:
|
||||
|
||||
```
|
||||
sudo classroom-printer-setup 10.20.30.40
|
||||
```
|
||||
|
||||
This creates or updates one queue named `Classroom_Color`, points it directly
|
||||
to `socket://10.20.30.40:9100`, enables it, accepts jobs, and makes it the
|
||||
system default. The printer's address is never stored in the package.
|
||||
|
||||
An explicit URI is also accepted when deployment requires another supported
|
||||
transport:
|
||||
|
||||
```
|
||||
sudo classroom-printer-setup ipp://10.20.30.40/ipp/print
|
||||
sudo classroom-printer-setup --no-default socket://10.20.30.40:9100
|
||||
```
|
||||
|
||||
The command is idempotent: running it again updates the same queue. It does not
|
||||
contact the printer while configuring CUPS, and an unreachable printer only
|
||||
leaves jobs queued; it never affects boot.
|
||||
|
||||
Automatic remote queue browsing is disabled. `cups-browsed` is stopped and
|
||||
disabled if another package installed it, so unrelated advertised label and
|
||||
check-in printers are not turned into local classroom queues.
|
||||
|
||||
## Administration
|
||||
|
||||
List the available Debian drivers, including the selected PCL path:
|
||||
|
||||
```
|
||||
lpinfo -m
|
||||
lpinfo -m | grep -i -E 'Ricoh|PCL|PostScript|IPP Everywhere'
|
||||
```
|
||||
|
||||
Show the classroom queue, its URI, and the system default:
|
||||
|
||||
```
|
||||
lpstat -p Classroom_Color -v
|
||||
lpstat -d
|
||||
```
|
||||
|
||||
Set the default manually:
|
||||
|
||||
```
|
||||
sudo lpadmin -d Classroom_Color
|
||||
```
|
||||
|
||||
Print CUPS's test page:
|
||||
|
||||
```
|
||||
lp -d Classroom_Color /usr/share/cups/data/testprint
|
||||
```
|
||||
|
||||
Inspect or cancel queued jobs:
|
||||
|
||||
```
|
||||
lpstat -o Classroom_Color
|
||||
cancel -a Classroom_Color
|
||||
```
|
||||
|
||||
The printer language choice is based on Ricoh's published
|
||||
[MP C3002/C3502 specifications](https://assets.ricoh-usa.com/j2jqn9lauv41/1CzJla6qwny2d9oBVHHlID/5bfa366a01cf82162ea24287f99c8af9/aficiompc3002.pdf).
|
||||
+10
-1
@@ -51,8 +51,17 @@ classroom package and apply as soon as Chrome exists.
|
||||
|
||||
Only for rooms with `DVD_ENABLED=true`. See [dvd.md](dvd.md).
|
||||
|
||||
## Networking and printing
|
||||
|
||||
NetworkManager and CUPS are Debian package dependencies, not deferred network
|
||||
provisioning steps. Their services are enabled by the package and neither
|
||||
requires Internet access or hardware to be present.
|
||||
|
||||
- Wi-Fi administration: [networking.md](networking.md)
|
||||
- Church color printer setup: [printing.md](printing.md)
|
||||
|
||||
## What provisioning does not do
|
||||
|
||||
It never upgrades or reconfigures anything already installed, and it
|
||||
The boot provisioning command 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.
|
||||
|
||||
@@ -19,6 +19,31 @@ before promoting to stable with `./update_stable.sh`.
|
||||
- [ ] A machine with no network still boots to a working shell, and
|
||||
provisioning succeeds at a later boot once it is online
|
||||
|
||||
## Networking
|
||||
|
||||
- [ ] `nmtui` is installed and `NetworkManager.service` is enabled
|
||||
- [ ] An Ethernet interface declared in `/etc/network/interfaces` remains
|
||||
managed by ifupdown and keeps its existing address
|
||||
- [ ] `sudo nmtui` can save and activate a Wi-Fi connection
|
||||
- [ ] The saved Wi-Fi connection reconnects after reboot
|
||||
- [ ] A machine with no Wi-Fi adapter reaches the classroom normally without
|
||||
an online-wait delay or NetworkManager failure
|
||||
- [ ] Ethernet-only operation is unchanged
|
||||
|
||||
## Printing
|
||||
|
||||
- [ ] `cups.service` is enabled and `lpstat -r` reports that the scheduler runs
|
||||
- [ ] `lpinfo -m` contains the generic color PCL 6/PCL XL driver documented in
|
||||
[printing.md](printing.md)
|
||||
- [ ] `sudo classroom-printer-setup <printer-ip>` creates only
|
||||
`Classroom_Color`, enables it, and makes it the system default
|
||||
- [ ] Repeating the helper updates the same queue without creating a duplicate
|
||||
- [ ] Chrome opens the system print dialog and shows the configured queue
|
||||
- [ ] Unrelated label/check-in printers are not automatically added
|
||||
- [ ] The CUPS test page prints in color on the Ricoh MP C3502
|
||||
- [ ] With the Ricoh powered off, the classroom still boots normally and a job
|
||||
simply remains queued
|
||||
|
||||
## UI scale
|
||||
|
||||
UI_SCALE sets sway's output scale, so it scales the whole session.
|
||||
@@ -169,6 +194,19 @@ With `DVD_ENABLED=true`:
|
||||
- [ ] Commercial CSS disc plays without anyone having run codec setup
|
||||
by hand
|
||||
|
||||
## Sanctuary LIVE capability
|
||||
|
||||
With `LIVE_ENABLED=false`:
|
||||
|
||||
- [ ] No LIVE button or spacing hole appears in the dock
|
||||
- [ ] No live-specific service or process starts
|
||||
|
||||
With `LIVE_ENABLED=true`:
|
||||
|
||||
- [ ] LIVE appears after DVD when DVD is enabled, and after Browser otherwise
|
||||
- [ ] Clicking it shows the existing placeholder notification
|
||||
- [ ] No live playback is attempted
|
||||
|
||||
### DVD mode
|
||||
|
||||
- [ ] Pressing DVD covers the **whole** display immediately, dock and
|
||||
@@ -206,3 +244,8 @@ With `DVD_ENABLED=true`:
|
||||
- [ ] SHA-256 verification passes
|
||||
- [ ] Install succeeds and the endpoint reboots
|
||||
- [ ] Everything above still works after the update
|
||||
- [ ] Upgrade from 0.1.36 preserves every existing `device.conf` value
|
||||
- [ ] `LIVE_ENABLED=false` is appended exactly once on upgrade and remains
|
||||
single after reinstall
|
||||
- [ ] `tools/validate-trixie-release.sh` passes against the 0.1.37 package and
|
||||
the 0.1.36 package
|
||||
|
||||
@@ -19,6 +19,70 @@ UI_SCALE_MIN = 0.8
|
||||
UI_SCALE_MAX = 1.5
|
||||
|
||||
|
||||
DEVICE_CONFIG_TEMPLATE = """# Ekklesia Classroom device configuration
|
||||
|
||||
UPDATE_URL=https://updates.riverofglorymn.org
|
||||
CHANNEL=stable
|
||||
|
||||
# Set to true on classrooms that have a DVD drive.
|
||||
DVD_ENABLED=false
|
||||
|
||||
# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
|
||||
# implemented yet; this only controls whether the current placeholder appears.
|
||||
LIVE_ENABLED=false
|
||||
|
||||
# Size of the dock, clock and notifications, 0.8 to 1.5.
|
||||
# Larger rooms and larger TVs want a larger number.
|
||||
UI_SCALE=1.0
|
||||
|
||||
# Church message broker. The room is this machine's hostname unless ROOM
|
||||
# is set. Leave MQTT_ENABLED=false until a broker exists.
|
||||
MQTT_ENABLED=false
|
||||
MQTT_HOST=
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME=
|
||||
MQTT_PASSWORD=
|
||||
"""
|
||||
|
||||
|
||||
# Sections added after the first classroom release. Keeping them separate lets
|
||||
# an upgrade make a setting discoverable without replacing anything an
|
||||
# administrator has already put in device.conf.
|
||||
DEVICE_CONFIG_SECTIONS = (
|
||||
(
|
||||
"DVD_ENABLED",
|
||||
"""# Set to true on classrooms that have a DVD drive.
|
||||
DVD_ENABLED=false
|
||||
""",
|
||||
),
|
||||
(
|
||||
"LIVE_ENABLED",
|
||||
"""# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
|
||||
# implemented yet; this only controls whether the current placeholder appears.
|
||||
LIVE_ENABLED=false
|
||||
""",
|
||||
),
|
||||
(
|
||||
"UI_SCALE",
|
||||
"""# Size of the dock, clock and notifications, 0.8 to 1.5.
|
||||
# Larger rooms and larger TVs want a larger number.
|
||||
UI_SCALE=1.0
|
||||
""",
|
||||
),
|
||||
(
|
||||
"MQTT_ENABLED",
|
||||
"""# Church message broker. The room is this machine's hostname unless ROOM
|
||||
# is set. Leave MQTT_ENABLED=false until a broker exists.
|
||||
MQTT_ENABLED=false
|
||||
MQTT_HOST=
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME=
|
||||
MQTT_PASSWORD=
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def read_device_config(path=None):
|
||||
config = {}
|
||||
path = Path(path) if path is not None else CONFIG_FILE
|
||||
@@ -45,6 +109,47 @@ def enabled(config, key, default=False):
|
||||
return config[key].lower() in TRUTHY
|
||||
|
||||
|
||||
def dock_items(config):
|
||||
"""Dock items in display order, with disabled capabilities omitted."""
|
||||
items = ["browser"]
|
||||
|
||||
if enabled(config, "DVD_ENABLED"):
|
||||
items.append("dvd")
|
||||
|
||||
if enabled(config, "LIVE_ENABLED"):
|
||||
items.append("live")
|
||||
|
||||
return tuple(items)
|
||||
|
||||
|
||||
def ensure_device_config(path=None):
|
||||
"""Create device.conf or append any missing settings exactly once.
|
||||
|
||||
Existing bytes are otherwise left alone: per-room settings, comments and
|
||||
even keys this release does not know about all survive an upgrade.
|
||||
"""
|
||||
path = Path(path) if path is not None else CONFIG_FILE
|
||||
|
||||
if not path.exists():
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(DEVICE_CONFIG_TEMPLATE)
|
||||
return
|
||||
|
||||
text = path.read_text()
|
||||
config = read_device_config(path)
|
||||
additions = []
|
||||
|
||||
for key, section in DEVICE_CONFIG_SECTIONS:
|
||||
if key not in config:
|
||||
additions.append(section.rstrip("\n"))
|
||||
|
||||
if not additions:
|
||||
return
|
||||
|
||||
separator = "" if not text or text.endswith("\n") else "\n"
|
||||
path.write_text(text + separator + "\n\n".join(additions) + "\n")
|
||||
|
||||
|
||||
def ui_scale(config):
|
||||
"""UI_SCALE, clamped to something a classroom can actually use."""
|
||||
raw = config.get("UI_SCALE", "")
|
||||
|
||||
@@ -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, python3-gi-cairo, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, adwaita-icon-theme, libnotify-bin, curl, gnupg, ca-certificates, python3-paho-mqtt
|
||||
Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups, cups-client, cups-filters, foomatic-db-compressed-ppds, python3-gi, python3-gi-cairo, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, adwaita-icon-theme, libnotify-bin, curl, gnupg, ca-certificates, python3-paho-mqtt
|
||||
Recommends: vlc
|
||||
Description: Ekklesia managed classroom environment
|
||||
A minimal Wayland classroom environment for Ekklesia-managed church computers.
|
||||
|
||||
+24
-55
@@ -30,6 +30,7 @@ chmod 0755 /usr/local/bin/classroom-scale
|
||||
chmod 0755 /usr/local/bin/classroom-shutdown
|
||||
chmod 0755 /usr/local/bin/classroom-chrome-setup
|
||||
chmod 0755 /usr/local/bin/classroom-provision
|
||||
chmod 0755 /usr/local/bin/classroom-printer-setup
|
||||
|
||||
# Reading the optical drive needs group cdrom. Takes effect at the next
|
||||
# login, which the OTA reboot provides.
|
||||
@@ -37,62 +38,14 @@ if getent group cdrom >/dev/null 2>&1; then
|
||||
adduser classroom cdrom >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ ! -f /etc/ekklesia-classroom/device.conf ]; then
|
||||
cat > /etc/ekklesia-classroom/device.conf <<'CONFIG'
|
||||
# Ekklesia Classroom device configuration
|
||||
# Create device.conf on a new endpoint, or append only settings an older
|
||||
# endpoint does not yet know. The helper preserves every existing byte apart
|
||||
# from the append, so per-room choices always survive an upgrade.
|
||||
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 - <<'PY'
|
||||
from classroom_config import ensure_device_config
|
||||
|
||||
UPDATE_URL=https://updates.riverofglorymn.org
|
||||
CHANNEL=stable
|
||||
|
||||
# Set to true on classrooms that have a DVD drive.
|
||||
DVD_ENABLED=false
|
||||
|
||||
# Size of the dock, clock and notifications, 0.8 to 1.5.
|
||||
# Larger rooms and larger TVs want a larger number.
|
||||
UI_SCALE=1.0
|
||||
|
||||
# Church message broker. The room is this machine's hostname unless ROOM
|
||||
# is set. Leave MQTT_ENABLED=false until a broker exists.
|
||||
MQTT_ENABLED=false
|
||||
MQTT_HOST=
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME=
|
||||
MQTT_PASSWORD=
|
||||
CONFIG
|
||||
fi
|
||||
|
||||
# Make the settings discoverable on classrooms installed before they
|
||||
# existed. Both defaults are what those rooms already do, so nothing
|
||||
# about their behaviour changes.
|
||||
if ! grep -q '^DVD_ENABLED=' /etc/ekklesia-classroom/device.conf; then
|
||||
cat >> /etc/ekklesia-classroom/device.conf <<'CONFIG'
|
||||
|
||||
# Set to true on classrooms that have a DVD drive.
|
||||
DVD_ENABLED=false
|
||||
CONFIG
|
||||
fi
|
||||
|
||||
if ! grep -q '^UI_SCALE=' /etc/ekklesia-classroom/device.conf; then
|
||||
cat >> /etc/ekklesia-classroom/device.conf <<'CONFIG'
|
||||
|
||||
# Size of the dock, clock and notifications, 0.8 to 1.5.
|
||||
# Larger rooms and larger TVs want a larger number.
|
||||
UI_SCALE=1.0
|
||||
CONFIG
|
||||
fi
|
||||
|
||||
if ! grep -q '^MQTT_ENABLED=' /etc/ekklesia-classroom/device.conf; then
|
||||
cat >> /etc/ekklesia-classroom/device.conf <<'CONFIG'
|
||||
|
||||
# Church message broker. The room is this machine's hostname unless ROOM
|
||||
# is set. Leave MQTT_ENABLED=false until a broker exists.
|
||||
MQTT_ENABLED=false
|
||||
MQTT_HOST=
|
||||
MQTT_PORT=1883
|
||||
MQTT_USERNAME=
|
||||
MQTT_PASSWORD=
|
||||
CONFIG
|
||||
fi
|
||||
ensure_device_config()
|
||||
PY
|
||||
|
||||
# The broker password lives here, so the file stops being world readable.
|
||||
# Everything that reads it runs as root or as the classroom user.
|
||||
@@ -101,6 +54,22 @@ chmod 0640 /etc/ekklesia-classroom/device.conf
|
||||
|
||||
systemctl daemon-reload || true
|
||||
|
||||
# NetworkManager ships nmtui and deliberately leaves interfaces declared in
|
||||
# /etc/network/interfaces under ifupdown's control (managed=false). This gives
|
||||
# administrators saved, auto-reconnecting Wi-Fi without taking over an
|
||||
# existing static Ethernet setup. No classroom service needs to hold boot for
|
||||
# NetworkManager's online wait, so keep that waiter disabled.
|
||||
systemctl enable NetworkManager.service || true
|
||||
systemctl disable NetworkManager-wait-online.service || true
|
||||
systemctl start NetworkManager.service || true
|
||||
|
||||
# CUPS is ready at boot, but no queue is created until an administrator runs
|
||||
# classroom-printer-setup. cups-browsed is unnecessary here and would expose
|
||||
# every advertised printer on the church network if installed independently.
|
||||
systemctl enable cups.socket cups.service || true
|
||||
systemctl disable --now cups-browsed.service || true
|
||||
systemctl start cups.socket || true
|
||||
|
||||
systemctl enable ekklesia-classroom-update.timer || true
|
||||
systemctl start ekklesia-classroom-update.timer || true
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ sys.path.insert(0, "/usr/local/lib/ekklesia-classroom")
|
||||
import classroom_style as style # noqa: E402
|
||||
from classroom_config import ( # noqa: E402
|
||||
SHELL_SCALE,
|
||||
enabled,
|
||||
dock_items,
|
||||
read_device_config,
|
||||
scaled,
|
||||
)
|
||||
@@ -311,6 +311,8 @@ class ClassroomShell(Gtk.Window):
|
||||
)
|
||||
dock.get_style_context().add_class("dock")
|
||||
|
||||
items = dock_items(self.config)
|
||||
|
||||
dock.pack_start(
|
||||
self.make_button(
|
||||
"browser",
|
||||
@@ -322,11 +324,7 @@ class ClassroomShell(Gtk.Window):
|
||||
0,
|
||||
)
|
||||
|
||||
if enabled(
|
||||
self.config,
|
||||
"DVD_ENABLED",
|
||||
False,
|
||||
):
|
||||
if "dvd" in items:
|
||||
dock.pack_start(
|
||||
self.make_button(
|
||||
"dvd",
|
||||
@@ -342,6 +340,7 @@ class ClassroomShell(Gtk.Window):
|
||||
|
||||
self.start_disc_watch()
|
||||
|
||||
if "live" in items:
|
||||
dock.pack_start(
|
||||
self.make_button(
|
||||
"sanctuary",
|
||||
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Configure the one explicitly managed classroom printer. The Ricoh Aficio
|
||||
# MP C3502 supports PCL 6 as standard; Debian Trixie's compressed Foomatic
|
||||
# database supplies this generic color PCL 6/PCL XL path without vendor code.
|
||||
|
||||
QUEUE="Classroom_Color"
|
||||
DRIVER="foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd"
|
||||
MAKE_MODEL="Ricoh Aficio MP C3502 (Generic PCL 6 Color)"
|
||||
LOCATION="Ekklesia Classroom"
|
||||
MAKE_DEFAULT=true
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: classroom-printer-setup [--no-default] PRINTER-IP-OR-URI
|
||||
|
||||
Examples:
|
||||
sudo classroom-printer-setup 10.20.30.40
|
||||
sudo classroom-printer-setup socket://printer.example.org:9100
|
||||
|
||||
An IP address or hostname uses JetDirect/AppSocket on port 9100. Explicit
|
||||
socket://, ipp://, ipps:// and lpd:// URIs are also accepted.
|
||||
USAGE
|
||||
}
|
||||
|
||||
log() {
|
||||
echo "[classroom-printer-setup] $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
log "ERROR: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "${CLASSROOM_PRINTER_SETUP_TESTING:-false}" != "true" && $EUID -ne 0 ]]; then
|
||||
fail "run this command as root: sudo classroom-printer-setup <printer-ip>"
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "--no-default" ]]; then
|
||||
MAKE_DEFAULT=false
|
||||
shift
|
||||
fi
|
||||
|
||||
if [[ $# -ne 1 || "$1" == "-h" || "$1" == "--help" ]]; then
|
||||
usage
|
||||
[[ $# -eq 1 ]] && exit 0
|
||||
exit 2
|
||||
fi
|
||||
|
||||
address="$1"
|
||||
|
||||
case "$address" in
|
||||
socket://*|ipp://*|ipps://*|lpd://*)
|
||||
uri="$address"
|
||||
;;
|
||||
*://*)
|
||||
fail "unsupported printer URI scheme: $address"
|
||||
;;
|
||||
*)
|
||||
if [[ ! "$address" =~ ^[A-Za-z0-9._:-]+$ ]]; then
|
||||
fail "printer address contains unsupported characters: $address"
|
||||
fi
|
||||
uri="socket://${address}:9100"
|
||||
;;
|
||||
esac
|
||||
|
||||
for command in lpadmin lpinfo lpstat cupsenable cupsaccept; do
|
||||
command -v "$command" >/dev/null 2>&1 || \
|
||||
fail "$command is missing; reinstall the ekklesia-classroom package"
|
||||
done
|
||||
|
||||
if ! lpstat -r >/dev/null 2>&1; then
|
||||
command -v systemctl >/dev/null 2>&1 && \
|
||||
systemctl start cups.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
lpstat -r >/dev/null 2>&1 || \
|
||||
fail "CUPS is not running; check: systemctl status cups"
|
||||
|
||||
models="$(lpinfo -m)" || fail "CUPS could not list installed printer drivers"
|
||||
|
||||
if ! grep -q "^${DRIVER} " <<< "$models"; then
|
||||
fail "the Debian generic color PCL 6 driver is not installed"
|
||||
fi
|
||||
|
||||
log "configuring ${QUEUE} at ${uri}"
|
||||
|
||||
# Repeating this command updates the same named queue; it never creates a
|
||||
# second destination. None of these operations connects to the printer.
|
||||
lpadmin \
|
||||
-p "$QUEUE" \
|
||||
-v "$uri" \
|
||||
-m "$DRIVER" \
|
||||
-D "$MAKE_MODEL" \
|
||||
-L "$LOCATION" || fail "CUPS could not configure ${QUEUE}"
|
||||
|
||||
cupsenable "$QUEUE" || fail "CUPS could not enable ${QUEUE}"
|
||||
cupsaccept "$QUEUE" || fail "CUPS could not accept jobs for ${QUEUE}"
|
||||
|
||||
if [[ "$MAKE_DEFAULT" == true ]]; then
|
||||
lpadmin -d "$QUEUE" || fail "CUPS could not make ${QUEUE} the default"
|
||||
fi
|
||||
|
||||
log "${QUEUE} is configured, enabled and accepting jobs."
|
||||
|
||||
if [[ "$MAKE_DEFAULT" == true ]]; then
|
||||
log "${QUEUE} is the system default printer."
|
||||
fi
|
||||
|
||||
log "The printer may remain offline until it is reachable; this does not affect boot."
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
PYTHONPATH="$ROOT/lib" python3 - <<'PY'
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from classroom_config import dock_items, ensure_device_config, read_device_config
|
||||
|
||||
passed = failed = 0
|
||||
|
||||
|
||||
def check(label, condition, detail=""):
|
||||
global passed, failed
|
||||
if condition:
|
||||
print(f" ok {label}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" FAIL {label}")
|
||||
if detail:
|
||||
print(f" {detail}")
|
||||
failed += 1
|
||||
|
||||
|
||||
print("1. dock capabilities")
|
||||
cases = [
|
||||
({}, ("browser",)),
|
||||
({"DVD_ENABLED": "true"}, ("browser", "dvd")),
|
||||
({"LIVE_ENABLED": "true"}, ("browser", "live")),
|
||||
(
|
||||
{"DVD_ENABLED": "true", "LIVE_ENABLED": "true"},
|
||||
("browser", "dvd", "live"),
|
||||
),
|
||||
]
|
||||
|
||||
for config, expected in cases:
|
||||
actual = dock_items(config)
|
||||
check(f"{config or 'both disabled'} -> {expected}", actual == expected, actual)
|
||||
|
||||
check(
|
||||
"false spellings do not leave placeholder items",
|
||||
dock_items({"DVD_ENABLED": "false", "LIVE_ENABLED": "off"}) == ("browser",),
|
||||
)
|
||||
check(
|
||||
"existing dock order is browser, DVD, LIVE",
|
||||
dock_items({"DVD_ENABLED": "yes", "LIVE_ENABLED": "1"})
|
||||
== ("browser", "dvd", "live"),
|
||||
)
|
||||
|
||||
print()
|
||||
print("2. fresh device configuration")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "etc" / "device.conf"
|
||||
ensure_device_config(path)
|
||||
config = read_device_config(path)
|
||||
check("fresh config is created", path.is_file())
|
||||
check("DVD defaults off", config.get("DVD_ENABLED") == "false")
|
||||
check("LIVE defaults off", config.get("LIVE_ENABLED") == "false")
|
||||
check("MQTT defaults off", config.get("MQTT_ENABLED") == "false")
|
||||
check("stable remains the fresh-install default", config.get("CHANNEL") == "stable")
|
||||
|
||||
print()
|
||||
print("3. upgrade from 0.1.36")
|
||||
old = """# endpoint configuration from 0.1.36
|
||||
UPDATE_URL=https://updates.example.test
|
||||
CHANNEL=canary
|
||||
DVD_ENABLED=true
|
||||
UI_SCALE=1.25
|
||||
MQTT_ENABLED=true
|
||||
MQTT_HOST=broker.example.test
|
||||
MQTT_PORT=1884
|
||||
MQTT_USERNAME=room
|
||||
MQTT_PASSWORD=secret
|
||||
CUSTOM_SETTING=keep-me
|
||||
"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "device.conf"
|
||||
path.write_text(old)
|
||||
|
||||
ensure_device_config(path)
|
||||
first = path.read_text()
|
||||
config = read_device_config(path)
|
||||
|
||||
check("old bytes remain at the start", first.startswith(old))
|
||||
check("channel survives", config.get("CHANNEL") == "canary")
|
||||
check("DVD choice survives", config.get("DVD_ENABLED") == "true")
|
||||
check("UI scale survives", config.get("UI_SCALE") == "1.25")
|
||||
check("MQTT host survives", config.get("MQTT_HOST") == "broker.example.test")
|
||||
check("MQTT password survives", config.get("MQTT_PASSWORD") == "secret")
|
||||
check("unknown settings survive", config.get("CUSTOM_SETTING") == "keep-me")
|
||||
check("LIVE is appended once", first.count("LIVE_ENABLED=") == 1)
|
||||
|
||||
ensure_device_config(path)
|
||||
second = path.read_text()
|
||||
check("reinstall makes no further change", second == first)
|
||||
check("LIVE still occurs once", second.count("LIVE_ENABLED=") == 1)
|
||||
|
||||
print()
|
||||
print(f"passed: {passed} failed: {failed}")
|
||||
raise SystemExit(1 if failed else 0)
|
||||
PY
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT="$ROOT/scripts/classroom-printer-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 to contain: $expected"
|
||||
echo " got: $actual"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
check_absent() {
|
||||
local label="$1" unexpected="$2" actual="$3"
|
||||
if [[ "$actual" != *"$unexpected"* ]]; then
|
||||
echo " ok $label"
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
echo " FAIL $label"
|
||||
echo " expected not to contain: $unexpected"
|
||||
echo " got: $actual"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir -p "$WORK/bin"
|
||||
|
||||
for command in lpadmin cupsenable cupsaccept; do
|
||||
cat > "$WORK/bin/$command" <<'SHIM'
|
||||
#!/bin/bash
|
||||
echo "$(basename "$0") $*" >> "$CUPS_LOG"
|
||||
SHIM
|
||||
done
|
||||
|
||||
cat > "$WORK/bin/lpstat" <<'SHIM'
|
||||
#!/bin/bash
|
||||
[[ "${FAKE_CUPS_RUNNING:-true}" == "true" ]]
|
||||
SHIM
|
||||
|
||||
cat > "$WORK/bin/lpinfo" <<'SHIM'
|
||||
#!/bin/bash
|
||||
if [[ "${FAKE_DRIVER_PRESENT:-true}" == "true" ]]; then
|
||||
echo "foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd Generic PCL 6/PCL XL Printer Foomatic/pxlcolor (recommended)"
|
||||
else
|
||||
echo "everywhere IPP Everywhere"
|
||||
fi
|
||||
SHIM
|
||||
|
||||
chmod +x "$WORK/bin"/*
|
||||
export PATH="$WORK/bin:$PATH"
|
||||
export CUPS_LOG="$WORK/cups.log"
|
||||
export CLASSROOM_PRINTER_SETUP_TESTING=true
|
||||
|
||||
run() {
|
||||
"$SCRIPT" "$@" 2>&1
|
||||
}
|
||||
|
||||
echo "1. creates the explicit classroom queue"
|
||||
: > "$CUPS_LOG"
|
||||
out="$(run 10.20.30.40)"
|
||||
log="$(cat "$CUPS_LOG")"
|
||||
check "uses AppSocket by IP" "-v socket://10.20.30.40:9100" "$log"
|
||||
check "uses Trixie's generic color PCL 6 driver" \
|
||||
"Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd" "$log"
|
||||
check "uses one stable queue name" "lpadmin -p Classroom_Color" "$log"
|
||||
check "enables the queue" "cupsenable Classroom_Color" "$log"
|
||||
check "accepts jobs" "cupsaccept Classroom_Color" "$log"
|
||||
check "defaults the queue" "lpadmin -d Classroom_Color" "$log"
|
||||
check "reports offline-safe behavior" "may remain offline" "$out"
|
||||
|
||||
echo
|
||||
echo "2. repeated setup updates the same queue"
|
||||
run 10.20.30.40 >/dev/null
|
||||
configs="$(grep -c '^lpadmin -p Classroom_Color' "$CUPS_LOG")"
|
||||
check "two runs configured the same queue twice" "2" "$configs"
|
||||
check_absent "no generated queue suffix" "Classroom_Color_" "$(cat "$CUPS_LOG")"
|
||||
|
||||
echo
|
||||
echo "3. explicit URIs and non-default setup"
|
||||
: > "$CUPS_LOG"
|
||||
run --no-default ipp://printer.example.test/ipp/print >/dev/null
|
||||
log="$(cat "$CUPS_LOG")"
|
||||
check "keeps an explicit IPP URI" "-v ipp://printer.example.test/ipp/print" "$log"
|
||||
check_absent "--no-default omits the default change" "lpadmin -d" "$log"
|
||||
|
||||
echo
|
||||
echo "4. errors are useful"
|
||||
if out="$(run smb://printer/share)"; then
|
||||
check "unsupported scheme fails" "failure" "unexpected success"
|
||||
else
|
||||
check "unsupported scheme is named" "unsupported printer URI scheme" "$out"
|
||||
fi
|
||||
|
||||
export FAKE_DRIVER_PRESENT=false
|
||||
if out="$(run 10.20.30.40)"; then
|
||||
check "missing driver fails" "failure" "unexpected success"
|
||||
else
|
||||
check "missing driver is explained" "generic color PCL 6 driver is not installed" "$out"
|
||||
fi
|
||||
unset FAKE_DRIVER_PRESENT
|
||||
|
||||
echo
|
||||
echo "passed: $passed failed: $failed"
|
||||
[[ "$failed" -eq 0 ]]
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
python3 - "$ROOT" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
control = (root / "packaging/debian/control.template").read_text()
|
||||
postinst = (root / "packaging/debian/postinst").read_text()
|
||||
provision = (root / "scripts/classroom-provision").read_text()
|
||||
units = "\n".join(path.read_text() for path in (root / "systemd").rglob("*.service"))
|
||||
canary = (root / "update_canary.sh").read_text()
|
||||
policy = json.loads(
|
||||
(root / "config/chrome/policies/managed/ekklesia-classroom.json").read_text()
|
||||
)
|
||||
|
||||
passed = failed = 0
|
||||
|
||||
|
||||
def check(label, condition):
|
||||
global passed, failed
|
||||
if condition:
|
||||
print(f" ok {label}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" FAIL {label}")
|
||||
failed += 1
|
||||
|
||||
|
||||
depends = re.search(r"^Depends: (.*)$", control, re.MULTILINE).group(1).split(", ")
|
||||
|
||||
print("1. Trixie package contract")
|
||||
for package in (
|
||||
"network-manager",
|
||||
"cups",
|
||||
"cups-client",
|
||||
"cups-filters",
|
||||
"foomatic-db-compressed-ppds",
|
||||
):
|
||||
check(f"depends on {package}", package in depends)
|
||||
|
||||
print()
|
||||
print("2. networking service behavior")
|
||||
check("NetworkManager is explicitly enabled", "systemctl enable NetworkManager.service || true" in postinst)
|
||||
check("NetworkManager start is best effort", "systemctl start NetworkManager.service || true" in postinst)
|
||||
check("online waiter is disabled", "systemctl disable NetworkManager-wait-online.service || true" in postinst)
|
||||
check("no classroom NetworkManager config overrides ifupdown", not (root / "config/network").exists())
|
||||
|
||||
print()
|
||||
print("3. printing service behavior")
|
||||
check("CUPS service and socket are enabled", "systemctl enable cups.socket cups.service || true" in postinst)
|
||||
check("CUPS start is best effort", "systemctl start cups.socket || true" in postinst)
|
||||
check("automatic remote queue browsing is disabled", "systemctl disable --now cups-browsed.service || true" in postinst)
|
||||
check("printer setup is never a boot service", "classroom-printer-setup" not in units)
|
||||
check("printer setup is never provisioning", "classroom-printer-setup" not in provision)
|
||||
check("Chrome uses the system print dialog", policy.get("DisablePrintPreview") is True)
|
||||
|
||||
print()
|
||||
print("4. canary release gate")
|
||||
validation = canary.index('"$ROOT/tools/validate-trixie-release.sh"')
|
||||
package_copy = canary.index('cp "$package_path"')
|
||||
first_rsync = canary.index("rsync -av")
|
||||
check("Trixie validation runs before the repository copy", validation < package_copy)
|
||||
check("Trixie validation runs before any upload", validation < first_rsync)
|
||||
|
||||
print()
|
||||
print(f"passed: {passed} failed: {failed}")
|
||||
raise SystemExit(1 if failed else 0)
|
||||
PY
|
||||
@@ -145,6 +145,10 @@ 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"
|
||||
|
||||
# Shared helpers imported by the classroom commands.
|
||||
install -Dm644 \
|
||||
"$ROOT/lib/classroom_config.py" \
|
||||
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||
echo "Usage: $0 CANDIDATE.deb [PREVIOUS.deb]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
candidate="$(realpath "$1")"
|
||||
previous=""
|
||||
|
||||
[[ -f "$candidate" ]] || { echo "Package not found: $candidate" >&2; exit 1; }
|
||||
|
||||
if [[ $# -eq 2 ]]; then
|
||||
previous="$(realpath "$2")"
|
||||
[[ -f "$previous" ]] || { echo "Package not found: $previous" >&2; exit 1; }
|
||||
fi
|
||||
|
||||
echo "Validating $(basename "$candidate") on Debian Trixie"
|
||||
|
||||
docker_args=(
|
||||
run --rm -i
|
||||
-v "$candidate:/packages/candidate.deb:ro"
|
||||
)
|
||||
|
||||
if [[ -n "$previous" ]]; then
|
||||
docker_args+=( -v "$previous:/packages/previous.deb:ro" )
|
||||
fi
|
||||
|
||||
docker "${docker_args[@]}" debian:trixie-slim \
|
||||
bash -s -- "${previous:+/packages/previous.deb}" <<'CONTAINER'
|
||||
set -euo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
|
||||
previous="${1:-}"
|
||||
|
||||
if [[ -n "$previous" ]]; then
|
||||
apt-get install -y -qq "$previous" >/tmp/previous-install.log
|
||||
|
||||
cat > /etc/ekklesia-classroom/device.conf <<'CONFIG'
|
||||
# Configuration carried forward from 0.1.36
|
||||
UPDATE_URL=https://updates.riverofglorymn.org
|
||||
CHANNEL=canary
|
||||
DVD_ENABLED=true
|
||||
UI_SCALE=1.25
|
||||
MQTT_ENABLED=true
|
||||
MQTT_HOST=broker.example.test
|
||||
MQTT_PORT=1884
|
||||
MQTT_USERNAME=classroom
|
||||
MQTT_PASSWORD=preserve-me
|
||||
CUSTOM_SETTING=preserve-me-too
|
||||
CONFIG
|
||||
fi
|
||||
|
||||
apt-get install -y -qq /packages/candidate.deb >/tmp/candidate-install.log
|
||||
|
||||
command -v nmtui >/dev/null
|
||||
command -v lpadmin >/dev/null
|
||||
command -v lpinfo >/dev/null
|
||||
|
||||
systemctl is-enabled NetworkManager.service >/dev/null
|
||||
systemctl is-enabled cups.service >/dev/null
|
||||
! systemctl is-enabled NetworkManager-wait-online.service >/dev/null 2>&1
|
||||
|
||||
grep -q '^managed=false$' /etc/NetworkManager/NetworkManager.conf
|
||||
|
||||
# A normal Docker container has Ethernet and no Wi-Fi adapter. Reaching this
|
||||
# point proves package configuration did not require Wi-Fi hardware.
|
||||
! find /sys/class/net -mindepth 2 -maxdepth 2 -type d -name wireless | grep -q .
|
||||
|
||||
/usr/sbin/cupsd
|
||||
sleep 1
|
||||
|
||||
driver='foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd'
|
||||
lpinfo -m | grep -q "^${driver} "
|
||||
|
||||
classroom-printer-setup 192.0.2.10 >/tmp/printer-first.log
|
||||
classroom-printer-setup 192.0.2.10 >/tmp/printer-second.log
|
||||
lpstat -p Classroom_Color -d -v | grep -q 'socket://192.0.2.10:9100'
|
||||
|
||||
if [[ -n "$previous" ]]; then
|
||||
grep -q '^CHANNEL=canary$' /etc/ekklesia-classroom/device.conf
|
||||
grep -q '^DVD_ENABLED=true$' /etc/ekklesia-classroom/device.conf
|
||||
grep -q '^UI_SCALE=1.25$' /etc/ekklesia-classroom/device.conf
|
||||
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
|
||||
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
|
||||
[[ "$(grep -c '^LIVE_ENABLED=' /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
|
||||
|
||||
apt-get install --reinstall -y -qq /packages/candidate.deb \
|
||||
>/tmp/candidate-reinstall.log
|
||||
[[ "$(grep -c '^LIVE_ENABLED=' /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
|
||||
fi
|
||||
|
||||
echo "Trixie package, upgrade, networking, printing and reinstall checks passed."
|
||||
CONTAINER
|
||||
@@ -22,6 +22,22 @@ echo
|
||||
|
||||
"$ROOT/tools/build-release.sh" "$version"
|
||||
|
||||
# Validate the exact artifact that will be published. If a previous canary is
|
||||
# available locally, exercise the real upgrade path as well as a fresh install.
|
||||
validation_args=("$package_path")
|
||||
|
||||
if [[ -f "$ROOT/repo/canary/manifest.json" ]]; then
|
||||
previous_package_name="$(python3 -c \
|
||||
'import json; print(json.load(open("repo/canary/manifest.json"))["package"].rsplit("/", 1)[-1])')"
|
||||
previous_package="$ROOT/repo/packages/$previous_package_name"
|
||||
|
||||
if [[ -f "$previous_package" && "$previous_package" != "$package_path" ]]; then
|
||||
validation_args+=("$previous_package")
|
||||
fi
|
||||
fi
|
||||
|
||||
"$ROOT/tools/validate-trixie-release.sh" "${validation_args[@]}"
|
||||
|
||||
hash="$(sha256sum "$package_path" | awk '{print $1}')"
|
||||
|
||||
mkdir -p "$ROOT/repo/packages" "$ROOT/repo/canary"
|
||||
|
||||
Reference in New Issue
Block a user