socketry-cargo-bake 0.19.1

Cargo launcher for project-local Bake tasks
// Released under the MIT License.
// Copyright, 2026, by Samuel Williams.

mod options;
mod project;

use options::Options;
use project::Project;
use std::ffi::OsString;
use std::process::{Command, ExitCode};

type Result<Output> = std::result::Result<Output, Box<dyn std::error::Error>>;

fn cargo() -> Command {
    Command::new(cargo_program(std::env::var_os("CARGO")))
}

fn cargo_program(program: Option<OsString>) -> OsString {
    program.unwrap_or_else(|| "cargo".into())
}

#[cfg(not(test))]
fn run() -> Result<i32> {
    run_with_arguments(std::env::args_os().skip(1), std::env::current_dir())
}

// Windows terminates directly from `std::process::exit`, before LLVM's profile
// runtime can write the child process's coverage data.
#[cfg(all(not(test), coverage, windows))]
unsafe extern "C" {
    fn __llvm_profile_write_file() -> i32;
}

#[cfg(all(not(test), coverage, windows))]
fn write_coverage_profile() {
    // The coverage runtime is linked by cargo-llvm-cov for coverage builds.
    unsafe {
        let _ = __llvm_profile_write_file();
    }
}

#[cfg(all(not(test), not(all(coverage, windows))))]
fn write_coverage_profile() {}

fn run_with_arguments(
    arguments: impl IntoIterator<Item = OsString>,
    current_directory: std::io::Result<std::path::PathBuf>,
) -> Result<i32> {
    let options = Options::parse(arguments)?;
    if options.help {
        println!(
            "cargo bake [OPTIONS] [TASK [ARGUMENTS] [:: TASK ...]]\n\nCompile and run the project's bake/ task crate.\n\nLauncher options (before TASK):\n  --manifest-path PATH  Project Cargo.toml (defaults to nearest ancestor)\n  --offline             Disable Cargo network access\n  --locked              Require unchanged Cargo.lock files\n  --release             Use Cargo's release profile\n  --regenerate          Create or refresh the private Bake task crate\n  --help                Show this launcher help without compiling tasks\n  --version             Show the launcher version\n\nTask options:\n  --list                List registered tasks\n  --json                Format the final task result as JSON\n  TASK --help           Show task arguments\n\nDefaults: workspace-root/bake/Cargo.toml. Override with\n[workspace.metadata.bake] or [package.metadata.bake] manifest = \"...\"."
        );
        return Ok(0);
    }
    if options.version {
        println!("cargo-bake {}", env!("CARGO_PKG_VERSION"));
        return Ok(0);
    }
    if options.regenerate && !options.arguments.is_empty() {
        return Err("--regenerate cannot be combined with task arguments".into());
    }
    run_project(options, current_directory)
}

fn run_project(
    options: Options,
    current_directory: std::io::Result<std::path::PathBuf>,
) -> Result<i32> {
    let mut command = cargo();
    run_project_with_command(options, current_directory, &mut command, command_status)
}

fn run_project_with_command(
    options: Options,
    current_directory: std::io::Result<std::path::PathBuf>,
    command: &mut Command,
    execute: impl FnOnce(&mut Command) -> Result<i32>,
) -> Result<i32> {
    match configure_project_command(options, current_directory, command)? {
        Some(code) => Ok(code),
        None => execute(command),
    }
}

fn configure_project_command(
    options: Options,
    current_directory: std::io::Result<std::path::PathBuf>,
    command: &mut Command,
) -> Result<Option<i32>> {
    let current_directory = current_directory?;
    if options.regenerate {
        Project::regenerate(&current_directory, &options)?;
        return Ok(Some(0));
    }
    let project = Project::discover(&current_directory, &options)?;
    command
        .args(["run", "--quiet", "--manifest-path"])
        .arg(&project.task_manifest)
        .arg("--package")
        .arg(&project.package)
        .arg("--bin")
        .arg(&project.binary)
        .current_dir(&project.root)
        .env("BAKE_PROJECT_ROOT", &project.root);
    options.configure(command);
    if options.release {
        command.arg("--release");
    }
    command.arg("--").args(&options.arguments);
    Ok(None)
}

fn command_status(command: &mut Command) -> Result<i32> {
    let status = command.status()?;
    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;
        Ok(status
            .code()
            .unwrap_or_else(|| 128 + status.signal().unwrap_or(1)))
    }
    #[cfg(not(unix))]
    Ok(status.code().unwrap_or(1))
}

fn process_result(
    result: Result<i32>,
    exit: impl FnOnce(i32) -> ExitCode,
    report: impl FnOnce(String),
) -> ExitCode {
    match result {
        Ok(code) => exit(code),
        Err(error) => {
            report(format!("cargo bake: {error}"));
            ExitCode::FAILURE
        }
    }
}

#[cfg(not(test))]
fn main() -> ExitCode {
    // Preserve the full process code, including Windows child exit codes.
    process_result(
        run(),
        |code| {
            write_coverage_profile();
            std::process::exit(code)
        },
        |message| eprintln!("{message}"),
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    fn assert_exit_code(code: i32) -> ExitCode {
        assert_eq!(code, 23);
        ExitCode::SUCCESS
    }

    fn assert_error_message(error: String) {
        assert_eq!(error, "cargo bake: expected failure");
    }

    #[test]
    fn cargo_program_uses_the_environment_or_default() {
        assert_eq!(cargo_program(None), "cargo");
        assert_eq!(cargo_program(Some("custom-cargo".into())), "custom-cargo");
        assert_eq!(
            cargo().get_program(),
            cargo_program(std::env::var_os("CARGO"))
        );
    }

    #[test]
    fn process_result_preserves_exit_codes_and_reports_errors() {
        let result = process_result(Ok(23), assert_exit_code, assert_error_message);
        assert_eq!(result, ExitCode::SUCCESS);

        let result = process_result(
            Err("expected failure".into()),
            assert_exit_code,
            assert_error_message,
        );
        assert_eq!(result, ExitCode::FAILURE);
    }

    #[test]
    fn handles_help_version_and_invalid_regeneration_arguments_without_a_project() {
        let directory = tempfile::tempdir().unwrap();
        let current_directory = || Ok(directory.path().to_path_buf());

        assert_eq!(
            run_with_arguments(["--help"].map(OsString::from), current_directory()).unwrap(),
            0
        );
        assert_eq!(
            run_with_arguments(["--version"].map(OsString::from), current_directory()).unwrap(),
            0
        );
        assert!(
            run_with_arguments(
                ["--regenerate", "greet"].map(OsString::from),
                current_directory(),
            )
            .is_err()
        );
        assert!(
            run_with_arguments(
                ["--help"].map(OsString::from),
                Err(std::io::Error::other("no current directory")),
            )
            .is_ok()
        );
    }

    #[test]
    fn reports_project_discovery_and_regeneration_errors() {
        let directory = tempfile::tempdir().unwrap();
        assert!(run_project(Options::default(), Ok(directory.path().to_path_buf())).is_err());

        std::fs::write(
            directory.path().join("Cargo.toml"),
            "[package]\nname = \"example\"\nversion = \"0.1.0\"\nedition = \"2024\"\n[package.metadata.bake]\nmanifest = \"\"\n",
        )
        .unwrap();
        assert!(
            run_project(
                Options {
                    regenerate: true,
                    ..Options::default()
                },
                Ok(directory.path().to_path_buf()),
            )
            .is_err()
        );

        assert!(
            run_project(
                Options::default(),
                Err(std::io::Error::other("no current directory")),
            )
            .is_err()
        );
    }

    #[test]
    fn reports_child_process_start_failures() {
        let directory = tempfile::tempdir().unwrap();
        let mut command = Command::new(PathBuf::from(directory.path()).join("missing-cargo"));

        assert!(command_status(&mut command).is_err());
    }

    #[test]
    fn configures_the_task_command_and_returns_regeneration_results() {
        let directory = tempfile::tempdir().unwrap();
        let root = directory.path();
        std::fs::create_dir_all(root.join("bake/src")).unwrap();
        std::fs::write(
            root.join("Cargo.toml"),
            "[workspace]\nmembers = [\"bake\"]\nresolver = \"3\"\n",
        )
        .unwrap();
        std::fs::write(
            root.join("bake/Cargo.toml"),
            "[package]\nname = \"example-bake\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
        )
        .unwrap();
        std::fs::write(root.join("bake/src/main.rs"), "fn main() {}\n").unwrap();

        let canonical_root = root.canonicalize().unwrap();
        let canonical_manifest = root.join("bake/Cargo.toml").canonicalize().unwrap();
        let mut command = Command::new("fake-cargo");
        let options = Options {
            offline: true,
            locked: true,
            release: true,
            arguments: ["greet", "Ada"].map(OsString::from).into(),
            ..Options::default()
        };
        let status =
            run_project_with_command(options, Ok(root.to_path_buf()), &mut command, |command| {
                assert_eq!(
                    command
                        .get_args()
                        .map(|argument| argument.to_os_string())
                        .collect::<Vec<_>>(),
                    [
                        "run",
                        "--quiet",
                        "--manifest-path",
                        canonical_manifest.to_str().unwrap(),
                        "--package",
                        "example-bake",
                        "--bin",
                        "example-bake",
                        "--offline",
                        "--locked",
                        "--release",
                        "--",
                        "greet",
                        "Ada",
                    ]
                    .map(OsString::from)
                    .to_vec()
                );
                assert_eq!(command.get_current_dir(), Some(canonical_root.as_path()));
                Ok(23)
            })
            .unwrap();
        assert_eq!(status, 23);

        assert_eq!(
            run_project_with_command(
                Options::default(),
                Ok(root.to_path_buf()),
                &mut Command::new("fake-cargo"),
                |command| {
                    assert!(!command.get_args().any(|argument| argument == "--release"));
                    Ok(17)
                },
            )
            .unwrap(),
            17
        );

        assert_eq!(
            run_project_with_command(
                Options {
                    regenerate: true,
                    ..Options::default()
                },
                Ok(root.to_path_buf()),
                &mut Command::new("fake-cargo"),
                command_status,
            )
            .unwrap(),
            0
        );
    }

    #[test]
    fn command_status_preserves_successful_child_exit_codes() {
        #[cfg(unix)]
        let mut command = {
            let mut command = Command::new("sh");
            command.args(["-c", "exit 23"]);
            command
        };
        #[cfg(windows)]
        let mut command = {
            let mut command = Command::new("cmd.exe");
            command.args(["/C", "exit 23"]);
            command
        };

        assert_eq!(command_status(&mut command).unwrap(), 23);
    }
}