zisk-build 1.1.0-alpha

Build tooling for compiling guest programs to RISC-V ELF for the ZisK zkVM
use crate::{BuildArgs, HELPER_TARGET_SUBDIR, ZISK_TARGET};
use anyhow::{Context, Result};
use cargo_metadata::camino::Utf8PathBuf;
use std::{path::PathBuf, process::Command};

/// Get the command to build the program locally.
pub(crate) fn create_command(
    args: &BuildArgs,
    program_dir: &Utf8PathBuf,
    program_metadata: &cargo_metadata::Metadata,
) -> Result<Command> {
    // Construct the cargo run command
    let mut command = Command::new("cargo");
    command.args(["+zisk", "build"]);
    // Add the feature selection flags
    if let Some(features) = &args.features {
        command.arg("--features").arg(features);
    }
    if args.all_features {
        command.arg("--all-features");
    }

    if args.no_default_features {
        command.arg("--no-default-features");
    }
    if args.release {
        command.arg("--release");
    }

    for package in &args.packages {
        command.args(["--package", package]);
    }
    for bin in &args.binaries {
        command.args(["--bin", bin]);
    }

    command.args(["--target", ZISK_TARGET]);

    // Set up the command to inherit the parent's stdout and stderr
    // command.stdout(Stdio::inherit());
    // command.stderr(Stdio::inherit());

    // // Execute the command
    // let status = command.status().context("Failed to execute cargo build command")?;
    // if !status.success() {
    //     return Err(anyhow!("Cargo run command failed with status {}", status));
    // }

    let rustc_bin = zisk_rustc()?;

    command.env("RUSTC", rustc_bin.display().to_string()).env_remove("RUSTC_WORKSPACE_WRAPPER");

    let canonicalized_program_dir =
        program_dir.canonicalize().context("Failed to canonicalize program directory")?;
    command.current_dir(canonicalized_program_dir);

    // Use a separate subdirectory to avoid conflicts with the host build
    command.env("CARGO_TARGET_DIR", program_metadata.target_directory.join(HELPER_TARGET_SUBDIR));

    Ok(command)
}

/// Path to the ZisK toolchain's rustc (`<sysroot>/bin/rustc`) — the only rustc
/// that can load the custom guest target spec. Cached: both `create_command`
/// and `guest_rustflags` need it, and the sysroot cannot change mid-process.
pub(crate) fn zisk_rustc() -> Result<PathBuf> {
    static ZISK_RUSTC: std::sync::OnceLock<std::result::Result<PathBuf, String>> =
        std::sync::OnceLock::new();
    ZISK_RUSTC
        .get_or_init(|| zisk_rustc_lookup().map_err(|err| format!("{err:#}")))
        .clone()
        .map_err(|err| anyhow::anyhow!(err))
}

fn zisk_rustc_lookup() -> Result<PathBuf> {
    let output = Command::new("rustc")
        .env("RUSTUP_TOOLCHAIN", crate::RUSTUP_TOOLCHAIN_NAME)
        .arg("--print")
        .arg("sysroot")
        .output()
        .map_err(|_| {
            anyhow::anyhow!(
                "ZisK toolchain '{}' is not installed or rustup is not available.\n\
                 Run `cargo zisk toolchain install` to install it.",
                crate::RUSTUP_TOOLCHAIN_NAME
            )
        })?;

    if !output.status.success() {
        anyhow::bail!(
            "ZisK toolchain '{}' is not installed.\n\
             Run `cargo zisk toolchain install` to install it.",
            crate::RUSTUP_TOOLCHAIN_NAME
        );
    }

    let stdout_string =
        String::from_utf8(output.stdout).context("Can't parse rustc --print sysroot stdout")?;

    Ok(PathBuf::from(stdout_string.trim()).join("bin/rustc"))
}