Skip to main content

tatara_vm/
config.rs

1//! Typed VM definitions — the Lisp authoring surface.
2
3use serde::{Deserialize, Serialize};
4use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize,
7         gen_platform::TypedDispatcher,
8         gen_platform::Discriminant,
9         gen_platform::IsVariant,
10         gen_platform::FromStrKind)]
11#[discriminant(also_display)]
12#[serde(tag = "kind")]
13pub enum Hypervisor {
14    /// Apple Virtualization.framework via the `vfkit` CLI — Linux guests.
15    /// Default on Darwin.
16    Vfkit,
17    /// Apple Virtualization.framework for **Darwin guests** (Apple Silicon
18    /// hosts only). Needs a Darwin IPSW; bootable via `tart` or Apple's own
19    /// `macosvm` tool.
20    VfkitDarwin,
21    /// Portable fallback. KVM on Linux, HVF on Darwin/Intel.
22    Qemu,
23    /// pleme-io's Rust-native Virtualization.framework wrapper — drives the
24    /// VM in-process, no CLI shell-out.
25    Kasou,
26    /// pleme-io's Rust-native libkrun (Hypervisor.framework) wrapper — the
27    /// lowest blessed macOS VM interface, driven in-process via `tateru`, no
28    /// CLI shell-out. The default máquina engine (see `theory/MAQUINA.md`).
29    Libkrun,
30}
31
32impl Default for Hypervisor {
33    fn default() -> Self {
34        Self::Vfkit
35    }
36}
37
38// Fleet-wide dispatcher-catalog registration. Tatara becomes
39// the TENTH consumer class adopting gen-platform's typed-
40// dispatcher catamorphism (after gen / caixa / wasm-platform /
41// cofre / shigoto / engenho / magma / kura / pangea). See
42// theory/UNIFIED-COMPUTING-MODEL.md §VI.
43gen_platform::register_dispatcher!("tatara.hypervisor", Hypervisor);
44
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(tag = "kind")]
47pub enum GuestKernel {
48    /// Pull the kernel derivation from nixpkgs (or another pkg-set).
49    Bridge { attr_path: String },
50    /// Raw tatara Derivation for a fully-custom kernel build.
51    Custom { derivation: tatara_nix::Derivation },
52    /// Darwin guest boot assets (IPSW + RestoreImage). Only meaningful with
53    /// `Hypervisor::VfkitDarwin` / `Hypervisor::Kasou` on an Apple-Silicon
54    /// host. The IPSW path is passed through to Virtualization.framework's
55    /// `MacOSRestoreImage` API.
56    DarwinIpsw { ipsw_path: String },
57}
58
59impl Default for GuestKernel {
60    fn default() -> Self {
61        Self::Bridge {
62            attr_path: "linuxPackages.kernel".into(),
63        }
64    }
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(tag = "kind")]
69pub enum GuestRootfs {
70    /// Derive the rootfs from a tatara-os `SystemConfig` by name (resolved at
71    /// emit time against a provided SystemConfig registry).
72    System { name: String },
73    /// Pre-built rootfs image as a tatara Derivation (ext4 image in $out/rootfs.img).
74    Image { derivation: tatara_nix::Derivation },
75    /// Bridge to a nixpkgs attribute producing a disk image (e.g., `nixos-generators.qcow`).
76    Bridge { attr_path: String },
77}
78
79impl Default for GuestRootfs {
80    fn default() -> Self {
81        Self::Bridge {
82            attr_path: "nixpkgs-images.minimal-rootfs".into(),
83        }
84    }
85}
86
87/// Unit-variant enum serialized as plain strings: `"Nat"`, `"Bridge"`, `"None"`.
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
89pub enum NetworkKind {
90    /// NAT via the host.
91    Nat,
92    /// Bridge to a host interface.
93    Bridge,
94    /// No network.
95    None,
96}
97
98impl Default for NetworkKind {
99    fn default() -> Self {
100        Self::Nat
101    }
102}
103
104#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct NetworkSpec {
107    #[serde(default)]
108    pub kind: NetworkKind,
109    #[serde(default)]
110    pub subnet: Option<String>,
111    #[serde(default)]
112    pub host_interface: Option<String>,
113}
114
115impl Default for NetworkSpec {
116    fn default() -> Self {
117        Self {
118            kind: NetworkKind::Nat,
119            subnet: None,
120            host_interface: None,
121        }
122    }
123}
124
125#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct ShareSpec {
128    /// Host-side path (absolute).
129    pub host: String,
130    /// Guest mount point (absolute).
131    pub guest: String,
132    /// Read-only share? Default false.
133    #[serde(default)]
134    pub read_only: bool,
135}
136
137/// The whole guest as one typed value. Parses from `(defvm …)` forms.
138///
139/// ```lisp
140/// (defvm plex-guest
141///   :cpus 4
142///   :memory_mib 4096
143///   :hypervisor (:kind "Vfkit")
144///   :kernel     (:kind "Bridge" :attr_path "linuxPackages.kernel")
145///   :rootfs     (:kind "Bridge" :attr_path "nixpkgs-images.minimal-rootfs")
146///   :network    (:kind "Nat")
147///   :cmdline    ("console=hvc0" "init=/bin/tatara-init"))
148/// ```
149#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151#[tatara(keyword = "defvm")]
152pub struct VmSpec {
153    pub name: String,
154    #[serde(default = "default_cpus")]
155    pub cpus: u32,
156    #[serde(default = "default_mem_mib")]
157    pub memory_mib: u32,
158    #[serde(default)]
159    pub hypervisor: Hypervisor,
160    #[serde(default)]
161    pub kernel: GuestKernel,
162    #[serde(default)]
163    pub initrd: Option<tatara_nix::Derivation>,
164    #[serde(default)]
165    pub rootfs: GuestRootfs,
166    #[serde(default)]
167    pub network: NetworkSpec,
168    #[serde(default)]
169    pub shares: Vec<ShareSpec>,
170    /// Kernel command-line. Joined with spaces in emitted config.
171    #[serde(default = "default_cmdline")]
172    pub cmdline: Vec<String>,
173}
174
175fn default_cpus() -> u32 {
176    2
177}
178
179fn default_mem_mib() -> u32 {
180    2048
181}
182
183fn default_cmdline() -> Vec<String> {
184    vec!["console=hvc0".into(), "init=/bin/tatara-init".into()]
185}
186
187impl VmSpec {
188    /// The baseline used in examples — proves the Lisp → typed chain without
189    /// any external files. Matches what our vfkit emitter wants.
190    pub fn plex_default(name: impl Into<String>) -> Self {
191        Self {
192            name: name.into(),
193            cpus: default_cpus(),
194            memory_mib: default_mem_mib(),
195            hypervisor: Hypervisor::Vfkit,
196            kernel: GuestKernel::default(),
197            initrd: None,
198            rootfs: GuestRootfs::default(),
199            network: NetworkSpec::default(),
200            shares: vec![],
201            cmdline: default_cmdline(),
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use tatara_lisp::{domain::TataraDomain, read};
210
211    #[test]
212    fn minimal_defvm_parses() {
213        // Top-level kwargs use kebab-case (derive macro convention);
214        // nested serde types use snake_case inside their attr-sets.
215        let forms = read(
216            r#"(defvm
217                 :name       "plex-guest"
218                 :cpus       2
219                 :memory-mib 2048)"#,
220        )
221        .unwrap();
222        let v = VmSpec::compile_from_sexp(&forms[0]).unwrap();
223        assert_eq!(v.name, "plex-guest");
224        assert_eq!(v.cpus, 2);
225        assert_eq!(v.memory_mib, 2048);
226    }
227
228    #[test]
229    fn full_defvm_parses_with_shares_and_network() {
230        let forms = read(
231            r#"(defvm
232                 :name       "plex-guest"
233                 :cpus       4
234                 :memory-mib 4096
235                 :hypervisor (:kind "Vfkit")
236                 :kernel     (:kind "Bridge" :attr_path "linuxPackages.kernel")
237                 :rootfs     (:kind "Bridge" :attr_path "nixpkgs-images.minimal-rootfs")
238                 :network    (:kind "Nat" :subnet "10.200.0.0/24")
239                 :shares     ((:host "/Users/drzzln/code" :guest "/mnt/code" :read_only #f))
240                 :cmdline    ("console=hvc0" "init=/bin/tatara-init"))"#,
241        )
242        .unwrap();
243        let v = VmSpec::compile_from_sexp(&forms[0]).unwrap();
244        assert_eq!(v.cpus, 4);
245        assert_eq!(v.memory_mib, 4096);
246        assert_eq!(v.shares.len(), 1);
247        assert_eq!(v.shares[0].host, "/Users/drzzln/code");
248        assert_eq!(v.shares[0].guest, "/mnt/code");
249        match v.network.kind {
250            NetworkKind::Nat => (),
251            _ => panic!("expected Nat"),
252        }
253    }
254
255    #[test]
256    fn defaults_are_darwin_friendly() {
257        let v = VmSpec::plex_default("plex");
258        assert!(matches!(v.hypervisor, Hypervisor::Vfkit));
259        assert!(v.cmdline.iter().any(|a| a.contains("tatara-init")));
260    }
261}