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 # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
.env .env
# Misc
test-config.json

256
Cargo.lock generated

@ -12,6 +12,17 @@ dependencies = [
"regex", "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]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.17.0" version = "0.17.0"
@ -33,7 +44,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.7",
"once_cell", "once_cell",
"version_check", "version_check",
] ]
@ -265,9 +276,9 @@ dependencies = [
[[package]] [[package]]
name = "cargo_toml" name = "cargo_toml"
version = "0.12.4" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a621d5d6d6c8d086dbaf1fe659981da41a1b63c6bdbba30b4dbb592c6d3bd49" checksum = "aa0e3586af56b3bfa51fca452bd56e8dbbbd5d8d81cbf0b7e4e35b695b537eb8"
dependencies = [ dependencies = [
"serde", "serde",
"toml", "toml",
@ -334,7 +345,7 @@ checksum = "29c39203181991a7dd4343b8005bd804e7a9a37afb8ac070e43771e8c820bbde"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-tz-build", "chrono-tz-build",
"phf", "phf 0.11.1",
] ]
[[package]] [[package]]
@ -344,8 +355,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f509c3a87b33437b05e2458750a0700e5bdd6956176773e6c7d6dd15a283a0c" checksum = "6f509c3a87b33437b05e2458750a0700e5bdd6956176773e6c7d6dd15a283a0c"
dependencies = [ dependencies = [
"parse-zoneinfo", "parse-zoneinfo",
"phf", "phf 0.11.1",
"phf_codegen", "phf_codegen 0.11.1",
] ]
[[package]] [[package]]
@ -361,9 +372,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.0.14" version = "4.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea54a38e4bce14ff6931c72e5b3c43da7051df056913d4e7e1fcdb1c03df69d" checksum = "06badb543e734a2d6568e19a40af66ed5364360b9226184926f89d229b4b4267"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
@ -756,9 +767,9 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]] [[package]]
name = "embed-nu" name = "embed-nu"
version = "0.3.0" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adf2e40b13c4f1e3cd0d5494d8a225c3d5064f1c83ff373b04e783b0df7b1c23" checksum = "9fd30e83a59e9eeb1931ae5b43d34aeea424d9c1bd88bb79e65bdec1e0676642"
dependencies = [ dependencies = [
"nu-command", "nu-command",
"nu-engine", "nu-engine",
@ -1045,6 +1056,17 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.7" version = "0.2.7"
@ -1420,6 +1442,25 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "kernel32-sys" name = "kernel32-sys"
version = "0.2.2" version = "0.2.2"
@ -1735,6 +1776,15 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "nom" name = "nom"
version = "1.2.4" version = "1.2.4"
@ -1880,7 +1930,7 @@ dependencies = [
"pathdiff", "pathdiff",
"powierza-coefficient", "powierza-coefficient",
"quick-xml 0.23.1", "quick-xml 0.23.1",
"rand", "rand 0.8.5",
"rayon", "rayon",
"reedline", "reedline",
"reqwest", "reqwest",
@ -1903,7 +1953,7 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
"url", "url",
"users 0.11.0", "users 0.11.0",
"uuid", "uuid 1.2.1",
"wax", "wax",
"windows", "windows",
] ]
@ -1977,7 +2027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa9667e6ddf13e01a5f192caa8570b19f062dfe1fa70af6f28b2ab56c369d57" checksum = "9fa9667e6ddf13e01a5f192caa8570b19f062dfe1fa70af6f28b2ab56c369d57"
dependencies = [ dependencies = [
"nu-ansi-term", "nu-ansi-term",
"rand", "rand 0.8.5",
] ]
[[package]] [[package]]
@ -2337,13 +2387,32 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 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]] [[package]]
name = "phf" name = "phf"
version = "0.11.1" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
dependencies = [ 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]] [[package]]
@ -2352,8 +2421,18 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
dependencies = [ dependencies = [
"phf_generator", "phf_generator 0.11.1",
"phf_shared", "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]] [[package]]
@ -2362,8 +2441,17 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
dependencies = [ dependencies = [
"phf_shared", "phf_shared 0.11.1",
"rand", "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]] [[package]]
@ -2463,6 +2551,21 @@ dependencies = [
"rustix", "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]] [[package]]
name = "pure-rust-locales" name = "pure-rust-locales"
version = "0.5.6" version = "0.5.6"
@ -2507,6 +2610,20 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -2514,8 +2631,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha 0.3.1",
"rand_core", "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]] [[package]]
@ -2525,7 +2652,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "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]] [[package]]
@ -2534,7 +2670,25 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ 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]] [[package]]
@ -2576,7 +2730,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.7",
"redox_syscall", "redox_syscall",
"thiserror", "thiserror",
] ]
@ -2780,11 +2934,12 @@ checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
[[package]] [[package]]
name = "rusty-value" name = "rusty-value"
version = "0.5.1" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48dca1efc1cbe6434e3ed1afb6ce7bb3152af7a0f568bb5f5ea316388e333c3e" checksum = "eb7ccab1b447c1ba376a608ef1fd25e3b7872e9162a1147274c263e7675a7c98"
dependencies = [ dependencies = [
"rusty-value-derive", "rusty-value-derive",
"serde_json",
] ]
[[package]] [[package]]
@ -3363,12 +3518,15 @@ dependencies = [
"embed-nu", "embed-nu",
"lazy_static", "lazy_static",
"paste", "paste",
"rusty-value",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
"tokio", "tokio",
"toml",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"valico",
] ]
[[package]] [[package]]
@ -3551,6 +3709,15 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68" 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]] [[package]]
name = "url" name = "url"
version = "2.3.1" version = "2.3.1"
@ -3594,13 +3761,44 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" 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]] [[package]]
name = "uuid" name = "uuid"
version = "1.2.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
dependencies = [ 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]] [[package]]
@ -3675,6 +3873,12 @@ dependencies = [
"try-lock", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.0+wasi-snapshot-preview1" 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
clap = { version = "4.0.14", features = ["derive"] } clap = { version = "4.0.17", features = ["derive"] }
color-eyre = "0.6.2" color-eyre = "0.6.2"
dotenv = "0.15.0" dotenv = "0.15.0"
embed-nu = "0.3.0" embed-nu = "0.3.3"
lazy_static = "1.4.0" lazy_static = "1.4.0"
paste = "1.0.9" paste = "1.0.9"
rusty-value = { version = "0.6.0", features = ["derive", "json"] }
serde = { version = "1.0.145", features = ["derive"] } serde = { version = "1.0.145", features = ["derive"] }
serde_json = "1.0.86" serde_json = "1.0.86"
thiserror = "1.0.37" thiserror = "1.0.37"
tokio = { version = "1.21.2", features = ["rt", "io-std", "io-util", "process", "time", "macros", "tracing", "fs"] } 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 = "0.1.37"
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"
valico = "3.6.1"
[build-dependencies] [build-dependencies]
cargo_toml = "0.12.4" cargo_toml = "0.13.0"
serde = { version = "1.0.145", features = ["derive"] } 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. 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. Each distro has its own folder with several subfolders corresponding to all integration tasks.
``` ```
<distro> <distro>
| distro.toml
| config.schema.json
| <task> | <task>
| up.nu | up.nu
| down.nu | down.nu
@ -23,6 +25,68 @@ Each distro has its own folder with several subfolders corresponding to all inte
| down.nu | 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. 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. 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` # Applies all system changes of `setup-users`
def main [cfg] { def main [cfg] {
echo "Executing up task `setup-users` with config" $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 /// Generates empty script files for the installation
#[command()] #[command()]
GenerateScripts(GenerateScriptsArgs), GenerateScripts(GenerateScriptsArgs),
/// *For testing purposes only*
/// Generates the JSON for an empty config file
#[command()]
CreateEmptyConfig(CreateEmptyConfigArgs),
} }
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
@ -41,3 +46,10 @@ pub struct GenerateScriptsArgs {
#[arg()] #[arg()]
pub path: PathBuf, 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)] #[derive(Error, Debug)]
pub enum AppError { 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), ScriptNotFound(PathBuf),
#[error("Nu error {0}")] #[error("Nu error when executing script: {0}")]
NuError(#[from] embed_nu::Error), 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), MissingMain(PathBuf),
}
#[error("Failed to execute script")] #[derive(Error, Debug)]
FailedToExecuteScript, pub enum DistroConfigError {
#[error("IO Error when trying to read distro config: {0}")]
Io(#[from] io::Error),
#[error("Missing config")] #[error("Encountered invalid Toml when parsing distro config: {0}")]
MissingConfig, 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), Io(#[from] io::Error),
#[error("JSON deserialization error {0}")] #[error("Encountered invalid JSON when parsing json-schema: {0}")]
JSON(#[from] serde_json::Error), 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 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!( pub(crate) mod utils;
setup_users => SetupUsersTask, use std::path::PathBuf;
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<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 { use distro::{distro_config::DistroConfig, loader::OSConfigLoader};
fn default() -> Self { use error::AppResult;
Self { use task::task_executor::TaskExecutor;
loader: ScriptLoader::new(), pub use utils::generate_script_files;
config: None, 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<TaskExecutor> {
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))
} }

@ -1,12 +1,13 @@
use args::{Args, Command, GenerateScriptsArgs, InstallFromConfigArgs}; use args::{Args, Command, CreateEmptyConfigArgs, GenerateScriptsArgs, InstallFromConfigArgs};
use clap::Parser; use clap::Parser;
use tokio::{fs::OpenOptions, io::AsyncReadExt}; use rusty_value::into_json::{EnumRepr, IntoJson, IntoJsonOptions};
use tourmaline::{config::Config, error::AppResult, generate_script_files, TaskExecutor}; use tokio::fs;
use tourmaline::{distro::OSConfig, error::AppResult, generate_script_files};
mod args; mod args;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() -> color_eyre::Result<()> {
color_eyre::install().unwrap(); color_eyre::install().unwrap();
dotenv::dotenv().unwrap(); dotenv::dotenv().unwrap();
let args = Args::parse(); let args = Args::parse();
@ -14,21 +15,32 @@ async fn main() {
match args.command { match args.command {
Command::InstallFromConfig(args) => install_from_config(args).await, Command::InstallFromConfig(args) => install_from_config(args).await,
Command::GenerateScripts(args) => generate_scripts(args).await, Command::GenerateScripts(args) => generate_scripts(args).await,
} Command::CreateEmptyConfig(args) => generate_empty_config(args).await,
.unwrap(); }?;
Ok(())
} }
/// Installs the distro from a given configuration file
async fn install_from_config(args: InstallFromConfigArgs) -> AppResult<()> { async fn install_from_config(args: InstallFromConfigArgs) -> AppResult<()> {
let mut file = OpenOptions::new().read(true).open(args.path).await?; tourmaline::create_executor(args.path)
let mut cfg_contents = String::new(); .await?
file.read_to_string(&mut cfg_contents).await?; .with_base_tasks()
let config: Config = serde_json::from_str(&cfg_contents)?; .with_custom_tasks()
.execute()
TaskExecutor::with_config(config)
.install_from_config()
.await .await
} }
async fn generate_scripts(args: GenerateScriptsArgs) -> AppResult<()> { async fn generate_scripts(args: GenerateScriptsArgs) -> AppResult<()> {
generate_script_files(args.path).await 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 tokio::fs;
use crate::error::AppResult; use crate::error::AppResult;
use crate::tasks::all_tasks; use crate::task::base_task::ALL_BASE_TASKS;
const DEFAULT_CONFIG_DIR: &str = "/etc"; const DEFAULT_CONFIG_DIR: &str = "/etc";
lazy_static::lazy_static! { 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 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<()> { pub async fn generate_script_files<P: AsRef<Path>>(output: P) -> AppResult<()> {
let output = output.as_ref(); let output = output.as_ref();
let tasks = all_tasks(); for task in &*ALL_BASE_TASKS {
let key_data = task.key_data();
for (name, up_script, down_script) in tasks { let name = key_data.task_name;
let script_dir = output.join(&name); let script_dir = output.join(name);
if !script_dir.exists() { if !script_dir.exists() {
fs::create_dir_all(&script_dir).await?; fs::create_dir_all(&script_dir).await?;
} }
let up_path = output.join(&up_script); let up_path = output.join("up.nu");
let down_path = output.join(down_script); let down_path = output.join("down.nu");
fs::write( fs::write(
&up_path, &up_path,

Loading…
Cancel
Save