cargo-elfpak 0.2.0

Cargo subcommand for packaging a binary and its Linux runtime closure
Documentation
use crate::metadata::Selection;
use anyhow::{Context, Result, bail};
use cargo_metadata::Message;
use std::{
    ffi::OsString,
    io::{BufReader, Read},
    path::{Path, PathBuf},
    process::{ChildStderr, Command, Stdio},
    thread,
};

const CARGO_STDERR_MAX_BYTES: usize = 1024 * 1024;

#[derive(Debug)]
pub(crate) struct BuildRequest<'a> {
    pub(crate) selection: &'a Selection,
    pub(crate) current_dir: &'a Path,
    pub(crate) manifest_path: Option<&'a Path>,
    pub(crate) release: bool,
    pub(crate) profile: Option<&'a str>,
    pub(crate) target: Option<&'a str>,
    pub(crate) target_dir: Option<&'a Path>,
    pub(crate) features: &'a [String],
    pub(crate) all_features: bool,
    pub(crate) no_default_features: bool,
    pub(crate) locked: bool,
    pub(crate) offline: bool,
    pub(crate) frozen: bool,
    pub(crate) quiet: bool,
}

#[derive(Debug)]
pub(crate) struct BuildArtifact {
    pub(crate) executable: PathBuf,
    pub(crate) fresh: bool,
}

pub(crate) fn run(request: &BuildRequest<'_>) -> Result<BuildArtifact> {
    let cargo = std::env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo"));
    let mut command = Command::new(&cargo);
    command
        .current_dir(request.current_dir)
        .args([
            "build",
            "--message-format=json-render-diagnostics",
            "--package",
            &request.selection.package_name,
            "--bin",
            &request.selection.binary_name,
        ])
        .stdout(Stdio::piped());
    append_path_option(&mut command, "--manifest-path", request.manifest_path);
    append_flag(&mut command, "--release", request.release);
    append_value_option(&mut command, "--profile", request.profile);
    append_value_option(&mut command, "--target", request.target);
    append_path_option(&mut command, "--target-dir", request.target_dir);
    for features in request.features {
        command.args(["--features", features]);
    }
    append_flag(&mut command, "--all-features", request.all_features);
    append_flag(
        &mut command,
        "--no-default-features",
        request.no_default_features,
    );
    append_flag(&mut command, "--locked", request.locked);
    append_flag(&mut command, "--offline", request.offline);
    append_flag(&mut command, "--frozen", request.frozen);
    append_flag(&mut command, "--quiet", request.quiet);
    if request.quiet {
        command.stderr(Stdio::piped());
    }

    let mut child = command
        .spawn()
        .with_context(|| format!("could not run `{}`", cargo.to_string_lossy()))?;
    let stdout = child.stdout.take().context("could not read Cargo output")?;
    let stderr_reader = child.stderr.take().map(read_stderr);
    let mut artifacts = Vec::new();

    for message in Message::parse_stream(BufReader::new(stdout)) {
        match message.context("could not parse Cargo build output")? {
            Message::CompilerArtifact(artifact)
                if artifact.package_id == request.selection.package_id
                    && artifact.target.name == request.selection.binary_name
                    && artifact.target.is_bin() =>
            {
                if let Some(executable) = artifact.executable {
                    artifacts.push(BuildArtifact {
                        executable: executable.into_std_path_buf(),
                        fresh: artifact.fresh,
                    });
                }
            }
            _ => {}
        }
    }

    let status = child.wait().context("could not wait for Cargo build")?;
    let stderr = match stderr_reader {
        Some(reader) => reader
            .join()
            .map_err(|_| anyhow::anyhow!("Cargo stderr reader panicked"))??,
        None => CapturedStderr::default(),
    };
    if !status.success() {
        eprint!("{}", String::from_utf8_lossy(&stderr.bytes));
        if stderr.truncated {
            eprintln!("Cargo stderr was truncated after {CARGO_STDERR_MAX_BYTES} bytes");
        }
        bail!("Cargo build failed with status {status}");
    }
    match artifacts.len() {
        1 => Ok(artifacts.remove(0)),
        0 => bail!(
            "Cargo produced no executable artifact for package `{}` binary `{}`",
            request.selection.package_name,
            request.selection.binary_name
        ),
        count => bail!(
            "Cargo produced {count} executable artifacts for package `{}` binary `{}`",
            request.selection.package_name,
            request.selection.binary_name
        ),
    }
}

#[derive(Default)]
struct CapturedStderr {
    bytes: Vec<u8>,
    truncated: bool,
}

fn read_stderr(stderr: ChildStderr) -> thread::JoinHandle<std::io::Result<CapturedStderr>> {
    thread::spawn(move || {
        let mut bytes = Vec::new();
        let limit = u64::try_from(CARGO_STDERR_MAX_BYTES).expect("stderr limit fits u64");
        let mut limited = stderr.take(limit + 1);
        limited.read_to_end(&mut bytes)?;
        let truncated = bytes.len() > CARGO_STDERR_MAX_BYTES;
        if truncated {
            bytes.truncate(CARGO_STDERR_MAX_BYTES);
        }
        std::io::copy(&mut limited.into_inner(), &mut std::io::sink())?;
        Ok(CapturedStderr { bytes, truncated })
    })
}

fn append_flag(command: &mut Command, flag: &str, enabled: bool) {
    if enabled {
        command.arg(flag);
    }
}

fn append_value_option(command: &mut Command, flag: &str, value: Option<&str>) {
    if let Some(value) = value {
        command.args([flag, value]);
    }
}

fn append_path_option(command: &mut Command, flag: &str, value: Option<&Path>) {
    if let Some(value) = value {
        command.arg(flag).arg(value);
    }
}