Skip to main content

xtask_todo_lib/devshell/vm/config/
mod.rs

1//! Environment-driven configuration for optional devshell VM execution (`DEVSHELL_VM`, backend, Lima name).
2
3mod constants;
4mod repo;
5#[cfg(test)]
6mod tests;
7
8pub use constants::*;
9#[cfg(all(feature = "beta-vm", target_os = "windows"))]
10pub(crate) use repo::{devshell_repo_root_from_path, devshell_repo_root_with_containerfile};
11
12/// How the devshell workspace is backed: memory VFS + push/pull (**[`WorkspaceMode::Sync`]**) vs guest-primary (**[`WorkspaceMode::Guest`]**, planned).
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum WorkspaceMode {
15    /// Mode S: in-memory `Vfs` authority; `cargo`/`rustup` sync with guest when using γ.
16    Sync,
17    /// Mode P: guest mount is the source of truth for the project tree (incremental implementation).
18    Guest,
19}
20
21/// Read [`ENV_DEVSHELL_VM_WORKSPACE_MODE`] from the environment.
22#[must_use]
23pub fn workspace_mode_from_env() -> WorkspaceMode {
24    match std::env::var(ENV_DEVSHELL_VM_WORKSPACE_MODE) {
25        Ok(s) if s.trim().eq_ignore_ascii_case("guest") => WorkspaceMode::Guest,
26        _ => WorkspaceMode::Sync,
27    }
28}
29
30/// Reads [`ENV_DEVSHELL_VM_EXEC_TIMEOUT_MS`]: positive integer milliseconds for β **`exec`** JSON
31/// **`timeout_ms`**, or [`None`] if unset, invalid, or zero.
32#[must_use]
33pub fn exec_timeout_ms_from_env() -> Option<u64> {
34    std::env::var(ENV_DEVSHELL_VM_EXEC_TIMEOUT_MS)
35        .ok()
36        .and_then(|s| s.trim().parse::<u64>().ok())
37        .filter(|&ms| ms > 0)
38}
39
40/// Parsed VM-related environment.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct VmConfig {
43    /// `DEVSHELL_VM` enabled.
44    pub enabled: bool,
45    /// Raw backend string (trimmed); see `ENV_DEVSHELL_VM_BACKEND` for defaults.
46    pub backend: String,
47    /// Eager VM/session start when REPL opens (vs lazy on first `rustup`/`cargo`).
48    pub eager_start: bool,
49    /// Lima instance name.
50    pub lima_instance: String,
51}
52
53fn truthy(s: &str) -> bool {
54    let s = s.trim();
55    s == "1"
56        || s.eq_ignore_ascii_case("true")
57        || s.eq_ignore_ascii_case("yes")
58        || s.eq_ignore_ascii_case("on")
59}
60
61fn falsy(s: &str) -> bool {
62    let s = s.trim();
63    s == "0"
64        || s.eq_ignore_ascii_case("false")
65        || s.eq_ignore_ascii_case("no")
66        || s.eq_ignore_ascii_case("off")
67}
68
69fn default_backend_for_release() -> String {
70    #[cfg(all(windows, feature = "beta-vm"))]
71    {
72        return "beta".to_string();
73    }
74    #[cfg(unix)]
75    {
76        "lima".to_string()
77    }
78    #[cfg(not(any(unix, all(windows, feature = "beta-vm"))))]
79    {
80        "host".to_string()
81    }
82}
83
84fn vm_enabled_from_env() -> bool {
85    if cfg!(test) {
86        return std::env::var(ENV_DEVSHELL_VM).is_ok_and(|s| truthy(&s));
87    }
88    match std::env::var(ENV_DEVSHELL_VM) {
89        Err(_) => true,
90        Ok(s) if s.trim().is_empty() => false,
91        Ok(s) if falsy(&s) => false,
92        Ok(s) => truthy(&s),
93    }
94}
95
96fn backend_from_env() -> String {
97    let from_var = std::env::var(ENV_DEVSHELL_VM_BACKEND)
98        .ok()
99        .map(|s| s.trim().to_string())
100        .filter(|s| !s.is_empty());
101    if let Some(b) = from_var {
102        return b;
103    }
104    if cfg!(test) {
105        "auto".to_string()
106    } else {
107        default_backend_for_release()
108    }
109}
110
111impl VmConfig {
112    /// Read configuration from process environment.
113    #[must_use]
114    pub fn from_env() -> Self {
115        let enabled = vm_enabled_from_env();
116
117        let backend = backend_from_env();
118
119        let eager_start = std::env::var(ENV_DEVSHELL_VM_EAGER).is_ok_and(|s| truthy(&s));
120
121        let lima_instance = std::env::var(ENV_DEVSHELL_VM_LIMA_INSTANCE)
122            .ok()
123            .map(|s| s.trim().to_string())
124            .filter(|s| !s.is_empty())
125            .unwrap_or_else(|| "devshell-rust".to_string());
126
127        Self {
128            enabled,
129            backend,
130            eager_start,
131            lima_instance,
132        }
133    }
134
135    /// Config with VM mode off (for tests).
136    #[must_use]
137    pub const fn disabled() -> Self {
138        Self {
139            enabled: false,
140            backend: String::new(),
141            eager_start: false,
142            lima_instance: String::new(),
143        }
144    }
145
146    /// Normalized backend: `host` and `auto` use the host temp sandbox; `lima` uses γ (Unix; see `docs/devshell-vm-gamma.md`).
147    #[must_use]
148    pub fn use_host_sandbox(&self) -> bool {
149        let b = self.backend.to_ascii_lowercase();
150        b == "host" || b == "auto" || b.is_empty()
151    }
152
153    /// Effective workspace mode after combining [`workspace_mode_from_env`] with VM availability (guest-primary design §6).
154    ///
155    /// Returns [`WorkspaceMode::Guest`] only when the user requested **`guest`**, [`VmConfig::enabled`] is true,
156    /// [`VmConfig::use_host_sandbox`] is false, and the backend is **`lima`** or **`beta`**. Otherwise returns
157    /// [`WorkspaceMode::Sync`] without erroring.
158    #[must_use]
159    pub fn workspace_mode_effective(&self) -> WorkspaceMode {
160        let requested = workspace_mode_from_env();
161        if matches!(requested, WorkspaceMode::Sync) {
162            return WorkspaceMode::Sync;
163        }
164
165        let effective = if !self.enabled || self.use_host_sandbox() {
166            WorkspaceMode::Sync
167        } else {
168            let b = self.backend.to_ascii_lowercase();
169            if b == "lima" || b == "beta" {
170                WorkspaceMode::Guest
171            } else {
172                WorkspaceMode::Sync
173            }
174        };
175
176        if matches!(requested, WorkspaceMode::Guest)
177            && matches!(effective, WorkspaceMode::Sync)
178            && !cfg!(test)
179        {
180            eprintln!(
181                "dev_shell: DEVSHELL_VM_WORKSPACE_MODE=guest requires VM enabled and backend lima or beta; using sync mode."
182            );
183        }
184
185        effective
186    }
187}