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 const INSTALL_SH: &str = r"set -e
20export DEBIAN_FRONTEND=noninteractive
21if ! sudo -n true 2>/dev/null; then
22 echo 'dev_shell: guest: sudo needs a password; run in the VM: sudo apt update && sudo apt install -y build-essential' >&2
23 exit 1
24fi
25sudo apt-get update -qq
26sudo apt-get install -y -qq build-essential
27";
28
29 fn maybe_ensure_guest_build_essential(&mut self) -> Result<(), VmError> {
31 if self.flag_is_set(Self::FLAG_GUEST_BUILD_ESSENTIAL_DONE) {
32 return Ok(());
33 }
34
35 if !auto_build_essential_enabled() {
36 self.set_flag(Self::FLAG_GUEST_BUILD_ESSENTIAL_DONE);
37 return Ok(());
38 }
39
40 let probe = self.limactl_shell_script_sh("/", "command -v gcc >/dev/null 2>&1")?;
41 if probe.status.success() {
42 self.set_flag(Self::FLAG_GUEST_BUILD_ESSENTIAL_DONE);
43 return Ok(());
44 }
45
46 eprintln!("dev_shell: guest: no C compiler (gcc) in PATH; attempting apt install build-essential…");
47
48 let has_apt =
49 self.limactl_shell_script_sh("/", "test -x /usr/bin/apt-get && test -x /usr/bin/dpkg")?;
50 if !has_apt.status.success() {
51 eprintln!(
52 "dev_shell: guest: no apt-get/dpkg; install gcc + binutils manually (see docs/devshell-vm-gamma.md)."
53 );
54 self.set_flag(Self::FLAG_GUEST_BUILD_ESSENTIAL_DONE);
55 return Ok(());
56 }
57
58 let out = self.limactl_shell_script_sh("/", Self::INSTALL_SH)?;
59 if out.status.success() {
60 eprintln!(
61 "dev_shell: guest: build-essential installed (gcc available for cargo link)."
62 );
63 } else {
64 let stderr = String::from_utf8_lossy(&out.stderr);
65 let stdout = String::from_utf8_lossy(&out.stdout);
66 eprintln!(
67 "dev_shell: guest: automatic build-essential install failed (exit {:?}).",
68 out.status.code()
69 );
70 if !stdout.trim().is_empty() {
71 eprintln!("dev_shell: guest stdout: {stdout}");
72 }
73 if !stderr.trim().is_empty() {
74 eprintln!("dev_shell: guest stderr: {stderr}");
75 }
76 eprintln!(
77 "dev_shell: hint: in the guest shell: sudo apt update && sudo apt install -y build-essential"
78 );
79 }
80 self.set_flag(Self::FLAG_GUEST_BUILD_ESSENTIAL_DONE);
81 Ok(())
82 }
83
84 fn maybe_guest_todo_probe_hint_and_install(&mut self) -> Result<(), VmError> {
86 if self.flag_is_set(Self::FLAG_GUEST_TODO_HINT_DONE) {
87 return Ok(());
88 }
89 if !guest_todo_hint_enabled() {
90 self.set_flag(Self::FLAG_GUEST_TODO_HINT_DONE);
91 return Ok(());
92 }
93
94 let cwd =
95 std::env::current_dir().map_err(|e| VmError::Lima(format!("current_dir: {e}")))?;
96
97 let grel = guest_todo_release_dir_for_cwd(&self.workspace_parent, &self.guest_mount, &cwd);
98 let script = grel.as_ref().map_or_else(
99 || "command -v todo >/dev/null 2>&1".to_string(),
100 |gr| {
101 format!(
102 "command -v todo >/dev/null 2>&1 || test -x {}",
103 shell_single_quote_sh(&format!("{gr}/todo"))
104 )
105 },
106 );
107
108 let probe = self.limactl_shell_script_sh("/", &script)?;
109 if probe.status.success() {
110 self.set_flag(Self::FLAG_GUEST_TODO_HINT_DONE);
111 return Ok(());
112 }
113
114 eprintln!(
115 "dev_shell: guest: `todo` not found (do not use apt `devtodo` — unrelated package)."
116 );
117
118 let meta = cargo_metadata_workspace_and_target(&cwd);
119 let host_has_todo = meta
120 .as_ref()
121 .is_ok_and(|(_, td)| td.join("release").join("todo").is_file());
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.set_flag(Self::FLAG_GUEST_TODO_HINT_DONE);
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.flag_is_set(Self::FLAG_LIMA_HINTS_CHECKED) {
205 self.set_flag(Self::FLAG_LIMA_HINTS_CHECKED);
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.flag_is_set(Self::FLAG_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}