#![allow(clippy::pedantic, clippy::nursery)]
use std::net::{SocketAddr, TcpStream};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
use super::config::{
devshell_repo_root_with_containerfile, ENV_DEVSHELL_VM_BETA_SESSION_STAGING,
ENV_DEVSHELL_VM_DISABLE_PODMAN_SSH_HOME, ENV_DEVSHELL_VM_SKIP_PODMAN_BOOTSTRAP,
};
use super::VmError;
const SIDECAR_PORT: u16 = 9847;
const CONTAINER_NAME: &str = "cargo-devshell-sidecar";
const IMAGE_TAG: &str = "devshell-vm:local";
const MSG_PODMAN_INSTALL: &str = "\
dev_shell (beta VM): Podman is not available or not on PATH.
Try in order:
1) Install: winget install -e --id Podman.Podman
2) Verify: podman version
3) If needed: podman machine start
4) Docs: https://podman.io/getting-started/installation
5) Host-only: set DEVSHELL_VM_BACKEND=host (no VM sidecar)";
const MSG_NO_CONTAINERFILE: &str = "\
dev_shell (beta VM): cannot auto-build the sidecar image — no containers/devshell-vm/Containerfile in parent directories.
Do one of:
A) cd to your xtask_todo clone root (the repo that contains containers/devshell-vm/), then run cargo-devshell again; or
B) Build once from that repo: podman build -f containers/devshell-vm/Containerfile -t devshell-vm:local .
then run the container manually (see docs/devshell-vm-windows.md); or
C) Skip auto Podman and use a running sidecar: set DEVSHELL_VM_SKIP_PODMAN_BOOTSTRAP=1
and ensure DEVSHELL_VM_SOCKET=tcp:127.0.0.1:9847 reaches devshell-vm; or
D) Host-only: set DEVSHELL_VM_BACKEND=host";
#[cfg(windows)]
fn link_podman_machine_into_ssh_home(ssh_home: &Path) -> std::io::Result<()> {
let real_profile = match std::env::var("USERPROFILE") {
Ok(s) if !s.trim().is_empty() => PathBuf::from(s.trim()),
_ => return Ok(()),
};
let real_podman = real_profile
.join(".local")
.join("share")
.join("containers")
.join("podman");
if !real_podman.is_dir() {
return Ok(());
}
let link = ssh_home
.join(".local")
.join("share")
.join("containers")
.join("podman");
if link.exists() {
return Ok(());
}
if let Some(parent) = link.parent() {
std::fs::create_dir_all(parent)?;
}
std::os::windows::fs::symlink_dir(&real_podman, &link)
}
#[cfg(not(windows))]
fn link_podman_machine_into_ssh_home(_ssh_home: &Path) -> std::io::Result<()> {
Ok(())
}
fn ssh_home_for_podman() -> std::io::Result<PathBuf> {
let home = std::env::temp_dir().join("cargo-devshell-ssh-home");
let ssh = home.join(".ssh");
std::fs::create_dir_all(&ssh)?;
let kh = ssh.join("known_hosts");
if !kh.exists() {
std::fs::write(&kh, "")?;
}
link_podman_machine_into_ssh_home(&home).unwrap_or_else(|e| {
eprintln!(
"dev_shell: could not symlink %USERPROFILE%\\.local\\share\\containers\\podman into isolated profile (SSH workaround): {e}\n\
Podman may not see an existing machine until you enable Windows Developer Mode (symlinks) or run elevated once.\n\
Or set {}=1 to use your real profile (and fix or unlock .ssh\\known_hosts).",
ENV_DEVSHELL_VM_DISABLE_PODMAN_SSH_HOME
);
});
Ok(home)
}
#[cfg(windows)]
fn apply_windows_podman_profile_env(cmd: &mut Command, home: &Path) {
cmd.env("USERPROFILE", home);
cmd.env("HOME", home);
if let Some(s) = home.to_str() {
let b = s.as_bytes();
if b.len() >= 2 && b[1] == b':' {
if let (Some(drive), Some(rest)) = (s.get(..2), s.get(2..)) {
cmd.env("HOMEDRIVE", drive);
cmd.env("HOMEPATH", rest);
}
}
}
}
fn apply_podman_ssh_home_env(cmd: &mut Command) {
if std::env::var(ENV_DEVSHELL_VM_DISABLE_PODMAN_SSH_HOME).is_ok() {
return;
}
match ssh_home_for_podman() {
Ok(home) => {
#[cfg(windows)]
apply_windows_podman_profile_env(cmd, &home);
#[cfg(not(windows))]
cmd.env("HOME", &home);
}
Err(e) => {
eprintln!("dev_shell: could not create temp HOME for podman (SSH known_hosts workaround): {e}");
}
}
}
fn podman_command() -> Command {
let mut c = Command::new("podman");
apply_podman_ssh_home_env(&mut c);
c
}
fn podman_version_succeeds() -> bool {
podman_command()
.args(["--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn sidecar_tcp_reachable() -> bool {
let addr: SocketAddr = format!("127.0.0.1:{SIDECAR_PORT}")
.parse()
.expect("valid addr");
TcpStream::connect_timeout(&addr, Duration::from_millis(300)).is_ok()
}
fn wait_for_sidecar(timeout: Duration) -> bool {
let start = Instant::now();
while start.elapsed() < timeout {
if sidecar_tcp_reachable() {
return true;
}
std::thread::sleep(Duration::from_millis(150));
}
false
}
fn try_install_podman_via_winget() {
if !Command::new("winget")
.args(["--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
eprintln!("dev_shell: winget not found — install Podman manually from https://podman.io/ or use Chocolatey/choco install podman.");
return;
}
eprintln!("dev_shell: Podman not on PATH; trying: winget install -e --id Podman.Podman");
let status = Command::new("winget")
.args([
"install",
"-e",
"--id",
"Podman.Podman",
"--accept-package-agreements",
"--accept-source-agreements",
])
.status();
match status {
Ok(s) if s.success() => eprintln!(
"dev_shell: winget reported success. Open a NEW terminal, then run: podman version"
),
Ok(_) => eprintln!(
"dev_shell: winget install failed — try an elevated (Administrator) terminal, or install from https://podman.io/"
),
Err(e) => eprintln!("dev_shell: could not run winget: {e}"),
}
}
fn try_podman_engine_ready() {
if podman_command()
.args(["info"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return;
}
eprintln!("dev_shell: podman info failed — trying: podman machine start");
let _ = podman_command().args(["machine", "start"]).status();
std::thread::sleep(Duration::from_secs(2));
}
fn podman_image_exists(image: &str) -> bool {
podman_command()
.args(["image", "exists", image])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn podman_build(repo_root: &Path, image: &str) -> Result<(), VmError> {
let st = podman_command()
.args([
"build",
"-f",
"containers/devshell-vm/Containerfile",
"-t",
image,
".",
])
.current_dir(repo_root)
.status()
.map_err(|e| VmError::Ipc(format!("podman build: {e}\n{MSG_PODMAN_INSTALL}")))?;
if !st.success() {
return Err(VmError::Ipc(
"podman build failed (see stderr above: network, disk, or Containerfile).\n\
Retry after fixing, or: set DEVSHELL_VM_BACKEND=host"
.to_string(),
));
}
Ok(())
}
fn podman_rm_force() {
let _ = podman_command().args(["rm", "-f", CONTAINER_NAME]).output();
}
pub fn ensure(workspace_parent: &Path) -> Result<(), VmError> {
if std::env::var(ENV_DEVSHELL_VM_SKIP_PODMAN_BOOTSTRAP).is_ok() {
return Ok(());
}
if sidecar_tcp_reachable() {
return Ok(());
}
if !podman_version_succeeds() {
try_install_podman_via_winget();
}
if !podman_version_succeeds() {
return Err(VmError::Ipc(MSG_PODMAN_INSTALL.to_string()));
}
try_podman_engine_ready();
let repo_root = match devshell_repo_root_with_containerfile() {
Some(p) => p,
None => {
eprintln!("{MSG_NO_CONTAINERFILE}");
return Ok(());
}
};
if !podman_image_exists(IMAGE_TAG) {
eprintln!("dev_shell: building {IMAGE_TAG} (first run may take a few minutes)…");
podman_build(&repo_root, IMAGE_TAG)?;
}
podman_rm_force();
let ws = workspace_parent.as_os_str().to_string_lossy();
let vol = format!("{ws}:/workspace");
let st = podman_command()
.args([
"run",
"-d",
"--name",
CONTAINER_NAME,
"-p",
"9847:9847",
"-v",
&vol,
IMAGE_TAG,
])
.current_dir(&repo_root)
.status()
.map_err(|e| VmError::Ipc(format!("podman run: {e}\n{MSG_PODMAN_INSTALL}")))?;
if !st.success() {
return Err(VmError::Ipc(
"podman run failed (check: podman machine start, port 9847, volume path).\n\
Verify: podman ps -a\n\
Host-only: set DEVSHELL_VM_BACKEND=host"
.to_string(),
));
}
if std::env::var(ENV_DEVSHELL_VM_BETA_SESSION_STAGING).is_err() {
std::env::set_var(ENV_DEVSHELL_VM_BETA_SESSION_STAGING, "/workspace");
}
if !wait_for_sidecar(Duration::from_secs(45)) {
return Err(VmError::Ipc(
"sidecar did not open tcp/127.0.0.1:9847 in time.\n\
Check: podman logs cargo-devshell-sidecar\n\
Or: set DEVSHELL_VM_BACKEND=host"
.to_string(),
));
}
Ok(())
}