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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user