use std::path::Path;
use tokio::process::Command;
#[derive(Debug, serde::Deserialize)]
struct SystemStatus {
status: String,
#[serde(rename = "appRoot")]
app_root: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
struct VersionInfo {
version: String,
#[serde(rename = "buildType")]
build_type: Option<String>,
commit: Option<String>,
#[serde(rename = "appName")]
app_name: Option<String>,
}
pub async fn run(container_cli: &str) {
if !check_cli(container_cli).await {
return;
}
println!();
println!("container system:");
let Some(status) = fetch_system_status(container_cli).await else {
return;
};
if ensure_system_running(container_cli, &status).await {
ensure_kernel(container_cli, &status).await;
}
ensure_rosetta_disabled(container_cli).await;
}
async fn check_cli(container_cli: &str) -> bool {
let Ok(bin_path) = which::which(container_cli) else {
println!("container CLI: {container_cli} (NOT FOUND)");
println!(" Install from: https://github.com/apple/container/releases");
return false;
};
println!("container CLI: {} (found)", bin_path.display());
match Command::new(container_cli)
.args(["system", "version", "--format", "json"])
.output()
.await
{
Ok(output) => match serde_json::from_slice::<Vec<VersionInfo>>(&output.stdout) {
Ok(versions) => {
let info = versions
.iter()
.find(|v| v.app_name.as_deref() == Some("container"))
.or_else(|| versions.first());
match info {
Some(v) => {
let build = v.build_type.as_deref().unwrap_or("release");
let commit = v.commit.as_deref().unwrap_or("");
println!(" version: {} ({build}, {commit})", v.version);
}
None => println!(" version: (empty version response)"),
}
}
Err(e) => {
let raw = String::from_utf8_lossy(&output.stdout);
println!(
" version: (JSON parse error: {e}; output: {})",
raw.trim()
);
}
},
Err(e) => println!(" version: (error running version command: {e})"),
}
true
}
async fn fetch_system_status(container_cli: &str) -> Option<SystemStatus> {
let output = match Command::new(container_cli)
.args(["system", "status", "--format", "json"])
.output()
.await
{
Ok(o) => o,
Err(e) => {
println!(" status: (error running status command: {e})");
return None;
}
};
match serde_json::from_slice::<SystemStatus>(&output.stdout) {
Ok(s) => Some(s),
Err(e) => {
let stderr = String::from_utf8_lossy(&output.stderr);
println!(
" status: (JSON parse error: {e}; stderr: {})",
stderr.trim()
);
None
}
}
}
async fn ensure_system_running(container_cli: &str, status: &SystemStatus) -> bool {
if status.status == "running" {
println!(" status: running \u{2713}");
return true;
}
println!(
" status: {} \u{2014} starting (with kernel install)...",
status.status
);
match Command::new(container_cli)
.args(["system", "start", "--enable-kernel-install"])
.status()
.await
{
Ok(s) if s.success() => {
println!(" status: started \u{2713}");
true
}
Ok(s) => {
println!(
" status: `{container_cli} system start` failed (exit code: {:?})",
s.code()
);
false
}
Err(e) => {
println!(" status: failed to start: {e}");
false
}
}
}
async fn ensure_kernel(container_cli: &str, status: &SystemStatus) {
let kernel_present = status
.app_root
.as_deref()
.filter(|r| !r.is_empty())
.is_some_and(|root| {
let kernels_dir = Path::new(root).join("kernels");
std::fs::read_dir(kernels_dir).is_ok_and(|mut d| d.next().is_some())
});
if kernel_present {
println!(" kernel: configured \u{2713}");
return;
}
println!(" kernel: not configured \u{2014} installing recommended kernel...");
match Command::new(container_cli)
.args(["system", "kernel", "set", "--recommended"])
.status()
.await
{
Ok(s) if s.success() => println!(" kernel: installed \u{2713}"),
Ok(s) => {
println!(
" kernel: `{container_cli} system kernel set --recommended` failed \
(exit code: {:?})",
s.code()
);
println!(" Run it manually for details.");
}
Err(e) => println!(" kernel: failed to run: {e}"),
}
}
async fn ensure_rosetta_disabled(container_cli: &str) {
let rosetta_installed = Command::new("/usr/bin/arch")
.args(["-x86_64", "/usr/bin/true"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.is_ok_and(|s| s.success());
if rosetta_installed {
return;
}
let prop_ok = Command::new(container_cli)
.args(["system", "property", "set", "build.rosetta", "false"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.is_ok_and(|s| s.success());
if prop_ok {
println!(" build: Rosetta not installed \u{2014} set build.rosetta=false \u{2713}");
} else {
println!(" build: Rosetta not installed but could not set property");
println!(" Run: {container_cli} system property set build.rosetta false");
}
let stopped = Command::new(container_cli)
.args(["builder", "stop"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.is_ok_and(|s| s.success());
let deleted = Command::new(container_cli)
.args(["builder", "delete", "--force"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.is_ok_and(|s| s.success());
if stopped || deleted {
println!(" build: builder reset \u{2713}");
}
}