use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
if let Err(err) = run() {
eprintln!("build.rs: {err}");
std::process::exit(1);
}
}
fn run() -> Result<(), String> {
println!("cargo::rustc-check-cfg=cfg(rob_stub)");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=shim");
println!("cargo:rerun-if-changed=native");
println!("cargo:rerun-if-env-changed=ROB_GO_TAGS");
println!("cargo:rerun-if-env-changed=ROB_ALLOW_CROSS");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
println!("cargo:rerun-if-env-changed=CC");
println!("cargo:rerun-if-env-changed=CGO_ENABLED");
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|e| e.to_string())?);
let out_dir = PathBuf::from(env::var("OUT_DIR").map_err(|e| e.to_string())?);
let pointer_width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap_or_default();
if pointer_width != "64" {
return Err(format!(
"the Buildah ABI is LP64; target pointer width is {pointer_width}"
));
}
emit_abi(&manifest, &out_dir)?;
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "linux" {
if target_os == "macos" {
embed_guest(&manifest, &out_dir)?;
}
compile_stub();
return Ok(());
}
let host = env::var("HOST").unwrap_or_default();
let target = env::var("TARGET").unwrap_or_default();
if host != target && env::var_os("ROB_ALLOW_CROSS").is_none() {
return Err(format!(
"refusing to cross-compile the Buildah c-archive from {host} to {target}. \
cgo needs a Linux C toolchain. Set CC and ROB_ALLOW_CROSS=1 to attempt it. \
Non-Linux hosts link a stub; build on Linux for the real engine."
));
}
link_go_archive(&manifest, &out_dir, &host, &target)?;
Ok(())
}
fn compile_stub() {
cc::Build::new()
.file("native/stub.c")
.include("shim/include")
.compile("robshim");
println!("cargo:rustc-cfg=rob_stub");
}
fn embed_guest(manifest: &Path, out_dir: &Path) -> Result<(), String> {
println!("cargo:rerun-if-env-changed=ROB_GUEST_KERNEL");
println!("cargo:rerun-if-env-changed=ROB_GUEST_INITRD");
println!("cargo:rerun-if-env-changed=ROB_REQUIRE_EMBEDDED_GUEST");
println!("cargo:rerun-if-changed=guest/out/vmlinuz");
println!("cargo:rerun-if-changed=guest/out/initramfs");
let kernel_dest = out_dir.join("rob-guest-vmlinuz");
let initrd_dest = out_dir.join("rob-guest-initramfs");
match guest_sources(manifest)? {
Some((kernel, initrd)) => {
println!("cargo:rerun-if-changed={}", kernel.display());
println!("cargo:rerun-if-changed={}", initrd.display());
std::fs::copy(&kernel, &kernel_dest)
.map_err(|err| format!("copying {}: {err}", kernel.display()))?;
std::fs::copy(&initrd, &initrd_dest)
.map_err(|err| format!("copying {}: {err}", initrd.display()))?;
if std::fs::metadata(&kernel_dest)
.map(|m| m.len())
.unwrap_or(0)
== 0
|| std::fs::metadata(&initrd_dest)
.map(|m| m.len())
.unwrap_or(0)
== 0
{
return Err("refusing to embed an empty guest kernel or initramfs".into());
}
}
None => {
std::fs::write(&kernel_dest, []).map_err(|err| err.to_string())?;
std::fs::write(&initrd_dest, []).map_err(|err| err.to_string())?;
}
}
Ok(())
}
fn guest_sources(manifest: &Path) -> Result<Option<(PathBuf, PathBuf)>, String> {
let kernel_env = env::var("ROB_GUEST_KERNEL");
let initrd_env = env::var("ROB_GUEST_INITRD");
let (pair, from_env) = match (kernel_env, initrd_env) {
(Ok(kernel), Ok(initrd)) => ((PathBuf::from(kernel), PathBuf::from(initrd)), true),
(Ok(_), Err(_)) | (Err(_), Ok(_)) => {
return Err("set both ROB_GUEST_KERNEL and ROB_GUEST_INITRD".into());
}
(Err(_), Err(_)) => {
let dir = manifest.join("guest/out");
((dir.join("vmlinuz"), dir.join("initramfs")), false)
}
};
if pair.0.is_file() && pair.1.is_file() {
return Ok(Some(pair));
}
if env::var_os("ROB_REQUIRE_EMBEDDED_GUEST").is_some() {
return Err(format!(
"refusing to build without a Linux guest image. Looked for {} and {}.",
pair.0.display(),
pair.1.display()
));
}
if from_env {
return Err(format!(
"ROB_GUEST_KERNEL ({}) or ROB_GUEST_INITRD ({}) is missing",
pair.0.display(),
pair.1.display()
));
}
println!(
"cargo:warning=Linux guest images not found; the macOS binary will look for guest/out at runtime"
);
Ok(None)
}
fn emit_abi(manifest: &Path, out_dir: &Path) -> Result<(), String> {
let compiler = env::var("CC").unwrap_or_else(|_| "cc".to_string());
let bin = out_dir.join("abi_size");
let output = Command::new(&compiler)
.arg("-I")
.arg(manifest.join("shim/include"))
.arg("-o")
.arg(&bin)
.arg(manifest.join("native/abi_size.c"))
.output()
.map_err(|err| format!("compiling abi_size.c with {compiler}: {err}"))?;
if !output.status.success() {
return Err(format!(
"abi_size.c failed to compile:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
let printed = Command::new(&bin)
.output()
.map_err(|err| format!("running abi_size: {err}"))?;
if !printed.status.success() {
return Err(format!(
"abi_size failed:\n{}",
String::from_utf8_lossy(&printed.stderr)
));
}
std::fs::write(out_dir.join("abi_gen.rs"), &printed.stdout).map_err(|err| err.to_string())?;
Ok(())
}
fn link_go_archive(
manifest: &Path,
out_dir: &Path,
host: &str,
target: &str,
) -> Result<(), String> {
let go = require_go()?;
let mut tags = vec![
"exclude_graphdriver_btrfs".to_string(),
"exclude_graphdriver_devicemapper".to_string(),
"containers_image_openpgp".to_string(),
];
let skip_optional = env::var_os("ROB_DISABLE_OPTIONAL_LIBS").is_some();
let seccomp = if skip_optional {
false
} else if pkg_config_exists("libseccomp") {
true
} else {
println!(
"cargo:warning=libseccomp not found; building without the seccomp tag. RUN steps that install a seccomp profile will fail."
);
false
};
if seccomp {
tags.push("seccomp".to_string());
}
if !skip_optional && pkg_config_exists("libapparmor") {
tags.push("apparmor".to_string());
}
if let Ok(extra) = env::var("ROB_GO_TAGS") {
tags.extend(
extra
.split(|c: char| c == ',' || c.is_whitespace())
.filter(|s| !s.is_empty())
.map(str::to_string),
);
}
let archive = out_dir.join("librobshim.a");
let mut cmd = Command::new(&go);
cmd.current_dir(manifest.join("shim"))
.env("CGO_ENABLED", "1")
.env("GO111MODULE", "on")
.arg("build")
.arg("-buildmode=c-archive")
.arg("-trimpath")
.arg("-mod=readonly")
.arg("-buildvcs=false")
.arg("-tags")
.arg(tags.join(","))
.arg("-o")
.arg(&archive)
.arg(".");
if host != target {
cmd.env("GOOS", "linux");
cmd.env("GOARCH", goarch(target)?);
if let Ok(cc) = env::var("CC") {
cmd.env("CC", cc);
}
}
let output = cmd
.output()
.map_err(|err| format!("starting `{go} build`: {err}"))?;
if !output.status.success() {
return Err(format!(
"go build -buildmode=c-archive failed:\n{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
));
}
cc::Build::new()
.file("native/unshare_early.c")
.compile("rob_unshare_early");
println!("cargo:rustc-link-search=native={}", out_dir.display());
println!("cargo:rustc-link-lib=static:+whole-archive=robshim");
println!("cargo:rustc-link-lib=pthread");
println!("cargo:rustc-link-lib=dl");
println!("cargo:rustc-link-lib=m");
println!("cargo:rustc-link-lib=resolv");
if seccomp {
link_pkg_config("libseccomp")?;
}
if !skip_optional && pkg_config_exists("libapparmor") {
link_pkg_config("libapparmor")?;
}
Ok(())
}
fn require_go() -> Result<String, String> {
let go = env::var("GO").unwrap_or_else(|_| "go".to_string());
let output = Command::new(&go).arg("version").output().map_err(|err| {
format!("`{go}` not found ({err}). Linux builds of this crate need Go >= 1.26.")
})?;
if !output.status.success() {
return Err(format!("`{go} version` failed"));
}
let text = String::from_utf8_lossy(&output.stdout).to_string();
let token = text
.split_whitespace()
.find(|word| word.starts_with("go1.") || word.starts_with("go2."))
.ok_or_else(|| format!("cannot parse Go version from `{text}`"))?;
let numeric = token.trim_start_matches("go");
let mut parts = numeric.split('.');
let major: u32 = parts.next().unwrap_or("0").parse().unwrap_or(0);
let minor: u32 = parts.next().unwrap_or("0").parse().unwrap_or(0);
if major < 1 || (major == 1 && minor < 26) {
return Err(format!(
"{token} is too old. Buildah v1.45.1 requires Go >= 1.26."
));
}
Ok(go)
}
fn goarch(target: &str) -> Result<&'static str, String> {
let arch = target.split('-').next().unwrap_or("");
match arch {
"x86_64" => Ok("amd64"),
"aarch64" => Ok("arm64"),
"arm" => Ok("arm"),
"riscv64" => Ok("riscv64"),
"s390x" => Ok("s390x"),
"powerpc64le" => Ok("ppc64le"),
other => Err(format!("no GOARCH mapping for target arch {other}")),
}
}
fn pkg_config_exists(package: &str) -> bool {
Command::new("pkg-config")
.args(["--exists", package])
.status()
.map(|status| status.success())
.unwrap_or(false)
}
fn link_pkg_config(package: &str) -> Result<(), String> {
let output = Command::new("pkg-config")
.args(["--libs", package])
.output()
.map_err(|err| format!("pkg-config --libs {package}: {err}"))?;
if !output.status.success() {
return Err(format!(
"pkg-config --libs {package} failed:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
let text = String::from_utf8_lossy(&output.stdout);
for token in text.split_whitespace() {
if let Some(dir) = token.strip_prefix("-L") {
println!("cargo:rustc-link-search=native={dir}");
} else if let Some(lib) = token.strip_prefix("-l") {
println!("cargo:rustc-link-lib=dylib={lib}");
}
}
Ok(())
}