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:
@@ -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 2–4 work items and blocker detail
|
||||
**Version:** 2.2 (2026-04-29)
|
||||
**Updated:** Phase 1 test coverage complete; refined Phase 2–4 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 | 4–6 | Must finish honestly before claiming runtime trust |
|
||||
| Phase 2: Wayland Compositor Proof | 4–6 | Can overlap with late Phase 1 cleanup |
|
||||
| Phase 3: KWin Desktop Session | 6–10 | Starts after Phase 2; **lower bound is optimistic — assumes stub/shim cleanup stays bounded** |
|
||||
| Phase 3: KWin Desktop Session | 6–10 | Starts after Phase 2; **lower bound is optimistic — assumes remaining QML/session cleanup stays bounded** |
|
||||
| Phase 4: KDE Plasma Session | 8–12 | Starts after Phase 3; **lower bound assumes kirigami/knewstuff stubs resolve without major rework** |
|
||||
| Phase 5: Hardware GPU Enablement | 12–20 | Starts after Phase 1, parallel with 3–4 |
|
||||
|
||||
|
||||
@@ -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 1–5) |
|
||||
| `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` | Canonical desktop path plan (v2.2, Phase 1–5) |
|
||||
| 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"
|
||||
|
||||
|
||||
@@ -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 fail(name: &str, detail: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
result: CheckResult::Fail,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
fn skip(name: &str, detail: &str) -> Self {
|
||||
Check { name: name.to_string(), result: CheckResult::Skip, detail: detail.to_string() }
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
Check::fail("SESSION_ENTRY", "no KDE session entry point found")
|
||||
|
||||
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);
|
||||
}
|
||||
Check::fail("DRM_DEVICE", "no DRM device found at /scheme/drm/card0 or /dev/dri/card0")
|
||||
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")
|
||||
}
|
||||
|
||||
#[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(())
|
||||
|
||||
Executable
+188
@@ -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
|
||||
Executable
+119
@@ -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
|
||||
Reference in New Issue
Block a user