Skip to content

fix(deb): measure Depends: on Wayland and check the closure there (#325) - #327

Merged
charliek merged 2 commits into
mainfrom
feature/plan-024-wayland-deps
Aug 10, 2026
Merged

fix(deb): measure Depends: on Wayland and check the closure there (#325)#327
charliek merged 2 commits into
mainfrom
feature/plan-024-wayland-deps

Conversation

@charliek

@charliek charliek commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Plan 024, PR 2 of 6 — workstream W-A. Closes #325. Follows #326.

The finding is bigger than the issue title

#325 says "libwayland-egl1 is missing". It is, but so is libegl1, and the two
have to land together — adding libegl1 alone would create a crash rather than prevent
one. More importantly, the measurement method the plan specified could not have found
either of them.

Seven configurations, not four

The plan pinned a {Wayland, X11} × {Vulkan ICD, absent} matrix. I ran it. All four
cells pass, and not one of them ever probes libwayland-egl.so.1
— so their union would
have reproduced today's list and left the gap wide open.

The reason is a third axis nobody had: with neither a Vulkan ICD nor libEGL, wgpu
enumerates zero adapters and iced falls back to its tiny-skia CPU compositor. The entire
GLES path — and every library it loads — is never entered. --no-install-recommends
produces exactly that state, which is why the container check has been green on this bug
since it was written.

# display Vulkan ICD libEGL libwayland-egl1 result backend
M1 Wayland present absent absent starts, stays up Vulkan (llvmpipe)
M2 Wayland absent absent absent starts, stays up none → tiny-skia
M3 X11 present absent absent starts, stays up Vulkan (llvmpipe)
M4 X11 absent absent absent starts, stays up none → tiny-skia
M5 Wayland absent present absent SIGABRT, exit 134 Gl (EGL), dies at surface configure
M6 X11 absent present absent starts, stays up Gl (EGL/X11 — needs no wayland-egl)
M7 Wayland absent present present starts, stays up Gl (EGL/Wayland)

M5 is the real bug:

wgpu error: Validation Error
  … <wgpu::api::surface::Surface>::configure
  … <iced_renderer::fallback::Compositor<iced_wgpu…, iced_tiny_skia…>>::create_surface

preceded by eight ENOENT probes for libwayland-egl.so.1. iced's fallback compositor
covers compositor creation — which is why M2/M4 degrade gracefully — but the
wl_egl_window failure surfaces later, at surface configure, where there is no
fallback. It is a hard abort, not a degraded render. M7 is the positive control: the only
difference from M5 is apt-get install libwayland-egl1.

Screenshot byte counts are a clean renderer fingerprint throughout: 23 249 B for every
GPU-backend leg, 23 146 B for the two tiny-skia legs — all non-trivial, so every "starts"
row above rendered a real frame.

Depends: delta

Added libegl1, libwayland-egl1. Removed nothing. All eleven existing entries were
re-confirmed by trace. libvulkan1 correctly stays in Recommends (measured: the app
starts, stays up and renders with no loader and no ICD). libdecor is confirmed NOT
needed
— zero hits across all seven traces; sctk-adwaita draws decorations in-process.

The closure check now runs Wayland, with the compositor outside the container

--display x11|wayland|both, defaulting to both, so ci.yml's iced-release and
release.yml's linux job pick up the Wayland leg with no workflow change.

Ubuntu's weston depends on libwayland-egl1. Installing it beside the package under
test would satisfy the very dependency being tested and the negative control would pass on
a broken package — so the compositor runs in its own container and only its socket is
shared. The package container installs nothing but the .deb. The X11 leg gets the same
treatment (Xvfb with -ac, so no xauth in the package container either), which closes
the same latent shape on that side.

Two new assertions, and the first is the one that matters

Soname resolvability via ldconfig -p — this is the detector. A launch-only check
cannot catch this bug at all: in a --no-install-recommends container the app starts and
renders fine without either library. The launch is now the backstop, not the detector.
libwayland-egl.so.1 is asserted on the Wayland leg only, because that is the truth —
which is what makes the negative control asymmetric.

Liveness. App::bootstrap binds the IPC socket before iced creates the window, so a
renderer/EGL failure that kills the process a moment later can still answer one identify
inside the poll window. The app must now still be alive and answering after a further 8 s,
and exit with an expected status. Proven non-vacuous with a wrapper binary that binds and
then dies — the check reports answered identify and then exited (status 9).

Verification (Linux VM, aarch64, real nfpm build from this branch)

check result
both legs against the fixed package pass, 54 s total
negative control — libwayland-egl1 stripped, libegl1 kept — Wayland leg fails: ::MISSING:: libwayland-egl.so.1, exit 3
same stripped package, X11 leg passes, exit 0
harness contamination disproved: 136 packages with the dep, 135 without; diff of the sorted lists is exactly one line, libwayland-egl1:arm64. No weston/xvfb/xauth in the package container.
liveness assertion non-vacuous proven with a die-after-bind wrapper
libegl1 without a vendor implementation safe — see below

shellcheck linux/scripts/*.sh + bash -n clean (CI's exact commands).

One risk I chased down before shipping

libegl1 is the vendor-neutral loader; I expected libegl-mesa0 (the actual
implementation) to be a Recommends, which --no-install-recommends would drop — leaving
a loadable-but-dead libEGL that might reach Surface::configure and reproduce the very
SIGABRT this change prevents. The premise was wrong: on Ubuntu 24.04 libegl1
Depends on libegl-mesa0. I forced the vendor-less state anyway
(dpkg --purge --force-depends libegl-mesa0) and it is also safe — eglInitialize fails
during adapter enumeration, so wgpu reports zero adapters and iced falls back before any
surface exists. The abort needs a working EGL to get as far as surface configure.

Review findings — four correctness bugs, all found by running the harness

  • The harness could never have passed. Three shell defects: the payload's own
    apostrophes closed the -c quote and spilled half the script into the outer shell; a
    ${binary:Package} format string expanded on the wrong side and tripped set -u; and
    wait under set -e meant every successful leg exited 143 into the generic failure
    path.
  • LAUNCH_TIMEOUT wrapped apt-get as well as the launch, so a slow mirror would have
    been reported as a Depends: diagnosis. Install and launch are separate phases now.
  • The remapped inner-timeout code collided with docker's own exit 125, letting a daemon
    error masquerade as an app-launch timeout. It is 122 now; 125/126/127 report as harness
    failures.
  • The soname check was an unanchored grep with unescaped dots — libEGL.so.10 would have
    satisfied libEGL.so.1. Exact first-field match now.

Accepted risks / limits

  • Measured on aarch64. The dlopen sonames come from Rust source constants that are not
    cfg(target_arch)-gated, so the list should be arch-invariant — and this PR's CI is an
    amd64 run of the same check, which is the confirmation.
  • No seat under headless weston, so the Wayland legs can't exercise cursor loading;
    those packages are justified by the X11 legs and the binary's dlopen table.
  • llvmpipe only — a real GPU driver may dlopen more, but those arrive via
    mesa-vulkan-drivers/libgl1-mesa-dri's own Depends: and must not be pinned here.
  • One /tmp dir per leg used to leak (the compositor's socket is root-owned); cleanup now
    borrows root from a throwaway container. Verified: zero leaked dirs after a run.

No impact on

Secrets, privacy. Package size grows by the libegl1 closure, which every desktop already
has installed.

Summary by CodeRabbit

  • New Features

    • Added validation support for X11, Wayland, or both display environments.
    • Added configurable timeouts and application liveness checks during package verification.
    • Improved diagnostics for dependency, startup, environment, and container failures.
  • Bug Fixes

    • Added required graphics runtime dependencies to support GLES fallback and prevent surface-configuration failures.
    • Added explicit verification of required Wayland libraries.

The .deb's `Depends:` was measured under X11 only (plan 022 used `strace`
under `xvfb-run`) and the closure check is X11-only too, so the gap was
structurally invisible: the X11 EGL platform never calls
`wl_egl_window_create`, so no amount of X11 testing can notice a missing
Wayland library. Wayland is the primary Linux target.

## Depends: += libegl1, libwayland-egl1

Measured over SEVEN configurations, not the four the plan called for:
{Wayland, X11} x {Vulkan ICD, none} x {libEGL reachable, not}.

The third axis is the whole finding. With neither a Vulkan ICD nor
libEGL, wgpu enumerates zero adapters and iced quietly renders on the CPU
via tiny-skia — so the GLES path is never entered and
`libwayland-egl.so.1` is never even probed. All four cells of the planned
matrix pass, none of them loads it, and their union would have kept the
gap open.

The failure, once libEGL is reachable (which on a real desktop it always
is): roost does not degrade, it SIGABRTs (exit 134) inside
`Surface::configure` after eight ENOENT probes for
`libwayland-egl.so.1`. iced's fallback compositor covers compositor
*creation*, not surface configure. Installing `libwayland-egl1` into that
same container is the positive control — it comes up on the GL backend
having opened the library.

So the two entries travel together: adding `libegl1` alone would CREATE
that crash on Wayland rather than prevent it. Nothing was removed; all
eleven existing entries were re-confirmed by trace. `libvulkan1` stays in
Recommends — measured, the app starts, stays up and renders a real frame
with no loader and no ICD. `libdecor` is confirmed NOT needed (zero hits
in all seven traces): sctk-adwaita draws decorations in-process.

## The closure check runs both display servers, compositor OUT of container

`--display x11|wayland|both`, defaulting to both, so the existing CI and
release callers pick up the Wayland leg with no workflow change.

Ubuntu's `weston` itself depends on `libwayland-egl1`, so installing it
beside the package under test would satisfy the very dependency under
test and the negative control would pass on a broken package. The
compositor therefore runs in its own container and only its socket is
shared; the package container installs NOTHING but the .deb. The X11 leg
gets the same treatment — Xvfb with `-ac` so no `xauth` is needed either.

Two assertions were added, and the first is the load-bearing one:

* every dlopened soname must resolve via `ldconfig -p`. This is the
  detector. A launch-only check cannot catch this bug at all, because in
  a `--no-install-recommends` container the app starts and renders
  without either library.
* liveness: the app must still be alive and answering `identify` after a
  further 8s, and exit with an expected status. `App::bootstrap` binds
  the IPC socket before iced creates the window, so a single `identify`
  only proves bootstrap got that far.

Verified in a Linux VM (aarch64): both legs pass in 54s; the negative
control — `libwayland-egl1` stripped from `Depends:`, `libegl1` kept —
reds the Wayland leg with `::MISSING:: libwayland-egl.so.1` and leaves
X11 green, which is the asymmetry the X11-only check could not express.
The `dpkg` state proves it: 136 packages with the dependency, 135
without, `diff` of the sorted lists exactly one line.

## Review findings

Fixed during review (four correctness bugs in the harness, all found by
running it rather than reading it):

* three shell-quoting/`set -e` defects meant the harness could never pass
  — the payload's own apostrophes closed the `-c` quote, a `${binary:Package}`
  format string expanded on the wrong side, and `wait` under `set -e`
  exited 143 on every successful shutdown.
* `LAUNCH_TIMEOUT` wrapped `apt-get` as well as the launch, so a slow
  mirror would have been reported as a Depends: diagnosis. Install and
  launch are now separate phases.
* the remapped inner-timeout code collided with docker's own exit 125,
  letting a daemon error masquerade as an app-launch timeout. It is 122
  now, and 125/126/127 are reported as harness failures.
* the soname check was an unanchored `grep` with unescaped dots, so
  `libEGL.so.10` would have satisfied `libEGL.so.1`. It is an exact
  first-field match now.

Known limit: measured on aarch64. The dlopen sonames come from Rust
source constants that are not `cfg(target_arch)`-gated, so the list
should be arch-invariant, but a confirming amd64 run is cheap and CI will
do it on this PR.

Closes #325.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgxiEBQTqgNjPswKqcs12d
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2de15520-3f1f-425c-9a1b-ef7754740e1b

📥 Commits

Reviewing files that changed from the base of the PR and between 3967655 and 3dc5f3a.

📒 Files selected for processing (2)
  • linux/scripts/verify-deb-closure.sh
  • packaging/nfpm.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • packaging/nfpm.yaml
  • linux/scripts/verify-deb-closure.sh

📝 Walkthrough

Walkthrough

The Debian closure check now supports X11 and Wayland validation. It starts isolated compositors, checks display-specific shared libraries, verifies application liveness, applies bounded timeouts, and reports distinct failure classes. Package metadata adds libegl1 and libwayland-egl1.

Changes

Debian closure validation

Layer / File(s) Summary
Display selection and compositor setup
linux/scripts/verify-deb-closure.sh:3, linux/scripts/verify-deb-closure.sh:14-151, linux/scripts/verify-deb-closure.sh:153-210
The script accepts --display x11|wayland|both, configures image and timeout variables, installs cleanup handling, and starts Weston or Xvfb with bounded readiness checks.
Dependency and liveness validation
linux/scripts/verify-deb-closure.sh:221-319, packaging/nfpm.yaml:36-43, packaging/nfpm.yaml:69-81
The package container checks exact shared-library sonames and adds a Wayland libwayland-egl.so.1 check. The launch phase verifies startup and continued responsiveness. Package metadata adds libegl1 and libwayland-egl1.
Per-display execution and result handling
linux/scripts/verify-deb-closure.sh:321-420
The script runs each display leg with independent environments, budgets, cleanup, diagnostics, and sequential X11/Wayland execution.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant verify-deb-closure.sh
  participant Display_container
  participant Package_container
  participant Debian_application

  verify-deb-closure.sh->>Display_container: Start Weston or Xvfb
  Display_container-->>verify-deb-closure.sh: Report display readiness
  verify-deb-closure.sh->>Package_container: Install .deb and inspect sonames
  Package_container-->>verify-deb-closure.sh: Return dependency result
  verify-deb-closure.sh->>Debian_application: Launch with display environment
  Debian_application-->>verify-deb-closure.sh: Remain alive and responsive
Loading

Possibly related PRs

  • charliek/roost#317: Introduced the Debian closure script that this pull request extends.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the Wayland dependency measurement and closure-check update.
Linked Issues check ✅ Passed The PR adds Wayland dependencies, measures runtime requirements, and adds Wayland closure validation as required by [#325].
Out of Scope Changes check ✅ Passed The changes stay within [#325] by updating Debian dependencies and improving X11/Wayland closure verification.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/plan-024-wayland-deps

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
linux/scripts/verify-deb-closure.sh (1)

139-181: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

COMPOSITOR_TIMEOUT is spent twice per leg.

linux/scripts/verify-deb-closure.sh:139 gives the full COMPOSITOR_TIMEOUT to docker run -d (image pull), and linux/scripts/verify-deb-closure.sh:172 then starts a fresh readiness loop of the same duration. Worst case per leg is 600s at the default, and 1200s for --display both. The bound holds, so this is not a hang, but the single variable name reads like a single budget. Consider deriving the readiness deadline from the remaining time, or renaming to make the two-phase budget explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@linux/scripts/verify-deb-closure.sh` around lines 139 - 181, Make the
compositor startup budget explicit in the flow around docker run and the
readiness loop: avoid presenting COMPOSITOR_TIMEOUT as two independent
full-duration phases. Either track a shared deadline and have the socket
readiness loop use only the remaining time after docker run, or rename/configure
the variables to clearly represent separate pull and readiness budgets while
preserving the intended total bound.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@linux/scripts/verify-deb-closure.sh`:
- Around line 259-275: Add an early validation near the LAUNCH_TIMEOUT and
ROOST_CLOSURE_LIVENESS_SECONDS definitions that rejects configurations where the
liveness delay can consume the launch timeout’s available headroom, using the
existing timeout values and a clear failure message. Ensure the harness exits
before launching rather than allowing the launch phase to be misreported as a
timeout.
- Around line 326-354: Update cleanup to remove the deterministic package
container named "${run_id}-${leg}-package" in addition to compositor_cid. Ensure
this cleanup runs after DOCKER_TIMEOUT interrupts the docker client so the
daemon-side container and its share_dir bind mount are reaped.

In `@packaging/nfpm.yaml`:
- Around line 36-43: Update the measurement comment’s “first six axes” wording
to refer to the first two axes, and remove the obsolete lead-in on that line
while preserving the separate libEGL-axis explanation.

---

Nitpick comments:
In `@linux/scripts/verify-deb-closure.sh`:
- Around line 139-181: Make the compositor startup budget explicit in the flow
around docker run and the readiness loop: avoid presenting COMPOSITOR_TIMEOUT as
two independent full-duration phases. Either track a shared deadline and have
the socket readiness loop use only the remaining time after docker run, or
rename/configure the variables to clearly represent separate pull and readiness
budgets while preserving the intended total bound.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a24736a7-774b-45e5-9f25-20ca1762b7e5

📥 Commits

Reviewing files that changed from the base of the PR and between 6b6be00 and 3967655.

📒 Files selected for processing (2)
  • linux/scripts/verify-deb-closure.sh
  • packaging/nfpm.yaml

Comment thread linux/scripts/verify-deb-closure.sh
Comment thread linux/scripts/verify-deb-closure.sh
Comment thread packaging/nfpm.yaml Outdated
* `ROOST_CLOSURE_LIVENESS_SECONDS` is spent inside the phase bounded by
  `LAUNCH_TIMEOUT`, and the two are independent knobs. Raising the first
  past the second made the launch phase time out and report "the package
  did not come up" — the opposite of what happened. The combination is
  now rejected up front.
* `timeout` around `docker run` kills the docker CLIENT; the daemon keeps
  the container and `--rm` never fires. The orphan outlived the script
  AND kept the shared directory bind-mounted, so the directory cleanup
  silently failed too. `cleanup` now removes it by its deterministic name.
* The nfpm comment said "the first six axes" for a three-axis,
  eight-cell matrix.

Chasing the second one surfaced a third leak the reviewer could not have
seen from the diff: **Xvfb chowns its socket directory to root and sets
the sticky bit**, so after the X11 leg the host cannot even rmdir the
emptied directory. The shared path is now two levels — containers mount
only the inner one, so the outer stays ours and is removable once a
throwaway container has emptied it as root.

Verified in the Linux VM: both legs still pass, and a run now leaves zero
temp directories and zero containers behind (previously one directory per
X11 leg).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgxiEBQTqgNjPswKqcs12d
@charliek
charliek merged commit 62d4174 into main Aug 10, 2026
15 checks passed
@charliek
charliek deleted the feature/plan-024-wayland-deps branch August 10, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deb: Wayland runtime deps missing from Depends: (libwayland-egl1); closure check is X11-only so it cannot catch them

1 participant