use std::path::{Path, PathBuf};
use std::process::Command;
use clap::ValueHint;
use eyre::{Context, Result, bail};
use tempfile::TempDir;
use crate::cli::oci::common::perform_build;
use crate::config::Settings;
use crate::file;
use crate::oci::BuildOptions;
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Run {
#[clap(long, default_value = "auto")]
engine: Engine,
#[clap(long)]
from: Option<String>,
#[clap(long, value_hint = ValueHint::DirPath, conflicts_with_all = &["from", "mount_point", "no_mise"])]
image_dir: Option<PathBuf>,
#[clap(long)]
keep: bool,
#[clap(long)]
mount_point: Option<String>,
#[clap(long)]
no_mise: bool,
#[clap(long = "volume", alias = "mount", value_name = "HOST:CONTAINER")]
volume: Vec<String>,
#[clap(short = 'e', long = "env", value_name = "KEY=VAL")]
env: Vec<String>,
#[clap(short, long)]
interactive: bool,
#[clap(short, long)]
tty: bool,
#[clap(short = 'w', long = "workdir")]
workdir: Option<String>,
#[clap(last = true)]
cmd: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, clap::ValueEnum)]
enum Engine {
Auto,
Podman,
Docker,
}
impl Run {
pub async fn run(self) -> Result<()> {
Settings::get().ensure_experimental("mise oci run")?;
if let Some(d) = &self.image_dir
&& !d.join("index.json").is_file()
{
bail!(
"{}: does not look like an OCI image layout (missing index.json)",
d.display()
);
}
let engine = select_engine(self.engine)?;
let (image_dir, _tempdir_guard): (PathBuf, Option<TempDir>) =
if let Some(d) = &self.image_dir {
(d.clone(), None)
} else {
let td = TempDir::with_prefix("mise-oci-run-")
.wrap_err("creating temp dir for oci build output")?;
let out_dir = td.path().join("image");
let opts = BuildOptions {
out_dir: out_dir.clone(),
from: self.from.clone(),
tag: Some("mise-oci:run".to_string()),
mount_point: self.mount_point.clone(),
include_mise: !self.no_mise,
};
let built = perform_build(opts).await?;
info!("built image: {}", built.manifest_digest);
(out_dir, Some(td))
};
let image_ref = load_image(engine, &image_dir)?;
let engine_bin = engine_name(engine);
let mut args: Vec<String> = vec!["run".into()];
if !self.keep {
args.push("--rm".into());
}
if self.interactive {
args.push("-i".into());
}
if self.tty {
args.push("-t".into());
}
for e in &self.env {
args.push("-e".into());
args.push(e.clone());
}
for v in &self.volume {
args.push("-v".into());
args.push(v.clone());
}
if let Some(w) = &self.workdir {
args.push("-w".into());
args.push(w.clone());
}
args.push(image_ref.clone());
args.extend(self.cmd.clone());
let run_result = Command::new(engine_bin)
.args(&args)
.status()
.wrap_err_with(|| format!("exec {engine_bin} {args:?}"));
if !self.keep {
let rmi = Command::new(engine_bin)
.args(["rmi", "--force", &image_ref])
.output();
match rmi {
Ok(out) if out.status.success() => {}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
debug!("{engine_bin} rmi {image_ref} failed: {stderr}");
}
Err(e) => debug!("failed to spawn {engine_bin} rmi: {e}"),
}
}
let status = run_result?;
if let Some(code) = status.code() {
if code != 0 {
std::process::exit(code);
}
} else if !status.success() {
bail!("{engine_bin} exited abnormally: {status:?}");
}
Ok(())
}
}
fn select_engine(requested: Engine) -> Result<Engine> {
match requested {
Engine::Podman => {
if file::which("podman").is_some() {
Ok(Engine::Podman)
} else {
bail!("--engine podman requested but `podman` was not found on PATH")
}
}
Engine::Docker => {
if file::which("docker").is_none() {
bail!("--engine docker requested but `docker` was not found on PATH");
}
if file::which("skopeo").is_none() {
bail!(
"--engine docker requires `skopeo` (to load the OCI layout into the \
docker daemon). Install skopeo or use `--engine podman`."
);
}
Ok(Engine::Docker)
}
Engine::Auto => {
if file::which("podman").is_some() {
Ok(Engine::Podman)
} else if file::which("docker").is_some() && file::which("skopeo").is_some() {
Ok(Engine::Docker)
} else {
bail!(
"no supported container engine found. Install one of:\n \
- podman (native OCI-layout support)\n \
- docker + skopeo (to load the OCI layout into the docker daemon)"
)
}
}
}
}
fn engine_name(engine: Engine) -> &'static str {
match engine {
Engine::Podman => "podman",
Engine::Docker => "docker",
Engine::Auto => unreachable!("select_engine resolves Auto to a concrete engine"),
}
}
fn load_image(engine: Engine, image_dir: &Path) -> Result<String> {
match engine {
Engine::Podman => {
let src = format!("oci:{}", image_dir.display());
let out = Command::new("podman")
.args(["pull", "--quiet", &src])
.output()
.wrap_err("running `podman pull`")?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
bail!("podman pull failed: {}: {stderr}", out.status);
}
let id = String::from_utf8(out.stdout)
.wrap_err("podman pull produced non-utf8 output")?
.trim()
.to_string();
if id.is_empty() {
bail!("podman pull succeeded but printed no image ID");
}
Ok(id)
}
Engine::Docker => {
let tag = format!(
"mise-oci:run-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let src = format!("oci:{}", image_dir.display());
let dst = format!("docker-daemon:{tag}");
let status = Command::new("skopeo")
.args(["copy", &src, &dst])
.status()
.wrap_err("running `skopeo copy`")?;
if !status.success() {
bail!(
"skopeo copy failed ({status:?}). Ensure the docker daemon is running and \
your user has access to the socket."
);
}
Ok(tag)
}
Engine::Auto => unreachable!(),
}
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
Build the current mise.toml and drop into bash:
$ <bold>mise oci run -it -- bash</bold>
Run a one-shot command with env + volume (note: `-v` is reserved
for --verbose, so use `--volume`):
$ <bold>mise oci run -e DEBUG=1 --volume $PWD:/work -w /work -- npm test</bold>
Re-use a previously built layout (skip the build step):
$ <bold>mise oci build -o ./img && mise oci run --image-dir ./img -- node -e 'console.log(process.version)'</bold>
<bold><underline>Engines:</underline></bold>
Prefers <bold>podman</bold> (loads OCI layouts natively). Falls back to
<bold>docker + skopeo</bold>. Pass <bold>--engine podman</bold> or <bold>--engine docker</bold> to override.
"#
);