Add config loading and task executor initialization

config-extension
trivernis 2 years ago
parent 3f85c87202
commit 618c1ddf9f
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -1,11 +1,14 @@
use std::{collections::HashMap, path::PathBuf};
use serde::Deserialize;
use tokio::fs;
use crate::{error::AppResult, utils::CFG_PATH};
/// The config file of a distro that defines
/// how that distro should be installed
#[derive(Clone, Debug, Deserialize)]
pub struct Config {
pub struct DistroConfig {
/// Metadata about the distro
pub distro: DistroMetadata,
@ -36,9 +39,19 @@ pub struct OSConfigMetadata {
#[derive(Clone, Debug, Deserialize)]
pub struct TaskConfig {
/// The name of the config field
pub config_field: String,
pub config_key: String,
/// If the task should be skipped if the
/// config value of that task is null
pub skip_on_null_config: bool,
pub skip_on_false: bool,
}
impl DistroConfig {
pub async fn load() -> AppResult<Self> {
let path = CFG_PATH.join("distro.toml");
let contents = fs::read_to_string(path).await?;
let cfg = serde_json::from_str::<Self>(&contents)?;
Ok(cfg)
}
}

@ -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,7 +1,8 @@
mod base_config;
use std::collections::HashMap;
use std::{collections::HashMap, path::Path};
pub use base_config::BaseConfig;
pub mod loader;
use embed_nu::{
rusty_value::{
Fields, Float, HashablePrimitive, HashableValue, Integer, Primitive, RustyValue, Struct,
@ -10,6 +11,7 @@ use embed_nu::{
RustyIntoValue,
};
use serde::Deserialize;
use tokio::fs;
use crate::error::{AppError, AppResult};
@ -25,7 +27,7 @@ pub struct OSConfig {
impl OSConfig {
pub fn get_nu_value<K: AsRef<str>>(&self, key: K) -> AppResult<embed_nu::Value> {
let value = self.into_rusty_value();
let value = self.clone().into_rusty_value();
let mut fields = if let Value::Struct(Struct { fields, .. }) = value {
if let Fields::Named(named) = fields {
named
@ -110,3 +112,12 @@ fn json_to_rusty_value(val: serde_json::Value) -> embed_nu::rusty_value::Value {
}
}
}
impl OSConfig {
pub(crate) async fn load(path: &Path) -> AppResult<Self> {
let contents = fs::read_to_string(path).await?;
let cfg = serde_json::from_str::<Self>(&contents)?;
Ok(cfg)
}
}

@ -24,9 +24,18 @@ pub enum AppError {
#[error("Missing config key {0}")]
MissingConfigKey(String),
#[error("The os config is invalid: {0}")]
InvalidConfig(String),
#[error("IO Error: {0}")]
Io(#[from] io::Error),
#[error("JSON deserialization error {0}")]
JSON(#[from] serde_json::Error),
#[error("The task has been skipped")]
Skipped,
#[error("Failed to process config schema: {0}")]
ConfigSchema(#[from] valico::json_schema::SchemaError),
}

@ -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,11 +1,8 @@
use args::{Args, Command, GenerateEmptyConfigArgs, GenerateScriptsArgs, InstallFromConfigArgs};
use clap::Parser;
use rusty_value::into_json::{EnumRepr, IntoJson, IntoJsonOptions};
use tokio::{
fs::{self, OpenOptions},
io::AsyncReadExt,
};
use tourmaline::{distro::OSConfig, error::AppResult, generate_script_files, TaskExecutor};
use tokio::fs;
use tourmaline::{distro::OSConfig, error::AppResult, generate_script_files};
mod args;
@ -23,14 +20,13 @@ async fn main() {
.unwrap();
}
/// Installs the distro from a given configuration file
async fn install_from_config(args: InstallFromConfigArgs) -> AppResult<()> {
let mut file = OpenOptions::new().read(true).open(args.path).await?;
let mut cfg_contents = String::new();
file.read_to_string(&mut cfg_contents).await?;
let config: OSConfig = serde_json::from_str(&cfg_contents)?;
TaskExecutor::with_config(config)
.install_from_config()
tourmaline::create_executor(args.path)
.await?
.with_base_tasks()
.with_custom_tasks()
.execute()
.await
}

@ -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)
}
}

@ -2,7 +2,7 @@ use std::path::PathBuf;
use crate::{distro::OSConfig, error::AppResult};
use super::{exec_builder::ExecBuilder, Task, TaskTrait};
use super::{exec_builder::ExecBuilder, TaskTrait};
use embed_nu::IntoValue;
use lazy_static::lazy_static;
@ -19,84 +19,104 @@ pub enum BaseTask {
SetupUsers,
}
impl BaseTask {
fn config_key(&self) -> Option<&'static str> {
let field = match self {
BaseTask::ConfigureLocale => "locale",
BaseTask::ConfigureNetwork => "network",
BaseTask::CreatePartitions => "partitions",
BaseTask::InstallBootloader => "bootloader",
BaseTask::InstallDesktop => "desktop",
BaseTask::InstallExtraPackages => "extra_packages",
BaseTask::SetupRootUser => "root_user",
BaseTask::SetupUsers => "users",
_ => return None,
};
Some(field)
}
#[derive(Clone, Copy)]
pub struct BaseTaskKeydata {
pub task_name: &'static str,
pub config_key: Option<&'static str>,
}
fn task_name(&self) -> &'static str {
impl BaseTask {
pub fn key_data(&self) -> BaseTaskKeydata {
match self {
BaseTask::ConfigureLocale => "configure-locale",
BaseTask::ConfigureNetwork => "configure-network",
BaseTask::CreatePartitions => "create-partitions",
BaseTask::InstallBase => "install-base",
BaseTask::InstallBootloader => "install-bootloader",
BaseTask::InstallDesktop => "install-desktop",
BaseTask::InstallExtraPackages => "install-extra-packages",
BaseTask::SetupRootUser => "setup-root-user",
BaseTask::SetupUsers => "setup-users",
BaseTask::ConfigureLocale => BaseTaskKeydata {
task_name: "configure-locale",
config_key: Some("locale"),
},
BaseTask::ConfigureNetwork => BaseTaskKeydata {
task_name: "configure-network",
config_key: Some("network"),
},
BaseTask::CreatePartitions => BaseTaskKeydata {
task_name: "create-partitions",
config_key: Some("partitions"),
},
BaseTask::InstallBase => BaseTaskKeydata {
task_name: "install-base",
config_key: None,
},
BaseTask::InstallBootloader => BaseTaskKeydata {
task_name: "install-bootloader",
config_key: Some("bootloader"),
},
BaseTask::InstallDesktop => BaseTaskKeydata {
task_name: "install-desktop",
config_key: Some("desktop"),
},
BaseTask::InstallExtraPackages => BaseTaskKeydata {
task_name: "install-extra-packages",
config_key: Some("extra_packages"),
},
BaseTask::SetupRootUser => BaseTaskKeydata {
task_name: "setup-root-user",
config_key: Some("root_user"),
},
BaseTask::SetupUsers => BaseTaskKeydata {
task_name: "setup-users",
config_key: Some("users"),
},
}
}
}
impl TaskTrait for BaseTask {
fn up(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
let script = PathBuf::from(self.task_name()).join("up.nu");
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
let key_data = self.key_data();
let script = PathBuf::from(key_data.task_name).join("up.nu");
let task_config = if let Some(key) = self.config_key() {
let task_config = if let Some(key) = key_data.config_key {
config.get_nu_value(key)?
} else {
Option::<()>::None.into_value()
};
Ok(ExecBuilder {
Ok(Some(ExecBuilder {
script,
os_config: config.to_owned(),
task_config,
})
}))
}
fn down(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
let script = PathBuf::from(self.task_name()).join("down.nu");
let task_config = if let Some(key) = self.config_key() {
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
let key_data = self.key_data();
let script = PathBuf::from(key_data.task_name).join("down.nu");
let task_config = if let Some(key) = key_data.config_key {
config.get_nu_value(key)?
} else {
Option::<()>::None.into_value()
};
Ok(ExecBuilder {
Ok(Some(ExecBuilder {
script,
os_config: config.to_owned(),
task_config,
})
}))
}
}
lazy_static! {
pub static ref ALL_BASE_TASKS: Vec<Task> = get_all_base_tasks();
pub static ref ALL_BASE_TASKS: Vec<BaseTask> = get_all_base_tasks();
}
fn get_all_base_tasks() -> Vec<Task> {
fn get_all_base_tasks() -> Vec<BaseTask> {
vec![
Task::Base(BaseTask::ConfigureLocale),
Task::Base(BaseTask::ConfigureNetwork),
Task::Base(BaseTask::CreatePartitions),
Task::Base(BaseTask::InstallBase),
Task::Base(BaseTask::InstallBootloader),
Task::Base(BaseTask::InstallDesktop),
Task::Base(BaseTask::InstallExtraPackages),
Task::Base(BaseTask::SetupRootUser),
Task::Base(BaseTask::SetupUsers),
BaseTask::ConfigureLocale,
BaseTask::ConfigureNetwork,
BaseTask::CreatePartitions,
BaseTask::InstallBase,
BaseTask::InstallBootloader,
BaseTask::InstallDesktop,
BaseTask::InstallExtraPackages,
BaseTask::SetupRootUser,
BaseTask::SetupUsers,
]
}

@ -9,37 +9,47 @@ pub struct CustomTask {
config_key: String,
up_script: PathBuf,
down_script: PathBuf,
skip_on_false: bool,
}
impl CustomTask {
pub fn from_name_and_key(name: String, config_key: String) -> Self {
pub fn from_name_and_key(name: String, config_key: String, skip_on_false: bool) -> Self {
let base_path = PathBuf::from(name);
Self {
config_key,
up_script: base_path.join("up.nu"),
down_script: base_path.join("down.nu"),
skip_on_false,
}
}
}
impl TaskTrait for CustomTask {
fn up(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
let task_config = config.get_nu_value(&self.config_key)?;
Ok(ExecBuilder {
script: self.up_script.to_owned(),
os_config: config.to_owned(),
task_config,
})
if self.skip_on_false && task_config.is_nothing() {
Ok(None)
} else {
Ok(Some(ExecBuilder {
script: self.up_script.to_owned(),
os_config: config.to_owned(),
task_config,
}))
}
}
fn down(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
let task_config = config.get_nu_value(&self.config_key)?;
Ok(ExecBuilder {
script: self.down_script.to_owned(),
os_config: config.to_owned(),
task_config,
})
if self.skip_on_false && task_config.is_nothing() {
Ok(None)
} else {
Ok(Some(ExecBuilder {
script: self.down_script.to_owned(),
os_config: config.to_owned(),
task_config,
}))
}
}
}

@ -7,8 +7,8 @@ pub mod exec_builder;
pub mod task_executor;
pub trait TaskTrait {
fn up(&self, config: &OSConfig) -> AppResult<ExecBuilder>;
fn down(&self, config: &OSConfig) -> AppResult<ExecBuilder>;
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>>;
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>>;
}
#[derive(Clone, Debug)]
@ -19,7 +19,7 @@ pub enum Task {
impl TaskTrait for Task {
#[inline]
fn up(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
fn up(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
match self {
Task::Base(b) => b.up(config),
Task::Custom(c) => c.up(config),
@ -27,7 +27,7 @@ impl TaskTrait for Task {
}
#[inline]
fn down(&self, config: &OSConfig) -> AppResult<ExecBuilder> {
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
match self {
Task::Base(b) => b.down(config),
Task::Custom(c) => c.down(config),

@ -1,20 +1,21 @@
use crate::{
distro::{config::Config, OSConfig},
distro::{distro_config::DistroConfig, OSConfig},
error::AppResult,
};
use super::{base_task::ALL_BASE_TASKS, custom_task::CustomTask, Task, TaskTrait};
pub struct TaskExecutor {
config: Config,
distro_config: DistroConfig,
os_config: OSConfig,
tasks: Vec<Task>,
}
impl TaskExecutor {
pub fn new(os_config: OSConfig, config: Config) -> Self {
/// Creates a new task executor with the given OSConfig and Distro Config
pub fn new(os_config: OSConfig, distro_config: DistroConfig) -> Self {
Self {
config,
distro_config,
os_config,
tasks: Vec::new(),
}
@ -22,7 +23,11 @@ impl TaskExecutor {
/// Adds all base tasks to the executor
pub fn with_base_tasks(&mut self) -> &mut Self {
let mut base_tasks = (*ALL_BASE_TASKS).clone();
let mut base_tasks = (*ALL_BASE_TASKS)
.iter()
.cloned()
.map(Task::Base)
.collect::<Vec<_>>();
self.tasks.append(&mut base_tasks);
self
@ -31,11 +36,15 @@ impl TaskExecutor {
/// Adds all custom tasks to the executor
pub fn with_custom_tasks(&mut self) -> &mut Self {
let mut custom_tasks = self
.config
.distro_config
.tasks
.iter()
.map(|(name, task)| {
CustomTask::from_name_and_key(name.to_owned(), task.config_field.to_owned())
CustomTask::from_name_and_key(
name.to_owned(),
task.config_key.to_owned(),
task.skip_on_false,
)
})
.map(Task::Custom)
.collect::<Vec<_>>();
@ -47,7 +56,9 @@ impl TaskExecutor {
/// Executes all tasks
pub async fn execute(&mut self) -> AppResult<()> {
for task in &self.tasks {
task.up(&self.os_config)?.exec().await?;
if let Some(up_task) = task.up(&self.os_config)? {
up_task.exec().await?
}
}
Ok(())

@ -16,14 +16,15 @@ pub async fn generate_script_files<P: AsRef<Path>>(output: P) -> AppResult<()> {
let output = output.as_ref();
for task in &*ALL_BASE_TASKS {
task.up()
let script_dir = output.join(&name);
let key_data = task.key_data();
let name = key_data.task_name;
let script_dir = output.join(name);
if !script_dir.exists() {
fs::create_dir_all(&script_dir).await?;
}
let up_path = output.join(&up_script);
let down_path = output.join(down_script);
let up_path = output.join("up.nu");
let down_path = output.join("down.nu");
fs::write(
&up_path,

Loading…
Cancel
Save