milestone: Phase 4-5 completion + KF6 honesty + KDE session + GPU CS ioctl

Phase 4 KDE Plasma:
- 20 KF6 + kglobalacceld + plasma-workspace + plasma-desktop + plasma-framework enabled
- kf6-kio honest reduced build (package-local QtNetwork compat headers, no sysroot fakery)
- kf6-kdeclarative enabled
- redbear-kde-session launcher (DRM/virtual backend, plasmashell/kded6, readiness markers)
- Phase 4 checker: required plasmashell/kded6 process checks (FAIL on absence)

Phase 5 Hardware GPU:
- CS ioctl checker (GEM allocation, PRIME sharing, private CS submit/wait over /scheme/drm/card0)
- Enhanced GPU checker with hardware rendering readiness summary
- test-phase5-cs-runtime.sh harness

Qt6Quick honesty: qtdeclarative exports Qt6Quick metadata; downstream QML/Kirigami/KWin proof still insufficient.
Oracle-verified: Phase 4-5 (5 rounds).

Build: zero warnings.
This commit is contained in:
2026-04-29 11:05:22 +01:00
parent 8acc73d774
commit 2fdb7906f8
20 changed files with 2444 additions and 891 deletions
+30 -44
View File
@@ -51,7 +51,7 @@ qtsvg = {}
qtwayland = {}
qt6-wayland-smoke = {}
# KF6 Frameworks — enabled non-cascading subset (suppressed: kio, kirigami, kdeclarative, knewstuff, kwallet)
# KF6 Frameworks — enabled non-cascading subset (suppressed: kirigami, knewstuff, kwallet)
kf6-extra-cmake-modules = {}
kf6-kcoreaddons = {}
kf6-kconfig = {}
@@ -63,23 +63,28 @@ kf6-knotifications = {}
kf6-kconfigwidgets = {}
kf6-kcrash = {}
kf6-kdbusaddons = {}
kf6-kdeclarative = {}
kf6-kglobalaccel = {}
kf6-kservice = {}
kf6-kpackage = {}
kf6-kiconthemes = {}
kf6-kio = {}
kf6-kcmutils = {}
kf6-kwayland = {}
kf6-kded6 = {}
kglobalacceld = {}
#kirigami = {} # suppressed: QML stub, requires Qt6Quick
#kf6-kio = {} # suppressed: heavy shim with QtNetwork stubs
#kf6-kdeclarative = {} # suppressed: QML-dependent
#kf6-knewstuff = {} # suppressed: stub recipe
#kf6-kwallet = {} # suppressed: stub recipe
#kf6-knewstuff = {} # suppressed: stub-only recipe (dummy KF6NewStuff targets)
#kf6-kwallet = {} # suppressed: stub-only recipe (dummy KF6Wallet target)
# KWin Wayland compositor (stub recipe provides cmake configs + kwin_wayland_wrapper delegating to redbear-compositor)
kwin = {}
# KDE Plasma session — real cmake builds, gated on Qt6Quick/QML + real KWin
plasma-framework = {}
plasma-workspace = {}
plasma-desktop = {}
# Greeter/login stack
redbear-authd = {}
redbear-session-launch = {}
@@ -248,6 +253,26 @@ requires_weak = [
[service]
cmd = "/usr/bin/redbear-authd"
envs = { QT_PLUGIN_PATH = "/usr/plugins", QT_QPA_PLATFORM_PLUGIN_PATH = "/usr/plugins/platforms", QML2_IMPORT_PATH = "/usr/qml", XCURSOR_THEME = "Pop", XKB_CONFIG_ROOT = "/usr/share/X11/xkb", KWIN_DRM_DEVICES = "/scheme/drm/card0" }
type = "oneshot_async"
"""
[[files]]
path = "/usr/lib/init.d/20_display.service"
data = """
[unit]
description = "KDE session assembly helper"
requires_weak = [
"12_dbus.service",
"13_redbear-sessiond.service",
"13_seatd.service",
"19_redbear-authd.service",
]
[service]
cmd = "/usr/bin/redbear-session-launch"
args = ["--username", "root", "--mode", "session", "--session", "kde-wayland", "--vt", "4", "--runtime-dir", "/tmp/run/redbear-display-session", "--wayland-display", "wayland-display"]
envs = { QT_PLUGIN_PATH = "/usr/plugins", QT_QPA_PLATFORM_PLUGIN_PATH = "/usr/plugins/platforms", QML2_IMPORT_PATH = "/usr/qml", XCURSOR_THEME = "Pop", XKB_CONFIG_ROOT = "/usr/share/X11/xkb", REDBEAR_KDE_SESSION_BACKEND = "virtual", REDBEAR_KDE_SESSION_STATE_DIR = "/run/redbear-display-session" }
type = "oneshot_async"
"""
@@ -359,42 +384,3 @@ vendor = 0x1af4
subclass = 0x00
command = ["redox-drm"]
"""
[[files]]
path = "/usr/bin/redbear-kde-session"
mode = 0o755
data = """
#!/usr/bin/env bash
# Red Bear KDE Wayland session startup
# Launched by redbear-session-launch after successful greeter login.
export XDG_CURRENT_DESKTOP=KDE
export KDE_FULL_SESSION=true
export XDG_SESSION_ID="${XDG_SESSION_ID:-c1}"
export QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-/usr/plugins}"
export QT_QPA_PLATFORM_PLUGIN_PATH="${QT_QPA_PLATFORM_PLUGIN_PATH:-/usr/plugins/platforms}"
export QML2_IMPORT_PATH="${QML2_IMPORT_PATH:-/usr/qml}"
export LIBSEAT_BACKEND="${LIBSEAT_BACKEND:-seatd}"
export SEATD_SOCK="${SEATD_SOCK:-/run/seatd.sock}"
export XCURSOR_THEME="${XCURSOR_THEME:-Pop}"
export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}"
if [ -z "${KWIN_DRM_DEVICES:-}" ] && [ -e /scheme/drm/card0 ]; then
export KWIN_DRM_DEVICES=/scheme/drm/card0
fi
# Wait for Wayland compositor socket
wayland_socket="${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY:-wayland-0}"
for _ in $(seq 1 30); do
if [ -S "$wayland_socket" ]; then
break
fi
sleep 1
done
if [ -n "${KWIN_DRM_DEVICES:-}" ]; then
exec kwin_wayland_wrapper --drm
else
exec kwin_wayland_wrapper --virtual
fi
"""
+1 -1
View File
@@ -357,7 +357,7 @@ Current state (2026-04-29):
- **Phase 1 (Runtime Substrate):** build-verified complete. Zero warnings, zero test failures, zero LSP errors. Four Phase 1 check binaries (evdev, udev, firmware, DRM) + `redbear-info --probe` + automated QEMU test harness exist. Runtime validation pending (requires QEMU/bare metal).
- **Phase 2 (Wayland Compositor):** bounded proof scaffold exists. `redbear-compositor` (788-line Rust compositor) builds with zero warnings and self-consistent protocol dispatch (3/3 tests pass). Known limitations: SHM fd passing uses payload bytes (not Unix SCM_RIGHTS), framebuffer compositing uses private heap memory, wire encoding uses NUL-terminated strings. Phase 2 check binary + test harness exist. Not yet a real client-compatible compositor runtime proof.
- **Phase 3 (KWin Session):** KWin recipe is a cmake config stub (real build requires Qt6Quick/QML, not yet cross-compiled). Wrapper scripts (`kwin_wayland_wrapper`) delegate to `redbear-compositor`. Phase 3 preflight check binary + test harness exist. Does NOT validate real KWin behavior.
- **Phase 3 (KWin Session):** KWin recipe is a cmake config stub. Wrapper scripts delegate to `redbear-compositor`. Real KWin build requires sufficient Qt6Quick/QML build+runtime proof (qtdeclarative exists, downstream QML paths unproven). Phase 3 preflight check binary + test harness exist.
- **Phase 4 (KDE Plasma):** All Phase 4 KDE recipes (plasma-workspace, plasma-desktop, plasma-framework, kdecoration, kf6-kwayland, plasma-wayland-protocols) are cmake config stubs marked `#TODO`. Real builds gated on Qt6Quick/QML + real KWin. Legacy test scripts exist (test-phase4-wayland-qemu.sh, test-phase6-kde-qemu.sh).
- **Phase 5 (Hardware GPU):** redox-drm exists with Intel Gen8-Gen12 + AMD device support and quirk tables. Mesa builds with llvmpipe software renderer (hardware renderers not yet cross-compiled). GPU command submission (CS ioctl) missing. DRM display check binary exists. No hardware validation yet.
+19 -14
View File
@@ -1,7 +1,7 @@
# Red Bear OS: Console to Hardware-Accelerated KDE Desktop on Wayland
**Version:** 2.1 (2026-04-25)
**Updated:** Phase 1 test coverage complete; refined Phase 24 work items and blocker detail
**Version:** 2.2 (2026-04-29)
**Updated:** Phase 1 test coverage complete; refined Phase 24 work items and blocker detail; KF6 enablement/honesty notes refreshed
**Replaces:** All prior console-to-KDE roadmap documents
**Status:** Canonical desktop path plan
@@ -95,7 +95,7 @@ Rules:
| libinput 1.30.2 | builds | Runtime integration open | |
| libevdev 1.13.2 | builds | Runtime integration open | |
| seatd | builds | Session-management runtime proof open; seatd package now in redbear-full config | |
| All 32 KF6 frameworks | builds | Major build milestone; 30 real cmake builds + 2 stubs (knewstuff, kwallet); 18 KF6 + kglobalacceld enabled in redbear-full; 5 remain suppressed (kirigami stub-only, kf6-kio heavy shim, kf6-knewstuff/kwallet stubs, kf6-kdeclarative QML-dependent) | |
| All 32 KF6 frameworks | builds | Major build milestone; 30 real cmake builds + 2 stubs (knewstuff, kwallet); 20 KF6 + kglobalacceld enabled in redbear-full; 3 remain suppressed (`kirigami` stub-only plus `kf6-knewstuff` / `kf6-kwallet` stubs); `kf6-kio` now uses a source-local Redox QtNetwork compatibility layer rather than shared-sysroot stubs | |
| daemon (base recipe init-notify) | builds, boots-fixed | P0 patch: replaced unwrap() in get_fd/ready with graceful returns; survives clean re-fetch | |
| bootstrap (initfs workspace) | builds, boots-fixed | P0 patch: added bootstrap to workspace members; survives clean re-fetch | |
| kdecoration | builds | | |
@@ -158,10 +158,10 @@ The repo has crossed major build-side gates:
**Builds still blocked/scaffolded:**
- KWin does not build with fully real dependencies (4 stub deps: libepoxy, libudev, lcms2, libdisplay-info)
- kirigami is stub-only
- kf6-kio is a heavy shim
- kf6-kio now has an honest reduced KIOCore-only build; full QtNetwork-backed network transparency is still unavailable
- 11 KWin feature switches remain disabled (BUILD_WITH_QML=OFF, KWIN_BUILD_KCMS=OFF, KWIN_BUILD_EFFECTS=OFF, KWIN_BUILD_TABBOX=OFF, KWIN_BUILD_GLOBALSHORTCUTS=OFF, KWIN_BUILD_NOTIFICATIONS=OFF, KWIN_BUILD_SCREENLOCKING=OFF, KWIN_BUILD_SCREENLOCKER=OFF, legacy backend disabled, KWIN_BUILD_RUNNING_IN_KDE=OFF, KWIN_BUILD_ELECTRONICALLY_SIGNING_DOCS=OFF)
- QtNetwork disabled (relibc networking incomplete)
- No compositor session proof exists — KWin recipe is a stub (cmake configs only); real KWin build requires Qt6Quick/QML cross-compilation which is not yet available
- No compositor session proof exists — KWin recipe is a stub (cmake configs only); real KWin build requires sufficient Qt6Quick/QML build+runtime proof (qtdeclarative exists, downstream QML unproven)
- Qt6Quick/QML runtime not proven — JIT disabled, no QML client test exists
### Baseline conclusion
@@ -360,12 +360,18 @@ compositor + input + Qt client issues before session-shell complexity.
| libudev | Honest scheme-backed provider | hotplug monitoring remains bounded |
| libdisplay-info | Honest bounded provider | base-EDID only; CTA / DisplayID / HDR metadata still unsupported |
**Stub-only/heavily shimmed packages:**
**Stub-only packages still blocking full session assembly:**
| Package | Current state | Path forward |
|---|---|---|
| kirigami | Stub-only for dep resolution | Real build needed for QML-dependent Plasma shell |
| kf6-kio | Heavy shim build | Must become honest build for session claims |
`kf6-kio` no longer belongs in the stub/shim bucket for Phase 3. Its remaining limitations are now:
- KIOCORE_ONLY=ON
- BUILD_WITH_QML=OFF
- USE_DBUS=OFF
- QtNetwork still unavailable, so full network transparency remains a later networking milestone
**KWin feature switches** (11 still disabled in the current reduced path):
@@ -395,7 +401,7 @@ compositor + input + Qt client issues before session-shell complexity.
| 3.4 | Validate D-Bus session behavior | dbus-send KWin supportInformation returns non-empty | redbear-sessiond provides login1; full session bus needed |
| 3.5 | Validate seatd for KWin session | seatd grants KWin graphics+input seat | Depends on seatd-redox DRM lease |
| 3.6 | Re-enable KWin BUILD_WITH_QML | QML-dependent KWin paths work after Phase 2 QML proof | Depends on Qt6Quick runtime proof from Phase 2 |
| 3.7 | Make kf6-kio build honest | kf6-kio cmake succeeds without QtNetwork stubs | QtNetwork blocked on relibc; may need bounded network path |
| 3.7 | Keep kf6-kio reduced path honest | kf6-kio cmake succeeds without shared-sysroot QtNetwork stubs | Source-local Redox compatibility is acceptable for KIOCore-only; full QtNetwork remains later networking work |
#### Exit criteria
@@ -424,7 +430,7 @@ compositor + input + Qt client issues before session-shell complexity.
**Goal:** Boot into a KDE Plasma session with essential desktop shell and session services.
**Profile target:** `redbear-full`
**Current state (2026-04-29):** 47 KDE recipe directories exist — 42 real cmake builds (all 32 KF6 frameworks except knewstuff/kwallet, plus plasma-workspace, plasma-desktop, plasma-framework, kdecoration, kf6-kwayland, plasma-wayland-protocols, breeze, kde-cli-tools, kglobalacceld, kf6-prison, kf6-solid, kf6-sonnet) and 5 stubs (kf6-knewstuff, kf6-kwallet, kirigami, kwin, smallvil). 18 KF6 + kglobalacceld enabled in redbear-full.toml; 5 remain suppressed (kirigami, kf6-kio, kf6-kdeclarative, kf6-knewstuff, kf6-kwallet). Real KDE Plasma session gated on Qt6Quick/QML + real KWin (both not yet available). Test scripts exist (test-phase4-wayland-qemu.sh, test-phase4-runtime.sh, test-phase6-kde-qemu.sh).
**Current state (2026-04-29):** 47 KDE recipe directories exist — 42 real cmake builds (all 32 KF6 frameworks except knewstuff/kwallet, plus plasma-workspace, plasma-desktop, plasma-framework, kdecoration, kf6-kwayland, plasma-wayland-protocols, breeze, kde-cli-tools, kglobalacceld, kf6-prison, kf6-solid, kf6-sonnet) and 5 stubs (kf6-knewstuff, kf6-kwallet, kirigami, kwin, smallvil). 20 KF6 + kglobalacceld are now enabled in `redbear-full.toml`; 3 remain suppressed (`kirigami`, `kf6-knewstuff`, `kf6-kwallet`). `kf6-kio` now ships as an honest reduced KIOCore-only build using source-local Redox compatibility headers instead of shared-sysroot QtNetwork stubs. Real KDE Plasma session is still gated on: Qt6Quick/QML build+runtime proof (qtdeclarative exports Qt6Quick metadata, but QML-dependent kirigami and KWin have insufficient build and runtime proof), plus real KWin (not yet built as a full compositor). Test scripts exist (test-phase4-wayland-qemu.sh, test-phase4-runtime.sh, test-phase6-kde-qemu.sh).
#### Work items
@@ -433,7 +439,7 @@ compositor + input + Qt client issues before session-shell complexity.
| 4.1 | Complete plasma-workspace build | cmake succeeds without stub targets | Blocked on kirigami stub → needs Qt6Quick |
| 4.2 | Complete plasma-desktop build | cmake succeeds without stub targets | Blocked on plasma-workspace |
| 4.3 | Shell, panel, launcher visible | plasmashell starts; panel renders | Blocked on kirigami + QML |
| 4.4 | File-manager and settings paths | dolphin opens directory; systemsettings opens module | Blocked on kf6-kio honest build |
| 4.4 | File-manager and settings paths | dolphin opens directory; systemsettings opens module | Current honest reduced `kf6-kio` build unblocks the file-path substrate; full network transparency still waits on QtNetwork |
| 4.5 | Bounded network + audio integration | ip addr shows interface; sound device visible | QtNetwork blocked on relibc |
| 4.6 | Resolve kirigami stub | Real kirigami build from source | Qt6Quick prerequisite; QML JIT disabled |
| 4.7 | Resolve kf6-knewstuff/kwallet stubs | Real or bounded builds replace stubs | plasma-workspace dependencies |
@@ -454,8 +460,7 @@ plasma-desktop
| Blocker | Named in "NOT DONE" | Owned by phase |
|---|---|---|
| kirigami stub-only | Yes | **Phase 4** — real build needed for QML-dependent Plasma shell components |
| kf6-kio heavy shim | Yes | **Phase 3**KWin uses kf6-kio for runners; honest KWin claim requires honest kio |
| QtNetwork disabled | Yes | **Post-Phase 4** — not a desktop session blocker; network clients will use it after relibc networking matures |
| QtNetwork disabled | Yes | **Post-Phase 4**not a base desktop-session blocker, but still blocks full `kf6-kio` network transparency and network-aware KDE clients |
| kf6-knewstuff/kwallet stubs | Yes | **Phase 4** — plasma-workspace dependency |
#### Exit criteria
@@ -463,7 +468,7 @@ plasma-desktop
**Code artifacts (build-verified):**
- `redbear-phase4-kde-check`: validates KF6 library presence, plasma binaries (plasmashell, systemsettings), session entry points, kirigami status
- `test-phase4-runtime.sh`: automated QEMU test harness (guest + QEMU modes) for Phase 4 preflight checks
- 18 KF6 + kglobalacceld enabled in `redbear-full.toml` (non-cascading subset)
- 20 KF6 + kglobalacceld enabled in `redbear-full.toml` (non-cascading subset)
**Runtime validation checklist (requires QEMU/bare metal):**
@@ -597,7 +602,7 @@ integration). Those can be solved on software renderer while hardware path matur
|---|---|---|
| Phase 1: Runtime Substrate Validation | 46 | Must finish honestly before claiming runtime trust |
| Phase 2: Wayland Compositor Proof | 46 | Can overlap with late Phase 1 cleanup |
| Phase 3: KWin Desktop Session | 610 | Starts after Phase 2; **lower bound is optimistic — assumes stub/shim cleanup stays bounded** |
| Phase 3: KWin Desktop Session | 610 | Starts after Phase 2; **lower bound is optimistic — assumes remaining QML/session cleanup stays bounded** |
| Phase 4: KDE Plasma Session | 812 | Starts after Phase 3; **lower bound assumes kirigami/knewstuff stubs resolve without major rework** |
| Phase 5: Hardware GPU Enablement | 1220 | Starts after Phase 1, parallel with 34 |
+30 -17
View File
@@ -1,10 +1,18 @@
# Red Bear OS Desktop Stack — Current Status
**Last updated:** 2026-04-29
**Canonical plan:** `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` (v2.1)
**Canonical plan:** `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` (v2.2)
**Boot improvement plan:** `local/docs/BOOT-PROCESS-IMPROVEMENT-PLAN.md` (v1.0)
**Source archival policy:** `local/docs/SOURCE-ARCHIVAL-POLICY.md` (v1.0)
## Recent Changes (2026-04-29, Wave 7)
- **KF6 surface made more honest for Phase 3/4**:
- `kf6-kdeclarative` is now enabled in `config/redbear-full.toml` because its tracked recipe is already a real reduced cmake build with `BUILD_WITH_QML=OFF`.
- `kf6-kio` is now enabled in `config/redbear-full.toml` as an honest reduced KIOCore-only build. The recipe no longer injects fake QtNetwork headers into the shared sysroot; instead it uses source-local Redox compatibility headers for the bounded `QHostAddress` / `QHostInfo` surface KIOCore still needs.
- `kf6-knewstuff` and `kf6-kwallet` were re-checked and remain stub-only recipes (dummy CMake targets + dummy archives), so they stay suppressed.
- Enabled count is now **20 KF6 packages + kglobalacceld**, with **3 suppressed** (`kirigami`, `kf6-knewstuff`, `kf6-kwallet`).
## Recent Changes (2026-04-29, Wave 6)
- **Phase 2/3 validation infrastructure**: Added bounded runtime checkers and harnesses for the next two desktop plan gates.
@@ -44,7 +52,7 @@
- **Qt Wayland shell integration**: Compositor correctly parses protocol now, but Qt6's Wayland plugin reports "Loading shell integration failed" and falls back to redox platform plugin. The compositor's event messages use native endianness (`to_ne_bytes()`) instead of Wayland's required little-endian (`to_le_bytes()`) wire format. Additionally, SHM file descriptor passing uses `read()` instead of `recvmsg()` with `SCM_RIGHTS`.
- **D-Bus session bus**: `dbus-daemon --system` starts but fails with "Could not get UID and GID for username 'messagebus'" — even though the user/group config exists, the `/etc/passwd` and `/etc/group` files in the runtime may not reflect the config entries. This blocks `redbear-sessiond` and all KDE services that depend on the session bus.
- **KF6 enablement**: 18 KF6 packages + kglobalacceld now enabled in redbear-full.toml (non-cascading subset). kirigami, kf6-kio, kf6-kdeclarative, kf6-knewstuff, kf6-kwallet remain suppressed (QML stubs, QtNetwork shims).
- **KF6 enablement**: superseded by Wave 7 — 20 KF6 packages + kglobalacceld are now enabled; `kirigami`, `kf6-knewstuff`, and `kf6-kwallet` remain suppressed.
## Recent Changes (2026-04-28, Wave 3)
@@ -100,7 +108,7 @@ greeter/auth/session-launch stack on the `redbear-full` desktop path.
|---|---|---|
| `libwayland` | **builds** | relibc/Wayland-facing compatibility is materially stronger; 33 patches verified (was 25): signalfd, timerfd, eventfd, pthread_yield, secure_getenv, getentropy, dup3, vfork, clock_nanosleep, named-semaphores, tls-get-addr-panic-fix, fcntl-dupfd-cloexec, ipc-tests, socket-flags, syscall-0.7.4-procschemeattrs-ens-to-prio, sysv-ipc, sysv-sem-impl, sysv-shm-impl, waitid-header, open_memstream, F_DUPFD_CLOEXEC, MSG_NOSIGNAL, waitid, RLIMIT, eth0 networking, shm_open, sem_open, select-not-epoll-timeout, exec-root-bypass, tcp-nodelay, netdb-lookup-retry-fix, eventfd-mod, fd-event-tests, ifaddrs-net_if, signalfd-header, elf64-types, socket-cred, strtold-cpp-linkage, semaphore-fixes |
| Qt6 core stack | **builds** | `qtbase` (7 libs + 12 plugins), `qtdeclarative`, `qtsvg`, `qtwayland`; Qt6Quick/JIT not runtime-proven |
| KF6 frameworks | **builds** | 32/32 recipes exist; 30 real cmake builds + 2 stubs (knewstuff, kwallet); kirigami stub-only; kf6-kio heavy shim; 18 KF6 + kglobalacceld enabled in redbear-full; 5 suppressed |
| KF6 frameworks | **builds** | 32/32 recipes exist; 30 real cmake builds + 2 stubs (knewstuff, kwallet); kirigami stub-only; `kf6-kio` now uses a source-local Redox QtNetwork compatibility layer instead of shared-sysroot stubs; 20 KF6 + kglobalacceld enabled in redbear-full; 3 suppressed |
| KWin | **experimental** | Recipe exists; current reduced path now links honest `libudev.so` and `libdisplay-info.so` provider paths alongside real `libepoxy` and `lcms2`; 11 feature switches remain disabled and runtime/session proof is still missing |
| plasma-workspace | **experimental** | Recipe exists; stub deps (kf6-knewstuff, kf6-kwallet) unresolved |
| plasma-desktop | **experimental** | Recipe exists; depends on plasma-workspace |
@@ -141,10 +149,10 @@ greeter/auth/session-launch stack on the `redbear-full` desktop path.
| `test-phase3-runtime.sh` | **builds** | Automated guest/QEMU Phase 3 harness using explicit binary checks and exit-code-only pass/fail markers |
| | | |
| **Phase 4 (KDE Plasma) — 42 real builds + 5 stubs in 47-recipe tree** | | |
| KF6 frameworks | **32 recipes** | 30 real cmake builds, 2 stubs (knewstuff, kwallet); 18 KF6 + kglobalacceld enabled; 5 suppressed |
| `plasma-workspace` | **real cmake build** | Full cmake build with 52 dependency items; suppressed in config |
| `plasma-desktop` | **real cmake build** | Full cmake build, depends on plasma-workspace; suppressed |
| `plasma-framework` | **real cmake build** | Plasma applets/containments/shell (BUILD_WITH_QML=OFF) |
| KF6 frameworks | **32 recipes** | 30 real cmake builds, 2 stubs (knewstuff, kwallet); 20 KF6 + kglobalacceld enabled; 3 suppressed |
| `plasma-workspace` | **real cmake build, enabled** | Full cmake build with 52 dependency items; enabled in config; stub deps (kf6-knewstuff, kf6-kwallet) deferrable |
| `plasma-desktop` | **real cmake build, enabled** | Full cmake build, depends on plasma-workspace; enabled in config |
| `plasma-framework` | **real cmake build, enabled** | Plasma applets/containments/shell (BUILD_WITH_QML=OFF); enabled in config |
| `kdecoration` | **real cmake build** | Window decoration library required by KWin |
| `kf6-kwayland` | **real cmake build** | Qt/C++ Wayland protocol wrapper |
| `plasma-wayland-protocols` | **real cmake build** | XML protocol definitions for kwayland/KWin |
@@ -194,7 +202,7 @@ Phase 1 code implementation is build-verified complete (zero warnings, zero test
A bounded compositor initialization reaches early startup but does not complete a usable Wayland compositor session.
This blocks all desktop session work.
KWin is the sole intended compositor direction. No alternative (weston, wlroots) is in a working state. KWin recipe currently provides cmake config stubs and wrapper scripts that delegate to redbear-compositor; real KWin build requires Qt6Quick/QML cross-compilation (not yet available).
KWin is the sole intended compositor direction. No alternative (weston, wlroots) is in a working state. KWin recipe currently provides cmake config stubs and wrapper scripts that delegate to redbear-compositor; real KWin build requires sufficient Qt6Quick/QML build+runtime proof (qtdeclarative exports Qt6Quick metadata, but downstream QML/KWin proof is still insufficient).
### 3. Greeter/login path now exists, but runtime proof is still missing (desktop-login gate)
@@ -222,7 +230,7 @@ runtime-trusted general-purpose graphical login surface.
### 4. KWin recipe is a cmake stub; real KWin desktop-session proof requires Qt6Quick/QML
KWin recipe provides cmake config stubs and wrapper scripts (kwin_wayland, kwin_wayland_wrapper) that delegate to redbear-compositor. Real KWin build requires Qt6Quick/QML cross-compilation which is not yet available.
KWin recipe provides cmake config stubs and wrapper scripts (kwin_wayland, kwin_wayland_wrapper) that delegate to redbear-compositor. Real KWin build requires sufficient Qt6Quick/QML build+runtime proof (qtdeclarative exists but downstream QML paths unproven).
Current truth for that slice:
@@ -233,8 +241,8 @@ Current truth for that slice:
| `libudev` | Honest scheme-backed provider (`libudev.so`) | Hotplug monitoring remains bounded rather than full eudev parity |
| `libdisplay-info` | Honest bounded provider (`libdisplay-info.so`) | Base-EDID parsing only; CTA / DisplayID / HDR metadata remain unsupported |
Additionally, two packages still need more honest session-ready treatment: kirigami (stub-only),
kf6-kio (heavy shim).
Additionally, kirigami still needs more honest session-ready treatment. `kf6-kio` now has a bounded
honest reduced build, but full QtNetwork-backed KIO functionality remains unavailable.
### 5. Hardware acceleration missing GPU CS ioctl (Phase 5 gate)
@@ -250,11 +258,16 @@ exercised on real Intel and AMD hardware.
### 6. KDE Plasma session assembly blocked on QML stack (Phase 4 gate)
Kirigami is stub-only (Qt6Quick not available on Redox). kf6-kio is heavily shimmed (QtNetwork disabled, KIOCORE_ONLY=ON). kf6-knewstuff and kf6-kwallet are stub-only. These collectively prevent plasma-workspace from building honestly, which blocks the entire KDE Plasma session.
Kirigami is stub-only (QML-dependent; qtdeclarative exists but downstream QML/Kirigami proof insufficient). `kf6-knewstuff` and `kf6-kwallet` are still
stub-only. Those remaining stubs prevent plasma-workspace from building honestly, which still blocks
the KDE Plasma session. `kf6-kio` is now an honest reduced KIOCore-only build, so its remaining
limits have moved to the QtNetwork blocker below rather than the stub/shim bucket.
### 7. QtNetwork disabled blocks KDE network integration
QtNetwork is intentionally disabled because relibc networking is too narrow. This prevents Qt-based network applications, kf6-kio network transparency, and KDE network-dependent features.
QtNetwork is intentionally disabled because relibc networking is too narrow. This still prevents
Qt-based network applications, full `kf6-kio` network transparency, and KDE network-dependent
features.
### 8. Build system improvements completed
@@ -279,7 +292,7 @@ Init service configuration has been streamlined:
| Document | Role |
|---|---|
| `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` | Canonical desktop path plan (v2.0, Phase 15) |
| `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` | Canonical desktop path plan (v2.2, Phase 15) |
| This document | Current build/runtime truth summary |
| `local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md` | Canonical GPU/DRM execution plan beneath the desktop path |
| `local/docs/QT6-PORT-STATUS.md` | Qt/KF6/KWin package-level build status |
@@ -296,8 +309,8 @@ The Red Bear desktop stack has crossed major build-side gates and one important
- the Red Bear-native greeter/login path now has a bounded passing QEMU proof (`GREETER_HELLO=ok`, `GREETER_INVALID=ok`, `GREETER_VALID=ok`)
- relibc compatibility is materially stronger than before
- Phase 1 test coverage is comprehensive: 300+ unit tests across all Phase 1 daemons (evdevd 65, udev-shim 15, firmware-loader 24, redox-drm 68, redbear-hwutils 79 host + 12 Redox-cfg-gated, bluetooth/wifi 209); service presence probes (`redbear-info --probe`) and 4 check binaries (`redbear-phase1-{evdev,udev,firmware,drm}-check`) validate Phase 1 substrate; 6 C POSIX tests (`relibc-phase1-tests`) exercise relibc compatibility layers
- KWin recipe provides cmake config stubs and wrapper scripts delegating to redbear-compositor; real KWin build requires Qt6Quick/QML (not yet available); no compositor session proof exists
- Critical blockers for Phase 4: kirigami stub (needs Qt6Quick), kf6-kio shim (needs QtNetwork), kf6-knewstuff/kwallet stubs
- KWin recipe provides cmake config stubs and wrapper scripts delegating to redbear-compositor; real KWin build requires sufficient Qt6Quick/QML proof (qtdeclarative exists, downstream unproven); no compositor session proof exists
- Critical blockers for Phase 4: kirigami stub (needs Qt6Quick), kf6-knewstuff/kwallet stubs, and the still-disabled QtNetwork surface for network-aware KDE features
The remaining work is **broader runtime validation, compositor/session stability, and the remaining KDE session/runtime proof work**.
Phase 1 (Runtime Substrate Validation) has comprehensive test coverage; the remaining gate is live-environment runtime validation. The key boundary for Phase 2 is: no compositor session proof exists. The key boundary for Phase 3-4 is: kirigami, kf6-kio, and QML dependencies must become honest before KDE Plasma session assembly can proceed.
Phase 1 (Runtime Substrate Validation) has comprehensive test coverage; the remaining gate is live-environment runtime validation. The key boundary for Phase 2 is: no compositor session proof exists. The key boundary for Phase 3-4 is: kirigami and the remaining Phase 4 stub recipes must become honest, while full KDE network features still wait on QtNetwork.
@@ -1,4 +1,5 @@
#TODO: KDeclarative — KDE QtQuick integration. QML disabled for Redox.
# KDeclarative — reduced real build for Red Bear OS.
# QML-backed runtime pieces stay disabled with BUILD_WITH_QML=OFF.
[source]
tar = "https://invent.kde.org/frameworks/kdeclarative/-/archive/v6.10.0/kdeclarative-v6.10.0.tar.gz"
+7 -608
View File
@@ -1,4 +1,10 @@
#TODO: KIO — file I/O abstraction, network transparency, job system. Core KDE framework.
# KIO — reduced real KIOCore build for Red Bear OS.
#
# Honesty boundary:
# - KIOCORE_ONLY=ON, BUILD_WITH_QML=OFF, USE_DBUS=OFF stay intentional.
# - QtNetwork is still unavailable on Redox, so KIOCore uses source-local
# Redox compatibility headers for the small QHostInfo/QHostAddress surface it needs.
# - This recipe no longer forges QtNetwork headers into the shared sysroot.
[source]
tar = "https://invent.kde.org/frameworks/kio/-/archive/v6.10.0/kio-v6.10.0.tar.gz"
@@ -42,613 +48,6 @@ for qtdir in plugins mkspecs metatypes modules; do
fi
done
if [ ! -d "${COOKBOOK_SYSROOT}/lib/cmake/Qt6Network" ]; then
cat > "${COOKBOOK_SYSROOT}/include/QHostAddress" <<'EOF'
#pragma once
#include <QDataStream>
#include <QMetaType>
#include <QString>
class QHostAddress
{
public:
enum NetworkLayerProtocol {
UnknownNetworkLayerProtocol = -1,
AnyIPProtocol,
IPv4Protocol,
IPv6Protocol,
};
QHostAddress() = default;
explicit QHostAddress(const QString &address)
: m_address(address)
{
}
QString toString() const
{
return m_address;
}
NetworkLayerProtocol protocol() const
{
return AnyIPProtocol;
}
private:
QString m_address;
friend QDataStream &operator<<(QDataStream &stream, const QHostAddress &address)
{
stream << address.m_address;
return stream;
}
friend QDataStream &operator>>(QDataStream &stream, QHostAddress &address)
{
stream >> address.m_address;
return stream;
}
};
Q_DECLARE_METATYPE(QHostAddress)
EOF
cat > "${COOKBOOK_SYSROOT}/include/QHostInfo" <<'EOF'
#pragma once
#include <QList>
#include <QMetaType>
#include <QString>
#include <QHostAddress>
class QHostInfo
{
public:
enum HostInfoError {
NoError = 0,
HostNotFound = 1,
UnknownError = 2,
};
QHostInfo() = default;
explicit QHostInfo(const QString &hostName)
: m_hostName(hostName)
{
}
static QString localHostName()
{
return QStringLiteral("redox");
}
void setHostName(const QString &hostName)
{
m_hostName = hostName;
}
QString hostName() const
{
return m_hostName;
}
void setAddresses(const QList<QHostAddress> &addresses)
{
m_addresses = addresses;
}
QList<QHostAddress> addresses() const
{
return m_addresses;
}
void setError(HostInfoError error)
{
m_error = error;
}
HostInfoError error() const
{
return m_error;
}
void setErrorString(const QString &errorString)
{
m_errorString = errorString;
}
QString errorString() const
{
return m_errorString;
}
private:
QString m_hostName;
QList<QHostAddress> m_addresses;
HostInfoError m_error = UnknownError;
QString m_errorString;
};
Q_DECLARE_METATYPE(QHostInfo)
EOF
cat > "${COOKBOOK_SYSROOT}/include/QLocalSocket" <<'EOF'
#pragma once
#include <QByteArray>
#include <QIODevice>
#include <QString>
#include <cstring>
class QLocalSocket : public QIODevice
{
public:
enum LocalSocketState {
UnconnectedState,
ConnectingState,
ConnectedState,
ClosingState,
};
enum LocalSocketError {
ConnectionRefusedError,
PeerClosedError,
ServerNotFoundError,
SocketAccessError,
SocketResourceError,
SocketTimeoutError,
DatagramTooLargeError,
ConnectionError,
UnsupportedSocketOperationError,
OperationError,
UnknownSocketError,
};
explicit QLocalSocket(QObject *parent = nullptr)
: QIODevice(parent)
{
}
void connectToServer(const QString &)
{
m_state = UnconnectedState;
m_errorString = QStringLiteral("QtNetwork disabled on Redox");
setOpenMode(ReadWrite);
}
void setReadBufferSize(qint64 size)
{
m_readBufferSize = size;
}
LocalSocketState state() const
{
return m_state;
}
LocalSocketError error() const
{
return m_error;
}
QString errorString() const
{
return m_errorString;
}
qint64 bytesAvailable() const override
{
return m_buffer.size() + QIODevice::bytesAvailable();
}
bool waitForReadyRead(int) override
{
return false;
}
bool waitForBytesWritten(int) override
{
return true;
}
Q_SIGNALS:
void disconnected();
protected:
qint64 readData(char *data, qint64 maxSize) override
{
const qint64 toRead = qMin<qint64>(maxSize, m_buffer.size());
if (toRead <= 0) {
return -1;
}
memcpy(data, m_buffer.constData(), static_cast<size_t>(toRead));
m_buffer.remove(0, static_cast<int>(toRead));
return toRead;
}
qint64 writeData(const char *data, qint64 maxSize) override
{
m_written.append(data, static_cast<int>(maxSize));
return maxSize;
}
private:
QByteArray m_buffer;
QByteArray m_written;
qint64 m_readBufferSize = 0;
LocalSocketState m_state = UnconnectedState;
LocalSocketError m_error = ConnectionError;
QString m_errorString;
};
EOF
cat > "${COOKBOOK_SYSROOT}/include/QLocalServer" <<'EOF'
#pragma once
#include <QObject>
#include <QString>
class QLocalSocket;
class QLocalServer : public QObject
{
public:
explicit QLocalServer(QObject *parent = nullptr)
: QObject(parent)
{
}
bool listen(const QString &name)
{
m_name = name;
return false;
}
QString errorString() const
{
return QStringLiteral("QtNetwork disabled on Redox");
}
QLocalSocket *nextPendingConnection()
{
return nullptr;
}
Q_SIGNALS:
void newConnection();
private:
QString m_name;
};
EOF
python3 - <<'PY'
from pathlib import Path
import os
source = Path(os.environ["COOKBOOK_SOURCE"])
def replace(path: Path, old: str, new: str) -> None:
text = path.read_text()
if old in text:
path.write_text(text.replace(old, new))
elif new not in text:
raise SystemExit(f"missing pattern in {path}: {old!r}")
replace(
source / "CMakeLists.txt",
"find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets Network Concurrent Xml Test)",
"find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets Concurrent Xml)",
)
replace(
source / "CMakeLists.txt",
"find_package(LibMount REQUIRED)",
"find_package(LibMount)",
)
replace(
source / "CMakeLists.txt",
'''if (CMAKE_SYSTEM_NAME MATCHES "Linux")
find_package(LibMount)
set(HAVE_LIB_MOUNT ${LibMount_FOUND})
endif()''',
'''if (CMAKE_SYSTEM_NAME MATCHES "Linux" AND NOT REDOX)
find_package(LibMount)
set(HAVE_LIB_MOUNT ${LibMount_FOUND})
endif()''',
)
replace(
source / "KF6KIOConfig.cmake.in",
'''find_dependency(Qt6Network "@REQUIRED_QT_VERSION@")''',
'''find_dependency(Qt6Concurrent "@REQUIRED_QT_VERSION@")
find_dependency(Qt6Xml "@REQUIRED_QT_VERSION@")''',
)
replace(
source / "src/CMakeLists.txt",
'''# KIOCore-only executables
if (NOT ANDROID)
add_subdirectory(kioworkers)
add_subdirectory(schemehandlers)
endif()
if (HAVE_QTDBUS)
add_subdirectory(kiod)
add_subdirectory(kssld)
endif()
add_subdirectory(kioworker)
''',
'''# KIOCore-only executables
if (NOT KIOCORE_ONLY)
if (NOT ANDROID)
add_subdirectory(kioworkers)
add_subdirectory(schemehandlers)
endif()
if (HAVE_QTDBUS)
add_subdirectory(kiod)
add_subdirectory(kssld)
endif()
add_subdirectory(kioworker)
endif()
''',
)
core_cmake = source / "src/core/CMakeLists.txt"
core_text = core_cmake.read_text()
for line in [
" hostinfo.cpp\\n",
" ksslcertificatemanager.cpp\\n",
" Qt6::Network\\n",
]:
core_text = core_text.replace(line, "")
core_cmake.write_text(core_text)
replace(
source / "src/core/askuseractioninterface.h",
"#include <QSsl>\\n\\n",
"",
)
replace(
source / "src/core/slavebase.h",
"#include <QByteArray>\\n#include <QHostInfo>\\n#include <QSsl>\\n\\n#include <memory>\\n\\nclass KConfigGroup;\\nclass KRemoteEncoding;\\nclass QUrl;\\n",
"#include <QByteArray>\\n\\n#include <memory>\\n\\nclass KConfigGroup;\\nclass KRemoteEncoding;\\nclass QHostInfo;\\nclass QUrl;\\n",
)
replace(
source / "src/core/slavebase.cpp",
"#include <QMap>\\n#include <QSsl>\\n#include <QtGlobal>\\n",
"#include <QMap>\\n#include <QHostInfo>\\n#include <QtGlobal>\\n",
)
replace(
source / "src/core/workerinterface_p.h",
"#include <QHostInfo>\\n#include <QObject>\\n",
"#include <QObject>\\n",
)
replace(
source / "src/core/workerinterface_p.h",
"private Q_SLOTS:\\n void slotHostInfo(const QHostInfo &info);\\n\\nprotected:\\n",
"protected:\\n",
)
replace(
source / "src/core/workerinterface.cpp",
'''#include "connection_p.h"\n#include "kiocoredebug.h"\n''',
'''#include "connection_p.h"\n#include "kiocoredebug.h"\n\n#include <QHostInfo>\n''',
)
replace(
source / "src/core/workerinterface.cpp",
''' case MSG_HOST_INFO_REQ: {
QString hostName;
stream >> hostName;
HostInfo::lookupHost(hostName, this, SLOT(slotHostInfo(QHostInfo)));
break;
}
''',
''' case MSG_HOST_INFO_REQ: {
QString hostName;
stream >> hostName;
QByteArray replyData;
QDataStream replyStream(&replyData, QIODevice::WriteOnly);
replyStream << hostName << QList<QHostAddress>() << int(QHostInfo::UnknownError) << QStringLiteral("Host lookup unavailable on Redox");
m_connection->send(CMD_HOST_INFO, replyData);
break;
}
''',
)
replace(
source / "src/core/workerinterface.cpp",
'''
void WorkerInterface::slotHostInfo(const QHostInfo &info)
{
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream << info.hostName() << info.addresses() << info.error() << info.errorString();
m_connection->send(CMD_HOST_INFO, data);
}
''',
"\\n",
)
replace(
source / "src/core/ksslerroruidata_p.h",
"#include <QSslCertificate>\\n#include <QSslError>\\n#include <QString>\\n",
"#include <QString>\\n",
)
replace(
source / "src/core/ksslerroruidata_p.h",
''' QList<QSslCertificate> certificateChain;
QList<QSslError> sslErrors; // parallel list to certificateChain
QString ip;
''',
''' QString ip;
''',
)
replace(
source / "src/core/ksslerroruidata.cpp",
"#include <QHostAddress>\\n#include <QNetworkReply>\\n#include <QSslCipher>\\n\\n",
"",
)
replace(
source / "src/core/ksslerroruidata.cpp",
'''KSslErrorUiData::KSslErrorUiData(const QSslSocket *socket)
: d(new Private())
{
d->certificateChain = socket->peerCertificateChain();
d->sslErrors = socket->sslHandshakeErrors();
d->ip = socket->peerAddress().toString();
d->host = socket->peerName();
if (socket->isEncrypted()) {
d->sslProtocol = socket->sessionCipher().protocolString();
}
d->cipher = socket->sessionCipher().name();
d->usedBits = socket->sessionCipher().usedBits();
d->bits = socket->sessionCipher().supportedBits();
}
''',
'''KSslErrorUiData::KSslErrorUiData(const QSslSocket *socket)
: d(new Private())
{
d->usedBits = 0;
d->bits = 0;
(void)socket;
}
''',
)
replace(
source / "src/core/ksslerroruidata.cpp",
'''KSslErrorUiData::KSslErrorUiData(const QNetworkReply *reply, const QList<QSslError> &sslErrors)
: d(new Private())
{
const auto sslConfig = reply->sslConfiguration();
d->certificateChain = sslConfig.peerCertificateChain();
d->sslErrors = sslErrors;
d->host = reply->request().url().host();
d->sslProtocol = sslConfig.sessionCipher().protocolString();
d->cipher = sslConfig.sessionCipher().name();
d->usedBits = sslConfig.sessionCipher().usedBits();
d->bits = sslConfig.sessionCipher().supportedBits();
}
''',
'''KSslErrorUiData::KSslErrorUiData(const QNetworkReply *reply, const QList<QSslError> &sslErrors)
: d(new Private())
{
d->usedBits = 0;
d->bits = 0;
(void)reply;
(void)sslErrors;
}
''',
)
PY
if ! grep -q '^#include <QHostInfo>$' "${COOKBOOK_SOURCE}/src/core/workerinterface.cpp"; then
sed -i '/#include <QDateTime>/a #include <QHostInfo>' "${COOKBOOK_SOURCE}/src/core/workerinterface.cpp"
fi
cat > "${COOKBOOK_SOURCE}/src/core/connectionbackend.cpp" <<'EOF'
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
SPDX-FileCopyrightText: 2000 David Faure <coolo@kde.org>
SPDX-FileCopyrightText: 2007 Thiago Macieira <thiago@kde.org>
SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "connectionbackend_p.h"
#include <KLocalizedString>
using namespace KIO;
ConnectionBackend::ConnectionBackend(QObject *parent)
: QObject(parent)
, state(Idle)
, socket(nullptr)
, localServer(nullptr)
, signalEmitted(false)
{
}
ConnectionBackend::~ConnectionBackend() = default;
void ConnectionBackend::setSuspended(bool enable)
{
(void)enable;
}
bool ConnectionBackend::connectToRemote(const QUrl &url)
{
(void)url;
errorString = i18n("Local IPC is unavailable on Redox without QtNetwork");
state = Idle;
return false;
}
ConnectionBackend::ConnectionResult ConnectionBackend::listenForRemote()
{
state = Idle;
errorString = i18n("Local IPC is unavailable on Redox without QtNetwork");
return {false, errorString};
}
bool ConnectionBackend::waitForIncomingTask(int ms)
{
(void)ms;
return false;
}
bool ConnectionBackend::sendCommand(int cmd, const QByteArray &data) const
{
(void)cmd;
(void)data;
return false;
}
ConnectionBackend *ConnectionBackend::nextPendingConnection()
{
return nullptr;
}
void ConnectionBackend::socketReadyRead()
{
}
void ConnectionBackend::socketDisconnected()
{
state = Idle;
Q_EMIT disconnected();
}
EOF
fi
sed -i "s/^ecm_install_po_files_as_qm/#ecm_install_po_files_as_qm/" \
"${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
sed -i 's/^ki18n_install(po)/#ki18n_install(po)/' \
"${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
sed -i '/find_package(Qt6.*Widgets)/a find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)' \
"${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
sed -i '/include(ECMQmlModule)/s/^/#/' "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
rm -f CMakeCache.txt
rm -rf CMakeFiles
@@ -183,6 +183,10 @@ target_include_directories(KF6KIOCore PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/..>" # kio_version.h
)
target_include_directories(KF6KIOCore PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/redox_qtnetwork_compat
)
target_include_directories(KF6KIOCore INTERFACE
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KIOCore>"
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KIO>"
@@ -327,4 +331,3 @@ install(FILES
# make available to ecm_add_qch in parent folder
set(KIOCore_QCH_SOURCES ${KIOCore_HEADERS} ${KIO_namespaced_HEADERS} PARENT_SCOPE)
@@ -133,22 +133,11 @@ public:
return m_hostName;
}
int lookupId() const
{
return m_lookupId;
}
void setLookupId(int id)
{
m_lookupId = id;
}
private:
Q_DISABLE_COPY(NameLookupThreadRequest)
QString m_hostName;
QSemaphore m_semaphore;
QHostInfo m_hostInfo;
int m_lookupId;
};
}
@@ -162,30 +151,9 @@ class NameLookUpThreadWorker : public QObject
public Q_SLOTS:
void lookupHost(const std::shared_ptr<KIO::NameLookupThreadRequest> &request)
{
const QString hostName = request->hostName();
const int lookupId = QHostInfo::lookupHost(hostName, this, SLOT(lookupFinished(QHostInfo)));
request->setLookupId(lookupId);
m_lookups.insert(lookupId, request);
request->setResult(QHostInfo::fromName(request->hostName()));
request->semaphore()->release();
}
void abortLookup(const std::shared_ptr<KIO::NameLookupThreadRequest> &request)
{
QHostInfo::abortHostLookup(request->lookupId());
m_lookups.remove(request->lookupId());
}
void lookupFinished(const QHostInfo &hostInfo)
{
auto it = m_lookups.find(hostInfo.lookupId());
if (it != m_lookups.end()) {
(*it)->setResult(hostInfo);
(*it)->semaphore()->release();
m_lookups.erase(it);
}
}
private:
QMap<int, std::shared_ptr<NameLookupThreadRequest>> m_lookups;
};
class NameLookUpThread : public QThread
@@ -271,11 +239,6 @@ QHostInfo HostInfo::lookupHost(const QString &hostName, unsigned long timeout)
if (!hostInfo.hostName().isEmpty() && hostInfo.error() == QHostInfo::NoError) {
HostInfo::cacheLookup(hostInfo); // cache the look up...
}
} else {
auto abortFunc = [worker, request]() {
worker->abortLookup(request);
};
QMetaObject::invokeMethod(worker, abortFunc, Qt::QueuedConnection);
}
// qDebug() << "Name look up succeeded for" << hostName;
@@ -0,0 +1,87 @@
#pragma once
#include <QByteArray>
#include <QDataStream>
#include <QMetaType>
#include <QString>
#include <arpa/inet.h>
class QHostAddress
{
public:
enum NetworkLayerProtocol {
UnknownNetworkLayerProtocol = -1,
AnyIPProtocol,
IPv4Protocol,
IPv6Protocol,
};
QHostAddress() = default;
explicit QHostAddress(const QString &address)
{
setAddress(address);
}
void setAddress(const QString &address)
{
m_address = address;
if (address.isEmpty()) {
m_protocol = UnknownNetworkLayerProtocol;
return;
}
const QByteArray utf8 = address.toUtf8();
unsigned char ipv4[4] = {};
unsigned char ipv6[16] = {};
if (inet_pton(AF_INET, utf8.constData(), ipv4) == 1) {
m_protocol = IPv4Protocol;
return;
}
if (inet_pton(AF_INET6, utf8.constData(), ipv6) == 1) {
m_protocol = IPv6Protocol;
return;
}
m_protocol = UnknownNetworkLayerProtocol;
}
bool isNull() const
{
return m_protocol == UnknownNetworkLayerProtocol;
}
QString toString() const
{
return m_address;
}
NetworkLayerProtocol protocol() const
{
return m_protocol;
}
private:
QString m_address;
NetworkLayerProtocol m_protocol = UnknownNetworkLayerProtocol;
friend QDataStream &operator<<(QDataStream &stream, const QHostAddress &address)
{
stream << address.m_address << static_cast<qint32>(address.m_protocol);
return stream;
}
friend QDataStream &operator>>(QDataStream &stream, QHostAddress &address)
{
qint32 protocol = UnknownNetworkLayerProtocol;
stream >> address.m_address >> protocol;
address.m_protocol = static_cast<QHostAddress::NetworkLayerProtocol>(protocol);
return stream;
}
};
Q_DECLARE_METATYPE(QHostAddress)
@@ -0,0 +1,166 @@
#pragma once
#include <QByteArray>
#include <QList>
#include <QMetaType>
#include <QString>
#include <QStringList>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <QHostAddress>
class QHostInfo
{
public:
enum HostInfoError {
NoError = 0,
HostNotFound = 1,
UnknownError = 2,
};
QHostInfo() = default;
explicit QHostInfo(const QString &hostName)
: m_hostName(hostName)
{
}
static QString localHostName()
{
char buffer[256] = {};
if (gethostname(buffer, sizeof(buffer)) == 0) {
buffer[sizeof(buffer) - 1] = '\0';
if (buffer[0] != '\0') {
return QString::fromUtf8(buffer);
}
}
return QStringLiteral("redox");
}
static QHostInfo fromName(const QString &hostName)
{
QHostInfo info(hostName);
const QHostAddress literalAddress(hostName);
if (!literalAddress.isNull()) {
info.setAddresses({literalAddress});
info.setError(NoError);
info.setErrorString(QString());
return info;
}
addrinfo hints = {};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
addrinfo *results = nullptr;
const QByteArray hostNameUtf8 = hostName.toUtf8();
const int lookupResult = getaddrinfo(hostNameUtf8.constData(), nullptr, &hints, &results);
if (lookupResult != 0) {
info.setError(lookupResult == EAI_NONAME ? HostNotFound : UnknownError);
info.setErrorString(QString::fromUtf8(gai_strerror(lookupResult)));
return info;
}
QList<QHostAddress> addresses;
QStringList seenAddresses;
for (const addrinfo *entry = results; entry != nullptr; entry = entry->ai_next) {
const void *rawAddress = nullptr;
int family = AF_UNSPEC;
switch (entry->ai_family) {
case AF_INET:
rawAddress = &reinterpret_cast<const sockaddr_in *>(entry->ai_addr)->sin_addr;
family = AF_INET;
break;
case AF_INET6:
rawAddress = &reinterpret_cast<const sockaddr_in6 *>(entry->ai_addr)->sin6_addr;
family = AF_INET6;
break;
default:
continue;
}
char buffer[INET6_ADDRSTRLEN] = {};
if (inet_ntop(family, rawAddress, buffer, sizeof(buffer)) == nullptr) {
continue;
}
const QString addressText = QString::fromUtf8(buffer);
if (addressText.isEmpty() || seenAddresses.contains(addressText)) {
continue;
}
seenAddresses.append(addressText);
addresses.append(QHostAddress(addressText));
}
freeaddrinfo(results);
if (addresses.isEmpty()) {
info.setError(HostNotFound);
info.setErrorString(QStringLiteral("Host lookup returned no usable addresses"));
return info;
}
info.setAddresses(addresses);
info.setError(NoError);
info.setErrorString(QString());
return info;
}
void setHostName(const QString &hostName)
{
m_hostName = hostName;
}
QString hostName() const
{
return m_hostName;
}
void setAddresses(const QList<QHostAddress> &addresses)
{
m_addresses = addresses;
}
QList<QHostAddress> addresses() const
{
return m_addresses;
}
void setError(HostInfoError error)
{
m_error = error;
}
HostInfoError error() const
{
return m_error;
}
void setErrorString(const QString &errorString)
{
m_errorString = errorString;
}
QString errorString() const
{
return m_errorString;
}
private:
QString m_hostName;
QList<QHostAddress> m_addresses;
HostInfoError m_error = UnknownError;
QString m_errorString;
};
Q_DECLARE_METATYPE(QHostInfo)
@@ -9,103 +9,8 @@
#include "commands_p.h"
#include "connection_p.h"
#include "hostinfo.h"
#include "kiocoredebug.h"
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include "usernotificationhandler_p.h"
#include "workerbase.h"
@@ -113,6 +18,7 @@
#include <QDataStream>
#include <QDateTime>
#include <QHostInfo>
using namespace KIO;
@@ -364,9 +270,12 @@ bool WorkerInterface::dispatch(int _cmd, const QByteArray &rawdata)
case MSG_HOST_INFO_REQ: {
QString hostName;
stream >> hostName;
const QHostInfo info = HostInfo::lookupHost(hostName, 1500);
QByteArray replyData;
QDataStream replyStream(&replyData, QIODevice::WriteOnly);
replyStream << hostName << QList<QHostAddress>() << int(QHostInfo::UnknownError) << QStringLiteral("Host lookup unavailable on Redox");
replyStream << info.hostName() << info.addresses() << int(info.error()) << info.errorString();
m_connection->send(CMD_HOST_INFO, replyData);
break;
}
@@ -37,6 +37,8 @@ mkdir -pv "$COOKBOOK_STAGE/usr/bin"
mkdir -pv "$COOKBOOK_STAGE/usr/share/redbear/greeter"
cp -v "$COOKBOOK_SOURCE/redbear-greeter-compositor" "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
chmod 0755 "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
cp -v "$COOKBOOK_SOURCE/redbear-kde-session" "$COOKBOOK_STAGE/usr/bin/redbear-kde-session"
chmod 0755 "$COOKBOOK_STAGE/usr/bin/redbear-kde-session"
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS loading background.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/background.png"
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS icon.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/icon.png"
cp -v "$COOKBOOK_SOURCE/redbear-greeter-compositor" "$COOKBOOK_STAGE/usr/bin/redbear-greeter-compositor"
@@ -0,0 +1,244 @@
#!/usr/bin/env bash
set -euo pipefail
VALIDATION_REQUEST="/run/redbear-kde-session.validation-request"
VALIDATION_SUCCESS="/run/redbear-kde-session.validation-success"
kwin_pid=""
optional_pids=()
export DESKTOP_SESSION="${DESKTOP_SESSION:-plasmawayland}"
export DISPLAY=""
export KDE_FULL_SESSION="${KDE_FULL_SESSION:-true}"
export KDE_SESSION_VERSION="${KDE_SESSION_VERSION:-6}"
export LIBSEAT_BACKEND="${LIBSEAT_BACKEND:-seatd}"
export LOGNAME="${LOGNAME:-${USER:-root}}"
export PATH="${PATH:-/usr/bin:/bin}"
export QML2_IMPORT_PATH="${QML2_IMPORT_PATH:-/usr/qml}"
export QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-/usr/plugins}"
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-wayland}"
export QT_QPA_PLATFORM_PLUGIN_PATH="${QT_QPA_PLATFORM_PLUGIN_PATH:-/usr/plugins/platforms}"
export SEATD_SOCK="${SEATD_SOCK:-/run/seatd.sock}"
export USER="${USER:-root}"
export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}"
export XCURSOR_THEME="${XCURSOR_THEME:-Pop}"
export XDG_CURRENT_DESKTOP="${XDG_CURRENT_DESKTOP:-KDE}"
export XDG_SESSION_DESKTOP="${XDG_SESSION_DESKTOP:-KDE}"
export XDG_SESSION_ID="${XDG_SESSION_ID:-c1}"
export XDG_SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}"
export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}"
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
export XDG_RUNTIME_DIR="/tmp/run/user/$(id -u)"
fi
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null || true
choose_state_dir() {
local requested="${REDBEAR_KDE_SESSION_STATE_DIR:-}"
if [ -n "$requested" ]; then
mkdir -p "$requested" 2>/dev/null || true
if [ -d "$requested" ] && [ -w "$requested" ]; then
printf '%s\n' "$requested"
return 0
fi
fi
if [ -d /run ] && [ -w /run ]; then
printf '%s\n' "/run"
return 0
fi
printf '%s\n' "$XDG_RUNTIME_DIR"
}
session_state_dir="$(choose_state_dir)"
mkdir -p "$session_state_dir"
chmod 700 "$session_state_dir" 2>/dev/null || true
session_env_file="$session_state_dir/redbear-kde-session.env"
session_ready_file="$session_state_dir/redbear-kde-session.ready"
panel_ready_file="$session_state_dir/redbear-kde-session.panel-ready"
rm -f "$session_ready_file" "$panel_ready_file"
cleanup() {
local status=$?
trap - EXIT INT TERM
for pid in "${optional_pids[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
fi
done
if [ -n "$kwin_pid" ] && kill -0 "$kwin_pid" 2>/dev/null; then
kill "$kwin_pid" 2>/dev/null || true
wait "$kwin_pid" 2>/dev/null || true
fi
exit "$status"
}
trap cleanup EXIT INT TERM
kwin_mode="virtual"
set_kwin_mode() {
local requested="${REDBEAR_KDE_SESSION_BACKEND:-auto}"
case "$requested" in
drm)
if [ -z "${KWIN_DRM_DEVICES:-}" ] && [ -e /scheme/drm/card0 ]; then
export KWIN_DRM_DEVICES=/scheme/drm/card0
fi
if [ -n "${KWIN_DRM_DEVICES:-}" ]; then
kwin_mode="drm"
else
kwin_mode="virtual"
fi
;;
virtual)
kwin_mode="virtual"
;;
auto|"")
if [ -n "${KWIN_DRM_DEVICES:-}" ]; then
kwin_mode="drm"
elif [ -e /scheme/drm/card0 ]; then
export KWIN_DRM_DEVICES=/scheme/drm/card0
kwin_mode="drm"
else
kwin_mode="virtual"
fi
;;
*)
kwin_mode="virtual"
;;
esac
}
set_kwin_mode
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/dev/null 2>&1; then
eval "$(dbus-launch --sh-syntax)"
fi
write_session_environment() {
{
printf 'DBUS_SESSION_BUS_ADDRESS=%s\n' "${DBUS_SESSION_BUS_ADDRESS:-}"
printf 'DESKTOP_SESSION=%s\n' "$DESKTOP_SESSION"
printf 'KDE_FULL_SESSION=%s\n' "$KDE_FULL_SESSION"
printf 'KDE_SESSION_VERSION=%s\n' "$KDE_SESSION_VERSION"
printf 'KWIN_DRM_DEVICES=%s\n' "${KWIN_DRM_DEVICES:-}"
printf 'KWIN_MODE=%s\n' "$kwin_mode"
printf 'QML2_IMPORT_PATH=%s\n' "$QML2_IMPORT_PATH"
printf 'QT_PLUGIN_PATH=%s\n' "$QT_PLUGIN_PATH"
printf 'QT_QPA_PLATFORM=%s\n' "$QT_QPA_PLATFORM"
printf 'QT_QPA_PLATFORM_PLUGIN_PATH=%s\n' "$QT_QPA_PLATFORM_PLUGIN_PATH"
printf 'SEATD_SOCK=%s\n' "$SEATD_SOCK"
printf 'SESSION_STATE_DIR=%s\n' "$session_state_dir"
printf 'WAYLAND_DISPLAY=%s\n' "$WAYLAND_DISPLAY"
printf 'XDG_CURRENT_DESKTOP=%s\n' "$XDG_CURRENT_DESKTOP"
printf 'XDG_RUNTIME_DIR=%s\n' "$XDG_RUNTIME_DIR"
printf 'XDG_SESSION_DESKTOP=%s\n' "$XDG_SESSION_DESKTOP"
printf 'XDG_SESSION_ID=%s\n' "$XDG_SESSION_ID"
printf 'XDG_SESSION_TYPE=%s\n' "$XDG_SESSION_TYPE"
printf 'XKB_CONFIG_ROOT=%s\n' "$XKB_CONFIG_ROOT"
} > "$session_env_file"
chmod 600 "$session_env_file" 2>/dev/null || true
}
write_session_environment
if command -v dbus-update-activation-environment >/dev/null 2>&1; then
dbus-update-activation-environment \
DBUS_SESSION_BUS_ADDRESS \
DBUS_SESSION_BUS_PID \
DESKTOP_SESSION \
KDE_FULL_SESSION \
KDE_SESSION_VERSION \
KWIN_DRM_DEVICES \
QML2_IMPORT_PATH \
QT_PLUGIN_PATH \
QT_QPA_PLATFORM \
QT_QPA_PLATFORM_PLUGIN_PATH \
WAYLAND_DISPLAY \
XDG_CURRENT_DESKTOP \
XDG_RUNTIME_DIR \
XDG_SESSION_DESKTOP \
XDG_SESSION_ID \
XDG_SESSION_TYPE \
XKB_CONFIG_ROOT \
XCURSOR_THEME
fi
wait_for_wayland_socket() {
local socket_path="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"
local attempts=0
while [ "$attempts" -lt 40 ]; do
if [ -S "$socket_path" ] || [ -e "$socket_path" ]; then
return 0
fi
if [ -n "$kwin_pid" ] && ! kill -0 "$kwin_pid" 2>/dev/null; then
return 1
fi
attempts=$((attempts + 1))
sleep 1
done
return 1
}
mark_validation_success() {
if [ -e "$VALIDATION_REQUEST" ]; then
: > "$VALIDATION_SUCCESS" 2>/dev/null || true
fi
}
launch_optional_component() {
local program="$1"
local ready_marker="$2"
if ! command -v "$program" >/dev/null 2>&1; then
return 0
fi
"$program" &
local pid=$!
optional_pids+=("$pid")
if [ -n "$ready_marker" ]; then
sleep 1
if kill -0 "$pid" 2>/dev/null; then
: > "$ready_marker"
fi
fi
}
kwin_args=()
if [ "$kwin_mode" = "drm" ]; then
kwin_args+=(--drm)
else
kwin_args+=(--virtual)
fi
kwin_wayland_wrapper "${kwin_args[@]}" &
kwin_pid=$!
if ! wait_for_wayland_socket; then
printf '%s\n' "redbear-kde-session: kwin_wayland_wrapper failed to expose $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" >&2
exit 1
fi
: > "$session_ready_file"
mark_validation_success
launch_optional_component kded6 ""
launch_optional_component plasmashell "$panel_ready_file"
wait "$kwin_pid"
@@ -26,3 +26,4 @@ template = "cargo"
"/usr/bin/redbear-phase3-kwin-check" = "redbear-phase3-kwin-check"
"/usr/bin/redbear-phase4-kde-check" = "redbear-phase4-kde-check"
"/usr/bin/redbear-phase5-gpu-check" = "redbear-phase5-gpu-check"
"/usr/bin/redbear-phase5-cs-check" = "redbear-phase5-cs-check"
@@ -127,6 +127,10 @@ path = "src/bin/redbear-phase4-kde-check.rs"
name = "redbear-phase5-gpu-check"
path = "src/bin/redbear-phase5-gpu-check.rs"
[[bin]]
name = "redbear-phase5-cs-check"
path = "src/bin/redbear-phase5-cs-check.rs"
[dependencies]
redbear-login-protocol = { path = "../../redbear-login-protocol/source" }
serde = { version = "1", features = ["derive"] }
@@ -1,58 +1,129 @@
// Phase 4 KDE Plasma preflight check.
// Validates KF6 library presence, plasma binaries, and session entry points.
// Does NOT validate real KDE Plasma session behavior (blocked on Qt6Quick/QML + real KWin).
// Phase 4 KDE Plasma session check.
// Validates the installed KDE session entry point plus a bounded runtime surface
// exposed by the Red Bear session launcher and helper service.
use std::process;
const PROGRAM: &str = "redbear-phase4-kde-check";
const USAGE: &str = "Usage: redbear-phase4-kde-check [--json]\n\n\
Phase 4 KDE Plasma preflight check. Validates KF6 library and plasma binary\n\
presence. Does NOT validate real KDE session behavior (gated on Qt6Quick/QML).";
Phase 4 KDE Plasma session check. Validates KF6 library presence, the\n\
Red Bear KDE session entry point, KDE session environment capture, core\n\
helper processes, and a basic panel-readiness proxy.";
#[cfg(target_os = "redox")]
use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
process::Command,
};
#[cfg(target_os = "redox")]
const REDBEAR_KDE_SESSION_ENV_FILE: &str = "redbear-kde-session.env";
#[cfg(target_os = "redox")]
const REDBEAR_KDE_SESSION_READY_FILE: &str = "redbear-kde-session.ready";
#[cfg(target_os = "redox")]
const REDBEAR_KDE_SESSION_PANEL_READY_FILE: &str = "redbear-kde-session.panel-ready";
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult { Pass, Fail, Skip }
enum CheckResult {
Pass,
Fail,
Skip,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self { Self::Pass => "PASS", Self::Fail => "FAIL", Self::Skip => "SKIP" }
match self {
Self::Pass => "PASS",
Self::Fail => "FAIL",
Self::Skip => "SKIP",
}
}
}
#[cfg(target_os = "redox")]
struct Check { name: String, result: CheckResult, detail: String }
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Pass, detail: detail.to_string() }
fn pass(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.into(),
}
fn fail(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Fail, detail: detail.to_string() }
}
fn skip(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Skip, detail: detail.to_string() }
fn fail(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.into(),
}
}
fn skip(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.into(),
}
}
}
#[cfg(target_os = "redox")]
struct Report { checks: Vec<Check>, json_mode: bool }
struct Report {
checks: Vec<Check>,
json_mode: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self { Report { checks: Vec::new(), json_mode } }
fn add(&mut self, check: Check) { self.checks.push(check); }
fn any_failed(&self) -> bool { self.checks.iter().any(|c| c.result == CheckResult::Fail) }
fn new(json_mode: bool) -> Self {
Self {
checks: Vec::new(),
json_mode,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks
.iter()
.any(|check| check.result == CheckResult::Fail)
}
fn check_passed(&self, name: &str) -> bool {
self.checks
.iter()
.find(|check| check.name == name)
.is_some_and(|check| check.result == CheckResult::Pass)
}
fn print(&self) {
if self.json_mode { self.print_json(); } else { self.print_human(); }
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]", CheckResult::Fail => "[FAIL]", CheckResult::Skip => "[SKIP]",
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
@@ -60,129 +131,573 @@ impl Report {
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck { name: String, result: String, detail: String }
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
kf6_libs_present: bool, plasma_binaries_present: bool,
session_entry: bool, kirigami_available: bool, checks: Vec<JsonCheck>,
overall_success: bool,
kf6_libs_present: bool,
plasma_binaries_present: bool,
session_entry: bool,
session_environment: bool,
plasmashell_process: bool,
kded6_process: bool,
panel_rendering_ready: bool,
kirigami_available: bool,
checks: Vec<JsonCheck>,
}
let kf6_libs = self.checks.iter().find(|c| c.name == "KF6_LIBRARIES").map_or(false, |c| c.result == CheckResult::Pass);
let plasma_bins = self.checks.iter().find(|c| c.name == "PLASMA_BINARIES").map_or(false, |c| c.result == CheckResult::Pass);
let session_entry = self.checks.iter().find(|c| c.name == "SESSION_ENTRY").map_or(false, |c| c.result == CheckResult::Pass);
let kirigami = self.checks.iter().find(|c| c.name == "KIRIGAMI_STATUS").map_or(false, |c| c.result == CheckResult::Pass);
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
}).collect();
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { kf6_libs_present: kf6_libs, plasma_binaries_present: plasma_bins, session_entry, kirigami_available: kirigami, checks }) {
let checks = self
.checks
.iter()
.map(|check| JsonCheck {
name: check.name.clone(),
result: check.result.label().to_string(),
detail: check.detail.clone(),
})
.collect::<Vec<_>>();
let report = JsonReport {
overall_success: !self.any_failed(),
kf6_libs_present: self.check_passed("KF6_LIBRARIES"),
plasma_binaries_present: self.check_passed("PLASMA_BINARIES"),
session_entry: self.check_passed("SESSION_ENTRY"),
session_environment: self.check_passed("SESSION_ENVIRONMENT"),
plasmashell_process: self.check_passed("PLASMASHELL_PROCESS"),
kded6_process: self.check_passed("KDED6_PROCESS"),
panel_rendering_ready: self.check_passed("PANEL_RENDERING_READY"),
kirigami_available: self.check_passed("KIRIGAMI_STATUS"),
checks,
};
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[derive(Clone, Debug)]
struct SessionEnvironment {
source: String,
values: BTreeMap<String, String>,
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => { println!("{USAGE}"); return Err(String::new()); }
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
#[cfg(target_os = "redox")]
fn check_kf6_libraries() -> Check {
let key_libs = [
"/usr/lib/libKF6CoreAddons.so", "/usr/lib/libKF6ConfigCore.so",
"/usr/lib/libKF6I18n.so", "/usr/lib/libKF6WindowSystem.so",
"/usr/lib/libKF6Notifications.so", "/usr/lib/libKF6Service.so",
"/usr/lib/libKF6CoreAddons.so",
"/usr/lib/libKF6ConfigCore.so",
"/usr/lib/libKF6I18n.so",
"/usr/lib/libKF6WindowSystem.so",
"/usr/lib/libKF6Notifications.so",
"/usr/lib/libKF6Service.so",
"/usr/lib/libKF6WaylandClient.so",
];
let mut found = 0usize;
let mut missing = Vec::new();
for lib in key_libs {
if std::path::Path::new(lib).exists() {
if Path::new(lib).exists() {
found += 1;
} else {
missing.push(lib);
}
}
if found >= 6 {
let preview: Vec<_> = missing.iter().take(3).map(|s| s.rsplit('/').next().unwrap_or(s)).collect();
if missing.is_empty() {
Check::pass("KF6_LIBRARIES", &format!("{}/{} key KF6 libs found", found, key_libs.len()))
Check::pass(
"KF6_LIBRARIES",
format!("{found}/{} key KF6 libraries found", key_libs.len()),
)
} else {
Check::pass("KF6_LIBRARIES", &format!("{}/{} found, missing: {}", found, key_libs.len(), preview.join(", ")))
let preview = missing
.iter()
.take(3)
.map(|path| path.rsplit('/').next().unwrap_or(path))
.collect::<Vec<_>>()
.join(", ");
Check::pass(
"KF6_LIBRARIES",
format!("{found}/{} found, missing: {preview}", key_libs.len()),
)
}
} else {
Check::fail("KF6_LIBRARIES", &format!("only {}/{} key KF6 libs found", found, key_libs.len()))
Check::fail(
"KF6_LIBRARIES",
format!("only {found}/{} key KF6 libraries found", key_libs.len()),
)
}
}
#[cfg(target_os = "redox")]
fn check_plasma_binaries() -> Check {
let bins = ["/usr/bin/plasmashell", "/usr/bin/systemsettings", "/usr/bin/kwin_wayland_wrapper"];
let mut found = 0usize;
for bin in bins {
if std::path::Path::new(bin).exists() { found += 1; }
}
if found >= 2 {
Check::pass("PLASMA_BINARIES", &format!("{}/{} plasma binaries present", found, bins.len()))
} else if found == 1 {
Check::fail("PLASMA_BINARIES", &format!("only {}/{} plasma binaries present", found, bins.len()))
} else {
Check::fail("PLASMA_BINARIES", "no plasma binaries found")
let required = [
"/usr/bin/redbear-kde-session",
"/usr/bin/kwin_wayland_wrapper",
"/usr/bin/plasmashell",
"/usr/bin/kded6",
];
let optional: &[&str] = &[];
let missing_required = required
.iter()
.copied()
.filter(|path| !Path::new(path).exists())
.collect::<Vec<_>>();
if !missing_required.is_empty() {
return Check::fail(
"PLASMA_BINARIES",
format!(
"missing required session binaries: {}",
missing_required.join(", ")
),
);
}
let found_optional = optional
.iter()
.copied()
.filter(|path| Path::new(path).exists())
.collect::<Vec<_>>();
Check::pass(
"PLASMA_BINARIES",
format!(
"required session binaries present; optional helpers found: {}/{}",
found_optional.len(),
optional.len()
),
)
}
#[cfg(target_os = "redox")]
fn check_session_entry() -> Check {
let entries = ["/usr/bin/startplasma-wayland", "/usr/lib/plasma-session"];
for e in entries {
if std::path::Path::new(e).exists() {
return Check::pass("SESSION_ENTRY", e);
let entry = "/usr/bin/redbear-kde-session";
if Path::new(entry).exists() {
Check::pass("SESSION_ENTRY", entry)
} else {
Check::fail("SESSION_ENTRY", "missing /usr/bin/redbear-kde-session")
}
}
Check::fail("SESSION_ENTRY", "no KDE session entry point found")
#[cfg(target_os = "redox")]
fn env_value(name: &str) -> Option<String> {
env::var(name).ok().filter(|value| !value.trim().is_empty())
}
#[cfg(target_os = "redox")]
fn candidate_state_dirs() -> Vec<PathBuf> {
let mut dirs = vec![
PathBuf::from("/run"),
PathBuf::from("/run/redbear-display-session"),
];
if let Some(dir) = env_value("XDG_RUNTIME_DIR") {
let runtime_dir = PathBuf::from(dir);
if !dirs.contains(&runtime_dir) {
dirs.push(runtime_dir);
}
}
dirs
}
#[cfg(target_os = "redox")]
fn candidate_state_files(file_name: &str) -> Vec<PathBuf> {
candidate_state_dirs()
.into_iter()
.map(|dir| dir.join(file_name))
.collect::<Vec<_>>()
}
#[cfg(target_os = "redox")]
fn parse_key_value_file(path: &Path) -> Result<BTreeMap<String, String>, String> {
let contents = fs::read_to_string(path)
.map_err(|err| format!("failed to read {}: {err}", path.display()))?;
let mut values = BTreeMap::new();
for raw_line in contents.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
values.insert(key.to_string(), value.to_string());
}
}
Ok(values)
}
#[cfg(target_os = "redox")]
fn load_session_environment() -> Result<SessionEnvironment, String> {
for path in candidate_state_files(REDBEAR_KDE_SESSION_ENV_FILE) {
if path.exists() {
let values = parse_key_value_file(&path)?;
return Ok(SessionEnvironment {
source: path.display().to_string(),
values,
});
}
}
let mut values = BTreeMap::new();
for key in [
"XDG_SESSION_TYPE",
"XDG_CURRENT_DESKTOP",
"KDE_FULL_SESSION",
"QT_PLUGIN_PATH",
"QT_QPA_PLATFORM_PLUGIN_PATH",
"QML2_IMPORT_PATH",
"WAYLAND_DISPLAY",
"XDG_RUNTIME_DIR",
] {
if let Some(value) = env_value(key) {
values.insert(key.to_string(), value);
}
}
if values.is_empty() {
let paths = candidate_state_files(REDBEAR_KDE_SESSION_ENV_FILE)
.into_iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
Err(format!("no KDE session environment file found in: {paths}"))
} else {
Ok(SessionEnvironment {
source: String::from("current process environment"),
values,
})
}
}
#[cfg(target_os = "redox")]
fn check_required_env_value(
values: &BTreeMap<String, String>,
key: &str,
expected: &str,
) -> Result<(), String> {
match values.get(key) {
Some(value) if value == expected => Ok(()),
Some(value) => Err(format!("{key}={value} (expected {expected})")),
None => Err(format!("missing {key}")),
}
}
#[cfg(target_os = "redox")]
fn check_nonempty_env_value(values: &BTreeMap<String, String>, key: &str) -> Result<(), String> {
match values.get(key) {
Some(value) if !value.trim().is_empty() => Ok(()),
Some(_) => Err(format!("{key} is empty")),
None => Err(format!("missing {key}")),
}
}
#[cfg(target_os = "redox")]
fn check_session_environment() -> Check {
match load_session_environment() {
Ok(session) => {
let checks = [
check_required_env_value(&session.values, "XDG_SESSION_TYPE", "wayland"),
check_required_env_value(&session.values, "XDG_CURRENT_DESKTOP", "KDE"),
check_required_env_value(&session.values, "KDE_FULL_SESSION", "true"),
check_nonempty_env_value(&session.values, "QT_PLUGIN_PATH"),
check_nonempty_env_value(&session.values, "QT_QPA_PLATFORM_PLUGIN_PATH"),
check_nonempty_env_value(&session.values, "QML2_IMPORT_PATH"),
];
let failures = checks
.into_iter()
.filter_map(Result::err)
.collect::<Vec<_>>();
if failures.is_empty() {
Check::pass(
"SESSION_ENVIRONMENT",
format!("captured KDE session environment from {}", session.source),
)
} else {
Check::fail(
"SESSION_ENVIRONMENT",
format!(
"invalid KDE session environment from {}: {}",
session.source,
failures.join("; ")
),
)
}
}
Err(err) => Check::fail("SESSION_ENVIRONMENT", err),
}
}
#[cfg(target_os = "redox")]
fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|err| format!("failed to run {label}: {err}"))?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = if !stderr.trim().is_empty() {
stderr.trim().to_string()
} else if !stdout.trim().is_empty() {
stdout.trim().to_string()
} else {
String::from("no output")
};
return Err(format!(
"{label} exited with status {}: {detail}",
output.status
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[cfg(target_os = "redox")]
fn process_output() -> Result<String, String> {
run_command("ps", &[], "ps")
}
#[cfg(target_os = "redox")]
fn output_has_process(output: &str, process_name: &str) -> bool {
output.lines().any(|line| line.contains(process_name))
}
#[cfg(target_os = "redox")]
fn check_required_process(process_name: &str, binary_path: &str, check_name: &str) -> Check {
if !Path::new(binary_path).exists() {
return Check::fail(check_name, format!("{binary_path} is not installed"));
}
match process_output() {
Ok(output) => {
if output_has_process(&output, process_name) {
Check::pass(check_name, format!("{process_name} appears in ps output"))
} else {
Check::fail(
check_name,
format!("{process_name} is not present in ps output"),
)
}
}
Err(err) => Check::fail(check_name, err),
}
}
#[cfg(target_os = "redox")]
fn first_existing_state_file(file_name: &str) -> Option<PathBuf> {
candidate_state_files(file_name)
.into_iter()
.find(|path| path.exists())
}
#[cfg(target_os = "redox")]
fn wayland_socket_from_session_env(values: &BTreeMap<String, String>) -> Option<PathBuf> {
let runtime_dir = values.get("XDG_RUNTIME_DIR")?;
let display = values.get("WAYLAND_DISPLAY")?;
Some(PathBuf::from(runtime_dir).join(display))
}
#[cfg(target_os = "redox")]
fn check_panel_rendering_readiness() -> Check {
if !Path::new("/usr/bin/plasmashell").exists() {
return Check::skip(
"PANEL_RENDERING_READY",
"plasmashell is not installed, panel readiness cannot be checked",
);
}
if let Some(path) = first_existing_state_file(REDBEAR_KDE_SESSION_PANEL_READY_FILE) {
return Check::pass(
"PANEL_RENDERING_READY",
format!("panel readiness marker present at {}", path.display()),
);
}
let session = match load_session_environment() {
Ok(session) => session,
Err(err) => return Check::fail("PANEL_RENDERING_READY", err),
};
let socket_path = match wayland_socket_from_session_env(&session.values) {
Some(path) => path,
None => {
return Check::fail(
"PANEL_RENDERING_READY",
"session environment is missing XDG_RUNTIME_DIR or WAYLAND_DISPLAY",
);
}
};
let processes = match process_output() {
Ok(output) => output,
Err(err) => return Check::fail("PANEL_RENDERING_READY", err),
};
if output_has_process(&processes, "plasmashell") && socket_path.exists() {
Check::pass(
"PANEL_RENDERING_READY",
format!(
"plasmashell is running and Wayland socket is present at {}",
socket_path.display()
),
)
} else {
Check::fail(
"PANEL_RENDERING_READY",
format!(
"missing panel marker and runtime proxy (plasmashell process/socket {})",
socket_path.display()
),
)
}
}
#[cfg(target_os = "redox")]
fn check_session_ready_marker() -> Check {
if let Some(path) = first_existing_state_file(REDBEAR_KDE_SESSION_READY_FILE) {
Check::pass(
"SESSION_READY_MARKER",
format!("session readiness marker present at {}", path.display()),
)
} else {
let paths = candidate_state_files(REDBEAR_KDE_SESSION_READY_FILE)
.into_iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
Check::fail(
"SESSION_READY_MARKER",
format!("no readiness marker found in: {paths}"),
)
}
}
#[cfg(target_os = "redox")]
fn check_kirigami_status() -> Check {
let kirigami_lib = "/usr/lib/libKF6Kirigami.so";
if std::path::Path::new(kirigami_lib).exists() {
if Path::new(kirigami_lib).exists() {
Check::pass("KIRIGAMI_STATUS", "kirigami library present")
} else {
Check::skip("KIRIGAMI_STATUS", "kirigami not available (QML stub, requires Qt6Quick)")
Check::skip(
"KIRIGAMI_STATUS",
"kirigami not available (QML stub, requires Qt6Quick)",
)
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|a| a == "-h" || a == "--help") { println!("{USAGE}"); return Err(String::new()); }
if std::env::args().any(|arg| arg == "-h" || arg == "--help") {
println!("{USAGE}");
return Err(String::new());
}
println!("{PROGRAM}: KDE Plasma check requires Redox runtime");
return Ok(());
}
#[cfg(target_os = "redox")]
{
let json_mode = parse_args()?;
let mut report = Report::new(json_mode);
report.add(check_kf6_libraries());
report.add(check_plasma_binaries());
report.add(check_session_entry());
report.add(check_session_environment());
report.add(check_session_ready_marker());
report.add(check_required_process(
"plasmashell",
"/usr/bin/plasmashell",
"PLASMASHELL_PROCESS",
));
report.add(check_required_process(
"kded6",
"/usr/bin/kded6",
"KDED6_PROCESS",
));
report.add(check_panel_rendering_readiness());
report.add(check_kirigami_status());
report.print();
if report.any_failed() { return Err("one or more Phase 4 checks failed".to_string()); }
if report.any_failed() {
return Err(String::from("one or more Phase 4 KDE checks failed"));
}
Ok(())
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() { process::exit(0); }
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
#[cfg(all(test, target_os = "redox"))]
mod tests {
use super::*;
#[test]
fn parse_key_value_file_collects_session_values() {
let temp_dir = std::env::temp_dir().join("redbear-phase4-kde-check-tests");
fs::create_dir_all(&temp_dir).expect("temp dir should be created");
let path = temp_dir.join("env.txt");
fs::write(
&path,
"XDG_SESSION_TYPE=wayland\nKDE_FULL_SESSION=true\nQML2_IMPORT_PATH=/usr/qml\n",
)
.expect("env file should be written");
let parsed = parse_key_value_file(&path).expect("env file should parse");
assert_eq!(
parsed.get("XDG_SESSION_TYPE"),
Some(&String::from("wayland"))
);
assert_eq!(parsed.get("KDE_FULL_SESSION"), Some(&String::from("true")));
assert_eq!(
parsed.get("QML2_IMPORT_PATH"),
Some(&String::from("/usr/qml"))
);
}
#[test]
fn check_required_env_value_matches_expected_value() {
let mut values = BTreeMap::new();
values.insert(String::from("XDG_SESSION_TYPE"), String::from("wayland"));
assert!(check_required_env_value(&values, "XDG_SESSION_TYPE", "wayland").is_ok());
assert!(check_required_env_value(&values, "XDG_SESSION_TYPE", "x11").is_err());
}
}
@@ -0,0 +1,673 @@
// Phase 5 GPU command-submission validation checker.
// Validates DRM command-submission protocol reachability over /scheme/drm/card0.
// Does NOT claim real hardware render validation yet.
use std::process;
const PROGRAM: &str = "redbear-phase5-cs-check";
const USAGE: &str = "Usage: redbear-phase5-cs-check [--json]\n\n\
Phase 5 GPU command-submission validation. Probes DRM private CS ioctls,\n\
PRIME buffer sharing, GEM allocation, and fence/wait support. Real\n\
hardware rendering validation is still pending.";
#[cfg(target_os = "redox")]
const DRM_IOCTL_BASE: usize = 0x00A0;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CREATE: usize = DRM_IOCTL_BASE + 26;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CLOSE: usize = DRM_IOCTL_BASE + 27;
#[cfg(target_os = "redox")]
const DRM_IOCTL_PRIME_HANDLE_TO_FD: usize = DRM_IOCTL_BASE + 29;
#[cfg(target_os = "redox")]
const DRM_IOCTL_PRIME_FD_TO_HANDLE: usize = DRM_IOCTL_BASE + 30;
#[cfg(target_os = "redox")]
const DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT: usize = DRM_IOCTL_BASE + 31;
#[cfg(target_os = "redox")]
const DRM_IOCTL_REDOX_PRIVATE_CS_WAIT: usize = DRM_IOCTL_BASE + 32;
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult {
Pass,
Fail,
Skip,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self {
Self::Pass => "PASS",
Self::Fail => "FAIL",
Self::Skip => "SKIP",
}
}
}
#[cfg(target_os = "redox")]
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.to_string(),
}
}
fn fail(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.to_string(),
}
}
fn skip(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.to_string(),
}
}
}
#[cfg(target_os = "redox")]
struct Report {
checks: Vec<Check>,
json_mode: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self {
Self {
checks: Vec::new(),
json_mode,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|check| check.result == CheckResult::Fail)
}
fn print(&self) {
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
}
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
command_submission_protocol: bool,
prime_buffer_sharing: bool,
gem_buffer_allocation: bool,
fence_sync_support: bool,
hardware_validation_pending: bool,
checks: Vec<JsonCheck>,
}
let check_passed = |name: &str| {
self.checks
.iter()
.find(|check| check.name == name)
.is_some_and(|check| check.result == CheckResult::Pass)
};
let checks = self
.checks
.iter()
.map(|check| JsonCheck {
name: check.name.clone(),
result: check.result.label().to_string(),
detail: check.detail.clone(),
})
.collect::<Vec<_>>();
if let Err(err) = serde_json::to_writer(
std::io::stdout(),
&JsonReport {
command_submission_protocol: check_passed("CS_IOCTL_PROTOCOL"),
prime_buffer_sharing: check_passed("PRIME_BUFFER_SHARING"),
gem_buffer_allocation: check_passed("GEM_BUFFER_ALLOCATION"),
fence_sync_support: check_passed("FENCE_SYNC_SUPPORT"),
hardware_validation_pending: true,
checks,
},
) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCreateWire {
size: u64,
handle: u32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCloseWire {
handle: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeHandleToFdWire {
handle: u32,
flags: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeHandleToFdResponseWire {
fd: i32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeFdToHandleWire {
fd: i32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeFdToHandleResponseWire {
handle: u32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmit {
src_handle: u32,
dst_handle: u32,
src_offset: u64,
dst_offset: u64,
byte_count: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmitResult {
seqno: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsWait {
seqno: u64,
timeout_ns: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsWaitResult {
completed: u8,
pad: [u8; 7],
completed_seqno: u64,
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
#[cfg(target_os = "redox")]
fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
use std::mem::{MaybeUninit, size_of};
if bytes.len() != size_of::<T>() {
return Err(format!(
"unexpected DRM response size: expected {} bytes, got {}",
size_of::<T>(),
bytes.len()
));
}
let mut out = MaybeUninit::<T>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
Ok(out.assume_init())
}
}
#[cfg(target_os = "redox")]
fn bytes_of<T>(value: &T) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
(value as *const T).cast::<u8>(),
std::mem::size_of::<T>(),
)
}
}
#[cfg(target_os = "redox")]
fn open_drm_card(path: &str) -> Result<std::fs::File, String> {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(|err| format!("failed to open {path}: {err}"))
}
#[cfg(target_os = "redox")]
fn drm_query(file: &mut std::fs::File, request: usize, payload: &[u8]) -> Result<Vec<u8>, String> {
use std::io::{Read, Write};
let mut request_buf = request.to_le_bytes().to_vec();
request_buf.extend_from_slice(payload);
file.write_all(&request_buf)
.map_err(|err| format!("failed to send DRM ioctl {request:#x}: {err}"))?;
let mut response = vec![0u8; 4096];
let len = file
.read(&mut response)
.map_err(|err| format!("failed to read DRM ioctl {request:#x} response: {err}"))?;
response.truncate(len);
Ok(response)
}
#[cfg(target_os = "redox")]
fn close_gem(file: &mut std::fs::File, handle: u32) {
let request = DrmGemCloseWire { handle };
let _ = drm_query(file, DRM_IOCTL_GEM_CLOSE, bytes_of(&request));
}
#[cfg(target_os = "redox")]
fn run_redox(json_mode: bool) -> Result<(), String> {
let mut report = Report::new(json_mode);
let card_path = "/scheme/drm/card0";
if !std::path::Path::new(card_path).exists() {
report.add(Check::fail(
"CS_IOCTL_PROTOCOL",
"/scheme/drm/card0 missing; cannot probe command submission",
));
report.add(Check::skip(
"GEM_BUFFER_ALLOCATION",
"blocked: DRM card is unavailable",
));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: DRM card is unavailable",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: DRM card is unavailable",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
let mut exporter = match open_drm_card(card_path) {
Ok(file) => file,
Err(err) => {
report.add(Check::fail("CS_IOCTL_PROTOCOL", &err));
report.add(Check::skip(
"GEM_BUFFER_ALLOCATION",
"blocked: DRM card could not be opened",
));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: DRM card could not be opened",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: DRM card could not be opened",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
};
let mut importer = match open_drm_card(card_path) {
Ok(file) => file,
Err(err) => {
report.add(Check::fail("CS_IOCTL_PROTOCOL", &format!("opened exporter but importer failed: {err}")));
report.add(Check::skip(
"GEM_BUFFER_ALLOCATION",
"blocked: second DRM handle could not be opened",
));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: second DRM handle could not be opened",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: second DRM handle could not be opened",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
};
let mut exporter_handle = None;
let mut importer_src_handle = None;
let mut importer_dst_handle = None;
let create_exporter = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
match drm_query(&mut exporter, DRM_IOCTL_GEM_CREATE, bytes_of(&create_exporter))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => {
exporter_handle = Some(created.handle);
report.add(Check::pass(
"GEM_BUFFER_ALLOCATION",
&format!("allocated exporter GEM handle {} (4096 bytes)", created.handle),
));
}
Err(err) => {
report.add(Check::fail("GEM_BUFFER_ALLOCATION", &err));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: GEM allocation failed",
));
report.add(Check::skip(
"CS_IOCTL_PROTOCOL",
"blocked: GEM allocation failed",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: GEM allocation failed",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
}
if let Some(handle) = exporter_handle {
let export = DrmPrimeHandleToFdWire { handle, flags: 0 };
let prime_result = drm_query(&mut exporter, DRM_IOCTL_PRIME_HANDLE_TO_FD, bytes_of(&export))
.and_then(|response| decode_wire_exact::<DrmPrimeHandleToFdResponseWire>(&response))
.and_then(|exported| {
if exported.fd < 0 {
return Err(format!(
"PRIME export returned invalid token {} for GEM {}",
exported.fd, handle
));
}
let import = DrmPrimeFdToHandleWire {
fd: exported.fd,
pad: 0,
};
drm_query(&mut importer, DRM_IOCTL_PRIME_FD_TO_HANDLE, bytes_of(&import))
.and_then(|response| decode_wire_exact::<DrmPrimeFdToHandleResponseWire>(&response))
.map(|imported| (exported.fd, imported.handle))
});
match prime_result {
Ok((token, imported_handle)) => {
importer_src_handle = Some(imported_handle);
report.add(Check::pass(
"PRIME_BUFFER_SHARING",
&format!(
"export token {} imported as GEM handle {} on a second DRM fd",
token, imported_handle
),
));
}
Err(err) => {
report.add(Check::fail("PRIME_BUFFER_SHARING", &err));
report.add(Check::skip(
"CS_IOCTL_PROTOCOL",
"blocked: PRIME import/export failed",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: PRIME import/export failed",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
close_gem(&mut exporter, handle);
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
}
}
let create_importer = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
match drm_query(&mut importer, DRM_IOCTL_GEM_CREATE, bytes_of(&create_importer))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => importer_dst_handle = Some(created.handle),
Err(err) => {
report.add(Check::fail(
"CS_IOCTL_PROTOCOL",
&format!("secondary GEM allocation for CS submit failed: {err}"),
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: no destination GEM for CS submit",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
if let Some(handle) = importer_src_handle {
close_gem(&mut importer, handle);
}
if let Some(handle) = exporter_handle {
close_gem(&mut exporter, handle);
}
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
}
let submit_result = match (importer_src_handle, importer_dst_handle) {
(Some(src_handle), Some(dst_handle)) => {
let submit = RedoxPrivateCsSubmit {
src_handle,
dst_handle,
src_offset: 0,
dst_offset: 0,
byte_count: 64,
};
drm_query(
&mut importer,
DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT,
bytes_of(&submit),
)
.and_then(|response| decode_wire_exact::<RedoxPrivateCsSubmitResult>(&response))
.map(|result| (src_handle, dst_handle, result.seqno))
}
_ => Err("command submission prerequisites were incomplete".to_string()),
};
match submit_result {
Ok((src_handle, dst_handle, seqno)) => {
report.add(Check::pass(
"CS_IOCTL_PROTOCOL",
&format!(
"private CS submit accepted shared GEM {} -> local GEM {} (seqno {})",
src_handle, dst_handle, seqno
),
));
let wait = RedoxPrivateCsWait {
seqno,
timeout_ns: 0,
};
match drm_query(
&mut importer,
DRM_IOCTL_REDOX_PRIVATE_CS_WAIT,
bytes_of(&wait),
)
.and_then(|response| decode_wire_exact::<RedoxPrivateCsWaitResult>(&response))
{
Ok(wait_result) => {
let completed = match wait_result.completed {
0 => false,
1 => true,
value => {
report.add(Check::fail(
"FENCE_SYNC_SUPPORT",
&format!(
"wait ioctl returned invalid completion flag {} for seqno {}",
value, seqno
),
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"protocol-level CS proof exists, but real hardware rendering validation is still pending",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
};
report.add(Check::pass(
"FENCE_SYNC_SUPPORT",
&format!(
"bounded wait ioctl responded for seqno {} (completed={}, completed_seqno={}); real sync-object validation is still pending",
seqno, completed, wait_result.completed_seqno
),
));
}
Err(err) => {
report.add(Check::fail("FENCE_SYNC_SUPPORT", &err));
}
}
}
Err(err) => {
report.add(Check::fail("CS_IOCTL_PROTOCOL", &err));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: command submission ioctl failed",
));
}
}
if let Some(handle) = importer_dst_handle {
close_gem(&mut importer, handle);
}
if let Some(handle) = importer_src_handle {
close_gem(&mut importer, handle);
}
if let Some(handle) = exporter_handle {
close_gem(&mut exporter, handle);
}
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"protocol-level CS proof exists, but real hardware rendering validation is still pending",
));
report.print();
if report.any_failed() {
return Err("one or more Phase 5 CS checks failed".to_string());
}
Ok(())
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|arg| arg == "-h" || arg == "--help") {
println!("{USAGE}");
return Err(String::new());
}
println!("{PROGRAM}: CS check requires Redox runtime");
Ok(())
}
#[cfg(target_os = "redox")]
{
let json_mode = parse_args()?;
run_redox(json_mode)
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
@@ -10,6 +10,15 @@ const USAGE: &str = "Usage: redbear-phase5-gpu-check [--json]\n\n\
GPU firmware, and Mesa rendering infrastructure. Hardware validation\n\
requires real AMD/Intel GPU + command submission (CS ioctl).";
#[cfg(target_os = "redox")]
const DRM_IOCTL_BASE: usize = 0x00A0;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CREATE: usize = DRM_IOCTL_BASE + 26;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CLOSE: usize = DRM_IOCTL_BASE + 27;
#[cfg(target_os = "redox")]
const DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT: usize = DRM_IOCTL_BASE + 31;
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult { Pass, Fail, Skip }
@@ -65,21 +74,68 @@ impl Report {
#[derive(serde::Serialize)]
struct JsonReport {
drm_device: bool, gpu_firmware: bool, mesa_dri: bool,
display_modes: bool, checks: Vec<JsonCheck>,
display_modes: bool, cs_ioctl: bool, gem_buffers: bool,
hardware_rendering_ready: bool, checks: Vec<JsonCheck>,
}
let drm = self.checks.iter().find(|c| c.name == "DRM_DEVICE").map_or(false, |c| c.result == CheckResult::Pass);
let firmware = self.checks.iter().find(|c| c.name == "GPU_FIRMWARE").map_or(false, |c| c.result == CheckResult::Pass);
let mesa = self.checks.iter().find(|c| c.name == "MESA_DRI").map_or(false, |c| c.result == CheckResult::Pass);
let modes = self.checks.iter().find(|c| c.name == "DISPLAY_MODES").map_or(false, |c| c.result == CheckResult::Pass);
let cs_ioctl = self.checks.iter().find(|c| c.name == "CS_IOCTL_PROTOCOL").map_or(false, |c| c.result == CheckResult::Pass);
let gem_buffers = self.checks.iter().find(|c| c.name == "GEM_BUFFER_ALLOCATION").map_or(false, |c| c.result == CheckResult::Pass);
let hardware_ready = self.checks.iter().find(|c| c.name == "HARDWARE_RENDERING_READY").map_or(false, |c| c.result == CheckResult::Pass);
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
}).collect();
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { drm_device: drm, gpu_firmware: firmware, mesa_dri: mesa, display_modes: modes, checks }) {
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport {
drm_device: drm,
gpu_firmware: firmware,
mesa_dri: mesa,
display_modes: modes,
cs_ioctl,
gem_buffers,
hardware_rendering_ready: hardware_ready,
checks,
}) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCreateWire {
size: u64,
handle: u32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCloseWire {
handle: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmit {
src_handle: u32,
dst_handle: u32,
src_offset: u64,
dst_offset: u64,
byte_count: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmitResult {
seqno: u64,
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
@@ -95,13 +151,18 @@ fn parse_args() -> Result<bool, String> {
#[cfg(target_os = "redox")]
fn check_drm_device() -> Check {
let paths = ["/scheme/drm/card0", "/dev/dri/card0"];
for p in paths {
if std::path::Path::new(p).exists() {
return Check::pass("DRM_DEVICE", p);
let scheme_path = "/scheme/drm/card0";
if std::path::Path::new(scheme_path).exists() {
return Check::pass("DRM_DEVICE", scheme_path);
}
let dev_alias = "/dev/dri/card0";
if std::path::Path::new(dev_alias).exists() {
return Check::fail(
"DRM_DEVICE",
"/dev/dri/card0 exists, but Phase 5 CS probing requires /scheme/drm/card0",
);
}
Check::fail("DRM_DEVICE", "no DRM device found at /scheme/drm/card0 or /dev/dri/card0")
Check::fail("DRM_DEVICE", "no DRM device found at /scheme/drm/card0")
}
#[cfg(target_os = "redox")]
@@ -155,6 +216,216 @@ fn check_display_modes() -> Check {
}
}
#[cfg(target_os = "redox")]
fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
use std::mem::{MaybeUninit, size_of};
if bytes.len() != size_of::<T>() {
return Err(format!(
"unexpected DRM response size: expected {} bytes, got {}",
size_of::<T>(),
bytes.len()
));
}
let mut out = MaybeUninit::<T>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
Ok(out.assume_init())
}
}
#[cfg(target_os = "redox")]
fn bytes_of<T>(value: &T) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
(value as *const T).cast::<u8>(),
std::mem::size_of::<T>(),
)
}
}
#[cfg(target_os = "redox")]
fn open_scheme_drm_card() -> Result<std::fs::File, String> {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/scheme/drm/card0")
.map_err(|err| format!("failed to open /scheme/drm/card0: {err}"))
}
#[cfg(target_os = "redox")]
fn drm_query(file: &mut std::fs::File, request: usize, payload: &[u8]) -> Result<Vec<u8>, String> {
use std::io::{Read, Write};
let mut request_buf = request.to_le_bytes().to_vec();
request_buf.extend_from_slice(payload);
file.write_all(&request_buf)
.map_err(|err| format!("failed to send DRM ioctl {request:#x}: {err}"))?;
let mut response = vec![0u8; 4096];
let len = file
.read(&mut response)
.map_err(|err| format!("failed to read DRM ioctl {request:#x} response: {err}"))?;
response.truncate(len);
Ok(response)
}
#[cfg(target_os = "redox")]
fn check_gem_buffer_allocation() -> Check {
let mut card = match open_scheme_drm_card() {
Ok(card) => card,
Err(err) => return Check::fail("GEM_BUFFER_ALLOCATION", &err),
};
let request = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
match drm_query(&mut card, DRM_IOCTL_GEM_CREATE, bytes_of(&request))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => {
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created.handle,
}),
);
Check::pass(
"GEM_BUFFER_ALLOCATION",
&format!("allocated GEM handle {} over /scheme/drm/card0", created.handle),
)
}
Err(err) => Check::fail("GEM_BUFFER_ALLOCATION", &err),
}
}
#[cfg(target_os = "redox")]
fn check_cs_ioctl_protocol() -> Check {
let mut card = match open_scheme_drm_card() {
Ok(card) => card,
Err(err) => return Check::fail("CS_IOCTL_PROTOCOL", &err),
};
let first = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
let second = first;
let created_a = match drm_query(&mut card, DRM_IOCTL_GEM_CREATE, bytes_of(&first))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => created,
Err(err) => {
return Check::fail(
"CS_IOCTL_PROTOCOL",
&format!("source GEM allocation failed before CS probe: {err}"),
);
}
};
let created_b = match drm_query(&mut card, DRM_IOCTL_GEM_CREATE, bytes_of(&second))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => created,
Err(err) => {
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created_a.handle,
}),
);
return Check::fail(
"CS_IOCTL_PROTOCOL",
&format!("destination GEM allocation failed before CS probe: {err}"),
);
}
};
let submit = RedoxPrivateCsSubmit {
src_handle: created_a.handle,
dst_handle: created_b.handle,
src_offset: 0,
dst_offset: 0,
byte_count: 64,
};
let result = drm_query(
&mut card,
DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT,
bytes_of(&submit),
)
.and_then(|response| decode_wire_exact::<RedoxPrivateCsSubmitResult>(&response));
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created_b.handle,
}),
);
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created_a.handle,
}),
);
match result {
Ok(response) => Check::pass(
"CS_IOCTL_PROTOCOL",
&format!(
"private CS submit accepted GEM {} -> {} (seqno {})",
created_a.handle, created_b.handle, response.seqno
),
),
Err(err) => Check::fail("CS_IOCTL_PROTOCOL", &err),
}
}
#[cfg(target_os = "redox")]
fn check_hardware_rendering_ready(report: &Report) -> Check {
let required = [
"DRM_DEVICE",
"GPU_FIRMWARE",
"MESA_DRI",
"DISPLAY_MODES",
"GEM_BUFFER_ALLOCATION",
"CS_IOCTL_PROTOCOL",
];
let missing = required
.iter()
.copied()
.filter(|name| {
!report
.checks
.iter()
.any(|check| check.name == *name && check.result == CheckResult::Pass)
})
.collect::<Vec<_>>();
if missing.is_empty() {
Check::pass(
"HARDWARE_RENDERING_READY",
"Phase 5 preflight prerequisites are present; real hardware rendering validation is still pending",
)
} else {
Check::fail(
"HARDWARE_RENDERING_READY",
&format!(
"missing hardware rendering prerequisites: {}",
missing.join(", ")
),
)
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
@@ -170,6 +441,10 @@ fn run() -> Result<(), String> {
report.add(check_gpu_firmware());
report.add(check_mesa_dri_hardware());
report.add(check_display_modes());
report.add(check_gem_buffer_allocation());
report.add(check_cs_ioctl_protocol());
let readiness = check_hardware_rendering_ready(&report);
report.add(readiness);
report.print();
if report.any_failed() { return Err("one or more Phase 5 checks failed".to_string()); }
Ok(())
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env bash
# test-kde-session.sh — bounded KDE session assembly proof inside a Red Bear runtime.
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: test-kde-session.sh
Launch a bounded virtual KDE session through redbear-session-launch, then verify
the session environment, compositor process, and optional Plasma helpers.
USAGE
}
for arg in "$@"; do
case "$arg" in
--help|-h|help)
usage
exit 0
;;
*)
printf 'ERROR: unsupported argument %s\n' "$arg" >&2
usage >&2
exit 1
;;
esac
done
state_dir="${REDBEAR_KDE_SESSION_STATE_DIR:-/tmp/run/redbear-kde-session-test}"
runtime_dir="${REDBEAR_KDE_SESSION_RUNTIME_DIR:-/tmp/run/redbear-kde-session-runtime}"
display_name="${REDBEAR_KDE_SESSION_DISPLAY:-wayland-kde-test}"
session_pid=""
cleanup() {
local status=$?
trap - EXIT INT TERM
if [[ -n "$session_pid" ]] && kill -0 "$session_pid" 2>/dev/null; then
kill "$session_pid" 2>/dev/null || true
wait "$session_pid" 2>/dev/null || true
fi
exit "$status"
}
trap cleanup EXIT INT TERM
require_binary() {
local program="$1"
if command -v "$program" >/dev/null 2>&1; then
printf 'KDE_SESSION_BINARY_%s=ok\n' "$program"
else
printf 'KDE_SESSION_BINARY_%s=missing\n' "$program" >&2
exit 1
fi
}
wait_for_file() {
local target="$1"
local attempts="$2"
local count=0
while (( count < attempts )); do
if [[ -e "$target" ]]; then
return 0
fi
count=$((count + 1))
sleep 1
done
return 1
}
wait_for_process_pattern() {
local pattern="$1"
local attempts="$2"
local count=0
while (( count < attempts )); do
if ps | grep -Eq "$pattern"; then
return 0
fi
count=$((count + 1))
sleep 1
done
return 1
}
require_env_value() {
local file="$1"
local key="$2"
local expected="$3"
if grep -Eq "^${key}=${expected}$" "$file"; then
printf 'KDE_SESSION_ENV_%s=ok\n' "$key"
else
printf 'KDE_SESSION_ENV_%s=unexpected\n' "$key" >&2
exit 1
fi
}
require_process_pattern() {
local pattern="$1"
local label="$2"
if wait_for_process_pattern "$pattern" 15; then
printf 'KDE_SESSION_PROCESS_%s=ok\n' "$label"
else
printf 'KDE_SESSION_PROCESS_%s=missing\n' "$label" >&2
exit 1
fi
}
check_optional_process() {
local binary="$1"
local pattern="$2"
local label="$3"
if command -v "$binary" >/dev/null 2>&1; then
require_process_pattern "$pattern" "$label"
else
printf 'KDE_SESSION_PROCESS_%s=skipped_missing_binary\n' "$label"
fi
}
require_binary redbear-session-launch
require_binary redbear-kde-session
require_binary kwin_wayland_wrapper
rm -rf "$state_dir" "$runtime_dir"
mkdir -p "$state_dir" "$runtime_dir"
chmod 700 "$state_dir" "$runtime_dir" 2>/dev/null || true
env \
QT_PLUGIN_PATH=/usr/plugins \
QT_QPA_PLATFORM_PLUGIN_PATH=/usr/plugins/platforms \
QML2_IMPORT_PATH=/usr/qml \
XCURSOR_THEME=Pop \
XKB_CONFIG_ROOT=/usr/share/X11/xkb \
REDBEAR_KDE_SESSION_BACKEND=virtual \
REDBEAR_KDE_SESSION_STATE_DIR="$state_dir" \
redbear-session-launch \
--username root \
--mode session \
--session kde-wayland \
--vt 4 \
--runtime-dir "$runtime_dir" \
--wayland-display "$display_name" &
session_pid=$!
ready_file="$state_dir/redbear-kde-session.ready"
env_file="$state_dir/redbear-kde-session.env"
panel_ready_file="$state_dir/redbear-kde-session.panel-ready"
if wait_for_file "$ready_file" 40; then
printf 'KDE_SESSION_START=ok\n'
else
printf 'KDE_SESSION_START=timeout\n' >&2
exit 1
fi
if [[ ! -f "$env_file" ]]; then
printf 'KDE_SESSION_ENV_FILE=missing\n' >&2
exit 1
fi
printf 'KDE_SESSION_ENV_FILE=%s\n' "$env_file"
require_env_value "$env_file" XDG_SESSION_TYPE wayland
require_env_value "$env_file" XDG_CURRENT_DESKTOP KDE
require_env_value "$env_file" KDE_FULL_SESSION true
require_env_value "$env_file" KWIN_MODE virtual
require_process_pattern '(kwin_wayland_wrapper|redbear-compositor)' COMPOSITOR
check_optional_process kded6 'kded6' KDED6
check_optional_process plasmashell 'plasmashell' PLASMASHELL
if command -v plasmashell >/dev/null 2>&1; then
if wait_for_file "$panel_ready_file" 15; then
printf 'KDE_SESSION_PANEL_READY=ok\n'
else
printf 'KDE_SESSION_PANEL_READY=missing\n' >&2
exit 1
fi
else
printf 'KDE_SESSION_PANEL_READY=skipped_missing_plasmashell\n'
fi
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# Phase 5 GPU command-submission validation harness.
# Validates GEM allocation, PRIME sharing, CS ioctl reachability, and fence waits.
# Real hardware rendering validation is still pending.
set -euo pipefail
PROG="$(basename "$0")"
usage() {
cat <<'EOF'
Usage: test-phase5-cs-runtime.sh [--guest|--qemu CONFIG]
Modes:
--guest Run inside already-booted Red Bear OS
--qemu CONFIG Launch QEMU with CONFIG and run checks
Exit: 0 if all pass, 1 otherwise.
EOF
exit 1
}
MODE=""
CONFIG=""
while [[ $# -gt 0 ]]; do
case "$1" in
--guest) MODE="guest"; shift ;;
--qemu) MODE="qemu"; CONFIG="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "$PROG: unknown: $1"; usage ;;
esac
done
[[ -z "$MODE" ]] && usage
run_guest_checks() {
local failures=0
run_check() {
local name="$1" cmd="$2" desc="$3"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo " FAIL $name: $cmd not found ($desc)"
failures=$((failures + 1))
return 0
fi
echo " Running $name..."
if "$cmd" --json >/dev/null 2>&1; then
echo " PASS $name: $desc"
else
echo " FAIL $name: $desc (exit non-zero)"
failures=$((failures + 1))
fi
}
echo "=== Phase 5 GPU Command Submission Validation ==="
echo
run_check "CS" "redbear-phase5-cs-check" "CS ioctls + GEM + PRIME + fence wait"
echo
echo "=== Phase 5 CS Summary ==="
if [[ $failures -eq 0 ]]; then
echo "ALL PHASE 5 CS CHECKS PASSED"
else
echo "FAILURES: $failures"
exit 1
fi
exit 0
}
require_qemu_path() {
local value="$1"
local label="$2"
if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then
echo "$PROG: $label contains a newline or carriage return"
exit 1
fi
if [[ "$value" == *,* ]]; then
echo "$PROG: $label must not contain commas for QEMU -drive parsing"
exit 1
fi
}
run_qemu_checks() {
local arch="${ARCH:-x86_64}"
local image="build/${arch}/${CONFIG}/harddrive.img"
local firmware="${FIRMWARE_PATH:-/usr/share/ovmf/x64/OVMF.fd}"
require_qemu_path "$image" "image path"
require_qemu_path "$firmware" "firmware path"
if [[ ! -f "$image" ]]; then
echo "$PROG: image not found: $image (build with: make all CONFIG_NAME=$CONFIG)"
exit 1
fi
if [[ ! -f "$firmware" ]]; then
echo "$PROG: firmware not found: $firmware"
exit 1
fi
env RBOS_PHASE5_CS_IMAGE="$image" RBOS_PHASE5_CS_FIRMWARE="$firmware" expect <<'EXPECT_SCRIPT'
log_user 1; set timeout 300
spawn qemu-system-x86_64 -name {Red Bear OS} -device qemu-xhci -smp 4 -m 2048 -bios $env(RBOS_PHASE5_CS_FIRMWARE) -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev:debug -machine q35 -device virtio-net,netdev=net0 -netdev user,id=net0 -nographic -vga none -drive file=$env(RBOS_PHASE5_CS_IMAGE),format=raw,if=none,id=drv0 -device nvme,drive=drv0,serial=NVME_SERIAL -enable-kvm -cpu host
expect "login:"; send "root\r"
expect "assword:"; send "password\r"
expect "Type 'help' for available commands."
send "echo __READY__\r"; expect "__READY__"
send "redbear-phase5-cs-check --json >/dev/null 2>&1 && echo __P5CS_OK__ || echo __P5CS_FAIL__\r"
expect { "__P5CS_OK__" { } "__P5CS_FAIL__" { puts "FAIL: Phase 5 CS"; exit 1 } timeout { puts "FAIL: timeout"; exit 1 } eof { puts "FAIL: eof"; exit 1 } }
puts "ALL PHASE 5 CS CHECKS PASSED"
EXPECT_SCRIPT
exit $?
}
case "$MODE" in
guest) run_guest_checks ;;
qemu)
export FIRMWARE_PATH="${FIRMWARE_PATH:-/usr/share/ovmf/x64/OVMF.fd}"
run_qemu_checks
;;
*) usage ;;
esac