use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::process::Command;
const SERVER_BIN: &str = "vllm-mlx";
const MIN_VERSION: (u64, u64, u64) = (0, 3, 0);
const PIP_SPEC: &str = "vllm-mlx>=0.3.0";
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
#[error("`uv` is required to provision the vllm-mlx runtime but was not found on PATH; \
install uv (https://docs.astral.sh/uv/) or `pip install vllm-mlx` yourself")]
UvMissing,
#[error("provisioning step `{step}` failed: {detail}")]
Provision { step: &'static str, detail: String },
#[error("vllm-mlx still not found after provisioning into {0}")]
NotFoundAfterInstall(PathBuf),
}
#[derive(Debug, Clone)]
pub struct VllmRuntime {
pub server: PathBuf,
}
fn managed_root() -> Option<PathBuf> {
std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".car").join("visual-runtime"))
}
fn managed_server_bin() -> Option<PathBuf> {
managed_root().map(|r| r.join("bin").join(SERVER_BIN))
}
fn search_dirs() -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = std::env::var_os("PATH")
.map(|paths| std::env::split_paths(&paths).collect())
.unwrap_or_default();
if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
dirs.push(home.join(".local").join("bin"));
dirs.push(
home.join(".local")
.join("share")
.join("uv")
.join("tools")
.join("vllm-mlx")
.join("bin"),
);
dirs.push(home.join(".car").join("visual-runtime").join("bin"));
}
dedupe(dirs)
}
fn dedupe(paths: Vec<PathBuf>) -> Vec<PathBuf> {
let mut out = Vec::new();
for p in paths {
if !out.contains(&p) {
out.push(p);
}
}
out
}
pub fn resolve_existing() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("CAR_VLLM_MLX_BIN").map(PathBuf::from) {
if p.is_file() {
return absolutize(&p);
}
}
for dir in search_dirs() {
let candidate = dir.join(SERVER_BIN);
if candidate.is_file() && version_ok(&candidate) {
return absolutize(&candidate);
}
}
None
}
fn version_ok(binary: &Path) -> bool {
binary_version(binary).map(|v| v >= MIN_VERSION).unwrap_or(false)
}
fn binary_version(binary: &Path) -> Option<(u64, u64, u64)> {
let head = std::fs::read_to_string(binary).ok()?;
let interp = head.lines().next()?.strip_prefix("#!")?.trim();
let out = std::process::Command::new(interp)
.args([
"-c",
"import importlib.metadata as m; print(m.version('vllm-mlx'))",
])
.output()
.ok()?;
if !out.status.success() {
return None;
}
parse_semver(String::from_utf8_lossy(&out.stdout).trim())
}
fn parse_semver(s: &str) -> Option<(u64, u64, u64)> {
let lead = |part: &str| -> u64 {
part.chars()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.parse()
.unwrap_or(0)
};
let mut it = s.split('.');
let major = it.next()?.trim().parse().ok()?;
Some((major, lead(it.next().unwrap_or("0")), lead(it.next().unwrap_or("0"))))
}
fn absolutize(p: &Path) -> Option<PathBuf> {
std::fs::canonicalize(p).ok().or_else(|| Some(p.to_path_buf()))
}
pub async fn ensure_runtime() -> Result<VllmRuntime, RuntimeError> {
if let Some(server) = resolve_existing() {
return Ok(VllmRuntime { server });
}
provision().await?;
let server = managed_server_bin()
.filter(|p| p.is_file())
.or_else(resolve_existing)
.ok_or_else(|| RuntimeError::NotFoundAfterInstall(managed_root().unwrap_or_default()))?;
Ok(VllmRuntime { server })
}
async fn provision() -> Result<(), RuntimeError> {
if which("uv").is_none() {
return Err(RuntimeError::UvMissing);
}
let root = managed_root().ok_or_else(|| RuntimeError::Provision {
step: "resolve-home",
detail: "HOME is not set".into(),
})?;
std::fs::create_dir_all(&root).map_err(|e| RuntimeError::Provision {
step: "mkdir",
detail: e.to_string(),
})?;
run_uv(
"venv",
&[
"venv".into(),
"--python".into(),
"python3".into(),
root.display().to_string(),
],
)
.await?;
let venv_python = root.join("bin").join("python");
run_uv(
"pip-install",
&[
"pip".into(),
"install".into(),
"--python".into(),
venv_python.display().to_string(),
PIP_SPEC.into(),
],
)
.await?;
Ok(())
}
async fn run_uv(step: &'static str, args: &[String]) -> Result<(), RuntimeError> {
let output = Command::new("uv")
.args(args)
.kill_on_drop(true)
.output()
.await
.map_err(|e| RuntimeError::Provision {
step,
detail: e.to_string(),
})?;
if output.status.success() {
Ok(())
} else {
Err(RuntimeError::Provision {
step,
detail: format!(
"uv exited with {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
),
})
}
}
pub(crate) fn which(name: &str) -> Option<PathBuf> {
std::env::var_os("PATH")?
.to_str()?
.split(':')
.map(|d| Path::new(d).join(name))
.find(|p| p.is_file())
}
pub async fn health_ok(endpoint: &str, timeout: Duration) -> bool {
let url = format!("{}/health", endpoint.trim_end_matches('/'));
let client = match reqwest::Client::builder().timeout(timeout).build() {
Ok(c) => c,
Err(_) => return false,
};
matches!(client.get(&url).send().await, Ok(r) if r.status().is_success())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn managed_paths_are_under_dot_car() {
if std::env::var_os("HOME").is_none() {
return; }
let root = managed_root().unwrap();
assert!(root.ends_with(".car/visual-runtime"), "root: {}", root.display());
let bin = managed_server_bin().unwrap();
assert!(bin.ends_with(".car/visual-runtime/bin/vllm-mlx"));
}
#[test]
fn search_dirs_include_path_and_managed() {
let dirs = search_dirs();
if std::env::var_os("HOME").is_some() {
assert!(
dirs.iter().any(|d| d.ends_with(".car/visual-runtime/bin")),
"managed bin dir missing from search set"
);
}
}
#[test]
fn parse_semver_handles_plain_and_prerelease() {
assert_eq!(parse_semver("0.3.0"), Some((0, 3, 0)));
assert_eq!(parse_semver("1.2.10"), Some((1, 2, 10)));
assert_eq!(parse_semver("0.6.3rc1"), Some((0, 6, 3)));
assert_eq!(parse_semver("0.2"), Some((0, 2, 0)));
assert!(parse_semver("not-a-version").is_none());
assert!((0, 2, 9) < MIN_VERSION);
assert!((0, 3, 0) >= MIN_VERSION);
assert!((0, 6, 3) >= MIN_VERSION);
}
#[test]
fn env_override_resolves_when_file_exists() {
std::env::set_var("CAR_VLLM_MLX_BIN", "/nonexistent/vllm-mlx-xyz");
let resolved = resolve_existing();
assert!(
resolved
.as_ref()
.map(|p| !p.ends_with("vllm-mlx-xyz"))
.unwrap_or(true),
"non-file override must not be returned"
);
std::env::remove_var("CAR_VLLM_MLX_BIN");
}
}