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