diff --git a/.gitignore b/.gitignore index 92b4cd8..78da254 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk .env + +# Misc +test-config.json diff --git a/Cargo.lock b/Cargo.lock index 6853ea1..4514d4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,17 @@ dependencies = [ "regex", ] +[[package]] +name = "addr" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936697e9caf938eb2905036100edf8e1269da8291f8a02f5fe7b37073784eec0" +dependencies = [ + "no-std-net", + "psl", + "psl-types", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -33,7 +44,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.7", "once_cell", "version_check", ] @@ -265,9 +276,9 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a621d5d6d6c8d086dbaf1fe659981da41a1b63c6bdbba30b4dbb592c6d3bd49" +checksum = "aa0e3586af56b3bfa51fca452bd56e8dbbbd5d8d81cbf0b7e4e35b695b537eb8" dependencies = [ "serde", "toml", @@ -334,7 +345,7 @@ checksum = "29c39203181991a7dd4343b8005bd804e7a9a37afb8ac070e43771e8c820bbde" dependencies = [ "chrono", "chrono-tz-build", - "phf", + "phf 0.11.1", ] [[package]] @@ -344,8 +355,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f509c3a87b33437b05e2458750a0700e5bdd6956176773e6c7d6dd15a283a0c" dependencies = [ "parse-zoneinfo", - "phf", - "phf_codegen", + "phf 0.11.1", + "phf_codegen 0.11.1", ] [[package]] @@ -361,9 +372,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.14" +version = "4.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea54a38e4bce14ff6931c72e5b3c43da7051df056913d4e7e1fcdb1c03df69d" +checksum = "06badb543e734a2d6568e19a40af66ed5364360b9226184926f89d229b4b4267" dependencies = [ "atty", "bitflags", @@ -756,9 +767,9 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "embed-nu" -version = "0.3.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf2e40b13c4f1e3cd0d5494d8a225c3d5064f1c83ff373b04e783b0df7b1c23" +checksum = "9fd30e83a59e9eeb1931ae5b43d34aeea424d9c1bd88bb79e65bdec1e0676642" dependencies = [ "nu-command", "nu-engine", @@ -1045,6 +1056,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.7" @@ -1420,6 +1442,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-pointer" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe841b94e719a482213cee19dd04927cf412f26d8dc84c5a446c081e49c2997" +dependencies = [ + "serde_json", +] + +[[package]] +name = "jsonway" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effcb749443c905fbaef49d214f8b1049c240e0adb7af9baa0e201e625e4f9de" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1735,6 +1776,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "no-std-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bcece43b12349917e096cddfa66107277f123e6c96a5aea78711dc601a47152" +dependencies = [ + "serde", +] + [[package]] name = "nom" version = "1.2.4" @@ -1880,7 +1930,7 @@ dependencies = [ "pathdiff", "powierza-coefficient", "quick-xml 0.23.1", - "rand", + "rand 0.8.5", "rayon", "reedline", "reqwest", @@ -1903,7 +1953,7 @@ dependencies = [ "unicode-segmentation", "url", "users 0.11.0", - "uuid", + "uuid 1.2.1", "wax", "windows", ] @@ -1977,7 +2027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa9667e6ddf13e01a5f192caa8570b19f062dfe1fa70af6f28b2ab56c369d57" dependencies = [ "nu-ansi-term", - "rand", + "rand 0.8.5", ] [[package]] @@ -2337,13 +2387,32 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + [[package]] name = "phf" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" dependencies = [ - "phf_shared", + "phf_shared 0.11.1", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", ] [[package]] @@ -2352,8 +2421,18 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.1", + "phf_shared 0.11.1", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", ] [[package]] @@ -2362,8 +2441,17 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" dependencies = [ - "phf_shared", - "rand", + "phf_shared 0.11.1", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", ] [[package]] @@ -2463,6 +2551,21 @@ dependencies = [ "rustix", ] +[[package]] +name = "psl" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07242622d9f4b9c1a6fe9c2691cf18ee7d34400a5eed2e1668c756bfaea93fb3" +dependencies = [ + "psl-types", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "pure-rust-locales" version = "0.5.6" @@ -2507,6 +2610,20 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + [[package]] name = "rand" version = "0.8.5" @@ -2514,8 +2631,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -2525,7 +2652,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -2534,7 +2670,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.7", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -2576,7 +2730,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", + "getrandom 0.2.7", "redox_syscall", "thiserror", ] @@ -2780,11 +2934,12 @@ checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "rusty-value" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dca1efc1cbe6434e3ed1afb6ce7bb3152af7a0f568bb5f5ea316388e333c3e" +checksum = "eb7ccab1b447c1ba376a608ef1fd25e3b7872e9162a1147274c263e7675a7c98" dependencies = [ "rusty-value-derive", + "serde_json", ] [[package]] @@ -3363,12 +3518,15 @@ dependencies = [ "embed-nu", "lazy_static", "paste", + "rusty-value", "serde", "serde_json", "thiserror", "tokio", + "toml", "tracing", "tracing-subscriber", + "valico", ] [[package]] @@ -3551,6 +3709,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68" +[[package]] +name = "uritemplate-next" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcde98d1fc3f528255b1ecb22fb688ee0d23deb672a8c57127df10b98b4bd18c" +dependencies = [ + "regex", +] + [[package]] name = "url" version = "2.3.1" @@ -3594,13 +3761,44 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.7", +] + [[package]] name = "uuid" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" dependencies = [ - "getrandom", + "getrandom 0.2.7", +] + +[[package]] +name = "valico" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647856408e327686b6640397d19f60fac3e64c3bfaa6afc409da63ef7da45edb" +dependencies = [ + "addr", + "base64", + "chrono", + "json-pointer", + "jsonway", + "percent-encoding", + "phf 0.8.0", + "phf_codegen 0.8.0", + "regex", + "serde", + "serde_json", + "uritemplate-next", + "url", + "uuid 0.8.2", ] [[package]] @@ -3675,6 +3873,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 3022824..4b02839 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,19 +16,22 @@ path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "4.0.14", features = ["derive"] } +clap = { version = "4.0.17", features = ["derive"] } color-eyre = "0.6.2" dotenv = "0.15.0" -embed-nu = "0.3.0" +embed-nu = "0.3.3" lazy_static = "1.4.0" paste = "1.0.9" +rusty-value = { version = "0.6.0", features = ["derive", "json"] } serde = { version = "1.0.145", features = ["derive"] } serde_json = "1.0.86" thiserror = "1.0.37" tokio = { version = "1.21.2", features = ["rt", "io-std", "io-util", "process", "time", "macros", "tracing", "fs"] } +toml = "0.5.9" tracing = "0.1.37" tracing-subscriber = "0.3.16" +valico = "3.6.1" [build-dependencies] -cargo_toml = "0.12.4" +cargo_toml = "0.13.0" serde = { version = "1.0.145", features = ["derive"] } diff --git a/README.md b/README.md index 650b7cb..3439d30 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,15 @@ Tourmaline is a (planned) agnostic, asynchronous and awesome installer framework Just run `cargo run -- help` or `trm help` for now. -## Scripts +## Distro Config -Scripts for all supported distros are stored in the `config` folder of this repository. +Scripts and config files for all supported distros are stored in the `config` folder of this repository. Each distro has its own folder with several subfolders corresponding to all integration tasks. ``` + | distro.toml + | config.schema.json | | up.nu | down.nu @@ -23,6 +25,68 @@ Each distro has its own folder with several subfolders corresponding to all inte | down.nu ``` +### Distro config + +The `distro.toml` file contains config options of the distro and allows one to define additional +tasks to perform on install. +The basic schema of this config file looks like this: + +```toml +# Distro metadata +[distro] + +# Name of the distro +name = "Distro Name" + +# Website of the distro +website = "https://mydistro.com" + + +# Configuration of the OSConfig file +[config] + +# Reference to the JSON Schema describing the config extension of that distro +#[default: "config.schema.json"] +schema = "config.schema.json" + + +# Task-related configuration +[tasks] + +# The key correspons to the name of the task on the file system. +# In this case the scripts `custom-task/up.nu` and `custom-task/down.nu` must +# exist in the distro's config folder +[tasks.custom-task] + +# The key inside the extended config that is used to pass the config +# value to this task +# If this key is empty no value is passed to the task +# [default: none] +config_key = "enable_flatpak" + +# If set to true, the task will be skipped if the value of the task's config +# (indexed by config_key) is null or false. This setting is ignored when +# no config_key is defined for this task +# [default: false] +skip_on_false = true + +# The order in which the task(s) should be executed. Note that +# custom tasks alsways get executed after the base task. +# [default: usize::MAX] +order = 10 +``` + +# Config Schema + +The config schema describing the extended OSConfig for that distro can be configured in the +`distro.toml` and defaults to `config.schema.json`. +This file is a [JSON Schema](https://json-schema.org/) describing the additional fields +required in the config. Make sure to mark required values as such so that the config +get's properly validated. + + +### Scripts + The `up.nu` scripts contain the required steps to apply the change described by the task. This can include installing packages, creating configuration files and starting systemd units. diff --git a/configs/crystal/config.schema.json b/configs/crystal/config.schema.json new file mode 100644 index 0000000..c1e5275 --- /dev/null +++ b/configs/crystal/config.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://getcryst.al/config.schema.json", + "type": "object", + "properties": { + "enable_flatpak": { + "type": "boolean" + }, + "enable_timeshift": { + "type": "boolean" + }, + "enable_zramd": { + "type": "boolean" + }, + "unakite": { + "type": "object", + "properties": { + "root": { + "type": "string" + }, + "old_root": { + "type": "string" + }, + "efidir": { + "type": "string" + }, + "bootdev": { + "type": "string" + } + }, + "required": [ + "root", + "old_root", + "efidir", + "bootdev" + ] + } + }, + "required": [ + "enable_flatpak", + "enable_timeshift", + "enable_zramd", + "unakite" + ] +} \ No newline at end of file diff --git a/configs/crystal/distro.toml b/configs/crystal/distro.toml new file mode 100644 index 0000000..52f8132 --- /dev/null +++ b/configs/crystal/distro.toml @@ -0,0 +1,25 @@ +[distro] +name = "Crystal Linux" +website = "https://getcryst.al" + +[config] +schema = "config.schema.json" + +[tasks] + +[tasks.install-flatpak] +config_key = "enable_flatpak" +skip_on_false = true + +[tasks.install-timeshift] +config_key = "enable_timeshift" +skip_on_false = true + +[tasks.install-zramd] +config_key = "enable_zramd" +skip_on_false = true +order = 10 + +[tasks.configure-unakite] +config_key = "unakite" +order = 0 \ No newline at end of file diff --git a/configs/crystal/setup-users/up.nu b/configs/crystal/setup-users/up.nu index 8b5f6fe..b8b65f8 100644 --- a/configs/crystal/setup-users/up.nu +++ b/configs/crystal/setup-users/up.nu @@ -1,4 +1,5 @@ # Applies all system changes of `setup-users` def main [cfg] { echo "Executing up task `setup-users` with config" $cfg + echo $TRM_CONFIG } diff --git a/src/args.rs b/src/args.rs index cf7e84a..ca1f2b2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -26,6 +26,11 @@ pub enum Command { /// Generates empty script files for the installation #[command()] GenerateScripts(GenerateScriptsArgs), + + /// *For testing purposes only* + /// Generates the JSON for an empty config file + #[command()] + CreateEmptyConfig(CreateEmptyConfigArgs), } #[derive(Debug, Clone, Parser)] @@ -41,3 +46,10 @@ pub struct GenerateScriptsArgs { #[arg()] pub path: PathBuf, } + +#[derive(Debug, Clone, Parser)] +pub struct CreateEmptyConfigArgs { + /// The path to the empty configuration file + #[arg(default_value = "config.json")] + pub path: PathBuf, +} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index bcbdfa5..0000000 --- a/src/config.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::path::PathBuf; - -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::tasks::{ - BootloaderConfig, BootloaderPreset, DesktopConfig, ExtraPackages, Kernel, KernelConfig, - LocaleConfig, NetworkConfig, Partitions, PartitionsConfig, RootUserConfig, UnakiteConfig, - UsersConfig, -}; - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub struct Config { - pub locale: LocaleConfig, - pub network: NetworkConfig, - pub partitions: PartitionsConfig, - pub bootloader: BootloaderConfig, - pub kernels: KernelConfig, - pub desktop: DesktopConfig, - pub users: UsersConfig, - pub root_user: RootUserConfig, - pub unakite: Option, - pub extra_packages: ExtraPackages, - pub enable_timeshift: bool, - pub enable_flatpak: bool, - pub enable_zramd: bool, -} - -impl Config { - pub(crate) fn empty() -> Self { - Self { - locale: LocaleConfig { - locale: Vec::new(), - keymap: String::new(), - timezone: String::new(), - }, - network: NetworkConfig { - hostname: String::new(), - ipv6_loopback: false, - }, - partitions: PartitionsConfig { - device: PathBuf::new(), - efi_partition: false, - partitions: Partitions::Auto, - }, - bootloader: BootloaderConfig { - preset: BootloaderPreset::GrubEfi, - location: PathBuf::new(), - }, - kernels: KernelConfig { - default: Kernel(String::new()), - additional: Vec::new(), - }, - desktop: DesktopConfig::KdePlasma, - users: UsersConfig { users: Vec::new() }, - root_user: RootUserConfig { - password: String::new(), - }, - unakite: None, - extra_packages: Vec::new(), - enable_timeshift: false, - enable_flatpak: false, - enable_zramd: false, - } - } -} diff --git a/src/distro/distro_config.rs b/src/distro/distro_config.rs new file mode 100644 index 0000000..43fcb6e --- /dev/null +++ b/src/distro/distro_config.rs @@ -0,0 +1,71 @@ +use std::{collections::HashMap, path::PathBuf}; + +use serde::Deserialize; +use tokio::fs; + +use crate::{error::DistroConfigError, utils::CFG_PATH}; + +/// The config file of a distro that defines +/// how that distro should be installed +#[derive(Clone, Debug, Deserialize)] +pub struct DistroConfig { + /// Metadata about the distro + pub distro: DistroMetadata, + + /// Configuration related to the Operating system + /// setup configuration + pub config: OSConfigMetadata, + + /// Additional distro specific tasks + pub tasks: HashMap, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct DistroMetadata { + /// The name of the distro + pub name: String, + + /// The website of the distro + pub website: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct OSConfigMetadata { + /// The path of the config schema file + pub schema: Option, +} + +/// The configuration of a single task +#[derive(Clone, Debug, Deserialize)] +pub struct TaskConfig { + /// The name of the config field + pub config_key: Option, + + /// If the task should be skipped if the + /// config value of that task is null + #[serde(default)] + pub skip_on_false: bool, + + /// The execution order of this task + /// Note that custom tasks always get executed after + /// the base tasks + /// If not set this value defaults to [usize::MAX] + #[serde(default = "default_order")] + pub order: usize, +} + +#[inline] +fn default_order() -> usize { + usize::MAX +} + +impl DistroConfig { + #[tracing::instrument(level = "trace", skip_all)] + pub async fn load() -> Result { + let path = CFG_PATH.join("distro.toml"); + let contents = fs::read_to_string(path).await?; + let cfg = toml::from_str::(&contents)?; + + Ok(cfg) + } +} diff --git a/src/distro/mod.rs b/src/distro/mod.rs new file mode 100644 index 0000000..c9d10a4 --- /dev/null +++ b/src/distro/mod.rs @@ -0,0 +1,4 @@ +pub mod distro_config; +mod os_config; + +pub use os_config::*; diff --git a/src/distro/os_config/base_config.rs b/src/distro/os_config/base_config.rs new file mode 100644 index 0000000..e4686c2 --- /dev/null +++ b/src/distro/os_config/base_config.rs @@ -0,0 +1,162 @@ +use std::path::PathBuf; + +use embed_nu::rusty_value::*; +use serde::Deserialize; + +/// The base configuration of the operating system +/// This config alone should provide all required configuraiton +/// values to create a base distro installation while still being +/// distro agnostic. +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub struct BaseConfig { + pub locale: LocaleConfig, + pub network: NetworkConfig, + pub partitions: PartitionsConfig, + pub bootloader: BootloaderConfig, + pub kernels: KernelConfig, + pub desktop: DesktopConfig, + pub users: UsersConfig, + pub root_user: RootUserConfig, + pub extra_packages: ExtraPackages, +} + +impl BaseConfig { + pub(crate) fn empty() -> Self { + Self { + locale: LocaleConfig { + locale: Vec::new(), + keymap: String::new(), + timezone: String::new(), + }, + network: NetworkConfig { + hostname: String::new(), + ipv6_loopback: false, + }, + partitions: PartitionsConfig { + device: PathBuf::new(), + efi_partition: false, + partitions: Partitions::Auto, + }, + bootloader: BootloaderConfig { + preset: BootloaderPreset::GrubEfi, + location: PathBuf::new(), + }, + kernels: KernelConfig { + default: Kernel(String::new()), + additional: Vec::new(), + }, + desktop: DesktopConfig::KdePlasma, + users: UsersConfig(Vec::new()), + root_user: RootUserConfig { + password: String::new(), + }, + extra_packages: Vec::new(), + } + } +} + +#[derive(Clone, Deserialize, RustyValue, Debug)] +pub struct LocaleConfig { + pub locale: Vec, + pub keymap: String, + pub timezone: String, +} + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub struct NetworkConfig { + pub hostname: String, + pub ipv6_loopback: bool, +} + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub struct PartitionsConfig { + pub device: PathBuf, + pub efi_partition: bool, + pub partitions: Partitions, +} + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub enum Partitions { + Auto, + Manual(Vec), +} + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub struct Partition { + pub mountpoint: PathBuf, + pub blockdevice: PathBuf, + pub filesystem: Option, +} + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub enum FileSystem { + VFat, + Bfs, + CramFS, + Ext2, + Ext3, + Ext4, + Fat, + Msdos, + Xfs, + Btrfs, + Minix, + F2fs, +} + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub struct BootloaderConfig { + pub preset: BootloaderPreset, + pub location: PathBuf, +} + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub enum BootloaderPreset { + GrubEfi, + Legacy, +} + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub enum DesktopConfig { + Onyx, + KdePlasma, + Mate, + Gnome, + Cinnamon, + Xfce, + Budgie, + Enlightenment, + Lxqt, + Sway, + I3Gaps, + HerbstluftWM, + AwesomeWM, + Bspwm, +} + +pub type ExtraPackages = Vec; + +#[derive(Clone, Debug, RustyValue, Deserialize)] +pub struct KernelConfig { + pub default: Kernel, + pub additional: Vec, +} + +#[derive(Clone, Debug, RustyValue, Deserialize)] +pub struct Kernel(pub String); + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub struct RootUserConfig { + pub password: String, +} + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub struct UsersConfig(Vec); + +#[derive(Clone, Debug, Deserialize, RustyValue)] +pub struct User { + pub name: String, + pub password: String, + pub sudoer: bool, + pub shell: String, +} diff --git a/src/distro/os_config/loader.rs b/src/distro/os_config/loader.rs new file mode 100644 index 0000000..8c3274a --- /dev/null +++ b/src/distro/os_config/loader.rs @@ -0,0 +1,77 @@ +use std::path::PathBuf; + +use tokio::fs; +use valico::json_schema::Scope; + +use crate::{ + distro::distro_config::DistroConfig, + error::{AppResult, OSConfigError, SchemaError}, + utils::CFG_PATH, +}; + +use super::OSConfig; + +#[derive(Debug)] +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, + } + } + + #[tracing::instrument(level = "trace", skip_all)] + pub async fn load(&self) -> AppResult { + 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) + } + + #[tracing::instrument(level = "trace", skip_all)] + async fn load_extension_schema(&self) -> Result { + let schema_path = self + .distro_cfg + .config + .schema + .as_ref() + .map(|p| CFG_PATH.join(p)) + .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) + } + + #[tracing::instrument(level = "trace", skip_all)] + fn validate_config(schema: serde_json::Value, config: &OSConfig) -> AppResult<()> { + let mut scope = Scope::new(); + let schema = scope + .compile_and_return(schema, true) + .map_err(SchemaError::ParseSchema)?; + let ext_value = serde_json::Value::Object(config.extended.clone().into_iter().collect()); + + let result = schema.validate(&ext_value); + + if result.is_valid() { + tracing::debug!("Config is valid"); + Ok(()) + } else { + let msg = result + .errors + .into_iter() + .map(|e| format!("{} > {}", e.get_path(), e.get_title())) + .collect::>() + .join("\n"); + tracing::error!("Config is invalid"); + + Err(OSConfigError::Validation(msg).into()) + } + } +} diff --git a/src/distro/os_config/mod.rs b/src/distro/os_config/mod.rs new file mode 100644 index 0000000..d2fbf9d --- /dev/null +++ b/src/distro/os_config/mod.rs @@ -0,0 +1,123 @@ +mod base_config; +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, + Value, + }, + RustyIntoValue, +}; +use serde::Deserialize; +use tokio::fs; + +use crate::error::{AppResult, OSConfigError}; + +/// Represents the full configuration of the OS including extensions defined +/// by the distro +#[derive(Clone, Debug, Deserialize)] +pub struct OSConfig { + #[serde(flatten)] + pub base: BaseConfig, + #[serde(flatten)] + pub extended: HashMap, +} + +impl OSConfig { + pub fn get_nu_value>(&self, key: K) -> AppResult { + let value = self.clone().into_rusty_value(); + let mut fields = if let Value::Struct(Struct { fields, .. }) = value { + if let Fields::Named(named) = fields { + named + } else { + panic!("OSConfig fields don't have a name?!"); + } + } else { + panic!("OSConfig is not a struct?!"); + }; + fields + .remove(key.as_ref()) + .map(|v| v.into_value()) + .ok_or_else(|| OSConfigError::MissingConfigKey(key.as_ref().to_owned()).into()) + } +} + +impl RustyValue for OSConfig { + fn into_rusty_value(self) -> embed_nu::rusty_value::Value { + let base = self.base.into_rusty_value(); + + let mut fields = if let Value::Struct(Struct { fields, .. }) = base { + if let Fields::Named(named) = fields { + named + } else { + panic!("Base fields don't have a name?!"); + } + } else { + panic!("Base is not a struct?!"); + }; + let ext = self.extended.into_iter().map(ext_field_to_rusty_value); + fields.extend(ext); + + Value::Struct(Struct { + name: String::from("Config"), + fields: Fields::Named(fields), + }) + } +} + +impl OSConfig { + pub fn empty() -> Self { + Self { + base: BaseConfig::empty(), + extended: HashMap::new(), + } + } +} + +#[inline] +fn ext_field_to_rusty_value( + entry: (String, serde_json::Value), +) -> (String, embed_nu::rusty_value::Value) { + (entry.0, json_to_rusty_value(entry.1)) +} + +fn json_to_rusty_value(val: serde_json::Value) -> embed_nu::rusty_value::Value { + match val { + serde_json::Value::Null => Value::None, + serde_json::Value::Bool(b) => Value::Primitive(Primitive::Bool(b)), + serde_json::Value::Number(num) => Value::Primitive(if num.is_i64() { + Primitive::Integer(Integer::I64(num.as_i64().unwrap())) + } else if num.is_u64() { + Primitive::Integer(Integer::U64(num.as_u64().unwrap())) + } else { + Primitive::Float(Float::F64(num.as_f64().unwrap())) + }), + serde_json::Value::String(s) => Value::Primitive(Primitive::String(s)), + serde_json::Value::Array(a) => { + Value::List(a.into_iter().map(json_to_rusty_value).collect()) + } + serde_json::Value::Object(o) => { + let vals = o + .into_iter() + .map(|(k, v)| { + ( + HashableValue::Primitive(HashablePrimitive::String(k)), + json_to_rusty_value(v), + ) + }) + .collect::>(); + Value::Map(vals) + } + } +} + +impl OSConfig { + pub(crate) async fn load(path: &Path) -> Result { + let contents = fs::read_to_string(path).await?; + let cfg = serde_json::from_str::(&contents)?; + + Ok(cfg) + } +} diff --git a/src/error.rs b/src/error.rs index 001c77c..c1279df 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,24 +6,75 @@ pub type AppResult = std::result::Result; #[derive(Error, Debug)] pub enum AppError { - #[error("Could not find the script file")] + #[error("Missing config")] + MissingConfig, + + #[error("IO Error: {0}")] + Io(#[from] io::Error), + + #[error("JSON deserialization error {0}")] + JSON(#[from] serde_json::Error), + + #[error(transparent)] + Script(#[from] ScriptError), + + #[error(transparent)] + DistroConfig(#[from] DistroConfigError), + + #[error(transparent)] + Schema(#[from] SchemaError), + + #[error(transparent)] + OSConfig(#[from] OSConfigError), +} + +#[derive(Error, Debug)] +pub enum ScriptError { + #[error("IO Error when trying to read script file: {0}")] + Io(#[from] io::Error), + + #[error("Could not find the script file at {0}")] ScriptNotFound(PathBuf), - #[error("Nu error {0}")] + #[error("Nu error when executing script: {0}")] NuError(#[from] embed_nu::Error), - #[error("Could not find the main mehod in the script file {0}")] + #[error("Could not find the main method in the script file: {0}")] MissingMain(PathBuf), +} - #[error("Failed to execute script")] - FailedToExecuteScript, +#[derive(Error, Debug)] +pub enum DistroConfigError { + #[error("IO Error when trying to read distro config: {0}")] + Io(#[from] io::Error), - #[error("Missing config")] - MissingConfig, + #[error("Encountered invalid Toml when parsing distro config: {0}")] + InvalidToml(#[from] toml::de::Error), +} - #[error("IO Error: {0}")] +#[derive(Error, Debug)] +pub enum SchemaError { + #[error("IO Error when trying to read json-schema file: {0}")] Io(#[from] io::Error), - #[error("JSON deserialization error {0}")] - JSON(#[from] serde_json::Error), + #[error("Encountered invalid JSON when parsing json-schema: {0}")] + InvalidJson(#[from] serde_json::Error), + + #[error("Failed to parse the json-schema: {0}")] + ParseSchema(#[from] valico::json_schema::SchemaError), +} + +#[derive(Error, Debug)] +pub enum OSConfigError { + #[error("IO Error when trying to read OSConfig file: {0}")] + Io(#[from] io::Error), + + #[error("Encountered invalid JSON when parsing OSConfig: {0}")] + InvalidJson(#[from] serde_json::Error), + + #[error("The os config is invalid:\n{0}")] + Validation(String), + + #[error("Missing config key {0}")] + MissingConfigKey(String), } diff --git a/src/lib.rs b/src/lib.rs index fa2af32..95fe6cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,134 +1,22 @@ -use config::Config; -use error::{AppError, AppResult}; -use scripting::{ - loader::ScriptLoader, - script::{NuScript, Script}, -}; -use tasks::*; - -pub mod config; pub mod error; -pub(crate) mod scripting; -pub mod tasks; -pub(crate) mod utils; -pub use utils::generate_script_files; - -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] -pub enum TaskOperation { - Up, - Down, -} - -pub struct TaskExecutor { - config: Option, - 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: Config) -> 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)?; - self.create_partitions(TaskOperation::Up, config.partitions) - .await?; - self.install_base(TaskOperation::Up, ()).await?; - self.install_kernels(TaskOperation::Up, config.kernels) - .await?; - self.install_bootloader(TaskOperation::Up, config.bootloader) - .await?; - self.configure_locale(TaskOperation::Up, config.locale) - .await?; - self.configure_network(TaskOperation::Up, config.network) - .await?; - - if config.enable_zramd { - self.install_zramd(TaskOperation::Up, ()).await?; - } - if config.enable_timeshift { - self.install_timeshift(TaskOperation::Up, ()).await?; - } - if config.enable_flatpak { - self.install_flatpak(TaskOperation::Up, ()).await?; - } - self.setup_users(TaskOperation::Up, config.users).await?; - self.setup_root_user(TaskOperation::Up, config.root_user) - .await?; - self.install_desktop(TaskOperation::Up, config.desktop) - .await?; - self.install_extra_packages(TaskOperation::Up, config.extra_packages) - .await?; - - if let Some(unakite) = config.unakite { - self.configure_unakite(TaskOperation::Up, unakite).await?; - } - - Ok(()) - } - - async fn execute_task(&self, args: S::Args) -> AppResult<()> { - let script = self.loader.load::()?; - self.execute(script, args.clone()).await?; - - Ok(()) - } - - #[inline] - async fn execute(&self, mut script: NuScript, 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", Config::empty()) - } - .set_global_var("TRM_VERSION", env!("CARGO_PKG_VERSION")) - .execute(args) - .await - } -} +pub(crate) mod utils; +use std::path::PathBuf; -impl Default for TaskExecutor { - fn default() -> Self { - Self { - loader: ScriptLoader::new(), - config: None, - } - } +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; + +/// Creates a new executor with the given os config for the current distro +#[tracing::instrument(level = "trace")] +pub async fn create_executor(os_cfg_path: PathBuf) -> AppResult { + let distro_config = DistroConfig::load().await?; + let os_config = OSConfigLoader::new(os_cfg_path, &distro_config) + .load() + .await?; + + Ok(TaskExecutor::new(os_config, distro_config)) } diff --git a/src/main.rs b/src/main.rs index 4ff8551..68730dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ -use args::{Args, Command, GenerateScriptsArgs, InstallFromConfigArgs}; +use args::{Args, Command, CreateEmptyConfigArgs, GenerateScriptsArgs, InstallFromConfigArgs}; use clap::Parser; -use tokio::{fs::OpenOptions, io::AsyncReadExt}; -use tourmaline::{config::Config, error::AppResult, generate_script_files, TaskExecutor}; +use rusty_value::into_json::{EnumRepr, IntoJson, IntoJsonOptions}; +use tokio::fs; +use tourmaline::{distro::OSConfig, error::AppResult, generate_script_files}; mod args; #[tokio::main(flavor = "current_thread")] -async fn main() { +async fn main() -> color_eyre::Result<()> { color_eyre::install().unwrap(); dotenv::dotenv().unwrap(); let args = Args::parse(); @@ -14,21 +15,32 @@ async fn main() { match args.command { Command::InstallFromConfig(args) => install_from_config(args).await, Command::GenerateScripts(args) => generate_scripts(args).await, - } - .unwrap(); + Command::CreateEmptyConfig(args) => generate_empty_config(args).await, + }?; + + Ok(()) } +/// 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: Config = 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 } async fn generate_scripts(args: GenerateScriptsArgs) -> AppResult<()> { generate_script_files(args.path).await } + +async fn generate_empty_config(args: CreateEmptyConfigArgs) -> AppResult<()> { + let config = OSConfig::empty().into_json_with_options(&IntoJsonOptions { + enum_repr: EnumRepr::Untagged, + }); + let config_string = serde_json::to_string_pretty(&config)?; + fs::write(args.path, config_string).await?; + + Ok(()) +} diff --git a/src/scripting/loader.rs b/src/scripting/loader.rs deleted file mode 100644 index 9280850..0000000 --- a/src/scripting/loader.rs +++ /dev/null @@ -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::SCRIPT_PATH.to_owned(), - } - } - - /// Loads the given script file - #[tracing::instrument(level = "trace", skip_all)] - pub fn load(&self) -> AppResult> { - let script_path = self.script_dir.join(S::name()); - - if !script_path.exists() { - Err(AppError::ScriptNotFound(script_path)) - } else { - Ok(NuScript::new(script_path)) - } - } -} diff --git a/src/scripting/mod.rs b/src/scripting/mod.rs deleted file mode 100644 index 27a381d..0000000 --- a/src/scripting/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod loader; -pub mod script; diff --git a/src/scripting/script.rs b/src/scripting/script.rs deleted file mode 100644 index a6437b2..0000000 --- a/src/scripting/script.rs +++ /dev/null @@ -1,83 +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; -} - -impl ScriptArgs for T { - fn get_args(self) -> Vec { - 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 { - path: PathBuf, - vars: HashMap, - __phantom: PhantomData, -} - -impl NuScript { - pub(crate) fn new(path: PathBuf) -> Self { - Self { - path, - vars: HashMap::new(), - __phantom: PhantomData, - } - } - - /// Adds a global variable - pub fn set_global_var(&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") { - ctx.call_fn("main", args.get_args())?; - Ok(()) - } else { - Err(AppError::MissingMain(self.path.clone())) - } - } - - async fn read_file(&self) -> AppResult { - fs::read_to_string(&self.path).await.map_err(AppError::from) - } -} diff --git a/src/task/base_task.rs b/src/task/base_task.rs new file mode 100644 index 0000000..429dad8 --- /dev/null +++ b/src/task/base_task.rs @@ -0,0 +1,145 @@ +use std::path::PathBuf; + +use crate::{distro::OSConfig, error::AppResult}; + +use super::{exec_builder::ExecBuilder, TaskTrait}; +use embed_nu::IntoValue; +use lazy_static::lazy_static; + +#[derive(Clone, Debug)] +pub enum BaseTask { + ConfigureLocale, + ConfigureNetwork, + CreatePartitions, + InstallBase, + InstallBootloader, + InstallDesktop, + InstallKernels, + InstallExtraPackages, + SetupRootUser, + SetupUsers, +} + +#[derive(Clone, Copy)] +pub struct BaseTaskKeydata { + pub task_name: &'static str, + pub config_key: Option<&'static str>, +} + +impl BaseTask { + pub fn key_data(&self) -> BaseTaskKeydata { + match self { + 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"), + }, + BaseTask::InstallKernels => BaseTaskKeydata { + task_name: "install-kernels", + config_key: Some("kernels"), + }, + } + } +} + +impl TaskTrait for BaseTask { + #[tracing::instrument(level = "trace", skip_all)] + fn up(&self, config: &OSConfig) -> AppResult> { + let key_data = self.key_data(); + let script = PathBuf::from(key_data.task_name).join("up.nu"); + + let task_config = if let Some(key) = key_data.config_key { + config.get_nu_value(key)? + } else { + Option::<()>::None.into_value() + }; + + Ok(Some(ExecBuilder { + script, + os_config: config.to_owned(), + task_config, + })) + } + + #[tracing::instrument(level = "trace", skip_all)] + fn down(&self, config: &OSConfig) -> AppResult> { + 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(Some(ExecBuilder { + script, + os_config: config.to_owned(), + task_config, + })) + } + + fn order(&self) -> usize { + match self { + BaseTask::CreatePartitions => 10, + BaseTask::InstallBase => 20, + BaseTask::InstallKernels => 30, + BaseTask::InstallBootloader => 40, + BaseTask::ConfigureLocale => 50, + BaseTask::ConfigureNetwork => 60, + BaseTask::SetupRootUser => 70, + BaseTask::SetupUsers => 80, + BaseTask::InstallDesktop => 90, + BaseTask::InstallExtraPackages => 100, + } + } +} + +lazy_static! { + pub static ref ALL_BASE_TASKS: Vec = get_all_base_tasks(); +} + +fn get_all_base_tasks() -> Vec { + vec![ + BaseTask::ConfigureLocale, + BaseTask::ConfigureNetwork, + BaseTask::CreatePartitions, + BaseTask::InstallBase, + BaseTask::InstallBootloader, + BaseTask::InstallDesktop, + BaseTask::InstallExtraPackages, + BaseTask::SetupRootUser, + BaseTask::SetupUsers, + BaseTask::InstallKernels, + ] +} diff --git a/src/task/custom_task.rs b/src/task/custom_task.rs new file mode 100644 index 0000000..81f3e2f --- /dev/null +++ b/src/task/custom_task.rs @@ -0,0 +1,87 @@ +use std::path::PathBuf; + +use embed_nu::IntoValue; + +use crate::{distro::OSConfig, error::AppResult}; + +use super::{exec_builder::ExecBuilder, TaskTrait}; + +#[derive(Clone, Debug)] +pub struct CustomTask { + config_key: Option, + up_script: PathBuf, + down_script: PathBuf, + skip_on_false: bool, + order: usize, +} + +impl CustomTask { + pub fn new( + name: String, + config_key: Option, + skip_on_false: bool, + order: usize, + ) -> 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, + order, + } + } +} + +impl TaskTrait for CustomTask { + #[tracing::instrument(level = "trace", skip_all)] + fn up(&self, config: &OSConfig) -> AppResult> { + let task_config = if let Some(key) = self.config_key.as_ref() { + config.get_nu_value(key)? + } else { + Option::<()>::None.into_value() + }; + + if self.skip_on_false && self.config_key.is_some() && config_is_falsy(&task_config) { + Ok(None) + } else { + Ok(Some(ExecBuilder { + script: self.up_script.to_owned(), + os_config: config.to_owned(), + task_config, + })) + } + } + + #[tracing::instrument(level = "trace", skip_all)] + fn down(&self, config: &OSConfig) -> AppResult> { + let task_config = if let Some(key) = self.config_key.as_ref() { + config.get_nu_value(key)? + } else { + Option::<()>::None.into_value() + }; + + if self.skip_on_false && self.config_key.is_some() && config_is_falsy(&task_config) { + Ok(None) + } else { + Ok(Some(ExecBuilder { + script: self.down_script.to_owned(), + os_config: config.to_owned(), + task_config, + })) + } + } + + fn order(&self) -> usize { + self.order + } +} + +fn config_is_falsy(config: &embed_nu::Value) -> bool { + if config.is_nothing() { + return true; + } else if let Ok(b) = config.as_bool() { + return !b; + } + false +} diff --git a/src/task/exec_builder.rs b/src/task/exec_builder.rs new file mode 100644 index 0000000..b20a19b --- /dev/null +++ b/src/task/exec_builder.rs @@ -0,0 +1,47 @@ +use std::path::PathBuf; + +use embed_nu::{Argument, CommandGroupConfig, Context, ValueIntoExpression}; +use tokio::fs; + +use crate::{distro::OSConfig, error::ScriptError, utils::CFG_PATH}; + +pub struct ExecBuilder { + pub script: PathBuf, + pub os_config: OSConfig, + pub task_config: embed_nu::Value, +} + +impl ExecBuilder { + #[tracing::instrument(level = "trace", skip_all)] + pub async fn exec(self) -> Result<(), ScriptError> { + let script_contents = self.get_script_contents().await?; + let mut ctx = Context::builder() + .with_command_groups(CommandGroupConfig::default().all_groups(true))? + .add_var("TRM_CONFIG", self.os_config)? + .add_script(script_contents)? + .build()?; + + if ctx.has_fn("main") { + let pipeline = ctx.call_fn( + "main", + vec![Argument::Positional(self.task_config.into_expression())], + )?; + ctx.print_pipeline_stderr(pipeline)?; + + Ok(()) + } else { + Err(ScriptError::MissingMain(self.script)) + } + } + + #[tracing::instrument(level = "trace", skip_all)] + async fn get_script_contents(&self) -> Result { + let path = CFG_PATH.join(&self.script); + + if path.exists() { + fs::read_to_string(path).await.map_err(ScriptError::from) + } else { + Err(ScriptError::ScriptNotFound(path)) + } + } +} diff --git a/src/task/mod.rs b/src/task/mod.rs new file mode 100644 index 0000000..f98f81b --- /dev/null +++ b/src/task/mod.rs @@ -0,0 +1,73 @@ +use std::cmp::Ordering; + +use crate::{distro::OSConfig, error::AppResult}; + +use self::{base_task::BaseTask, custom_task::CustomTask, exec_builder::ExecBuilder}; +pub mod base_task; +pub mod custom_task; +pub mod exec_builder; +pub mod task_executor; + +pub trait TaskTrait { + fn up(&self, config: &OSConfig) -> AppResult>; + fn down(&self, config: &OSConfig) -> AppResult>; + /// Used to decide the execution order + /// smaller values mean the task get's executed earlier + fn order(&self) -> usize; +} + +#[derive(Clone, Debug)] +pub enum Task { + Base(BaseTask), + Custom(CustomTask), +} + +impl Task { + pub fn is_custom(&self) -> bool { + match self { + Task::Base(_) => false, + Task::Custom(_) => true, + } + } + + pub fn is_base(&self) -> bool { + !self.is_custom() + } + + pub fn compare(&self, other: &Self) -> Ordering { + if self.is_base() && other.is_custom() { + Ordering::Less + } else if self.is_custom() && other.is_base() || self.order() > other.order() { + Ordering::Greater + } else if self.order() < other.order() { + Ordering::Less + } else { + Ordering::Equal + } + } +} + +impl TaskTrait for Task { + #[inline] + fn up(&self, config: &OSConfig) -> AppResult> { + match self { + Task::Base(b) => b.up(config), + Task::Custom(c) => c.up(config), + } + } + + #[inline] + fn down(&self, config: &OSConfig) -> AppResult> { + match self { + Task::Base(b) => b.down(config), + Task::Custom(c) => c.down(config), + } + } + + fn order(&self) -> usize { + match self { + Task::Base(b) => b.order(), + Task::Custom(c) => c.order(), + } + } +} diff --git a/src/task/task_executor.rs b/src/task/task_executor.rs new file mode 100644 index 0000000..ba4a882 --- /dev/null +++ b/src/task/task_executor.rs @@ -0,0 +1,72 @@ +use crate::{ + distro::{distro_config::DistroConfig, OSConfig}, + error::AppResult, +}; + +use super::{base_task::ALL_BASE_TASKS, custom_task::CustomTask, Task, TaskTrait}; + +pub struct TaskExecutor { + distro_config: DistroConfig, + os_config: OSConfig, + tasks: Vec, +} + +impl TaskExecutor { + /// Creates a new task executor with the given OSConfig and Distro Config + pub fn new(os_config: OSConfig, distro_config: DistroConfig) -> Self { + Self { + distro_config, + os_config, + tasks: Vec::new(), + } + } + + /// Adds all base tasks to the executor + #[tracing::instrument(level = "trace", skip_all)] + pub fn with_base_tasks(&mut self) -> &mut Self { + let mut base_tasks = (*ALL_BASE_TASKS) + .iter() + .cloned() + .map(Task::Base) + .collect::>(); + self.tasks.append(&mut base_tasks); + + self + } + + /// Adds all custom tasks to the executor + #[tracing::instrument(level = "trace", skip_all)] + pub fn with_custom_tasks(&mut self) -> &mut Self { + let mut custom_tasks = self + .distro_config + .tasks + .iter() + .map(|(name, task)| { + CustomTask::new( + name.to_owned(), + task.config_key.to_owned(), + task.skip_on_false, + task.order, + ) + }) + .map(Task::Custom) + .collect::>(); + self.tasks.append(&mut custom_tasks); + + self + } + + /// Executes all tasks + #[tracing::instrument(level = "trace", skip_all)] + pub async fn execute(&mut self) -> AppResult<()> { + self.tasks.sort_by(Task::compare); + + for task in &self.tasks { + if let Some(up_task) = task.up(&self.os_config)? { + up_task.exec().await? + } + } + + Ok(()) + } +} diff --git a/src/tasks/configure_locale.rs b/src/tasks/configure_locale.rs deleted file mode 100644 index bdbeffc..0000000 --- a/src/tasks/configure_locale.rs +++ /dev/null @@ -1,16 +0,0 @@ -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::task; - -task!(ConfigureLocale { - name = "configure-locale" - args = LocaleConfig -}); - -#[derive(Clone, Deserialize, RustyValue, Debug)] -pub struct LocaleConfig { - pub locale: Vec, - pub keymap: String, - pub timezone: String, -} diff --git a/src/tasks/configure_network.rs b/src/tasks/configure_network.rs deleted file mode 100644 index 4e00ea4..0000000 --- a/src/tasks/configure_network.rs +++ /dev/null @@ -1,15 +0,0 @@ -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::task; - -task!(ConfigureNetwork { - name = "configure-network" - args = NetworkConfig -}); - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub struct NetworkConfig { - pub hostname: String, - pub ipv6_loopback: bool, -} diff --git a/src/tasks/configure_unakite.rs b/src/tasks/configure_unakite.rs deleted file mode 100644 index 5b8060f..0000000 --- a/src/tasks/configure_unakite.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::path::PathBuf; - -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::task; - -task!(ConfigureUnakite { - name = "configure-unakite" - args = UnakiteConfig -}); - -#[derive(Clone, Debug, RustyValue, Deserialize)] -pub struct UnakiteConfig { - pub root: PathBuf, - pub old_root: PathBuf, - pub efidir: PathBuf, - pub bootdev: PathBuf, -} diff --git a/src/tasks/create_partitions.rs b/src/tasks/create_partitions.rs deleted file mode 100644 index da5cea0..0000000 --- a/src/tasks/create_partitions.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::path::PathBuf; - -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::task; - -task!(CreatePartitions { - name = "create-partitions" - args = PartitionsConfig -}); - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub struct PartitionsConfig { - pub device: PathBuf, - pub efi_partition: bool, - pub partitions: Partitions, -} - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub enum Partitions { - Auto, - Manual(Vec), -} - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub struct Partition { - pub mountpoint: PathBuf, - pub blockdevice: PathBuf, - pub filesystem: Option, -} - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub enum FileSystem { - VFAT, - BFS, - CramFS, - Ext2, - Ext3, - Ext4, - FAT, - MSDOS, - XFS, - BTRFS, - Minix, - F2FS, -} diff --git a/src/tasks/install_base.rs b/src/tasks/install_base.rs deleted file mode 100644 index 3f4a29e..0000000 --- a/src/tasks/install_base.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::task; - -task!(InstallBase { - name = "install-base" - args = InstallBaseArgs -}); - -type InstallBaseArgs = (); diff --git a/src/tasks/install_bootloader.rs b/src/tasks/install_bootloader.rs deleted file mode 100644 index 574f746..0000000 --- a/src/tasks/install_bootloader.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::path::PathBuf; - -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::task; - -task!(InstallBootloader { - name = "install-bootloader" - args = BootloaderConfig -}); - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub struct BootloaderConfig { - pub preset: BootloaderPreset, - pub location: PathBuf, -} - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub enum BootloaderPreset { - GrubEfi, - Legacy, -} diff --git a/src/tasks/install_desktop.rs b/src/tasks/install_desktop.rs deleted file mode 100644 index d589e5d..0000000 --- a/src/tasks/install_desktop.rs +++ /dev/null @@ -1,27 +0,0 @@ -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::task; - -task!(InstallDesktop { - name = "install-desktop" - args = DesktopConfig -}); - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub enum DesktopConfig { - Onyx, - KdePlasma, - Mate, - Gnome, - Cinnamon, - Xfce, - Budgie, - Enlightenment, - Lxqt, - Sway, - I3Gaps, - HerbstluftWM, - AwesomeWM, - BSPWM, -} diff --git a/src/tasks/install_extra_packages.rs b/src/tasks/install_extra_packages.rs deleted file mode 100644 index f4575a1..0000000 --- a/src/tasks/install_extra_packages.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::task; - -task!(InstallExtraPackages { - name = "install-extra-packages" - args = ExtraPackages -}); - -pub type ExtraPackages = Vec; diff --git a/src/tasks/install_flatpak.rs b/src/tasks/install_flatpak.rs deleted file mode 100644 index 3803573..0000000 --- a/src/tasks/install_flatpak.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::task; - -task!(InstallFlatpak { - name = "install-flatpak" - args = FlatpakConfig -}); - -pub type FlatpakConfig = (); diff --git a/src/tasks/install_kernels.rs b/src/tasks/install_kernels.rs deleted file mode 100644 index e76aa1d..0000000 --- a/src/tasks/install_kernels.rs +++ /dev/null @@ -1,18 +0,0 @@ -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::task; - -task!(InstallKernels { - name = "install-kernels" - args = KernelConfig -}); - -#[derive(Clone, Debug, RustyValue, Deserialize)] -pub struct KernelConfig { - pub default: Kernel, - pub additional: Vec, -} - -#[derive(Clone, Debug, RustyValue, Deserialize)] -pub struct Kernel(pub String); diff --git a/src/tasks/install_timeshift.rs b/src/tasks/install_timeshift.rs deleted file mode 100644 index 7717726..0000000 --- a/src/tasks/install_timeshift.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::task; - -task!(InstallTimeshift { - name = "install-timeshift" - args = TimeshiftConfig -}); - -pub type TimeshiftConfig = (); diff --git a/src/tasks/install_zramd.rs b/src/tasks/install_zramd.rs deleted file mode 100644 index cb323de..0000000 --- a/src/tasks/install_zramd.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::task; - -task!(InstallZRamD { - name = "install-zramd" - args = ZRamDConfig -}); - -pub type ZRamDConfig = (); diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs deleted file mode 100644 index 45aa8d7..0000000 --- a/src/tasks/mod.rs +++ /dev/null @@ -1,118 +0,0 @@ -mod configure_locale; -mod configure_network; -mod configure_unakite; -mod create_partitions; -mod install_base; -mod install_bootloader; -mod install_desktop; -mod install_extra_packages; -mod install_flatpak; -mod install_kernels; -mod install_timeshift; -mod install_zramd; -mod setup_root_user; -mod setup_users; - -use std::{fmt, path::PathBuf}; - -pub use configure_locale::*; -pub use configure_network::*; -pub use configure_unakite::*; -pub use create_partitions::*; -pub use install_base::*; -pub use install_bootloader::*; -pub use install_desktop::*; -pub use install_extra_packages::*; -pub use install_flatpak::*; -pub use install_kernels::*; -pub use install_timeshift::*; -pub use install_zramd::*; -pub use setup_root_user::*; -pub use setup_users::*; - -use crate::scripting::script::{Script, ScriptArgs}; - -pub trait Task { - type Config: ScriptArgs + fmt::Debug + Clone; - type UpScript: Script; - type DownScript: Script; - - fn name() -> &'static str; -} - -/// Defines a script -/// This macro doesn't accept a file extension for the script name -/// as it is reused for the hook name -#[macro_export] -macro_rules! task { - ($task:ident { - name = $name:literal - args = $argtype:ident - }) => { - paste::item! { - pub struct [<$task Task>]; - - impl $crate::tasks::Task for [<$task Task>] { - type Config = $argtype; - type UpScript = [<$task UpScript>]; - type DownScript = [<$task DownScript>]; - - #[inline] - fn name() -> &'static str { - $name - } - } - - pub struct [<$task UpScript>]; - - impl $crate::scripting::script::Script for [<$task UpScript>] { - type Args = $argtype; - - #[inline] - fn name() -> &'static str { - "up.nu" - } - } - - pub struct [<$task DownScript>]; - - impl $crate::scripting::script::Script for [<$task DownScript>] { - type Args = $argtype; - - #[inline] - fn name() -> &'static str { - "down.nu" - } - } - } - }; -} - -pub(crate) fn all_tasks() -> Vec<(&'static str, PathBuf, PathBuf)> { - macro_rules! task_scripts { - ($task:ident) => {{ - let base = PathBuf::from($task::name()); - ( - $task::name(), - base.join(<$task as Task>::UpScript::name()), - base.join(<$task as Task>::DownScript::name()), - ) - }}; - } - vec![ - task_scripts!(ConfigureLocaleTask), - task_scripts!(ConfigureNetworkTask), - task_scripts!(ConfigureUnakiteTask), - task_scripts!(CreatePartitionsTask), - task_scripts!(InstallBaseTask), - task_scripts!(InstallBootloaderTask), - task_scripts!(InstallDesktopTask), - task_scripts!(InstallExtraPackagesTask), - task_scripts!(InstallFlatpakTask), - task_scripts!(InstallKernelsTask), - task_scripts!(InstallTimeshiftTask), - task_scripts!(InstallZRamDTask), - task_scripts!(SetupRootUserTask), - task_scripts!(SetupUsersTask), - ] -} diff --git a/src/tasks/setup_root_user.rs b/src/tasks/setup_root_user.rs deleted file mode 100644 index 7670bfd..0000000 --- a/src/tasks/setup_root_user.rs +++ /dev/null @@ -1,14 +0,0 @@ -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::task; - -task!(SetupRootUser { - name = "setup-root-user" - args = RootUserConfig -}); - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub struct RootUserConfig { - pub password: String, -} diff --git a/src/tasks/setup_users.rs b/src/tasks/setup_users.rs deleted file mode 100644 index 2853571..0000000 --- a/src/tasks/setup_users.rs +++ /dev/null @@ -1,22 +0,0 @@ -use embed_nu::rusty_value::*; -use serde::Deserialize; - -use crate::task; - -task!(SetupUsers { - name = "setup-users" - args = UsersConfig -}); - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub struct UsersConfig { - pub users: Vec, -} - -#[derive(Clone, Debug, Deserialize, RustyValue)] -pub struct User { - pub name: String, - pub password: String, - pub sudoer: bool, - pub shell: String, -} diff --git a/src/utils.rs b/src/utils.rs index d60d0f4..9a5347c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,28 +4,27 @@ use std::{env, path::PathBuf}; use tokio::fs; use crate::error::AppResult; -use crate::tasks::all_tasks; +use crate::task::base_task::ALL_BASE_TASKS; const DEFAULT_CONFIG_DIR: &str = "/etc"; lazy_static::lazy_static! { pub static ref CFG_PATH: PathBuf = env::var("TRM_CFG_PATH").map(PathBuf::from).unwrap_or_else(|_| PathBuf::from(DEFAULT_CONFIG_DIR).join("tourmaline")); - pub static ref SCRIPT_PATH: PathBuf = CFG_PATH.join("scripts"); } pub async fn generate_script_files>(output: P) -> AppResult<()> { let output = output.as_ref(); - let tasks = all_tasks(); - - for (name, up_script, down_script) in tasks { - let script_dir = output.join(&name); + for task in &*ALL_BASE_TASKS { + 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,