use std::{fs, io, io::Write, path::PathBuf, process::Stdio};
use anyhow::{anyhow, bail, ensure, Context};
use cargo_metadata::{Artifact, ArtifactProfile, Message};
use clap::Parser;
use risc0_build::cargo_command;
use risc0_zkvm::{default_executor, ExecutorEnv, ExitCode};
use tempfile::{tempdir, TempDir};
const ZIP_CONTENTS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/cargo-risczero.zip"));
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum BuildSubcommand {
Build,
Test,
}
impl AsRef<str> for BuildSubcommand {
fn as_ref(&self) -> &'static str {
match &self {
Self::Build => "build",
Self::Test => "test",
}
}
}
#[derive(Parser)]
pub struct BuildCommand {
#[clap(long, default_value = "./Cargo.toml")]
pub manifest_path: PathBuf,
#[clap(long)]
pub target_dir: Option<PathBuf>,
pub args: Vec<String>,
}
fn get_zip_file(dir: &TempDir, filename: &str) -> anyhow::Result<PathBuf> {
let mut zip = zip::ZipArchive::new(io::Cursor::new(ZIP_CONTENTS))?;
let mut file = zip.by_name(filename)?;
let dest_path = dir.path().join(filename);
let mut dest_file = fs::File::create(&dest_path)?;
io::copy(&mut file, &mut dest_file)?;
Ok(dest_path)
}
impl BuildCommand {
pub fn run(&self, subcommand: BuildSubcommand) -> anyhow::Result<()> {
let manifest_path = match fs::canonicalize(&self.manifest_path) {
Ok(path) => path,
Err(ref err) => bail!(
"failed to resolve manifest path `{}`: {}",
&self.manifest_path.display(),
err
),
};
let tmpdir = tempdir()?;
let rust_runtime = get_zip_file(&tmpdir, "risc0-zkvm-platform.a")?;
let target_dir = &self
.target_dir
.clone()
.unwrap_or_else(|| risc0_build::get_target_dir(&manifest_path));
fs::create_dir_all(&target_dir)
.with_context(|| "failed to ensure target directory exists")?;
let mut cmd = cargo_command(
subcommand.as_ref(),
&[
"-C",
&format!(
"link_arg={}",
rust_runtime
.to_str()
.ok_or_else(|| anyhow!("invalid path string for rust_runtime"))?
),
],
);
cmd.arg("--message-format=json");
cmd.args(&[
"--manifest-path",
manifest_path
.to_str()
.ok_or_else(|| anyhow!("invalid path string for manifest_path"))?,
"--target-dir",
target_dir
.to_str()
.ok_or_else(|| anyhow!("invalid path string for target_dir"))?,
]);
let mut no_run_flag = false;
let mut test_args = vec![];
match subcommand {
BuildSubcommand::Test => {
let mut test_args_delimiter_seen = false;
cmd.arg("--no-run");
for arg in &self.args {
if test_args_delimiter_seen {
test_args.push(arg.clone());
} else if arg == "--no-run" {
no_run_flag = true;
} else if arg == "--" {
test_args_delimiter_seen = true;
} else {
cmd.arg(&arg);
}
}
}
BuildSubcommand::Build => {
cmd.args(&self.args);
}
}
println!("Running command: {:?}", &cmd);
let mut child = cmd.stdout(Stdio::piped()).spawn()?;
let reader = std::io::BufReader::new(
child
.stdout
.take()
.ok_or(anyhow!("failed to read from cmd stdout"))?,
);
let mut tests: Vec<String> = Vec::new();
for message in Message::parse_stream(reader) {
match message? {
Message::CompilerArtifact(Artifact {
executable: Some(exec_path),
profile: ArtifactProfile { test: true, .. },
..
}) => {
tests.push(exec_path.to_string());
}
Message::CompilerMessage(msg) => {
write!(io::stderr(), "{}", msg)?;
}
_ => (),
}
}
let output = child
.wait()
.with_context(|| "couldn't get cargo's exit status")?;
if !output.success() {
bail!("failed to build crate")
}
if subcommand == BuildSubcommand::Test && !no_run_flag {
eprintln!("Running tests: {tests:?}");
for test in tests {
eprintln!("Running test in guest: {test} {test_args:?}");
let env = ExecutorEnv::builder()
.args(&[test.clone()])
.args(&test_args)
.env_var("RUST_TEST_NOCAPTURE", "1")
.build()?;
let exec = default_executor();
let session = exec.execute(env, &fs::read(test)?)?;
ensure!(
session.exit_code == ExitCode::Halted(0),
"test exited with code {:?}",
session.exit_code
);
}
};
Ok(())
}
}