git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitf1a2f23701d93d5633764caf09b91e203e239af8
parent91c93b3b33
authorLucas Galante <[email protected]>
date2026-09-20 21:30
ccebuild install-system: ship udev rules, and install a new root binary

Two things the Power page's plug/unplug automation needed from the root
side:

- udev rules a crate ships in its udev/ dir go to /etc/udev/rules.d, with
  a udevadm reload and a power_supply re-trigger when one changed, so a
  freshly installed rule applies the plan now rather than at the next
  replug.
- cce-power-apply joins the /usr/bin pairs list, and a pair whose
  destination is absent is now installed (with no backup) instead of
  skipped — the skip made a NEW root binary uninstallable by the one
  command meant to install it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 WORKSPACE.md     |  2 +-
 scripts/ccebuild | 39 +++++++++++++++++++++++++++++++++++----
 2 files changed, 36 insertions(+), 5 deletions(-)

diff --git a/WORKSPACE.md b/WORKSPACE.md
index 0a2fc2c..ebb6f27 100644
--- a/WORKSPACE.md
+++ b/WORKSPACE.md
@@ -92,7 +92,7 @@ ccebuild install cce-mail  # just one package (what each crate's `make install`
 ccebuild restart            # restart user services left on a replaced binary
 ccebuild status             # built-vs-installed drift, AND running-vs-installed
 ccebuild prune              # target/ artifacts of crates cargo no longer knows
-ccebuild install-system     # the root-owned binaries, units, PAM stacks (one sudo prompt; --dry-run to preview)
+ccebuild install-system     # the root-owned binaries, units, PAM stacks, udev rules (one sudo prompt; --dry-run to preview)
 ```
 
 The full deploy loop is `ccebuild install && ccebuild restart`. `ccebuild` derives
diff --git a/scripts/ccebuild b/scripts/ccebuild
index 279c473..4b9306f 100755
--- a/scripts/ccebuild
+++ b/scripts/ccebuild
@@ -194,6 +194,14 @@ pam_files() {
         -not -path "$WS/target/*" 2>/dev/null
 }
 
+# udev rules a crate ships in its udev/ dir, for /etc/udev/rules.d. Root-owned
+# like the system units, so install-system only; today that is the Power
+# page's plug/unplug trigger (cce-system-interface/udev/).
+udev_rules() {
+    find "$WS" -mindepth 3 -maxdepth 3 -path '*/udev/*.rules' -type f \
+        -not -path "$WS/target/*" 2>/dev/null
+}
+
 # Helper scripts a crate ships in its own scripts/ dir. Not just this crate's:
 # a script belongs in the repo whose code it is about (cce-keyring-selftest
 # reports on the keyring chain, so it lives with it in cce-display-manager), and
@@ -663,16 +671,21 @@ cmd_install_system() {
 
     local pairs=(
         "cce-display-manager:/usr/bin/cce-display-manager"
+        # The Power page's root-side applier: the udev rule and system unit
+        # both name this path.
+        "cce-power-apply:/usr/bin/cce-power-apply"
     )
     local entry bin dest src
     for entry in "${pairs[@]}"; do
         bin=${entry%%:*}; dest=${entry#*:}
         src="$WS/target/release/$bin"
         [ -f "$src" ] || die "$bin not built — run: ccebuild build"
-        [ -e "$dest" ] || { printf 'ccebuild: %s absent, skipping\n' "$dest"; continue; }
-        if cmp -s "$src" "$dest"; then continue; fi
+        if [ -e "$dest" ] && cmp -s "$src" "$dest"; then continue; fi
         printf '  %s -> %s\n' "$bin" "$dest"
-        queue cp -a "$dest" "$dest.bak-$stamp"
+        # A first install has nothing to back up. (This used to skip an
+        # absent destination, which made a NEW root binary — cce-power-apply
+        # — uninstallable by the one command meant to install it.)
+        if [ -e "$dest" ]; then queue cp -a "$dest" "$dest.bak-$stamp"; fi
         # `install` unlinks first: safe even though these are running as root.
         queue install -m 755 "$src" "$dest"
     done
@@ -699,6 +712,24 @@ cmd_install_system() {
         queue install -m 644 "$file" "$dest"
     done < <(pam_files)
 
+    # udev rules: reload after a change, and re-trigger the power_supply
+    # class so a freshly installed plug/unplug rule applies the plan now
+    # rather than at the next replug. (A trigger is not a restart: it runs
+    # the oneshot the rule names, nothing else.)
+    local rules_changed=0
+    while read -r file; do
+        dest="/etc/udev/rules.d/$(basename "$file")"
+        if [ -e "$dest" ] && cmp -s "$file" "$dest"; then continue; fi
+        printf '  %s -> %s\n' "$(basename "$file")" "$dest"
+        if [ -e "$dest" ]; then queue cp -a "$dest" "$dest.bak-$stamp"; fi
+        queue install -m 644 "$file" "$dest"
+        rules_changed=1
+    done < <(udev_rules)
+    if [ "$rules_changed" -eq 1 ]; then
+        queue udevadm control --reload
+        queue udevadm trigger --subsystem-match=power_supply --action=change
+    fi
+
     if [ "$plan" = "set -e" ]; then
         printf '==> system artifacts already up to date\n'
         return 0
@@ -753,7 +784,7 @@ usage: ccebuild <command>
                         knows about (renamed/retired); dry-run by default
   install-system [--dry-run] [--pkexec|--sudo]
                         update the root-owned artifacts: binaries, system
-                        units, /etc/pam.d stacks — one prompt for the whole
+                        units, /etc/pam.d stacks, udev rules — one prompt for the whole
                         batch; backs up each changed file and restarts
                         nothing (--dry-run: print the root commands only).
                         Authenticates with sudo on a terminal and pkexec