Skip to main content

xtask_todo_lib/devshell/vm/
mod.rs

1//! Optional session-scoped VM execution (γ CLI / β sidecar): host [`SessionHolder::Host`], Unix γ [`SessionHolder::Gamma`].
2
3#![allow(clippy::pedantic, clippy::nursery)]
4
5use std::cell::RefCell;
6use std::io::Write;
7use std::rc::Rc;
8
9mod config;
10#[cfg(unix)]
11mod lima_diagnostics;
12#[cfg(all(unix, feature = "beta-vm"))]
13mod session_beta;
14#[cfg(unix)]
15mod session_gamma;
16mod session_host;
17pub mod sync;
18
19pub use config::{
20    VmConfig, ENV_DEVSHELL_VM, ENV_DEVSHELL_VM_BACKEND, ENV_DEVSHELL_VM_EAGER,
21    ENV_DEVSHELL_VM_LIMA_INSTANCE, ENV_DEVSHELL_VM_SOCKET,
22};
23#[cfg(unix)]
24pub use lima_diagnostics::ENV_DEVSHELL_VM_LIMA_HINTS;
25#[cfg(unix)]
26pub use session_gamma::{
27    GammaSession, ENV_DEVSHELL_VM_GUEST_WORKSPACE, ENV_DEVSHELL_VM_LIMACTL,
28    ENV_DEVSHELL_VM_STOP_ON_EXIT, ENV_DEVSHELL_VM_WORKSPACE_PARENT,
29};
30pub use session_host::HostSandboxSession;
31pub use sync::{pull_workspace_to_vfs, push_full, push_incremental, VmSyncError};
32
33use std::process::ExitStatus;
34
35use super::sandbox;
36use super::vfs::Vfs;
37
38/// Errors from VM session operations.
39#[derive(Debug)]
40pub enum VmError {
41    Sandbox(sandbox::SandboxError),
42    Sync(VmSyncError),
43    /// Backend not implemented on this OS or not wired yet.
44    BackendNotImplemented(&'static str),
45    /// Lima / `limactl` or γ orchestration failure (message for stderr).
46    Lima(String),
47    /// β IPC / `devshell-vm` protocol failure.
48    Ipc(String),
49}
50
51impl std::fmt::Display for VmError {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::Sandbox(e) => write!(f, "{e}"),
55            Self::Sync(e) => write!(f, "{e}"),
56            Self::BackendNotImplemented(s) => write!(f, "vm backend not implemented: {s}"),
57            Self::Lima(s) => f.write_str(s),
58            Self::Ipc(s) => f.write_str(s),
59        }
60    }
61}
62
63impl std::error::Error for VmError {
64    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
65        match self {
66            Self::Sandbox(e) => Some(e),
67            Self::Sync(e) => Some(e),
68            Self::BackendNotImplemented(_) | Self::Lima(_) | Self::Ipc(_) => None,
69        }
70    }
71}
72
73/// Abstraction for a devshell execution session (host temp dir, γ VM, or β sidecar).
74pub trait VmExecutionSession {
75    /// Prepare the session (e.g. start VM, initial push). No-op for host temp export.
76    fn ensure_ready(&mut self, vfs: &Vfs, vfs_cwd: &str) -> Result<(), VmError>;
77
78    /// Run `rustup` or `cargo` with cwd matching `vfs_cwd`; update `vfs` as defined by the backend.
79    fn run_rust_tool(
80        &mut self,
81        vfs: &mut Vfs,
82        vfs_cwd: &str,
83        program: &str,
84        args: &[String],
85    ) -> Result<ExitStatus, VmError>;
86
87    /// Tear down (e.g. final pull, stop VM).
88    fn shutdown(&mut self, vfs: &mut Vfs, vfs_cwd: &str) -> Result<(), VmError>;
89}
90
91/// Active VM / sandbox backend for one REPL or script run.
92#[derive(Debug)]
93pub enum SessionHolder {
94    Host(HostSandboxSession),
95    /// γ: Lima + host workspace sync (Unix only).
96    #[cfg(unix)]
97    Gamma(GammaSession),
98    /// β: JSON-lines over Unix socket to `devshell-vm` (Unix + `beta-vm` feature).
99    #[cfg(all(unix, feature = "beta-vm"))]
100    Beta(session_beta::BetaSession),
101}
102
103impl SessionHolder {
104    /// Build session from config.
105    ///
106    /// # Errors
107    /// On Unix, `DEVSHELL_VM_BACKEND=lima` uses [`GammaSession`]; fails with [`VmError::Lima`] if `limactl` is missing.
108    /// On non-Unix, `lima` returns [`VmError::BackendNotImplemented`].
109    pub fn try_from_config(config: &VmConfig) -> Result<Self, VmError> {
110        if !config.enabled {
111            return Ok(Self::Host(HostSandboxSession::new()));
112        }
113        if config.use_host_sandbox() {
114            return Ok(Self::Host(HostSandboxSession::new()));
115        }
116        #[cfg(all(unix, feature = "beta-vm"))]
117        if config.backend.eq_ignore_ascii_case("beta") {
118            return session_beta::BetaSession::new(config).map(SessionHolder::Beta);
119        }
120        #[cfg(not(all(unix, feature = "beta-vm")))]
121        if config.backend.eq_ignore_ascii_case("beta") {
122            return Err(VmError::BackendNotImplemented(
123                "DEVSHELL_VM_BACKEND=beta requires Unix and building xtask-todo-lib with `--features beta-vm`",
124            ));
125        }
126        #[cfg(unix)]
127        if config.backend.eq_ignore_ascii_case("lima") {
128            return GammaSession::new(config).map(SessionHolder::Gamma);
129        }
130        #[cfg(not(unix))]
131        if config.backend.eq_ignore_ascii_case("lima") {
132            return Err(VmError::BackendNotImplemented(
133                "lima backend is only supported on Linux and macOS",
134            ));
135        }
136        Err(VmError::BackendNotImplemented(
137            "unknown DEVSHELL_VM_BACKEND (try host, auto, lima, or beta); see docs/devshell-vm-gamma.md",
138        ))
139    }
140
141    /// Host sandbox only (tests and callers that do not read `VmConfig`).
142    #[must_use]
143    pub fn new_host() -> Self {
144        Self::Host(HostSandboxSession::new())
145    }
146
147    pub fn ensure_ready(&mut self, vfs: &Vfs, vfs_cwd: &str) -> Result<(), VmError> {
148        match self {
149            Self::Host(s) => VmExecutionSession::ensure_ready(s, vfs, vfs_cwd),
150            #[cfg(unix)]
151            Self::Gamma(s) => VmExecutionSession::ensure_ready(s, vfs, vfs_cwd),
152            #[cfg(all(unix, feature = "beta-vm"))]
153            Self::Beta(s) => VmExecutionSession::ensure_ready(s, vfs, vfs_cwd),
154        }
155    }
156
157    pub fn run_rust_tool(
158        &mut self,
159        vfs: &mut Vfs,
160        vfs_cwd: &str,
161        program: &str,
162        args: &[String],
163    ) -> Result<ExitStatus, VmError> {
164        match self {
165            Self::Host(s) => VmExecutionSession::run_rust_tool(s, vfs, vfs_cwd, program, args),
166            #[cfg(unix)]
167            Self::Gamma(s) => VmExecutionSession::run_rust_tool(s, vfs, vfs_cwd, program, args),
168            #[cfg(all(unix, feature = "beta-vm"))]
169            Self::Beta(s) => VmExecutionSession::run_rust_tool(s, vfs, vfs_cwd, program, args),
170        }
171    }
172
173    pub fn shutdown(&mut self, vfs: &mut Vfs, vfs_cwd: &str) -> Result<(), VmError> {
174        match self {
175            Self::Host(s) => VmExecutionSession::shutdown(s, vfs, vfs_cwd),
176            #[cfg(unix)]
177            Self::Gamma(s) => VmExecutionSession::shutdown(s, vfs, vfs_cwd),
178            #[cfg(all(unix, feature = "beta-vm"))]
179            Self::Beta(s) => VmExecutionSession::shutdown(s, vfs, vfs_cwd),
180        }
181    }
182}
183
184/// Build [`SessionHolder`] from the environment. On failure (e.g. default γ Lima but `limactl` missing), writes to `stderr` and returns `Err(())`.
185/// Use **`DEVSHELL_VM=off`** or **`DEVSHELL_VM_BACKEND=host`** to force the host temp sandbox.
186#[allow(clippy::result_unit_err)] // binary entry uses `()`; message already on stderr
187pub fn try_session_rc(stderr: &mut dyn Write) -> Result<Rc<RefCell<SessionHolder>>, ()> {
188    let config = VmConfig::from_env();
189    match SessionHolder::try_from_config(&config) {
190        Ok(s) => Ok(Rc::new(RefCell::new(s))),
191        Err(e) => {
192            let _ = writeln!(stderr, "dev_shell: {e}");
193            Err(())
194        }
195    }
196}