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

commit85fd2f9eb221bc8df2fec9c9aed818a39b0e531c
parentabc0bbcc70
authorLucas Galante <[email protected]>
date2026-08-13 21:52
feat: ccebuild package filter; Makefile delegates to it

`ccebuild install [PKG...]` now accepts package names, so a per-crate
`make install` installs exactly that crate's bins and units rather than the
whole workspace. Unknown names are rejected up front instead of silently
installing nothing. Helper scripts install when cce-fx is in scope, so the
compositor's recipe collapses to one line and still produces a superset of what
it did before: cce-fx, ccectl, the cce->cce-fx symlink, all four scripts
(ccebuild included) and gpu-watcher.service.

The compositor invokes ./scripts/ccebuild from the repo rather than PATH — it
is what installs ccebuild, so it cannot depend on it already being installed.

Verified: cce-ui installs 3 bins (was 2, missing cce-bevel), cce-display-manager
4 (was 1, missing the keyring helpers); a full install still lands 36 files and
status reports 32 up to date, 0 stale, 0 missing.

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

 Makefile         | 23 ++++-----------
 scripts/ccebuild | 88 ++++++++++++++++++++++++++++++++++++++++++++++----------
 2 files changed, 78 insertions(+), 33 deletions(-)

diff --git a/Makefile b/Makefile
index 5dbd205..0a67e67 100644
--- a/Makefile
+++ b/Makefile
@@ -3,25 +3,12 @@
 build:
 	cargo build --release
 
+# Binaries, helper scripts and user units are enumerated by ccebuild from
+# cargo metadata, so this crate's extra [[bin]] targets are picked up without
+# being named here — hand-listing them is what left cce-bevel and the keyring
+# helpers uninstalled for weeks.
 install: build
-	mkdir -p ~/.local/bin
-	@if [ -f ../target/release/cce-fx ]; then \
-		install -m 755 ../target/release/cce-fx ~/.local/bin/cce-fx; \
-		ln -sf cce-fx ~/.local/bin/cce; \
-	else \
-		echo "Error: cce-fx binary not found"; exit 1; \
-	fi
-	@if [ -f ../target/release/ccectl ]; then \
-		install -m 755 ../target/release/ccectl ~/.local/bin/ccectl; \
-	else \
-		echo "Error: ccectl binary not found"; exit 1; \
-	fi
-	install -m 755 scripts/cce-desktop-menu ~/.local/bin/cce-desktop-menu
-	install -m 755 scripts/cce-app-menu ~/.local/bin/cce-app-menu
-	install -m 755 scripts/gpu-watcher ~/.local/bin/gpu-watcher
-	install -m 755 scripts/ccebuild ~/.local/bin/ccebuild
-	mkdir -p ~/.config/systemd/user
-	install -m 644 scripts/gpu-watcher.service ~/.config/systemd/user/gpu-watcher.service
+	./scripts/ccebuild install --no-build cce-fx
 
 run:
 	cargo run --bin cce-fx
diff --git a/scripts/ccebuild b/scripts/ccebuild
index 6fc8563..651055d 100755
--- a/scripts/ccebuild
+++ b/scripts/ccebuild
@@ -41,10 +41,18 @@ resolve_workspace() {
 }
 
 # Every bin target cargo knows about, minus EXCLUDE. This is the whole point of
-# the script: one authoritative list, not 20 hand-written ones.
+# the script: one authoritative list, not 20 hand-written ones. With package
+# names as arguments, only those packages' bins — what a per-crate `make install`
+# needs.
 workspace_bins() {
+    local filter='.packages[]'
+    if [ "$#" -gt 0 ]; then
+        local json
+        json=$(printf '%s\n' "$@" | jq -R . | jq -sc .)
+        filter=".packages[] | select(.name as \$n | $json | index(\$n))"
+    fi
     cargo metadata --manifest-path "$WS/Cargo.toml" --no-deps --format-version 1 2>/dev/null \
-        | jq -r '.packages[].targets[] | select(.kind | index("bin")) | .name' \
+        | jq -r "$filter | .targets[] | select(.kind | index(\"bin\")) | .name" \
         | sort -u \
         | while read -r bin; do
             case " ${EXCLUDE[*]} " in *" $bin "*) continue ;; esac
@@ -52,6 +60,16 @@ workspace_bins() {
           done
 }
 
+# Guard against a typo'd package name silently installing nothing.
+assert_packages() {
+    local known name
+    known=$(cargo metadata --manifest-path "$WS/Cargo.toml" --no-deps --format-version 1 2>/dev/null \
+            | jq -r '.packages[].name')
+    for name in "$@"; do
+        printf '%s\n' "$known" | grep -qxF "$name" || die "unknown package '$name'"
+    done
+}
+
 # User-session unit files shipped by crates (system units excluded — see above).
 user_units() {
     find "$WS" -maxdepth 3 -name '*.service' -not -path "$WS/target/*" 2>/dev/null \
@@ -72,8 +90,23 @@ cmd_build() {
     ( cd "$WS" && cargo build --release --workspace )
 }
 
+# install [--no-build] [PKG...] — no packages means the whole workspace; named
+# packages install only their own bins and units, which is what the per-crate
+# Makefile wrappers call.
 cmd_install() {
-    [ "${1:-}" = --no-build ] || cmd_build
+    local build=1
+    [ "${1:-}" = --no-build ] && { build=0; shift; }
+    local pkgs=("$@")
+    [ ${#pkgs[@]} -gt 0 ] && assert_packages "${pkgs[@]}"
+
+    if [ "$build" -eq 1 ]; then
+        if [ ${#pkgs[@]} -gt 0 ]; then
+            printf '==> building %s (release)\n' "${pkgs[*]}"
+            ( cd "$WS" && cargo build --release "${pkgs[@]/#/-p}" )
+        else
+            cmd_build
+        fi
+    fi
 
     mkdir -p "$BINDIR" "$UNITDIR"
     local n=0 missing=()
@@ -88,29 +121,41 @@ cmd_install() {
         # currently running is safe (the live process keeps its inode).
         install -m 755 "$WS/target/release/$bin" "$BINDIR/$bin"
         n=$((n + 1))
-    done < <(workspace_bins)
+    done < <(workspace_bins "${pkgs[@]}")
 
     # The compositor is installed as cce-fx and invoked as `cce`.
     if [ -f "$BINDIR/cce-fx" ]; then
         ln -sf cce-fx "$BINDIR/cce"
     fi
 
-    printf '==> installing helper scripts\n'
-    local s
-    for s in "$WS"/cce-compositor/scripts/*; do
-        [ -f "$s" ] || continue
-        case "$s" in *.service) continue ;; esac
-        install -m 755 "$s" "$BINDIR/$(basename "$s")"
-        n=$((n + 1))
-    done
+    # Helper scripts live in cce-compositor; only install them when that crate is
+    # in scope (i.e. a full install, or an explicit cce-fx install).
+    if [ ${#pkgs[@]} -eq 0 ] || [[ " ${pkgs[*]} " == *" cce-fx "* ]]; then
+        printf '==> installing helper scripts\n'
+        local s
+        for s in "$WS"/cce-compositor/scripts/*; do
+            [ -f "$s" ] || continue
+            case "$s" in *.service) continue ;; esac
+            install -m 755 "$s" "$BINDIR/$(basename "$s")"
+            n=$((n + 1))
+        done
+    fi
 
-    printf '==> installing user units -> %s\n' "$UNITDIR"
-    local u
+    local u units=0
     while read -r u; do
         [ -n "$u" ] || continue
+        # Filtered install: only units belonging to a selected package's crate.
+        if [ ${#pkgs[@]} -gt 0 ]; then
+            local dir
+            dir=$(basename "$(dirname "$u")")
+            [ "$dir" = scripts ] && dir=$(basename "$(dirname "$(dirname "$u")")")
+            crate_selected "$dir" "${pkgs[@]}" || continue
+        fi
+        [ "$units" -eq 0 ] && printf '==> installing user units -> %s\n' "$UNITDIR"
         install -m 644 "$u" "$UNITDIR/$(basename "$u")"
+        units=$((units + 1))
     done < <(user_units)
-    systemctl --user daemon-reload 2>/dev/null || true
+    [ "$units" -gt 0 ] && { systemctl --user daemon-reload 2>/dev/null || true; }
 
     if [ ${#missing[@]} -gt 0 ]; then
         printf 'ccebuild: NOT BUILT, skipped: %s\n' "${missing[*]}" >&2
@@ -118,6 +163,19 @@ cmd_install() {
     printf '==> installed %d files\n' "$n"
 }
 
+# Is directory name $1 the crate dir of any of the packages in $2..? Package name
+# and directory usually match, but not always (cce-fx lives in cce-compositor).
+crate_selected() {
+    local dir=$1; shift
+    local pkg manifest
+    for pkg in "$@"; do
+        manifest=$(cargo metadata --manifest-path "$WS/Cargo.toml" --no-deps --format-version 1 2>/dev/null \
+                   | jq -r --arg p "$pkg" '.packages[] | select(.name == $p) | .manifest_path')
+        [ "$(basename "$(dirname "$manifest")")" = "$dir" ] && return 0
+    done
+    return 1
+}
+
 # Report drift three ways: built-vs-installed, and installed-vs-running. The
 # last one matters because an app launched straight out of target/ (or from a
 # stale path) keeps running old code that no reinstall can touch.