use std::path::{Path, PathBuf};
use std::process::Command;
use serde_json::Value;
use super::super::super::sandbox;
use super::super::VmError;
use super::env::{
ENV_DEVSHELL_VM_AUTO_BUILD_ESSENTIAL, ENV_DEVSHELL_VM_AUTO_BUILD_TODO_GUEST,
ENV_DEVSHELL_VM_AUTO_TODO_PATH, ENV_DEVSHELL_VM_GUEST_HOST_DIR,
ENV_DEVSHELL_VM_GUEST_TODO_HINT,
};
pub(super) fn truthy_env(key: &str) -> bool {
std::env::var(key)
.map(|s| {
let s = s.trim();
s == "1" || s.eq_ignore_ascii_case("true") || s.eq_ignore_ascii_case("yes")
})
.unwrap_or(false)
}
pub(super) fn auto_build_essential_enabled() -> bool {
match std::env::var(ENV_DEVSHELL_VM_AUTO_BUILD_ESSENTIAL) {
Err(_) => true,
Ok(s) => {
let s = s.trim();
if s.is_empty() {
return true;
}
!(s == "0"
|| s.eq_ignore_ascii_case("false")
|| s.eq_ignore_ascii_case("no")
|| s.eq_ignore_ascii_case("off"))
}
}
}
pub(super) fn resolve_limactl() -> Result<PathBuf, VmError> {
use super::env::ENV_DEVSHELL_VM_LIMACTL;
if let Ok(p) = std::env::var(ENV_DEVSHELL_VM_LIMACTL) {
let p = p.trim();
if !p.is_empty() {
return Ok(PathBuf::from(p));
}
}
sandbox::find_in_path("limactl").ok_or_else(|| {
VmError::Lima(
"limactl not found in PATH; install Lima (https://lima-vm.io/) or set DEVSHELL_VM_LIMACTL"
.to_string(),
)
})
}
pub(super) fn auto_todo_path_enabled() -> bool {
match std::env::var(ENV_DEVSHELL_VM_AUTO_TODO_PATH) {
Err(_) => true,
Ok(s) => {
let s = s.trim();
if s.is_empty() {
return true;
}
!(s == "0"
|| s.eq_ignore_ascii_case("false")
|| s.eq_ignore_ascii_case("no")
|| s.eq_ignore_ascii_case("off"))
}
}
}
pub(super) fn guest_todo_hint_enabled() -> bool {
match std::env::var(ENV_DEVSHELL_VM_GUEST_TODO_HINT) {
Err(_) => true,
Ok(s) => {
let s = s.trim();
if s.is_empty() {
return true;
}
!(s == "0"
|| s.eq_ignore_ascii_case("false")
|| s.eq_ignore_ascii_case("no")
|| s.eq_ignore_ascii_case("off"))
}
}
}
pub(super) fn auto_build_todo_guest_enabled() -> bool {
truthy_env(ENV_DEVSHELL_VM_AUTO_BUILD_TODO_GUEST)
}
pub(super) fn shell_single_quote_sh(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for c in s.chars() {
if c == '\'' {
out.push_str("'\"'\"'");
} else {
out.push(c);
}
}
out.push('\'');
out
}
pub(super) fn cargo_metadata_workspace_and_target(cwd: &Path) -> Result<(PathBuf, PathBuf), ()> {
let out = Command::new("cargo")
.args(["metadata", "--format-version", "1", "--no-deps"])
.current_dir(cwd)
.output()
.map_err(|_| ())?;
if !out.status.success() {
return Err(());
}
let v: Value = serde_json::from_slice(&out.stdout).map_err(|_| ())?;
let wr = v
.get("workspace_root")
.and_then(|x| x.as_str())
.map(PathBuf::from)
.ok_or(())?;
let td = v
.get("target_directory")
.and_then(|x| x.as_str())
.map(PathBuf::from)
.ok_or(())?;
Ok((wr, td))
}
fn cargo_metadata_target_dir(cwd: &Path) -> Result<PathBuf, ()> {
Ok(cargo_metadata_workspace_and_target(cwd)?.1)
}
pub(super) fn guest_dir_for_host_path_under_workspace(
workspace_parent: &Path,
guest_mount: &str,
host_path: &Path,
) -> Option<String> {
let hs = host_path.canonicalize().ok()?;
let ws = workspace_parent.canonicalize().ok()?;
let rel = hs.strip_prefix(&ws).ok()?;
let guest = if rel.as_os_str().is_empty() {
PathBuf::from(guest_mount)
} else {
Path::new(guest_mount).join(rel)
};
Some(guest.to_string_lossy().replace('\\', "/"))
}
pub(super) fn guest_host_dir_link_name() -> Option<String> {
match std::env::var(ENV_DEVSHELL_VM_GUEST_HOST_DIR) {
Err(_) => Some("host_dir".to_string()),
Ok(s) => {
let s = s.trim();
if s.is_empty()
|| s == "0"
|| s.eq_ignore_ascii_case("false")
|| s.eq_ignore_ascii_case("off")
|| s.eq_ignore_ascii_case("no")
{
None
} else if s
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
Some(s.to_string())
} else {
None
}
}
}
}
pub(super) fn guest_cargo_workspace_dir_for_cwd(
workspace_parent: &Path,
guest_mount: &str,
cwd: &Path,
) -> Option<String> {
let ws_root = cargo_metadata_workspace_and_target(cwd).ok()?.0;
let ws = workspace_parent.canonicalize().ok()?;
let ws_root_canon = ws_root.canonicalize().ok()?;
let rel = ws_root_canon.strip_prefix(&ws).ok()?;
Some(
Path::new(guest_mount)
.join(rel)
.to_string_lossy()
.replace('\\', "/"),
)
}
pub(super) fn guest_todo_release_dir_for_cwd(
workspace_parent: &Path,
guest_mount: &str,
cwd: &Path,
) -> Option<String> {
if !auto_todo_path_enabled() {
return None;
}
let release_dir = cargo_metadata_target_dir(cwd).ok()?.join("release");
let todo_bin = release_dir.join("todo");
if !todo_bin.is_file() {
return None;
}
let ws = workspace_parent.canonicalize().ok()?;
let release_canon = release_dir.canonicalize().ok()?;
let rel = release_canon.strip_prefix(&ws).ok()?;
let guest = Path::new(guest_mount).join(rel);
let s = guest.to_string_lossy().replace('\\', "/");
Some(s)
}
#[cfg(test)]
pub(super) fn sanitize_instance_segment(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect()
}
#[must_use]
pub fn workspace_parent_for_instance(instance: &str) -> PathBuf {
super::super::workspace_host::workspace_parent_for_instance(instance)
}
pub(super) fn guest_dir_for_cwd_inner(guest_mount: &str, vfs_cwd: &str) -> String {
super::super::guest_fs_ops::guest_project_dir_on_guest(guest_mount, vfs_cwd)
}