xtask_todo_lib/devshell/vm/session_gamma/session/
exec.rs1use 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 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 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 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}