mod exec;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use super::super::bash_single_quoted;
use super::super::lima_diagnostics;
use super::super::{VmConfig, VmError, WorkspaceMode};
use super::env::ENV_DEVSHELL_VM_GUEST_WORKSPACE;
use super::helpers::{
guest_dir_for_host_path_under_workspace, guest_host_dir_link_name,
guest_todo_release_dir_for_cwd, resolve_limactl, workspace_parent_for_instance,
};
#[derive(Debug)]
pub struct GammaSession {
lima_instance: String,
workspace_parent: PathBuf,
guest_mount: String,
limactl: PathBuf,
vm_started: bool,
lima_hints_checked: bool,
guest_build_essential_done: bool,
guest_todo_hint_done: bool,
sync_vfs_with_workspace: bool,
}
impl GammaSession {
pub fn new(config: &VmConfig) -> Result<Self, VmError> {
let limactl = resolve_limactl()?;
let workspace_parent = workspace_parent_for_instance(&config.lima_instance);
let guest_mount = std::env::var(ENV_DEVSHELL_VM_GUEST_WORKSPACE)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "/workspace".to_string());
let sync_vfs_with_workspace =
matches!(config.workspace_mode_effective(), WorkspaceMode::Sync);
Ok(Self {
lima_instance: config.lima_instance.clone(),
workspace_parent,
guest_mount,
limactl,
vm_started: false,
lima_hints_checked: false,
guest_build_essential_done: false,
guest_todo_hint_done: false,
sync_vfs_with_workspace,
})
}
#[must_use]
pub fn syncs_vfs_with_host_workspace(&self) -> bool {
self.sync_vfs_with_workspace
}
fn limactl_ensure_running(&mut self) -> Result<(), VmError> {
if self.vm_started {
return Ok(());
}
std::fs::create_dir_all(&self.workspace_parent).map_err(|e| {
VmError::Lima(format!(
"create workspace dir {}: {e}",
self.workspace_parent.display()
))
})?;
let st = Command::new(&self.limactl)
.args(["start", "-y", &self.lima_instance])
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|e| VmError::Lima(format!("limactl start: {e}")))?;
if !st.success() {
lima_diagnostics::emit_start_failure_hints(&self.lima_instance);
return Err(VmError::Lima(format!(
"limactl start '{}' failed (exit code {:?}); check instance name and `limactl list`",
self.lima_instance,
st.code()
)));
}
self.vm_started = true;
Ok(())
}
fn limactl_shell_script_sh(
&mut self,
guest_workdir: &str,
sh_script: &str,
) -> Result<std::process::Output, VmError> {
self.limactl_ensure_running()?;
Command::new(&self.limactl)
.arg("shell")
.arg("-y")
.arg("--workdir")
.arg(guest_workdir)
.arg(&self.lima_instance)
.arg("--")
.arg("/bin/sh")
.arg("-c")
.arg(sh_script)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| VmError::Lima(format!("limactl shell: {e}")))
}
fn limactl_shell(
&self,
guest_workdir: &str,
program: &str,
args: &[String],
) -> Result<ExitStatus, VmError> {
let st = Command::new(&self.limactl)
.arg("shell")
.arg("--workdir")
.arg(guest_workdir)
.arg(&self.lima_instance)
.arg("--")
.arg(program)
.args(args)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|e| VmError::Lima(format!("limactl shell: {e}")))?;
Ok(st)
}
pub(crate) fn limactl_shell_output(
&mut self,
guest_workdir: &str,
program: &str,
args: &[String],
) -> Result<std::process::Output, VmError> {
self.limactl_ensure_running()?;
Command::new(&self.limactl)
.arg("shell")
.arg("--workdir")
.arg(guest_workdir)
.arg(&self.lima_instance)
.arg("--")
.arg(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| VmError::Lima(format!("limactl shell: {e}")))
}
pub(crate) fn limactl_shell_stdin(
&mut self,
guest_workdir: &str,
program: &str,
args: &[String],
stdin_data: &[u8],
) -> Result<std::process::Output, VmError> {
self.limactl_ensure_running()?;
let mut child = Command::new(&self.limactl)
.arg("shell")
.arg("--workdir")
.arg(guest_workdir)
.arg(&self.lima_instance)
.arg("--")
.arg(program)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| VmError::Lima(format!("limactl shell: {e}")))?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(stdin_data)
.map_err(|e| VmError::Lima(format!("limactl shell: write stdin: {e}")))?;
}
child
.wait_with_output()
.map_err(|e| VmError::Lima(format!("limactl shell: {e}")))
}
#[must_use]
pub fn guest_mount(&self) -> &str {
&self.guest_mount
}
fn limactl_stop(&self) -> Result<(), VmError> {
let st = Command::new(&self.limactl)
.args(["stop", &self.lima_instance])
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|e| VmError::Lima(format!("limactl stop: {e}")))?;
if !st.success() {
return Err(VmError::Lima(format!(
"limactl stop '{}' failed (exit code {:?})",
self.lima_instance,
st.code()
)));
}
Ok(())
}
#[must_use]
pub fn workspace_parent(&self) -> &Path {
&self.workspace_parent
}
#[must_use]
pub fn limactl_path(&self) -> &Path {
&self.limactl
}
#[must_use]
pub fn lima_instance_name(&self) -> &str {
&self.lima_instance
}
#[must_use]
pub fn guest_todo_release_path_for_shell(&self) -> Option<String> {
let cwd = std::env::current_dir().ok()?;
guest_todo_release_dir_for_cwd(&self.workspace_parent, &self.guest_mount, &cwd)
}
#[must_use]
pub fn lima_interactive_shell_workdir_and_inner(&self) -> (String, String) {
let cwd = std::env::current_dir().ok();
let guest_proj = cwd.as_ref().and_then(|c| {
guest_dir_for_host_path_under_workspace(&self.workspace_parent, &self.guest_mount, c)
});
if guest_proj.is_none() {
if let Some(ref c) = cwd {
let _ = writeln!(
std::io::stderr(),
"dev_shell: lima: host cwd {} is outside workspace_parent {} — shell starts at {}.\n\
dev_shell: hint: `workspace_parent` defaults to the Cargo workspace root (or set DEVSHELL_VM_WORKSPACE_PARENT). \
In ~/.lima/<instance>/lima.yaml, mount that host path, e.g.:\n - location: \"{}\"\n mountPoint: {}\n writable: true\n\
Then limactl stop/start the instance. See docs/devshell-vm-gamma.md.",
c.display(),
self.workspace_parent.display(),
self.guest_mount,
self.workspace_parent.display(),
self.guest_mount
);
}
}
let workdir = guest_proj
.clone()
.unwrap_or_else(|| self.guest_mount.clone());
let mut inner = String::new();
if let Some(p) = self.guest_todo_release_path_for_shell() {
let _ = writeln!(
std::io::stderr(),
"dev_shell: prepending guest PATH with {p} (host todo under Lima workspace mount)"
);
inner.push_str(&format!("export PATH={}:$PATH; ", bash_single_quoted(&p)));
}
if let Some(ref gp) = guest_proj {
match guest_host_dir_link_name() {
Some(hd) => {
inner.push_str(&format!(
"export GUEST_PROJ={}; export HD={}; \
if [ -d \"$GUEST_PROJ\" ]; then cd \"$GUEST_PROJ\" || true; \
ln -sfn \"$GUEST_PROJ\" \"$HOME/$HD\" 2>/dev/null || true; \
ln -sf \"$HOME/$HD/.todo.json\" \"$HOME/.todo.json\" 2>/dev/null || true; \
fi; ",
bash_single_quoted(gp),
bash_single_quoted(&hd)
));
}
None => {
inner.push_str(&format!(
"export GUEST_PROJ={}; \
if [ -d \"$GUEST_PROJ\" ]; then cd \"$GUEST_PROJ\" || true; fi; ",
bash_single_quoted(gp)
));
}
}
}
inner.push_str("exec bash -l");
(workdir, inner)
}
}