Merge branch 'config-extension' into 'main'

Rewrite to allow for dynamic config extension and task definitions

See merge request crystal/software/tourmaline!3
chrooting
Michal Stopyra 2 years ago
commit 29a12c5e5e

3
.gitignore vendored

@ -11,3 +11,6 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
.env
# Misc
test-config.json

256
Cargo.lock generated

@ -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"

@ -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"] }

@ -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>
| distro.toml
| config.schema.json
| <task>
| 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.

@ -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"
]
}

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

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

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

@ -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<UnakiteConfig>,
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,
}
}
}

@ -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<String, TaskConfig>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct DistroMetadata {
/// The name of the distro
pub name: String,
/// The website of the distro
pub website: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct OSConfigMetadata {
/// The path of the config schema file
pub schema: Option<PathBuf>,
}
/// The configuration of a single task
#[derive(Clone, Debug, Deserialize)]
pub struct TaskConfig {
/// The name of the config field
pub config_key: Option<String>,
/// 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<Self, DistroConfigError> {
let path = CFG_PATH.join("distro.toml");
let contents = fs::read_to_string(path).await?;
let cfg = toml::from_str::<Self>(&contents)?;
Ok(cfg)
}
}

@ -0,0 +1,4 @@
pub mod distro_config;
mod os_config;
pub use os_config::*;

@ -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<String>,
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<Partition>),
}
#[derive(Clone, Debug, Deserialize, RustyValue)]
pub struct Partition {
pub mountpoint: PathBuf,
pub blockdevice: PathBuf,
pub filesystem: Option<FileSystem>,
}
#[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<String>;
#[derive(Clone, Debug, RustyValue, Deserialize)]
pub struct KernelConfig {
pub default: Kernel,
pub additional: Vec<Kernel>,
}
#[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<User>);
#[derive(Clone, Debug, Deserialize, RustyValue)]
pub struct User {
pub name: String,
pub password: String,
pub sudoer: bool,
pub shell: String,
}

@ -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<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)
}
#[tracing::instrument(level = "trace", skip_all)]
async fn load_extension_schema(&self) -> Result<serde_json::Value, SchemaError> {
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::<Vec<_>>()
.join("\n");
tracing::error!("Config is invalid");
Err(OSConfigError::Validation(msg).into())
}
}
}

@ -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<String, serde_json::Value>,
}
impl OSConfig {
pub fn get_nu_value<K: AsRef<str>>(&self, key: K) -> AppResult<embed_nu::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
} 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::<HashMap<_, _>>();
Value::Map(vals)
}
}
}
impl OSConfig {
pub(crate) async fn load(path: &Path) -> Result<Self, OSConfigError> {
let contents = fs::read_to_string(path).await?;
let cfg = serde_json::from_str::<Self>(&contents)?;
Ok(cfg)
}
}

@ -6,24 +6,75 @@ pub type AppResult<T> = std::result::Result<T, AppError>;
#[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),
}

@ -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<Config>,
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
);
pub(crate) mod utils;
use std::path::PathBuf;
/// 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?;
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;
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)
/// 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<TaskExecutor> {
let distro_config = DistroConfig::load().await?;
let os_config = OSConfigLoader::new(os_cfg_path, &distro_config)
.load()
.await?;
if let Some(unakite) = 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(())
}
#[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", Config::empty())
}
.set_global_var("TRM_VERSION", env!("CARGO_PKG_VERSION"))
.execute(args)
.await
}
}
impl Default for TaskExecutor {
fn default() -> Self {
Self {
loader: ScriptLoader::new(),
config: None,
}
}
Ok(TaskExecutor::new(os_config, distro_config))
}

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

@ -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<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,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<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") {
ctx.call_fn("main", args.get_args())?;
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)
}
}

@ -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<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) = 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<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(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<BaseTask> = get_all_base_tasks();
}
fn get_all_base_tasks() -> Vec<BaseTask> {
vec![
BaseTask::ConfigureLocale,
BaseTask::ConfigureNetwork,
BaseTask::CreatePartitions,
BaseTask::InstallBase,
BaseTask::InstallBootloader,
BaseTask::InstallDesktop,
BaseTask::InstallExtraPackages,
BaseTask::SetupRootUser,
BaseTask::SetupUsers,
BaseTask::InstallKernels,
]
}

@ -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<String>,
up_script: PathBuf,
down_script: PathBuf,
skip_on_false: bool,
order: usize,
}
impl CustomTask {
pub fn new(
name: String,
config_key: Option<String>,
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<Option<ExecBuilder>> {
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<Option<ExecBuilder>> {
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
}

@ -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<String, ScriptError> {
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))
}
}
}

@ -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<Option<ExecBuilder>>;
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>>;
/// 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<Option<ExecBuilder>> {
match self {
Task::Base(b) => b.up(config),
Task::Custom(c) => c.up(config),
}
}
#[inline]
fn down(&self, config: &OSConfig) -> AppResult<Option<ExecBuilder>> {
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(),
}
}
}

@ -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<Task>,
}
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::<Vec<_>>();
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::<Vec<_>>();
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(())
}
}

@ -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<String>,
pub keymap: String,
pub timezone: String,
}

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

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

@ -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<Partition>),
}
#[derive(Clone, Debug, Deserialize, RustyValue)]
pub struct Partition {
pub mountpoint: PathBuf,
pub blockdevice: PathBuf,
pub filesystem: Option<FileSystem>,
}
#[derive(Clone, Debug, Deserialize, RustyValue)]
pub enum FileSystem {
VFAT,
BFS,
CramFS,
Ext2,
Ext3,
Ext4,
FAT,
MSDOS,
XFS,
BTRFS,
Minix,
F2FS,
}

@ -1,8 +0,0 @@
use crate::task;
task!(InstallBase {
name = "install-base"
args = InstallBaseArgs
});
type InstallBaseArgs = ();

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

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

@ -1,8 +0,0 @@
use crate::task;
task!(InstallExtraPackages {
name = "install-extra-packages"
args = ExtraPackages
});
pub type ExtraPackages = Vec<String>;

@ -1,8 +0,0 @@
use crate::task;
task!(InstallFlatpak {
name = "install-flatpak"
args = FlatpakConfig
});
pub type FlatpakConfig = ();

@ -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<Kernel>,
}
#[derive(Clone, Debug, RustyValue, Deserialize)]
pub struct Kernel(pub String);

@ -1,8 +0,0 @@
use crate::task;
task!(InstallTimeshift {
name = "install-timeshift"
args = TimeshiftConfig
});
pub type TimeshiftConfig = ();

@ -1,8 +0,0 @@
use crate::task;
task!(InstallZRamD {
name = "install-zramd"
args = ZRamDConfig
});
pub type ZRamDConfig = ();

@ -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<Args = Self::Config>;
type DownScript: Script<Args = Self::Config>;
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),
]
}

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

@ -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<User>,
}
#[derive(Clone, Debug, Deserialize, RustyValue)]
pub struct User {
pub name: String,
pub password: String,
pub sudoer: bool,
pub shell: String,
}

@ -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<P: AsRef<Path>>(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,

Loading…
Cancel
Save