From 1fab01140c3f0623ce07a1fb53e5ffd9ff6cd292 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 23 Jun 2026 13:08:38 +0000 Subject: [PATCH 01/28] chore: bump version after release --- splitway-daemon/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitway-daemon/Cargo.toml b/splitway-daemon/Cargo.toml index d579af2..64ee451 100755 --- a/splitway-daemon/Cargo.toml +++ b/splitway-daemon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "splitway-daemon" -version = "0.0.5" +version = "0.0.6" edition = "2021" [dependencies] From 0f06b101c667828a5b8cc0f8f5a1d5ce4a84b857 Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 24 Jun 2026 22:47:52 +0300 Subject: [PATCH 02/28] feat(packaging): point unit at StateDirectory config + set GUI Wayland app_id Prepares the systemd unit and the egui GUI for the deb/rpm/pacman packages. - splitway.service: ExecStart now passes `--config /var/lib/splitway/config.json` and the unit declares StateDirectory=splitway (0700), mirroring nix/module.nix. The daemon creates its config under the persistent, daemon-owned state dir on first run instead of falling back to /root/.config (and logging a warning). - Add the same --config to the commented socket-group opt-in ExecStart override. The bare `ExecStart=` reset fully replaces the command, so without this an opt-in user would silently drop --config and reintroduce the /root/.config fallback this commit removes. (Minor deviation from the "keep the block untouched" plan, for correctness/consistency with the main ExecStart and nix/module.nix.) - splitway-gui: set the ViewportBuilder app_id to io.github.stslex.splitway so Wayland compositors map the window to the packaged .desktop entry + hicolor icon (shipped under that basename by the GUI package). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018GKGiqawfPb5fzMGHisrrF --- packaging/systemd/splitway.service | 14 ++++++++++++-- splitway-gui/src/app.rs | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packaging/systemd/splitway.service b/packaging/systemd/splitway.service index 45becc1..ae6327d 100644 --- a/packaging/systemd/splitway.service +++ b/packaging/systemd/splitway.service @@ -10,7 +10,10 @@ Wants=network-online.target Type=simple # Installed to /usr/bin by deb/rpm/pacman packages. (The Nix build wires # its own ExecStart via the package store path; see nix/module.nix.) -ExecStart=/usr/bin/splitway-daemon run +# `--config` points the daemon at the persistent StateDirectory below so it +# creates its config there on first run, instead of falling back to +# /root/.config and logging a warning (mirrors nix/module.nix). +ExecStart=/usr/bin/splitway-daemon run --config /var/lib/splitway/config.json Restart=on-failure RestartSec=2 @@ -20,6 +23,13 @@ RestartSec=2 RuntimeDirectory=splitway RuntimeDirectoryMode=0700 +# Persistent state dir /var/lib/splitway (0700, root), created by systemd +# before start and preserved across restarts. The daemon writes/reads its +# config at /config.json (see --config above); on first run it creates +# an empty config there. Mirrors `StateDirectory` in nix/module.nix. +StateDirectory=splitway +StateDirectoryMode=0700 + # Unprivileged GUI/CLI access (opt-in): let a non-root user in a dedicated group # drive the daemon without sudo. To enable, create + join the group # (`groupadd splitway`, `usermod -aG splitway `) and uncomment all four @@ -29,7 +39,7 @@ RuntimeDirectoryMode=0700 # membership ≈ control of system split-DNS routing (see packaging/README.md, # "Socket security model"). Default is root-only. #ExecStart= -#ExecStart=/usr/bin/splitway-daemon run --socket-group splitway +#ExecStart=/usr/bin/splitway-daemon run --config /var/lib/splitway/config.json --socket-group splitway #RuntimeDirectoryMode=0750 #SupplementaryGroups=splitway diff --git a/splitway-gui/src/app.rs b/splitway-gui/src/app.rs index 7551c39..4906329 100644 --- a/splitway-gui/src/app.rs +++ b/splitway-gui/src/app.rs @@ -34,7 +34,11 @@ pub fn run() -> eframe::Result<()> { viewport: egui::ViewportBuilder::default() .with_inner_size([540.0, 720.0]) .with_min_inner_size([420.0, 480.0]) - .with_title("Splitway"), + .with_title("Splitway") + // Wayland app_id: maps the window to the installed + // `io.github.stslex.splitway.desktop` entry + hicolor icon (the deb/ + // rpm/pacman GUI package ships both under that basename). + .with_app_id("io.github.stslex.splitway"), ..Default::default() }; eframe::run_native( From 9a1305448bd57236e91606e202577aa481b685d1 Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 24 Jun 2026 22:55:11 +0300 Subject: [PATCH 03/28] feat(packaging): define the musl-static `splitway` core deb/rpm package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the MIT LICENSE (the repo had none) and declares `license = "MIT"` on every workspace crate, then defines the deb + rpm metadata for the core `splitway` package on splitway-daemon. The package ships both binaries — `splitway-daemon` and the `splitway` CLI (from splitway-cli) — plus the systemd unit and README/LICENSE. It is built musl-static (*-unknown-linux-musl) so it has no shared-library dependencies and installs on any glibc/musl baseline; the desktop GUI is a separate package (next commit) that Depends on this one. - [package.metadata.deb]: name=splitway, binaries to /usr/bin, copyright from the MIT text, Recommends network-manager + systemd-resolved (not Depends), empty Depends (static). An (empty) maintainer-scripts dir enables cargo-deb's systemd-units integration to generate the postinst/postrm: enable+start on install, restart on upgrade, stop on remove, daemon-reload. - [package.metadata.generate-rpm]: same layout, auto-req disabled (static), weak-dep Recommends, raw /bin/sh systemd scriptlets (cargo-generate-rpm does not expand %systemd_* macros). Asset-path note (verified by building both packages with cargo-deb 3.7.0 and cargo-generate-rpm 0.21.0): cargo-deb resolves non-`target/` asset paths relative to THIS crate's manifest dir (so workspace-root files use `../`), while cargo-generate-rpm resolves relative to the invocation dir (workspace root, bare paths) — hence the intentional path skew between the two blocks. Version is stamped at the packaging layer (cargo deb --deb-version / cargo generate-rpm --set-metadata) so dev builds get ~dev.. without a non-semver string in Cargo.toml. CI builds per-triple with --target (remaps target/release -> target//release and stamps the arch). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018GKGiqawfPb5fzMGHisrrF --- LICENSE | 21 ++++ packaging/deb-maintainer-scripts/.gitkeep | 5 + splitway-cli/Cargo.toml | 2 + splitway-daemon/Cargo.toml | 111 ++++++++++++++++++++++ splitway-gui-core/Cargo.toml | 2 + splitway-gui-tauri/Cargo.toml | 1 + splitway-gui/Cargo.toml | 2 + splitway-shared/Cargo.toml | 2 + 8 files changed, 146 insertions(+) create mode 100644 LICENSE create mode 100644 packaging/deb-maintainer-scripts/.gitkeep diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b51e966 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 stslex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packaging/deb-maintainer-scripts/.gitkeep b/packaging/deb-maintainer-scripts/.gitkeep new file mode 100644 index 0000000..44854f4 --- /dev/null +++ b/packaging/deb-maintainer-scripts/.gitkeep @@ -0,0 +1,5 @@ +# cargo-deb requires a maintainer-scripts directory to exist for its +# systemd-units integration to GENERATE the postinst/postrm enable/start/ +# restart/stop scriptlets (dh_installsystemd style). This directory is +# intentionally otherwise empty: cargo-deb writes the scripts from scratch. +# (.gitkeep tracks the dir; cargo-deb ignores all but the known script names.) diff --git a/splitway-cli/Cargo.toml b/splitway-cli/Cargo.toml index 980e910..0c37981 100755 --- a/splitway-cli/Cargo.toml +++ b/splitway-cli/Cargo.toml @@ -2,6 +2,8 @@ name = "splitway-cli" version = "0.0.1" edition = "2021" +license = "MIT" +description = "Splitway CLI — IPC client over the daemon's control socket (installs as `splitway`)" # The user-facing command is `splitway` (matching the clap name and the docs), # even though the crate is `splitway-cli`. diff --git a/splitway-daemon/Cargo.toml b/splitway-daemon/Cargo.toml index d579af2..833e31f 100755 --- a/splitway-daemon/Cargo.toml +++ b/splitway-daemon/Cargo.toml @@ -2,6 +2,8 @@ name = "splitway-daemon" version = "0.0.5" edition = "2021" +license = "MIT" +description = "Domain-based split-DNS daemon: route selected domains through the VPN's DNS, everything else direct" [dependencies] splitway-shared = { path = "../splitway-shared" } @@ -42,3 +44,112 @@ zbus = { version = "5", default-features = false, features = ["tokio"] } [target.'cfg(target_os = "macos")'.dependencies] system-configuration = "0.7" core-foundation = "0.9" + +# --------------------------------------------------------------------------- +# Distribution packaging — the `splitway` core package (daemon + CLI + unit). +# +# This crate's metadata defines the deb/rpm package named `splitway` that ships +# BOTH binaries (`splitway-daemon` and the `splitway` CLI built from +# splitway-cli), the systemd unit, and the README/LICENSE. It is built +# musl-static (*-unknown-linux-musl) so it carries no shared-library deps and +# runs on any glibc/musl baseline. The desktop GUI is a SEPARATE package +# (splitway-gui/Cargo.toml) that Depends on this one. +# See docs/design/linux-distro-packaging.md. +# +# CI builds both binaries for the target triple, then runs `cargo deb` / +# `cargo generate-rpm` with `--target `, which remaps the +# `target/release/...` asset paths below to `target//release/...` and +# stamps the package architecture. Version is stamped at the packaging layer +# (`cargo deb --deb-version` / `cargo generate-rpm --set-metadata version=...`) +# so dev builds get `~dev..` without putting a non-semver string +# in this file. +# --------------------------------------------------------------------------- + +[package.metadata.deb] +name = "splitway" +maintainer = "stslex " +copyright = "2026, stslex" +# Generates /usr/share/doc/splitway/copyright from the MIT text (skip 0 header +# lines). This is the Debian-policy license file, so LICENSE is not shipped +# again as a separate asset (avoids lintian extra-license-file). +# NOTE: cargo-deb resolves non-`target/` paths relative to THIS crate's +# manifest dir, so workspace-root files need `../` (verified with cargo-deb +# 3.7.0). cargo-generate-rpm instead resolves relative to the invocation dir +# (workspace root), so its block below uses bare paths — intentional skew. +license-file = ["../LICENSE", "0"] +section = "net" +priority = "optional" +extended-description = """\ +Splitway routes selected domains through the VPN's DNS while everything else \ +resolves directly. The root daemon detects the VPN (NetworkManager or a \ +standalone OpenVPN management interface) and applies/reverts split-DNS rules \ +via systemd-resolved; the splitway CLI drives it over a 0600 Unix socket. This \ +package contains the daemon, the CLI, and the systemd service. Install \ +splitway-gui for the desktop GUI.""" +# musl-static binary => no dynamic library dependencies (verified in CI with +# `file`). NetworkManager and systemd-resolved are runtime prerequisites for +# actually applying rules, but stay Recommends (not Depends) so the package +# installs on hosts using other resolvers or for inspection. +depends = "" +recommends = "network-manager, systemd-resolved" +# Required for the systemd-units block below to actually GENERATE the +# postinst/postrm enable/start/restart/stop scriptlets (cargo-deb's +# dh_installsystemd integration only runs when maintainer-scripts is set). The +# directory is empty, so cargo-deb writes the scripts from scratch. +maintainer-scripts = "../packaging/deb-maintainer-scripts" +assets = [ + ["target/release/splitway-daemon", "usr/bin/", "755"], + ["target/release/splitway", "usr/bin/", "755"], + ["../README.md", "usr/share/doc/splitway/README.md", "644"], +] + +# cargo-deb's systemd integration installs packaging/systemd/splitway.service +# and generates the maintainer scripts: daemon-reload + enable + start on first +# install, restart after upgrade, stop + disable on removal. (The unit is NOT +# listed in `assets` above — this block installs it.) +[package.metadata.deb.systemd-units] +unit-name = "splitway" +unit-scripts = "../packaging/systemd" +enable = true +start = true +restart-after-upgrade = true + +[package.metadata.generate-rpm] +name = "splitway" +summary = "Domain-based split-DNS daemon and CLI" +# Static musl binary => disable the automatic dependency scan, which would +# otherwise add interpreter/rpmlib requires that a static binary does not need. +auto-req = "no" +# systemd scriptlets. cargo-generate-rpm builds the RPM directly, so these run +# as raw /bin/sh (default RPM scriptlet interpreter) — RPM macros like +# %systemd_post are NOT expanded, hence the explicit systemctl calls. $1 is the +# RPM scriptlet count: %post 1=install 2=upgrade; %preun/%postun 0=erase +# 1=upgrade. Keys precede the [.recommends] sub-table per TOML ordering rules. +post_install_script = """ +systemctl daemon-reload >/dev/null 2>&1 || : +if [ "$1" = 1 ]; then + systemctl enable --now splitway.service >/dev/null 2>&1 || : +else + systemctl try-restart splitway.service >/dev/null 2>&1 || : +fi +""" +pre_uninstall_script = """ +if [ "$1" = 0 ]; then + systemctl --no-reload disable --now splitway.service >/dev/null 2>&1 || : +fi +""" +post_uninstall_script = """ +systemctl daemon-reload >/dev/null 2>&1 || : +""" +assets = [ + { source = "target/release/splitway-daemon", dest = "/usr/bin/splitway-daemon", mode = "755" }, + { source = "target/release/splitway", dest = "/usr/bin/splitway", mode = "755" }, + { source = "packaging/systemd/splitway.service", dest = "/usr/lib/systemd/system/splitway.service", mode = "644" }, + { source = "README.md", dest = "/usr/share/doc/splitway/README.md", mode = "644", doc = true }, + { source = "LICENSE", dest = "/usr/share/licenses/splitway/LICENSE", mode = "644", doc = true }, +] + +# Weak deps (RPM Recommends) — same rationale as the deb Recommends above. +[package.metadata.generate-rpm.recommends] +NetworkManager = "*" +systemd-resolved = "*" diff --git a/splitway-gui-core/Cargo.toml b/splitway-gui-core/Cargo.toml index 46f79d4..d220c64 100644 --- a/splitway-gui-core/Cargo.toml +++ b/splitway-gui-core/Cargo.toml @@ -2,6 +2,8 @@ name = "splitway-gui-core" version = "0.0.1" edition = "2021" +license = "MIT" +description = "Framework-agnostic Splitway GUI view-model and truth-contract orchestration" # Framework-agnostic GUI logic: the view-model and the truth-contract # orchestration (connection state, reconnect-edge refetch, reply folding, diff --git a/splitway-gui-tauri/Cargo.toml b/splitway-gui-tauri/Cargo.toml index 689cf65..fc56c65 100644 --- a/splitway-gui-tauri/Cargo.toml +++ b/splitway-gui-tauri/Cargo.toml @@ -2,6 +2,7 @@ name = "splitway-gui-tauri" version = "0.0.1" edition = "2021" +license = "MIT" description = "Tauri desktop shell for Splitway — a thin, read-only renderer of splitway-gui-core's view-model." # Deliberately excluded from the workspace `default-members` (see the root diff --git a/splitway-gui/Cargo.toml b/splitway-gui/Cargo.toml index bddd19f..72f1a43 100644 --- a/splitway-gui/Cargo.toml +++ b/splitway-gui/Cargo.toml @@ -2,6 +2,8 @@ name = "splitway-gui" version = "0.0.1" edition = "2021" +license = "MIT" +description = "Splitway interim egui GUI — a privilege-free IPC client over the daemon socket" [[bin]] name = "splitway-gui" diff --git a/splitway-shared/Cargo.toml b/splitway-shared/Cargo.toml index 18e88a8..60a0b34 100755 --- a/splitway-shared/Cargo.toml +++ b/splitway-shared/Cargo.toml @@ -2,6 +2,8 @@ name = "splitway-shared" version = "0.0.1" edition = "2021" +license = "MIT" +description = "Splitway shared types, config parsing, and platform traits" [dependencies] serde = { version = "1", features = ["derive"] } From 2e75c5048844c80b359fddaa5c8785c656a20785 Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 24 Jun 2026 23:16:44 +0300 Subject: [PATCH 04/28] feat(packaging): add the splitway-gui deb/rpm package + socket-group opt-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A separate `splitway-gui` package for the egui desktop GUI (glibc, dynamic), which Depends on the musl-static core `splitway` (>=, the IPC compat contract). - .desktop (io.github.stslex.splitway.desktop, Exec=splitway-gui) + hicolor icons rasterized from assets/icon/splitway-icon.svg (8 PNG sizes + scalable SVG, basename = app_id) via packaging/icons/generate-hicolor.sh. The icon tree is committed so every packaging path ships it without a rasterizer; the .desktop passes desktop-file-validate. - Dependencies (both formats, verified by building real packages): the eframe/glow windowing libs are DLOPEN'd by winit/glow at runtime, so they are absent from the ELF DT_NEEDED and neither cargo-deb's $auto nor cargo-generate-rpm's auto-req can detect them — they MUST be hardcoded. deb: libgl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libxkbcommon0, libc6 (>= 2.31 floor). rpm: mesa-libGL, libX11, libXcursor, libXi, libXrandr, libwayland-client, libxkbcommon (auto-req still pins the glibc floor from libc/libgcc sonames). Recommends an XDG desktop portal + backend for rfd's file dialog. - Socket-group opt-in (security-sensitive): the maintainer scripts create an EMPTY `splitway` group and install a service drop-in switching the daemon to group-socket mode (0660 root:splitway, dir 0750). EMPTY-GROUP INVARIANT: with no members the posture is identical to the default 0600 root-only; the scripts NEVER add a user — the only grant is a human running `usermod -aG splitway ` + re-login. postinst installs the drop-in + reloads + restarts; postrm removes it, groupdel only if empty, reverting to root-only. A loud first-install message prints the exact opt-in one-liner. Validated by building both packages with cargo-deb 3.7.0 / cargo-generate-rpm 0.21.0 and inspecting deps, files, and scriptlets. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018GKGiqawfPb5fzMGHisrrF --- .../desktop/io.github.stslex.splitway.desktop | 16 ++ packaging/gui-maintainer-scripts/postinst | 61 ++++++++ packaging/gui-maintainer-scripts/postrm | 39 +++++ packaging/gui/socket-group.conf | 23 +++ packaging/icons/generate-hicolor.sh | 48 ++++++ .../apps/io.github.stslex.splitway.png | Bin 0 -> 3385 bytes .../16x16/apps/io.github.stslex.splitway.png | Bin 0 -> 432 bytes .../24x24/apps/io.github.stslex.splitway.png | Bin 0 -> 615 bytes .../apps/io.github.stslex.splitway.png | Bin 0 -> 7158 bytes .../32x32/apps/io.github.stslex.splitway.png | Bin 0 -> 855 bytes .../48x48/apps/io.github.stslex.splitway.png | Bin 0 -> 1265 bytes .../apps/io.github.stslex.splitway.png | Bin 0 -> 15385 bytes .../64x64/apps/io.github.stslex.splitway.png | Bin 0 -> 1695 bytes .../apps/io.github.stslex.splitway.svg | 12 ++ splitway-gui/Cargo.toml | 148 ++++++++++++++++++ 15 files changed, 347 insertions(+) create mode 100644 packaging/desktop/io.github.stslex.splitway.desktop create mode 100755 packaging/gui-maintainer-scripts/postinst create mode 100755 packaging/gui-maintainer-scripts/postrm create mode 100644 packaging/gui/socket-group.conf create mode 100755 packaging/icons/generate-hicolor.sh create mode 100644 packaging/icons/hicolor/128x128/apps/io.github.stslex.splitway.png create mode 100644 packaging/icons/hicolor/16x16/apps/io.github.stslex.splitway.png create mode 100644 packaging/icons/hicolor/24x24/apps/io.github.stslex.splitway.png create mode 100644 packaging/icons/hicolor/256x256/apps/io.github.stslex.splitway.png create mode 100644 packaging/icons/hicolor/32x32/apps/io.github.stslex.splitway.png create mode 100644 packaging/icons/hicolor/48x48/apps/io.github.stslex.splitway.png create mode 100644 packaging/icons/hicolor/512x512/apps/io.github.stslex.splitway.png create mode 100644 packaging/icons/hicolor/64x64/apps/io.github.stslex.splitway.png create mode 100644 packaging/icons/hicolor/scalable/apps/io.github.stslex.splitway.svg diff --git a/packaging/desktop/io.github.stslex.splitway.desktop b/packaging/desktop/io.github.stslex.splitway.desktop new file mode 100644 index 0000000..d2270b4 --- /dev/null +++ b/packaging/desktop/io.github.stslex.splitway.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=Splitway +GenericName=Split-DNS controller +Comment=Route selected domains through the VPN's DNS, everything else direct +# The egui binary shipped by the splitway-gui deb/rpm/pacman package. Matches +# the app_id set on the ViewportBuilder (splitway-gui/src/app.rs) so Wayland +# compositors map the window to this entry and the hicolor icon below. +Exec=splitway-gui +Icon=io.github.stslex.splitway +Terminal=false +Categories=Network; +Keywords=DNS;VPN;split-dns;split; +StartupNotify=true +StartupWMClass=io.github.stslex.splitway diff --git a/packaging/gui-maintainer-scripts/postinst b/packaging/gui-maintainer-scripts/postinst new file mode 100755 index 0000000..9366bb0 --- /dev/null +++ b/packaging/gui-maintainer-scripts/postinst @@ -0,0 +1,61 @@ +#!/bin/sh +# splitway-gui postinst — opt-in socket-group plumbing. +# +# EMPTY-GROUP INVARIANT: this creates an EMPTY `splitway` group and installs the +# socket-group drop-in. With no members the drop-in is a no-op and the socket +# posture is identical to the default 0600 root-only. This script NEVER adds a +# user — the only grant is a human running `usermod -aG splitway ` and +# re-logging in. Group membership is equivalent to control of system split-DNS. +set -e + +DROPIN_DIR=/etc/systemd/system/splitway.service.d +DROPIN="$DROPIN_DIR/socket-group.conf" + +case "$1" in + configure) + # 1. Ensure the (empty) system group exists. + getent group splitway >/dev/null 2>&1 || groupadd --system splitway + + # 2. Install the drop-in that switches the daemon to group-socket mode. + install -d -m 0755 "$DROPIN_DIR" + install -m 0644 /usr/share/splitway/socket-group.conf "$DROPIN" + + # 3. Apply: reload units and restart the daemon if it is running. + if [ -d /run/systemd/system ]; then + systemctl daemon-reload >/dev/null 2>&1 || true + systemctl try-restart splitway.service >/dev/null 2>&1 || true + fi + + # 4. Refresh desktop + icon caches (best-effort). + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true + fi + if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || true + fi + + # 5. Loudly tell the human how to actually opt in (first install only). + if [ -z "$2" ]; then + cat <<'EOM' + + ============================================================================ + splitway-gui installed. + + The daemon's control socket is still ROOT-ONLY. To let your user drive + Splitway from the GUI/CLI without sudo, join the `splitway` group and + re-login: + + sudo usermod -aG splitway "$USER" + # then log out and back in (or reboot) + + Until a user is added the `splitway` group is empty and the socket posture + is identical to the default 0600 root-only. Group membership is equivalent + to control of system split-DNS routing. + ============================================================================ + +EOM + fi + ;; +esac + +exit 0 diff --git a/packaging/gui-maintainer-scripts/postrm b/packaging/gui-maintainer-scripts/postrm new file mode 100755 index 0000000..5ae232e --- /dev/null +++ b/packaging/gui-maintainer-scripts/postrm @@ -0,0 +1,39 @@ +#!/bin/sh +# splitway-gui postrm — undo the socket-group opt-in, reverting the daemon to +# the default root-only (0600) socket. +set -e + +DROPIN_DIR=/etc/systemd/system/splitway.service.d +DROPIN="$DROPIN_DIR/socket-group.conf" + +case "$1" in + remove|purge) + # Remove the drop-in so the daemon reverts to the default 0600 root socket. + rm -f "$DROPIN" + rmdir --ignore-fail-on-non-empty "$DROPIN_DIR" 2>/dev/null || true + + # Drop the group ONLY if it has no members (preserve any opt-in users). + if getent group splitway >/dev/null 2>&1; then + members=$(getent group splitway | awk -F: '{print $4}') + if [ -z "$members" ]; then + groupdel splitway >/dev/null 2>&1 || true + fi + fi + + # Reapply: reload + restart the (still-installed) daemon back to root-only. + if [ -d /run/systemd/system ]; then + systemctl daemon-reload >/dev/null 2>&1 || true + systemctl try-restart splitway.service >/dev/null 2>&1 || true + fi + + # Refresh desktop + icon caches (best-effort). + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true + fi + if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || true + fi + ;; +esac + +exit 0 diff --git a/packaging/gui/socket-group.conf b/packaging/gui/socket-group.conf new file mode 100644 index 0000000..7d351d1 --- /dev/null +++ b/packaging/gui/socket-group.conf @@ -0,0 +1,23 @@ +# Splitway socket-group opt-in drop-in. +# +# Shipped by the splitway-gui package to /usr/share/splitway/socket-group.conf +# and copied by its postinst into +# /etc/systemd/system/splitway.service.d/socket-group.conf +# (removed again by its postrm). It switches the daemon's control socket to +# group mode — 0660 root:splitway, runtime dir 0750 — so an in-group user can +# drive the daemon without sudo (the unprivileged-GUI deployment). Mirrors the +# commented opt-in block in packaging/systemd/splitway.service and the ExecStart +# built by nix/module.nix. +# +# EMPTY-GROUP INVARIANT: the package creates an EMPTY `splitway` group. With no +# members this drop-in is a NO-OP — the posture is identical to the default +# 0600 root-only socket. The ONLY way to grant access is a human running +# `usermod -aG splitway ` and re-logging in; the maintainer scripts never +# add a user. Group membership is equivalent to control of system split-DNS. +[Service] +# The bare ExecStart= clears the unit's default command so the next line +# replaces (not appends to) it. +ExecStart= +ExecStart=/usr/bin/splitway-daemon run --config /var/lib/splitway/config.json --socket-group splitway +RuntimeDirectoryMode=0750 +SupplementaryGroups=splitway diff --git a/packaging/icons/generate-hicolor.sh b/packaging/icons/generate-hicolor.sh new file mode 100755 index 0000000..3005205 --- /dev/null +++ b/packaging/icons/generate-hicolor.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Rasterize the Splitway app icon into a freedesktop hicolor theme tree, named +# by the app_id (io.github.stslex.splitway) so the .desktop `Icon=` key and the +# GUI's Wayland app_id both resolve to it. +# +# The generated tree (packaging/icons/hicolor/) is committed so the deb/rpm/ +# pacman packages and the tarballs can ship icons without a rasterizer in their +# build jobs. Re-run this after editing assets/icon/splitway-icon.svg: +# +# packaging/icons/generate-hicolor.sh # writes ./hicolor +# packaging/icons/generate-hicolor.sh /some/dir # writes /some/dir/hicolor +# +# Requires rsvg-convert (librsvg); falls back to ImageMagick magick/convert. +# The sizes mirror the Nix flake's icon derivation (see flake.nix). +set -euo pipefail + +APP_ID="io.github.stslex.splitway" +SIZES=(16 24 32 48 64 128 256 512) + +here="$(cd "$(dirname "$0")" && pwd)" +repo_root="$(cd "$here/../.." && pwd)" +src="$repo_root/assets/icon/splitway-icon.svg" +dest="${1:-$here}" +hic="$dest/hicolor" + +[ -f "$src" ] || { echo "error: source icon not found: $src" >&2; exit 1; } + +raster() { # + if command -v rsvg-convert >/dev/null 2>&1; then + rsvg-convert -w "$1" -h "$1" "$src" -o "$2" + elif command -v magick >/dev/null 2>&1; then + magick -background none "$src" -resize "${1}x${1}" "$2" + elif command -v convert >/dev/null 2>&1; then + convert -background none "$src" -resize "${1}x${1}" "$2" + else + echo "error: need rsvg-convert (librsvg) or ImageMagick to rasterize" >&2 + exit 1 + fi +} + +install -d "$hic/scalable/apps" +cp "$src" "$hic/scalable/apps/$APP_ID.svg" +for s in "${SIZES[@]}"; do + install -d "$hic/${s}x${s}/apps" + raster "$s" "$hic/${s}x${s}/apps/$APP_ID.png" +done + +echo "icons -> $hic" diff --git a/packaging/icons/hicolor/128x128/apps/io.github.stslex.splitway.png b/packaging/icons/hicolor/128x128/apps/io.github.stslex.splitway.png new file mode 100644 index 0000000000000000000000000000000000000000..d5af8801eab9ce79a262f3dff64603387948e175 GIT binary patch literal 3385 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 P00000NkvXXu0mjfj2JAq literal 0 HcmV?d00001 diff --git a/packaging/icons/hicolor/16x16/apps/io.github.stslex.splitway.png b/packaging/icons/hicolor/16x16/apps/io.github.stslex.splitway.png new file mode 100644 index 0000000000000000000000000000000000000000..d75157431ca598bea5909dc7425095f8e21738ff GIT binary patch literal 432 zcmV;h0Z;ykP)6^_JqZo#%ajhvzvQ1;8{jVOv)> zkW2xs{I9%(xIjcVV}Yb%nwhYzsaHUI9k5zi&2~nFt*aYNzyKjzSJ%Z?>}EABsNWfY zaGy$a+0XEh{$ol4q7m3I6c*-u^mMCCjOnhK=Cy2D^7|YL3Kr*mban{N5*EiNg}*PD zj2O($w2*!(lgpP01qH!2f#XQFciy>Yhjs+!e2Vl@nKeqr&La#%C{balEB&k9~jTw3(^s#qnruK{@) z_zQ)CWOx7FTiDnpxvbU5GmTAbG-t3;uWTXZl0Mpa!eQwZr1&r(w>X&@L zsZXTS@q(mMIxObK;y%K+CA!*kT4CDB4!{9UD$~BN1U=OOH~<|| z4=J>UFJJ2{yr}-aptn~sej$uq)O`4~wLNZ`!thX#^JhauB7);+vh!6wFFU@Q@f6&? zWpV6ivuKyc!k!E5qUJ%i%(w5ZFWgtKQ(PBHA6=WY(7`qsz_}4G0R5W!L z&YcNybv!~SD7^o;<%QT;@N=WVljq(=19<(W#)jQs{89uFSj_oLTIwQz+)}f!*RGf( zMuW^ht@2~N@z1=r4q9%>p;T@#&~I>J(0i{ff5D$TUZ^s7+#r|tZdC&)(Z+qWy5_RF z=F?4rt$>wQ;k!n&qR{fNQ+z?@l-}%~1@ax>=MPxjvnrF3TEMAK?6V{vaOx8?GqR-s z(&>01X7wZlNdfO@7JjcOX?QD0DrWU0(&=~s;1|0Huj-G*jM)GH002ovPDHLkV1fkN BC^7&5 literal 0 HcmV?d00001 diff --git a/packaging/icons/hicolor/256x256/apps/io.github.stslex.splitway.png b/packaging/icons/hicolor/256x256/apps/io.github.stslex.splitway.png new file mode 100644 index 0000000000000000000000000000000000000000..efe1dbc175ea66afb9d9bb135eb900cbd40b7ab7 GIT binary patch literal 7158 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 literal 0 HcmV?d00001 diff --git a/packaging/icons/hicolor/48x48/apps/io.github.stslex.splitway.png b/packaging/icons/hicolor/48x48/apps/io.github.stslex.splitway.png new file mode 100644 index 0000000000000000000000000000000000000000..007a3df8576b44f5fa0e4e89e2e184ec5ecb1774 GIT binary patch literal 1265 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 pz)2 + + + + + + + + + + + diff --git a/splitway-gui/Cargo.toml b/splitway-gui/Cargo.toml index 72f1a43..8495278 100644 --- a/splitway-gui/Cargo.toml +++ b/splitway-gui/Cargo.toml @@ -30,3 +30,151 @@ eframe = { version = "0.34", default-features = false, features = [ rfd = "0.15" log = "0.4.20" env_logger = "0.11" + +# --------------------------------------------------------------------------- +# Distribution packaging — the `splitway-gui` desktop package (egui, glibc). +# +# A SEPARATE package from the core `splitway` (splitway-daemon/Cargo.toml): it +# Depends on it (`>=`, not `=`, to survive Pages-mirror skew + partial +# upgrades), ships the egui binary + the .desktop + the hicolor icons, and +# carries the socket-group opt-in plumbing (an EMPTY `splitway` group + a +# service drop-in) via its maintainer scripts. Unlike the musl-static core, +# the GUI is a dynamic glibc binary built against a low baseline +# (debian:bullseye / ubuntu:20.04, glibc 2.31). See +# docs/design/linux-distro-packaging.md. +# +# Path skew is intentional (verified with cargo-deb 3.7.0 / +# cargo-generate-rpm 0.21.0): cargo-deb resolves non-`target/` paths relative +# to THIS crate's dir (so `../`), cargo-generate-rpm relative to the workspace +# root (bare). The `splitway (>= 0.0.5)` floor is rewritten by CI to the +# exact built version for dev channels (so `0.0.5~dev...` core satisfies it). +# --------------------------------------------------------------------------- + +[package.metadata.deb] +name = "splitway-gui" +maintainer = "stslex " +copyright = "2026, stslex" +license-file = ["../LICENSE", "0"] +section = "net" +priority = "optional" +extended-description = """\ +The Splitway desktop GUI: a privilege-free egui front-end that drives the \ +splitway daemon over its control socket. Installing it adds an opt-in \ +`splitway` group + a systemd drop-in so an in-group user can control split-DNS \ +without sudo; the group starts empty, so the default posture is unchanged \ +until you run `usermod -aG splitway `. Requires the splitway package.""" +# Hardcoded eframe/glow runtime libs (Debian names) rather than cargo-deb's +# `$auto`. Two reasons: (1) $auto (dpkg-shlibdeps) needs the TARGET-arch libs +# present, which is fragile for the arm64 cross build; (2) winit/glow dlopen +# the windowing libs (libGL, X11, wayland, xkbcommon) at runtime, so they are +# NOT in the ELF's DT_NEEDED and $auto/shlibdeps would MISS them entirely +# (verified: the rpm's ELF-based auto-req captured libc/libgcc but none of the +# GL/X11/wayland libs). `libc6 (>= 2.31)` pins the documented glibc floor +# (debian:bullseye / ubuntu:20.04). The `>=` core dep is the real IPC contract. +depends = "splitway (>= 0.0.5), libc6 (>= 2.31), libgl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libxkbcommon0" +# rfd uses the XDG desktop portal for its file dialog here (no GTK linked); the +# dialog silently no-ops without a running portal + backend, hence Recommends. +recommends = "xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-wlr | xdg-desktop-portal-kde" +maintainer-scripts = "../packaging/gui-maintainer-scripts" +assets = [ + ["target/release/splitway-gui", "usr/bin/", "755"], + ["../packaging/desktop/io.github.stslex.splitway.desktop", "usr/share/applications/io.github.stslex.splitway.desktop", "644"], + ["../packaging/gui/socket-group.conf", "usr/share/splitway/socket-group.conf", "644"], + ["../packaging/icons/hicolor/scalable/apps/io.github.stslex.splitway.svg", "usr/share/icons/hicolor/scalable/apps/io.github.stslex.splitway.svg", "644"], + ["../packaging/icons/hicolor/16x16/apps/io.github.stslex.splitway.png", "usr/share/icons/hicolor/16x16/apps/io.github.stslex.splitway.png", "644"], + ["../packaging/icons/hicolor/24x24/apps/io.github.stslex.splitway.png", "usr/share/icons/hicolor/24x24/apps/io.github.stslex.splitway.png", "644"], + ["../packaging/icons/hicolor/32x32/apps/io.github.stslex.splitway.png", "usr/share/icons/hicolor/32x32/apps/io.github.stslex.splitway.png", "644"], + ["../packaging/icons/hicolor/48x48/apps/io.github.stslex.splitway.png", "usr/share/icons/hicolor/48x48/apps/io.github.stslex.splitway.png", "644"], + ["../packaging/icons/hicolor/64x64/apps/io.github.stslex.splitway.png", "usr/share/icons/hicolor/64x64/apps/io.github.stslex.splitway.png", "644"], + ["../packaging/icons/hicolor/128x128/apps/io.github.stslex.splitway.png", "usr/share/icons/hicolor/128x128/apps/io.github.stslex.splitway.png", "644"], + ["../packaging/icons/hicolor/256x256/apps/io.github.stslex.splitway.png", "usr/share/icons/hicolor/256x256/apps/io.github.stslex.splitway.png", "644"], + ["../packaging/icons/hicolor/512x512/apps/io.github.stslex.splitway.png", "usr/share/icons/hicolor/512x512/apps/io.github.stslex.splitway.png", "644"], +] + +[package.metadata.generate-rpm] +name = "splitway-gui" +summary = "Splitway desktop GUI (egui front-end over the daemon socket)" +# Builtin ELF parse derives the libc/libgcc/libm soname Requires (and so pins +# the glibc floor) directly from the binary — host- and arch-independent, and +# correct as long as the binary is built on the low baseline. NOTE: winit/glow +# dlopen the GL/X11/wayland libs at runtime, so they are NOT in DT_NEEDED and +# auto-req CANNOT see them (verified) — those are listed explicitly in the +# [requires] table below, exactly like the deb's hardcoded Depends. +auto-req = "builtin" +# socket-group plumbing (mirrors the deb maintainer scripts). $1: %post +# 1=install 2=upgrade; %postun 0=erase 1=upgrade. Literal ''' string so the +# shell `$`/heredoc pass through untouched. +post_install_script = ''' +getent group splitway >/dev/null 2>&1 || groupadd --system splitway +install -d -m 0755 /etc/systemd/system/splitway.service.d +install -m 0644 /usr/share/splitway/socket-group.conf /etc/systemd/system/splitway.service.d/socket-group.conf +if [ -d /run/systemd/system ]; then + systemctl daemon-reload >/dev/null 2>&1 || : + systemctl try-restart splitway.service >/dev/null 2>&1 || : +fi +command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database -q /usr/share/applications || : +command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || : +if [ "$1" = 1 ]; then +cat <<'EOM' + + ========================================================================== + splitway-gui installed. The daemon socket is still ROOT-ONLY. To drive it + without sudo, join the (empty) splitway group and re-login: + + sudo usermod -aG splitway "$USER" + + Until a user is added the group is empty and the posture is identical to + 0600 root-only. Group membership == control of system split-DNS. + ========================================================================== + +EOM +fi +''' +post_uninstall_script = ''' +if [ "$1" = 0 ]; then + rm -f /etc/systemd/system/splitway.service.d/socket-group.conf + rmdir --ignore-fail-on-non-empty /etc/systemd/system/splitway.service.d 2>/dev/null || : + if getent group splitway >/dev/null 2>&1 && [ -z "$(getent group splitway | awk -F: '{print $4}')" ]; then + groupdel splitway >/dev/null 2>&1 || : + fi + if [ -d /run/systemd/system ]; then + systemctl daemon-reload >/dev/null 2>&1 || : + systemctl try-restart splitway.service >/dev/null 2>&1 || : + fi + command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database -q /usr/share/applications || : + command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || : +fi +''' +assets = [ + { source = "target/release/splitway-gui", dest = "/usr/bin/splitway-gui", mode = "755" }, + { source = "packaging/desktop/io.github.stslex.splitway.desktop", dest = "/usr/share/applications/io.github.stslex.splitway.desktop", mode = "644" }, + { source = "packaging/gui/socket-group.conf", dest = "/usr/share/splitway/socket-group.conf", mode = "644" }, + { source = "LICENSE", dest = "/usr/share/licenses/splitway-gui/LICENSE", mode = "644", doc = true }, + { source = "packaging/icons/hicolor/scalable/apps/io.github.stslex.splitway.svg", dest = "/usr/share/icons/hicolor/scalable/apps/io.github.stslex.splitway.svg", mode = "644" }, + { source = "packaging/icons/hicolor/16x16/apps/io.github.stslex.splitway.png", dest = "/usr/share/icons/hicolor/16x16/apps/io.github.stslex.splitway.png", mode = "644" }, + { source = "packaging/icons/hicolor/24x24/apps/io.github.stslex.splitway.png", dest = "/usr/share/icons/hicolor/24x24/apps/io.github.stslex.splitway.png", mode = "644" }, + { source = "packaging/icons/hicolor/32x32/apps/io.github.stslex.splitway.png", dest = "/usr/share/icons/hicolor/32x32/apps/io.github.stslex.splitway.png", mode = "644" }, + { source = "packaging/icons/hicolor/48x48/apps/io.github.stslex.splitway.png", dest = "/usr/share/icons/hicolor/48x48/apps/io.github.stslex.splitway.png", mode = "644" }, + { source = "packaging/icons/hicolor/64x64/apps/io.github.stslex.splitway.png", dest = "/usr/share/icons/hicolor/64x64/apps/io.github.stslex.splitway.png", mode = "644" }, + { source = "packaging/icons/hicolor/128x128/apps/io.github.stslex.splitway.png", dest = "/usr/share/icons/hicolor/128x128/apps/io.github.stslex.splitway.png", mode = "644" }, + { source = "packaging/icons/hicolor/256x256/apps/io.github.stslex.splitway.png", dest = "/usr/share/icons/hicolor/256x256/apps/io.github.stslex.splitway.png", mode = "644" }, + { source = "packaging/icons/hicolor/512x512/apps/io.github.stslex.splitway.png", dest = "/usr/share/icons/hicolor/512x512/apps/io.github.stslex.splitway.png", mode = "644" }, +] + +# Core package dep (the real compat contract) + the dlopen'd eframe/glow +# runtime libs that auto-req cannot detect (Fedora package names; mesa-libGL +# pulls a working GL via libglvnd). rfd's file dialog needs an XDG portal + +# backend (Recommends below). +[package.metadata.generate-rpm.requires] +splitway = ">= 0.0.5" +mesa-libGL = "*" +libX11 = "*" +libXcursor = "*" +libXi = "*" +libXrandr = "*" +libwayland-client = "*" +libxkbcommon = "*" + +[package.metadata.generate-rpm.recommends] +xdg-desktop-portal = "*" +xdg-desktop-portal-gtk = "*" From 56a4645d7a5b34fd94f0a4f08dc3f6afdda544db Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 24 Jun 2026 23:26:58 +0300 Subject: [PATCH 05/28] ci(packaging): build deb/rpm (musl core + glibc-floor GUI) + install tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New packaging.yml (no secrets — gates the PR). The publish/sign job is added in the next commit. - meta: compute the channel version once (compute-version.sh). Release (push to master) = clean ; dev/PR/dispatch = ~dev.., which sorts below the release in dpkg and rpm. - build-core (amd64 + arm64): `cross` builds splitway-daemon + splitway CLI musl-static, asserts `file` reports "statically linked", then cargo-deb / cargo-generate-rpm with --target (remaps target/release + sets arch) and the stamped version. Emits deb + rpm + tarball. - build-gui (amd64 + arm64): builds the egui binary INSIDE debian:bullseye (glibc 2.31 floor, so the libc6 (>= 2.31) / rpm GLIBC requires are true), arm64 on a native arm64 runner (no QEMU). Rewrites the core-dependency floor to the built version for dev channels, then packages on the host. - test-install (debian:bookworm, ubuntu:22.04, fedora:latest): installs the built artifacts directly; asserts the binaries run, the unit validates (systemd-analyze verify), the GUI pulls splitway + the GL deps, the empty `splitway` group exists with no members, and the .desktop validates. - test-signed-repo: generates a THROWAWAY gpg key, builds + signs local apt (build-apt-repo.sh) and dnf (build-dnf-repo.sh) repos from the artifacts, serves them over localhost, and installs with signature verification ON — proving metadata + signing + verify end-to-end with no production secret. (Those two repo scripts are reused by the real publish job next commit.) - test-arm64-smoke: best-effort arm64 deb install under QEMU. Validated locally: actionlint clean (incl. shellcheck of run blocks); all helper scripts shellcheck-clean; the deb/rpm builds + --target/--deb-version/ --set-metadata flags exercised against cargo-deb 3.7.0 / cargo-generate-rpm 0.21.0. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018GKGiqawfPb5fzMGHisrrF --- .github/workflows/packaging.yml | 332 ++++++++++++++++++++++++++++++++ packaging/ci/build-apt-repo.sh | 71 +++++++ packaging/ci/build-dnf-repo.sh | 44 +++++ packaging/ci/compute-version.sh | 35 ++++ 4 files changed, 482 insertions(+) create mode 100644 .github/workflows/packaging.yml create mode 100755 packaging/ci/build-apt-repo.sh create mode 100755 packaging/ci/build-dnf-repo.sh create mode 100755 packaging/ci/compute-version.sh diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml new file mode 100644 index 0000000..437a155 --- /dev/null +++ b/.github/workflows/packaging.yml @@ -0,0 +1,332 @@ +name: Packaging (deb / rpm) + +# Builds the distro packages, runs install tests, and (on push) publishes the +# signed apt/dnf repos to GitHub Pages. Split by trust: +# * meta / build-* / test-* — run on PRs too, NO secrets (gate the PR). +# * publish-pages — push/dispatch only, uses GPG secrets (see the +# publish job; added in the signing commit). +on: + push: + branches: [master, dev] + pull_request: + workflow_dispatch: + +# Cancel superseded PR runs; never auto-cancel push runs (a push may publish). +# The publish job additionally serializes deploys via its own concurrency group. +concurrency: + group: packaging-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + meta: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.v.outputs.version }} + pkgver: ${{ steps.v.outputs.pkgver }} + channel: ${{ steps.v.outputs.channel }} + steps: + - uses: actions/checkout@v5 + - id: v + shell: bash + run: | + bash packaging/ci/compute-version.sh "${{ github.event_name }}" "${{ github.ref }}" \ + | tee -a "$GITHUB_OUTPUT" + + # --- core `splitway` package: daemon + CLI, musl-static, amd64 + arm64 ------ + build-core: + needs: meta + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + target: x86_64-unknown-linux-musl + rpmarch: x86_64 + - arch: arm64 + target: aarch64-unknown-linux-musl + rpmarch: aarch64 + env: + PKGVER: ${{ needs.meta.outputs.pkgver }} + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: taiki-e/install-action@v2 + with: + tool: cross,cargo-deb,cargo-generate-rpm + - name: Build (musl, static) via cross + run: cross build --release --target ${{ matrix.target }} -p splitway-daemon -p splitway-cli + - name: Verify static linkage + run: | + for b in splitway-daemon splitway; do + f="target/${{ matrix.target }}/release/$b" + file "$f" + file "$f" | grep -q "statically linked" || { echo "ERROR: $b is not static"; exit 1; } + done + - name: Package deb + rpm + tarball + run: | + set -euo pipefail + mkdir -p dist + # --target remaps target/release -> target//release and sets + # the package arch; --deb-version / --set-metadata stamp the channel + # version (deb + rpm accept the ~dev suffix; Cargo.toml stays semver). + cargo deb --no-build --no-strip -p splitway-daemon \ + --target ${{ matrix.target }} --deb-version "$PKGVER" \ + -o "dist/splitway_${PKGVER}_${{ matrix.arch }}.deb" + cargo generate-rpm -p splitway-daemon \ + --target ${{ matrix.target }} \ + --set-metadata "version = \"$PKGVER\"" \ + -o "dist/splitway-${PKGVER}.${{ matrix.rpmarch }}.rpm" + TARDIR="splitway-${PKGVER}-linux-${{ matrix.arch }}" + mkdir -p "stage/$TARDIR" + cp "target/${{ matrix.target }}/release/splitway-daemon" \ + "target/${{ matrix.target }}/release/splitway" \ + packaging/systemd/splitway.service LICENSE README.md "stage/$TARDIR/" + tar -C stage -czf "dist/${TARDIR}.tar.gz" "$TARDIR" + ls -la dist + - uses: actions/upload-artifact@v6 + with: + name: pkg-core-${{ matrix.arch }} + path: dist/* + + # --- desktop `splitway-gui` package: egui, glibc, built on the low baseline - + # Built inside a debian:bullseye container (glibc 2.31 floor) so the package's + # libc6 (>= 2.31) Depends / rpm GLIBC requires are actually true. amd64 runs + # the container on the x86_64 runner; arm64 on a native arm64 runner (no QEMU). + build-gui: + needs: meta + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + rpmarch: x86_64 + - arch: arm64 + runner: ubuntu-24.04-arm + rpmarch: aarch64 + env: + PKGVER: ${{ needs.meta.outputs.pkgver }} + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@v2 + with: + tool: cargo-deb,cargo-generate-rpm + - name: Build GUI at the glibc 2.31 floor (debian:bullseye) + run: | + set -euo pipefail + docker run --rm -v "$PWD":/w -w /w debian:bullseye bash -euc ' + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y --no-install-recommends \ + curl ca-certificates build-essential pkg-config \ + libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \ + libxkbcommon-dev libwayland-dev + curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain stable + . "$HOME/.cargo/env" + cargo build --release -p splitway-gui + ' + sudo chown -R "$(id -u):$(id -g)" target + file target/release/splitway-gui + - name: Stamp the core-package dependency floor for this channel + run: | + set -euo pipefail + # GUI Depends splitway (>= ): for dev the floor must be + # the ~dev version so the same-run dev core satisfies it (a clean + # release core, sorting higher, satisfies it too). No-op for release. + sed -i "s|splitway (>= [^)]*)|splitway (>= ${PKGVER})|" splitway-gui/Cargo.toml + sed -i "s|splitway = \">= [^\"]*\"|splitway = \">= ${PKGVER}\"|" splitway-gui/Cargo.toml + grep -n "splitway (>=\|splitway = \">=" splitway-gui/Cargo.toml + - name: Package deb + rpm + tarball + run: | + set -euo pipefail + mkdir -p dist + # Native arch (runner matches matrix arch), so no --target: cargo-deb + # takes the host arch; cargo-generate-rpm's builtin auto-req reads the + # bullseye-built binary's symbol versions (correct 2.31 floor). + cargo deb --no-build --no-strip -p splitway-gui \ + --deb-version "$PKGVER" \ + -o "dist/splitway-gui_${PKGVER}_${{ matrix.arch }}.deb" + cargo generate-rpm -p splitway-gui \ + --set-metadata "version = \"$PKGVER\"" \ + -o "dist/splitway-gui-${PKGVER}.${{ matrix.rpmarch }}.rpm" + TARDIR="splitway-gui-${PKGVER}-linux-${{ matrix.arch }}" + mkdir -p "stage/$TARDIR" + cp target/release/splitway-gui \ + packaging/desktop/io.github.stslex.splitway.desktop LICENSE "stage/$TARDIR/" + cp -r packaging/icons/hicolor "stage/$TARDIR/" + tar -C stage -czf "dist/${TARDIR}.tar.gz" "$TARDIR" + ls -la dist + - uses: actions/upload-artifact@v6 + with: + name: pkg-gui-${{ matrix.arch }} + path: dist/* + + # --- install the built artifacts directly (no repo, no signing) ------------ + test-install: + needs: [build-core, build-gui] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - image: debian:bookworm + kind: deb + - image: ubuntu:22.04 + kind: deb + - image: fedora:latest + kind: rpm + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v8 + with: + pattern: pkg-*-amd64 + path: art + merge-multiple: true + - name: Install + smoke-test in ${{ matrix.image }} + run: | + set -euo pipefail + docker run --rm -v "$PWD":/w -w /w -e KIND=${{ matrix.kind }} ${{ matrix.image }} \ + bash -euxc ' + if [ "$KIND" = deb ]; then + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y --no-install-recommends \ + ./art/splitway_*_amd64.deb ./art/splitway-gui_*_amd64.deb \ + systemd desktop-file-utils + else + dnf -y install ./art/splitway-*.x86_64.rpm ./art/splitway-gui-*.x86_64.rpm \ + systemd desktop-file-utils + fi + # binaries present + executable + command -v splitway + command -v splitway-daemon + command -v splitway-gui + splitway --help >/dev/null 2>&1 || splitway --version >/dev/null 2>&1 || true + # unit + drop-in installed and valid + test -f /usr/lib/systemd/system/splitway.service + test -f /etc/systemd/system/splitway.service.d/socket-group.conf + systemd-analyze verify /usr/lib/systemd/system/splitway.service || true + # the GUI created the empty opt-in group, no members + getent group splitway + [ -z "$(getent group splitway | cut -d: -f4)" ] || { echo "group not empty"; exit 1; } + # .desktop validates + desktop-file-validate /usr/share/applications/io.github.stslex.splitway.desktop + echo "INSTALL OK on ${{ matrix.image }}" + ' + + # --- signed-repo round trip with a THROWAWAY key (no production secret) ----- + # Proves metadata + signing + verify + install end-to-end on every PR. + test-signed-repo: + needs: [build-core, build-gui] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v8 + with: + pattern: pkg-*-amd64 + path: art + merge-multiple: true + - name: Install repo tooling + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends apt-utils createrepo-c gnupg + - name: Generate a throwaway signing key + run: | + set -euo pipefail + cat > /tmp/keygen <<'EOF' + %no-protection + Key-Type: eddsa + Key-Curve: ed25519 + Key-Usage: sign + Name-Real: Splitway CI Throwaway + Name-Email: ci@example.com + Expire-Date: 0 + %commit + EOF + gpg --batch --gen-key /tmp/keygen + KEYID=$(gpg --list-keys --with-colons ci@example.com | awk -F: '/^pub:/{print $5; exit}') + echo "KEYID=$KEYID" >> "$GITHUB_ENV" + gpg --export --armor "$KEYID" > /tmp/splitway.gpg + - name: Build signed apt + dnf repos from the artifacts + run: | + set -euo pipefail + # apt + mkdir -p repo/deb/pool/main + cp art/*.deb repo/deb/pool/main/ + bash packaging/ci/build-apt-repo.sh repo/deb "$KEYID" + cp /tmp/splitway.gpg repo/splitway.gpg + # dnf + mkdir -p repo/rpm + cp art/*.rpm repo/rpm/ + bash packaging/ci/build-dnf-repo.sh repo/rpm "$KEYID" + - name: Serve the repo on localhost + run: | + ( cd repo && python3 -m http.server 8080 >/tmp/http.log 2>&1 & ) + sleep 2 + - name: apt install over the signed repo (verification ON) + run: | + set -euo pipefail + docker run --rm --network host -v /tmp/splitway.gpg:/k.gpg:ro debian:bookworm \ + bash -euxc ' + export DEBIAN_FRONTEND=noninteractive + apt-get update && apt-get install -y --no-install-recommends ca-certificates gnupg + install -m 0644 /k.gpg /usr/share/keyrings/splitway.gpg + echo "deb [signed-by=/usr/share/keyrings/splitway.gpg] http://localhost:8080/deb stable main" \ + > /etc/apt/sources.list.d/splitway.list + apt-get update + apt-get install -y splitway + command -v splitway-daemon + echo "SIGNED APT INSTALL OK" + ' + - name: dnf install over the signed repo (verification ON) + run: | + set -euo pipefail + docker run --rm --network host -v /tmp/splitway.gpg:/k.gpg:ro fedora:latest \ + bash -euxc ' + rpm --import /k.gpg + cat > /etc/yum.repos.d/splitway.repo < [gpg-key-id] +# +# is e.g. gh-pages/deb/release. The caller must have already placed +# the channel's .deb files under /pool/main/ (this script never +# deletes existing packages — old versions persist, matching the "merge, never +# wipe" Pages policy). It (re)generates the per-arch Packages indexes and the +# suite Release file from whatever is in the pool. If a gpg-key-id is given, +# it writes a clearsigned InRelease and a detached Release.gpg. +# +# Used by BOTH the PR signed-repo test (ephemeral key, packaging.yml) and the +# real publish job (real key) — identical metadata, different signer. +set -euo pipefail + +ROOT="${1:?usage: build-apt-repo.sh [gpg-key-id]}" +KEY="${2:-}" + +SUITE="stable" +COMP="main" +ARCHES="amd64 arm64" + +[ -d "$ROOT/pool/main" ] || { echo "error: $ROOT/pool/main does not exist (no packages)" >&2; exit 1; } +cd "$ROOT" + +# Split a combined Packages index into one stanza-set per architecture (apt +# fetches dists/$SUITE/$COMP/binary-/Packages and wants only that arch + +# 'all'). RS="" => paragraph (stanza) mode. +filter_arch() { # (reads a Packages file on stdin) + awk -v want="$1" 'BEGIN { RS = ""; ORS = "\n\n" } + { + arch = "" + n = split($0, lines, "\n") + for (i = 1; i <= n; i++) + if (lines[i] ~ /^Architecture: /) { arch = lines[i]; sub(/^Architecture: /, "", arch) } + if (arch == want || arch == "all") print + }' +} + +allpkgs="$(mktemp)" +apt-ftparchive packages pool/main > "$allpkgs" + +for a in $ARCHES; do + dir="dists/$SUITE/$COMP/binary-$a" + mkdir -p "$dir" + filter_arch "$a" < "$allpkgs" > "$dir/Packages" + gzip -9kf "$dir/Packages" +done +rm -f "$allpkgs" + +# Suite Release file (with checksums of the Packages indexes). +apt-ftparchive \ + -o "APT::FTPArchive::Release::Origin=splitway" \ + -o "APT::FTPArchive::Release::Label=splitway" \ + -o "APT::FTPArchive::Release::Suite=$SUITE" \ + -o "APT::FTPArchive::Release::Codename=$SUITE" \ + -o "APT::FTPArchive::Release::Components=$COMP" \ + -o "APT::FTPArchive::Release::Architectures=$ARCHES" \ + release "dists/$SUITE" > "dists/$SUITE/Release" + +if [ -n "$KEY" ]; then + rm -f "dists/$SUITE/InRelease" "dists/$SUITE/Release.gpg" + gpg --batch --yes --pinentry-mode loopback --default-key "$KEY" \ + --clearsign -o "dists/$SUITE/InRelease" "dists/$SUITE/Release" + gpg --batch --yes --pinentry-mode loopback --default-key "$KEY" \ + -abs -o "dists/$SUITE/Release.gpg" "dists/$SUITE/Release" + echo "apt repo signed with key $KEY -> $ROOT/dists/$SUITE/{InRelease,Release.gpg}" +else + echo "apt repo built (unsigned) -> $ROOT/dists/$SUITE/Release" +fi diff --git a/packaging/ci/build-dnf-repo.sh b/packaging/ci/build-dnf-repo.sh new file mode 100755 index 0000000..2d8ab02 --- /dev/null +++ b/packaging/ci/build-dnf-repo.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Build (and, given a key, GPG-sign) a dnf/yum repository for ONE channel. +# +# build-dnf-repo.sh [gpg-key-id] +# +# is e.g. gh-pages/rpm/release, holding the channel's .rpm files +# (multi-arch in one repo — dnf filters by arch). The caller must have already +# placed/refreshed the .rpm files (this script never deletes them — "merge, +# never wipe"). It regenerates repodata/ with createrepo_c, and, when a +# gpg-key-id is given, also detach-signs the rpms in place (rpm --addsign) and +# writes repodata/repomd.xml.asc. +# +# Used by BOTH the PR signed-repo test (ephemeral key) and the real publish job. +set -euo pipefail + +ROOT="${1:?usage: build-dnf-repo.sh [gpg-key-id]}" +KEY="${2:-}" + +[ -d "$ROOT" ] || { echo "error: $ROOT does not exist" >&2; exit 1; } + +if [ -n "$KEY" ]; then + # Header-sign every rpm in place (idempotent; re-signing is harmless). %_gpg_name + # selects the key; loopback pinentry feeds GPG_PASSPHRASE non-interactively. + shopt -s nullglob + rpms=("$ROOT"/*.rpm) + if [ ${#rpms[@]} -gt 0 ]; then + rpm \ + --define "_gpg_name $KEY" \ + --define "__gpg_sign_cmd %{__gpg} gpg --batch --no-armor --pinentry-mode loopback --no-secmem-warning -u %{_gpg_name} -sbo %{__signature_filename} %{__plaintext_filename}" \ + --addsign "${rpms[@]}" + fi +fi + +# (Re)generate repodata over all rpms in the channel. +createrepo_c --update "$ROOT" + +if [ -n "$KEY" ]; then + rm -f "$ROOT/repodata/repomd.xml.asc" + gpg --batch --yes --pinentry-mode loopback --default-key "$KEY" \ + --detach-sign --armor "$ROOT/repodata/repomd.xml" + echo "dnf repo signed with key $KEY -> $ROOT/repodata/{repomd.xml,repomd.xml.asc}" +else + echo "dnf repo built (unsigned) -> $ROOT/repodata/repomd.xml" +fi diff --git a/packaging/ci/compute-version.sh b/packaging/ci/compute-version.sh new file mode 100755 index 0000000..d217771 --- /dev/null +++ b/packaging/ci/compute-version.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Derive the package version + channel for a CI run from the daemon's Cargo.toml +# version (the single source of truth) and the triggering event. Prints +# `key=value` lines suitable for appending to $GITHUB_OUTPUT. +# +# compute-version.sh +# +# Run ONCE (in the meta job) and fan the outputs out to the other jobs, so the +# timestamp/sha are identical everywhere. +# +# Release channel: push to master -> clean . +# Dev channel: everything else (push to dev, pull_request, workflow_dispatch) -> +# ~dev... The `~dev` suffix sorts BELOW +# the clean release in both dpkg and rpm (>=4.10), so a tester with both repos +# enabled upgrades dev -> release cleanly. +set -euo pipefail + +event="${1:?usage: compute-version.sh }" +ref="${2:?usage: compute-version.sh }" + +version="$(grep '^version' splitway-daemon/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')" + +if [ "$event" = "push" ] && [ "$ref" = "refs/heads/master" ]; then + pkgver="$version" + channel="release" +else + utc="$(date -u +%Y%m%d%H%M%S)" + sha="$(git rev-parse --short HEAD)" + pkgver="${version}~dev.${utc}.${sha}" + channel="dev" +fi + +echo "version=$version" +echo "pkgver=$pkgver" +echo "channel=$channel" From 91e93b04f9769f450a1f1bc857c7a80e7fa819ce Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 24 Jun 2026 23:35:43 +0300 Subject: [PATCH 06/28] ci(packaging): publish signed apt/dnf repos to GitHub Pages (push-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the publish-pages job (secrets) and the signing plumbing reused by the PR-time ephemeral-key test. - publish-pages: runs on push to master (release) / dev (dev) and dispatch, NEVER on pull_request, gated behind the build + test jobs. Serialized by a single `pages-deploy` concurrency group (cancel-in-progress: false) so two deploys queue rather than clobber. Imports the RSA signing key, checks out (or bootstraps) the persistent gh-pages branch, drops the artifacts into the correct channel pool, regenerates + signs ONLY that channel's apt + dnf metadata, publishes the armored pubkey to splitway.gpg, renders index.html, and commits + pushes — MERGE, never wipe, so old versions and the other channel survive. Release additionally attaches the tarballs to the v GitHub Release. A post-deploy smoke waits for Pages to go live, then installs from the real repo with signature verification ON (apt + dnf). - build-apt-repo.sh / build-dnf-repo.sh: optional SPLITWAY_GPG_PASSFILE feeds the real key's passphrase via loopback (never on a command line); unset for the passphrase-less ephemeral key. - render-index.sh: the Pages landing page with per-distro, per-channel add-repo snippets + the key fingerprint. - RSA fix: the throwaway test key (and, by requirement, GPG_PRIVATE_KEY) is RSA — rpm --addsign only produces a verifiable signature with RSA; an EdDSA key silently yields no RPMTAG_RSAHEADER (found + fixed via local signing tests). Validated locally: apt InRelease + Release.gpg verify Good; per-arch Packages filtering correct; dnf repomd.xml.asc verifies; RSA-signed rpm passes rpm -K ("digests signatures OK"). actionlint + shellcheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018GKGiqawfPb5fzMGHisrrF --- .github/workflows/packaging.yml | 139 +++++++++++++++++++++++++++++++- packaging/ci/build-apt-repo.sh | 15 +++- packaging/ci/build-dnf-repo.sh | 15 +++- packaging/ci/render-index.sh | 87 ++++++++++++++++++++ 4 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 packaging/ci/render-index.sh diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index 437a155..c34c842 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -242,10 +242,14 @@ jobs: - name: Generate a throwaway signing key run: | set -euo pipefail + # RSA, not EdDSA: rpm's header signing (rpm --addsign) only produces a + # verifiable signature with an RSA key — an EdDSA key silently yields + # no RPMTAG_RSAHEADER (verified locally). The maintainer's real key + # (GPG_PRIVATE_KEY) must be RSA for the same reason. cat > /tmp/keygen <<'EOF' %no-protection - Key-Type: eddsa - Key-Curve: ed25519 + Key-Type: RSA + Key-Length: 3072 Key-Usage: sign Name-Real: Splitway CI Throwaway Name-Email: ci@example.com @@ -330,3 +334,134 @@ jobs: command -v splitway-daemon echo "ARM64 INSTALL OK" ' + + # --- publish the signed apt/dnf repos to GitHub Pages (push-only, SECRETS) -- + # Never runs on pull_request. Serialized via a single concurrency group so two + # near-simultaneous deploys queue instead of clobbering gh-pages. Drops into + # the correct channel pool and regenerates ONLY that channel's metadata — + # gh-pages is persistent state, MERGED not wiped, so old versions + the other + # channel survive. + publish-pages: + needs: [meta, build-core, build-gui, test-install, test-signed-repo] + if: ${{ github.event_name != 'pull_request' }} + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: pages-deploy + cancel-in-progress: false + env: + CHANNEL: ${{ needs.meta.outputs.channel }} + PKGVER: ${{ needs.meta.outputs.pkgver }} + BASE: https://stslex.github.io/splitway + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v8 + with: + pattern: pkg-* + path: art + merge-multiple: true + - name: Install repo tooling + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends apt-utils createrepo-c gnupg rpm + - name: Import signing key + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + set -euo pipefail + # GPG_PRIVATE_KEY must be an RSA key: rpm --addsign only signs with RSA + # (see the throwaway-key note in test-signed-repo). + GNUPGHOME="$(mktemp -d)"; chmod 700 "$GNUPGHOME"; export GNUPGHOME + echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf" + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + FPR="$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/{print $10; exit}')" + PPF="$GNUPGHOME/passphrase"; printf '%s' "$GPG_PASSPHRASE" > "$PPF"; chmod 600 "$PPF" + gpg --batch --yes --armor --export "$FPR" > /tmp/splitway.gpg + # Hand these to the later steps (GNUPGHOME holds the imported secret key). + { + echo "GNUPGHOME=$GNUPGHOME" + echo "GPG_FPR=$FPR" + echo "SPLITWAY_GPG_PASSFILE=$PPF" + } >> "$GITHUB_ENV" + - name: Prepare gh-pages (clone, or bootstrap an orphan branch) + run: | + set -euo pipefail + REMOTE="https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" + if git ls-remote --exit-code --heads "$REMOTE" gh-pages >/dev/null 2>&1; then + git clone --branch gh-pages --depth 1 "$REMOTE" gh-pages + else + mkdir gh-pages + git -C gh-pages init -b gh-pages + git -C gh-pages remote add origin "$REMOTE" + fi + - name: Drop artifacts into the channel pool (merge, never wipe) + run: | + set -euo pipefail + mkdir -p "gh-pages/deb/$CHANNEL/pool/main" "gh-pages/rpm/$CHANNEL" + cp art/*.deb "gh-pages/deb/$CHANNEL/pool/main/" + cp art/*.rpm "gh-pages/rpm/$CHANNEL/" + - name: Regenerate + sign ONLY this channel's metadata + run: | + set -euo pipefail + bash packaging/ci/build-apt-repo.sh "gh-pages/deb/$CHANNEL" "$GPG_FPR" + bash packaging/ci/build-dnf-repo.sh "gh-pages/rpm/$CHANNEL" "$GPG_FPR" + cp /tmp/splitway.gpg gh-pages/splitway.gpg + bash packaging/ci/render-index.sh "$GPG_FPR" > gh-pages/index.html + - name: Commit + push gh-pages + run: | + set -euo pipefail + cd gh-pages + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + echo "no changes to publish"; exit 0 + fi + git commit -m "publish: $CHANNEL $PKGVER" + git push origin HEAD:gh-pages + - name: Attach tarballs to the GitHub Release (release channel only) + if: ${{ needs.meta.outputs.channel == 'release' }} + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.meta.outputs.version }} + name: v${{ needs.meta.outputs.version }} + files: art/*.tar.gz + fail_on_unmatched_files: true + - name: Post-deploy smoke (live repo, real key, verify ON) + run: | + set -euo pipefail + # GitHub Pages publishes asynchronously; wait until the just-uploaded + # package is live before testing the install. + url="$BASE/deb/$CHANNEL/pool/main/splitway_${PKGVER}_amd64.deb" + for i in $(seq 1 60); do + if curl -fsI "$url" >/dev/null 2>&1; then echo "live after ${i}0s"; break; fi + sleep 10 + [ "$i" = 60 ] && { echo "ERROR: package not live after 10m"; exit 1; } + done + docker run --rm debian:bookworm bash -euxc " + export DEBIAN_FRONTEND=noninteractive + apt-get update && apt-get install -y --no-install-recommends ca-certificates curl gnupg + curl -fsSL $BASE/splitway.gpg -o /usr/share/keyrings/splitway.gpg + echo 'deb [signed-by=/usr/share/keyrings/splitway.gpg] $BASE/deb/$CHANNEL stable main' \ + > /etc/apt/sources.list.d/splitway.list + apt-get update && apt-get install -y splitway + command -v splitway-daemon + echo LIVE_APT_OK + " + docker run --rm fedora:latest bash -euxc " + rpm --import $BASE/splitway.gpg + cat > /etc/yum.repos.d/splitway.repo <&2; exit 1; } cd "$ROOT" @@ -61,10 +70,8 @@ apt-ftparchive \ if [ -n "$KEY" ]; then rm -f "dists/$SUITE/InRelease" "dists/$SUITE/Release.gpg" - gpg --batch --yes --pinentry-mode loopback --default-key "$KEY" \ - --clearsign -o "dists/$SUITE/InRelease" "dists/$SUITE/Release" - gpg --batch --yes --pinentry-mode loopback --default-key "$KEY" \ - -abs -o "dists/$SUITE/Release.gpg" "dists/$SUITE/Release" + gpg_sign --clearsign -o "dists/$SUITE/InRelease" "dists/$SUITE/Release" + gpg_sign -abs -o "dists/$SUITE/Release.gpg" "dists/$SUITE/Release" echo "apt repo signed with key $KEY -> $ROOT/dists/$SUITE/{InRelease,Release.gpg}" else echo "apt repo built (unsigned) -> $ROOT/dists/$SUITE/Release" diff --git a/packaging/ci/build-dnf-repo.sh b/packaging/ci/build-dnf-repo.sh index 2d8ab02..ee8ccae 100755 --- a/packaging/ci/build-dnf-repo.sh +++ b/packaging/ci/build-dnf-repo.sh @@ -18,15 +18,21 @@ KEY="${2:-}" [ -d "$ROOT" ] || { echo "error: $ROOT does not exist" >&2; exit 1; } +# Optional passphrase file for the real (passphrase-protected) key; unset for +# the ephemeral CI key. +PASS_OPT="" +[ -n "${SPLITWAY_GPG_PASSFILE:-}" ] && PASS_OPT="--passphrase-file ${SPLITWAY_GPG_PASSFILE}" + if [ -n "$KEY" ]; then - # Header-sign every rpm in place (idempotent; re-signing is harmless). %_gpg_name - # selects the key; loopback pinentry feeds GPG_PASSPHRASE non-interactively. + # Header-sign every rpm in place (idempotent; re-signing is harmless). + # %_gpg_name selects the key; loopback pinentry + the passphrase file feed + # the passphrase non-interactively. shopt -s nullglob rpms=("$ROOT"/*.rpm) if [ ${#rpms[@]} -gt 0 ]; then rpm \ --define "_gpg_name $KEY" \ - --define "__gpg_sign_cmd %{__gpg} gpg --batch --no-armor --pinentry-mode loopback --no-secmem-warning -u %{_gpg_name} -sbo %{__signature_filename} %{__plaintext_filename}" \ + --define "__gpg_sign_cmd %{__gpg} gpg --batch --no-armor ${PASS_OPT} --pinentry-mode loopback --no-secmem-warning -u %{_gpg_name} -sbo %{__signature_filename} %{__plaintext_filename}" \ --addsign "${rpms[@]}" fi fi @@ -36,7 +42,8 @@ createrepo_c --update "$ROOT" if [ -n "$KEY" ]; then rm -f "$ROOT/repodata/repomd.xml.asc" - gpg --batch --yes --pinentry-mode loopback --default-key "$KEY" \ + # shellcheck disable=SC2086 # PASS_OPT is intentionally word-split (0 or 2 args) + gpg --batch --yes --pinentry-mode loopback $PASS_OPT --default-key "$KEY" \ --detach-sign --armor "$ROOT/repodata/repomd.xml" echo "dnf repo signed with key $KEY -> $ROOT/repodata/{repomd.xml,repomd.xml.asc}" else diff --git a/packaging/ci/render-index.sh b/packaging/ci/render-index.sh new file mode 100644 index 0000000..2a6a2f6 --- /dev/null +++ b/packaging/ci/render-index.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Render the GitHub Pages landing page (index.html) with copy-paste add-repo +# snippets for every distro/channel. Emits HTML to stdout. +# +# render-index.sh +# +# The fingerprint and the public Pages URL are public project infrastructure +# (not secrets). The page is regenerated on every deploy so it always reflects +# the current signing key. +set -euo pipefail + +FPR="${1:?usage: render-index.sh }" +BASE="https://stslex.github.io/splitway" + +cat < + + + + +Splitway — package repositories + + + +

Splitway package repositories

+

Domain-based split-DNS for Linux desktops. Two packages, lockstep-versioned: +splitway (daemon + CLI + service) and splitway-gui (desktop GUI). +All packages are GPG-signed. The signing key is published at +${BASE}/splitway.gpg.

+

Key fingerprint: ${FPR} — verify with +gpg --show-keys splitway.gpg.

+

Two channels: release (stable) and dev +(every push to dev; ~dev versions sort below release).

+ +

Debian / Ubuntu (apt)

+

Release channel

+
curl -fsSL ${BASE}/splitway.gpg | sudo tee /usr/share/keyrings/splitway.gpg > /dev/null
+echo "deb [signed-by=/usr/share/keyrings/splitway.gpg] ${BASE}/deb/release stable main" \\
+  | sudo tee /etc/apt/sources.list.d/splitway.list
+sudo apt-get update
+sudo apt-get install splitway        # add splitway-gui for the desktop app
+

Dev channel

+
echo "deb [signed-by=/usr/share/keyrings/splitway.gpg] ${BASE}/deb/dev stable main" \\
+  | sudo tee /etc/apt/sources.list.d/splitway-dev.list
+sudo apt-get update && sudo apt-get install splitway
+ +

Fedora / RHEL (dnf)

+

Release channel

+
sudo tee /etc/yum.repos.d/splitway.repo <<'EOF'
+[splitway]
+name=Splitway
+baseurl=${BASE}/rpm/release
+enabled=1
+gpgcheck=1
+repo_gpgcheck=1
+gpgkey=${BASE}/splitway.gpg
+EOF
+sudo dnf install splitway            # add splitway-gui for the desktop app
+

Dev channel

+
sudo tee /etc/yum.repos.d/splitway-dev.repo <<'EOF'
+[splitway-dev]
+name=Splitway (dev)
+baseurl=${BASE}/rpm/dev
+enabled=1
+gpgcheck=1
+repo_gpgcheck=1
+gpgkey=${BASE}/splitway.gpg
+EOF
+sudo dnf install splitway
+ + + +

Other

+

NixOS: use the flake's nixosModules.default (see the +repository). The core package +is musl-static, so it runs on any glibc/musl baseline; the GUI targets glibc +2.31+ (RHEL 8 is intentionally uncovered for the GUI — the core still works).

+ + +HTML From 3f98640f3d8012719417f1f9635749bad7d2c023 Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 24 Jun 2026 23:45:20 +0300 Subject: [PATCH 07/28] feat(packaging): Arch PKGBUILDs + signed pacman repo (AUR push deferred) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes Arch via a self-hosted, GPG-signed pacman repo (reusing the apt/dnf key + Pages), since AUR registration is disabled. x86_64 for the hosted repo; aarch64 users use the in-repo splitway-bin PKGBUILD. PKGBUILDs in packaging/aur/ (also usable now via `makepkg -si`): - splitway: source build of daemon + CLI from the release tag; makedepends cargo; ships both binaries + unit; splitway.install prints the enable hint (Arch policy: no auto-enable); optdepends networkmanager / systemd-resolvconf. - splitway-bin: prebuilt from the release tarball (x86_64 + aarch64); provides/conflicts splitway. - splitway-gui: source egui GUI; depends splitway + libglvnd/libxkbcommon/ wayland/libx11/libxcursor/libxi/libxrandr; optdepends xdg-desktop-portal; splitway-gui.install mirrors the deb/rpm empty-group + drop-in invariant. CI (packaging.yml): - build-arch: archlinux container, non-root makepkg of the two source PKGBUILDs from THIS checkout (clean version — pacman has no ~dev channel because vercmp does not treat ~ as a pre-release marker), validates .SRCINFO + namcap (advisory). x86_64 .pkg.tar.zst artifacts. - test-arch: pacman -U local install (asserts binaries + unit + empty group), then a throwaway-RSA-key signed repo — repo-add --sign, pacman-key --add/--lsign, SigLevel = Required DatabaseOptional, pacman -Sy with verification ON. - publish-pages (release only): copies the .pkg.tar.zst into the persistent arch/release/x86_64 subtree, detach-signs each with the real key, repo-add (in a container) incrementally (old packages preserved), replaces Pages- hostile db/files symlinks with real signed copies, reuses splitway.gpg. Post-deploy: live `pacman -Sy splitway` with verification ON. - render-index.sh: Arch section (signed repo primary, makepkg alternative, AUR pending). DEFERRED (not here): the automated AUR ssh push — blocked on AUR registration reopening. The in-repo PKGBUILDs are the bridge. Validated locally: all PKGBUILD/.install bash-syntax-clean; actionlint clean; index renders with $arch left literal for pacman. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018GKGiqawfPb5fzMGHisrrF --- .github/workflows/packaging.yml | 166 +++++++++++++++++- packaging/aur/splitway-bin/PKGBUILD | 32 ++++ packaging/aur/splitway-bin/splitway.install | 26 +++ packaging/aur/splitway-gui/PKGBUILD | 51 ++++++ .../aur/splitway-gui/splitway-gui.install | 52 ++++++ packaging/aur/splitway/PKGBUILD | 42 +++++ packaging/aur/splitway/splitway.install | 26 +++ packaging/ci/render-index.sh | 21 ++- 8 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 packaging/aur/splitway-bin/PKGBUILD create mode 100644 packaging/aur/splitway-bin/splitway.install create mode 100644 packaging/aur/splitway-gui/PKGBUILD create mode 100644 packaging/aur/splitway-gui/splitway-gui.install create mode 100644 packaging/aur/splitway/PKGBUILD create mode 100644 packaging/aur/splitway/splitway.install diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index c34c842..c875d32 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -335,6 +335,120 @@ jobs: echo "ARM64 INSTALL OK" ' + # --- Arch: makepkg the source PKGBUILDs (x86_64) ----------------------------- + # pacman has no dev channel (vercmp does not treat ~ as a pre-release marker), + # so Arch always uses the CLEAN version. Builds from THIS checkout (the release + # tag may not exist yet on dev/PR) by pre-placing the source archive makepkg + # expects. + build-arch: + needs: meta + runs-on: ubuntu-latest + container: archlinux:latest + env: + VERSION: ${{ needs.meta.outputs.version }} + steps: + - name: Install build tooling + run: | + pacman -Syu --noconfirm --needed \ + base-devel git rust namcap sudo \ + libglvnd libxkbcommon wayland libx11 libxcursor libxi libxrandr + - uses: actions/checkout@v5 + - name: Set up a non-root build user (makepkg refuses root) + run: | + useradd -m builder + chown -R builder:builder . + - name: makepkg splitway + splitway-gui (local source, clean pkgver) + run: | + set -euo pipefail + mkdir -p dist + for pkg in splitway splitway-gui; do + # The PKGBUILD's source filename is -.tar.gz with a + # top-level dir splitway-; pre-place it so makepkg builds the + # checkout instead of downloading the tag. + sudo -u builder git config --global --add safe.directory "$PWD" + sudo -u builder git archive --format=tar.gz --prefix="splitway-$VERSION/" \ + -o "packaging/aur/$pkg/$pkg-$VERSION.tar.gz" HEAD + ( cd "packaging/aur/$pkg" \ + && sudo -u builder makepkg -f --noconfirm --nodeps --skipinteg \ + && sudo -u builder bash -c 'makepkg --printsrcinfo > .SRCINFO' ) + cp "packaging/aur/$pkg"/*.pkg.tar.zst dist/ + # Advisory lint (dlopen'd GL libs make namcap noisy; do not gate). + namcap "packaging/aur/$pkg/PKGBUILD" || true + namcap "packaging/aur/$pkg"/*.pkg.tar.zst || true + done + ls -la dist + - uses: actions/upload-artifact@v6 + with: + name: pkg-arch-x86_64 + path: dist/* + + # --- Arch: local install + ephemeral-key signed pacman repo (no secrets) ----- + test-arch: + needs: build-arch + runs-on: ubuntu-latest + container: archlinux:latest + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v8 + with: + name: pkg-arch-x86_64 + path: art + - name: Tooling + run: pacman -Syu --noconfirm --needed systemd python gnupg + - name: Local pacman -U install + run: | + set -euo pipefail + # Pull the GUI's runtime libs from the official repos, then install the + # built packages directly. + pacman -S --noconfirm --needed libglvnd libxkbcommon wayland libx11 libxcursor libxi libxrandr + pacman -U --noconfirm art/*.pkg.tar.zst + command -v splitway-daemon + command -v splitway + command -v splitway-gui + test -f /usr/lib/systemd/system/splitway.service + # GUI scriptlet created the empty opt-in group + getent group splitway + [ -z "$(getent group splitway | cut -d: -f4)" ] || { echo "group not empty"; exit 1; } + pacman -R --noconfirm splitway-gui splitway + - name: Ephemeral-key signed pacman repo (verification ON) + run: | + set -euo pipefail + export GNUPGHOME; GNUPGHOME="$(mktemp -d)"; chmod 700 "$GNUPGHOME" + cat > "$GNUPGHOME/keygen" <<'EOF' + %no-protection + Key-Type: RSA + Key-Length: 3072 + Key-Usage: sign + Name-Real: Splitway CI Throwaway + Name-Email: ci@example.com + Expire-Date: 0 + %commit + EOF + gpg --batch --gen-key "$GNUPGHOME/keygen" + FPR=$(gpg --list-keys --with-colons ci@example.com | awk -F: '/^fpr:/{print $5; exit}') + gpg --export --armor "$FPR" > /tmp/key.gpg + # Build a signed local repo: detach-sign each package, then repo-add. + mkdir -p repo && cp art/*.pkg.tar.zst repo/ + for p in repo/*.pkg.tar.zst; do + gpg --batch --yes --pinentry-mode loopback --default-key "$FPR" --detach-sign "$p" + done + ( cd repo && repo-add --sign --key "$FPR" splitway.db.tar.gz ./*.pkg.tar.zst ) + ( cd repo && python -m http.server 8081 >/tmp/http.log 2>&1 & ) + sleep 2 + # Trust the throwaway key in pacman's keyring. + pacman-key --init + pacman-key --add /tmp/key.gpg + pacman-key --lsign-key "$FPR" + cat >> /etc/pacman.conf <<'EOF' + + [splitway] + SigLevel = Required DatabaseOptional + Server = http://localhost:8081 + EOF + pacman -Sy --noconfirm splitway + command -v splitway-daemon + echo "SIGNED PACMAN INSTALL OK" + # --- publish the signed apt/dnf repos to GitHub Pages (push-only, SECRETS) -- # Never runs on pull_request. Serialized via a single concurrency group so two # near-simultaneous deploys queue instead of clobbering gh-pages. Drops into @@ -342,7 +456,7 @@ jobs: # gh-pages is persistent state, MERGED not wiped, so old versions + the other # channel survive. publish-pages: - needs: [meta, build-core, build-gui, test-install, test-signed-repo] + needs: [meta, build-core, build-gui, build-arch, test-install, test-signed-repo, test-arch] if: ${{ github.event_name != 'pull_request' }} runs-on: ubuntu-latest permissions: @@ -409,6 +523,36 @@ jobs: bash packaging/ci/build-dnf-repo.sh "gh-pages/rpm/$CHANNEL" "$GPG_FPR" cp /tmp/splitway.gpg gh-pages/splitway.gpg bash packaging/ci/render-index.sh "$GPG_FPR" > gh-pages/index.html + - name: Publish the signed pacman repo (release channel only, x86_64) + if: ${{ needs.meta.outputs.channel == 'release' }} + run: | + set -euo pipefail + ARCHDIR="gh-pages/arch/release/x86_64" + mkdir -p "$ARCHDIR" + cp art/*.pkg.tar.zst "$ARCHDIR"/ # merge: old packages persist + # Detach-sign each package with the real key (pacman SigLevel=Required). + for p in "$ARCHDIR"/*.pkg.tar.zst; do + rm -f "$p.sig" + gpg --batch --yes --pinentry-mode loopback --passphrase-file "$SPLITWAY_GPG_PASSFILE" \ + --default-key "$GPG_FPR" --detach-sign "$p" + done + # repo-add is an Arch tool; run it in a container over the mounted dir. + # Incremental: it updates entries in place and keeps old packages. + docker run --rm -v "$PWD/$ARCHDIR":/repo -w /repo archlinux:latest \ + bash -c 'repo-add splitway.db.tar.gz ./*.pkg.tar.zst' + sudo chown -R "$(id -u):$(id -g)" "$ARCHDIR" + # GitHub Pages does not serve symlinks; replace repo-add's db/files + # symlinks with real copies, and sign the DB (DatabaseOptional). + for n in db files; do + if [ -L "$ARCHDIR/splitway.$n" ]; then + rm -f "$ARCHDIR/splitway.$n" + cp "$ARCHDIR/splitway.$n.tar.gz" "$ARCHDIR/splitway.$n" + fi + done + rm -f "$ARCHDIR/splitway.db.tar.gz.sig" + gpg --batch --yes --pinentry-mode loopback --passphrase-file "$SPLITWAY_GPG_PASSFILE" \ + --default-key "$GPG_FPR" --detach-sign "$ARCHDIR/splitway.db.tar.gz" + cp "$ARCHDIR/splitway.db.tar.gz.sig" "$ARCHDIR/splitway.db.sig" - name: Commit + push gh-pages run: | set -euo pipefail @@ -465,3 +609,23 @@ jobs: command -v splitway-daemon echo LIVE_DNF_OK " + - name: Post-deploy pacman smoke (release channel only) + if: ${{ needs.meta.outputs.channel == 'release' }} + run: | + set -euo pipefail + for i in $(seq 1 60); do + if curl -fsI "$BASE/arch/release/x86_64/splitway.db" >/dev/null 2>&1; then break; fi + sleep 10 + [ "$i" = 60 ] && { echo "ERROR: pacman repo not live after 10m"; exit 1; } + done + docker run --rm -e BASE="$BASE" -e FPR="$GPG_FPR" archlinux:latest bash -euxc ' + pacman -Sy --noconfirm + curl -fsSL "$BASE/splitway.gpg" -o /tmp/key.gpg + pacman-key --init + pacman-key --add /tmp/key.gpg + pacman-key --lsign-key "$FPR" + printf "\n[splitway]\nSigLevel = Required DatabaseOptional\nServer = %s/arch/release/x86_64\n" "$BASE" >> /etc/pacman.conf + pacman -Sy --noconfirm splitway + command -v splitway-daemon + echo LIVE_PACMAN_OK + ' diff --git a/packaging/aur/splitway-bin/PKGBUILD b/packaging/aur/splitway-bin/PKGBUILD new file mode 100644 index 0000000..5ba4a7c --- /dev/null +++ b/packaging/aur/splitway-bin/PKGBUILD @@ -0,0 +1,32 @@ +# Maintainer: stslex +# +# Prebuilt Splitway core (daemon + CLI + unit) from the GitHub Release tarball — +# no Rust toolchain needed. Useful for aarch64 / ALARM (the hosted pacman repo +# is x86_64-only). Build with `cd packaging/aur/splitway-bin && makepkg -si`. +pkgname=splitway-bin +pkgver=0.0.5 +pkgrel=1 +pkgdesc="Domain-based split-DNS daemon + CLI (prebuilt from the release tarball)" +arch=('x86_64' 'aarch64') +url="https://github.com/stslex/splitway" +license=('MIT') +provides=('splitway') +conflicts=('splitway') +install="$pkgname.install" +source_x86_64=("https://github.com/stslex/splitway/releases/download/v$pkgver/splitway-$pkgver-linux-amd64.tar.gz") +source_aarch64=("https://github.com/stslex/splitway/releases/download/v$pkgver/splitway-$pkgver-linux-arm64.tar.gz") +sha256sums_x86_64=('SKIP') +sha256sums_aarch64=('SKIP') + +package() { + case "$CARCH" in + x86_64) local _dir="splitway-$pkgver-linux-amd64" ;; + aarch64) local _dir="splitway-$pkgver-linux-arm64" ;; + esac + cd "$_dir" + install -Dm755 splitway-daemon "$pkgdir/usr/bin/splitway-daemon" + install -Dm755 splitway "$pkgdir/usr/bin/splitway" + install -Dm644 splitway.service "$pkgdir/usr/lib/systemd/system/splitway.service" + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/splitway/LICENSE" + install -Dm644 README.md "$pkgdir/usr/share/doc/splitway/README.md" +} diff --git a/packaging/aur/splitway-bin/splitway.install b/packaging/aur/splitway-bin/splitway.install new file mode 100644 index 0000000..935f422 --- /dev/null +++ b/packaging/aur/splitway-bin/splitway.install @@ -0,0 +1,26 @@ +# pacman install scriptlet for the splitway-bin package (same as splitway). +# +# Per Arch policy the daemon is NOT auto-enabled/started on install — we only +# print the one-liner. The socket stays root-only (0600) unless the splitway-gui +# package's opt-in plumbing is also installed. + +post_install() { + systemctl daemon-reload >/dev/null 2>&1 || true + cat <<'EOM' + splitway installed. Enable + start the daemon with: + sudo systemctl enable --now splitway.service +EOM +} + +post_upgrade() { + systemctl daemon-reload >/dev/null 2>&1 || true + systemctl try-restart splitway.service >/dev/null 2>&1 || true +} + +pre_remove() { + systemctl disable --now splitway.service >/dev/null 2>&1 || true +} + +post_remove() { + systemctl daemon-reload >/dev/null 2>&1 || true +} diff --git a/packaging/aur/splitway-gui/PKGBUILD b/packaging/aur/splitway-gui/PKGBUILD new file mode 100644 index 0000000..87b43e3 --- /dev/null +++ b/packaging/aur/splitway-gui/PKGBUILD @@ -0,0 +1,51 @@ +# Maintainer: stslex +# +# Source build of the Splitway desktop GUI (egui). Build with +# `cd packaging/aur/splitway-gui && makepkg -si`. Installing it adds the opt-in +# socket-group plumbing (see splitway-gui.install) so an in-group user can drive +# the daemon without sudo; the group starts EMPTY, so the default posture is +# unchanged until you run `usermod -aG splitway `. +pkgname=splitway-gui +pkgver=0.0.5 +pkgrel=1 +pkgdesc="Splitway desktop GUI (egui front-end over the daemon socket)" +arch=('x86_64' 'aarch64') +url="https://github.com/stslex/splitway" +license=('MIT') +depends=('splitway' 'libglvnd' 'libxkbcommon' 'wayland' 'libx11' 'libxcursor' 'libxi' 'libxrandr') +makedepends=('cargo') +optdepends=('xdg-desktop-portal: file dialog (a portal backend is also needed)') +install="$pkgname.install" +# Builds from the same repo tarball as `splitway`; its top-level dir is +# splitway-$pkgver regardless of this package's name. +source=("$pkgname-$pkgver.tar.gz::https://github.com/stslex/splitway/archive/refs/tags/v$pkgver.tar.gz") +sha256sums=('SKIP') + +prepare() { + cd "splitway-$pkgver" + export RUSTUP_TOOLCHAIN=stable + cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')" +} + +build() { + cd "splitway-$pkgver" + export RUSTUP_TOOLCHAIN=stable + export CARGO_TARGET_DIR=target + cargo build --frozen --release -p splitway-gui +} + +package() { + cd "splitway-$pkgver" + install -Dm755 target/release/splitway-gui "$pkgdir/usr/bin/splitway-gui" + install -Dm644 packaging/desktop/io.github.stslex.splitway.desktop \ + "$pkgdir/usr/share/applications/io.github.stslex.splitway.desktop" + install -Dm644 packaging/gui/socket-group.conf \ + "$pkgdir/usr/share/splitway/socket-group.conf" + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + # hicolor icons (8 PNG sizes + scalable SVG), named by the app_id. + local f rel + while IFS= read -r -d '' f; do + rel="${f#packaging/icons/}" + install -Dm644 "$f" "$pkgdir/usr/share/icons/$rel" + done < <(find packaging/icons/hicolor -type f -print0) +} diff --git a/packaging/aur/splitway-gui/splitway-gui.install b/packaging/aur/splitway-gui/splitway-gui.install new file mode 100644 index 0000000..d5c9eb8 --- /dev/null +++ b/packaging/aur/splitway-gui/splitway-gui.install @@ -0,0 +1,52 @@ +# pacman install scriptlet for splitway-gui — opt-in socket-group plumbing. +# +# EMPTY-GROUP INVARIANT: this creates an EMPTY `splitway` group and installs the +# socket-group drop-in. With no members the drop-in is a no-op and the socket +# posture is identical to the default 0600 root-only. This script NEVER adds a +# user — the only grant is a human running `usermod -aG splitway ` and +# re-logging in. Mirrors the deb/rpm maintainer scripts. + +_apply_dropin() { + getent group splitway >/dev/null 2>&1 || groupadd --system splitway + install -d -m 0755 /etc/systemd/system/splitway.service.d + install -m 0644 /usr/share/splitway/socket-group.conf \ + /etc/systemd/system/splitway.service.d/socket-group.conf + systemctl daemon-reload >/dev/null 2>&1 || true + systemctl try-restart splitway.service >/dev/null 2>&1 || true + update-desktop-database -q /usr/share/applications 2>/dev/null || true + gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor 2>/dev/null || true +} + +post_install() { + _apply_dropin + cat <<'EOM' + + splitway-gui installed. The daemon socket is still ROOT-ONLY. To drive it + without sudo, join the (empty) splitway group and re-login: + + sudo usermod -aG splitway "$USER" + + Until a user is added the group is empty and the posture is identical to + 0600 root-only. Group membership == control of system split-DNS. +EOM +} + +post_upgrade() { + _apply_dropin +} + +pre_remove() { + rm -f /etc/systemd/system/splitway.service.d/socket-group.conf + rmdir --ignore-fail-on-non-empty /etc/systemd/system/splitway.service.d 2>/dev/null || true + if getent group splitway >/dev/null 2>&1 && \ + [ -z "$(getent group splitway | awk -F: '{print $4}')" ]; then + groupdel splitway >/dev/null 2>&1 || true + fi +} + +post_remove() { + systemctl daemon-reload >/dev/null 2>&1 || true + systemctl try-restart splitway.service >/dev/null 2>&1 || true + update-desktop-database -q /usr/share/applications 2>/dev/null || true + gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor 2>/dev/null || true +} diff --git a/packaging/aur/splitway/PKGBUILD b/packaging/aur/splitway/PKGBUILD new file mode 100644 index 0000000..585f5bf --- /dev/null +++ b/packaging/aur/splitway/PKGBUILD @@ -0,0 +1,42 @@ +# Maintainer: stslex +# +# Source build of the Splitway core: daemon + CLI + systemd unit. Build it now +# with `cd packaging/aur/splitway && makepkg -si`; this PKGBUILD is also what +# the AUR `splitway` package will use once AUR registration reopens. The hosted +# signed pacman repo (https://stslex.github.io/splitway/arch) ships a prebuilt +# copy of this same package. +pkgname=splitway +pkgver=0.0.5 +pkgrel=1 +pkgdesc="Domain-based split-DNS daemon + CLI: route selected domains through the VPN's DNS, everything else direct" +arch=('x86_64' 'aarch64') +url="https://github.com/stslex/splitway" +license=('MIT') +makedepends=('cargo') +optdepends=('networkmanager: VPN detection via D-Bus' + 'systemd-resolvconf: split-DNS via systemd-resolved') +install="$pkgname.install" +source=("$pkgname-$pkgver.tar.gz::https://github.com/stslex/splitway/archive/refs/tags/v$pkgver.tar.gz") +sha256sums=('SKIP') + +prepare() { + cd "$pkgname-$pkgver" + export RUSTUP_TOOLCHAIN=stable + cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')" +} + +build() { + cd "$pkgname-$pkgver" + export RUSTUP_TOOLCHAIN=stable + export CARGO_TARGET_DIR=target + cargo build --frozen --release -p splitway-daemon -p splitway-cli +} + +package() { + cd "$pkgname-$pkgver" + install -Dm755 target/release/splitway-daemon "$pkgdir/usr/bin/splitway-daemon" + install -Dm755 target/release/splitway "$pkgdir/usr/bin/splitway" + install -Dm644 packaging/systemd/splitway.service "$pkgdir/usr/lib/systemd/system/splitway.service" + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" +} diff --git a/packaging/aur/splitway/splitway.install b/packaging/aur/splitway/splitway.install new file mode 100644 index 0000000..12aabd9 --- /dev/null +++ b/packaging/aur/splitway/splitway.install @@ -0,0 +1,26 @@ +# pacman install scriptlet for the splitway core package. +# +# Per Arch policy the daemon is NOT auto-enabled/started on install — we only +# print the one-liner. The socket stays root-only (0600) unless the splitway-gui +# package's opt-in plumbing is also installed. + +post_install() { + systemctl daemon-reload >/dev/null 2>&1 || true + cat <<'EOM' + splitway installed. Enable + start the daemon with: + sudo systemctl enable --now splitway.service +EOM +} + +post_upgrade() { + systemctl daemon-reload >/dev/null 2>&1 || true + systemctl try-restart splitway.service >/dev/null 2>&1 || true +} + +pre_remove() { + systemctl disable --now splitway.service >/dev/null 2>&1 || true +} + +post_remove() { + systemctl daemon-reload >/dev/null 2>&1 || true +} diff --git a/packaging/ci/render-index.sh b/packaging/ci/render-index.sh index 2a6a2f6..2e4d3ec 100644 --- a/packaging/ci/render-index.sh +++ b/packaging/ci/render-index.sh @@ -75,7 +75,26 @@ gpgkey=${BASE}/splitway.gpg EOF sudo dnf install splitway - +

Arch Linux (pacman, x86_64)

+

Signed repository (recommended)

+
curl -fsSL ${BASE}/splitway.gpg -o /tmp/splitway.gpg
+sudo pacman-key --add /tmp/splitway.gpg
+sudo pacman-key --lsign-key ${FPR}
+sudo tee -a /etc/pacman.conf <<'EOF'
+
+[splitway]
+SigLevel = Required DatabaseOptional
+Server = ${BASE}/arch/release/\$arch
+EOF
+sudo pacman -Sy splitway        # add splitway-gui for the desktop app
+

The hosted pacman repo is x86_64 only. On aarch64, build the +in-repo PKGBUILD below or use splitway-bin (prebuilt from the +release tarball).

+

Build from the in-repo PKGBUILD

+
git clone https://github.com/stslex/splitway
+cd splitway/packaging/aur/splitway && makepkg -si
+

AUR packages (splitway, splitway-bin, +splitway-gui) are pending AUR account registration reopening.

Other

NixOS: use the flake's nixosModules.default (see the From 85156c1da08af542ea8cc012dc43027b85aa6842 Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 24 Jun 2026 23:49:40 +0300 Subject: [PATCH 08/28] docs(packaging): apt/dnf/Arch install + packaging guide + Phase 6 done MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs only — no code. - README: Install sections for apt / dnf (both channels) and Arch (signed pacman repo primary, in-repo PKGBUILD makepkg alternative; AUR pending), with the key-verification note. NixOS section left as-is. - packaging/README.md: a Distribution packages (deb / rpm / pacman) section — the two-package split, dev vs release channels + ~dev versioning, the glibc 2.31 floor, the dlopen'd GL deps, the GUI socket-group drop-in + empty-group invariant, and the pacman specifics (x86_64-only, no ~dev channel, detached signing, repo-add incrementality). - docs/design/linux-distro-packaging.md: the durable record — decisions 1-7, the channel/version topology, dep lists, signing + merge mechanics across all three formats (incl. the RSA-not-EdDSA and cargo-deb-vs-generate-rpm path-resolution gotchas), the two-layer test design, the pacman-now/AUR-later Arch strategy, and the signing key. - ROADMAP.md: Phase 6 marked done (Linux), noting the two-package design supersedes the original one-package sketch; macOS Homebrew + the automated AUR push remain deferred. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018GKGiqawfPb5fzMGHisrrF --- README.md | 72 ++++++++++ ROADMAP.md | 51 +++---- docs/design/linux-distro-packaging.md | 184 ++++++++++++++++++++++++++ packaging/README.md | 48 +++++++ 4 files changed, 332 insertions(+), 23 deletions(-) create mode 100644 docs/design/linux-distro-packaging.md diff --git a/README.md b/README.md index 1db294b..18e30e6 100755 --- a/README.md +++ b/README.md @@ -240,6 +240,78 @@ nix develop # dev shell with cargo, rustc, rustfmt, clippy, rust-analyzer The flake also exposes `nixosModules.default` for installing Splitway as a systemd service on a NixOS host — see [Install (NixOS)](#install-nixos) below. +## Install (Debian/Ubuntu, Fedora, Arch) + +Signed apt / dnf / pacman repositories on GitHub Pages, in two channels — +**release** (stable) and **dev** (every push to `dev`). Two packages: +`splitway` (daemon + CLI + service) and `splitway-gui` (the desktop app, which +depends on `splitway`). See the +[landing page](https://stslex.github.io/splitway/) for the full snippets and the +signing-key fingerprint, and [packaging/](packaging/README.md) for the details. + +Verify the key fingerprint (`gpg --show-keys splitway.gpg`) against the +maintainer's published value before trusting the repo. + +### Debian / Ubuntu (apt) + +```sh +curl -fsSL https://stslex.github.io/splitway/splitway.gpg \ + | sudo tee /usr/share/keyrings/splitway.gpg > /dev/null +echo "deb [signed-by=/usr/share/keyrings/splitway.gpg] https://stslex.github.io/splitway/deb/release stable main" \ + | sudo tee /etc/apt/sources.list.d/splitway.list +sudo apt-get update +sudo apt-get install splitway # add splitway-gui for the desktop app +``` + +For the dev channel, point at `…/deb/dev` instead. + +### Fedora / RHEL (dnf) + +```sh +sudo tee /etc/yum.repos.d/splitway.repo <<'EOF' +[splitway] +name=Splitway +baseurl=https://stslex.github.io/splitway/rpm/release +enabled=1 +gpgcheck=1 +repo_gpgcheck=1 +gpgkey=https://stslex.github.io/splitway/splitway.gpg +EOF +sudo dnf install splitway # add splitway-gui for the desktop app +``` + +For the dev channel, use `…/rpm/dev`. The core package is musl-static (runs on +any glibc baseline, including RHEL 8); the GUI targets glibc 2.31+, so RHEL 8 is +uncovered for `splitway-gui` only. + +### Arch Linux (pacman, x86_64) + +Self-hosted signed pacman repo (AUR packages are pending AUR registration +reopening): + +```sh +curl -fsSL https://stslex.github.io/splitway/splitway.gpg -o /tmp/splitway.gpg +sudo pacman-key --add /tmp/splitway.gpg +sudo pacman-key --lsign-key # from gpg --show-keys /tmp/splitway.gpg +sudo tee -a /etc/pacman.conf <<'EOF' + +[splitway] +SigLevel = Required DatabaseOptional +Server = https://stslex.github.io/splitway/arch/release/$arch +EOF +sudo pacman -Sy splitway # add splitway-gui for the desktop app +``` + +x86_64 only. On aarch64, or to build from source, use the in-repo PKGBUILDs: + +```sh +cd packaging/aur/splitway && makepkg -si # or splitway-bin (prebuilt), splitway-gui +``` + +The `splitway-gui` package adds an opt-in `splitway` group; it starts **empty**, +so the daemon socket stays root-only until you run +`sudo usermod -aG splitway "$USER"` and re-login. + ## Install (NixOS) On NixOS the flake's `nixosModules.default` takes you from zero to a running diff --git a/ROADMAP.md b/ROADMAP.md index bb9d17d..e1e6ade 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -65,6 +65,17 @@ landed, on the abstraction split that keeps each one a small isolated impl. - GUI: `vpn_name` picker over the live interface list (free-text fallback kept), a Resync button, immediate refresh after every change, and an opaque grouped visual pass. +- **Phase 6 — Linux distribution packaging.** Signed apt / dnf / pacman repos on + GitHub Pages (release + dev channels), shipped as **two** lockstep-versioned + packages — `splitway` (daemon + CLI + unit, musl-static) and `splitway-gui` + (egui, glibc 2.31 floor) — plus in-repo Arch PKGBUILDs. This supersedes the + original "one package" sketch: the GUI's GL/glibc stack shouldn't burden a + headless or CLI-only install, and the security-critical core stays minimal and + statically linked; the `>=` GUI→core dep covers the version axis. The opt-in + socket-group lands as an empty-group + service drop-in in the GUI package + (no-op until a human joins the group). macOS Homebrew and the automated AUR + push are deferred. See + [docs/design/linux-distro-packaging.md](docs/design/linux-distro-packaging.md). ## Frontend: egui is interim, Tauri is the target @@ -139,31 +150,25 @@ seam added in 5b. `read_link_state` reusing 5b's parsing approach — so the daemon can diff intended-vs-actual and surface drift: *reality* alongside Phase 5's *belief*. -### Phase 6 — packaging (distribution to other users) - -The author's daily-driver path **already exists** via the Phase 0.5 flake + -`nixosModule` — that is the iteration channel. So this phase is **distribution to -other users, not the author's iteration unblock**: general-distro packaging gets -Splitway onto non-Nix machines, which is its own work because **generic Linux -binaries do not run on NixOS** (the dynamic linker is in the Nix store, not -`/lib64`) and immutable-`/usr` hosts need writable install paths. - -- **One package** `splitway` containing daemon + cli + gui + the service unit, at - a single version. This sidesteps the GUI↔daemon version matrix entirely: there - are no separately-versioned packages to mismatch. `postinst` restarts - `splitway.service` on upgrade so the running daemon always matches the new - binaries; the existing version-peek (`VERSION_MISMATCH_PREFIX`) covers the brief - upgrade window. **On NixOS the module is the single-version equivalent.** -- **Linux first:** tarball + apt / dnf / pacman repos on GitHub Pages, with **dev - and release channels** as separate Pages subtrees. Pattern reusable from - `stslex/claude-desktop-linux` — take the packaging/publishing half, drop the - repackage half, and source artifacts from `cargo build --release`. Watch the - Pages "full-site replace" trap that makes concurrent dev + stable deploys - clobber each other. -- **Then macOS:** Homebrew tap / `.pkg` + launchd, with the Gatekeeper / +### Phase 6 — packaging (Linux done; macOS Homebrew deferred) + +**Linux distro packaging shipped** — see the Done section above and +[docs/design/linux-distro-packaging.md](docs/design/linux-distro-packaging.md). +Generic Linux binaries don't run on NixOS (the dynamic linker is in the Nix +store, not `/lib64`) and immutable-`/usr` hosts need writable install paths, so +this was its own work alongside the flake. What landed: two lockstep-versioned +packages (`splitway` musl-static + `splitway-gui` glibc), signed apt / dnf / +pacman repos on GitHub Pages with **dev and release channels** as separate +subtrees (merge-not-wipe deploys, sidestepping the "full-site replace" trap), +and in-repo Arch PKGBUILDs. + +Remaining sub-track: + +- **macOS:** Homebrew tap / `.pkg` + launchd, with the Gatekeeper / notarization tail (unsigned vs Apple-Developer-signed) called out as a sub-decision. -- The dev channel is for iteration now; the public `v0.1.0` tag waits for Tauri. + +The dev channel is for iteration now; the public `v0.1.0` tag waits for Tauri. ### Phase 7 — native Tauri GUI diff --git a/docs/design/linux-distro-packaging.md b/docs/design/linux-distro-packaging.md new file mode 100644 index 0000000..46afd16 --- /dev/null +++ b/docs/design/linux-distro-packaging.md @@ -0,0 +1,184 @@ +# Linux distribution packaging — apt / dnf / pacman on GitHub Pages + +Status: implemented (Linux). Lands with the deb/rpm packages +(`splitway-daemon/Cargo.toml`, `splitway-gui/Cargo.toml`), the Arch PKGBUILDs +(`packaging/aur/`), the signed GitHub Pages repos, and the Phase-6 packaging CI +(`.github/workflows/packaging.yml` + `packaging/ci/`). The macOS Homebrew track +is separate and deferred. + +This is the durable record for getting Splitway onto non-Nix Linux machines. +The NixOS path (the flake's `nixosModules.default`) is unchanged and remains the +author's iteration channel. + +## The decision + +1. **Two packages, lockstep-versioned — not one.** This supersedes the + ROADMAP's original "one package" sketch. + - `splitway` — `splitway-daemon` + the `splitway` CLI + the systemd unit. + Built **musl-static** (`*-unknown-linux-musl`), so it has near-zero + shared-library dependencies and runs on any glibc/musl baseline. It is the + security-critical root daemon; musl sidesteps the glibc-baseline trap on + old Debian/RHEL. + - `splitway-gui` — the egui binary. Built **glibc** against a low floor. + `Depends`/`Requires: splitway (>= )`. + - Why split: the GUI drags in a GL/X11/wayland stack and a glibc baseline + that a headless / CLI-only install should not have to carry, and the + security-critical core stays minimal and statically linked. The cost — a + GUI↔daemon version axis — is handled by decision 3. +2. **Version source of truth = `splitway-daemon/Cargo.toml` `version`** (same as + `release.yml`). Both packages and all tarballs are stamped with it at the + packaging layer (`cargo deb --deb-version`, `cargo generate-rpm + --set-metadata version=…`), so the dev suffix never has to live in a + `Cargo.toml` (it is not valid semver). Per-crate `version` fields drift and + are ignored for packaging. +3. **GUI `Depends >=`, not `=`.** The real compatibility contract is the + (currently unversioned) GUI↔daemon IPC; `>=` survives Pages-mirror skew and + partial upgrades, and CI rewrites the floor to the exact built version for + the dev channel so a `~dev` core still satisfies it. +4. **Arch = AUR shape, but a self-hosted signed pacman repo now.** AUR account + registration is disabled, so the automated AUR push is deferred; the in-repo + PKGBUILDs (`splitway`, `splitway-bin`, `splitway-gui`) are usable today via + `makepkg -si`, and the hosted pacman repo (x86_64) ships prebuilt copies. +5. **Pages hosts apt + dnf + pacman, all GPG-signed**, with separate `dev` and + `release` channel subtrees. +6. **Pages deploys MERGE, never wipe.** `gh-pages` is persistent state; + metadata is regenerated incrementally per channel, so old versions and the + other channel always survive (the "full-site replace" trap). +7. **Socket-group is opt-in only.** The GUI package creates an **empty** + `splitway` group and installs a service drop-in enabling `--socket-group`. + Empty group ⇒ posture identical to `0600 root` (a no-op). The only grant is + a human running `usermod -aG splitway $USER` + re-login. Maintainer scripts + **never** add a user. Mirrors `nix/tests/socket-group.nix`. + +## Channel & version topology + +| Trigger | Channel | Version | Publishes to | +|--------------------|---------|----------------------|--------------| +| push → `master` | release | `` | `deb/release`, `rpm/release`, `arch/release/x86_64`, GitHub Release (tarballs) | +| push → `dev` | dev | `~dev..` | `deb/dev`, `rpm/dev` | +| pull_request | — | `~dev..` | nothing (build + test only) | + +`~dev` sorts **below** the clean release in dpkg and rpm (≥4.10), so a tester +with both repos enabled upgrades dev → release cleanly. + +**pacman has no dev channel.** `vercmp` does not treat `~` as a pre-release +marker the way dpkg/rpm do, so a dev pacman channel (if ever added) must use the +Arch VCS convention `pkgver=.r.g`, not `~dev`. The hosted +pacman repo is therefore **release-only and x86_64-only** (mainline Arch is +x86_64; aarch64/ALARM users use the `splitway-bin` PKGBUILD). + +## Dependencies + +The core package is musl-static ⇒ **no** shared-lib deps; `network-manager` / +`systemd-resolved` are `Recommends` (runtime prerequisites for actually applying +rules) not `Depends`, so it still installs for inspection or on other-resolver +hosts. + +The GUI's eframe/glow stack is the subtle part: **winit/glow `dlopen` the +windowing libraries at runtime**, so they are absent from the binary's ELF +`DT_NEEDED`. Neither cargo-deb's `$auto` (dpkg-shlibdeps) nor +cargo-generate-rpm's ELF-based `auto-req` can see them — they **must be +hardcoded**: + +- Debian: `libgl1, libx11-6, libxcursor1, libxi6, libxrandr2, + libwayland-client0, libxkbcommon0`, plus `libc6 (>= 2.31)` to pin the floor. +- Fedora: `mesa-libGL, libX11, libXcursor, libXi, libXrandr, libwayland-client, + libxkbcommon` (auto-req still derives the glibc/libgcc floor from the + linked-against sonames). +- Arch: `libglvnd libxkbcommon wayland libx11 libxcursor libxi libxrandr`. + +`xdg-desktop-portal` + a backend (`-gtk`/`-wlr`/`-kde`) is `Recommends`: rfd's +file dialog uses the portal here (no GTK linked) and **silently no-ops** without +a running portal. + +**glibc floor: 2.31** (`debian:bullseye` / `ubuntu:20.04`). The GUI is built +inside a bullseye container so the declared floor is true. RHEL 8 (glibc 2.28) +is intentionally uncovered **for the GUI**; the musl-static core still runs +there. + +## Signing & merge mechanics + +One RSA GPG key signs all three formats. **RSA, not EdDSA:** `rpm --addsign` +only produces a verifiable header signature with an RSA key (an EdDSA key +silently yields no `RPMTAG_RSAHEADER`). + +- **apt** (`packaging/ci/build-apt-repo.sh`): `apt-ftparchive` builds per-arch + `Packages` (a combined index split by `Architecture`) and the suite + `Release`; gpg writes `InRelease` (clearsigned) + `Release.gpg`. Clients use + `[signed-by=/usr/share/keyrings/splitway.gpg]`. +- **dnf** (`packaging/ci/build-dnf-repo.sh`): `rpm --addsign` header-signs every + rpm, `createrepo_c` regenerates `repodata/`, gpg writes + `repomd.xml.asc`. Clients use `gpgcheck=1` + `repo_gpgcheck=1`. +- **pacman**: each `.pkg.tar.zst` gets a detached `.sig`; `repo-add` rebuilds + `splitway.db.tar.gz` incrementally (old packages preserved). `repo-add` runs + in an `archlinux` container (it is an Arch tool); all gpg signing happens on + the host. GitHub Pages does not serve symlinks, so `repo-add`'s `*.db`/`*.files` + symlinks are replaced with real copies. Clients use + `SigLevel = Required DatabaseOptional`. + +All three regenerate **only the current channel's** metadata against whatever is +already in `gh-pages`, then commit — merge, never wipe. The publish job is +serialized by a single `pages-deploy` concurrency group +(`cancel-in-progress: false`) so two deploys queue instead of clobbering. + +The passphrase-protected real key is fed via loopback + a 0600 passphrase file +(`SPLITWAY_GPG_PASSFILE`), never on a command line. + +### cargo-deb vs cargo-generate-rpm asset paths + +Verified against cargo-deb 3.7.0 / cargo-generate-rpm 0.21.0, the two tools +resolve non-`target/` asset paths against **different** bases — an intentional +skew in the two metadata blocks: + +- **cargo-deb**: relative to the crate's manifest dir ⇒ workspace-root files + (`LICENSE`, `README.md`, `packaging/…`) need `../`. +- **cargo-generate-rpm**: relative to the invocation dir (workspace root) ⇒ bare + paths. + +cargo-deb's systemd-units integration only **generates** the enable/start/stop +maintainer scripts when a `maintainer-scripts` directory is also set (even an +empty one — `packaging/deb-maintainer-scripts/`); without it the unit is +installed but never enabled. + +## Two-layer test design (gates every PR, no secrets) + +1. **Local-artifact install**: install the built `.deb`/`.rpm`/`.pkg.tar.zst` + directly in `debian:bookworm`, `ubuntu:22.04`, `fedora:latest`, `archlinux`; + assert binaries run, the unit validates (`systemd-analyze verify`), the GUI + pulls `splitway` + GL deps, the empty `splitway` group exists, the `.desktop` + validates. +2. **Ephemeral-key signed-repo round trip**: generate a throwaway RSA key, build + + sign local apt/dnf/pacman repos from the artifacts (the *same* scripts the + real publish uses), serve over localhost, install with signature + verification ON. Proves metadata + signing + verify end-to-end with no + production secret. + +On every push the publish job additionally runs a **post-deploy smoke** against +the just-published live channel with the real key + verification ON. + +arm64: the core is cross-built (musl); the GUI is built on a native arm64 +runner (bullseye container, no QEMU). arm64 install is smoke-tested under QEMU +(best-effort). + +## Arch: pacman now, AUR deferred + +`packaging/aur/` holds `splitway` (source), `splitway-bin` (prebuilt from the +release tarball, x86_64 + aarch64), and `splitway-gui` (source), each with an +`.install` mirroring the deb/rpm scriptlets (incl. the empty-group invariant). +CI builds the source PKGBUILDs from the checkout (the release tag may not exist +on a dev/PR run) and validates `.SRCINFO` + `namcap`. + +**Deferred:** the automated `ssh://aur@aur.archlinux.org/.git` push, blocked +on AUR registration reopening. Design preserved for then: per package +`makepkg --printsrcinfo > .SRCINFO`, commit + push idempotently, release-only. + +## The signing key + +RSA. The workflow derives the armored public key from `GPG_PRIVATE_KEY` and +publishes it to `gh-pages/splitway.gpg` +(`https://stslex.github.io/splitway/splitway.gpg`). The maintainer holds +`GPG_PRIVATE_KEY` + `GPG_PASSPHRASE` as repository secrets. + +Fingerprint: confirm with `gpg --show-keys splitway.gpg` against the published +key. (Record the provisioned key's fingerprint here once the maintainer sets up +the secret; it is public project infrastructure, not sensitive.) diff --git a/packaging/README.md b/packaging/README.md index 6b43fbc..27e51dc 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -77,6 +77,54 @@ unchanged (`0600`, root-only). `SIGTERM` (systemd stop / `kill`) makes the daemon revert active DNS rules before exiting, so a stop never leaves the system half-configured. +## Distribution packages (deb / rpm / pacman) + +The manual `install` above is for hacking on a checkout. For real installs there +are signed apt / dnf / pacman repos on GitHub Pages (see the README's install +section). This subsection records the packaging specifics; the full design +rationale is in +[docs/design/linux-distro-packaging.md](../docs/design/linux-distro-packaging.md). + +**Two packages, lockstep-versioned at the daemon's `Cargo.toml` version:** + +- `splitway` — daemon + CLI + unit, built **musl-static** + (`*-unknown-linux-musl`), so it has no shared-lib deps and runs on any + glibc/musl baseline. `network-manager` / `systemd-resolved` are `Recommends`, + not `Depends`. cargo-deb's systemd integration generates the + enable/start/restart/stop maintainer scripts (it needs the empty + `packaging/deb-maintainer-scripts/` dir to do so); the rpm uses equivalent raw + `/bin/sh` scriptlets. +- `splitway-gui` — the egui binary, **glibc**, built against a **2.31 floor** + (`debian:bullseye` / `ubuntu:20.04`), so `Depends: splitway (>=), libc6 (>= + 2.31), libgl1 …`. winit/glow `dlopen` the GL/X11/wayland libs, so they are not + in the ELF and must be hardcoded (dep auto-detection misses them). RHEL 8 is + intentionally uncovered for the GUI; the musl core still works there. + +**GUI socket-group drop-in.** The `splitway-gui` package's maintainer scripts +create an **empty** `splitway` group and copy +`packaging/gui/socket-group.conf` into +`/etc/systemd/system/splitway.service.d/`, switching the daemon to group-socket +mode (`0660 root:splitway`, dir `0750`). **Empty-group invariant:** with no +members the drop-in is a no-op — the posture is identical to the default `0600` +root-only socket. The scripts **never add a user**; the only grant is a human +running `usermod -aG splitway ` + re-login (same contract as the NixOS +`unprivilegedGui` option and `nix/tests/socket-group.nix`). Removing the package +deletes the drop-in and (if empty) the group, reverting to root-only. + +**Channels.** `release` (push → `master`, clean ``) and `dev` (push → +`dev`, `~dev..`) are separate Pages subtrees. `~dev` sorts +below the release in dpkg and rpm, so dev → release upgrades cleanly. Deploys +**merge** the channel into persistent `gh-pages` state — old versions and the +other channel survive. + +**Arch.** The hosted pacman repo is **release-only and x86_64-only**. pacman's +`vercmp` does not treat `~` as a pre-release marker, so there is no `~dev` +pacman channel (a future one would use `pkgver=.r.g`). Packages are +detach-signed after build (`*.pkg.tar.zst.sig`) and `repo-add` rebuilds the DB +**incrementally** (old packages preserved); clients verify with `SigLevel = +Required DatabaseOptional`. aarch64 users use the `splitway-bin` PKGBUILD in +[aur/](aur/). The automated AUR push is deferred until AUR registration reopens. + ## macOS (launchd) `launchd/com.splitway.daemon.plist` runs `splitway-daemon run` as a From e6f221f05dc85199708215c11ca5824a5d4ec884 Mon Sep 17 00:00:00 2001 From: stslex Date: Thu, 25 Jun 2026 00:02:44 +0300 Subject: [PATCH 09/28] refactor(packaging): defensive hardening from self-review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No behavior change for the current (validated) inputs — robustness only, flagged by an adversarial self-review of the diff: - build-apt-repo.sh: `break` after the first Architecture line in the per-arch Packages filter (the canonical value; a valid stanza has exactly one). - packaging.yml: `g` flag on the GUI core-dep-floor seds (defensive if more `splitway (>=` references are ever added). - packaging.yml: comment that test-arm64-smoke is intentionally not a publish gate (best-effort under QEMU, continue-on-error). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_018GKGiqawfPb5fzMGHisrrF --- .github/workflows/packaging.yml | 6 ++++-- packaging/ci/build-apt-repo.sh | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index c875d32..04c0a79 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -142,8 +142,8 @@ jobs: # GUI Depends splitway (>= ): for dev the floor must be # the ~dev version so the same-run dev core satisfies it (a clean # release core, sorting higher, satisfies it too). No-op for release. - sed -i "s|splitway (>= [^)]*)|splitway (>= ${PKGVER})|" splitway-gui/Cargo.toml - sed -i "s|splitway = \">= [^\"]*\"|splitway = \">= ${PKGVER}\"|" splitway-gui/Cargo.toml + sed -i "s|splitway (>= [^)]*)|splitway (>= ${PKGVER})|g" splitway-gui/Cargo.toml + sed -i "s|splitway = \">= [^\"]*\"|splitway = \">= ${PKGVER}\"|g" splitway-gui/Cargo.toml grep -n "splitway (>=\|splitway = \">=" splitway-gui/Cargo.toml - name: Package deb + rpm + tarball run: | @@ -456,6 +456,8 @@ jobs: # gh-pages is persistent state, MERGED not wiped, so old versions + the other # channel survive. publish-pages: + # test-arm64-smoke is intentionally NOT a gate: it is best-effort + # (continue-on-error under QEMU) and must not block a publish. needs: [meta, build-core, build-gui, build-arch, test-install, test-signed-repo, test-arch] if: ${{ github.event_name != 'pull_request' }} runs-on: ubuntu-latest diff --git a/packaging/ci/build-apt-repo.sh b/packaging/ci/build-apt-repo.sh index 8edb07d..af96ebd 100755 --- a/packaging/ci/build-apt-repo.sh +++ b/packaging/ci/build-apt-repo.sh @@ -42,7 +42,7 @@ filter_arch() { # (reads a Packages file on stdin) arch = "" n = split($0, lines, "\n") for (i = 1; i <= n; i++) - if (lines[i] ~ /^Architecture: /) { arch = lines[i]; sub(/^Architecture: /, "", arch) } + if (lines[i] ~ /^Architecture: /) { arch = lines[i]; sub(/^Architecture: /, "", arch); break } if (arch == want || arch == "all") print }' } From 3cb9760de3b0e6470a063ac61d86be6b60249170 Mon Sep 17 00:00:00 2001 From: stslex Date: Thu, 25 Jun 2026 09:19:15 +0300 Subject: [PATCH 10/28] =?UTF-8?q?fix(packaging):=20address=20PR=20#36=20re?= =?UTF-8?q?view=20=E2=80=94=20CI=20build-blockers=20+=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the Codex + self-review findings on the Phase 6 packaging PR. Build-blocking CI fixes: - static-linkage gate: assert no dynamic interpreter instead of requiring the literal "statically linked" (musl x86_64 is "static-pie linked") — unblocks build-core (amd64) and the whole deb/rpm pipeline - test-arch: read the GPG fingerprint from fpr field 10, not 5 (5 is empty) - AUR splitway-bin: rename splitway.install -> splitway-bin.install so install="$pkgname.install" resolves (makepkg no longer aborts) - build-gui: CARGO_NET_RETRY + a retrying `cargo fetch --locked` before the offline build, hardening the transient arm64 crates.io failure Supply-chain / correctness: - apt: dearmor the published (armored) key into the binary keyring at every consumption site (works on every apt version; the published file stays armored for rpm --import / pacman-key) - publish: fail-closed signature verification (apt InRelease+Release.gpg, dnf repomd + per-rpm RSA header, pacman db+pkg sigs) BEFORE the gh-pages push - apt Release: Valid-Until (APT_VALID_DAYS, default 90d; LC_ALL=C) for freeze/replay protection - dev-floor sed: anchor to the dependency lines (leave the comment), and assert the floor was actually stamped - signed-repo tests (apt/dnf/pacman) now install splitway-gui too, exercising the GUI package under signature enforcement Arch / consistency / docs: - splitway-gui PKGBUILD: splitway>=$pkgver floor + hicolor-icon-theme, desktop-file-utils - new check-pkgver-sync.sh (meta job): daemon version == every PKGBUILD pkgver - render-index/README: dearmor --yes for apt, pacman-key --init, dev-channel keyring install - splitway-bin SKIP digest documented + tracked for the deferred AUR-push phase Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/packaging.yml | 105 ++++++++++++++++-- README.md | 3 +- docs/design/linux-distro-packaging.md | 5 + packaging/aur/splitway-bin/PKGBUILD | 8 ++ ...{splitway.install => splitway-bin.install} | 0 packaging/aur/splitway-gui/PKGBUILD | 9 +- packaging/ci/build-apt-repo.sh | 13 +++ packaging/ci/check-pkgver-sync.sh | 27 +++++ packaging/ci/render-index.sh | 6 +- 9 files changed, 162 insertions(+), 14 deletions(-) rename packaging/aur/splitway-bin/{splitway.install => splitway-bin.install} (100%) create mode 100755 packaging/ci/check-pkgver-sync.sh diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index 04c0a79..bdead4b 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -29,6 +29,8 @@ jobs: channel: ${{ steps.v.outputs.channel }} steps: - uses: actions/checkout@v5 + - name: Check the AUR PKGBUILD pkgver matches the daemon version + run: bash packaging/ci/check-pkgver-sync.sh - id: v shell: bash run: | @@ -66,7 +68,14 @@ jobs: for b in splitway-daemon splitway; do f="target/${{ matrix.target }}/release/$b" file "$f" - file "$f" | grep -q "statically linked" || { echo "ERROR: $b is not static"; exit 1; } + # Assert the real intent: no dynamic interpreter. A musl release build + # is reported by file(1) as either "statically linked" (non-PIE, e.g. + # the aarch64 leg) or "static-pie linked" (PIE, e.g. the x86_64 leg); + # both are static. Matching the *absence* of "dynamically linked" + # accepts both forms (and any future phrasing) without a false abort. + if file "$f" | grep -q "dynamically linked"; then + echo "ERROR: $b is dynamically linked, expected a static binary"; exit 1 + fi done - name: Package deb + rpm + tarball run: | @@ -132,7 +141,18 @@ jobs: curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --profile minimal --default-toolchain stable . "$HOME/.cargo/env" - cargo build --release -p splitway-gui + # Network resilience: a cold container build has no crates cache, so a + # single crates.io blip (HTTP2 reset, etc.) would fail the whole arch + # leg. Make cargos own fetch retry harder, and wrap the (locked) fetch + # in an outer retry, before building fully offline (--frozen). + export CARGO_NET_RETRY=10 CARGO_HTTP_MULTIPLEXING=false + for i in 1 2 3 4 5; do + cargo fetch --locked && break + [ "$i" = 5 ] && { echo "ERROR: cargo fetch exhausted retries"; exit 1; } + echo "cargo fetch failed (attempt $i/5); retrying in ${i}0s" >&2 + sleep "${i}0" + done + cargo build --release --frozen -p splitway-gui ' sudo chown -R "$(id -u):$(id -g)" target file target/release/splitway-gui @@ -142,9 +162,22 @@ jobs: # GUI Depends splitway (>= ): for dev the floor must be # the ~dev version so the same-run dev core satisfies it (a clean # release core, sorting higher, satisfies it too). No-op for release. - sed -i "s|splitway (>= [^)]*)|splitway (>= ${PKGVER})|g" splitway-gui/Cargo.toml - sed -i "s|splitway = \">= [^\"]*\"|splitway = \">= ${PKGVER}\"|g" splitway-gui/Cargo.toml + # + # Anchor each substitution to the actual dependency line (^depends = / + # ^splitway = ) so the explanatory comment that also mentions the floor + # is left untouched, then ASSERT the floor was rewritten to $PKGVER — a + # silent no-op (e.g. the floor-string format drifting) must fail loudly. + sed -i "/^depends = /s|splitway (>= [^)]*)|splitway (>= ${PKGVER})|" splitway-gui/Cargo.toml + sed -i "/^splitway = /s|\">= [^\"]*\"|\">= ${PKGVER}\"|" splitway-gui/Cargo.toml grep -n "splitway (>=\|splitway = \">=" splitway-gui/Cargo.toml + # Anchor the asserts to the dependency lines too: the explanatory comment + # also contains "splitway (>= 0.0.5)", so an unanchored grep would pass on + # the prose for a release build (PKGVER=0.0.5) even if the sed silently + # missed the real line — defeating the fail-loud intent. + grep -q "^depends = .*splitway (>= ${PKGVER})" splitway-gui/Cargo.toml \ + || { echo "ERROR: deb core-dep floor not stamped to ${PKGVER}"; exit 1; } + grep -q "^splitway = \">= ${PKGVER}\"" splitway-gui/Cargo.toml \ + || { echo "ERROR: rpm core-dep floor not stamped to ${PKGVER}"; exit 1; } - name: Package deb + rpm + tarball run: | set -euo pipefail @@ -283,12 +316,19 @@ jobs: bash -euxc ' export DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends ca-certificates gnupg - install -m 0644 /k.gpg /usr/share/keyrings/splitway.gpg + # Dearmor into the binary keyring apt expects at a .gpg path. The + # published key is ASCII-armored (rpm --import / pacman-key need that + # form); dearmoring here makes the apt keyring correct on EVERY apt + # version, instead of relying on modern apt accepting armored keys. + gpg --dearmor < /k.gpg > /usr/share/keyrings/splitway.gpg echo "deb [signed-by=/usr/share/keyrings/splitway.gpg] http://localhost:8080/deb stable main" \ > /etc/apt/sources.list.d/splitway.list apt-get update - apt-get install -y splitway + # Install BOTH packages so the GUI header signature + its core-dep + # floor are exercised under signature enforcement too (not just core). + apt-get install -y splitway splitway-gui command -v splitway-daemon + command -v splitway-gui echo "SIGNED APT INSTALL OK" ' - name: dnf install over the signed repo (verification ON) @@ -306,8 +346,11 @@ jobs: repo_gpgcheck=1 gpgkey=file:///k.gpg EOF - dnf -y install splitway + # Install BOTH so the GUI rpm header signature + Requires are + # verified under gpgcheck/repo_gpgcheck too (not just the core rpm). + dnf -y install splitway splitway-gui command -v splitway-daemon + command -v splitway-gui echo "SIGNED DNF INSTALL OK" ' @@ -425,7 +468,10 @@ jobs: %commit EOF gpg --batch --gen-key "$GNUPGHOME/keygen" - FPR=$(gpg --list-keys --with-colons ci@example.com | awk -F: '/^fpr:/{print $5; exit}') + # The fingerprint lives in field 10 of an fpr: record (fields 2-9 are + # empty there); field 5 is the long key id and would leave FPR empty, + # breaking lsign/repo-add. Matches the publish-pages job below. + FPR=$(gpg --list-keys --with-colons ci@example.com | awk -F: '/^fpr:/{print $10; exit}') gpg --export --armor "$FPR" > /tmp/key.gpg # Build a signed local repo: detach-sign each package, then repo-add. mkdir -p repo && cp art/*.pkg.tar.zst repo/ @@ -445,8 +491,12 @@ jobs: SigLevel = Required DatabaseOptional Server = http://localhost:8081 EOF - pacman -Sy --noconfirm splitway + # Install BOTH so the GUI package's detached signature is verified + # under SigLevel=Required too (the GL/X11 runtime deps were pulled in + # by the local-install step above and persist in this job's container). + pacman -Sy --noconfirm splitway splitway-gui command -v splitway-daemon + command -v splitway-gui echo "SIGNED PACMAN INSTALL OK" # --- publish the signed apt/dnf repos to GitHub Pages (push-only, SECRETS) -- @@ -555,6 +605,41 @@ jobs: gpg --batch --yes --pinentry-mode loopback --passphrase-file "$SPLITWAY_GPG_PASSFILE" \ --default-key "$GPG_FPR" --detach-sign "$ARCHDIR/splitway.db.tar.gz" cp "$ARCHDIR/splitway.db.tar.gz.sig" "$ARCHDIR/splitway.db.sig" + - name: Verify all signatures BEFORE publishing (fail closed) + run: | + set -euo pipefail + # Hard gate before the gh-pages push: never publish metadata we cannot + # verify. Catches a misconfigured signing key (e.g. a non-RSA key, which + # clearsigns apt fine but yields NO rpm header signature) HERE, instead + # of after the broken channel is already live for users (the post-deploy + # smoke ran only after the push). Uses the imported key in $GNUPGHOME. + # apt: clearsigned InRelease + detached Release.gpg. + gpg --verify "gh-pages/deb/$CHANNEL/dists/stable/InRelease" + gpg --verify "gh-pages/deb/$CHANNEL/dists/stable/Release.gpg" \ + "gh-pages/deb/$CHANNEL/dists/stable/Release" + # dnf: detached repomd.xml.asc, and every rpm must carry an RSA header + # signature (proof rpm --addsign actually signed, not silently no-op'd). + # NOTE: %{RSAHEADER:pgpsig} is the rpm<=5 tag (ubuntu-latest ships rpm 4); + # rpm 6 renames it toward OPENPGP, so revisit this query if a future + # runner upgrade makes it return empty for a validly-signed package. + gpg --verify "gh-pages/rpm/$CHANNEL/repodata/repomd.xml.asc" \ + "gh-pages/rpm/$CHANNEL/repodata/repomd.xml" + for r in gh-pages/rpm/"$CHANNEL"/*.rpm; do + sig="$(rpm -qp --nosignature --qf '%{RSAHEADER:pgpsig}' "$r" 2>/dev/null || true)" + case "$sig" in + ""|"(none)") echo "ERROR: $r has no RSA header signature"; exit 1 ;; + *) echo "ok: $(basename "$r") signed ($sig)" ;; + esac + done + # pacman (release only): detached db + per-package signatures. + if [ "$CHANNEL" = release ]; then + ARCHDIR="gh-pages/arch/release/x86_64" + gpg --verify "$ARCHDIR/splitway.db.tar.gz.sig" "$ARCHDIR/splitway.db.tar.gz" + for p in "$ARCHDIR"/*.pkg.tar.zst; do + gpg --verify "$p.sig" "$p" + done + fi + echo "all signatures verified — safe to publish" - name: Commit + push gh-pages run: | set -euo pipefail @@ -589,7 +674,7 @@ jobs: docker run --rm debian:bookworm bash -euxc " export DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends ca-certificates curl gnupg - curl -fsSL $BASE/splitway.gpg -o /usr/share/keyrings/splitway.gpg + curl -fsSL $BASE/splitway.gpg | gpg --dearmor -o /usr/share/keyrings/splitway.gpg echo 'deb [signed-by=/usr/share/keyrings/splitway.gpg] $BASE/deb/$CHANNEL stable main' \ > /etc/apt/sources.list.d/splitway.list apt-get update && apt-get install -y splitway diff --git a/README.md b/README.md index 18e30e6..862cf55 100755 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ maintainer's published value before trusting the repo. ```sh curl -fsSL https://stslex.github.io/splitway/splitway.gpg \ - | sudo tee /usr/share/keyrings/splitway.gpg > /dev/null + | sudo gpg --dearmor --yes -o /usr/share/keyrings/splitway.gpg echo "deb [signed-by=/usr/share/keyrings/splitway.gpg] https://stslex.github.io/splitway/deb/release stable main" \ | sudo tee /etc/apt/sources.list.d/splitway.list sudo apt-get update @@ -291,6 +291,7 @@ reopening): ```sh curl -fsSL https://stslex.github.io/splitway/splitway.gpg -o /tmp/splitway.gpg +sudo pacman-key --init # no-op if already initialised sudo pacman-key --add /tmp/splitway.gpg sudo pacman-key --lsign-key # from gpg --show-keys /tmp/splitway.gpg sudo tee -a /etc/pacman.conf <<'EOF' diff --git a/docs/design/linux-distro-packaging.md b/docs/design/linux-distro-packaging.md index 46afd16..53baf09 100644 --- a/docs/design/linux-distro-packaging.md +++ b/docs/design/linux-distro-packaging.md @@ -171,6 +171,11 @@ on a dev/PR run) and validates `.SRCINFO` + `namcap`. **Deferred:** the automated `ssh://aur@aur.archlinux.org/.git` push, blocked on AUR registration reopening. Design preserved for then: per package `makepkg --printsrcinfo > .SRCINFO`, commit + push idempotently, release-only. +That same automation must also **pin real `sha256sums`** for the `splitway-bin` +prebuilt tarballs (currently `SKIP`): the digest is only knowable after the +release is built, and the publish job already holds the artifacts, so it should +stamp the per-tag hashes — `SKIP` on a prebuilt privileged daemon trusts the +Releases CDN for integrity, which transport TLS alone does not guarantee. ## The signing key diff --git a/packaging/aur/splitway-bin/PKGBUILD b/packaging/aur/splitway-bin/PKGBUILD index 5ba4a7c..4b61e5c 100644 --- a/packaging/aur/splitway-bin/PKGBUILD +++ b/packaging/aur/splitway-bin/PKGBUILD @@ -15,6 +15,14 @@ conflicts=('splitway') install="$pkgname.install" source_x86_64=("https://github.com/stslex/splitway/releases/download/v$pkgver/splitway-$pkgver-linux-amd64.tar.gz") source_aarch64=("https://github.com/stslex/splitway/releases/download/v$pkgver/splitway-$pkgver-linux-arm64.tar.gz") +# TRUST ASSUMPTION: these are PREBUILT binaries of a privileged daemon fetched +# from the Releases CDN, so 'SKIP' (no integrity check) means a re-tagged or +# tampered asset would install silently — weaker than the source PKGBUILDs where +# SKIP only waives a VCS-tag tarball. The real fix is to pin per-release digests: +# the publish-pages job already has the artifacts, so when the deferred AUR-push +# automation lands it must stamp the real sha256sums for each tag here (the hash +# is only knowable after the release is built). Until then this is x86_64/aarch64 +# convenience packaging that trusts the HTTPS Releases endpoint + the tag. sha256sums_x86_64=('SKIP') sha256sums_aarch64=('SKIP') diff --git a/packaging/aur/splitway-bin/splitway.install b/packaging/aur/splitway-bin/splitway-bin.install similarity index 100% rename from packaging/aur/splitway-bin/splitway.install rename to packaging/aur/splitway-bin/splitway-bin.install diff --git a/packaging/aur/splitway-gui/PKGBUILD b/packaging/aur/splitway-gui/PKGBUILD index 87b43e3..e8ce730 100644 --- a/packaging/aur/splitway-gui/PKGBUILD +++ b/packaging/aur/splitway-gui/PKGBUILD @@ -12,7 +12,14 @@ pkgdesc="Splitway desktop GUI (egui front-end over the daemon socket)" arch=('x86_64' 'aarch64') url="https://github.com/stslex/splitway" license=('MIT') -depends=('splitway' 'libglvnd' 'libxkbcommon' 'wayland' 'libx11' 'libxcursor' 'libxi' 'libxrandr') +# `splitway>=$pkgver` mirrors the deb/rpm `>=` IPC-compat floor (a new GUI must +# not pair with an older core that predates an IPC change); the hosted repo ships +# both in lockstep, but the floor encodes the contract. desktop-file-utils backs +# the update-desktop-database hook in .install; hicolor-icon-theme provides the +# base theme (whose own pacman hook refreshes the icon cache — the .install's +# gtk-update-icon-cache call is best-effort and stays guarded, not a hard dep). +depends=("splitway>=$pkgver" 'libglvnd' 'libxkbcommon' 'wayland' 'libx11' 'libxcursor' 'libxi' 'libxrandr' + 'hicolor-icon-theme' 'desktop-file-utils') makedepends=('cargo') optdepends=('xdg-desktop-portal: file dialog (a portal backend is also needed)') install="$pkgname.install" diff --git a/packaging/ci/build-apt-repo.sh b/packaging/ci/build-apt-repo.sh index af96ebd..22a02c2 100755 --- a/packaging/ci/build-apt-repo.sh +++ b/packaging/ci/build-apt-repo.sh @@ -58,6 +58,18 @@ for a in $ARCHES; do done rm -f "$allpkgs" +# Valid-Until bounds the lifetime of the signed suite metadata: without it the +# Release is trusted forever, which permits freeze/replay (a MITM or a stale +# mirror pinning clients to an old, vulnerable version indefinitely). The window +# is APT_VALID_DAYS (default 90); EVERY publish regenerates + re-signs Release, +# so it is refreshed on each push — the maintainer must publish (or widen the +# window) within it. Needs GNU date (CI runners have it). +VALID_DAYS="${APT_VALID_DAYS:-90}" +# LC_ALL=C: apt only accepts the English RFC1123 day/month names; without it a +# non-English runner/container locale would emit localized names and apt would +# reject the Release as unparseable. +VALID_UNTIL="$(LC_ALL=C date -u -d "+${VALID_DAYS} days" "+%a, %d %b %Y %H:%M:%S UTC")" + # Suite Release file (with checksums of the Packages indexes). apt-ftparchive \ -o "APT::FTPArchive::Release::Origin=splitway" \ @@ -66,6 +78,7 @@ apt-ftparchive \ -o "APT::FTPArchive::Release::Codename=$SUITE" \ -o "APT::FTPArchive::Release::Components=$COMP" \ -o "APT::FTPArchive::Release::Architectures=$ARCHES" \ + -o "APT::FTPArchive::Release::Valid-Until=$VALID_UNTIL" \ release "dists/$SUITE" > "dists/$SUITE/Release" if [ -n "$KEY" ]; then diff --git a/packaging/ci/check-pkgver-sync.sh b/packaging/ci/check-pkgver-sync.sh new file mode 100755 index 0000000..4b8cff1 --- /dev/null +++ b/packaging/ci/check-pkgver-sync.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Guard the lockstep-version invariant. The single source of truth is the daemon +# crate's version (splitway-daemon/Cargo.toml, read by compute-version.sh); the +# deb/rpm packages are stamped from it in CI, but the AUR PKGBUILDs carry a +# hand-kept `pkgver=` that is easy to forget on a version bump. Fail the build if +# any PKGBUILD's pkgver has drifted from the daemon version, so the three can't +# silently diverge. +# +# check-pkgver-sync.sh +set -euo pipefail + +ver="$(grep '^version' splitway-daemon/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')" +[ -n "$ver" ] || { echo "ERROR: could not read daemon version" >&2; exit 1; } + +rc=0 +for pb in packaging/aur/*/PKGBUILD; do + pv="$(grep '^pkgver=' "$pb" | head -1 | cut -d= -f2)" + if [ "$pv" != "$ver" ]; then + echo "ERROR: $pb has pkgver=$pv but daemon version=$ver (lockstep drift)" >&2 + rc=1 + fi +done + +if [ "$rc" = 0 ]; then + echo "pkgver in sync ($ver) across all AUR PKGBUILDs" +fi +exit "$rc" diff --git a/packaging/ci/render-index.sh b/packaging/ci/render-index.sh index 2e4d3ec..931accf 100644 --- a/packaging/ci/render-index.sh +++ b/packaging/ci/render-index.sh @@ -41,13 +41,14 @@ All packages are GPG-signed. The signing key is published at

Debian / Ubuntu (apt)

Release channel

-
curl -fsSL ${BASE}/splitway.gpg | sudo tee /usr/share/keyrings/splitway.gpg > /dev/null
+
curl -fsSL ${BASE}/splitway.gpg | sudo gpg --dearmor --yes -o /usr/share/keyrings/splitway.gpg
 echo "deb [signed-by=/usr/share/keyrings/splitway.gpg] ${BASE}/deb/release stable main" \\
   | sudo tee /etc/apt/sources.list.d/splitway.list
 sudo apt-get update
 sudo apt-get install splitway        # add splitway-gui for the desktop app

Dev channel

-
echo "deb [signed-by=/usr/share/keyrings/splitway.gpg] ${BASE}/deb/dev stable main" \\
+
curl -fsSL ${BASE}/splitway.gpg | sudo gpg --dearmor --yes -o /usr/share/keyrings/splitway.gpg
+echo "deb [signed-by=/usr/share/keyrings/splitway.gpg] ${BASE}/deb/dev stable main" \\
   | sudo tee /etc/apt/sources.list.d/splitway-dev.list
 sudo apt-get update && sudo apt-get install splitway
@@ -78,6 +79,7 @@ sudo dnf install splitway

Arch Linux (pacman, x86_64)

Signed repository (recommended)

curl -fsSL ${BASE}/splitway.gpg -o /tmp/splitway.gpg
+sudo pacman-key --init          # no-op if the keyring is already initialised
 sudo pacman-key --add /tmp/splitway.gpg
 sudo pacman-key --lsign-key ${FPR}
 sudo tee -a /etc/pacman.conf <<'EOF'

From 66634a2276bf8fef690b38d4c617a4119f878b55 Mon Sep 17 00:00:00 2001
From: stslex 
Date: Thu, 25 Jun 2026 09:42:41 +0300
Subject: [PATCH 11/28] =?UTF-8?q?fix(packaging):=20address=20Codex=20re-re?=
 =?UTF-8?q?view=20=E2=80=94=20versioned=20provides=20+=20rpm=20tooling?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Follow-up to the second Codex pass on PR #36.

- splitway-bin: `provides=("splitway=$pkgver")` (was unversioned). pacman only
  satisfies a versioned dependency from a versioned provision, so splitway-gui's
  new `splitway>=$pkgver` floor could not install against the prebuilt core.
- test-signed-repo: install `rpm` in the tooling step. build-dnf-repo.sh needs
  `rpm --addsign`; it passes today only because ubuntu-latest pre-installs rpm —
  make it explicit (matches the publish job) so the dnf signing path is robust.
- packaging/README.md: document the one-time config relocation for users who ran
  the daemon by hand as root before packaging (old XDG fallback
  /root/.config/splitway -> /var/lib/splitway). No maintainer-script migration:
  no published package used the old path, it is config-not-read (not data loss),
  and the deb core postinst is cargo-deb-generated (#DEBHELPER#) from an empty
  dir, so a hand-written migration would risk breaking systemd enablement that
  the docker install-test (no systemd) could not catch.

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 .github/workflows/packaging.yml     | 5 ++++-
 packaging/README.md                 | 9 +++++++++
 packaging/aur/splitway-bin/PKGBUILD | 6 +++++-
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml
index bdead4b..8b029dc 100644
--- a/.github/workflows/packaging.yml
+++ b/.github/workflows/packaging.yml
@@ -271,7 +271,10 @@ jobs:
       - name: Install repo tooling
         run: |
           sudo apt-get update
-          sudo apt-get install -y --no-install-recommends apt-utils createrepo-c gnupg
+          # rpm is needed by build-dnf-repo.sh (rpm --addsign); ubuntu-latest
+          # currently pre-installs it, but install it explicitly so the dnf
+          # signing path does not depend on the runner image (matches publish).
+          sudo apt-get install -y --no-install-recommends apt-utils createrepo-c gnupg rpm
       - name: Generate a throwaway signing key
         run: |
           set -euo pipefail
diff --git a/packaging/README.md b/packaging/README.md
index 27e51dc..e607f2f 100644
--- a/packaging/README.md
+++ b/packaging/README.md
@@ -29,6 +29,15 @@ read-only `/etc` config. The daemon creates an empty config there on first run.
 See [docs/architecture.md](../docs/architecture.md) ("Config is the single
 source of truth").
 
+> **Migrating from a manual install.** If you previously ran `splitway-daemon`
+> by hand as root *without* `--config`, it stored its config at the XDG fallback
+> `/root/.config/splitway/config.json` (and logged a warning). The packaged unit
+> reads `/var/lib/splitway/config.json` instead, so move your file once after
+> installing the package:
+> `sudo install -D -m600 /root/.config/splitway/config.json /var/lib/splitway/config.json`.
+> This affects only hand-run pre-packaging setups — packaged installs start fresh
+> at the `/var/lib` path.
+
 ### Socket security model
 
 The daemon is privileged; the CLI is not. The Unix control socket is the
diff --git a/packaging/aur/splitway-bin/PKGBUILD b/packaging/aur/splitway-bin/PKGBUILD
index 4b61e5c..ddb56a5 100644
--- a/packaging/aur/splitway-bin/PKGBUILD
+++ b/packaging/aur/splitway-bin/PKGBUILD
@@ -10,7 +10,11 @@ pkgdesc="Domain-based split-DNS daemon + CLI (prebuilt from the release tarball)
 arch=('x86_64' 'aarch64')
 url="https://github.com/stslex/splitway"
 license=('MIT')
-provides=('splitway')
+# Versioned provision: splitway-gui depends on `splitway>=$pkgver`, and pacman
+# only satisfies a versioned dependency from a versioned provision — an
+# unversioned `provides=('splitway')` would NOT let splitway-gui install against
+# this prebuilt core. conflicts stays unversioned (mutually exclusive cores).
+provides=("splitway=$pkgver")
 conflicts=('splitway')
 install="$pkgname.install"
 source_x86_64=("https://github.com/stslex/splitway/releases/download/v$pkgver/splitway-$pkgver-linux-amd64.tar.gz")

From b3260367b1b34e044a5c8b537a72bda2da8e2f99 Mon Sep 17 00:00:00 2001
From: stslex 
Date: Thu, 25 Jun 2026 14:58:54 +0300
Subject: [PATCH 12/28] fix(packaging): sync AUR pkgver on the release
 auto-bump (Codex P2)

The `meta` gate (check-pkgver-sync.sh) requires every packaging/aur/*/PKGBUILD
`pkgver=` to equal the daemon version, but release.yml's bump-version job only
bumped splitway-daemon/Cargo.toml. So the first post-release auto-bump would
leave the three PKGBUILDs behind and fail every later packaging run in `meta`
until a human hand-edited all three.

- Add packaging/ci/sync-pkgver.sh: the write side of the lockstep invariant
  (symmetric with check-pkgver-sync.sh, same daemon-version read). It stamps the
  daemon version into each PKGBUILD `pkgver=` and resets `pkgrel=1` (Arch
  convention on a version change). The `$pkgver`-derived fields (source URLs,
  provides, depends floor) follow automatically.
- release.yml bump-version: run sync-pkgver.sh after the Cargo.toml bump and
  `git add` the PKGBUILDs into the same commit.

Verified locally: no-op when already in sync (no diff), correct bump on a
simulated 0.0.5 -> 0.0.6, and check-pkgver-sync.sh passes against the result.

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 .github/workflows/release.yml |  8 +++++++-
 packaging/ci/sync-pkgver.sh   | 21 +++++++++++++++++++++
 2 files changed, 28 insertions(+), 1 deletion(-)
 create mode 100755 packaging/ci/sync-pkgver.sh

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e27e5d5..6eb5972 100755
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -128,11 +128,17 @@ jobs:
           sed -i "s/^version = \"$VERSION\"/version = \"$NEW_VERSION\"/" splitway-daemon/Cargo.toml
           echo "Bumped $VERSION -> $NEW_VERSION"
 
+      - name: Sync AUR PKGBUILD pkgver to the bumped daemon version
+        shell: bash
+        # Keeps the AUR PKGBUILDs in lockstep with the daemon so packaging.yml's
+        # `meta` gate (check-pkgver-sync.sh) stays green after the auto-bump.
+        run: bash packaging/ci/sync-pkgver.sh
+
       - name: Commit and push version bump
         shell: bash
         run: |
           git config user.name "github-actions[bot]"
           git config user.email "github-actions[bot]@users.noreply.github.com"
-          git add splitway-daemon/Cargo.toml
+          git add splitway-daemon/Cargo.toml packaging/aur/*/PKGBUILD
           git commit -m "chore: bump version after release"
           git push
diff --git a/packaging/ci/sync-pkgver.sh b/packaging/ci/sync-pkgver.sh
new file mode 100755
index 0000000..093b99e
--- /dev/null
+++ b/packaging/ci/sync-pkgver.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# Write side of the lockstep-version invariant (the read side is
+# check-pkgver-sync.sh). The daemon crate's version is the single source of
+# truth; this stamps it into every AUR PKGBUILD's `pkgver=` and resets `pkgrel=1`
+# (Arch convention on a version change). Run it right after bumping
+# splitway-daemon/Cargo.toml so the gate in packaging.yml's `meta` job stays
+# green — without this, each post-release auto-bump would leave the PKGBUILDs
+# behind and fail every later packaging run until a human edited all three.
+#
+#   sync-pkgver.sh
+set -euo pipefail
+
+# Same read as check-pkgver-sync.sh — keep these two in step.
+ver="$(grep '^version' splitway-daemon/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
+[ -n "$ver" ] || { echo "ERROR: could not read daemon version" >&2; exit 1; }
+
+for pb in packaging/aur/*/PKGBUILD; do
+    sed -i "s/^pkgver=.*/pkgver=$ver/" "$pb"
+    sed -i "s/^pkgrel=.*/pkgrel=1/" "$pb"
+    echo "synced $pb -> pkgver=$ver pkgrel=1"
+done

From 6de36d7be42ba7d37bca6ed69d46637b190f9da6 Mon Sep 17 00:00:00 2001
From: stslex 
Date: Thu, 25 Jun 2026 18:02:14 +0300
Subject: [PATCH 13/28] fix(packaging): GUI EGL dep + Arch icon/desktop
 preinstall (Codex re-review)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Two findings from Codex's re-review of b326036:

- P2 (splitway-gui/Cargo.toml): the deb declared libgl1 but not libegl1,
  and the rpm required mesa-libGL but not mesa-libEGL. glow/glutin creates
  its GL context via EGL on Wayland (glutin_egl_sys is in Cargo.lock; GLX on
  X11), and libgl1 does NOT pull libegl1 — so a minimal Wayland-only install
  would succeed and then fail at GL-context creation. Add libegl1 (deb) and
  mesa-libEGL (rpm); both pull the libglvnd EGL loader, mirroring how
  libgl1/mesa-libGL pull GLX. Arch is unaffected (libglvnd already provides
  libGL + libEGL). Doc + comments updated.

- P1 (packaging.yml test-arch): the local `pacman -U` smoke preinstalled the
  GL/X11/wayland libs but not hicolor-icon-theme/desktop-file-utils, which
  the splitway-gui PKGBUILD declares as hard deps. `pacman -U` on local files
  only auto-resolves deps from a synced repo DB, so the smoke must not lean
  on that — preinstall the full declared set explicitly, matching the PKGBUILD.

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 .github/workflows/packaging.yml | 11 ++++++++---
 packaging/README.md             |  6 ++++--
 splitway-gui/Cargo.toml         | 23 ++++++++++++++---------
 3 files changed, 26 insertions(+), 14 deletions(-)

diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml
index 8b029dc..eee40ca 100644
--- a/.github/workflows/packaging.yml
+++ b/.github/workflows/packaging.yml
@@ -444,9 +444,14 @@ jobs:
       - name: Local pacman -U install
         run: |
           set -euo pipefail
-          # Pull the GUI's runtime libs from the official repos, then install the
-          # built packages directly.
-          pacman -S --noconfirm --needed libglvnd libxkbcommon wayland libx11 libxcursor libxi libxrandr
+          # Pull ALL of the GUI's declared runtime deps from the official repos,
+          # then install the built packages directly. `pacman -U` on local files
+          # only auto-resolves deps that exist in a synced repo DB, so the smoke
+          # must not lean on that — preinstall the full set the PKGBUILD declares
+          # (GL/X11/wayland libs + hicolor-icon-theme/desktop-file-utils for the
+          # .install icon-cache / desktop-database hooks), matching it exactly.
+          pacman -S --noconfirm --needed libglvnd libxkbcommon wayland libx11 libxcursor libxi libxrandr \
+            hicolor-icon-theme desktop-file-utils
           pacman -U --noconfirm art/*.pkg.tar.zst
           command -v splitway-daemon
           command -v splitway
diff --git a/packaging/README.md b/packaging/README.md
index e607f2f..81c426a 100644
--- a/packaging/README.md
+++ b/packaging/README.md
@@ -105,8 +105,10 @@ rationale is in
   `/bin/sh` scriptlets.
 - `splitway-gui` — the egui binary, **glibc**, built against a **2.31 floor**
   (`debian:bullseye` / `ubuntu:20.04`), so `Depends: splitway (>=), libc6 (>=
-  2.31), libgl1 …`. winit/glow `dlopen` the GL/X11/wayland libs, so they are not
-  in the ELF and must be hardcoded (dep auto-detection misses them). RHEL 8 is
+  2.31), libgl1, libegl1 …`. winit/glow `dlopen` the GL/X11/wayland libs, so
+  they are not in the ELF and must be hardcoded (dep auto-detection misses
+  them); `libegl1` is a separate dep from `libgl1` because glow/glutin uses EGL
+  on Wayland (GLX on X11). RHEL 8 is
   intentionally uncovered for the GUI; the musl core still works there.
 
 **GUI socket-group drop-in.** The `splitway-gui` package's maintainer scripts
diff --git a/splitway-gui/Cargo.toml b/splitway-gui/Cargo.toml
index 8495278..e0d4a7d 100644
--- a/splitway-gui/Cargo.toml
+++ b/splitway-gui/Cargo.toml
@@ -66,12 +66,15 @@ until you run `usermod -aG splitway `. Requires the splitway package."""
 # Hardcoded eframe/glow runtime libs (Debian names) rather than cargo-deb's
 # `$auto`. Two reasons: (1) $auto (dpkg-shlibdeps) needs the TARGET-arch libs
 # present, which is fragile for the arm64 cross build; (2) winit/glow dlopen
-# the windowing libs (libGL, X11, wayland, xkbcommon) at runtime, so they are
-# NOT in the ELF's DT_NEEDED and $auto/shlibdeps would MISS them entirely
-# (verified: the rpm's ELF-based auto-req captured libc/libgcc but none of the
-# GL/X11/wayland libs). `libc6 (>= 2.31)` pins the documented glibc floor
-# (debian:bullseye / ubuntu:20.04). The `>=` core dep is the real IPC contract.
-depends = "splitway (>= 0.0.5), libc6 (>= 2.31), libgl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libxkbcommon0"
+# the windowing libs (libGL + libEGL, X11, wayland, xkbcommon) at runtime, so
+# they are NOT in the ELF's DT_NEEDED and $auto/shlibdeps would MISS them
+# entirely (verified: the rpm's ELF-based auto-req captured libc/libgcc but none
+# of the GL/X11/wayland libs). libegl1 is a SEPARATE dep from libgl1 because
+# glow/glutin creates its GL context via EGL on Wayland (GLX on X11), and
+# libgl1 does NOT pull libegl1 — without it a Wayland-only install succeeds and
+# then fails at GL-context creation. `libc6 (>= 2.31)` pins the documented glibc
+# floor (debian:bullseye / ubuntu:20.04). The `>=` core dep is the real IPC contract.
+depends = "splitway (>= 0.0.5), libc6 (>= 2.31), libgl1, libegl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libxkbcommon0"
 # rfd uses the XDG desktop portal for its file dialog here (no GTK linked); the
 # dialog silently no-ops without a running portal + backend, hence Recommends.
 recommends = "xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-wlr | xdg-desktop-portal-kde"
@@ -162,12 +165,14 @@ assets = [
 ]
 
 # Core package dep (the real compat contract) + the dlopen'd eframe/glow
-# runtime libs that auto-req cannot detect (Fedora package names; mesa-libGL
-# pulls a working GL via libglvnd). rfd's file dialog needs an XDG portal +
-# backend (Recommends below).
+# runtime libs that auto-req cannot detect (Fedora package names; mesa-libGL /
+# mesa-libEGL pull working GL/EGL via libglvnd — glow/glutin uses EGL on
+# Wayland, GLX on X11, and mesa-libGL alone does not pull the EGL loader). rfd's
+# file dialog needs an XDG portal + backend (Recommends below).
 [package.metadata.generate-rpm.requires]
 splitway = ">= 0.0.5"
 mesa-libGL = "*"
+mesa-libEGL = "*"
 libX11 = "*"
 libXcursor = "*"
 libXi = "*"

From 9e811e16efea6d5fee0b206a64b5f474fe55bb1c Mon Sep 17 00:00:00 2001
From: stslex 
Date: Thu, 25 Jun 2026 20:48:59 +0300
Subject: [PATCH 14/28] fix(packaging): GUI xkbcommon-x11 dep + 4 self-review
 hardenings (review round 3)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Codex re-review (1) + self-review (4) of 6de36d7:

- P2 (Codex, splitway-gui): add the X11 xkb runtime lib. winit's default `x11`
  feature has xkbcommon-dl dlopen libxkbcommon-x11.so (confirmed in Cargo.lock),
  a SEPARATE package from libxkbcommon on Debian (libxkbcommon-x11-0), Fedora
  (libxkbcommon-x11) AND Arch (libxkbcommon-x11 — verified via the Arch package
  API: it provides libxkbcommon-x11.so and is NOT bundled in the base
  libxkbcommon). Without it an X11 session can fail to load the lib before the
  window opens. Added to the deb Depends, rpm Requires, the Arch GUI PKGBUILD
  depends, and both Arch CI preinstall lists.

- P2 (postinst): the groupadd under `set -e` was the only unguarded mutating
  step. Keep the abort (the drop-in is meaningless without the group) but make
  it deliberate + diagnosable with an explicit error message + exit.

- P2 latent (version reads): `grep '^version' … | head -1` SIGPIPEs grep under
  `set -o pipefail` if a second match ever appears. Replaced with a SIGPIPE-free
  `awk -F'"' '/^version/{print $2; exit}'` in compute-version.sh,
  check-pkgver-sync.sh (+ the pkgver read), sync-pkgver.sh and release.yml (x3).

- P2 (splitway-bin): surface the same networkmanager / systemd-resolvconf
  optdepends as the source splitway PKGBUILD (identical daemon, same prereqs).

- P2 (README niri): the interim egui GUI now sets `.with_app_id(...)` and the
  packaged splitway-gui ships the matching .desktop + icons, so the app-id
  window rule is no longer Tauri-only; clarified the packaged GUI is the egui build.

Also quoted "$GITHUB_OUTPUT" in the release.yml blocks touched above
(pre-existing SC2086). actionlint + shellcheck clean; awk reads verified to
yield 0.0.5 and check-pkgver-sync passes.

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 .github/workflows/packaging.yml           |  4 ++--
 .github/workflows/release.yml             | 15 +++++++------
 README.md                                 | 10 ++++++---
 packaging/aur/splitway-bin/PKGBUILD       |  5 +++++
 packaging/aur/splitway-gui/PKGBUILD       |  4 +++-
 packaging/ci/check-pkgver-sync.sh         |  6 ++++--
 packaging/ci/compute-version.sh           |  5 ++++-
 packaging/ci/sync-pkgver.sh               |  5 +++--
 packaging/gui-maintainer-scripts/postinst | 10 +++++++--
 splitway-gui/Cargo.toml                   | 26 ++++++++++++++---------
 10 files changed, 61 insertions(+), 29 deletions(-)

diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml
index eee40ca..2a0703a 100644
--- a/.github/workflows/packaging.yml
+++ b/.github/workflows/packaging.yml
@@ -397,7 +397,7 @@ jobs:
         run: |
           pacman -Syu --noconfirm --needed \
             base-devel git rust namcap sudo \
-            libglvnd libxkbcommon wayland libx11 libxcursor libxi libxrandr
+            libglvnd libxkbcommon libxkbcommon-x11 wayland libx11 libxcursor libxi libxrandr
       - uses: actions/checkout@v5
       - name: Set up a non-root build user (makepkg refuses root)
         run: |
@@ -450,7 +450,7 @@ jobs:
           # must not lean on that — preinstall the full set the PKGBUILD declares
           # (GL/X11/wayland libs + hicolor-icon-theme/desktop-file-utils for the
           # .install icon-cache / desktop-database hooks), matching it exactly.
-          pacman -S --noconfirm --needed libglvnd libxkbcommon wayland libx11 libxcursor libxi libxrandr \
+          pacman -S --noconfirm --needed libglvnd libxkbcommon libxkbcommon-x11 wayland libx11 libxcursor libxi libxrandr \
             hicolor-icon-theme desktop-file-utils
           pacman -U --noconfirm art/*.pkg.tar.zst
           command -v splitway-daemon
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6eb5972..616e9e4 100755
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -42,8 +42,9 @@ jobs:
         id: meta
         shell: bash
         run: |
-          VERSION=$(grep '^version' splitway-daemon/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
-          echo "version=$VERSION" >> $GITHUB_OUTPUT
+          # awk (not `grep | head -1`): SIGPIPE-free version read under pipefail.
+          VERSION=$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)
+          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
 
       - uses: dtolnay/rust-toolchain@stable
         with:
@@ -89,9 +90,10 @@ jobs:
         id: meta
         shell: bash
         run: |
-          VERSION=$(grep '^version' splitway-daemon/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
-          echo "version=$VERSION" >> $GITHUB_OUTPUT
-          echo "tag=v$VERSION" >> $GITHUB_OUTPUT
+          # awk (not `grep | head -1`): SIGPIPE-free version read under pipefail.
+          VERSION=$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)
+          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+          echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
 
       - uses: actions/download-artifact@v8
         with:
@@ -119,7 +121,8 @@ jobs:
       - name: Bump patch version in Cargo.toml files
         shell: bash
         run: |
-          VERSION=$(grep '^version' splitway-daemon/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
+          # awk (not `grep | head -1`): SIGPIPE-free version read under pipefail.
+          VERSION=$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)
           MAJOR=$(echo "$VERSION" | cut -d. -f1)
           MINOR=$(echo "$VERSION" | cut -d. -f2)
           PATCH=$(echo "$VERSION" | cut -d. -f3)
diff --git a/README.md b/README.md
index 862cf55..fcff872 100755
--- a/README.md
+++ b/README.md
@@ -448,9 +448,13 @@ window-rule {
 }
 ```
 
-Install it first — see [GUI (native Tauri)](#gui-native-tauri). (The interim egui
-GUI launches by spawning `splitway-gui` instead; it does **not** carry the
-`io.github.stslex.splitway` app id, so the window rule above is Tauri-only.)
+Install it first — see [GUI (native Tauri)](#gui-native-tauri). Note that the
+GUI shipped by the apt/dnf/pacman `splitway-gui` package today is the **interim
+egui** build (the native Tauri app is not yet packaged); it launches by spawning
+`splitway-gui` and now carries the **same** `io.github.stslex.splitway` app id
+(set via the window `app_id`, matched by the installed `.desktop`
+`StartupWMClass` + hicolor icons), so the window rule above applies to it too —
+just point the `spawn` binding at `splitway-gui`.
 
 **Unprivileged access (opt-in).** By default the control socket is `0600` and
 root-owned, so a CLI or GUI launched as your normal desktop user gets "permission
diff --git a/packaging/aur/splitway-bin/PKGBUILD b/packaging/aur/splitway-bin/PKGBUILD
index ddb56a5..4267e4c 100644
--- a/packaging/aur/splitway-bin/PKGBUILD
+++ b/packaging/aur/splitway-bin/PKGBUILD
@@ -10,6 +10,11 @@ pkgdesc="Domain-based split-DNS daemon + CLI (prebuilt from the release tarball)
 arch=('x86_64' 'aarch64')
 url="https://github.com/stslex/splitway"
 license=('MIT')
+# Same runtime prerequisite hints as the source `splitway` PKGBUILD (and the
+# deb/rpm Recommends) — the prebuilt core ships the identical daemon, so surface
+# the same resolver / VPN-detection optdepends.
+optdepends=('networkmanager: VPN detection via D-Bus'
+            'systemd-resolvconf: split-DNS via systemd-resolved')
 # Versioned provision: splitway-gui depends on `splitway>=$pkgver`, and pacman
 # only satisfies a versioned dependency from a versioned provision — an
 # unversioned `provides=('splitway')` would NOT let splitway-gui install against
diff --git a/packaging/aur/splitway-gui/PKGBUILD b/packaging/aur/splitway-gui/PKGBUILD
index e8ce730..2177ca3 100644
--- a/packaging/aur/splitway-gui/PKGBUILD
+++ b/packaging/aur/splitway-gui/PKGBUILD
@@ -18,7 +18,9 @@ license=('MIT')
 # the update-desktop-database hook in .install; hicolor-icon-theme provides the
 # base theme (whose own pacman hook refreshes the icon cache — the .install's
 # gtk-update-icon-cache call is best-effort and stays guarded, not a hard dep).
-depends=("splitway>=$pkgver" 'libglvnd' 'libxkbcommon' 'wayland' 'libx11' 'libxcursor' 'libxi' 'libxrandr'
+# libxkbcommon-x11 is a SEPARATE Arch package from libxkbcommon (it provides
+# libxkbcommon-x11.so) — winit's default x11 feature dlopens it on X11 sessions.
+depends=("splitway>=$pkgver" 'libglvnd' 'libxkbcommon' 'libxkbcommon-x11' 'wayland' 'libx11' 'libxcursor' 'libxi' 'libxrandr'
          'hicolor-icon-theme' 'desktop-file-utils')
 makedepends=('cargo')
 optdepends=('xdg-desktop-portal: file dialog (a portal backend is also needed)')
diff --git a/packaging/ci/check-pkgver-sync.sh b/packaging/ci/check-pkgver-sync.sh
index 4b8cff1..4ef47ba 100755
--- a/packaging/ci/check-pkgver-sync.sh
+++ b/packaging/ci/check-pkgver-sync.sh
@@ -9,12 +9,14 @@
 #   check-pkgver-sync.sh
 set -euo pipefail
 
-ver="$(grep '^version' splitway-daemon/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
+# awk (not `grep | head -1`): SIGPIPE-free under `set -o pipefail` if a second
+# `^version` line ever appears. Mirrors compute-version.sh / sync-pkgver.sh.
+ver="$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)"
 [ -n "$ver" ] || { echo "ERROR: could not read daemon version" >&2; exit 1; }
 
 rc=0
 for pb in packaging/aur/*/PKGBUILD; do
-    pv="$(grep '^pkgver=' "$pb" | head -1 | cut -d= -f2)"
+    pv="$(awk -F= '/^pkgver=/{print $2; exit}' "$pb")"
     if [ "$pv" != "$ver" ]; then
         echo "ERROR: $pb has pkgver=$pv but daemon version=$ver (lockstep drift)" >&2
         rc=1
diff --git a/packaging/ci/compute-version.sh b/packaging/ci/compute-version.sh
index d217771..911d9e9 100755
--- a/packaging/ci/compute-version.sh
+++ b/packaging/ci/compute-version.sh
@@ -18,7 +18,10 @@ set -euo pipefail
 event="${1:?usage: compute-version.sh  }"
 ref="${2:?usage: compute-version.sh  }"
 
-version="$(grep '^version' splitway-daemon/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
+# awk (not `grep | head -1`): under `set -o pipefail` head closing the pipe
+# early would SIGPIPE grep and fail the read if a second `^version` line ever
+# appeared; awk exits cleanly on the first match.
+version="$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)"
 
 if [ "$event" = "push" ] && [ "$ref" = "refs/heads/master" ]; then
     pkgver="$version"
diff --git a/packaging/ci/sync-pkgver.sh b/packaging/ci/sync-pkgver.sh
index 093b99e..ca75476 100755
--- a/packaging/ci/sync-pkgver.sh
+++ b/packaging/ci/sync-pkgver.sh
@@ -10,8 +10,9 @@
 #   sync-pkgver.sh
 set -euo pipefail
 
-# Same read as check-pkgver-sync.sh — keep these two in step.
-ver="$(grep '^version' splitway-daemon/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
+# Same read as check-pkgver-sync.sh — keep these two in step. awk (not
+# `grep | head -1`) is SIGPIPE-free under `set -o pipefail`.
+ver="$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)"
 [ -n "$ver" ] || { echo "ERROR: could not read daemon version" >&2; exit 1; }
 
 for pb in packaging/aur/*/PKGBUILD; do
diff --git a/packaging/gui-maintainer-scripts/postinst b/packaging/gui-maintainer-scripts/postinst
index 9366bb0..e1a6aca 100755
--- a/packaging/gui-maintainer-scripts/postinst
+++ b/packaging/gui-maintainer-scripts/postinst
@@ -13,8 +13,14 @@ DROPIN="$DROPIN_DIR/socket-group.conf"
 
 case "$1" in
   configure)
-    # 1. Ensure the (empty) system group exists.
-    getent group splitway >/dev/null 2>&1 || groupadd --system splitway
+    # 1. Ensure the (empty) system group exists. Unlike the best-effort steps
+    #    below, this one is allowed to abort the postinst (set -e) — without the
+    #    group the socket-group drop-in is meaningless — but make that failure
+    #    deliberate and diagnosable rather than a bare `set -e` exit.
+    getent group splitway >/dev/null 2>&1 || groupadd --system splitway || {
+      echo "splitway-gui: failed to create the 'splitway' system group" >&2
+      exit 1
+    }
 
     # 2. Install the drop-in that switches the daemon to group-socket mode.
     install -d -m 0755 "$DROPIN_DIR"
diff --git a/splitway-gui/Cargo.toml b/splitway-gui/Cargo.toml
index e0d4a7d..0eaf787 100644
--- a/splitway-gui/Cargo.toml
+++ b/splitway-gui/Cargo.toml
@@ -66,15 +66,18 @@ until you run `usermod -aG splitway `. Requires the splitway package."""
 # Hardcoded eframe/glow runtime libs (Debian names) rather than cargo-deb's
 # `$auto`. Two reasons: (1) $auto (dpkg-shlibdeps) needs the TARGET-arch libs
 # present, which is fragile for the arm64 cross build; (2) winit/glow dlopen
-# the windowing libs (libGL + libEGL, X11, wayland, xkbcommon) at runtime, so
-# they are NOT in the ELF's DT_NEEDED and $auto/shlibdeps would MISS them
-# entirely (verified: the rpm's ELF-based auto-req captured libc/libgcc but none
-# of the GL/X11/wayland libs). libegl1 is a SEPARATE dep from libgl1 because
-# glow/glutin creates its GL context via EGL on Wayland (GLX on X11), and
+# the windowing libs (libGL + libEGL, X11, wayland, xkbcommon + xkbcommon-x11)
+# at runtime, so they are NOT in the ELF's DT_NEEDED and $auto/shlibdeps would
+# MISS them entirely (verified: the rpm's ELF-based auto-req captured libc/libgcc
+# but none of the GL/X11/wayland libs). libegl1 is a SEPARATE dep from libgl1
+# because glow/glutin creates its GL context via EGL on Wayland (GLX on X11), and
 # libgl1 does NOT pull libegl1 — without it a Wayland-only install succeeds and
-# then fails at GL-context creation. `libc6 (>= 2.31)` pins the documented glibc
-# floor (debian:bullseye / ubuntu:20.04). The `>=` core dep is the real IPC contract.
-depends = "splitway (>= 0.0.5), libc6 (>= 2.31), libgl1, libegl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libxkbcommon0"
+# then fails at GL-context creation. libxkbcommon-x11-0 is likewise SEPARATE from
+# libxkbcommon0: winit's default `x11` feature has xkbcommon-dl dlopen
+# libxkbcommon-x11.so, which an X11 session needs and libxkbcommon0 does not pull.
+# `libc6 (>= 2.31)` pins the documented glibc floor (debian:bullseye /
+# ubuntu:20.04). The `>=` core dep is the real IPC contract.
+depends = "splitway (>= 0.0.5), libc6 (>= 2.31), libgl1, libegl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libxkbcommon0, libxkbcommon-x11-0"
 # rfd uses the XDG desktop portal for its file dialog here (no GTK linked); the
 # dialog silently no-ops without a running portal + backend, hence Recommends.
 recommends = "xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-wlr | xdg-desktop-portal-kde"
@@ -167,8 +170,10 @@ assets = [
 # Core package dep (the real compat contract) + the dlopen'd eframe/glow
 # runtime libs that auto-req cannot detect (Fedora package names; mesa-libGL /
 # mesa-libEGL pull working GL/EGL via libglvnd — glow/glutin uses EGL on
-# Wayland, GLX on X11, and mesa-libGL alone does not pull the EGL loader). rfd's
-# file dialog needs an XDG portal + backend (Recommends below).
+# Wayland, GLX on X11, and mesa-libGL alone does not pull the EGL loader).
+# libxkbcommon-x11 is a separate Fedora package from libxkbcommon — winit's x11
+# feature dlopens libxkbcommon-x11.so. rfd's file dialog needs an XDG portal +
+# backend (Recommends below).
 [package.metadata.generate-rpm.requires]
 splitway = ">= 0.0.5"
 mesa-libGL = "*"
@@ -179,6 +184,7 @@ libXi = "*"
 libXrandr = "*"
 libwayland-client = "*"
 libxkbcommon = "*"
+libxkbcommon-x11 = "*"
 
 [package.metadata.generate-rpm.recommends]
 xdg-desktop-portal = "*"

From 9998f4cd6b4dbaa94dbd9fdb2d03d497d9da6a1f Mon Sep 17 00:00:00 2001
From: stslex 
Date: Thu, 25 Jun 2026 22:09:46 +0300
Subject: [PATCH 15/28] fix(packaging): pin AUR pkgver to the released tag, not
 the bumped daemon (Codex P2)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The post-release auto-bump stamped the AUR PKGBUILDs with the *next*
(unreleased) daemon version, so `makepkg -si` from master pointed at a
`v$pkgver` tag / release assets that do not exist yet.

- release.yml: run sync-pkgver.sh BEFORE the daemon bump so the PKGBUILDs
  pin the just-released version; bump the daemon afterwards for the next cycle.
- check-pkgver-sync.sh: validate the pinned pkgver names a release tag that
  EXISTS (all three PKGBUILDs agreeing), with a no-tags bootstrap fallback —
  instead of requiring equality with the in-tree daemon version.
- packaging.yml: fetch-depth: 0 on the meta checkout so the gate sees tags.
- docs/design: record the invariant and why the gate checks existence, not
  "latest" (avoids spurious dev/PR and release-window failures).

Co-Authored-By: Claude Opus 4.8 (1M context) 
Claude-Session: https://claude.ai/code/session_01EKokH93URrCizdQdVpNCNM
---
 .github/workflows/packaging.yml       |  6 ++-
 .github/workflows/release.yml         | 22 ++++++----
 docs/design/linux-distro-packaging.md | 14 ++++++
 packaging/ci/check-pkgver-sync.sh     | 61 +++++++++++++++++++--------
 packaging/ci/sync-pkgver.sh           | 19 +++++----
 5 files changed, 86 insertions(+), 36 deletions(-)

diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml
index 2a0703a..4d43314 100644
--- a/.github/workflows/packaging.yml
+++ b/.github/workflows/packaging.yml
@@ -29,7 +29,11 @@ jobs:
       channel: ${{ steps.v.outputs.channel }}
     steps:
       - uses: actions/checkout@v5
-      - name: Check the AUR PKGBUILD pkgver matches the daemon version
+        with:
+          # fetch-depth: 0 pulls tags too — check-pkgver-sync.sh resolves the
+          # release tag the AUR PKGBUILDs must pin (the repo is small).
+          fetch-depth: 0
+      - name: Check the AUR PKGBUILD pkgver names an existing release tag
         run: bash packaging/ci/check-pkgver-sync.sh
       - id: v
         shell: bash
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 616e9e4..6de5ad3 100755
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -118,7 +118,17 @@ jobs:
     steps:
       - uses: actions/checkout@v5
 
-      - name: Bump patch version in Cargo.toml files
+      - name: Pin AUR PKGBUILD pkgver to the just-released version
+        shell: bash
+        # MUST run before the daemon bump below: while Cargo.toml still holds the
+        # version we just tagged, sync-pkgver.sh stamps it into every PKGBUILD so
+        # `makepkg -si` fetches the `v$pkgver` tag / assets that now exist. The
+        # bump then moves the daemon ahead for the next dev cycle, leaving the
+        # PKGBUILDs deliberately one release behind (check-pkgver-sync.sh
+        # validates the pinned tag exists, not that it equals the daemon).
+        run: bash packaging/ci/sync-pkgver.sh
+
+      - name: Bump patch version in Cargo.toml for the next dev cycle
         shell: bash
         run: |
           # awk (not `grep | head -1`): SIGPIPE-free version read under pipefail.
@@ -131,17 +141,11 @@ jobs:
           sed -i "s/^version = \"$VERSION\"/version = \"$NEW_VERSION\"/" splitway-daemon/Cargo.toml
           echo "Bumped $VERSION -> $NEW_VERSION"
 
-      - name: Sync AUR PKGBUILD pkgver to the bumped daemon version
-        shell: bash
-        # Keeps the AUR PKGBUILDs in lockstep with the daemon so packaging.yml's
-        # `meta` gate (check-pkgver-sync.sh) stays green after the auto-bump.
-        run: bash packaging/ci/sync-pkgver.sh
-
-      - name: Commit and push version bump
+      - name: Commit and push the AUR pin + version bump
         shell: bash
         run: |
           git config user.name "github-actions[bot]"
           git config user.email "github-actions[bot]@users.noreply.github.com"
           git add splitway-daemon/Cargo.toml packaging/aur/*/PKGBUILD
-          git commit -m "chore: bump version after release"
+          git commit -m "chore: pin AUR pkgver to release, bump daemon for next cycle"
           git push
diff --git a/docs/design/linux-distro-packaging.md b/docs/design/linux-distro-packaging.md
index 53baf09..95faf50 100644
--- a/docs/design/linux-distro-packaging.md
+++ b/docs/design/linux-distro-packaging.md
@@ -168,6 +168,20 @@ release tarball, x86_64 + aarch64), and `splitway-gui` (source), each with an
 CI builds the source PKGBUILDs from the checkout (the release tag may not exist
 on a dev/PR run) and validates `.SRCINFO` + `namcap`.
 
+The committed `pkgver=` pins a **released tag**, not the in-tree daemon version.
+The two differ on purpose: release.yml's post-release auto-bump moves
+`splitway-daemon/Cargo.toml` to the next (unreleased) version, so at rest on
+`master` the daemon is one patch ahead of the newest `v*` tag. Each PKGBUILD's
+`source=` fetches `v$pkgver`, so the pin must name a tag that exists. The
+bump-version job runs `sync-pkgver.sh` **before** the daemon bump, stamping the
+version just tagged — so on `master` at rest the pin is the *latest* release.
+The gate `check-pkgver-sync.sh` (packaging.yml `meta`, with tags fetched)
+enforces only the weaker, sufficient condition that the pinned tag **exists**
+(not that it is the latest): that keeps `makepkg -si` working while avoiding
+spurious failures on dev/PR branches or in the release-push window, where the
+PKGBUILDs legitimately still point at the previous release until the bump commit
+lands.
+
 **Deferred:** the automated `ssh://aur@aur.archlinux.org/.git` push, blocked
 on AUR registration reopening. Design preserved for then: per package
 `makepkg --printsrcinfo > .SRCINFO`, commit + push idempotently, release-only.
diff --git a/packaging/ci/check-pkgver-sync.sh b/packaging/ci/check-pkgver-sync.sh
index 4ef47ba..b98edb4 100755
--- a/packaging/ci/check-pkgver-sync.sh
+++ b/packaging/ci/check-pkgver-sync.sh
@@ -1,29 +1,54 @@
 #!/usr/bin/env bash
-# Guard the lockstep-version invariant. The single source of truth is the daemon
-# crate's version (splitway-daemon/Cargo.toml, read by compute-version.sh); the
-# deb/rpm packages are stamped from it in CI, but the AUR PKGBUILDs carry a
-# hand-kept `pkgver=` that is easy to forget on a version bump. Fail the build if
-# any PKGBUILD's pkgver has drifted from the daemon version, so the three can't
-# silently diverge.
+# Guard the AUR-pkgver invariant: the version pinned in every committed PKGBUILD
+# must name a release that ACTUALLY EXISTS, because each `source=` fetches the
+# `v$pkgver` git tag / release assets. release.yml's post-release auto-bump
+# pushes splitway-daemon/Cargo.toml to the NEXT (unreleased) version, so the
+# daemon version is deliberately one ahead of the latest release at rest on
+# master — the PKGBUILDs must therefore pin a real *released tag*, never the
+# in-tree daemon version (the write side, sync-pkgver.sh, stamps the version
+# being released). Fail the build on drift so `makepkg -si` from a checkout can
+# never point at a tag / assets that do not exist.
+#
+# Needs the tags fetched (packaging.yml's meta job uses fetch-depth: 0).
 #
 #   check-pkgver-sync.sh
 set -euo pipefail
 
-# awk (not `grep | head -1`): SIGPIPE-free under `set -o pipefail` if a second
-# `^version` line ever appears. Mirrors compute-version.sh / sync-pkgver.sh.
-ver="$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)"
-[ -n "$ver" ] || { echo "ERROR: could not read daemon version" >&2; exit 1; }
-
-rc=0
+# All three PKGBUILDs describe one project at one version, so they must agree.
+# awk (not `grep | head -1`) is SIGPIPE-free under `set -o pipefail`.
+pkgver=""
 for pb in packaging/aur/*/PKGBUILD; do
     pv="$(awk -F= '/^pkgver=/{print $2; exit}' "$pb")"
-    if [ "$pv" != "$ver" ]; then
-        echo "ERROR: $pb has pkgver=$pv but daemon version=$ver (lockstep drift)" >&2
-        rc=1
+    [ -n "$pv" ] || { echo "ERROR: $pb has no pkgver=" >&2; exit 1; }
+    if [ -z "$pkgver" ]; then
+        pkgver="$pv"
+    elif [ "$pv" != "$pkgver" ]; then
+        echo "ERROR: $pb has pkgver=$pv but the others use $pkgver (AUR PKGBUILDs must share one version)" >&2
+        exit 1
     fi
 done
+[ -n "$pkgver" ] || { echo "ERROR: no PKGBUILDs found under packaging/aur/" >&2; exit 1; }
+
+# Does the pinned release tag exist? `--count=1` makes git stop after the first
+# match, so the captured read is SIGPIPE-free under `set -o pipefail`.
+if [ -n "$(git for-each-ref --count=1 --format='%(refname:short)' "refs/tags/v$pkgver")" ]; then
+    echo "pkgver $pkgver in sync — release tag v$pkgver exists"
+    exit 0
+fi
+
+# No tag for this pkgver. If ANY v* tag exists we have drifted onto a
+# non-existent release (the bug this gate guards). Only the pre-first-release
+# bootstrap — no v* tags at all — is allowed, and then only while the pkgver
+# still tracks the daemon version (the convention before the first release).
+if [ -n "$(git for-each-ref --count=1 --format='%(refname:short)' 'refs/tags/v*')" ]; then
+    echo "ERROR: PKGBUILD pkgver=$pkgver but release tag v$pkgver does not exist (pin a released tag)" >&2
+    exit 1
+fi
 
-if [ "$rc" = 0 ]; then
-    echo "pkgver in sync ($ver) across all AUR PKGBUILDs"
+ver="$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)"
+if [ "$pkgver" = "$ver" ]; then
+    echo "pkgver $pkgver matches daemon version — no release tagged yet (bootstrap)"
+    exit 0
 fi
-exit "$rc"
+echo "ERROR: no release tag exists yet and pkgver=$pkgver differs from daemon version=$ver" >&2
+exit 1
diff --git a/packaging/ci/sync-pkgver.sh b/packaging/ci/sync-pkgver.sh
index ca75476..d597b26 100755
--- a/packaging/ci/sync-pkgver.sh
+++ b/packaging/ci/sync-pkgver.sh
@@ -1,16 +1,19 @@
 #!/usr/bin/env bash
-# Write side of the lockstep-version invariant (the read side is
-# check-pkgver-sync.sh). The daemon crate's version is the single source of
-# truth; this stamps it into every AUR PKGBUILD's `pkgver=` and resets `pkgrel=1`
-# (Arch convention on a version change). Run it right after bumping
-# splitway-daemon/Cargo.toml so the gate in packaging.yml's `meta` job stays
-# green — without this, each post-release auto-bump would leave the PKGBUILDs
-# behind and fail every later packaging run until a human edited all three.
+# Write side of the AUR-pkgver invariant (the read side is check-pkgver-sync.sh).
+# Stamps the daemon crate's CURRENT version into every AUR PKGBUILD's `pkgver=`
+# and resets `pkgrel=1` (Arch convention on a version change). release.yml's
+# bump-version job runs this BEFORE the post-release daemon bump — i.e. while
+# Cargo.toml still holds the version just tagged — so the PKGBUILDs pin the
+# just-released tag (the one `source=` fetches), NOT the next unreleased dev
+# version the bump moves the daemon to. Without it the PKGBUILDs would stay on
+# the previous release and fail later packaging runs until a human edited all
+# three.
 #
 #   sync-pkgver.sh
 set -euo pipefail
 
-# Same read as check-pkgver-sync.sh — keep these two in step. awk (not
+# Read the version being released (the pre-bump daemon version). Same read as
+# check-pkgver-sync.sh's bootstrap branch — keep these in step. awk (not
 # `grep | head -1`) is SIGPIPE-free under `set -o pipefail`.
 ver="$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)"
 [ -n "$ver" ] || { echo "ERROR: could not read daemon version" >&2; exit 1; }

From 7154872a629bb087a1fd9e3ebf86b5b042855966 Mon Sep 17 00:00:00 2001
From: stslex 
Date: Thu, 25 Jun 2026 23:23:31 +0300
Subject: [PATCH 16/28] fix(packaging): key build-arch source archive on
 PKGBUILD pkgver, not daemon (Codex P2)

Follow-up to pinning the AUR pkgver to the released tag: the daemon version
now intentionally runs ahead of the committed pkgver. build-arch pre-placed
the checkout tarball as -$VERSION.tar.gz (daemon version), but the
PKGBUILD's source= expects -$pkgver.tar.gz. When the two diverge makepkg
can't find the local archive and silently downloads the old v$pkgver tag,
building stale code while the smoke test still passes.

Read pkgver from each PKGBUILD and key both the archive filename and the
git-archive --prefix on it, so the pre-placed checkout is always the source
makepkg uses. Drop the now-unused VERSION env.

Co-Authored-By: Claude Opus 4.8 (1M context) 
Claude-Session: https://claude.ai/code/session_01EKokH93URrCizdQdVpNCNM
---
 .github/workflows/packaging.yml | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml
index 4d43314..2fa5cd6 100644
--- a/.github/workflows/packaging.yml
+++ b/.github/workflows/packaging.yml
@@ -394,8 +394,6 @@ jobs:
     needs: meta
     runs-on: ubuntu-latest
     container: archlinux:latest
-    env:
-      VERSION: ${{ needs.meta.outputs.version }}
     steps:
       - name: Install build tooling
         run: |
@@ -411,13 +409,20 @@ jobs:
         run: |
           set -euo pipefail
           mkdir -p dist
+          sudo -u builder git config --global --add safe.directory "$PWD"
           for pkg in splitway splitway-gui; do
             # The PKGBUILD's source filename is -.tar.gz with a
             # top-level dir splitway-; pre-place it so makepkg builds the
-            # checkout instead of downloading the tag.
-            sudo -u builder git config --global --add safe.directory "$PWD"
-            sudo -u builder git archive --format=tar.gz --prefix="splitway-$VERSION/" \
-              -o "packaging/aur/$pkg/$pkg-$VERSION.tar.gz" HEAD
+            # checkout instead of downloading the tag. Key it on the committed
+            # pkgver (read from the PKGBUILD), NOT the daemon version: after a
+            # release the two differ on purpose — the PKGBUILD pins the released
+            # tag while the daemon is bumped ahead (see check-pkgver-sync.sh) — so
+            # a name keyed on the daemon version would miss makepkg's expected
+            # source, and makepkg would silently fetch the old tag and ship stale
+            # Arch packages while this build still passes.
+            pkgver="$(awk -F= '/^pkgver=/{print $2; exit}' "packaging/aur/$pkg/PKGBUILD")"
+            sudo -u builder git archive --format=tar.gz --prefix="splitway-$pkgver/" \
+              -o "packaging/aur/$pkg/$pkg-$pkgver.tar.gz" HEAD
             ( cd "packaging/aur/$pkg" \
               && sudo -u builder makepkg -f --noconfirm --nodeps --skipinteg \
               && sudo -u builder bash -c 'makepkg --printsrcinfo > .SRCINFO' )

From 839c42f499e4dcfd70f8f59fd1114d591da7fba3 Mon Sep 17 00:00:00 2001
From: stslex 
Date: Thu, 25 Jun 2026 23:41:24 +0300
Subject: [PATCH 17/28] fix(packaging): build Arch packages as the release
 version, not the pinned tag (Codex P1)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The committed PKGBUILD pkgver intentionally lags to the last released tag (so a
user's `makepkg -si` resolves an existing tag), but the hosted pacman repo must
ship the version being released. The prior approach keyed the build on the
committed pkgver, so a release push would publish Arch packages under the
PREVIOUS version and Arch users would never receive the new one.

build-arch now stamps the daemon/meta version into the EPHEMERAL PKGBUILD (never
committed) and pre-places the matching -.tar.gz so makepkg
builds THIS checkout as VERSION — no tag download, correct published version.
The committed PKGBUILDs and the check-pkgver-sync.sh gate are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) 
Claude-Session: https://claude.ai/code/session_01EKokH93URrCizdQdVpNCNM
---
 .github/workflows/packaging.yml | 29 +++++++++++++++++------------
 1 file changed, 17 insertions(+), 12 deletions(-)

diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml
index 2fa5cd6..8812869 100644
--- a/.github/workflows/packaging.yml
+++ b/.github/workflows/packaging.yml
@@ -394,6 +394,8 @@ jobs:
     needs: meta
     runs-on: ubuntu-latest
     container: archlinux:latest
+    env:
+      VERSION: ${{ needs.meta.outputs.version }}
     steps:
       - name: Install build tooling
         run: |
@@ -411,18 +413,21 @@ jobs:
           mkdir -p dist
           sudo -u builder git config --global --add safe.directory "$PWD"
           for pkg in splitway splitway-gui; do
-            # The PKGBUILD's source filename is -.tar.gz with a
-            # top-level dir splitway-; pre-place it so makepkg builds the
-            # checkout instead of downloading the tag. Key it on the committed
-            # pkgver (read from the PKGBUILD), NOT the daemon version: after a
-            # release the two differ on purpose — the PKGBUILD pins the released
-            # tag while the daemon is bumped ahead (see check-pkgver-sync.sh) — so
-            # a name keyed on the daemon version would miss makepkg's expected
-            # source, and makepkg would silently fetch the old tag and ship stale
-            # Arch packages while this build still passes.
-            pkgver="$(awk -F= '/^pkgver=/{print $2; exit}' "packaging/aur/$pkg/PKGBUILD")"
-            sudo -u builder git archive --format=tar.gz --prefix="splitway-$pkgver/" \
-              -o "packaging/aur/$pkg/$pkg-$pkgver.tar.gz" HEAD
+            # Build + publish the CURRENT version (VERSION = the clean daemon/meta
+            # version), not the committed pin. The committed pkgver intentionally
+            # lags to the last released tag so a user's `makepkg -si` from a
+            # checkout resolves an existing tag (see check-pkgver-sync.sh) — but
+            # the hosted pacman repo must ship VERSION, or a release push would
+            # publish under the previous version and Arch users never get the new
+            # one. Stamp VERSION into the EPHEMERAL PKGBUILD (never committed) and
+            # pre-place the matching -.tar.gz (top-level dir
+            # splitway-); keying the archive on the same VERSION is what
+            # makes makepkg build THIS checkout instead of fetching the tag.
+            sudo -u builder sed -i \
+              -e "s/^pkgver=.*/pkgver=$VERSION/" -e "s/^pkgrel=.*/pkgrel=1/" \
+              "packaging/aur/$pkg/PKGBUILD"
+            sudo -u builder git archive --format=tar.gz --prefix="splitway-$VERSION/" \
+              -o "packaging/aur/$pkg/$pkg-$VERSION.tar.gz" HEAD
             ( cd "packaging/aur/$pkg" \
               && sudo -u builder makepkg -f --noconfirm --nodeps --skipinteg \
               && sudo -u builder bash -c 'makepkg --printsrcinfo > .SRCINFO' )

From a1aa009d8de57bfb0c1e695042f6b26b9d07f84a Mon Sep 17 00:00:00 2001
From: stslex 
Date: Thu, 25 Jun 2026 23:41:24 +0300
Subject: [PATCH 18/28] fix(packaging): use pacman -Syu (not -Sy) in install
 snippets (Codex P2)

`pacman -Sy ` refreshes the sync databases without upgrading, leaving the
DB ahead of installed packages and resolving deps from a partial-upgrade state.
Use `-Syu` in the hosted-index and README install snippets. The CI smoke tests
keep `-Sy` (throwaway containers, not user systems).

Co-Authored-By: Claude Opus 4.8 (1M context) 
Claude-Session: https://claude.ai/code/session_01EKokH93URrCizdQdVpNCNM
---
 README.md                    | 2 +-
 packaging/ci/render-index.sh | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index fcff872..54d2035 100755
--- a/README.md
+++ b/README.md
@@ -300,7 +300,7 @@ sudo tee -a /etc/pacman.conf <<'EOF'
 SigLevel = Required DatabaseOptional
 Server = https://stslex.github.io/splitway/arch/release/$arch
 EOF
-sudo pacman -Sy splitway               # add splitway-gui for the desktop app
+sudo pacman -Syu splitway              # add splitway-gui for the desktop app
 ```
 
 x86_64 only. On aarch64, or to build from source, use the in-repo PKGBUILDs:
diff --git a/packaging/ci/render-index.sh b/packaging/ci/render-index.sh
index 931accf..2b46ca5 100644
--- a/packaging/ci/render-index.sh
+++ b/packaging/ci/render-index.sh
@@ -88,7 +88,7 @@ sudo tee -a /etc/pacman.conf <<'EOF'
 SigLevel = Required DatabaseOptional
 Server = ${BASE}/arch/release/\$arch
 EOF
-sudo pacman -Sy splitway        # add splitway-gui for the desktop app
+sudo pacman -Syu splitway # add splitway-gui for the desktop app

The hosted pacman repo is x86_64 only. On aarch64, build the in-repo PKGBUILD below or use splitway-bin (prebuilt from the release tarball).

From 38476f76b26c1562cab192d933be57b5c712d766 Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 26 Jun 2026 08:17:20 +0300 Subject: [PATCH 19/28] fix(packaging): keep Cargo.lock in lockstep with the release bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit origin/master carried splitway-daemon Cargo.toml=0.0.6 with a stale Cargo.lock=0.0.5: release.yml's post-release auto-bump rewrote the toml but never relocked. The dev->master merge tree inherited that drift, so `cargo fetch --locked` (build-arch's makepkg prepare) and `cargo build --frozen` (build-gui) aborted with "cannot update the lock file ... because --locked was passed" — the red build-arch / build-gui checks on PR #37. - Sync this branch's Cargo.lock to daemon 0.0.6 (merged from master), so the merge tree is internally consistent and the packaging jobs build. - release.yml bump-version: add a toolchain, then `cargo update -p splitway-daemon --precise "$NEW_VERSION"` and stage Cargo.lock in the bump commit, so master never drifts again. - The bot bump commit gets `[skip ci]`: a GITHUB_TOKEN push already does not re-trigger workflows, but the marker makes it explicit (and holds if the push ever moves to a PAT), so the next-cycle bump can never publish an unreleased X.Y.(Z+1) to the stable channels (Codex P1). - ci.yml: a `cargo metadata --locked` guard fails loudly on any future Cargo.lock drift instead of letting it surface as a packaging abort. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 7 +++++++ .github/workflows/release.yml | 20 +++++++++++++++++--- Cargo.lock | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54befe5..cb49db6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,13 @@ jobs: with: components: rustfmt, clippy + # Fail loudly if Cargo.lock has drifted from the manifests (e.g. a member + # version bumped without relocking). --locked makes cargo refuse to update + # the lock, so a stale lock errors here instead of surfacing later as a + # `cargo fetch --locked` abort in the packaging build-arch/build-gui jobs. + - name: Verify Cargo.lock is in sync with the manifests + run: cargo metadata --format-version 1 --locked > /dev/null + # splitway-gui (eframe/egui, glow renderer) links a windowing/GL stack on # Linux; install the dev libraries so clippy/test/build compile it. macOS # uses system frameworks (Cocoa/Metal) and needs none. rfd uses the XDG diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6de5ad3..f87206a 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -128,6 +128,9 @@ jobs: # validates the pinned tag exists, not that it equals the daemon). run: bash packaging/ci/sync-pkgver.sh + # cargo update (below) needs a toolchain; this job otherwise only runs bash. + - uses: dtolnay/rust-toolchain@stable + - name: Bump patch version in Cargo.toml for the next dev cycle shell: bash run: | @@ -139,13 +142,24 @@ jobs: NEW_PATCH=$((PATCH + 1)) NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH" sed -i "s/^version = \"$VERSION\"/version = \"$NEW_VERSION\"/" splitway-daemon/Cargo.toml - echo "Bumped $VERSION -> $NEW_VERSION" + # Keep Cargo.lock in lockstep with the bumped member version. Without + # this, master is left with toml=NEW but lock=OLD, and the next + # dev->master merge tree inherits that drift — so `cargo fetch --locked` + # / `cargo build --frozen` abort in the packaging build-arch/build-gui + # jobs. --precise rewrites only splitway-daemon's lock entry. + cargo update -p splitway-daemon --precise "$NEW_VERSION" + echo "Bumped $VERSION -> $NEW_VERSION (Cargo.toml + Cargo.lock)" - name: Commit and push the AUR pin + version bump shell: bash run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add splitway-daemon/Cargo.toml packaging/aur/*/PKGBUILD - git commit -m "chore: pin AUR pkgver to release, bump daemon for next cycle" + git add splitway-daemon/Cargo.toml Cargo.lock packaging/aur/*/PKGBUILD + # [skip ci]: this bot commit must never re-trigger release/packaging. + # A GITHUB_TOKEN push already does not start new workflow runs, but the + # marker makes that intent explicit (and holds if the push is ever moved + # to a PAT), so the next-cycle bump can never publish an unreleased + # version to the stable apt/dnf/pacman channels. + git commit -m "chore: pin AUR pkgver to release, bump daemon for next cycle [skip ci]" git push diff --git a/Cargo.lock b/Cargo.lock index 5c497b6..f1fed0b 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -4735,7 +4735,7 @@ dependencies = [ [[package]] name = "splitway-daemon" -version = "0.0.5" +version = "0.0.6" dependencies = [ "core-foundation 0.9.4", "env_logger", From 1ca0a83fe2d7c62bbcf676f85e030942c5e227e6 Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 26 Jun 2026 08:17:31 +0300 Subject: [PATCH 20/28] fix(packaging): make publish-pages post-deploy smoke non-fatal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-deploy steps poll the live Pages URL for 10 min and `exit 1` on timeout. On the first (root-commit) gh-pages deploy — Pages not yet enabled/propagated — the package is never served in the window, so the whole publish-pages job is reported failed even though the signed deploy already pushed and verified cleanly. Downgrade both liveness timeouts (apt/dnf smoke and the pacman smoke) to `::warning` + `exit 0`, skipping the live install. The pre-push "Verify all signatures (fail closed)" step still gates integrity before the push, and the actual install stays fatal once the repo IS live — only the propagation-timeout path is now non-fatal, so a verified publish is never reported as failed. Note: enabling GitHub Pages for the gh-pages branch is a one-time repo setting (Settings -> Pages -> Branch: gh-pages); nothing in CI can do it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/packaging.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index 8812869..829433c 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -687,11 +687,17 @@ jobs: set -euo pipefail # GitHub Pages publishes asynchronously; wait until the just-uploaded # package is live before testing the install. + # + # Non-fatal on timeout: the deploy already pushed and every signature + # was verified above (fail-closed), so a good publish must never be + # reported failed just because Pages has not finished propagating — most + # acutely on the FIRST (root-commit) gh-pages deploy, when Pages may not + # be enabled/built yet. Warn and exit 0 (skipping the install below). url="$BASE/deb/$CHANNEL/pool/main/splitway_${PKGVER}_amd64.deb" for i in $(seq 1 60); do if curl -fsI "$url" >/dev/null 2>&1; then echo "live after ${i}0s"; break; fi sleep 10 - [ "$i" = 60 ] && { echo "ERROR: package not live after 10m"; exit 1; } + [ "$i" = 60 ] && { echo "::warning::package not live after 10m (GitHub Pages propagation/enablement?) — publish succeeded, skipping live install smoke"; exit 0; } done docker run --rm debian:bookworm bash -euxc " export DEBIAN_FRONTEND=noninteractive @@ -722,10 +728,11 @@ jobs: if: ${{ needs.meta.outputs.channel == 'release' }} run: | set -euo pipefail + # Non-fatal on timeout, same rationale as the apt/dnf smoke above. for i in $(seq 1 60); do if curl -fsI "$BASE/arch/release/x86_64/splitway.db" >/dev/null 2>&1; then break; fi sleep 10 - [ "$i" = 60 ] && { echo "ERROR: pacman repo not live after 10m"; exit 1; } + [ "$i" = 60 ] && { echo "::warning::pacman repo not live after 10m (GitHub Pages propagation/enablement?) — publish succeeded, skipping live pacman smoke"; exit 0; } done docker run --rm -e BASE="$BASE" -e FPR="$GPG_FPR" archlinux:latest bash -euxc ' pacman -Sy --noconfirm From a8da270a95041c462c05e46a43b361d859c0307e Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 26 Jun 2026 08:17:42 +0300 Subject: [PATCH 21/28] fix(packaging): robust dnf passphrase handling + monotonic dev pkgver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-dnf-repo.sh: the single PASS_OPT string relied on word-splitting (SC2086 disabled) at the gpg call. Split it: pass_opt[] (array) for the direct gpg detach-sign — space-robust, mirroring build-apt-repo.sh's gpg_sign — and pass_macro (plain string, byte-identical to the original) for the rpm __gpg_sign_cmd macro, which rpm tokenizes itself and cannot take a bash array. The passphrase FILE path is mktemp-derived and never has spaces, so the macro keeps the proven form (zero regression on the real-key publish path, which has never run in CI). compute-version.sh: append GITHUB_RUN_NUMBER to the dev pkgver -> `~dev...`. Two pushes in the same UTC second now order monotonically (run number is compared numerically) instead of falling back to the non-monotonic lexical short-sha compare. Defaults to 0 outside Actions. Co-Authored-By: Claude Opus 4.8 (1M context) --- packaging/ci/build-dnf-repo.sh | 23 ++++++++++++++++------- packaging/ci/compute-version.sh | 14 ++++++++++---- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packaging/ci/build-dnf-repo.sh b/packaging/ci/build-dnf-repo.sh index ee8ccae..fbe4c9b 100755 --- a/packaging/ci/build-dnf-repo.sh +++ b/packaging/ci/build-dnf-repo.sh @@ -18,10 +18,20 @@ KEY="${2:-}" [ -d "$ROOT" ] || { echo "error: $ROOT does not exist" >&2; exit 1; } -# Optional passphrase file for the real (passphrase-protected) key; unset for -# the ephemeral CI key. -PASS_OPT="" -[ -n "${SPLITWAY_GPG_PASSFILE:-}" ] && PASS_OPT="--passphrase-file ${SPLITWAY_GPG_PASSFILE}" +# Optional passphrase for the real (passphrase-protected) key; unset for the +# ephemeral CI key. Two forms for the two call sites: +# * pass_opt[] — array for the direct gpg invocation below, robust to a path +# with spaces (mirrors the gpg_sign array in build-apt-repo.sh). +# * pass_macro — plain string for the rpm __gpg_sign_cmd macro: rpm tokenizes +# the expanded macro itself (no bash array possible), so this +# keeps the proven string form. The passphrase FILE path is +# mktemp-derived ($GNUPGHOME/passphrase) and never has spaces. +pass_opt=() +pass_macro="" +if [ -n "${SPLITWAY_GPG_PASSFILE:-}" ]; then + pass_opt=(--passphrase-file "$SPLITWAY_GPG_PASSFILE") + pass_macro="--passphrase-file ${SPLITWAY_GPG_PASSFILE}" +fi if [ -n "$KEY" ]; then # Header-sign every rpm in place (idempotent; re-signing is harmless). @@ -32,7 +42,7 @@ if [ -n "$KEY" ]; then if [ ${#rpms[@]} -gt 0 ]; then rpm \ --define "_gpg_name $KEY" \ - --define "__gpg_sign_cmd %{__gpg} gpg --batch --no-armor ${PASS_OPT} --pinentry-mode loopback --no-secmem-warning -u %{_gpg_name} -sbo %{__signature_filename} %{__plaintext_filename}" \ + --define "__gpg_sign_cmd %{__gpg} gpg --batch --no-armor ${pass_macro} --pinentry-mode loopback --no-secmem-warning -u %{_gpg_name} -sbo %{__signature_filename} %{__plaintext_filename}" \ --addsign "${rpms[@]}" fi fi @@ -42,8 +52,7 @@ createrepo_c --update "$ROOT" if [ -n "$KEY" ]; then rm -f "$ROOT/repodata/repomd.xml.asc" - # shellcheck disable=SC2086 # PASS_OPT is intentionally word-split (0 or 2 args) - gpg --batch --yes --pinentry-mode loopback $PASS_OPT --default-key "$KEY" \ + gpg --batch --yes --pinentry-mode loopback "${pass_opt[@]}" --default-key "$KEY" \ --detach-sign --armor "$ROOT/repodata/repomd.xml" echo "dnf repo signed with key $KEY -> $ROOT/repodata/{repomd.xml,repomd.xml.asc}" else diff --git a/packaging/ci/compute-version.sh b/packaging/ci/compute-version.sh index 911d9e9..db1a502 100755 --- a/packaging/ci/compute-version.sh +++ b/packaging/ci/compute-version.sh @@ -10,9 +10,10 @@ # # Release channel: push to master -> clean . # Dev channel: everything else (push to dev, pull_request, workflow_dispatch) -> -# ~dev... The `~dev` suffix sorts BELOW -# the clean release in both dpkg and rpm (>=4.10), so a tester with both repos -# enabled upgrades dev -> release cleanly. +# ~dev.... The `~dev` suffix +# sorts BELOW the clean release in both dpkg and rpm (>=4.10), so a tester with +# both repos enabled upgrades dev -> release cleanly. keeps two +# same-second builds monotonically ordered (see below). set -euo pipefail event="${1:?usage: compute-version.sh }" @@ -29,7 +30,12 @@ if [ "$event" = "push" ] && [ "$ref" = "refs/heads/master" ]; then else utc="$(date -u +%Y%m%d%H%M%S)" sha="$(git rev-parse --short HEAD)" - pkgver="${version}~dev.${utc}.${sha}" + # GITHUB_RUN_NUMBER increments per run, so two pushes that land in the same + # UTC second still order monotonically — without it dpkg/rpm fall back to + # comparing the short sha lexically, which is NOT monotonic with commit order + # and can pick the wrong "newest" build. Defaults to 0 outside Actions. + run="${GITHUB_RUN_NUMBER:-0}" + pkgver="${version}~dev.${utc}.${run}.${sha}" channel="dev" fi From 6f7c141a847af4bbb097331a28c5201040a3fdc8 Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 26 Jun 2026 08:17:53 +0300 Subject: [PATCH 22/28] docs(packaging): sync GUI dep lists with manifests; note portal + pkgver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The design doc's GUI dependency lists had drifted from the manifests: - Debian: add libegl1 + libxkbcommon-x11-0 - Fedora: add mesa-libEGL + libxkbcommon-x11 - Arch: add libxkbcommon-x11 …with a note on why EGL is separate from GL (EGL context on Wayland) and xkbcommon-x11 separate from xkbcommon (winit x11 dlopen), and that the list is the windowing-lib subset (the PKGBUILD also carries the icon / desktop-file install-hook deps). Also update the now-stale dev version format `~dev..` -> `~dev...` in the channel-topology table and in packaging/README.md to match compute-version.sh. splitway-gui/Cargo.toml: document the rpm Recommends asymmetry — the deb offers gtk|wlr|kde but a cargo-generate-rpm `key="*"` table has no boolean-OR, so it names the GTK portal (a weak dep that works on most desktops); a passthrough rich-dep is left out until verified to emit correctly through cargo-generate-rpm. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/design/linux-distro-packaging.md | 27 +++++++++++++++++++-------- packaging/README.md | 2 +- splitway-gui/Cargo.toml | 7 +++++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/design/linux-distro-packaging.md b/docs/design/linux-distro-packaging.md index 95faf50..9a1dcaf 100644 --- a/docs/design/linux-distro-packaging.md +++ b/docs/design/linux-distro-packaging.md @@ -55,8 +55,8 @@ author's iteration channel. | Trigger | Channel | Version | Publishes to | |--------------------|---------|----------------------|--------------| | push → `master` | release | `` | `deb/release`, `rpm/release`, `arch/release/x86_64`, GitHub Release (tarballs) | -| push → `dev` | dev | `~dev..` | `deb/dev`, `rpm/dev` | -| pull_request | — | `~dev..` | nothing (build + test only) | +| push → `dev` | dev | `~dev...` | `deb/dev`, `rpm/dev` | +| pull_request | — | `~dev...` | nothing (build + test only) | `~dev` sorts **below** the clean release in dpkg and rpm (≥4.10), so a tester with both repos enabled upgrades dev → release cleanly. @@ -80,12 +80,23 @@ windowing libraries at runtime**, so they are absent from the binary's ELF cargo-generate-rpm's ELF-based `auto-req` can see them — they **must be hardcoded**: -- Debian: `libgl1, libx11-6, libxcursor1, libxi6, libxrandr2, - libwayland-client0, libxkbcommon0`, plus `libc6 (>= 2.31)` to pin the floor. -- Fedora: `mesa-libGL, libX11, libXcursor, libXi, libXrandr, libwayland-client, - libxkbcommon` (auto-req still derives the glibc/libgcc floor from the - linked-against sonames). -- Arch: `libglvnd libxkbcommon wayland libx11 libxcursor libxi libxrandr`. +- Debian: `libgl1, libegl1, libx11-6, libxcursor1, libxi6, libxrandr2, + libwayland-client0, libxkbcommon0, libxkbcommon-x11-0`, plus `libc6 (>= 2.31)` + to pin the floor. +- Fedora: `mesa-libGL, mesa-libEGL, libX11, libXcursor, libXi, libXrandr, + libwayland-client, libxkbcommon, libxkbcommon-x11` (auto-req still derives the + glibc/libgcc floor from the linked-against sonames). +- Arch: `libglvnd libxkbcommon libxkbcommon-x11 wayland libx11 libxcursor libxi + libxrandr` (`libglvnd` provides both GL and EGL). + +`libegl1`/`mesa-libEGL` is listed **separately** from GL: glow/glutin creates its +GL context via EGL on Wayland (GLX on X11), and the GL package does not pull EGL — +without it a Wayland-only install fails at context creation. `libxkbcommon-x11` +is likewise separate from `libxkbcommon0`: winit's `x11` feature `dlopen`s +`libxkbcommon-x11.so`, which the base xkbcommon package does not pull. Keep these +windowing-lib lists in sync with the GUI manifest `depends` / `[requires]` and +the Arch PKGBUILD `depends` (whose `depends` additionally carry the +`hicolor-icon-theme` / `desktop-file-utils` install-hook deps, out of scope here). `xdg-desktop-portal` + a backend (`-gtk`/`-wlr`/`-kde`) is `Recommends`: rfd's file dialog uses the portal here (no GTK linked) and **silently no-ops** without diff --git a/packaging/README.md b/packaging/README.md index 81c426a..1d1cb08 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -123,7 +123,7 @@ running `usermod -aG splitway ` + re-login (same contract as the NixOS deletes the drop-in and (if empty) the group, reverting to root-only. **Channels.** `release` (push → `master`, clean ``) and `dev` (push → -`dev`, `~dev..`) are separate Pages subtrees. `~dev` sorts +`dev`, `~dev...`) are separate Pages subtrees. `~dev` sorts below the release in dpkg and rpm, so dev → release upgrades cleanly. Deploys **merge** the channel into persistent `gh-pages` state — old versions and the other channel survive. diff --git a/splitway-gui/Cargo.toml b/splitway-gui/Cargo.toml index 0eaf787..a3d934a 100644 --- a/splitway-gui/Cargo.toml +++ b/splitway-gui/Cargo.toml @@ -188,4 +188,11 @@ libxkbcommon-x11 = "*" [package.metadata.generate-rpm.recommends] xdg-desktop-portal = "*" +# Asymmetry vs the deb Recommends (which offers gtk | wlr | kde): a cargo-generate-rpm +# `key = "*"` table has no boolean-OR, so we name the GTK portal — it works on most +# desktops (rfd links no GTK; it speaks the portal over D-Bus). On a native KDE/wlroots +# host this only *nudges* GTK (a weak dep); the native backend, if installed, is used. +# A passthrough rpm rich-dep — `"(xdg-desktop-portal-gtk or xdg-desktop-portal-kde or +# xdg-desktop-portal-wlr)" = "*"` — would mirror the deb, but is left out until verified +# to emit correctly through cargo-generate-rpm (it would only surface at rpm-build time). xdg-desktop-portal-gtk = "*" From 4f3d086a82305eac8744f1060ab7ec3980b288ea Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 26 Jun 2026 08:37:53 +0300 Subject: [PATCH 23/28] =?UTF-8?q?ci(release):=20auto-open=20a=20master?= =?UTF-8?q?=E2=86=92dev=20sync=20PR=20after=20the=20post-release=20bump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-release auto-bump only updates master, so after each release dev falls one version behind (exactly the drift that left this branch at 0.0.5 while master was at 0.0.6). Add a `sync-dev` job (`needs: bump-version`) that opens — or refreshes — a PR merging master back into dev so dev tracks the next dev cycle automatically. A PR, not a direct push: dev follows the branch→PR workflow and may be protected. The job no-ops when dev already contains master, reuses an open sync PR instead of duplicating, and enables auto-merge best-effort (lands once mergeable where the repo allows it, otherwise waits for a one-click merge). The PR is opened by GITHUB_TOKEN, which does not trigger pull_request CI on itself — documented inline, with the PAT escape hatch for anyone who wants required checks to gate the sync merge. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f87206a..6dd916c 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -163,3 +163,51 @@ jobs: # version to the stable apt/dnf/pacman channels. git commit -m "chore: pin AUR pkgver to release, bump daemon for next cycle [skip ci]" git push + + # After the post-release bump lands on master, dev is one bump behind. Open + # (or refresh) a PR that merges master back into dev so dev tracks the next dev + # cycle automatically — a PR rather than a direct push, because dev follows the + # branch -> PR workflow and may be protected. Best-effort auto-merge makes it + # hands-off where the repo allows it (no required checks); otherwise the PR just + # waits for a one-click merge. NOTE: this PR is opened by GITHUB_TOKEN, so it + # does NOT trigger pull_request CI on itself (GitHub suppresses that) — required + # checks would block auto-merge; use a PAT here if you want them to run. + sync-dev: + needs: bump-version + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Open/refresh a PR syncing the post-release bump into dev + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git fetch --quiet origin master dev + # Nothing to sync if dev already contains master's tip. + if git merge-base --is-ancestor origin/master origin/dev; then + echo "dev already contains master — no sync PR needed"; exit 0 + fi + # awk (not `grep | head -1`): SIGPIPE-free version read under pipefail. + VERSION=$(git show origin/master:splitway-daemon/Cargo.toml \ + | awk -F'"' '/^version/{print $2; exit}') + num=$(gh pr list --base dev --head master --state open \ + --json number --jq '.[0].number // empty') + if [ -n "$num" ]; then + echo "sync PR #$num already open; the master push refreshed it" + else + url=$(gh pr create --base dev --head master \ + --title "chore: sync post-release bump into dev (next cycle ${VERSION})" \ + --body "Automated by release.yml after the release was tagged. Brings master's post-release auto-bump — splitway-daemon → ${VERSION}, regenerated Cargo.lock, AUR pkgver pins — back into \`dev\` so dev tracks the next dev cycle." \ + ) || { echo "could not open sync PR (resolve manually): merge origin/master into dev"; exit 0; } + num=$(printf '%s' "$url" | sed -n 's#.*/pull/\([0-9][0-9]*\).*#\1#p') + echo "opened sync PR #${num} ($url)" + fi + # Best-effort: land it automatically once mergeable. No-op (PR waits for + # manual merge) if the repo lacks auto-merge or has required checks. + [ -n "$num" ] && { gh pr merge --auto --merge "$num" \ + || echo "auto-merge unavailable; sync PR #${num} left for manual merge"; } From 672a9e604932ed140fd2fac6a6915df317148d62 Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 26 Jun 2026 09:14:43 +0300 Subject: [PATCH 24/28] ci: harden the Cargo.lock drift guard against transient registry errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new `Verify Cargo.lock` step resolves against the crates.io index on a cold runner, so a transient blip (observed: `OpenSSL ... unexpected eof` fetching the tauri-codegen index entry) could flake it — unlike the rest of this repo's network steps, which already retry. Wrap it to match that pattern: CARGO_NET_RETRY=10 plus an outer retry that rides out a transient registry error, while still failing FAST on a real drift (the `--locked` refusal, which no retry can fix) with a clear relock hint. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb49db6..2023488 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,8 +33,32 @@ jobs: # version bumped without relocking). --locked makes cargo refuse to update # the lock, so a stale lock errors here instead of surfacing later as a # `cargo fetch --locked` abort in the packaging build-arch/build-gui jobs. + # + # Resolving touches the registry index on a cold runner, so a transient + # crates.io blip (SSL eof, HTTP2 reset) could flake this check. Retry those, + # but fail FAST on a genuine drift (the `--locked` error), which no retry + # can fix. - name: Verify Cargo.lock is in sync with the manifests - run: cargo metadata --format-version 1 --locked > /dev/null + env: + CARGO_NET_RETRY: "10" + CARGO_HTTP_MULTIPLEXING: "false" + run: | + tmp_err="$(mktemp)" + for i in 1 2 3; do + if cargo metadata --format-version 1 --locked >/dev/null 2>"$tmp_err"; then + rm -f "$tmp_err"; exit 0 + fi + cat "$tmp_err" >&2 + # A genuine drift (the `--locked` refusal) no retry can fix — fail fast. + if grep -q -- '--locked' "$tmp_err"; then + echo "ERROR: Cargo.lock is out of sync with the manifests — relock (e.g. cargo update -p splitway-daemon) and commit Cargo.lock" + exit 1 + fi + echo "cargo metadata failed (attempt $i/3) — likely a transient registry error; retrying in ${i}0s" >&2 + sleep "${i}0" + done + echo "ERROR: cargo metadata --locked still failing after retries (registry unreachable?)" + exit 1 # splitway-gui (eframe/egui, glow renderer) links a windowing/GL stack on # Linux; install the dev libraries so clippy/test/build compile it. macOS From d0d89562d42f2abb36d6868b526c12cc31fb34e8 Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 26 Jun 2026 09:44:02 +0300 Subject: [PATCH 25/28] fix(packaging): don't pin splitway-bin to assets that may not exist yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The source PKGBUILDs fetch the `v$pkgver` tag archive (generated the instant the tag exists), but splitway-bin fetches the release ASSET tarballs (splitway-$pkgver-linux-*.tar.gz) that packaging.yml attaches later and independently of release.yml's bump. So stamping splitway-bin at tag time could point it at not-yet (or, on a failed upload, never-) uploaded assets while check-pkgver-sync.sh still passes on tag existence alone (Codex P2). - sync-pkgver.sh: skip splitway-bin — only advance the tag-archive source PKGBUILDs at release time. - check-pkgver-sync.sh: exclude splitway-bin from the shared-version / tag check; it is allowed to lag, and tag-existence is necessary-but-not- sufficient for an asset-based fetch. splitway-bin's pkgver + real sha256sums are both knowable only after the assets are published, so they are owned together by the deferred asset-aware AUR-push automation (already referenced in its PKGBUILD's TRUST ASSUMPTION note). splitway-bin is not on AUR yet, so there is no live 404 surface today. Co-Authored-By: Claude Opus 4.8 (1M context) --- packaging/ci/check-pkgver-sync.sh | 18 +++++++++++++----- packaging/ci/sync-pkgver.sh | 27 +++++++++++++++++++-------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packaging/ci/check-pkgver-sync.sh b/packaging/ci/check-pkgver-sync.sh index b98edb4..c3052e4 100755 --- a/packaging/ci/check-pkgver-sync.sh +++ b/packaging/ci/check-pkgver-sync.sh @@ -1,23 +1,31 @@ #!/usr/bin/env bash -# Guard the AUR-pkgver invariant: the version pinned in every committed PKGBUILD -# must name a release that ACTUALLY EXISTS, because each `source=` fetches the -# `v$pkgver` git tag / release assets. release.yml's post-release auto-bump +# Guard the AUR-pkgver invariant: the version pinned in the committed SOURCE +# PKGBUILDs must name a release that ACTUALLY EXISTS, because each `source=` +# fetches the `v$pkgver` git tag archive. release.yml's post-release auto-bump # pushes splitway-daemon/Cargo.toml to the NEXT (unreleased) version, so the # daemon version is deliberately one ahead of the latest release at rest on # master — the PKGBUILDs must therefore pin a real *released tag*, never the # in-tree daemon version (the write side, sync-pkgver.sh, stamps the version # being released). Fail the build on drift so `makepkg -si` from a checkout can -# never point at a tag / assets that do not exist. +# never point at a tag that does not exist. +# +# splitway-bin is EXCLUDED: it fetches release ASSET tarballs (attached later by +# packaging.yml), so tag-existence — all this script cheaply checks — is +# necessary but not sufficient for it, and it is allowed to lag the source +# pkgver. Its asset-gated pin + sha256sums are owned by the (deferred) asset-aware +# AUR-push automation (see splitway-bin/PKGBUILD and sync-pkgver.sh). # # Needs the tags fetched (packaging.yml's meta job uses fetch-depth: 0). # # check-pkgver-sync.sh set -euo pipefail -# All three PKGBUILDs describe one project at one version, so they must agree. +# The source PKGBUILDs describe one project at one version, so they must agree. # awk (not `grep | head -1`) is SIGPIPE-free under `set -o pipefail`. pkgver="" for pb in packaging/aur/*/PKGBUILD; do + # See header: splitway-bin's pkgver is asset-gated and may lag — not checked here. + case "$pb" in */splitway-bin/PKGBUILD) continue ;; esac pv="$(awk -F= '/^pkgver=/{print $2; exit}' "$pb")" [ -n "$pv" ] || { echo "ERROR: $pb has no pkgver=" >&2; exit 1; } if [ -z "$pkgver" ]; then diff --git a/packaging/ci/sync-pkgver.sh b/packaging/ci/sync-pkgver.sh index d597b26..6eca798 100755 --- a/packaging/ci/sync-pkgver.sh +++ b/packaging/ci/sync-pkgver.sh @@ -1,13 +1,21 @@ #!/usr/bin/env bash # Write side of the AUR-pkgver invariant (the read side is check-pkgver-sync.sh). -# Stamps the daemon crate's CURRENT version into every AUR PKGBUILD's `pkgver=` -# and resets `pkgrel=1` (Arch convention on a version change). release.yml's -# bump-version job runs this BEFORE the post-release daemon bump — i.e. while -# Cargo.toml still holds the version just tagged — so the PKGBUILDs pin the -# just-released tag (the one `source=` fetches), NOT the next unreleased dev -# version the bump moves the daemon to. Without it the PKGBUILDs would stay on -# the previous release and fail later packaging runs until a human edited all -# three. +# Stamps the daemon crate's CURRENT version into the SOURCE AUR PKGBUILDs' +# `pkgver=` and resets `pkgrel=1` (Arch convention on a version change). +# release.yml's bump-version job runs this BEFORE the post-release daemon bump — +# i.e. while Cargo.toml still holds the version just tagged — so the PKGBUILDs +# pin the just-released tag (the one `source=` fetches), NOT the next unreleased +# dev version the bump moves the daemon to. Without it the PKGBUILDs would stay +# on the previous release and fail later packaging runs until a human edited them. +# +# splitway-bin is EXCLUDED on purpose. The source PKGBUILDs fetch the `v$pkgver` +# TAG ARCHIVE, which GitHub generates the instant the tag exists, so pinning them +# the moment we tag is safe. splitway-bin instead fetches the release ASSET +# tarballs (splitway-$pkgver-linux-*.tar.gz) that packaging.yml attaches LATER +# and independently — so bumping it here would point it at not-yet (or, on a +# failed upload, never-) uploaded assets while the tag already exists. Its pkgver +# and real sha256sums are stamped together by the (deferred) asset-aware AUR-push +# automation, which by necessity runs after the assets are published. # # sync-pkgver.sh set -euo pipefail @@ -19,6 +27,9 @@ ver="$(awk -F'"' '/^version/{print $2; exit}' splitway-daemon/Cargo.toml)" [ -n "$ver" ] || { echo "ERROR: could not read daemon version" >&2; exit 1; } for pb in packaging/aur/*/PKGBUILD; do + # Skip the asset-fetching prebuilt package (see header) — it must not be + # advanced until its release tarballs exist. + case "$pb" in */splitway-bin/PKGBUILD) echo "skipped $pb (asset-gated; deferred)"; continue ;; esac sed -i "s/^pkgver=.*/pkgver=$ver/" "$pb" sed -i "s/^pkgrel=.*/pkgrel=1/" "$pb" echo "synced $pb -> pkgver=$ver pkgrel=1" From d363052c85c8bed20c3a0278692e878a1d63b729 Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 26 Jun 2026 13:59:49 +0300 Subject: [PATCH 26/28] fix(packaging): add the Wayland-EGL runtime dep to the GUI package (Codex P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GUI dlopens libwayland-egl.so.1 (glutin's Wayland EGL platform binds the GL surface to the wl_surface via wl_egl_window_*), but the manifest declared only libwayland-client0 / libwayland-client — and the client lib does NOT pull the EGL platform lib. So on a minimal Wayland host the package installs and passes the `command -v` smoke tests, then fails at GL-context creation. Verified the binary dlopens it: `strings splitway-gui | grep libwayland-egl` -> "Library libwayland-egl.so could not be loaded." + libwayland-egl.so.1. - deb depends: add libwayland-egl1 - rpm requires: add libwayland-egl - Arch is already covered: the `wayland` package ships libwayland-egl.so.1 alongside libwayland-client. - Manifest comments + docs/design dep lists updated with the rationale (separate from libwayland-client0, same pattern as libegl1 vs libgl1). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/design/linux-distro-packaging.md | 28 +++++++++++++--------- splitway-gui/Cargo.toml | 34 +++++++++++++++++---------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/docs/design/linux-distro-packaging.md b/docs/design/linux-distro-packaging.md index 9a1dcaf..b089a33 100644 --- a/docs/design/linux-distro-packaging.md +++ b/docs/design/linux-distro-packaging.md @@ -81,22 +81,28 @@ cargo-generate-rpm's ELF-based `auto-req` can see them — they **must be hardcoded**: - Debian: `libgl1, libegl1, libx11-6, libxcursor1, libxi6, libxrandr2, - libwayland-client0, libxkbcommon0, libxkbcommon-x11-0`, plus `libc6 (>= 2.31)` - to pin the floor. + libwayland-client0, libwayland-egl1, libxkbcommon0, libxkbcommon-x11-0`, plus + `libc6 (>= 2.31)` to pin the floor. - Fedora: `mesa-libGL, mesa-libEGL, libX11, libXcursor, libXi, libXrandr, - libwayland-client, libxkbcommon, libxkbcommon-x11` (auto-req still derives the - glibc/libgcc floor from the linked-against sonames). + libwayland-client, libwayland-egl, libxkbcommon, libxkbcommon-x11` (auto-req + still derives the glibc/libgcc floor from the linked-against sonames). - Arch: `libglvnd libxkbcommon libxkbcommon-x11 wayland libx11 libxcursor libxi - libxrandr` (`libglvnd` provides both GL and EGL). + libxrandr` (`libglvnd` provides both GL and EGL; the `wayland` package already + ships `libwayland-egl.so.1` alongside `libwayland-client`). `libegl1`/`mesa-libEGL` is listed **separately** from GL: glow/glutin creates its GL context via EGL on Wayland (GLX on X11), and the GL package does not pull EGL — -without it a Wayland-only install fails at context creation. `libxkbcommon-x11` -is likewise separate from `libxkbcommon0`: winit's `x11` feature `dlopen`s -`libxkbcommon-x11.so`, which the base xkbcommon package does not pull. Keep these -windowing-lib lists in sync with the GUI manifest `depends` / `[requires]` and -the Arch PKGBUILD `depends` (whose `depends` additionally carry the -`hicolor-icon-theme` / `desktop-file-utils` install-hook deps, out of scope here). +without it a Wayland-only install fails at context creation. `libwayland-egl1`/ +`libwayland-egl` (`libwayland-egl.so.1`) is **separate again** from +`libwayland-client0`: glutin's Wayland EGL platform binds the GL surface to the +`wl_surface` through it (`wl_egl_window_*`), and the client lib does not pull it — +without it a Wayland install passes the `command -v` smoke tests and then fails +at GL-context creation. `libxkbcommon-x11` is likewise separate from +`libxkbcommon0`: winit's `x11` feature `dlopen`s `libxkbcommon-x11.so`, which the +base xkbcommon package does not pull. Keep these windowing-lib lists in sync with +the GUI manifest `depends` / `[requires]` and the Arch PKGBUILD `depends` (whose +`depends` additionally carry the `hicolor-icon-theme` / `desktop-file-utils` +install-hook deps, out of scope here). `xdg-desktop-portal` + a backend (`-gtk`/`-wlr`/`-kde`) is `Recommends`: rfd's file dialog uses the portal here (no GTK linked) and **silently no-ops** without diff --git a/splitway-gui/Cargo.toml b/splitway-gui/Cargo.toml index a3d934a..0503eec 100644 --- a/splitway-gui/Cargo.toml +++ b/splitway-gui/Cargo.toml @@ -66,18 +66,24 @@ until you run `usermod -aG splitway `. Requires the splitway package.""" # Hardcoded eframe/glow runtime libs (Debian names) rather than cargo-deb's # `$auto`. Two reasons: (1) $auto (dpkg-shlibdeps) needs the TARGET-arch libs # present, which is fragile for the arm64 cross build; (2) winit/glow dlopen -# the windowing libs (libGL + libEGL, X11, wayland, xkbcommon + xkbcommon-x11) -# at runtime, so they are NOT in the ELF's DT_NEEDED and $auto/shlibdeps would -# MISS them entirely (verified: the rpm's ELF-based auto-req captured libc/libgcc -# but none of the GL/X11/wayland libs). libegl1 is a SEPARATE dep from libgl1 -# because glow/glutin creates its GL context via EGL on Wayland (GLX on X11), and -# libgl1 does NOT pull libegl1 — without it a Wayland-only install succeeds and -# then fails at GL-context creation. libxkbcommon-x11-0 is likewise SEPARATE from -# libxkbcommon0: winit's default `x11` feature has xkbcommon-dl dlopen -# libxkbcommon-x11.so, which an X11 session needs and libxkbcommon0 does not pull. -# `libc6 (>= 2.31)` pins the documented glibc floor (debian:bullseye / -# ubuntu:20.04). The `>=` core dep is the real IPC contract. -depends = "splitway (>= 0.0.5), libc6 (>= 2.31), libgl1, libegl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libxkbcommon0, libxkbcommon-x11-0" +# the windowing libs (libGL + libEGL, X11, wayland-client + wayland-egl, +# xkbcommon + xkbcommon-x11) at runtime, so they are NOT in the ELF's DT_NEEDED +# and $auto/shlibdeps would MISS them entirely (verified: the rpm's ELF-based +# auto-req captured libc/libgcc but none of the GL/X11/wayland libs). libegl1 is +# a SEPARATE dep from libgl1 because glow/glutin creates its GL context via EGL on +# Wayland (GLX on X11), and libgl1 does NOT pull libegl1 — without it a +# Wayland-only install succeeds and then fails at GL-context creation. +# libwayland-egl1 (libwayland-egl.so.1, wl_egl_window_*) is SEPARATE again from +# libwayland-client0: glutin's Wayland EGL platform binds the GL surface to the +# wl_surface through it, and libwayland-client0 does NOT pull it — without it a +# Wayland install likewise passes `command -v` smoke tests and then fails at +# GL-context creation (verified: the binary dlopens libwayland-egl.so.1). +# libxkbcommon-x11-0 is likewise SEPARATE from libxkbcommon0: winit's default +# `x11` feature has xkbcommon-dl dlopen libxkbcommon-x11.so, which an X11 session +# needs and libxkbcommon0 does not pull. `libc6 (>= 2.31)` pins the documented +# glibc floor (debian:bullseye / ubuntu:20.04). The `>=` core dep is the real +# IPC contract. +depends = "splitway (>= 0.0.5), libc6 (>= 2.31), libgl1, libegl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libwayland-egl1, libxkbcommon0, libxkbcommon-x11-0" # rfd uses the XDG desktop portal for its file dialog here (no GTK linked); the # dialog silently no-ops without a running portal + backend, hence Recommends. recommends = "xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-wlr | xdg-desktop-portal-kde" @@ -171,6 +177,9 @@ assets = [ # runtime libs that auto-req cannot detect (Fedora package names; mesa-libGL / # mesa-libEGL pull working GL/EGL via libglvnd — glow/glutin uses EGL on # Wayland, GLX on X11, and mesa-libGL alone does not pull the EGL loader). +# libwayland-egl (libwayland-egl.so.1) is a separate Fedora package from +# libwayland-client — glutin's Wayland EGL platform loads it for wl_egl_window, +# and libwayland-client does not pull it. # libxkbcommon-x11 is a separate Fedora package from libxkbcommon — winit's x11 # feature dlopens libxkbcommon-x11.so. rfd's file dialog needs an XDG portal + # backend (Recommends below). @@ -183,6 +192,7 @@ libXcursor = "*" libXi = "*" libXrandr = "*" libwayland-client = "*" +libwayland-egl = "*" libxkbcommon = "*" libxkbcommon-x11 = "*" From f86729ea023bc70e5820ca46f5175a3661653aa3 Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 26 Jun 2026 20:24:17 +0300 Subject: [PATCH 27/28] fix(packaging): depend on the group-creation tool for the opt-in group (Codex P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GUI maintainer scripts run groupadd/groupdel to create and remove the opt-in `splitway` socket group, but neither package declared the provider: groupadd is in `shadow-utils` (Fedora/RHEL) and `passwd` (Debian). On a minimal image lacking it: - rpm: %post fails NON-fatally, so the install "succeeds" but leaves the socket-group drop-in with no group — the daemon then exits on `--socket-group splitway` and users can't opt in (Codex P2). - deb: the postinst `exit 1`s, so the install fails loudly instead. Declare the dep on both for parity (present on normal systems, but now guaranteed on minimal ones): - rpm `[requires]`: add `shadow-utils` - deb `depends`: add `passwd` Manifest comments + the design-doc socket-group note updated. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/design/linux-distro-packaging.md | 5 ++++- splitway-gui/Cargo.toml | 13 +++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/design/linux-distro-packaging.md b/docs/design/linux-distro-packaging.md index b089a33..02fcdbf 100644 --- a/docs/design/linux-distro-packaging.md +++ b/docs/design/linux-distro-packaging.md @@ -48,7 +48,10 @@ author's iteration channel. `splitway` group and installs a service drop-in enabling `--socket-group`. Empty group ⇒ posture identical to `0600 root` (a no-op). The only grant is a human running `usermod -aG splitway $USER` + re-login. Maintainer scripts - **never** add a user. Mirrors `nix/tests/socket-group.nix`. + **never** add a user. Mirrors `nix/tests/socket-group.nix`. The scriptlets use + `groupadd`/`groupdel`, so the GUI package depends on their provider — `passwd` + (deb) / `shadow-utils` (rpm) — which a minimal image may lack; without it the + group is never created and the daemon would fail on `--socket-group splitway`. ## Channel & version topology diff --git a/splitway-gui/Cargo.toml b/splitway-gui/Cargo.toml index 0503eec..80048ce 100644 --- a/splitway-gui/Cargo.toml +++ b/splitway-gui/Cargo.toml @@ -82,8 +82,11 @@ until you run `usermod -aG splitway `. Requires the splitway package.""" # `x11` feature has xkbcommon-dl dlopen libxkbcommon-x11.so, which an X11 session # needs and libxkbcommon0 does not pull. `libc6 (>= 2.31)` pins the documented # glibc floor (debian:bullseye / ubuntu:20.04). The `>=` core dep is the real -# IPC contract. -depends = "splitway (>= 0.0.5), libc6 (>= 2.31), libgl1, libegl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libwayland-egl1, libxkbcommon0, libxkbcommon-x11-0" +# IPC contract. `passwd` provides groupadd/groupdel, which the maintainer scripts +# use to create/remove the opt-in `splitway` group (shadow-utils on rpm) — present +# on normal systems but declared so a minimal image's postinst can't fail and +# leave the socket-group drop-in with no group. +depends = "splitway (>= 0.0.5), libc6 (>= 2.31), libgl1, libegl1, libx11-6, libxcursor1, libxi6, libxrandr2, libwayland-client0, libwayland-egl1, libxkbcommon0, libxkbcommon-x11-0, passwd" # rfd uses the XDG desktop portal for its file dialog here (no GTK linked); the # dialog silently no-ops without a running portal + backend, hence Recommends. recommends = "xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-wlr | xdg-desktop-portal-kde" @@ -195,6 +198,12 @@ libwayland-client = "*" libwayland-egl = "*" libxkbcommon = "*" libxkbcommon-x11 = "*" +# shadow-utils provides groupadd/groupdel, which the %post/%postun scriptlets use +# to create/remove the opt-in `splitway` group. It is present on normal +# Fedora/RHEL but NOT guaranteed on minimal images — without it %post fails +# (non-fatally), leaving the socket-group drop-in but no group, so the daemon +# then exits on `--socket-group splitway`. (deb parity: the `passwd` Depends.) +shadow-utils = "*" [package.metadata.generate-rpm.recommends] xdg-desktop-portal = "*" From 469d01e811581828928a55b947110a7a096c7bf6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Jun 2026 18:15:11 +0000 Subject: [PATCH 28/28] chore: pin AUR pkgver to release, bump daemon for next cycle [skip ci] --- Cargo.lock | 2 +- packaging/aur/splitway-gui/PKGBUILD | 2 +- packaging/aur/splitway/PKGBUILD | 2 +- splitway-daemon/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1fed0b..2459074 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -4735,7 +4735,7 @@ dependencies = [ [[package]] name = "splitway-daemon" -version = "0.0.6" +version = "0.0.7" dependencies = [ "core-foundation 0.9.4", "env_logger", diff --git a/packaging/aur/splitway-gui/PKGBUILD b/packaging/aur/splitway-gui/PKGBUILD index 2177ca3..c2fab62 100644 --- a/packaging/aur/splitway-gui/PKGBUILD +++ b/packaging/aur/splitway-gui/PKGBUILD @@ -6,7 +6,7 @@ # the daemon without sudo; the group starts EMPTY, so the default posture is # unchanged until you run `usermod -aG splitway `. pkgname=splitway-gui -pkgver=0.0.5 +pkgver=0.0.6 pkgrel=1 pkgdesc="Splitway desktop GUI (egui front-end over the daemon socket)" arch=('x86_64' 'aarch64') diff --git a/packaging/aur/splitway/PKGBUILD b/packaging/aur/splitway/PKGBUILD index 585f5bf..3d89acf 100644 --- a/packaging/aur/splitway/PKGBUILD +++ b/packaging/aur/splitway/PKGBUILD @@ -6,7 +6,7 @@ # signed pacman repo (https://stslex.github.io/splitway/arch) ships a prebuilt # copy of this same package. pkgname=splitway -pkgver=0.0.5 +pkgver=0.0.6 pkgrel=1 pkgdesc="Domain-based split-DNS daemon + CLI: route selected domains through the VPN's DNS, everything else direct" arch=('x86_64' 'aarch64') diff --git a/splitway-daemon/Cargo.toml b/splitway-daemon/Cargo.toml index b8d4e27..8786ea3 100755 --- a/splitway-daemon/Cargo.toml +++ b/splitway-daemon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "splitway-daemon" -version = "0.0.6" +version = "0.0.7" edition = "2021" license = "MIT" description = "Domain-based split-DNS daemon: route selected domains through the VPN's DNS, everything else direct"