use std::ffi::{OsStr, OsString};
use std::fs;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::model::{Error, Result, ValidSource};
use crate::repository::materialize_source;
const BUILD_CONTAINERFILE: &str = include_str!("../Containerfile");
const RUNTIME_CONTAINERFILE: &str = include_str!("../RuntimeContainerfile");
const MAX_CAPTURE_BYTES: usize = 12 * 1024;
static COUNTER: AtomicU64 = AtomicU64::new(0);
pub(crate) fn check(source: &ValidSource, binary_name: &str) -> Result<()> {
build(source, binary_name).map(|_| ())
}
pub(crate) fn build(source: &ValidSource, binary_name: &str) -> Result<Vec<u8>> {
let image = image_name("build", BUILD_CONTAINERFILE);
ensure_image(OsStr::new("podman"), &image, BUILD_CONTAINERFILE)?;
let run = RunDirectory::new(source, "build")?;
run_cargo(
&image,
&run,
true,
"check.fetch",
&["fetch", "--color", "never"],
&[],
)?;
for (category, arguments) in [
("check.format", &["fmt", "--all", "--", "--check"][..]),
(
"check.build",
&[
"build",
"--workspace",
"--all-targets",
"--all-features",
"--locked",
"--offline",
"--color",
"never",
][..],
),
(
"check.clippy",
&[
"clippy",
"--workspace",
"--all-targets",
"--all-features",
"--locked",
"--offline",
"--color",
"never",
"--",
"-D",
"warnings",
][..],
),
(
"check.test",
&[
"test",
"--workspace",
"--all-targets",
"--all-features",
"--locked",
"--offline",
"--no-fail-fast",
"--color",
"never",
][..],
),
(
"check.doc_test",
&[
"test",
"--workspace",
"--all-features",
"--doc",
"--locked",
"--offline",
"--no-fail-fast",
"--color",
"never",
][..],
),
] {
run_cargo(&image, &run, false, category, arguments, &[])?;
}
run_cargo(
&image,
&run,
false,
"check.release",
&[
"build",
"--release",
"--bin",
binary_name,
"--target",
"x86_64-unknown-linux-musl",
"--all-features",
"--locked",
"--offline",
"--color",
"never",
],
&[
("CARGO_PROFILE_RELEASE_OPT_LEVEL", "z"),
("CARGO_PROFILE_RELEASE_LTO", "fat"),
("CARGO_PROFILE_RELEASE_CODEGEN_UNITS", "1"),
("CARGO_PROFILE_RELEASE_PANIC", "abort"),
("CARGO_PROFILE_RELEASE_STRIP", "symbols"),
("CARGO_PROFILE_RELEASE_INCREMENTAL", "false"),
],
)?;
let container_artifact = format!("/target/x86_64-unknown-linux-musl/release/{binary_name}");
let headers = run_program(
&image,
&run,
false,
"check.static",
"readelf",
&["-l", &container_artifact],
&[],
)?;
let dynamic = run_program(
&image,
&run,
false,
"check.static",
"readelf",
&["-d", &container_artifact],
&[],
)?;
let inspection = format!(
"{}\n{}\n{}\n{}",
String::from_utf8_lossy(&headers.stdout),
String::from_utf8_lossy(&headers.stderr),
String::from_utf8_lossy(&dynamic.stdout),
String::from_utf8_lossy(&dynamic.stderr)
);
if inspection.contains("Requesting program interpreter:") || inspection.contains("(NEEDED)") {
return Err(Error::new(
"check.static",
"release executable is dynamically linked",
));
}
let artifact = run
.target
.join("x86_64-unknown-linux-musl")
.join("release")
.join(binary_name);
let metadata = fs::symlink_metadata(&artifact)
.map_err(|error| Error::io("inspect release executable", &artifact, error))?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(Error::new(
"check.release",
format!(
"release artifact is not a regular file: {}",
artifact.display()
),
));
}
fs::read(&artifact).map_err(|error| Error::io("read release executable", &artifact, error))
}
pub(crate) fn ensure_runtime_image() -> Result<String> {
let image = image_name("runtime", RUNTIME_CONTAINERFILE);
ensure_image(OsStr::new("podman"), &image, RUNTIME_CONTAINERFILE)?;
Ok(image)
}
fn run_cargo(
image: &str,
run: &RunDirectory,
network: bool,
category: &'static str,
arguments: &[&str],
environment: &[(&str, &str)],
) -> Result<Output> {
run_program(
image,
run,
network,
category,
"cargo",
arguments,
environment,
)
}
fn run_program(
image: &str,
run: &RunDirectory,
network: bool,
category: &'static str,
program: &str,
arguments: &[&str],
environment: &[(&str, &str)],
) -> Result<Output> {
let mut command = podman_command(OsStr::new("podman"), run, network);
for &(key, value) in environment {
command.arg(format!("--env={key}={value}"));
}
command.arg(image).arg(program).args(arguments);
let output = run_capture(command, program)?;
if output.status.success() {
Ok(output)
} else {
Err(command_error(category, program, &output))
}
}
fn podman_command(podman: &OsStr, run: &RunDirectory, network: bool) -> Command {
let mut command = Command::new(podman);
command
.arg("run")
.arg("--rm")
.arg("--read-only")
.arg("--cap-drop=all")
.arg("--security-opt=no-new-privileges")
.arg("--userns=keep-id")
.arg("--tmpfs=/tmp:rw,nosuid,nodev,size=536870912")
.arg("--workdir=/workspace")
.arg("--env=CARGO_HOME=/cargo-home")
.arg("--env=CARGO_TARGET_DIR=/target")
.arg("--env=CARGO_TERM_COLOR=never")
.arg("--env=CARGO_NET_OFFLINE=true");
if network {
command.arg("--env=CARGO_NET_OFFLINE=false");
} else {
command.arg("--network=none");
}
command
.arg("--pull=never")
.arg("--volume")
.arg(volume_spec(&run.workspace, "/workspace"))
.arg("--volume")
.arg(volume_spec(&run.cargo_home, "/cargo-home"))
.arg("--volume")
.arg(volume_spec(&run.target, "/target"));
command
}
fn ensure_image(podman: &OsStr, image: &str, containerfile_contents: &str) -> Result<()> {
let mut inspect_command = Command::new(podman);
inspect_command.arg("image").arg("exists").arg(image);
let inspect = run_capture(inspect_command, "Podman image inspection")?;
if inspect.status.success() {
return Ok(());
}
if inspect.status.code() != Some(1) {
return Err(command_error(
"sandbox.image",
"Podman image inspection",
&inspect,
));
}
let build = TemporaryDirectory::new("image")?;
let containerfile = build.path().join("Containerfile");
fs::write(&containerfile, containerfile_contents)
.map_err(|error| Error::io("write embedded Containerfile", &containerfile, error))?;
let mut build_command = Command::new(podman);
build_command
.arg("build")
.arg("--tag")
.arg(image)
.arg("--file")
.arg(&containerfile)
.arg(build.path());
let output = run_capture(build_command, "Podman image build")?;
if output.status.success() {
Ok(())
} else {
Err(command_error(
"sandbox.image",
"Podman image build",
&output,
))
}
}
fn run_capture(mut command: Command, label: &str) -> Result<Output> {
command.stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = command.spawn().map_err(|error| {
Error::new("sandbox.start", format!("could not start {label}: {error}"))
})?;
let stdout = child
.stdout
.take()
.ok_or_else(|| Error::new("sandbox.start", format!("could not capture {label} stdout")))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| Error::new("sandbox.start", format!("could not capture {label} stderr")))?;
let (status, stdout, stderr) = std::thread::scope(|scope| {
let stdout_thread = scope.spawn(|| capture_stream(stdout));
let stderr_thread = scope.spawn(|| capture_stream(stderr));
let status = child.wait();
let stdout = stdout_thread.join();
let stderr = stderr_thread.join();
(status, stdout, stderr)
});
let status = status.map_err(|error| {
Error::new(
"sandbox.wait",
format!("could not wait for {label}: {error}"),
)
})?;
let stdout = stdout
.map_err(|_| Error::new("sandbox.capture", format!("{label} stdout reader panicked")))?
.map_err(|error| {
Error::new(
"sandbox.capture",
format!("could not read {label} stdout: {error}"),
)
})?;
let stderr = stderr
.map_err(|_| Error::new("sandbox.capture", format!("{label} stderr reader panicked")))?
.map_err(|error| {
Error::new(
"sandbox.capture",
format!("could not read {label} stderr: {error}"),
)
})?;
Ok(Output {
status,
stdout,
stderr,
})
}
fn capture_stream(mut reader: impl Read) -> io::Result<Vec<u8>> {
let mut retained = Vec::with_capacity(MAX_CAPTURE_BYTES);
let mut buffer = [0_u8; 4096];
let mut truncated = false;
loop {
let read = reader.read(&mut buffer)?;
if read == 0 {
break;
}
let remaining = MAX_CAPTURE_BYTES.saturating_sub(retained.len());
let keep = remaining.min(read);
retained.extend_from_slice(&buffer[..keep]);
truncated |= keep < read;
}
if truncated {
retained.extend_from_slice(b"\n[diagnostic truncated]");
}
Ok(retained)
}
fn command_error(category: &'static str, operation: &str, output: &Output) -> Error {
let stderr = bounded_text(&output.stderr);
let stdout = bounded_text(&output.stdout);
let detail = if stderr.trim().is_empty() {
stdout.trim()
} else {
stderr.trim()
};
Error::new(
category,
format!(
"{operation} exited with status {}: {}",
output
.status
.code()
.map_or_else(|| "signal".to_owned(), |code| code.to_string()),
if detail.is_empty() {
"no diagnostic output"
} else {
detail
}
),
)
}
fn bounded_text(bytes: &[u8]) -> String {
let keep = bytes.len().min(MAX_CAPTURE_BYTES);
let mut text = String::from_utf8_lossy(&bytes[..keep]).into_owned();
if keep < bytes.len() {
text.push_str("\n[diagnostic truncated]");
}
text
}
fn volume_spec(source: &Path, destination: &str) -> OsString {
let mut value = source.as_os_str().to_os_string();
value.push(":");
value.push(destination);
value.push(":rw,Z");
value
}
fn image_name(kind: &str, containerfile: &str) -> String {
let mut hash = 0xcbf29ce484222325_u64;
for byte in containerfile.bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x100000001b3);
}
format!(
"localhost/kcode-rust-bins-{kind}:{}-{hash:016x}",
env!("CARGO_PKG_VERSION")
)
}
pub(crate) struct TemporaryDirectory(PathBuf);
impl TemporaryDirectory {
pub(crate) fn new(label: &str) -> Result<Self> {
let parent = std::env::temp_dir().join("kcode-rust-bins");
fs::create_dir_all(&parent)
.map_err(|error| Error::io("create temporary root", &parent, error))?;
for _ in 0..100 {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = parent.join(format!(
"{label}-{}-{nanos:x}-{counter:x}",
std::process::id()
));
match fs::create_dir(&path) {
Ok(()) => return Ok(Self(path)),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(Error::io("create temporary directory", &path, error)),
}
}
Err(Error::new(
"sandbox.prepare",
"could not allocate a unique temporary directory",
))
}
pub(crate) fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TemporaryDirectory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
struct RunDirectory {
_root: TemporaryDirectory,
workspace: PathBuf,
cargo_home: PathBuf,
target: PathBuf,
}
impl RunDirectory {
fn new(source: &ValidSource, label: &str) -> Result<Self> {
let root = TemporaryDirectory::new(label)?;
let workspace = root.path().join("workspace");
let cargo_home = root.path().join("cargo-home");
let target = root.path().join("target");
fs::create_dir(&workspace)
.map_err(|error| Error::io("create disposable workspace", &workspace, error))?;
materialize_source(&workspace, &source.files)?;
fs::create_dir(&cargo_home)
.map_err(|error| Error::io("create disposable Cargo home", &cargo_home, error))?;
fs::create_dir(&target)
.map_err(|error| Error::io("create disposable target", &target, error))?;
Ok(Self {
_root: root,
workspace,
cargo_home,
target,
})
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use std::fs;
use crate::model::{File, validate_source};
use super::{RunDirectory, podman_command};
#[test]
fn offline_build_command_is_isolated() {
let source = validate_source(
&[
File {
path: "Cargo.toml".to_owned(),
contents: "[package]\nname = \"demo\"\nversion = \"0.1.0\"\n".to_owned(),
},
File {
path: "Documentation.md".to_owned(),
contents: String::new(),
},
File {
path: "src/main.rs".to_owned(),
contents: "fn main() {}\n".to_owned(),
},
],
"demo",
)
.unwrap();
let run = RunDirectory::new(&source, "command-test").unwrap();
let command = podman_command(OsStr::new("podman"), &run, false);
let arguments = command
.get_args()
.map(|argument| argument.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert!(arguments.contains(&"--network=none".to_owned()));
assert!(arguments.contains(&"--cap-drop=all".to_owned()));
assert!(arguments.contains(&"--security-opt=no-new-privileges".to_owned()));
assert!(fs::metadata(&run.workspace).unwrap().is_dir());
}
}