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

commitd3d2687b31952b1447dc1f5b087cb3a49c64ab0d
parentcf81535ce6
authorLucas Galante <[email protected]>
date2026-08-26 20:52
ccebuild: install timers, and the services their timers activate

Two gaps a crate shipping a scheduled job walks straight into, both
found shipping cce-system-interface's restic backup:

- timers.target was missing from USER_UNIT_TARGETS, so a .timer with
  the only [Install] target a timer can sensibly have was classified
  as neither user nor system and installed nowhere.
- A timer-activated .service legitimately carries no [Install], and
  the no-[Install] fallback excludes .service outright — so the timer
  installed while the unit it triggers was silently dropped, which
  fails at 02:30 rather than at install time. The exclusion now lifts
  only when a sibling .timer/.socket/.path exists, keyed on the
  sibling so a stray service file is still skipped.

Verified no-op for every unit shipped today: the tree contains exactly
one no-[Install] .service (the new one), and nothing else declares
timers.target.

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

 scripts/ccebuild | 20 +++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/scripts/ccebuild b/scripts/ccebuild
index cac2115..a993e8f 100755
--- a/scripts/ccebuild
+++ b/scripts/ccebuild
@@ -24,7 +24,7 @@ EXCLUDE=(vk-smoke)
 # whole crate, so its user units silently never installed — the same
 # hand-maintained-list failure this script exists to avoid. Matching is exact:
 # graphical.target and graphical-session.target are different targets.
-USER_UNIT_TARGETS=(default.target graphical-session.target cce-session.target)
+USER_UNIT_TARGETS=(default.target graphical-session.target cce-session.target timers.target)
 
 PREFIX="${CCE_PREFIX:-$HOME/.local}"
 BINDIR="$PREFIX/bin"
@@ -107,7 +107,7 @@ unit_wanted_by() {
 
 # User-session unit files shipped by crates (system units excluded — see above).
 user_units() {
-    local unit wanted t match
+    local unit wanted t match base
     unit_files | while read -r unit; do
             wanted=$(unit_wanted_by "$unit")
             if [ -z "$wanted" ]; then
@@ -120,7 +120,21 @@ user_units() {
                 # .service is deliberately NOT eligible for this fallback —
                 # still requiring WantedBy there keeps the classification of
                 # every unit shipped today bit-for-bit unchanged.
-                case "$unit" in *.service) continue ;; esac
+                # .service is deliberately NOT eligible for this fallback,
+                # with ONE exception: a service activated by a sibling
+                # .timer/.socket/.path. That pair is how a scheduled or
+                # socket-activated job is expressed and the service half
+                # legitimately carries no [Install] — without this, shipping a
+                # timer installed the timer and silently dropped the unit it
+                # triggers. Keyed on the sibling so a stray service file is
+                # still skipped.
+                case "$unit" in
+                    *.service)
+                        base=${unit%.service}
+                        [ -f "$base.timer" ] || [ -f "$base.socket" ] \
+                            || [ -f "$base.path" ] || continue
+                        ;;
+                esac
                 printf '%s\n' "$unit"
                 continue
             fi