Sprint 0 — industrialisation : CI, tracing, tests xdg-shell, GPLv3

5 livrables d'industrialisation posés avant la phase 13 (client réel).

- .gitlab-ci.yml : pipeline minimal (fmt-check + tests core +
  tests frontend nightly). Pas de cross-compile Redox dans le MVP.
- rustfmt.toml + fmt.sh : config formatter racine + boucle sur les 23
  crates ; fmt sweep appliqué (d'où les diffs cosmétiques).
- tracing dans wayland-frontend : 22 println/eprintln remplacés par
  debug/info/warn/error selon sévérité. Préfixe "[frontend]"
  supprimé (le target tracing le fournit).
- tracing-subscriber dans le compositor : TeeWriter écrit sur
  stdout + /scheme/debug, filtre via RUST_LOG (défaut info).
  DebugSink/dlog supprimés.
- 15 tests unitaires xdg-shell après extraction de 2 helpers libres
  (clamp_to_min_max + should_throttle_configure). Couvre
  compute_resize_geom, contraintes min/max et throttling configure.
- LICENSE (GPLv3 texte officiel FSF) + .editorconfig.

Total tests : 27 → 42 automatisés (compositor-core + frontend).

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-14 20:46:07 +02:00
parent 2150f31d82
commit 8795f39f08
30 changed files with 1373 additions and 298 deletions

View file

@ -14,16 +14,27 @@
//! - sleep ~16ms
//!
//! Tourne 60 secondes max, exit propre.
//!
//! Logs : `tracing` + `tracing-subscriber`. Configurable via `RUST_LOG`
//! (cf. `EnvFilter::from_default_env`). Par défaut : `info`. Sortie
//! tee'd sur stdout ET sur `/scheme/debug` (= serial sous Redox/QEMU).
//! Exemples :
//! RUST_LOG=info → events lifecycle (focus, drag, garbage_collect)
//! RUST_LOG=debug → idem + détails protocole (set_min_size, hit_test…)
//! RUST_LOG=warn → uniquement anomalies (refused, rejected, mismatch)
use std::env;
use std::fs::OpenOptions;
use std::io::Write;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::{Command, ExitCode};
use std::sync::{Mutex, OnceLock};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use tracing::{error, info};
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
use redox_wl_compositor_core::Framebuffer;
use redox_wl_display::RedoxOutput;
use redox_wl_input::{InputBackend, InputEvent};
@ -32,36 +43,77 @@ use redox_wl_wayland_frontend::WaylandFrontend;
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
const BG_COLOR: u32 = 0xFF101820;
struct DebugSink(Mutex<Option<std::fs::File>>);
impl DebugSink {
/// MakeWriter qui tee chaque ligne de log vers stdout ET, si disponible,
/// vers `/scheme/debug` (serial console côté host quand on tourne sous
/// `make qemu`).
#[derive(Clone)]
struct TeeWriter {
debug_scheme: Arc<Mutex<Option<File>>>,
}
impl TeeWriter {
fn new() -> Self {
Self(Mutex::new(
OpenOptions::new().write(true).open("/scheme/debug").ok(),
))
}
fn writeln(&self, s: &str) {
println!("{s}");
if let Ok(mut g) = self.0.lock() {
if let Some(f) = g.as_mut() {
let _ = writeln!(f, "{s}");
}
let f = OpenOptions::new().write(true).open("/scheme/debug").ok();
Self {
debug_scheme: Arc::new(Mutex::new(f)),
}
}
}
fn dlog(s: &str) {
static SINK: OnceLock<DebugSink> = OnceLock::new();
SINK.get_or_init(DebugSink::new).writeln(s);
struct TeeWriterGuard {
debug_scheme: Arc<Mutex<Option<File>>>,
}
impl Write for TeeWriterGuard {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let _ = io::stdout().write_all(buf);
if let Ok(mut g) = self.debug_scheme.lock() {
if let Some(f) = g.as_mut() {
let _ = f.write_all(buf);
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
let _ = io::stdout().flush();
if let Ok(mut g) = self.debug_scheme.lock() {
if let Some(f) = g.as_mut() {
let _ = f.flush();
}
}
Ok(())
}
}
impl<'a> MakeWriter<'a> for TeeWriter {
type Writer = TeeWriterGuard;
fn make_writer(&'a self) -> Self::Writer {
TeeWriterGuard {
debug_scheme: Arc::clone(&self.debug_scheme),
}
}
}
fn init_tracing() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(TeeWriter::new())
.with_target(true)
.with_ansi(false)
.without_time()
.init();
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
dlog("[comp] Phase 6.4 — compositor Wayland démarrage");
info!("Phase 6.4 — compositor Wayland démarrage");
// Display
let mut output = RedoxOutput::open()?;
let our_vt = output.vt();
let fb_w = output.width();
let fb_h = output.height();
dlog(&format!("[comp] display {fb_w}x{fb_h}, VT={our_vt}"));
info!("display {fb_w}x{fb_h}, VT={our_vt}");
let _ = Command::new("inputd")
.arg("-A")
@ -69,7 +121,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.status();
thread::sleep(Duration::from_millis(300));
output.take_crtc()?;
dlog("[comp] CRTC pris");
info!("CRTC pris");
// Clear initial → fond bleu nuit pour signaler "compositor up"
{
@ -88,7 +140,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut frontend = WaylandFrontend::bind_absolute(&socket_path)?;
// Phase 7.3 : curseur visible dès le démarrage, placé au centre du fb.
frontend.set_cursor_initial_position((fb_w as i32) / 2, (fb_h as i32) / 2);
dlog(&format!("[comp] Wayland socket : {SOCKET_PATH}"));
info!("Wayland socket : {SOCKET_PATH}");
// Exporter WAYLAND_DISPLAY pour les clients lancés par l'OS qui regarderaient
// l'env. (Notre client de test va connecter explicitement au path.)
@ -108,12 +160,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
// 1. Accepter nouveaux clients Wayland
if let Err(e) = frontend.accept_pending_clients() {
dlog(&format!("[comp] accept err: {e}"));
error!("accept err: {e}");
}
// 2. Dispatch des requêtes Wayland en attente
if let Err(e) = frontend.dispatch_clients() {
dlog(&format!("[comp] dispatch err: {e}"));
error!("dispatch err: {e}");
}
// 2.5. Phase 7.6 : nettoyer les surfaces des clients déconnectés.
@ -124,7 +176,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
// 3. Input
if let Ok(events) = input.poll() {
if !events.is_empty() {
dlog(&format!("[comp] {} input events from inputd", events.len()));
tracing::debug!("{} input events from inputd", events.len());
}
for ev in events {
match &ev {
@ -132,13 +184,13 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
scancode, pressed, ..
} if *pressed && *scancode == 0x01 => {
// Esc → exit
dlog("[comp] Esc → exit");
info!("Esc → exit");
let _ = frontend.flush_clients();
let _ = std::fs::remove_file(SOCKET_PATH);
return Ok(());
}
InputEvent::Quit => {
dlog("[comp] Quit reçu");
info!("Quit reçu");
let _ = frontend.flush_clients();
let _ = std::fs::remove_file(SOCKET_PATH);
return Ok(());
@ -164,7 +216,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
// Phase 7.3 : curseur software par-dessus la composition.
frontend.draw_cursor(&mut output);
if let Err(e) = output.present_with_takeover() {
dlog(&format!("[comp] present err: {e}"));
error!("present err: {e}");
}
// 5. Frame callbacks done après le present
@ -174,33 +226,34 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
// 6. Flush vers les clients
if let Err(e) = frontend.flush_clients() {
dlog(&format!("[comp] flush err: {e}"));
error!("flush err: {e}");
}
// Log occasionnel
if tick % 30 == 0 {
dlog(&format!(
"[comp] tick={tick} surfaces={nb} elapsed={:.1}s",
info!(
"tick={tick} surfaces={nb} elapsed={:.1}s",
start.elapsed().as_secs_f32()
));
);
}
thread::sleep(frame_period);
}
dlog("[comp] timeout atteint, exit");
info!("timeout atteint, exit");
let _ = std::fs::remove_file(SOCKET_PATH);
Ok(())
}
fn main() -> ExitCode {
init_tracing();
match run() {
Ok(()) => {
dlog("[comp] PASS");
info!("PASS");
ExitCode::SUCCESS
}
Err(e) => {
dlog(&format!("[comp] FAIL: {e}"));
error!("FAIL: {e}");
ExitCode::FAILURE
}
}