Display arrangement persistence + centering fix, packaging hardening

- core/display.py: write_positions() persists Arrangement's drag-and-drop
  layout into niri's config.kdl (one output { position x= y= } per output),
  validated via `niri validate` on a temp copy with a .kdl.bak backup before
  writing — same pattern as core/keybindings.py's rebind(). Previously the
  page only ever called `niri msg output … position set`, which niri treats
  as live-only and drops on the next login/reload.
- gui/pages/display.py: Arrangement's Apply now runs each output's `niri msg
  output … position set` synchronously instead of queuing them all on the
  single-shot ProcessRunner (which rejects a second run() while the first is
  still async) — a 2-monitor apply previously moved only the first output
  and silently dropped the rest. _dock_to_nearest keeps the free axis at the
  dragged position (so a shorter display can sit vertically centered next to
  a taller rotated one) rather than forcing corner alignment.
- core/panel.py, files/__init__.py: incidental fixes alongside the above.
- packaging/: signing-key generation script + build-user systemd setup for
  the [tanin] AUR auto-rebuild pipeline; PKGBUILD bumped to pkgrel=5.
- src/taninux/browser/: new module for browser theme sync (Fuji accent).
This commit is contained in:
2026-07-13 14:19:50 +02:00
parent 8fb8efcbbd
commit faaba27ed4
26 changed files with 1516 additions and 56 deletions
+30 -3
View File
@@ -20,6 +20,14 @@
# TANIN_CHROOT clean chroot location (default ~/.cache/tanin-chroot)
# TANIN_AUR_CACHE AUR clone + state cache (default ~/.cache/tanin-aur)
# TANIN_PUBLISH_CMD upload hook, e.g. an rsync (default empty = local only)
# GPGKEY packager key fingerprint (REQUIRED — see gen-signing-key.sh)
#
# All the above default under $HOME, which under the shipped systemd service
# (User=tanin-build) resolves to /var/lib/tanin-build — so the dedicated build
# user gets its own repo/chroot/cache, isolated from the human account. The
# signing key must be reachable by that same user: either its GNUPGHOME
# (default ~/.gnupg, i.e. /var/lib/tanin-build/.gnupg under the service) holds
# the packager secret key, or GNUPGHOME is pointed at wherever it lives.
set -uo pipefail
REPO_NAME="tanin"
@@ -28,6 +36,7 @@ DB="$OUT/$REPO_NAME.db.tar.zst"
CHROOT="${TANIN_CHROOT:-$HOME/.cache/tanin-chroot}"
CACHE="${TANIN_AUR_CACHE:-$HOME/.cache/tanin-aur}"
PUBLISH="${TANIN_PUBLISH_CMD:-}"
GPGKEY="${GPGKEY:-}"
# AUR-only deps of TANINUX (not in the official repos). Mirrors the list in
# finish-tanin-repo.sh — keep them in sync.
@@ -36,6 +45,13 @@ AUR_PKGS=(eww-git tiramisu-git waypaper calamares librewolf-bin
log() { printf '[%s] %s\n' "$REPO_NAME-aur" "$*"; }
# makechrootpkg builds inside an isolated chroot with no access to the host's
# GPG agent/keyring, so packages are signed here on the host, right after
# they're copied out of the chroot into $OUT — not with makepkg --sign inside it.
sign_pkg() {
gpg --batch --yes --detach-sign --use-agent -u "$GPGKEY" "$1"
}
# --- one-time setup --------------------------------------------------------
setup() {
sudo pacman -S --needed --noconfirm devtools git jq curl
@@ -96,7 +112,11 @@ build_one() {
rm -f "$dir"/*.pkg.tar.zst
# -c = clean copy of the chroot each time; -r = which chroot; -- = makepkg args
if ( cd "$dir" && makechrootpkg -c -r "$CHROOT" -- --noconfirm ); then
cp "$dir"/*.pkg.tar.zst "$OUT"/ && log " ok: $p"
cp "$dir"/*.pkg.tar.zst "$OUT"/ || { log " COPY FAILED: $p"; return 1; }
for f in "$dir"/*.pkg.tar.zst; do
sign_pkg "$OUT/$(basename "$f")" || { log " SIGN FAILED: $p"; return 1; }
done
log " ok: $p (signed)"
built_version "$dir" > "$CACHE/$p.ver" 2>/dev/null || true
return 0
fi
@@ -106,6 +126,13 @@ build_one() {
run() {
[ -d "$CHROOT/root" ] || { log "no chroot — run '$0 setup' first"; exit 1; }
# Fail closed: never build/publish unsigned packages.
if [ -z "$GPGKEY" ]; then
log "GPGKEY is not set — refusing to build unsigned packages."
log "Run packaging/gen-signing-key.sh once (if you haven't), then set GPGKEY"
log "(e.g. Environment=GPGKEY=<fingerprint> in tanin-aur-update.service)."
exit 1
fi
mkdir -p "$OUT" "$CACHE"
local built=0 failed=0
for p in "${AUR_PKGS[@]}"; do
@@ -113,8 +140,8 @@ run() {
done
if [ "$built" -gt 0 ]; then
log "refreshing DB ($built rebuilt)"
repo-add -q "$DB" "$OUT"/*.pkg.tar.zst >/dev/null
log "refreshing DB ($built rebuilt, signed with $GPGKEY)"
repo-add -s -k "$GPGKEY" -q "$DB" "$OUT"/*.pkg.tar.zst >/dev/null
# keep only the newest file per package on disk
command -v paccache >/dev/null && paccache -rq -k1 -c "$OUT" >/dev/null 2>&1 || true
if [ -n "$PUBLISH" ]; then
+15 -3
View File
@@ -16,6 +16,17 @@ DB="$OUT/tanin.db.tar.zst"
LOGDIR="$OUT/logs"
mkdir -p "$OUT" "$LOGDIR"
# Fail closed: never build/publish unsigned packages. Run packaging/gen-signing-key.sh
# once, then `export GPGKEY=<fingerprint>` (or put it in your shell rc) before building.
GPGKEY="${GPGKEY:-}"
if [ -z "$GPGKEY" ]; then
echo "!! GPGKEY is not set — refusing to build unsigned packages." >&2
echo " Run packaging/gen-signing-key.sh once (if you haven't), then:" >&2
echo " export GPGKEY=<packager key fingerprint>" >&2
exit 1
fi
export GPGKEY
# tanin-icons builds from the installed Adwaita theme -> needs librsvg + python.
OWN_SIMPLE=(tanin-greet tanin-libadwaita tanin-setup tanin-eww tanin-icons tanin-desktop)
OWN_BUILD=(taninux)
@@ -27,7 +38,7 @@ AUR_REBUILD=(eww-git tiramisu-git waypaper calamares librewolf-bin
build() {
local p="$1" log="$LOGDIR/$p.log"
echo "==> building $p (log: $log)"
( cd "$HERE/$p" && makepkg -d -f --noconfirm ) >"$log" 2>&1 \
( cd "$HERE/$p" && makepkg -d -f --sign --noconfirm ) >"$log" 2>&1 \
&& cp "$HERE/$p/"*.pkg.tar.zst "$OUT/" 2>/dev/null \
&& echo " ok" || { echo " FAILED ($p) — see $log"; return 1; }
}
@@ -46,11 +57,12 @@ echo "** build logs (incl. makepkg warnings) in: $LOGDIR"
echo "** AUR rebuilds TODO (not in official repos): ${AUR_REBUILD[*]}"
echo " build each with paru/makepkg and copy the .pkg.tar.zst into $OUT, then re-run."
# refresh the DB (unsigned for now -> pacman.conf: SigLevel = Optional TrustAll)
# refresh the DB — packages are signed above (makepkg --sign, key=$GPGKEY);
# sign the DB too so pacman.conf can require Required DatabaseOptional.
shopt -s nullglob
pkgs=("$OUT"/*.pkg.tar.zst)
if (( ${#pkgs[@]} )); then
( cd "$OUT" && repo-add -q "$DB" ./*.pkg.tar.zst >/dev/null 2>&1 )
( cd "$OUT" && repo-add -s -k "$GPGKEY" -q "$DB" ./*.pkg.tar.zst >/dev/null 2>&1 )
echo "== [tanin] repo refreshed at $OUT =="
ls -1 "$OUT" | grep -E '\.pkg\.tar\.zst$|tanin\.db'
else
+15 -4
View File
@@ -11,9 +11,20 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
OUT="${TANIN_REPO_DIR:-$HOME/projects/tanin-repo}"
mkdir -p "$OUT"
# Fail closed: never build/publish unsigned packages. Run packaging/gen-signing-key.sh
# once, then `export GPGKEY=<fingerprint>` before running this script.
GPGKEY="${GPGKEY:-}"
if [ -z "$GPGKEY" ]; then
echo "!! GPGKEY is not set — refusing to build unsigned packages." >&2
echo " Run packaging/gen-signing-key.sh once (if you haven't), then:" >&2
echo " export GPGKEY=<packager key fingerprint>" >&2
exit 1
fi
export GPGKEY
echo "==> taninux (+ build deps)"
sudo pacman -S --needed --noconfirm python-build python-installer python-hatchling
( cd "$HERE/taninux" && makepkg -sf --noconfirm ) \
( cd "$HERE/taninux" && makepkg -sf --sign --noconfirm ) \
&& cp "$HERE/taninux/"*.pkg.tar.zst "$OUT/" && echo " ok" || echo " FAILED (taninux)"
AUR_PKGS="eww-git tiramisu-git waypaper calamares librewolf-bin arch-update timeshift-autosnap xdg-terminal-exec paru"
@@ -22,7 +33,7 @@ tmp="$(mktemp -d)"
for p in $AUR_PKGS; do
echo " -- $p"
if git clone --depth=1 "https://aur.archlinux.org/$p.git" "$tmp/$p" >/dev/null 2>&1 \
&& ( cd "$tmp/$p" && makepkg -sf --noconfirm ); then
&& ( cd "$tmp/$p" && makepkg -sf --sign --noconfirm ); then
cp "$tmp/$p/"*.pkg.tar.zst "$OUT/" && echo " ok"
else
echo " FAILED ($p)"
@@ -30,8 +41,8 @@ for p in $AUR_PKGS; do
done
rm -rf "$tmp"
echo "==> refresh DB"
( cd "$OUT" && repo-add -q "$OUT/tanin.db.tar.zst" "$OUT"/*.pkg.tar.zst )
echo "==> refresh DB (signed with $GPGKEY)"
( cd "$OUT" && repo-add -s -k "$GPGKEY" -q "$OUT/tanin.db.tar.zst" "$OUT"/*.pkg.tar.zst )
echo "== [tanin] now contains =="
ls -1 "$OUT" | grep -E '\.pkg\.tar\.zst$|tanin\.db$'
echo
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Generate the TANINUX packager signing key — THE DISTRO'S ROOT OF TRUST.
#
# Run this ONCE, by the maintainer, on a trusted machine. The resulting key
# signs every [tanin] package (makepkg --sign) and the repo DB (repo-add -s);
# every user who runs `pacman-key --lsign-key <fingerprint>` is trusting this
# key to vouch for everything TANINUX ships. Guard the private key + its
# passphrase like the root of the distro, because that's what it is.
#
# ./gen-signing-key.sh # prompts for Name-Email + a
# # gpg-agent passphrase prompt
# NAME_EMAIL=pkg@taninux.example ./gen-signing-key.sh
#
# This script does NOT set GPGKEY / wire it into the build scripts — that's a
# manual step below, so a compromised shell env can't silently point the
# build at the wrong key.
set -euo pipefail
command -v gpg >/dev/null || { echo "!! gpg not found" >&2; exit 1; }
NAME_REAL="TANINUX Packager"
NAME_EMAIL="${NAME_EMAIL:-}"
EXPIRE="${EXPIRE:-2y}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if [ -z "$NAME_EMAIL" ]; then
read -rp "Packager key Name-Email (placeholder, doesn't need to receive mail): " NAME_EMAIL
fi
[ -n "$NAME_EMAIL" ] || { echo "!! Name-Email is required" >&2; exit 1; }
echo "== generating: $NAME_REAL <$NAME_EMAIL> (Ed25519, expires in $EXPIRE) =="
echo " gpg will prompt (via pinentry/gpg-agent) for a passphrase — set a real one."
PARAMFILE="$(mktemp)"
trap 'rm -f "$PARAMFILE"' EXIT
cat > "$PARAMFILE" <<EOF
%ask-passphrase
Key-Type: eddsa
Key-Curve: ed25519
Key-Usage: sign
Name-Real: $NAME_REAL
Name-Email: $NAME_EMAIL
Expire-Date: $EXPIRE
%commit
EOF
gpg --batch --gen-key "$PARAMFILE"
FPR="$(gpg --list-keys --with-colons "$NAME_EMAIL" 2>/dev/null \
| awk -F: '/^fpr:/ { print $10; exit }')"
if [ -z "$FPR" ]; then
echo "!! key created but the fingerprint could not be auto-detected." >&2
echo " run: gpg --list-keys \"$NAME_EMAIL\"" >&2
exit 1
fi
cat <<EOT
============================================================
TANINUX packager key created
Fingerprint: $FPR
============================================================
Next steps (manual — nothing below runs automatically):
1) Export the PUBLIC key into the target keyring source, so
customize_airootfs.sh's \`pacman-key --add\` picks it up on the next ISO
build. Re-export replaces/updates the file (see the comment left in
customize_airootfs.sh for the matching --lsign-key line to add):
gpg --export "$FPR" > "$ROOT/iso/overrides/airootfs/etc/pacman.d/tanin-repo.gpg"
gpg --export "$FPR" > "$ROOT/iso/build-profile/airootfs/etc/pacman.d/tanin-repo.gpg"
2) Publish the public key for existing/new users to import (see the
Cutover section in docs/distribution.md), e.g.:
gpg --export "$FPR" > /path/to/served/tanin-repo.gpg
and record "$FPR" in docs/distribution.md's Cutover section.
3) Point the build scripts at it (build-tanin-repo.sh, finish-tanin-repo.sh,
aur-autoupdate.sh all read GPGKEY from the environment and refuse to
build unsigned if it's unset):
export GPGKEY="$FPR"
For the systemd-driven daily rebuild (packaging/systemd/tanin-aur-update.service,
running as the dedicated tanin-build user — see setup-build-user.sh),
set Environment=GPGKEY=$FPR in the unit, or an EnvironmentFile it reads,
and make sure GNUPGHOME for tanin-build actually holds this secret key.
4) BACK UP THE PRIVATE KEY (\`gpg --export-secret-keys $FPR\`) to an offline,
encrypted location. Losing it means re-keying the whole distro and
asking every user to re-import; leaking it means anyone can sign
packages TANINUX users will install as trusted.
EOT
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# One-time setup of the dedicated "tanin-build" account for the daily [tanin]
# AUR rebuild, so the root-equivalent makechrootpkg/arch-nspawn/mkarchroot
# sudo grant (tanin-aur-update.sudoers) lives on a locked system account
# instead of the maintainer's own login ("karim"). Run as root (or via sudo)
# on the machine that will run tanin-aur-update.timer.
#
# sudo ./setup-build-user.sh
#
# What it does:
# 1) create the tanin-build system user, home /var/lib/tanin-build, locked
# password (no interactive/SSH login — only systemd + sudo can act as it)
# 2) clone/sync this checkout's packaging scripts into ~tanin-build/taninux,
# because the systemd unit's %h paths resolve against tanin-build's home,
# not /home/karim
# 3) prepare ~tanin-build/.gnupg (mode 700) for the packager signing key
# 4) install the sudoers drop-in + systemd unit/timer
#
# What it deliberately does NOT do:
# - generate or import the signing key (packaging/gen-signing-key.sh is a
# separate, explicit step — key material shouldn't be created as a side
# effect of account provisioning)
# - enable/start the timer (review Environment=GPGKEY=... in the unit first)
set -euo pipefail
[ "$(id -u)" -eq 0 ] || { echo "!! run as root (sudo ./setup-build-user.sh)" >&2; exit 1; }
HERE="$(cd "$(dirname "$0")" && pwd)" # .../packaging/systemd
ROOT="$(cd "$HERE/../.." && pwd)" # taninux checkout root
BUILD_USER="tanin-build"
BUILD_HOME="/var/lib/tanin-build"
echo "==> user: $BUILD_USER (system account, home=$BUILD_HOME)"
if ! id "$BUILD_USER" >/dev/null 2>&1; then
useradd --system --create-home --home-dir "$BUILD_HOME" \
--shell /usr/bin/bash "$BUILD_USER"
passwd --lock "$BUILD_USER" # no password login — only sudo (via the
# NOPASSWD drop-in) and systemd User= can act as it
else
echo " already exists — skipping useradd"
fi
echo "==> syncing packaging scripts to $BUILD_HOME/taninux"
# The systemd unit uses %h-relative paths (ExecStart=%h/taninux/packaging/...),
# which resolve against tanin-build's home — so a copy of the checkout (or at
# least packaging/) has to live there too, owned by tanin-build, not karim.
install -d -o "$BUILD_USER" -g "$BUILD_USER" "$BUILD_HOME/taninux"
rsync -a --delete \
--exclude '.git' --exclude 'iso/out' --exclude 'iso/build-profile' \
"$ROOT/" "$BUILD_HOME/taninux/"
chown -R "$BUILD_USER:$BUILD_USER" "$BUILD_HOME/taninux"
echo " NOTE: re-run this script (or your own sync) after pulling changes —"
echo " it is a one-shot copy, not a live checkout."
echo "==> GPG homedir for the packager key"
install -d -m700 -o "$BUILD_USER" -g "$BUILD_USER" "$BUILD_HOME/.gnupg"
cat <<EOT
$BUILD_HOME/.gnupg is ready but EMPTY. The packager secret key created by
packaging/gen-signing-key.sh must be imported here before the daily
rebuild can sign anything, e.g. (as $BUILD_USER):
sudo -u $BUILD_USER gpg --homedir $BUILD_HOME/.gnupg --import packager-secret.asc
Trust implication: whoever can read $BUILD_HOME/.gnupg's secring can sign
packages as TANINUX — keep its permissions at 700/600 and don't put it on
a shared or less-trusted host than the maintainer's own signing machine.
EOT
echo "==> makepkg env"
install -d -m755 -o "$BUILD_USER" -g "$BUILD_USER" "$BUILD_HOME/.cache" "$BUILD_HOME/.config"
echo "==> installing sudoers drop-in"
install -m440 "$HERE/tanin-aur-update.sudoers" /etc/sudoers.d/tanin-aur-update
visudo -cf /etc/sudoers.d/tanin-aur-update
echo "==> installing systemd unit + timer (SYSTEM units — the .service sets"
echo " User=/Group=$BUILD_USER itself, so it must run under the system"
echo " manager, not --user; that's also what makes the sudo NOPASSWD"
echo " grant for tanin-build actually apply)"
install -m644 "$HERE/tanin-aur-update.service" /etc/systemd/system/
install -m644 "$HERE/tanin-aur-update.timer" /etc/systemd/system/
systemctl daemon-reload
cat <<EOT
== setup done ==
Still TODO before the timer can run for real:
1) packaging/gen-signing-key.sh (if not already done), then import the
secret key into $BUILD_HOME/.gnupg as shown above.
2) sudo systemctl edit tanin-aur-update.service
and set Environment=GPGKEY=<fingerprint> (or uncomment it in
/etc/systemd/system/tanin-aur-update.service directly).
3) sudo -u $BUILD_USER $BUILD_HOME/taninux/packaging/aur-autoupdate.sh setup
(one-time: installs devtools + creates the makechrootpkg chroot —
needs the sudo grant just installed, so run this AFTER step 4 too)
4) sudo systemctl enable --now tanin-aur-update.timer
EOT
+15 -3
View File
@@ -1,13 +1,25 @@
[Unit]
Description=TANINUX [tanin] repo — daily AUR rebuild
Documentation=file:%h/projects/taninux/packaging/aur-autoupdate.sh
Documentation=file:%h/taninux/packaging/aur-autoupdate.sh
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
# Adjust the path if your checkout lives elsewhere.
ExecStart=%h/projects/taninux/packaging/aur-autoupdate.sh run
# Dedicated, locked build account (see setup-build-user.sh) — NOT the human
# "karim" account. Isolates the root-equivalent makechrootpkg/arch-nspawn/
# mkarchroot sudo grant (tanin-aur-update.sudoers) off the personal login.
User=tanin-build
Group=tanin-build
# %h now resolves against tanin-build's home (/var/lib/tanin-build by
# setup-build-user.sh), NOT /home/karim — so this checkout must live there
# too (setup-build-user.sh clones/syncs it in). Adjust if it lives elsewhere.
ExecStart=%h/taninux/packaging/aur-autoupdate.sh run
# The packager signing key: GPGKEY must be set, and GNUPGHOME (default
# %h/.gnupg = /var/lib/tanin-build/.gnupg) must actually contain that secret
# key for tanin-build — see gen-signing-key.sh + setup-build-user.sh.
# Environment=GPGKEY=<packager key fingerprint, from gen-signing-key.sh>
# Environment=GNUPGHOME=%h/.gnupg # only needed if it's not already the default
# Be a good citizen — this is a background rebuild, not interactive work.
Nice=15
IOSchedulingClass=idle
+9 -3
View File
@@ -2,6 +2,12 @@
# Install: sudo install -m440 tanin-aur-update.sudoers /etc/sudoers.d/tanin-aur-update
# Validate: sudo visudo -cf /etc/sudoers.d/tanin-aur-update
#
# Replace "karim" if the timer runs as a different user. These are exactly the
# helpers devtools' makechrootpkg shells out to as root; nothing broader.
karim ALL=(root) NOPASSWD: /usr/bin/makechrootpkg, /usr/bin/arch-nspawn, /usr/bin/mkarchroot
# Subject is "tanin-build", a dedicated, locked (no password login) system
# account created by setup-build-user.sh — NOT the human "karim" account.
# makechrootpkg/arch-nspawn/mkarchroot are effectively unrestricted root (they
# bind-mount, chroot, and run arbitrary PKGBUILD-controlled commands as root),
# so this NOPASSWD grant is root-equivalent. Keeping it on a dedicated build
# account rather than the personal login means a compromised AUR PKGBUILD (or
# a bug in this pipeline) can't NOPASSWD-root the maintainer's own account —
# it's contained to whatever tanin-build can already reach.
tanin-build ALL=(root) NOPASSWD: /usr/bin/makechrootpkg, /usr/bin/arch-nspawn, /usr/bin/mkarchroot
+1 -1
View File
@@ -5,7 +5,7 @@
# Vor Publish: url + license bestätigen, source auf GitHub-Tag setzen.
pkgname=taninux
pkgver=0.2.0
pkgrel=2
pkgrel=5
pkgdesc="Central Linux management for Arch — GTK System Settings, Software Hub and TUI"
arch=('any')
url="https://taninux.kgva.ch"