use std::path::{Path, PathBuf};
use crate::error::Error;
pub const PODMAN_VERSION_LINUX_AMD64: &str = "5.8.4"; pub const PODMAN_VERSION_LINUX_ARM64: &str = "5.8.4"; pub const PODMAN_VERSION_MACOS_AMD64: &str = "5.8.4"; pub const PODMAN_VERSION_MACOS_ARM64: &str = "5.8.4"; pub const PODMAN_VERSION_WINDOWS_AMD64: &str = "5.8.4"; pub const PODMAN_VERSION_WINDOWS_ARM64: &str = "5.8.4";
const VFKIT_VERSION: &str = "0.6.1"; const GVPROXY_VERSION: &str = "0.8.9";
const DOWNLOAD_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600);
#[derive(Clone, Copy)]
enum ArchiveKind {
Zip,
TarGz,
}
struct Target {
version: &'static str,
url: String,
kind: ArchiveKind,
exe_rel: PathBuf,
ext: &'static str,
}
fn resolve_target() -> Result<Target, Error> {
let arch = match std::env::consts::ARCH {
"x86_64" => "amd64",
"aarch64" => "arm64",
other => {
return Err(Error::Podman(format!("unsupported architecture: {other}")));
}
};
match std::env::consts::OS {
"linux" => {
let version = if arch == "amd64" {
PODMAN_VERSION_LINUX_AMD64
} else {
PODMAN_VERSION_LINUX_ARM64
};
Ok(Target {
version,
url: format!(
"https://github.com/mgoltzsche/podman-static/releases/download/v{version}/podman-linux-{arch}.tar.gz"
),
kind: ArchiveKind::TarGz,
exe_rel: PathBuf::from(format!("podman-linux-{arch}/usr/local/bin/podman")),
ext: "tar.gz",
})
}
"macos" => {
let version = if arch == "amd64" {
PODMAN_VERSION_MACOS_AMD64
} else {
PODMAN_VERSION_MACOS_ARM64
};
Ok(Target {
url: format!(
"https://github.com/containers/podman/releases/download/v{version}/podman-remote-release-darwin_{arch}.zip"
),
kind: ArchiveKind::Zip,
exe_rel: PathBuf::from(format!("podman-{version}/usr/bin/podman")),
ext: "zip",
version,
})
}
"windows" => {
let version = if arch == "amd64" {
PODMAN_VERSION_WINDOWS_AMD64
} else {
PODMAN_VERSION_WINDOWS_ARM64
};
Ok(Target {
url: format!(
"https://github.com/containers/podman/releases/download/v{version}/podman-remote-release-windows_{arch}.zip"
),
kind: ArchiveKind::Zip,
exe_rel: PathBuf::from(format!("podman-{version}/usr/bin/podman.exe")),
ext: "zip",
version,
})
}
other => Err(Error::Podman(format!("unsupported OS: {other}"))),
}
}
pub async fn ensure_installed(bin_dir: PathBuf) -> Result<PathBuf, Error> {
let target = resolve_target()?;
let root = bin_dir.join("podman").join(target.version);
let marker = root.join(".objectiveai-install-complete");
let exe = root.join(&target.exe_rel);
if tokio::fs::try_exists(&marker).await.unwrap_or(false) {
return Ok(exe);
}
let claim = objectiveai_sdk::lockfile::wait_acquire(
&bin_dir.join("locks"),
"podman",
&format!("pid {}", std::process::id()),
)
.await
.map_err(|e| Error::Podman(format!("bin lock: {e}")))?;
let result = async {
if tokio::fs::try_exists(&marker).await.unwrap_or(false) {
return Ok(());
}
install(&bin_dir, &root, &marker, &exe, &target).await
}
.await;
claim
.release()
.map_err(|e| Error::Podman(format!("bin lock release: {e}")))?;
result?;
Ok(exe)
}
async fn install(
bin_dir: &Path,
root: &Path,
marker: &Path,
exe: &Path,
target: &Target,
) -> Result<(), Error> {
let podman_dir = bin_dir.join("podman");
if tokio::fs::try_exists(root).await.unwrap_or(false) {
let trash = podman_dir.join(format!("{}.trash-{}", target.version, std::process::id()));
tokio::fs::rename(root, &trash)
.await
.map_err(|e| Error::Podman(format!("move partial install aside: {e}")))?;
}
sweep_leftovers(&podman_dir).await;
tokio::fs::create_dir_all(root)
.await
.map_err(|e| Error::Podman(format!("mkdir {root:?}: {e}")))?;
let archive = podman_dir.join(format!(
"{}.download-{}.{}",
target.version,
std::process::id(),
target.ext
));
if let Err(e) = download_to(&target.url, &archive).await {
let _ = tokio::fs::remove_file(&archive).await;
return Err(e);
}
let mut result = run_extract(&archive, root, target.kind).await;
for _ in 0..2 {
if result.is_ok() {
break;
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
result = run_extract(&archive, root, target.kind).await;
}
let _ = tokio::fs::remove_file(&archive).await;
result?;
if !tokio::fs::try_exists(exe).await.unwrap_or(false) {
return Err(Error::Podman(format!(
"podman executable not found after extract at {exe:?}"
)));
}
if std::env::consts::OS == "macos" {
fetch_macos_helpers(exe).await?;
}
tokio::fs::write(marker, b"")
.await
.map_err(|e| Error::Podman(format!("write {marker:?}: {e}")))?;
Ok(())
}
async fn fetch_macos_helpers(exe: &Path) -> Result<(), Error> {
let dir = exe
.parent()
.ok_or_else(|| Error::Podman(format!("podman exe has no parent: {exe:?}")))?;
let vfkit = dir.join("vfkit");
let vfkit_url =
format!("https://github.com/crc-org/vfkit/releases/download/v{VFKIT_VERSION}/vfkit");
download_to(&vfkit_url, &vfkit).await?;
set_executable(&vfkit).await?;
let gvproxy = dir.join("gvproxy");
let gvproxy_url = format!(
"https://github.com/containers/gvisor-tap-vsock/releases/download/v{GVPROXY_VERSION}/gvproxy-darwin"
);
download_to(&gvproxy_url, &gvproxy).await?;
set_executable(&gvproxy).await?;
let _ = ad_hoc_codesign(&gvproxy).await;
Ok(())
}
async fn set_executable(path: &Path) -> Result<(), Error> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
.await
.map_err(|e| Error::Podman(format!("chmod {path:?}: {e}")))?;
}
#[cfg(not(unix))]
{
let _ = path;
}
Ok(())
}
async fn ad_hoc_codesign(path: &Path) -> Result<(), Error> {
let status = tokio::process::Command::new("codesign")
.arg("--force")
.arg("--sign")
.arg("-")
.arg(path)
.status()
.await
.map_err(|e| Error::Podman(format!("spawn codesign: {e}")))?;
if status.success() {
Ok(())
} else {
Err(Error::Podman(format!("codesign failed for {path:?}")))
}
}
async fn run_extract(archive: &Path, root: &Path, kind: ArchiveKind) -> Result<(), Error> {
let archive = archive.to_path_buf();
let root = root.to_path_buf();
tokio::task::spawn_blocking(move || extract_archive(&archive, &root, kind))
.await
.map_err(|e| Error::Podman(format!("extract task join: {e}")))?
}
async fn sweep_leftovers(podman_dir: &Path) {
let Ok(mut entries) = tokio::fs::read_dir(podman_dir).await else {
return;
};
while let Ok(Some(entry)) = entries.next_entry().await {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.contains(".trash-") && !name.contains(".download-") {
continue;
}
let path = entry.path();
if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
let _ = tokio::fs::remove_dir_all(&path).await;
} else {
let _ = tokio::fs::remove_file(&path).await;
}
}
}
fn extract_archive(archive: &Path, dest: &Path, kind: ArchiveKind) -> Result<(), Error> {
let file = std::fs::File::open(archive)
.map_err(|e| Error::Podman(format!("open {archive:?}: {e}")))?;
match kind {
ArchiveKind::Zip => {
let mut zip = zip::ZipArchive::new(file)
.map_err(|e| Error::Podman(format!("read zip: {e}")))?;
zip.extract(dest)
.map_err(|e| Error::Podman(format!("extract zip: {e}")))?;
}
ArchiveKind::TarGz => {
let decoder = flate2::read::GzDecoder::new(file);
let mut tar = tar::Archive::new(decoder);
tar.unpack(dest)
.map_err(|e| Error::Podman(format!("extract tar.gz: {e}")))?;
}
}
Ok(())
}
async fn download_to(url: &str, dst: &Path) -> Result<(), Error> {
use futures::StreamExt as _;
use tokio::io::AsyncWriteExt as _;
let client = reqwest::Client::new();
let resp = client
.get(url)
.header(
"User-Agent",
format!("objectiveai/{}", env!("CARGO_PKG_VERSION")),
)
.timeout(DOWNLOAD_TIMEOUT)
.send()
.await
.map_err(|e| Error::Podman(format!("http: {e}")))?;
let status = resp.status();
if !status.is_success() {
return Err(Error::Podman(format!("download {url}: status {status}")));
}
let mut file = tokio::fs::File::create(dst)
.await
.map_err(|e| Error::Podman(format!("create {dst:?}: {e}")))?;
let mut stream = resp.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| Error::Podman(format!("http: {e}")))?;
file.write_all(&chunk)
.await
.map_err(|e| Error::Podman(format!("write {dst:?}: {e}")))?;
}
file.flush()
.await
.map_err(|e| Error::Podman(format!("flush {dst:?}: {e}")))?;
Ok(())
}