#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
use std::{
env, fs,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
};
use cargo_metadata::{MetadataCommand, Package};
use openvm_platform::memory;
pub use self::config::GuestOptions;
mod config;
const RUSTUP_TOOLCHAIN_NAME: &str = "nightly-2024-10-30";
const BUILD_LOCKED_ENV: &str = "OPENVM_BUILD_LOCKED";
const BUILD_DEBUG_ENV: &str = "OPENVM_BUILD_DEBUG";
const SKIP_BUILD_ENV: &str = "OPENVM_SKIP_BUILD";
const GUEST_LOGFILE_ENV: &str = "OPENVM_GUEST_LOGFILE";
pub fn get_package(manifest_dir: impl AsRef<Path>) -> Package {
let manifest_path = fs::canonicalize(manifest_dir.as_ref().join("Cargo.toml")).unwrap();
let manifest_meta = MetadataCommand::new()
.manifest_path(&manifest_path)
.no_deps()
.exec()
.expect("cargo metadata command failed");
let mut matching: Vec<Package> = manifest_meta
.packages
.into_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()
}
pub fn get_target_dir(manifest_path: impl AsRef<Path>) -> PathBuf {
MetadataCommand::new()
.manifest_path(manifest_path.as_ref())
.no_deps()
.exec()
.expect("cargo metadata command failed")
.target_directory
.into()
}
pub fn get_dir_with_profile(
target_dir: impl AsRef<Path>,
profile: &str,
examples: bool,
) -> PathBuf {
let res = target_dir
.as_ref()
.join("riscv32im-risc0-zkvm-elf")
.join(profile);
if examples {
res.join("examples")
} else {
res
}
}
pub fn current_package() -> Package {
get_package(env::var("CARGO_MANIFEST_DIR").unwrap())
}
pub fn is_debug() -> bool {
get_env_var(BUILD_DEBUG_ENV) == "1"
}
pub fn is_skip_build() -> bool {
!get_env_var(SKIP_BUILD_ENV).is_empty()
}
fn get_env_var(name: &str) -> String {
println!("cargo:rerun-if-env-changed={name}");
env::var(name).unwrap_or_default()
}
pub fn guest_methods(
pkg: &Package,
target_dir: impl AsRef<Path>,
guest_features: &[String],
) -> Vec<PathBuf> {
let profile = if is_debug() { "debug" } else { "release" };
pkg.targets
.iter()
.filter(|target| {
target
.kind
.iter()
.any(|kind| kind == "bin" || kind == "example")
})
.filter(|target| {
target
.required_features
.iter()
.all(|required_feature| guest_features.contains(required_feature))
})
.map(|target| {
target_dir
.as_ref()
.join("riscv32im-risc0-zkvm-elf")
.join(profile)
.join(&target.name)
.to_path_buf()
})
.collect()
}
fn sanitized_cmd(tool: &str) -> Command {
let mut cmd = Command::new(tool);
for (key, _val) in env::vars().filter(|x| x.0.starts_with("CARGO")) {
cmd.env_remove(key);
}
cmd.env_remove("RUSTUP_TOOLCHAIN");
cmd
}
pub fn cargo_command(subcmd: &str, rust_flags: &[&str]) -> Command {
let toolchain = format!("+{RUSTUP_TOOLCHAIN_NAME}");
let rustc = sanitized_cmd("rustup")
.args([&toolchain, "which", "rustc"])
.output()
.expect("rustup failed to find nightly toolchain")
.stdout;
let rustc = String::from_utf8(rustc).unwrap();
let rustc = rustc.trim();
println!("Using rustc: {rustc}");
let mut cmd = sanitized_cmd("cargo");
let mut args = vec![&toolchain, subcmd, "--target", "riscv32im-risc0-zkvm-elf"];
if std::env::var(BUILD_LOCKED_ENV).is_ok() {
args.push("--locked");
}
args.extend_from_slice(&[
"-Z",
"build-std=alloc,core,proc_macro,panic_abort,std",
"-Z",
"build-std-features=compiler-builtins-mem",
]);
println!("Building guest package: cargo {}", args.join(" "));
let encoded_rust_flags = encode_rust_flags(rust_flags);
cmd.env("RUSTC", rustc)
.env("CARGO_ENCODED_RUSTFLAGS", encoded_rust_flags)
.args(args);
cmd
}
pub(crate) fn encode_rust_flags(rustc_flags: &[&str]) -> String {
[
rustc_flags,
&[
"-C",
"passes=lower-atomic",
"-C",
&format!("link-arg=-Ttext=0x{:08X}", memory::TEXT_START),
"-C",
"link-arg=--fatal-warnings",
"-C",
"panic=abort",
],
]
.concat()
.join("\x1f")
}
fn tty_println(msg: &str) {
let tty_file = env::var(GUEST_LOGFILE_ENV).unwrap_or_else(|_| "/dev/tty".to_string());
let mut tty = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(tty_file)
.ok();
if let Some(tty) = &mut tty {
writeln!(tty, "{msg}").unwrap();
} else {
eprintln!("{msg}");
}
}
pub fn build_guest_package(
pkg: &Package,
guest_opts: &GuestOptions,
runtime_lib: Option<&str>,
target_filter: &Option<TargetFilter>,
) -> Result<PathBuf, Option<i32>> {
if is_skip_build() {
return Err(None);
}
let target_dir = guest_opts
.target_dir
.clone()
.unwrap_or_else(|| get_target_dir(pkg.manifest_path.clone()));
fs::create_dir_all(&target_dir).unwrap();
let runtime_rust_flags = runtime_lib
.map(|lib| vec![String::from("-C"), format!("link_arg={}", lib)])
.unwrap_or_default();
let rust_flags: Vec<_> = [
runtime_rust_flags
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>(),
guest_opts.rustc_flags.iter().map(|s| s.as_str()).collect(),
]
.concat();
let mut cmd = cargo_command("build", &rust_flags);
let features_str = guest_opts.features.join(",");
if !features_str.is_empty() {
cmd.args(["--features", &features_str]);
}
cmd.args([
"--manifest-path",
pkg.manifest_path.as_str(),
"--target-dir",
target_dir.to_str().unwrap(),
]);
if let Some(target_filter) = target_filter {
cmd.args([
format!("--{}", target_filter.kind).as_str(),
target_filter.name.as_str(),
]);
}
let profile = if let Some(profile) = &guest_opts.profile {
profile
} else if is_debug() {
"dev"
} else {
"release"
};
cmd.args(["--profile", profile]);
cmd.args(&guest_opts.options);
let command_string = format!(
"{} {}",
cmd.get_program().to_string_lossy(),
cmd.get_args()
.map(|arg| arg.to_string_lossy())
.collect::<Vec<_>>()
.join(" ")
);
tty_println(&format!("cargo command: {command_string}"));
let mut child = cmd
.stderr(Stdio::piped())
.env("CARGO_TERM_COLOR", "always")
.spawn()
.expect("cargo build failed");
let stderr = child.stderr.take().unwrap();
tty_println(&format!(
"{}: Starting build for riscv32im-risc0-zkvm-elf",
pkg.name
));
for line in BufReader::new(stderr).lines() {
tty_println(&format!("{}: {}", pkg.name, line.unwrap()));
}
let res = child.wait().expect("Guest 'cargo build' failed");
if !res.success() {
Err(res.code())
} else {
Ok(get_dir_with_profile(
&target_dir,
profile,
target_filter
.as_ref()
.map(|t| t.kind == "example")
.unwrap_or(false),
))
}
}
#[derive(Default)]
pub struct TargetFilter {
pub name: String,
pub kind: String,
}
pub fn find_unique_executable<P: AsRef<Path>, Q: AsRef<Path>>(
pkg_dir: P,
target_dir: Q,
target_filter: &Option<TargetFilter>,
) -> eyre::Result<PathBuf> {
let pkg = get_package(pkg_dir.as_ref());
let elf_paths = pkg
.targets
.into_iter()
.filter(move |target| {
if let Some(target_filter) = target_filter {
return target.kind.iter().any(|k| k == &target_filter.kind)
&& target.name == target_filter.name;
}
true
})
.collect::<Vec<_>>();
if elf_paths.len() != 1 {
Err(eyre::eyre!(
"Expected 1 target, got {}: {:#?}",
elf_paths.len(),
elf_paths
))
} else {
Ok(target_dir.as_ref().join(&elf_paths[0].name))
}
}
pub 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().any(|line| line.trim().starts_with(name)) {
eprintln!("The '{name}' toolchain could not be found.");
std::process::exit(-1);
}
}