Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: ccebuild installs the .desktop entries crates ship
Desktop entries were the last piece of the DE installed by hand. All ten lived only in ~/.local/share/applications, outside version control entirely — the workspace root is not a git repo — and they had drifted the way unversioned files do: nine hardcoded Exec=/home/lsgalante/.local/bin/<bin>, and cce-test-interface pointed Icon at a path that had stopped existing. This is the same hand-maintained-list failure ccebuild already exists to avoid for binaries and units.
Crates now ship <name>.desktop at their own root, beside Cargo.toml and any unit file, and install picks them up filtered per package exactly like units, then refreshes the MIME cache. Discovery is -maxdepth 2 rather than the units' 3 because desktop entries belong at the crate root; the crate-directory logic the two loops share is factored out as file_crate_dir, which is what unwraps cce-compositor/scripts/ for the deeper unit case.
This is also what unblocks the settings app's Default Apps page: a cce app is only offered as a handler if its entry declares MimeType, so shipping these in-repo is the prerequisite for adding those declarations.
Co-Authored-By: Claude <[email protected]>
CLAUDE.md | 9 +++++++++
WORKSPACE.md | 23 +++++++++++++++++++++++
scripts/ccebuild | 48 +++++++++++++++++++++++++++++++++++++++++++-----
3 files changed, 75 insertions(+), 5 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 5e224ed..0c4c135 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -62,6 +62,15 @@ around it. When touching it, keep two invariants:
Matching is anchored: a basename must equal a dead crate name exactly, or that name
plus a hex hash.
+It also installs the **`.desktop` entries** crates ship at their own root into
+`$XDG_DATA_HOME/applications` (then `update-desktop-database`), discovered by
+`desktop_entries()` and filtered per package exactly like units. Discovery is
+`-maxdepth 2` — crate root only — so keep the file next to `Cargo.toml`; units get
+`-maxdepth 3` because `cce-compositor/scripts/` holds one, which is what the shared
+`file_crate_dir()` helper unwraps. These entries were unversioned hand-written files
+in `~/.local/share/applications` until 2026-08-16; see `./WORKSPACE.md` for the
+`Exec=`/`MimeType=` rules that go with them.
+
`ccebuild restart` deliberately cannot reach the compositor: `cce-fx` is not a user
unit (startcce launches it), and restarting it would tear down the session.
diff --git a/WORKSPACE.md b/WORKSPACE.md
index 6bc294c..04807f7 100644
--- a/WORKSPACE.md
+++ b/WORKSPACE.md
@@ -91,6 +91,29 @@ three `cce-keyring-unlock*` helpers). Each crate's `make install` is now a thin
wrapper around `ccebuild install --no-build <pkg>`; `make build/run/clean` are
unchanged. **Never add a binary name to a Makefile** — cargo already knows it.
+### Desktop entries
+
+A crate that should appear in the launcher — or be selectable as an XDG default —
+ships **`<crate>/<name>.desktop` at its own root**, next to `Cargo.toml` and beside
+any `*.service` it ships. `ccebuild install` copies those into
+`$XDG_DATA_HOME/applications` and runs `update-desktop-database`, filtered by
+package the same way units are.
+
+Two rules, both learned the hard way when these files lived only in
+`~/.local/share/applications` and were hand-edited there:
+
+- **`Exec=` is a bare binary name**, never an absolute path. `~/.local/bin` is the
+ first entry on the session PATH, and the launcher (`cce-cloud`) spawns through
+ `sh -c`, so the name resolves. Nine of the ten imported entries had baked in
+ `/home/lsgalante/.local/bin/…`.
+- **An app is only reachable as a default handler if it declares `MimeType=`.** The
+ settings app's Default Apps page builds each dropdown by scanning installed
+ entries for the ones claiming that category's MIME types, so an app with no
+ `MimeType=` line simply never appears as a candidate — which is why `cce-files`
+ could not be chosen as the file manager despite having an entry. Declaring a type
+ also means honoring it: the app has to accept the path or URL argv the field code
+ (`%f`/`%u`) passes it.
+
`ccebuild status` is the tool for "is what's running actually the code I built?".
Because `install` unlinks before writing, a process still on the old inode reports its
exe as `(deleted)`, which is how both `status` and `restart` detect drift. It also
diff --git a/scripts/ccebuild b/scripts/ccebuild
index 072d127..a2bd595 100755
--- a/scripts/ccebuild
+++ b/scripts/ccebuild
@@ -29,6 +29,7 @@ USER_UNIT_TARGETS=(default.target graphical-session.target cce-session.target)
PREFIX="${CCE_PREFIX:-$HOME/.local}"
BINDIR="$PREFIX/bin"
UNITDIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
+DESKTOPDIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
die() { printf 'ccebuild: %s\n' "$*" >&2; exit 1; }
@@ -94,6 +95,31 @@ user_units() {
done
}
+# XDG .desktop entries shipped by crates, at the crate root next to Cargo.toml.
+#
+# These used to be hand-written straight into ~/.local/share/applications, which
+# put them outside version control entirely: the workspace root is not a git
+# repo, so nothing there survives a fresh clone. They also went stale silently —
+# every one hardcoded Exec=/home/lsgalante/.local/bin/<bin>, and one pointed its
+# Icon at a path that no longer existed. A crate that ships its own entry is the
+# same rule as the units above: the file lives in the repo that owns it.
+#
+# An app is only reachable as an XDG default (mailto:, inode/directory, …) if it
+# declares a MimeType here, so this is also what makes cce apps selectable on the
+# settings app's Default Apps page.
+desktop_entries() {
+ find "$WS" -maxdepth 2 -name '*.desktop' -not -path "$WS/target/*" 2>/dev/null
+}
+
+# The crate directory a shipped data file belongs to. Units may sit one level
+# deeper (cce-compositor/scripts/gpu-watcher.service), so that case unwraps.
+file_crate_dir() {
+ local dir
+ dir=$(basename "$(dirname "$1")")
+ [ "$dir" = scripts ] && dir=$(basename "$(dirname "$(dirname "$1")")")
+ printf '%s\n' "$dir"
+}
+
cmd_build() {
printf '==> building workspace (release)\n'
# One invocation for the whole workspace. Building per-crate with -p would
@@ -120,7 +146,7 @@ cmd_install() {
fi
fi
- mkdir -p "$BINDIR" "$UNITDIR"
+ mkdir -p "$BINDIR" "$UNITDIR" "$DESKTOPDIR"
local n=0 missing=()
printf '==> installing binaries -> %s\n' "$BINDIR"
@@ -158,10 +184,7 @@ cmd_install() {
[ -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
+ crate_selected "$(file_crate_dir "$u")" "${pkgs[@]}" || continue
fi
[ "$units" -eq 0 ] && printf '==> installing user units -> %s\n' "$UNITDIR"
install -m 644 "$u" "$UNITDIR/$(basename "$u")"
@@ -169,6 +192,21 @@ cmd_install() {
done < <(user_units)
[ "$units" -gt 0 ] && { systemctl --user daemon-reload 2>/dev/null || true; }
+ local d desktops=0
+ while read -r d; do
+ [ -n "$d" ] || continue
+ # Same filtered install as the units above.
+ if [ ${#pkgs[@]} -gt 0 ]; then
+ crate_selected "$(file_crate_dir "$d")" "${pkgs[@]}" || continue
+ fi
+ [ "$desktops" -eq 0 ] && printf '==> installing desktop entries -> %s\n' "$DESKTOPDIR"
+ install -m 644 "$d" "$DESKTOPDIR/$(basename "$d")"
+ desktops=$((desktops + 1))
+ done < <(desktop_entries)
+ # Refreshes the MIME cache that makes a newly-declared MimeType resolvable.
+ # Absent on a minimal install and non-fatal there, so failure is ignored.
+ [ "$desktops" -gt 0 ] && { update-desktop-database "$DESKTOPDIR" 2>/dev/null || true; }
+
if [ ${#missing[@]} -gt 0 ]; then
printf 'ccebuild: NOT BUILT, skipped: %s\n' "${missing[*]}" >&2
fi