mod aggregation;
mod build;
mod command;
mod utils;
use build::build_program_internal;
pub use aggregation::{
guest_elf_map, resolve_aggregation, ResolvedAggregation, ResolvedCircuitPaths, ResolvedProgram,
};
use clap::Parser;
pub const RUSTUP_TOOLCHAIN_NAME: &str = "zisk";
pub const ZISK_LINKER_SCRIPT: &[u8] = include_bytes!("../zisk_linker_script.ld");
pub const ZISK_VERSION_MESSAGE: &str = concat!(
env!("CARGO_PKG_VERSION"),
" [",
env!("ZISK_COMPUTE_MODE"),
"]",
" (",
env!("VERGEN_GIT_SHA"),
" ",
env!("VERGEN_BUILD_TIMESTAMP"),
")"
);
pub const ZISK_TARGET: &str = "riscv64ima-zisk-zkvm-elf";
pub const HELPER_TARGET_SUBDIR: &str = "elf";
pub(crate) const HOST_RUSTFLAGS_VARS: &[&str] =
&["CARGO_ENCODED_RUSTFLAGS", "RUSTFLAGS", "CARGO_BUILD_RUSTFLAGS"];
fn guest_target_rustflags_var() -> String {
format!("CARGO_TARGET_{}_RUSTFLAGS", ZISK_TARGET.to_uppercase().replace(['-', '.'], "_"))
}
fn all_rustflags_vars() -> impl Iterator<Item = String> {
HOST_RUSTFLAGS_VARS.iter().map(|v| v.to_string()).chain([guest_target_rustflags_var()])
}
fn env_rustflags() -> Vec<String> {
use cargo_config2::Flags;
if let Some(encoded) = std::env::var_os("CARGO_ENCODED_RUSTFLAGS") {
Flags::from_encoded(&encoded.to_string_lossy()).flags
} else if let Some(rustflags) = std::env::var_os("RUSTFLAGS") {
Flags::from_space_separated(&rustflags.to_string_lossy()).flags
} else if let Some(target_flags) = std::env::var_os(guest_target_rustflags_var()) {
Flags::from_space_separated(&target_flags.to_string_lossy()).flags
} else if let Some(build_flags) = std::env::var_os("CARGO_BUILD_RUSTFLAGS") {
Flags::from_space_separated(&build_flags.to_string_lossy()).flags
} else {
Vec::new()
}
}
#[derive(Default, Clone, Parser, Debug)]
#[command(author, about, long_about = None, version = ZISK_VERSION_MESSAGE)]
pub struct BuildArgs {
#[clap(short = 'F', long)]
pub features: Option<String>,
#[clap(long)]
all_features: bool,
#[clap(long)]
release: bool,
#[clap(long)]
no_default_features: bool,
#[clap(long, value_name = "OUTPUT_DIRECTORY")]
output_directory: Option<String>,
#[clap(long, value_name = "ELF_NAME")]
elf_name: Option<String>,
#[clap(long, value_name = "ASM")]
pub asm: Option<bool>,
#[clap(long, value_name = "HINTS")]
pub hints: Option<bool>,
#[clap(long = "package", value_name = "PACKAGE")]
pub packages: Vec<String>,
#[clap(long = "bin", value_name = "BIN")]
pub binaries: Vec<String>,
}
impl BuildArgs {
pub fn release(mut self, release: bool) -> Self {
self.release = release;
self
}
}
pub const GUEST_TARGET_FEATURES: &[&str] = &["zba", "zbc", "zbkc", "zbkx", "zicond"];
pub fn target_features_from_features(features: Option<&str>, all_features: bool) -> Vec<String> {
let requested: Vec<&str> = features
.map(|f| f.split([',', ' ']).filter(|s| !s.is_empty()).collect())
.unwrap_or_default();
let enabled: Vec<String> = GUEST_TARGET_FEATURES
.iter()
.filter(|ext| {
all_features
|| requested.contains(*ext)
|| requested.contains(&format!("{ext}_native").as_str())
})
.map(|ext| format!("+{ext}"))
.collect();
if enabled.is_empty() {
Vec::new()
} else {
vec!["-C".to_string(), format!("target-feature={}", enabled.join(","))]
}
}
pub fn guest_rustflags(
program_dir: Option<&std::path::Path>,
inherit_env_rustflags: bool,
extra_flags: &[String],
) -> anyhow::Result<(tempfile::NamedTempFile, String)> {
use anyhow::Context;
use std::io::Write;
let mut linker_script = tempfile::Builder::new()
.prefix("zisk-")
.suffix(".ld")
.tempfile()
.context("Failed to create temporary Zisk linker script")?;
linker_script
.write_all(ZISK_LINKER_SCRIPT)
.context("Failed to write Zisk linker script to temp file")?;
let ignored: Vec<std::ffi::OsString> =
all_rustflags_vars().map(std::ffi::OsString::from).collect();
let env = std::env::vars_os().filter(|(key, _)| !ignored.contains(key));
let mut options = cargo_config2::ResolveOptions::default().env(env);
let zisk_rustc = command::zisk_rustc();
if let Ok(rustc) = &zisk_rustc {
options = options.rustc(cargo_config2::PathAndArgs::new(rustc));
}
let cwd = match program_dir {
Some(dir) => dir.canonicalize().with_context(|| {
format!("Failed to canonicalize program directory {}", dir.display())
})?,
None => std::env::current_dir().context("Failed to get current directory")?,
};
let mut flags = cargo_config2::Config::load_with_options(cwd, options)
.and_then(|config| config.rustflags(ZISK_TARGET))
.map(|flags| flags.map(|f| f.flags).unwrap_or_default())
.map_err(|err| match zisk_rustc {
Err(toolchain_err) => anyhow::Error::from(err).context(toolchain_err),
Ok(_) => anyhow::Error::from(err),
})
.with_context(|| format!("Failed to resolve cargo rustflags for target {ZISK_TARGET}"))?;
flags.retain(|f| !f.is_empty());
if inherit_env_rustflags {
flags.extend(env_rustflags());
}
flags.extend(extra_flags.iter().cloned());
flags.extend([
"--cfg".to_string(),
"zisk_guest".to_string(),
"-C".to_string(),
format!("link-arg=-T{}", linker_script.path().display()),
]);
let encoded =
cargo_config2::Flags::from(flags).encode().context("Failed to encode guest rustflags")?;
Ok((linker_script, encoded))
}
pub fn apply_guest_rustflags(
command: &mut std::process::Command,
program_dir: Option<&std::path::Path>,
inherit_env_rustflags: bool,
extra_flags: &[String],
) -> anyhow::Result<tempfile::NamedTempFile> {
let (linker_script, encoded_rustflags) =
guest_rustflags(program_dir, inherit_env_rustflags, extra_flags)?;
command.env("CARGO_ENCODED_RUSTFLAGS", encoded_rustflags);
for var in all_rustflags_vars().filter(|v| v != "CARGO_ENCODED_RUSTFLAGS") {
command.env_remove(var);
}
Ok(linker_script)
}
pub fn build_program(path: &str) {
build_program_internal(path, None)
}
pub fn build_program_asm(path: &str) {
let args = BuildArgs { asm: Some(true), ..Default::default() };
build_program_internal(path, Some(args))
}
pub fn build_program_with_args(path: &str, args: BuildArgs) {
build_program_internal(path, Some(args))
}
#[cfg(test)]
mod tests {
use super::*;
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct EnvVarGuard {
saved: Vec<(String, Option<std::ffi::OsString>)>,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl EnvVarGuard {
fn hermetic(dir: &std::path::Path) -> Self {
let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let cleared: Vec<String> = all_rustflags_vars().collect();
let mut saved: Vec<(String, Option<std::ffi::OsString>)> =
cleared.iter().map(|k| (k.clone(), std::env::var_os(k))).collect();
saved.push(("CARGO_HOME".to_string(), std::env::var_os("CARGO_HOME")));
for k in &cleared {
std::env::remove_var(k);
}
let cargo_home = dir.join("cargo-home");
std::fs::create_dir_all(&cargo_home).unwrap();
std::env::set_var("CARGO_HOME", cargo_home);
Self { saved, _lock: lock }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
for (key, value) in &self.saved {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
}
#[test]
fn guest_rustflags_keeps_config_and_appends_zisk_flags() {
let dir = tempfile::tempdir().unwrap();
let _env = EnvVarGuard::hermetic(dir.path());
std::fs::create_dir(dir.path().join(".cargo")).unwrap();
std::fs::write(
dir.path().join(".cargo/config.toml"),
format!(
"[target.{ZISK_TARGET}]\nrustflags = [\"-C\", \"llvm-args=--inline-threshold=1234\"]\n"
),
)
.unwrap();
let (script, encoded) = guest_rustflags(Some(dir.path()), false, &[]).unwrap();
let flags: Vec<&str> = encoded.split('\u{1f}').collect();
let config_flag = flags
.iter()
.position(|f| *f == "llvm-args=--inline-threshold=1234")
.expect("config rustflags dropped");
let cfg_flag =
flags.iter().position(|f| *f == "zisk_guest").expect("missing --cfg zisk_guest");
let t_flag = format!("link-arg=-T{}", script.path().display());
assert!(flags.contains(&t_flag.as_str()), "missing linker-script flag");
assert!(config_flag < cfg_flag);
assert_eq!(std::fs::read(script.path()).unwrap(), ZISK_LINKER_SCRIPT);
}
#[test]
fn guest_rustflags_drops_empty_flags() {
let dir = tempfile::tempdir().unwrap();
let _env = EnvVarGuard::hermetic(dir.path());
std::fs::create_dir(dir.path().join(".cargo")).unwrap();
std::fs::write(
dir.path().join(".cargo/config.toml"),
format!("[target.{ZISK_TARGET}]\nrustflags = [\"\"]\n"),
)
.unwrap();
let (_script, encoded) = guest_rustflags(Some(dir.path()), false, &[]).unwrap();
assert!(
encoded.split('\u{1f}').all(|f| !f.is_empty()),
"empty flag element in {encoded:?}"
);
}
#[test]
fn guest_rustflags_ignores_host_env_rustflags() {
let dir = tempfile::tempdir().unwrap();
let _env = EnvVarGuard::hermetic(dir.path());
std::env::set_var("CARGO_ENCODED_RUSTFLAGS", "-C\u{1f}link-arg=-Wl,--export-dynamic");
std::env::set_var("RUSTFLAGS", "-C target-cpu=native");
std::env::set_var("CARGO_BUILD_RUSTFLAGS", "-C link-arg=--host-only-marker");
let (_script, encoded) = guest_rustflags(Some(dir.path()), false, &[]).unwrap();
assert!(
!encoded.contains("export-dynamic"),
"host CARGO_ENCODED_RUSTFLAGS leaked into guest flags: {encoded:?}"
);
assert!(
!encoded.contains("target-cpu"),
"host RUSTFLAGS leaked into guest flags: {encoded:?}"
);
assert!(
!encoded.contains("host-only-marker"),
"host CARGO_BUILD_RUSTFLAGS leaked into guest flags: {encoded:?}"
);
}
#[test]
fn guest_rustflags_inherits_env_rustflags_when_requested() {
let dir = tempfile::tempdir().unwrap();
let _env = EnvVarGuard::hermetic(dir.path());
std::env::set_var("RUSTFLAGS", "--cfg my_guest_feature");
let (_script, encoded) = guest_rustflags(Some(dir.path()), true, &[]).unwrap();
let flags: Vec<&str> = encoded.split('\u{1f}').collect();
assert!(
flags.contains(&"my_guest_feature"),
"user RUSTFLAGS dropped in inherit mode: {encoded:?}"
);
}
#[test]
fn guest_rustflags_keeps_both_config_and_env_in_inherit_mode() {
let dir = tempfile::tempdir().unwrap();
let _env = EnvVarGuard::hermetic(dir.path());
std::fs::create_dir(dir.path().join(".cargo")).unwrap();
std::fs::write(
dir.path().join(".cargo/config.toml"),
format!(
"[target.{ZISK_TARGET}]\nrustflags = [\"-C\", \"llvm-args=--inline-threshold=1234\"]\n"
),
)
.unwrap();
std::env::set_var("RUSTFLAGS", "--cfg my_guest_feature");
let (_script, encoded) = guest_rustflags(Some(dir.path()), true, &[]).unwrap();
let flags: Vec<&str> = encoded.split('\u{1f}').collect();
let config_flag = flags
.iter()
.position(|f| *f == "llvm-args=--inline-threshold=1234")
.expect("config rustflags dropped in inherit mode");
let env_flag = flags
.iter()
.position(|f| *f == "my_guest_feature")
.expect("env RUSTFLAGS dropped in inherit mode");
assert!(config_flag < env_flag, "env rustflags must follow config: {encoded:?}");
}
#[test]
fn guest_rustflags_handles_per_target_env_var() {
let dir = tempfile::tempdir().unwrap();
let _env = EnvVarGuard::hermetic(dir.path());
std::fs::create_dir(dir.path().join(".cargo")).unwrap();
std::fs::write(
dir.path().join(".cargo/config.toml"),
format!(
"[target.{ZISK_TARGET}]\nrustflags = [\"-C\", \"llvm-args=--inline-threshold=1234\"]\n"
),
)
.unwrap();
std::env::set_var(guest_target_rustflags_var(), "--cfg per_target_feature");
let (_script, encoded) = guest_rustflags(Some(dir.path()), true, &[]).unwrap();
let flags: Vec<&str> = encoded.split('\u{1f}').collect();
let config_flag = flags
.iter()
.position(|f| *f == "llvm-args=--inline-threshold=1234")
.expect("config rustflags dropped by per-target env var");
let env_flag = flags
.iter()
.position(|f| *f == "per_target_feature")
.expect("per-target env rustflags dropped in inherit mode");
assert!(config_flag < env_flag, "per-target env rustflags must follow config: {encoded:?}");
}
#[test]
fn guest_rustflags_env_sources_are_mutually_exclusive() {
let dir = tempfile::tempdir().unwrap();
let _env = EnvVarGuard::hermetic(dir.path());
std::env::set_var("RUSTFLAGS", "--cfg global_source");
std::env::set_var(guest_target_rustflags_var(), "--cfg per_target_source");
let (_script, encoded) = guest_rustflags(Some(dir.path()), true, &[]).unwrap();
let flags: Vec<&str> = encoded.split('\u{1f}').collect();
assert!(flags.contains(&"global_source"), "global RUSTFLAGS dropped: {encoded:?}");
assert!(
!flags.contains(&"per_target_source"),
"per-target var must be dropped when a global source is set: {encoded:?}"
);
}
#[test]
fn guest_rustflags_applies_sole_cargo_build_rustflags() {
let dir = tempfile::tempdir().unwrap();
let _env = EnvVarGuard::hermetic(dir.path());
std::env::set_var("CARGO_BUILD_RUSTFLAGS", "--cfg build_source");
let (_script, encoded) = guest_rustflags(Some(dir.path()), true, &[]).unwrap();
let flags: Vec<&str> = encoded.split('\u{1f}').collect();
assert!(
flags.contains(&"build_source"),
"sole CARGO_BUILD_RUSTFLAGS dropped in inherit mode: {encoded:?}"
);
}
#[test]
fn target_features_selects_only_requested_extensions() {
assert!(target_features_from_features(None, false).is_empty());
assert!(target_features_from_features(Some("c_extension"), false).is_empty());
let flags = target_features_from_features(Some("zbkx, zba c_extension"), false);
assert_eq!(flags, vec!["-C".to_string(), "target-feature=+zba,+zbkx".to_string()]);
}
#[test]
fn target_features_all_features_enables_every_extension() {
let flags = target_features_from_features(None, true);
let expected: String =
GUEST_TARGET_FEATURES.iter().map(|e| format!("+{e}")).collect::<Vec<_>>().join(",");
assert_eq!(flags, vec!["-C".to_string(), format!("target-feature={expected}")]);
}
}