#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
use std::{
collections::HashMap,
default::Default,
env,
fs::{self, File},
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
};
use cargo_metadata::{MetadataCommand, Package};
use risc0_binfmt::{MemoryImage, Program};
use risc0_zkp::core::digest::{Digest, DIGEST_WORDS};
use risc0_zkvm_platform::{memory, PAGE_SIZE};
use serde::Deserialize;
const RUSTUP_TOOLCHAIN_NAME: &str = "risc0";
#[derive(Debug, Deserialize)]
struct Risc0Metadata {
methods: Vec<String>,
}
impl Risc0Metadata {
fn from_package(pkg: &Package) -> Option<Risc0Metadata> {
let obj = pkg.metadata.get("risc0").unwrap();
serde_json::from_value(obj.clone()).unwrap()
}
}
#[cfg(feature = "guest-list")]
#[derive(Debug, Clone)]
pub struct GuestListEntry<'a> {
pub name: &'a str,
pub elf: &'a [u8],
pub image_id: [u32; DIGEST_WORDS],
pub path: &'a str,
}
#[derive(Debug)]
struct Risc0Method {
name: String,
elf_path: PathBuf,
}
impl Risc0Method {
fn make_image_id(&self) -> Digest {
if !self.elf_path.exists() {
eprintln!(
"RISC-V method was not found at: {:?}",
self.elf_path.to_str().unwrap()
);
std::process::exit(-1);
}
let elf = fs::read(&self.elf_path).unwrap();
let program = Program::load_elf(&elf, memory::MEM_SIZE as u32).unwrap();
let image = MemoryImage::new(&program, PAGE_SIZE as u32).unwrap();
image.compute_id()
}
fn rust_def(&self) -> String {
let elf_path = self.elf_path.display();
if elf_path.to_string().contains('#') {
panic!("method path cannot include #: {}", elf_path);
}
let upper = self.name.to_uppercase().replace('-', "_");
let image_id: [u32; DIGEST_WORDS] = self.make_image_id().into();
let elf_contents = std::fs::read(&self.elf_path).unwrap();
format!(
r##"
pub const {upper}_ELF: &[u8] = &{elf_contents:?};
pub const {upper}_ID: [u32; 8] = {image_id:?};
pub const {upper}_PATH: &str = r#"{elf_path}"#;
"##
)
}
#[cfg(feature = "guest-list")]
fn guest_list_entry(&self) -> String {
let upper = self.name.to_uppercase().replace('-', "_");
format!(
r##"
GuestListEntry {{
name: "{upper}",
elf: {upper}_ELF,
image_id: {upper}_ID,
path: {upper}_PATH,
}}"##
)
}
}
fn get_package<P>(manifest_dir: P) -> Package
where
P: AsRef<Path>,
{
let manifest_path = manifest_dir.as_ref().join("Cargo.toml");
let manifest_meta = MetadataCommand::new()
.manifest_path(&manifest_path)
.no_deps()
.exec()
.unwrap();
let mut matching: Vec<&Package> = manifest_meta
.packages
.iter()
.filter(|pkg| {
let std_path: &Path = pkg.manifest_path.as_ref();
std_path == manifest_path
})
.collect();
if matching.is_empty() {
eprintln!(
"ERROR: No package found in {}",
manifest_dir.as_ref().display()
);
std::process::exit(-1);
}
if matching.len() > 1 {
eprintln!(
"ERROR: Multiple packages found in {}",
manifest_dir.as_ref().display()
);
std::process::exit(-1);
}
matching.pop().unwrap().clone()
}
fn current_package() -> Package {
get_package(env::var("CARGO_MANIFEST_DIR").unwrap())
}
fn guest_packages(pkg: &Package) -> Vec<Package> {
let manifest_dir = pkg.manifest_path.parent().unwrap();
Risc0Metadata::from_package(pkg)
.unwrap()
.methods
.iter()
.map(|inner| get_package(manifest_dir.join(inner)))
.collect()
}
fn guest_methods<P>(pkg: &Package, target_dir: P) -> Vec<Risc0Method>
where
P: AsRef<Path>,
{
pkg.targets
.iter()
.filter(|target| target.kind.iter().any(|kind| kind == "bin"))
.map(|target| Risc0Method {
name: target.name.clone(),
elf_path: target_dir
.as_ref()
.join("riscv32im-risc0-zkvm-elf")
.join("release")
.join(&target.name),
})
.collect()
}
fn build_guest_package<P>(pkg: &Package, target_dir: P, features: Vec<String>)
where
P: AsRef<Path>,
{
let skip_var_name = "RISC0_SKIP_BUILD";
println!("cargo:rerun-if-env-changed={}", skip_var_name);
if env::var(skip_var_name).is_ok() {
return;
}
fs::create_dir_all(target_dir.as_ref()).unwrap();
let mut args = vec![
"+risc0",
"build",
"--release",
"--target",
"riscv32im-risc0-zkvm-elf",
"--manifest-path",
pkg.manifest_path.as_str(),
"--target-dir",
target_dir.as_ref().to_str().unwrap(),
];
let features_str = features.join(",");
if !features.is_empty() {
args.push("--features");
args.push(&features_str);
}
println!("Building guest package: cargo {}", args.join(" "));
let mut cmd = Command::new("cargo");
for (key, _) in env::vars().filter(|x| x.0.starts_with("CARGO") || x.0.starts_with("RUSTUP")) {
cmd.env_remove(key);
}
let mut child = cmd
.env(
"CARGO_ENCODED_RUSTFLAGS",
[
"-C",
"passes=loweratomic",
"-C",
&format!("link-arg=-Ttext=0x{:08X}", memory::TEXT_START),
"-C",
"link-arg=--fatal-warnings",
]
.join("\x1f"),
)
.args(args)
.stderr(Stdio::piped())
.spawn()
.unwrap();
let stderr = child.stderr.take().unwrap();
let tty_file = env::var("RISC0_GUEST_LOGFILE").unwrap_or_else(|_| "/dev/tty".to_string());
let mut tty = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(tty_file)
.ok();
if let Some(tty) = &mut tty {
writeln!(
tty,
"{}: Starting build for riscv32im-risc0-zkvm-elf",
pkg.name
)
.unwrap();
}
for line in BufReader::new(stderr).lines() {
match &mut tty {
Some(tty) => writeln!(tty, "{}: {}", pkg.name, line.unwrap()).unwrap(),
None => eprintln!("{}", line.unwrap()),
}
}
let res = child.wait().expect("Guest 'cargo build' failed");
if !res.success() {
std::process::exit(res.code().unwrap());
}
}
fn detect_toolchain(name: &str) {
let result = Command::new("rustup")
.args(["toolchain", "list", "--verbose"])
.stderr(Stdio::inherit())
.output()
.unwrap();
if !result.status.success() {
eprintln!("Failed to run: 'rustup toolchain list --verbose'");
std::process::exit(result.status.code().unwrap());
}
let stdout = String::from_utf8(result.stdout).unwrap();
if stdout
.lines()
.find(|line| line.trim().starts_with(name))
.is_none()
{
eprintln!("The 'risc0' toolchain could not be found.");
eprintln!("To install the risc0 toolchain, use cargo-risczero.");
eprintln!("For example:");
eprintln!(" cargo install cargo-risczero");
eprintln!(" cargo risczero install");
std::process::exit(-1);
}
}
pub struct GuestOptions {
pub features: Vec<String>,
}
impl Default for GuestOptions {
fn default() -> Self {
GuestOptions { features: vec![] }
}
}
pub fn embed_methods_with_options(mut guest_pkg_to_options: HashMap<&str, GuestOptions>) {
let out_dir_env = env::var_os("OUT_DIR").unwrap();
let out_dir = Path::new(&out_dir_env); let guest_dir = out_dir
.parent() .unwrap()
.parent() .unwrap()
.parent() .unwrap()
.parent() .unwrap()
.join("riscv-guest");
let pkg = current_package();
let guest_packages = guest_packages(&pkg);
let methods_path = out_dir.join("methods.rs");
let mut methods_file = File::create(&methods_path).unwrap();
#[cfg(feature = "guest-list")]
let mut guest_list_entries = Vec::new();
#[cfg(feature = "guest-list")]
methods_file
.write_all(b"use risc0_build::GuestListEntry;\n")
.unwrap();
detect_toolchain(RUSTUP_TOOLCHAIN_NAME);
for guest_pkg in guest_packages {
println!("Building guest package {}.{}", pkg.name, guest_pkg.name);
let guest_options = guest_pkg_to_options
.remove(guest_pkg.name.as_str())
.unwrap_or_default();
build_guest_package(&guest_pkg, &guest_dir, guest_options.features);
for method in guest_methods(&guest_pkg, &guest_dir) {
methods_file
.write_all(method.rust_def().as_bytes())
.unwrap();
#[cfg(feature = "guest-list")]
guest_list_entries.push(method.guest_list_entry());
}
#[cfg(feature = "guest-list")]
methods_file
.write_all(
format!(
"\npub const GUEST_LIST: &[GuestListEntry] = &[{}];\n",
guest_list_entries.join(",")
)
.as_bytes(),
)
.unwrap();
}
println!("cargo:rerun-if-changed={}", methods_path.display());
}
pub fn embed_methods() {
embed_methods_with_options(HashMap::new())
}