xtask_todo_lib/devshell/vm/session_gamma/session/
mod.rs1mod exec;
4
5use std::io::Write;
6use std::path::{Path, PathBuf};
7use std::process::{Command, ExitStatus, Stdio};
8
9use super::super::bash_single_quoted;
10use super::super::lima_diagnostics;
11use super::super::{VmConfig, VmError, WorkspaceMode};
12use super::env::ENV_DEVSHELL_VM_GUEST_WORKSPACE;
13use super::helpers::{
14 guest_dir_for_host_path_under_workspace, guest_host_dir_link_name,
15 guest_todo_release_dir_for_cwd, resolve_limactl, workspace_parent_for_instance,
16};
17
18#[derive(Debug)]
20pub struct GammaSession {
21 lima_instance: String,
22 workspace_parent: PathBuf,
24 guest_mount: String,
26 limactl: PathBuf,
27 vm_started: bool,
28 lima_hints_checked: bool,
30 guest_build_essential_done: bool,
32 guest_todo_hint_done: bool,
34 sync_vfs_with_workspace: bool,
36}
37
38impl GammaSession {
39 pub fn new(config: &VmConfig) -> Result<Self, VmError> {
44 let limactl = resolve_limactl()?;
45 let workspace_parent = workspace_parent_for_instance(&config.lima_instance);
46 let guest_mount = std::env::var(ENV_DEVSHELL_VM_GUEST_WORKSPACE)
47 .ok()
48 .map(|s| s.trim().to_string())
49 .filter(|s| !s.is_empty())
50 .unwrap_or_else(|| "/workspace".to_string());
51
52 let sync_vfs_with_workspace =
53 matches!(config.workspace_mode_effective(), WorkspaceMode::Sync);
54
55 Ok(Self {
56 lima_instance: config.lima_instance.clone(),
57 workspace_parent,
58 guest_mount,
59 limactl,
60 vm_started: false,
61 lima_hints_checked: false,
62 guest_build_essential_done: false,
63 guest_todo_hint_done: false,
64 sync_vfs_with_workspace,
65 })
66 }
67
68 #[must_use]
70 pub fn syncs_vfs_with_host_workspace(&self) -> bool {
71 self.sync_vfs_with_workspace
72 }
73
74 fn limactl_ensure_running(&mut self) -> Result<(), VmError> {
75 if self.vm_started {
76 return Ok(());
77 }
78 std::fs::create_dir_all(&self.workspace_parent).map_err(|e| {
79 VmError::Lima(format!(
80 "create workspace dir {}: {e}",
81 self.workspace_parent.display()
82 ))
83 })?;
84
85 let st = Command::new(&self.limactl)
88 .args(["start", "-y", &self.lima_instance])
89 .stdin(Stdio::null())
90 .stdout(Stdio::inherit())
91 .stderr(Stdio::inherit())
92 .status()
93 .map_err(|e| VmError::Lima(format!("limactl start: {e}")))?;
94
95 if !st.success() {
96 lima_diagnostics::emit_start_failure_hints(&self.lima_instance);
97 return Err(VmError::Lima(format!(
98 "limactl start '{}' failed (exit code {:?}); check instance name and `limactl list`",
99 self.lima_instance,
100 st.code()
101 )));
102 }
103 self.vm_started = true;
104 Ok(())
105 }
106
107 fn limactl_shell_script_sh(
109 &mut self,
110 guest_workdir: &str,
111 sh_script: &str,
112 ) -> Result<std::process::Output, VmError> {
113 self.limactl_ensure_running()?;
114 Command::new(&self.limactl)
115 .arg("shell")
116 .arg("-y")
117 .arg("--workdir")
118 .arg(guest_workdir)
119 .arg(&self.lima_instance)
120 .arg("--")
121 .arg("/bin/sh")
122 .arg("-c")
123 .arg(sh_script)
124 .stdin(Stdio::null())
125 .stdout(Stdio::piped())
126 .stderr(Stdio::piped())
127 .output()
128 .map_err(|e| VmError::Lima(format!("limactl shell: {e}")))
129 }
130
131 fn limactl_shell(
132 &self,
133 guest_workdir: &str,
134 program: &str,
135 args: &[String],
136 ) -> Result<ExitStatus, VmError> {
137 let st = Command::new(&self.limactl)
138 .arg("shell")
139 .arg("--workdir")
140 .arg(guest_workdir)
141 .arg(&self.lima_instance)
142 .arg("--")
143 .arg(program)
144 .args(args)
145 .stdin(Stdio::inherit())
146 .stdout(Stdio::inherit())
147 .stderr(Stdio::inherit())
148 .status()
149 .map_err(|e| VmError::Lima(format!("limactl shell: {e}")))?;
150 Ok(st)
151 }
152
153 pub(crate) fn limactl_shell_output(
157 &mut self,
158 guest_workdir: &str,
159 program: &str,
160 args: &[String],
161 ) -> Result<std::process::Output, VmError> {
162 self.limactl_ensure_running()?;
163 Command::new(&self.limactl)
164 .arg("shell")
165 .arg("--workdir")
166 .arg(guest_workdir)
167 .arg(&self.lima_instance)
168 .arg("--")
169 .arg(program)
170 .args(args)
171 .stdin(Stdio::null())
172 .stdout(Stdio::piped())
173 .stderr(Stdio::piped())
174 .output()
175 .map_err(|e| VmError::Lima(format!("limactl shell: {e}")))
176 }
177
178 pub(crate) fn limactl_shell_stdin(
180 &mut self,
181 guest_workdir: &str,
182 program: &str,
183 args: &[String],
184 stdin_data: &[u8],
185 ) -> Result<std::process::Output, VmError> {
186 self.limactl_ensure_running()?;
187 let mut child = Command::new(&self.limactl)
188 .arg("shell")
189 .arg("--workdir")
190 .arg(guest_workdir)
191 .arg(&self.lima_instance)
192 .arg("--")
193 .arg(program)
194 .args(args)
195 .stdin(Stdio::piped())
196 .stdout(Stdio::piped())
197 .stderr(Stdio::piped())
198 .spawn()
199 .map_err(|e| VmError::Lima(format!("limactl shell: {e}")))?;
200 if let Some(mut stdin) = child.stdin.take() {
201 stdin
202 .write_all(stdin_data)
203 .map_err(|e| VmError::Lima(format!("limactl shell: write stdin: {e}")))?;
204 }
205 child
206 .wait_with_output()
207 .map_err(|e| VmError::Lima(format!("limactl shell: {e}")))
208 }
209
210 #[must_use]
212 pub fn guest_mount(&self) -> &str {
213 &self.guest_mount
214 }
215
216 fn limactl_stop(&self) -> Result<(), VmError> {
217 let st = Command::new(&self.limactl)
218 .args(["stop", &self.lima_instance])
219 .stdin(Stdio::null())
220 .stdout(Stdio::inherit())
221 .stderr(Stdio::inherit())
222 .status()
223 .map_err(|e| VmError::Lima(format!("limactl stop: {e}")))?;
224 if !st.success() {
225 return Err(VmError::Lima(format!(
226 "limactl stop '{}' failed (exit code {:?})",
227 self.lima_instance,
228 st.code()
229 )));
230 }
231 Ok(())
232 }
233
234 #[must_use]
236 pub fn workspace_parent(&self) -> &Path {
237 &self.workspace_parent
238 }
239
240 #[must_use]
242 pub fn limactl_path(&self) -> &Path {
243 &self.limactl
244 }
245
246 #[must_use]
248 pub fn lima_instance_name(&self) -> &str {
249 &self.lima_instance
250 }
251
252 #[must_use]
255 pub fn guest_todo_release_path_for_shell(&self) -> Option<String> {
256 let cwd = std::env::current_dir().ok()?;
257 guest_todo_release_dir_for_cwd(&self.workspace_parent, &self.guest_mount, &cwd)
258 }
259
260 #[must_use]
264 pub fn lima_interactive_shell_workdir_and_inner(&self) -> (String, String) {
265 let cwd = std::env::current_dir().ok();
266 let guest_proj = cwd.as_ref().and_then(|c| {
267 guest_dir_for_host_path_under_workspace(&self.workspace_parent, &self.guest_mount, c)
268 });
269
270 if guest_proj.is_none() {
271 if let Some(ref c) = cwd {
272 let _ = writeln!(
273 std::io::stderr(),
274 "dev_shell: lima: host cwd {} is outside workspace_parent {} — shell starts at {}.\n\
275dev_shell: hint: `workspace_parent` defaults to the Cargo workspace root (or set DEVSHELL_VM_WORKSPACE_PARENT). \
276In ~/.lima/<instance>/lima.yaml, mount that host path, e.g.:\n - location: \"{}\"\n mountPoint: {}\n writable: true\n\
277Then limactl stop/start the instance. See docs/devshell-vm-gamma.md.",
278 c.display(),
279 self.workspace_parent.display(),
280 self.guest_mount,
281 self.workspace_parent.display(),
282 self.guest_mount
283 );
284 }
285 }
286
287 let workdir = guest_proj
288 .clone()
289 .unwrap_or_else(|| self.guest_mount.clone());
290
291 let mut inner = String::new();
292 if let Some(p) = self.guest_todo_release_path_for_shell() {
293 let _ = writeln!(
294 std::io::stderr(),
295 "dev_shell: prepending guest PATH with {p} (host todo under Lima workspace mount)"
296 );
297 inner.push_str(&format!("export PATH={}:$PATH; ", bash_single_quoted(&p)));
298 }
299
300 if let Some(ref gp) = guest_proj {
301 match guest_host_dir_link_name() {
302 Some(hd) => {
303 inner.push_str(&format!(
304 "export GUEST_PROJ={}; export HD={}; \
305if [ -d \"$GUEST_PROJ\" ]; then cd \"$GUEST_PROJ\" || true; \
306 ln -sfn \"$GUEST_PROJ\" \"$HOME/$HD\" 2>/dev/null || true; \
307 ln -sf \"$HOME/$HD/.todo.json\" \"$HOME/.todo.json\" 2>/dev/null || true; \
308fi; ",
309 bash_single_quoted(gp),
310 bash_single_quoted(&hd)
311 ));
312 }
313 None => {
314 inner.push_str(&format!(
315 "export GUEST_PROJ={}; \
316if [ -d \"$GUEST_PROJ\" ]; then cd \"$GUEST_PROJ\" || true; fi; ",
317 bash_single_quoted(gp)
318 ));
319 }
320 }
321 }
322
323 inner.push_str("exec bash -l");
324 (workdir, inner)
325 }
326}