use scheme_edit::{list, quoted_sym, string_lit, sym, Document, Item, Node};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OsFlavor {
Guix,
Nonguix,
Panther,
}
#[derive(Debug, Clone)]
pub enum FsDevice {
Label(String),
Uuid(String),
UuidWithType(String, String),
Path(String),
}
#[derive(Debug, Clone)]
enum LuksSource {
Path(String),
Uuid(String),
}
#[derive(Debug, Clone)]
enum Bootloader {
Efi(String),
Bios(String),
}
#[derive(Debug, Clone)]
struct FileSystemSpec {
mount_point: String,
device: FsDevice,
fs_type: String,
dependencies_mapped: bool,
}
#[derive(Debug, Clone)]
struct UserSpec {
name: String,
comment: String,
groups: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct OsBuilder {
flavor: OsFlavor,
host_name: String,
timezone: String,
locale: String,
keyboard_layout: Option<(String, Option<String>)>,
bootloader: Option<Bootloader>,
luks: Option<(LuksSource, String)>,
file_systems: Vec<FileSystemSpec>,
users: Vec<UserSpec>,
packages: Option<String>,
services: Option<String>,
extra_fields: Vec<(String, String)>,
extra_modules: Vec<String>,
}
impl OsBuilder {
pub fn new(flavor: OsFlavor, host_name: &str, timezone: &str, locale: &str) -> Self {
Self {
flavor,
host_name: host_name.to_string(),
timezone: timezone.to_string(),
locale: locale.to_string(),
keyboard_layout: None,
bootloader: None,
luks: None,
file_systems: Vec::new(),
users: Vec::new(),
packages: None,
services: None,
extra_fields: Vec::new(),
extra_modules: Vec::new(),
}
}
pub fn keyboard_layout(mut self, layout: &str, variant: Option<&str>) -> Self {
self.keyboard_layout = Some((layout.to_string(), variant.map(str::to_string)));
self
}
pub fn bootloader_efi(mut self, esp_mount: &str) -> Self {
self.bootloader = Some(Bootloader::Efi(esp_mount.to_string()));
self
}
pub fn bootloader_bios(mut self, device: &str) -> Self {
self.bootloader = Some(Bootloader::Bios(device.to_string()));
self
}
pub fn luks_root(mut self, partition: &str, mapped_name: &str) -> Self {
self.luks = Some((
LuksSource::Path(partition.to_string()),
mapped_name.to_string(),
));
self
}
pub fn luks_root_uuid(mut self, uuid: &str, mapped_name: &str) -> Self {
self.luks = Some((LuksSource::Uuid(uuid.to_string()), mapped_name.to_string()));
self
}
pub fn file_system(
mut self,
mount_point: &str,
device: FsDevice,
fs_type: &str,
dependencies_mapped: bool,
) -> Self {
self.file_systems.push(FileSystemSpec {
mount_point: mount_point.to_string(),
device,
fs_type: fs_type.to_string(),
dependencies_mapped,
});
self
}
pub fn user(mut self, name: &str, comment: &str, groups: &[&str]) -> Self {
self.users.push(UserSpec {
name: name.to_string(),
comment: comment.to_string(),
groups: groups.iter().map(|g| (*g).to_string()).collect(),
});
self
}
pub fn packages_field(mut self, expr_source: &str) -> Self {
self.packages = Some(expr_source.to_string());
self
}
pub fn services_field(mut self, expr_source: &str) -> Self {
self.services = Some(expr_source.to_string());
self
}
pub fn extra_field(mut self, field: &str, expr_source: &str) -> Self {
self.extra_fields
.push((field.to_string(), expr_source.to_string()));
self
}
pub fn use_modules(mut self, modules: &[&str]) -> Self {
self.extra_modules
.extend(modules.iter().map(|m| (*m).to_string()));
self
}
pub fn build(self) -> String {
let mut out = String::new();
for line in self.preamble_lines() {
out.push_str(&line);
out.push('\n');
}
out.push('\n');
out.push_str(&self.os_node().to_pretty(0));
out.push('\n');
out
}
fn preamble_lines(&self) -> Vec<String> {
let mut lines = vec![
"(use-modules (gnu))".to_string(),
"(use-service-modules networking ssh desktop)".to_string(),
];
match self.flavor {
OsFlavor::Guix => {}
OsFlavor::Nonguix => {
lines.push("(use-modules (nongnu packages linux))".to_string());
lines.push("(use-modules (nongnu system linux-initrd))".to_string());
}
OsFlavor::Panther => {
lines.push("(use-modules (px system os))".to_string());
}
}
for m in &self.extra_modules {
lines.push(format!("(use-modules {m})"));
}
lines
}
fn os_node(&self) -> Node {
let mut fields = vec![sym("operating-system")];
if self.flavor == OsFlavor::Panther {
fields.push(list(vec![sym("inherit"), sym("%os-base")]));
}
fields.push(list(vec![sym("host-name"), string_lit(&self.host_name)]));
fields.push(list(vec![sym("timezone"), string_lit(&self.timezone)]));
fields.push(list(vec![sym("locale"), string_lit(&self.locale)]));
if let Some((layout, variant)) = &self.keyboard_layout {
let mut kb = vec![sym("keyboard-layout"), string_lit(layout)];
if let Some(v) = variant {
kb.push(string_lit(v));
}
fields.push(list(vec![sym("keyboard-layout"), list(kb)]));
}
if let Some(bl) = &self.bootloader {
fields.push(self.bootloader_node(bl));
}
if self.flavor == OsFlavor::Nonguix {
fields.push(list(vec![sym("kernel"), sym("linux")]));
fields.push(list(vec![sym("initrd"), sym("microcode-initrd")]));
fields.push(list(vec![
sym("firmware"),
list(vec![sym("list"), sym("linux-firmware")]),
]));
}
if let Some((source, name)) = &self.luks {
let source_node = match source {
LuksSource::Path(p) => string_lit(p),
LuksSource::Uuid(u) => list(vec![sym("uuid"), string_lit(u)]),
};
fields.push(list(vec![
sym("mapped-devices"),
list(vec![
sym("list"),
list(vec![
sym("mapped-device"),
list(vec![sym("source"), source_node]),
list(vec![sym("target"), string_lit(name)]),
list(vec![sym("type"), sym("luks-device-mapping")]),
]),
]),
]));
}
if !self.file_systems.is_empty() {
let mut fs_list = vec![sym("list")];
fs_list.extend(self.file_systems.iter().map(file_system_node));
fields.push(list(vec![
sym("file-systems"),
list(vec![
sym("append"),
list(fs_list),
sym("%base-file-systems"),
]),
]));
}
if !self.users.is_empty() {
let mut accounts = vec![sym("list")];
accounts.extend(self.users.iter().map(user_account_node));
fields.push(list(vec![
sym("users"),
list(vec![
sym("append"),
list(accounts),
sym("%base-user-accounts"),
]),
]));
} else if !self.extra_fields.iter().any(|(f, _)| f == "users") {
fields.push(list(vec![sym("users"), sym("%base-user-accounts")]));
}
if let Some(p) = &self.packages {
fields.push(list(vec![sym("packages"), parse_expr(p)]));
}
if let Some(s) = &self.services {
fields.push(list(vec![sym("services"), parse_expr(s)]));
}
for (field, expr) in &self.extra_fields {
fields.push(list(vec![sym(field), parse_expr(expr)]));
}
list(fields)
}
fn bootloader_node(&self, bl: &Bootloader) -> Node {
let (bl_sym, target) = match bl {
Bootloader::Efi(esp) => ("grub-efi-bootloader", esp),
Bootloader::Bios(dev) => ("grub-bootloader", dev),
};
let mut cfg = vec![
sym("bootloader-configuration"),
list(vec![sym("bootloader"), sym(bl_sym)]),
list(vec![
sym("targets"),
list(vec![sym("list"), string_lit(target)]),
]),
];
if self.keyboard_layout.is_some() {
cfg.push(list(vec![sym("keyboard-layout"), sym("keyboard-layout")]));
}
list(vec![sym("bootloader"), list(cfg)])
}
}
fn file_system_node(fs: &FileSystemSpec) -> Node {
let device = match &fs.device {
FsDevice::Label(l) => list(vec![sym("file-system-label"), string_lit(l)]),
FsDevice::Uuid(u) => list(vec![sym("uuid"), string_lit(u)]),
FsDevice::UuidWithType(u, tag) => list(vec![sym("uuid"), string_lit(u), quoted_sym(tag)]),
FsDevice::Path(p) => string_lit(p),
};
let mut parts = vec![
sym("file-system"),
list(vec![sym("mount-point"), string_lit(&fs.mount_point)]),
list(vec![sym("device"), device]),
list(vec![sym("type"), string_lit(&fs.fs_type)]),
];
if fs.dependencies_mapped {
parts.push(list(vec![sym("dependencies"), sym("mapped-devices")]));
}
list(parts)
}
fn user_account_node(u: &UserSpec) -> Node {
let mut groups = vec![sym("list")];
groups.extend(u.groups.iter().map(|g| string_lit(g)));
list(vec![
sym("user-account"),
list(vec![sym("name"), string_lit(&u.name)]),
list(vec![sym("comment"), string_lit(&u.comment)]),
list(vec![sym("group"), string_lit("users")]),
list(vec![sym("supplementary-groups"), list(groups)]),
])
}
fn parse_expr(src: &str) -> Node {
let doc =
Document::parse(src).unwrap_or_else(|e| panic!("invalid scheme expression `{src}`: {e}"));
doc.items
.into_iter()
.find_map(|i| match i {
Item::Node(n) => Some(n),
_ => None,
})
.unwrap_or_else(|| panic!("empty scheme expression `{src}`"))
}