Add config loading and task executor initialization
parent
3f85c87202
commit
618c1ddf9f
@ -1,4 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod distro_config;
|
||||
mod os_config;
|
||||
|
||||
pub use os_config::*;
|
||||
|
@ -0,0 +1,78 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tokio::fs;
|
||||
use valico::json_schema::Scope;
|
||||
|
||||
use crate::{
|
||||
distro::distro_config::DistroConfig,
|
||||
error::{AppError, AppResult},
|
||||
utils::CFG_PATH,
|
||||
};
|
||||
|
||||
use super::OSConfig;
|
||||
|
||||
pub struct OSConfigLoader<'a> {
|
||||
distro_cfg: &'a DistroConfig,
|
||||
cfg_path: PathBuf,
|
||||
}
|
||||
|
||||
impl<'a> OSConfigLoader<'a> {
|
||||
pub fn new(path: PathBuf, distro_cfg: &'a DistroConfig) -> Self {
|
||||
Self {
|
||||
distro_cfg,
|
||||
cfg_path: path,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load(&self) -> AppResult<OSConfig> {
|
||||
let schema = self.load_extension_schema().await?;
|
||||
let os_config = OSConfig::load(&self.cfg_path).await?;
|
||||
Self::validate_config(schema, &os_config)?;
|
||||
|
||||
Ok(os_config)
|
||||
}
|
||||
|
||||
async fn load_extension_schema(&self) -> AppResult<serde_json::Value> {
|
||||
let schema_path = self
|
||||
.distro_cfg
|
||||
.config
|
||||
.schema
|
||||
.as_ref()
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| CFG_PATH.join("config.schema.json"));
|
||||
let contents = fs::read_to_string(schema_path).await?;
|
||||
let schema = serde_json::from_str(&contents)?;
|
||||
|
||||
Ok(schema)
|
||||
}
|
||||
|
||||
fn validate_config(schema: serde_json::Value, config: &OSConfig) -> AppResult<()> {
|
||||
let mut scope = Scope::new();
|
||||
let schema = scope.compile_and_return(schema, true)?;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for (key, value) in config.extended.iter() {
|
||||
let result = schema.validate_in(value, key);
|
||||
|
||||
for error in result.errors {
|
||||
tracing::error!(
|
||||
"ConfigError: {} ({}) at {}",
|
||||
error.get_title(),
|
||||
error.get_code(),
|
||||
error.get_path(),
|
||||
);
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
let msg = errors
|
||||
.into_iter()
|
||||
.map(|e| format!("{} ({}) at {}", e.get_title(), e.get_code(), e.get_path()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
Err(AppError::InvalidConfig(msg))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,128 +1,21 @@
|
||||
use distro::OSConfig;
|
||||
use error::{AppError, AppResult};
|
||||
use scripting::{
|
||||
loader::ScriptLoader,
|
||||
script::{NuScript, Script},
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
pub(crate) mod scripting;
|
||||
|
||||
pub(crate) mod utils;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use distro::{distro_config::DistroConfig, loader::OSConfigLoader};
|
||||
use error::AppResult;
|
||||
use task::task_executor::TaskExecutor;
|
||||
pub use utils::generate_script_files;
|
||||
pub mod distro;
|
||||
pub mod task;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
|
||||
pub enum TaskOperation {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
pub struct TaskExecutor {
|
||||
config: Option<OSConfig>,
|
||||
loader: ScriptLoader,
|
||||
}
|
||||
|
||||
macro_rules! task_executors {
|
||||
($($function:ident => $task:ident),+) => {
|
||||
$(
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn $function(&self, operation: TaskOperation, cfg: <$task as crate::tasks::Task>::Config) -> AppResult<()> {
|
||||
match operation {
|
||||
TaskOperation::Up => self.execute_task::<<$task as crate::tasks::Task>::UpScript>(cfg).await,
|
||||
TaskOperation::Down => self.execute_task::<<$task as crate::tasks::Task>::DownScript>(cfg).await,
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskExecutor {
|
||||
/// Creates a new task executor with a given config
|
||||
pub fn with_config(config: OSConfig) -> Self {
|
||||
Self {
|
||||
config: Some(config),
|
||||
loader: ScriptLoader::new(),
|
||||
}
|
||||
}
|
||||
|
||||
task_executors!(
|
||||
setup_users => SetupUsersTask,
|
||||
configure_network => ConfigureNetworkTask,
|
||||
configure_unakite => ConfigureUnakiteTask,
|
||||
create_partitions => CreatePartitionsTask,
|
||||
install_base => InstallBaseTask,
|
||||
install_bootloader => InstallBootloaderTask,
|
||||
install_desktop => InstallDesktopTask,
|
||||
install_extra_packages => InstallExtraPackagesTask,
|
||||
install_flatpak => InstallFlatpakTask,
|
||||
install_kernels => InstallKernelsTask,
|
||||
install_timeshift => InstallTimeshiftTask,
|
||||
install_zramd => InstallZRamDTask,
|
||||
setup_root_user => SetupRootUserTask,
|
||||
configure_locale => ConfigureLocaleTask
|
||||
);
|
||||
|
||||
/// Installs the system from the given system configuration
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn install_from_config(&self) -> AppResult<()> {
|
||||
let config = self.config.clone().ok_or(AppError::MissingConfig)?;
|
||||
let base_config = config.base;
|
||||
|
||||
self.create_partitions(TaskOperation::Up, base_config.partitions)
|
||||
.await?;
|
||||
self.install_base(TaskOperation::Up, ()).await?;
|
||||
self.install_kernels(TaskOperation::Up, base_config.kernels)
|
||||
.await?;
|
||||
self.install_bootloader(TaskOperation::Up, base_config.bootloader)
|
||||
.await?;
|
||||
self.configure_locale(TaskOperation::Up, base_config.locale)
|
||||
.await?;
|
||||
self.configure_network(TaskOperation::Up, base_config.network)
|
||||
.await?;
|
||||
|
||||
if base_config.enable_zramd {
|
||||
self.install_zramd(TaskOperation::Up, ()).await?;
|
||||
}
|
||||
if base_config.enable_timeshift {
|
||||
self.install_timeshift(TaskOperation::Up, ()).await?;
|
||||
}
|
||||
if base_config.enable_flatpak {
|
||||
self.install_flatpak(TaskOperation::Up, ()).await?;
|
||||
}
|
||||
self.setup_users(TaskOperation::Up, base_config.users)
|
||||
.await?;
|
||||
self.setup_root_user(TaskOperation::Up, base_config.root_user)
|
||||
.await?;
|
||||
self.install_desktop(TaskOperation::Up, base_config.desktop)
|
||||
.await?;
|
||||
self.install_extra_packages(TaskOperation::Up, base_config.extra_packages)
|
||||
.await?;
|
||||
|
||||
if let Some(unakite) = base_config.unakite {
|
||||
self.configure_unakite(TaskOperation::Up, unakite).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_task<S: Script>(&self, args: S::Args) -> AppResult<()> {
|
||||
let script = self.loader.load::<S>()?;
|
||||
self.execute(script, args.clone()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
/// Creates a new executor with the given os config for the current distro
|
||||
pub async fn create_executor(os_cfg_path: PathBuf) -> AppResult<TaskExecutor> {
|
||||
let distro_config = DistroConfig::load().await?;
|
||||
let os_config = OSConfigLoader::new(os_cfg_path, &distro_config)
|
||||
.load()
|
||||
.await?;
|
||||
|
||||
#[inline]
|
||||
async fn execute<S: Script>(&self, mut script: NuScript<S>, args: S::Args) -> AppResult<()> {
|
||||
if let Some(cfg) = self.config.as_ref() {
|
||||
script.set_global_var("TRM_CONFIG", cfg.to_owned())
|
||||
} else {
|
||||
script.set_global_var("TRM_CONFIG", OSConfig::empty())
|
||||
}
|
||||
.set_global_var("TRM_VERSION", env!("CARGO_PKG_VERSION"))
|
||||
.execute(args)
|
||||
.await
|
||||
}
|
||||
Ok(TaskExecutor::new(os_config, distro_config))
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
|
||||
use super::script::{NuScript, Script};
|
||||
|
||||
/// A loader for nu script files
|
||||
pub struct ScriptLoader {
|
||||
script_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ScriptLoader {
|
||||
/// Creates a new script loader with the default config dir
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
script_dir: crate::utils::CFG_PATH.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the given script file
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn load<S: Script>(&self) -> AppResult<NuScript<S>> {
|
||||
let script_path = self.script_dir.join(S::name());
|
||||
|
||||
if !script_path.exists() {
|
||||
Err(AppError::ScriptNotFound(script_path))
|
||||
} else {
|
||||
Ok(NuScript::new(script_path))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
pub mod loader;
|
||||
pub mod script;
|
@ -1,84 +0,0 @@
|
||||
use core::fmt;
|
||||
use std::{collections::HashMap, marker::PhantomData, path::PathBuf};
|
||||
|
||||
use embed_nu::{
|
||||
rusty_value::RustyValue, Argument, CommandGroupConfig, ContextBuilder, IntoArgument, IntoValue,
|
||||
RawValue, Value,
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
|
||||
/// A trait implemented for a given nu script type to
|
||||
/// associate arguments
|
||||
pub trait Script {
|
||||
type Args: ScriptArgs + fmt::Debug + Clone;
|
||||
|
||||
/// Returns the (expected) name of the script file
|
||||
/// This function is used by the loader to load the associated file
|
||||
/// The name needs to include the file extension
|
||||
fn name() -> &'static str;
|
||||
}
|
||||
|
||||
/// Script arguments that can be collected in a Vec to
|
||||
/// be passed to the script
|
||||
pub trait ScriptArgs: RustyValue {
|
||||
fn get_args(self) -> Vec<Argument>;
|
||||
}
|
||||
|
||||
impl<T: RustyValue> ScriptArgs for T {
|
||||
fn get_args(self) -> Vec<Argument> {
|
||||
match self.into_value() {
|
||||
Value::List { vals, .. } => vals
|
||||
.into_iter()
|
||||
.map(|v| RawValue(v).into_argument())
|
||||
.collect(),
|
||||
val => vec![RawValue(val).into_argument()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A nu script instance that can be executed
|
||||
pub struct NuScript<S: Script> {
|
||||
path: PathBuf,
|
||||
vars: HashMap<String, Value>,
|
||||
__phantom: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S: Script> NuScript<S> {
|
||||
pub(crate) fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
path,
|
||||
vars: HashMap::new(),
|
||||
__phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a global variable
|
||||
pub fn set_global_var<S1: ToString, V: IntoValue>(&mut self, key: S1, value: V) -> &mut Self {
|
||||
self.vars.insert(key.to_string(), value.into_value());
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Executes the script with the given args
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn execute(&self, args: S::Args) -> AppResult<()> {
|
||||
let mut ctx = ContextBuilder::default()
|
||||
.with_command_groups(CommandGroupConfig::default().all_groups(true))?
|
||||
.add_script(self.read_file().await?)?
|
||||
.build()?;
|
||||
|
||||
if ctx.has_fn("main") {
|
||||
let pipeline = ctx.call_fn("main", args.get_args())?;
|
||||
ctx.print_pipeline(pipeline)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::MissingMain(self.path.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_file(&self) -> AppResult<String> {
|
||||
fs::read_to_string(&self.path).await.map_err(AppError::from)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue