Skip to main content

xtask_todo_lib/devshell/vm/session_gamma/session/
exec.rs

1//! Guest `build-essential` / `todo` setup and [`VmExecutionSession`] for γ.
2
3use std::process::ExitStatus;
4
5use super::super::super::super::vfs::Vfs;
6use super::super::super::lima_diagnostics;
7use super::super::super::sync::{pull_workspace_to_vfs, push_incremental};
8use super::super::super::{VmError, VmExecutionSession};
9use super::super::env::ENV_DEVSHELL_VM_STOP_ON_EXIT;
10use super::super::helpers::{
11    auto_build_essential_enabled, auto_build_todo_guest_enabled,
12    cargo_metadata_workspace_and_target, guest_cargo_workspace_dir_for_cwd,
13    guest_dir_for_cwd_inner, guest_todo_hint_enabled, guest_todo_release_dir_for_cwd,
14    shell_single_quote_sh, truthy_env,
15};
16use super::GammaSession;
17
18impl GammaSession {
19    /// If guest has no `gcc`, try Debian/Ubuntu `apt-get install -y build-essential` (non-interactive sudo).
20    fn maybe_ensure_guest_build_essential(&mut self) -> Result<(), VmError> {
21        if self.guest_build_essential_done {
22            return Ok(());
23        }
24
25        if !auto_build_essential_enabled() {
26            self.guest_build_essential_done = true;
27            return Ok(());
28        }
29
30        let probe = self.limactl_shell_script_sh("/", "command -v gcc >/dev/null 2>&1")?;
31        if probe.status.success() {
32            self.guest_build_essential_done = true;
33            return Ok(());
34        }
35
36        eprintln!("dev_shell: guest: no C compiler (gcc) in PATH; attempting apt install build-essential…");
37
38        let has_apt =
39            self.limactl_shell_script_sh("/", "test -x /usr/bin/apt-get && test -x /usr/bin/dpkg")?;
40        if !has_apt.status.success() {
41            eprintln!(
42                "dev_shell: guest: no apt-get/dpkg; install gcc + binutils manually (see docs/devshell-vm-gamma.md)."
43            );
44            self.guest_build_essential_done = true;
45            return Ok(());
46        }
47
48        // `sudo -n` fails if a password is required (no TTY here).
49        const INSTALL_SH: &str = r"set -e
50export DEBIAN_FRONTEND=noninteractive
51if ! sudo -n true 2>/dev/null; then
52  echo 'dev_shell: guest: sudo needs a password; run in the VM: sudo apt update && sudo apt install -y build-essential' >&2
53  exit 1
54fi
55sudo apt-get update -qq
56sudo apt-get install -y -qq build-essential
57";
58
59        let out = self.limactl_shell_script_sh("/", INSTALL_SH)?;
60        if out.status.success() {
61            eprintln!(
62                "dev_shell: guest: build-essential installed (gcc available for cargo link)."
63            );
64        } else {
65            let stderr = String::from_utf8_lossy(&out.stderr);
66            let stdout = String::from_utf8_lossy(&out.stdout);
67            eprintln!(
68                "dev_shell: guest: automatic build-essential install failed (exit {:?}).",
69                out.status.code()
70            );
71            if !stdout.trim().is_empty() {
72                eprintln!("dev_shell: guest stdout: {stdout}");
73            }
74            if !stderr.trim().is_empty() {
75                eprintln!("dev_shell: guest stderr: {stderr}");
76            }
77            eprintln!(
78                "dev_shell: hint: in the guest shell: sudo apt update && sudo apt install -y build-essential"
79            );
80        }
81        self.guest_build_essential_done = true;
82        Ok(())
83    }
84
85    /// If guest has no `todo` in PATH (and no executable under expected `target/release` when mapped), print install hints; optionally run `cargo build` in guest.
86    fn maybe_guest_todo_probe_hint_and_install(&mut self) -> Result<(), VmError> {
87        if self.guest_todo_hint_done {
88            return Ok(());
89        }
90        if !guest_todo_hint_enabled() {
91            self.guest_todo_hint_done = true;
92            return Ok(());
93        }
94
95        let cwd =
96            std::env::current_dir().map_err(|e| VmError::Lima(format!("current_dir: {e}")))?;
97
98        let grel = guest_todo_release_dir_for_cwd(&self.workspace_parent, &self.guest_mount, &cwd);
99        let script = match &grel {
100            Some(gr) => format!(
101                "command -v todo >/dev/null 2>&1 || test -x {}",
102                shell_single_quote_sh(&format!("{gr}/todo"))
103            ),
104            None => "command -v todo >/dev/null 2>&1".to_string(),
105        };
106
107        let probe = self.limactl_shell_script_sh("/", &script)?;
108        if probe.status.success() {
109            self.guest_todo_hint_done = true;
110            return Ok(());
111        }
112
113        eprintln!(
114            "dev_shell: guest: `todo` not found (do not use apt `devtodo` — unrelated package)."
115        );
116
117        let meta = cargo_metadata_workspace_and_target(&cwd);
118        let host_has_todo = meta
119            .as_ref()
120            .map(|(_, td)| td.join("release").join("todo").is_file())
121            .unwrap_or(false);
122
123        if meta.is_err() {
124            eprintln!("dev_shell: hint: from a repo checkout, run: cargo build -p xtask --release --bin todo");
125            eprintln!(
126                "dev_shell: hint: repo outside Lima mount: cargo xtask lima-todo (merges ~/.lima/{}/lima.yaml + restarts VM; use --print-only for fragment only)",
127                self.lima_instance
128            );
129        } else {
130            eprintln!("dev_shell: host: cargo build -p xtask --release --bin todo");
131            if grel.is_none() && host_has_todo {
132                eprintln!(
133                    "dev_shell: host: workspace outside Lima mount — run: cargo xtask lima-todo"
134                );
135                eprintln!(
136                    "dev_shell: host: (merges `mounts` + `env.PATH` into ~/.lima/{}/lima.yaml and runs limactl stop/start unless --no-restart)",
137                    self.lima_instance
138                );
139            }
140        }
141
142        if let Some(ref gw) =
143            guest_cargo_workspace_dir_for_cwd(&self.workspace_parent, &self.guest_mount, &cwd)
144        {
145            eprintln!("dev_shell: guest: cd {gw} && cargo build -p xtask --release --bin todo");
146            if auto_build_todo_guest_enabled() {
147                let q = shell_single_quote_sh(gw);
148                let build_sh = format!(
149                    "set -e
150cd {q}
151if ! command -v cargo >/dev/null 2>&1; then
152  echo 'dev_shell: guest: cargo not in PATH; install Rust in the VM first (see docs/devshell-vm-gamma.md).' >&2
153  exit 1
154fi
155cargo build -p xtask --release --bin todo
156"
157                );
158                let out = self.limactl_shell_script_sh("/", &build_sh)?;
159                if out.status.success() {
160                    eprintln!("dev_shell: guest: built target/release/todo.");
161                } else {
162                    let stderr = String::from_utf8_lossy(&out.stderr);
163                    let stdout = String::from_utf8_lossy(&out.stdout);
164                    eprintln!(
165                        "dev_shell: guest: automatic `cargo build` for todo failed (exit {:?}).",
166                        out.status.code()
167                    );
168                    if !stdout.trim().is_empty() {
169                        eprintln!("dev_shell: guest stdout: {stdout}");
170                    }
171                    if !stderr.trim().is_empty() {
172                        eprintln!("dev_shell: guest stderr: {stderr}");
173                    }
174                }
175            }
176        }
177
178        self.guest_todo_hint_done = true;
179        Ok(())
180    }
181}
182
183impl VmExecutionSession for GammaSession {
184    fn ensure_ready(&mut self, _vfs: &Vfs, _vfs_cwd: &str) -> Result<(), VmError> {
185        self.limactl_ensure_running()?;
186        self.maybe_ensure_guest_build_essential()?;
187        self.maybe_guest_todo_probe_hint_and_install()?;
188        Ok(())
189    }
190
191    fn run_rust_tool(
192        &mut self,
193        vfs: &mut Vfs,
194        vfs_cwd: &str,
195        program: &str,
196        args: &[String],
197    ) -> Result<ExitStatus, VmError> {
198        self.limactl_ensure_running()?;
199        if self.sync_vfs_with_workspace {
200            push_incremental(vfs, vfs_cwd, &self.workspace_parent).map_err(VmError::Sync)?;
201        }
202
203        let guest_dir = guest_dir_for_cwd_inner(&self.guest_mount, vfs_cwd);
204        if !self.lima_hints_checked {
205            self.lima_hints_checked = true;
206            lima_diagnostics::warn_if_guest_misconfigured(
207                &self.limactl,
208                &self.lima_instance,
209                &self.workspace_parent,
210                &self.guest_mount,
211                &guest_dir,
212            );
213        }
214
215        let status = self.limactl_shell(&guest_dir, program, args)?;
216
217        if !status.success() && (program == "cargo" || program == "rustup") {
218            lima_diagnostics::emit_tool_failure_hints(
219                &self.limactl,
220                &self.lima_instance,
221                &self.workspace_parent,
222                &self.guest_mount,
223                &guest_dir,
224                program,
225                &status,
226            );
227        }
228
229        if self.sync_vfs_with_workspace {
230            if let Err(e) = pull_workspace_to_vfs(&self.workspace_parent, vfs_cwd, vfs) {
231                eprintln!(
232                    "dev_shell: warning: vm workspace pull failed after `{program}` (VFS may be stale): {e}"
233                );
234            }
235        }
236
237        Ok(status)
238    }
239
240    fn shutdown(&mut self, vfs: &mut Vfs, vfs_cwd: &str) -> Result<(), VmError> {
241        if self.vm_started && self.sync_vfs_with_workspace {
242            if let Err(e) = pull_workspace_to_vfs(&self.workspace_parent, vfs_cwd, vfs) {
243                return Err(VmError::Sandbox(e));
244            }
245        }
246        if truthy_env(ENV_DEVSHELL_VM_STOP_ON_EXIT) {
247            let _ = self.limactl_stop();
248        }
249        Ok(())
250    }
251}