milestone: desktop path Phases 1-5

Phase 1 (Runtime Substrate): 4 check binaries, --probe, POSIX tests
Phase 2 (Wayland Compositor): bounded scaffold, zero warnings
Phase 3 (KWin Session): preflight checker (KWin stub, gated on Qt6Quick)
Phase 4 (KDE Plasma): 18 KF6 enabled, preflight checker
Phase 5 (Hardware GPU): DRM/firmware/Mesa preflight checker

Build: zero warnings, all scripts syntax-clean. Oracle-verified.
This commit is contained in:
2026-04-29 09:54:06 +01:00
parent b23714f542
commit 8acc73d774
508 changed files with 76526 additions and 396 deletions
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "init"
description = "Userspace process launcher"
version = "0.1.0"
edition = "2024"
license = "MIT"
[dependencies]
libc.workspace = true
libredox.workspace = true
plain.workspace = true
redox_syscall.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
config = { path = "../config" }
[lints]
workspace = true
+184
View File
@@ -0,0 +1,184 @@
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::Path;
use std::{env, fs, io};
use libredox::flag::{O_RDONLY, O_WRONLY};
use crate::scheduler::Scheduler;
use crate::unit::{UnitId, UnitStore};
mod scheduler;
mod script;
mod service;
mod unit;
fn switch_stdio(stdio: &str) -> io::Result<()> {
let stdin = libredox::Fd::open(stdio, O_RDONLY, 0)?;
let stdout = libredox::Fd::open(stdio, O_WRONLY, 0)?;
let stderr = libredox::Fd::open(stdio, O_WRONLY, 0)?;
stdin.dup2(0, &[])?;
stdout.dup2(1, &[])?;
stderr.dup2(2, &[])?;
Ok(())
}
struct InitConfig {
log_debug: bool,
skip_cmd: Vec<String>,
envs: BTreeMap<String, OsString>,
}
impl InitConfig {
fn new() -> Self {
let log_level = env::var("INIT_LOG_LEVEL").unwrap_or("INFO".into());
let log_debug = matches!(log_level.as_str(), "DEBUG" | "TRACE");
let skip_cmd: Vec<String> = match env::var("INIT_SKIP") {
Ok(v) if v.len() > 0 => v.split(',').map(|s| s.to_string()).collect(),
_ => Vec::new(),
};
Self {
log_debug,
skip_cmd,
envs: BTreeMap::from([("RUST_BACKTRACE".to_owned(), "1".into())]),
}
}
}
fn switch_root(unit_store: &mut UnitStore, config: &mut InitConfig, prefix: &Path, etcdir: &Path) {
eprintln!(
"init: switchroot to {} {}",
prefix.display(),
etcdir.display()
);
config
.envs
.insert("PATH".to_owned(), prefix.join("bin").into_os_string());
config.envs.insert(
"LD_LIBRARY_PATH".to_owned(),
prefix.join("lib").into_os_string(),
);
unit_store.config_dirs = vec![prefix.join("lib").join("init.d"), etcdir.join("init.d")];
let env_dirs = &[
prefix.join("lib").join("environment.d"),
etcdir.join("environment.d"),
];
match config::config_for_dirs(env_dirs) {
Ok(files) => {
for file in files {
match fs::read_to_string(&file) {
Ok(envs) => {
for env in envs.lines() {
if env.is_empty() || env.starts_with("#") {
continue;
}
let Some((key, value)) = env.split_once('=') else {
eprintln!(
"init: failed to parse env line from {}: {env:?}",
file.display(),
);
continue;
};
config
.envs
.insert(key.to_owned().into(), value.to_owned().into());
}
}
Err(err) => {
eprintln!(
"init: failed to read environment from {}: {err}",
file.display(),
);
}
}
}
}
Err(err) => {
eprintln!(
"init: failed to read environments from {}: {err}",
env_dirs
.iter()
.map(|dir| dir.display().to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
}
}
fn main() {
let mut init_config = InitConfig::new();
let mut unit_store = UnitStore::new();
let mut scheduler = Scheduler::new();
switch_root(
&mut unit_store,
&mut init_config,
Path::new("/scheme/initfs"),
Path::new("/scheme/initfs/etc"),
);
// Start logd first such that we can pass /scheme/log as stdio to all other services
scheduler
.schedule_start_and_report_errors(&mut unit_store, UnitId("00_logd.service".to_owned()));
scheduler.step(&mut unit_store, &mut init_config);
if let Err(err) = switch_stdio("/scheme/log") {
eprintln!("init: failed to switch stdio to '/scheme/log': {err}");
}
let runtime_target = UnitId("00_runtime.target".to_owned());
scheduler.schedule_start_and_report_errors(&mut unit_store, runtime_target.clone());
unit_store.set_runtime_target(runtime_target);
scheduler
.schedule_start_and_report_errors(&mut unit_store, UnitId("90_initfs.target".to_owned()));
scheduler.step(&mut unit_store, &mut init_config);
switch_root(
&mut unit_store,
&mut init_config,
Path::new("/usr"),
Path::new("/etc"),
);
{
// FIXME introduce multi-user.target unit and replace the config dir iteration
// scheduler.schedule_start_and_report_errors(&mut unit_store, UnitId("multi-user.target".to_owned()));
let entries = match config::config_for_dirs(&unit_store.config_dirs) {
Ok(entries) => entries,
Err(err) => {
eprintln!(
"init: failed to read configs from {}: {err}",
unit_store
.config_dirs
.iter()
.map(|dir| dir.display().to_string())
.collect::<Vec<_>>()
.join(", ")
);
return;
}
};
for entry in entries {
scheduler.schedule_start_and_report_errors(
&mut unit_store,
UnitId(entry.file_name().unwrap().to_str().unwrap().to_owned()),
);
}
};
scheduler.step(&mut unit_store, &mut init_config);
libredox::call::setrens(0, 0).expect("init: failed to enter null namespace");
loop {
let mut status = 0;
libredox::call::waitpid(0, &mut status, 0).unwrap();
}
}
+116
View File
@@ -0,0 +1,116 @@
use std::collections::VecDeque;
use crate::InitConfig;
use crate::unit::{Unit, UnitId, UnitKind, UnitStore};
pub struct Scheduler {
pending: VecDeque<Job>,
}
struct Job {
unit: UnitId,
kind: JobKind,
}
enum JobKind {
Start,
}
impl Scheduler {
pub fn new() -> Scheduler {
Scheduler {
pending: VecDeque::new(),
}
}
pub fn schedule_start_and_report_errors(
&mut self,
unit_store: &mut UnitStore,
unit_id: UnitId,
) {
let mut errors = vec![];
self.schedule_start(unit_store, unit_id, &mut errors);
for error in errors {
eprintln!("init: {error}");
}
}
pub fn schedule_start(
&mut self,
unit_store: &mut UnitStore,
unit_id: UnitId,
errors: &mut Vec<String>,
) {
let loaded_units = unit_store.load_units(unit_id.clone(), errors);
for unit_id in loaded_units {
if !unit_store.unit(&unit_id).conditions_met() {
continue;
}
self.pending.push_back(Job {
unit: unit_id,
kind: JobKind::Start,
});
}
}
pub fn step(&mut self, unit_store: &mut UnitStore, init_config: &mut InitConfig) {
'a: loop {
let Some(job) = self.pending.pop_front() else {
return;
};
match job.kind {
JobKind::Start => {
let unit = unit_store.unit_mut(&job.unit);
for dep in &unit.info.requires_weak {
for pending_job in &self.pending {
if &pending_job.unit == dep {
self.pending.push_back(job);
continue 'a;
}
}
}
run(unit, init_config);
}
}
}
}
}
fn run(unit: &mut Unit, config: &mut InitConfig) {
match &unit.kind {
UnitKind::LegacyScript { script } => {
for cmd in script.clone() {
if config.log_debug {
eprintln!("init: running: {cmd:?}");
}
cmd.run(config);
}
}
UnitKind::Service { service } => {
if config.skip_cmd.contains(&service.cmd) {
eprintln!("Skipping '{} {}'", service.cmd, service.args.join(" "));
return;
}
if config.log_debug {
eprintln!(
"Starting {} ({})",
unit.info.description.as_ref().unwrap_or(&unit.id.0),
service.cmd,
);
}
service.spawn(&config.envs);
}
UnitKind::Target {} => {
if config.log_debug {
eprintln!(
"Reached target {}",
unit.info.description.as_ref().unwrap_or(&unit.id.0),
);
}
}
}
}
+163
View File
@@ -0,0 +1,163 @@
use std::collections::BTreeMap;
use std::{env, io, iter};
use crate::InitConfig;
use crate::unit::UnitId;
pub fn subst_env<'a>(arg: &str) -> String {
if arg.starts_with('$') {
env::var(&arg[1..]).unwrap_or(String::new())
} else {
arg.to_owned()
}
}
pub struct Script(pub Vec<Command>, pub Vec<UnitId>);
impl Script {
pub fn from_str(config: &str, errors: &mut Vec<String>) -> io::Result<Script> {
let mut cmds = vec![];
let mut requires_weak = vec![];
for line_raw in config.lines() {
let line = line_raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let args = line.split(' ').map(subst_env);
match Command::parse(args, &mut requires_weak) {
Ok(None) => {}
Ok(Some(cmd)) => cmds.push(cmd),
Err(err) => errors.push(err),
}
}
Ok(Script(cmds, requires_weak))
}
}
#[derive(Clone, Debug)]
pub struct Command {
process: Process,
kind: CommandKind,
}
#[derive(Clone, Debug)]
enum CommandKind {
Oneshot,
OneshotAsync,
}
impl Command {
fn parse(
mut args: impl Iterator<Item = String>,
requires_weak: &mut Vec<UnitId>,
) -> Result<Option<Command>, String> {
let Some(cmd) = args.next() else {
return Ok(None);
};
match cmd.as_str() {
"requires_weak" => {
requires_weak.extend(args.map(UnitId));
Ok(None)
}
"nowait" => {
let process = Process::parse(args)?;
Ok(Some(Command {
process,
kind: CommandKind::OneshotAsync,
}))
}
_ => {
let process = Process::parse(iter::once(cmd).chain(args))?;
Ok(Some(Command {
process,
kind: CommandKind::Oneshot,
}))
}
}
}
pub fn run(&self, config: &mut InitConfig) {
let Command { process, kind } = self;
if config.skip_cmd.contains(&process.cmd) {
eprintln!(
"init: skipping '{} {}'",
process.cmd,
process.args.join(" ")
);
return;
}
let mut command = std::process::Command::new(&process.cmd);
command.args(process.args.iter().map(|arg| subst_env(arg)));
command.env_clear();
command.envs(&config.envs).envs(&process.envs);
let mut child = match command.spawn() {
Ok(child) => child,
Err(err) => {
eprintln!("init: failed to execute {:?}: {}", command, err);
return;
}
};
match kind {
CommandKind::Oneshot => match child.wait() {
Ok(exit_status) => {
if !exit_status.success() {
eprintln!("init: {command:?} failed with {exit_status}");
}
}
Err(err) => {
eprintln!("init: failed to wait for {:?}: {}", command, err)
}
},
CommandKind::OneshotAsync => {}
}
}
}
#[derive(Clone, Debug)]
struct Process {
cmd: String,
args: Vec<String>,
envs: BTreeMap<String, String>,
}
impl Process {
fn parse(parts: impl Iterator<Item = String>) -> Result<Process, String> {
let mut cmd = None;
let mut args = vec![];
let mut envs = BTreeMap::new();
for arg in parts {
if cmd.is_none() {
if let Some((env, value)) = arg.split_once('=') {
let value = if value == "$" {
env::var(env).unwrap_or_default()
} else {
subst_env(value)
};
if !value.is_empty() {
envs.insert(env.to_owned(), value);
}
} else {
cmd = Some(arg);
}
} else {
args.push(arg);
}
}
if let Some(cmd) = cmd {
Ok(Process { cmd, args, envs })
} else {
Err("no command given".to_owned())
}
}
}
+132
View File
@@ -0,0 +1,132 @@
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsString;
use std::io::Read;
use std::os::fd::{AsRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use std::process::Command;
use std::{env, io};
use serde::Deserialize;
use crate::script::subst_env;
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Service {
pub cmd: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub envs: BTreeMap<String, String>,
#[serde(default)]
pub inherit_envs: BTreeSet<String>,
#[serde(rename = "type")]
pub type_: ServiceType,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ServiceType {
#[default]
Notify,
Scheme(String),
Oneshot,
OneshotAsync,
}
impl Service {
pub fn spawn(&self, base_envs: &BTreeMap<String, OsString>) {
let mut command = Command::new(&self.cmd);
command.args(self.args.iter().map(|arg| subst_env(arg)));
command.env_clear();
for env in &self.inherit_envs {
if let Some(value) = env::var_os(env) {
command.env(env, value);
}
}
command.envs(base_envs).envs(&self.envs);
let (mut read_pipe, write_pipe) = io::pipe().unwrap();
unsafe { pass_fd(&mut command, "INIT_NOTIFY", write_pipe.into()) };
let mut child = match command.spawn() {
Ok(child) => child,
Err(err) => {
eprintln!("init: failed to execute {:?}: {}", command, err);
return;
}
};
match &self.type_ {
ServiceType::Notify => match read_pipe.read_exact(&mut [0]) {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
eprintln!("init: {command:?} exited without notifying readiness");
}
Err(err) => {
eprintln!("init: failed to wait for {command:?}: {err}");
}
},
ServiceType::Scheme(scheme) => {
let mut new_fd = usize::MAX;
loop {
match syscall::call_ro(
read_pipe.as_raw_fd() as usize,
unsafe { plain::as_mut_bytes(&mut new_fd) },
syscall::CallFlags::FD | syscall::CallFlags::FD_UPPER,
&[],
) {
Err(syscall::Error {
errno: syscall::EINTR,
}) => continue,
Ok(0) => {
eprintln!("init: {command:?} exited without notifying readiness");
return;
}
Ok(1) => break,
Ok(n) => {
eprintln!("init: incorrect amount of fds {n} returned");
return;
}
Err(err) => {
eprintln!("init: failed to wait for {command:?}: {err}");
return;
}
}
}
let current_namespace_fd = libredox::call::getns().expect("TODO");
libredox::call::register_scheme_to_ns(current_namespace_fd, scheme, new_fd)
.expect("TODO");
}
ServiceType::Oneshot => {
drop(read_pipe);
match child.wait() {
Ok(exit_status) => {
if !exit_status.success() {
eprintln!("init: {command:?} failed with {exit_status}");
}
}
Err(err) => {
eprintln!("init: failed to wait for {:?}: {}", command, err)
}
}
}
ServiceType::OneshotAsync => {}
}
}
}
unsafe fn pass_fd(cmd: &mut Command, env: &str, fd: OwnedFd) {
cmd.env(env, format!("{}", fd.as_raw_fd()));
unsafe {
cmd.pre_exec(move || {
// Pass notify pipe to child
if libc::fcntl(fd.as_raw_fd(), libc::F_SETFD, 0) == -1 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
});
}
}
+251
View File
@@ -0,0 +1,251 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::{fs, io};
use serde::Deserialize;
use crate::script::{Command, Script};
use crate::service::Service;
pub struct UnitStore {
pub config_dirs: Vec<PathBuf>,
units: BTreeMap<UnitId, Unit>,
runtime_target: Option<UnitId>,
}
impl UnitStore {
pub fn new() -> Self {
UnitStore {
config_dirs: vec![],
units: BTreeMap::new(),
runtime_target: None,
}
}
pub fn set_runtime_target(&mut self, unit_id: UnitId) {
assert!(self.runtime_target.is_none());
assert!(self.units.contains_key(&unit_id));
self.runtime_target = Some(unit_id);
}
fn load_single_unit(&mut self, unit_id: UnitId, errors: &mut Vec<String>) -> Option<UnitId> {
let (filename, instance) = if let Some((base_service, rest)) = unit_id.0.split_once('@') {
let Some((instance, ext)) = rest.rsplit_once('.') else {
errors.push(format!("script {} can't be instanced", unit_id.0));
return None;
};
(format!("{base_service}@.{ext}"), Some(instance))
} else {
(unit_id.0.clone(), None)
};
let Some(path) = self
.config_dirs
.iter()
.rev()
.map(|dir| dir.join(&filename))
.find(|path| path.exists())
else {
errors.push(format!("unit {} not found", unit_id.0));
return None;
};
let mut unit = match Unit::from_file(unit_id.clone(), &path, instance, errors) {
Ok(unit) => unit,
Err(err) => {
errors.push(format!("{}: {err}", path.display()));
return None;
}
};
if unit.info.default_dependencies {
if let Some(runtime_target) = self.runtime_target.clone() {
unit.info.requires_weak.push(runtime_target);
} else {
errors.push(format!(
"{}: dependency of the runtime target must have default dependencies disabled",
path.display(),
));
}
}
self.units.insert(unit_id.clone(), unit);
Some(unit_id)
}
pub fn load_units(&mut self, root_unit: UnitId, errors: &mut Vec<String>) -> Vec<UnitId> {
let mut loaded_units = vec![];
let mut pending_units = vec![root_unit];
while let Some(unit_id) = pending_units.pop() {
if self.units.contains_key(&unit_id) {
continue;
}
let unit = self.load_single_unit(unit_id, errors);
if let Some(unit) = unit {
loaded_units.push(unit.clone());
for dep in &self.unit(&unit).info.requires_weak {
pending_units.push(dep.clone());
}
}
}
loaded_units
}
pub fn unit(&self, unit: &UnitId) -> &Unit {
self.units.get(unit).unwrap()
}
pub fn unit_mut(&mut self, unit: &UnitId) -> &mut Unit {
self.units.get_mut(unit).unwrap()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
#[serde(transparent)]
pub struct UnitId(pub String);
pub struct Unit {
pub id: UnitId,
pub info: UnitInfo,
pub kind: UnitKind,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UnitInfo {
pub description: Option<String>,
#[serde(default = "true_bool")]
pub default_dependencies: bool,
#[serde(default)]
pub requires_weak: Vec<UnitId>,
pub condition_architecture: Option<Vec<String>>,
// FIXME replace this with hwd reading from the devicetree
pub condition_board: Option<Vec<String>>,
}
fn true_bool() -> bool {
true
}
pub enum UnitKind {
LegacyScript { script: Vec<Command> },
Service { service: Service },
Target {},
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SerializedService {
unit: UnitInfo,
service: Service,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SerializedTarget {
unit: UnitInfo,
}
fn instance_toml(value: toml::Value, instance: &str) -> toml::Value {
match value {
toml::Value::Integer(_)
| toml::Value::Float(_)
| toml::Value::Boolean(_)
| toml::Value::Datetime(_) => value,
toml::Value::String(s) => toml::Value::String(s.replace("$INSTANCE", instance)),
toml::Value::Array(values) => toml::Value::Array(
values
.into_iter()
.map(|value| instance_toml(value, instance))
.collect(),
),
toml::Value::Table(map) => toml::Value::Table(
map.into_iter()
.map(|(key, value)| (key, instance_toml(value, instance)))
.collect(),
),
}
}
impl Unit {
pub fn from_file(
id: UnitId,
config_path: &Path,
instance: Option<&str>,
errors: &mut Vec<String>,
) -> io::Result<Self> {
let config = fs::read_to_string(config_path)?;
let Some(ext) = config_path.extension().map(|ext| ext.to_str().unwrap()) else {
let script = Script::from_str(&config, errors)?;
return Ok(Unit {
id,
info: UnitInfo {
description: None,
default_dependencies: true,
requires_weak: script.1,
condition_architecture: None,
condition_board: None,
},
kind: UnitKind::LegacyScript { script: script.0 },
});
};
let toml_value: toml::Value = toml::from_str(&config)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
let toml_value = if let Some(instance) = instance {
instance_toml(toml_value, instance)
} else {
toml_value
};
let (info, kind) = match ext {
"service" => {
let service: SerializedService = toml_value
.try_into()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
(
service.unit,
UnitKind::Service {
service: service.service,
},
)
}
"target" => {
let target: SerializedTarget = toml_value
.try_into()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
(target.unit, UnitKind::Target {})
}
_ => return Err(io::Error::other("invalid file extension")),
};
Ok(Unit { id, info, kind })
}
pub fn conditions_met(&self) -> bool {
if let Some(condition_architecture) = &self.info.condition_architecture {
if !condition_architecture
.iter()
.any(|arch| arch == std::env::consts::ARCH)
{
return false;
}
}
if let Some(condition_board) = &self.info.condition_board {
if !condition_board
.iter()
.any(|board| Some(&**board) == option_env!("BOARD"))
{
return false;
}
}
true
}
}