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