1#![doc = include_str!("../README.md")]
4#![deny(missing_docs)]
5#![deny(rustdoc::broken_intra_doc_links)]
6#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
7
8use std::{
9 env, fs,
10 io::{BufRead, BufReader, Write},
11 path::{Path, PathBuf},
12 process::{Command, Stdio},
13};
14
15use cargo_metadata::{Metadata, MetadataCommand, Package};
16use openvm_platform::memory;
17
18pub use self::config::GuestOptions;
19
20mod config;
21
22pub const RUSTC_TARGET: &str = "riscv32im-risc0-zkvm-elf";
24pub const DEFAULT_RUSTUP_TOOLCHAIN_NAME: &str = "nightly-2025-02-14";
26
27pub fn get_rustup_toolchain_name() -> String {
29 env::var("OPENVM_RUST_TOOLCHAIN").unwrap_or_else(|_| DEFAULT_RUSTUP_TOOLCHAIN_NAME.to_string())
30}
31const BUILD_LOCKED_ENV: &str = "OPENVM_BUILD_LOCKED";
32const SKIP_BUILD_ENV: &str = "OPENVM_SKIP_BUILD";
33const GUEST_LOGFILE_ENV: &str = "OPENVM_GUEST_LOGFILE";
34const ALLOWED_CARGO_ENVS: &[&str] = &["CARGO_HOME"];
35
36pub fn get_package(manifest_dir: impl AsRef<Path>) -> Package {
39 let manifest_path = manifest_dir
40 .as_ref()
41 .join("Cargo.toml")
42 .canonicalize()
43 .unwrap();
44 let manifest_meta = get_metadata(&manifest_path);
45 let matching = find_matching_packages(&manifest_meta, &manifest_path);
46
47 if matching.is_empty() {
48 eprintln!(
49 "ERROR: No package found in {}",
50 manifest_dir.as_ref().display()
51 );
52 std::process::exit(-1);
53 }
54 if matching.len() > 1 {
55 eprintln!(
56 "ERROR: Multiple packages found in {}",
57 manifest_dir.as_ref().display()
58 );
59 std::process::exit(-1);
60 }
61 matching.into_iter().next().unwrap()
62}
63
64pub fn get_workspace_packages(manifest_dir: impl AsRef<Path>) -> Vec<Package> {
66 let manifest_path = manifest_dir
67 .as_ref()
68 .join("Cargo.toml")
69 .canonicalize()
70 .unwrap();
71 let manifest_meta = get_metadata(&manifest_path);
72 get_workspace_member_packages(manifest_meta)
73}
74
75pub fn get_in_scope_packages(manifest_dir: impl AsRef<Path>) -> Vec<Package> {
78 let manifest_path = manifest_dir
79 .as_ref()
80 .join("Cargo.toml")
81 .canonicalize()
82 .unwrap();
83 let manifest_meta = get_metadata(&manifest_path);
84
85 let matching = find_matching_packages(&manifest_meta, &manifest_path);
87
88 if !matching.is_empty() {
90 return matching;
91 }
92
93 get_workspace_member_packages(manifest_meta)
95}
96
97fn get_metadata(manifest_path: &Path) -> Metadata {
99 MetadataCommand::new()
100 .manifest_path(manifest_path)
101 .no_deps()
102 .exec()
103 .unwrap_or_else(|e| {
104 panic!(
105 "cargo metadata command failed for manifest path: {}: {e:?}",
106 manifest_path.display()
107 )
108 })
109}
110
111fn get_workspace_member_packages(manifest_meta: Metadata) -> Vec<Package> {
113 manifest_meta
114 .packages
115 .into_iter()
116 .filter(|pkg| manifest_meta.workspace_members.contains(&pkg.id))
117 .collect()
118}
119
120fn find_matching_packages(manifest_meta: &Metadata, manifest_path: &Path) -> Vec<Package> {
122 manifest_meta
123 .packages
124 .iter()
125 .filter(|pkg| {
126 let std_path: &Path = pkg.manifest_path.as_ref();
127 std_path == manifest_path
128 })
129 .cloned()
130 .collect()
131}
132
133pub fn get_target_dir(manifest_path: impl AsRef<Path>) -> PathBuf {
136 MetadataCommand::new()
137 .manifest_path(manifest_path.as_ref())
138 .no_deps()
139 .exec()
140 .expect("cargo metadata command failed")
141 .target_directory
142 .into()
143}
144
145pub fn get_workspace_root(manifest_path: impl AsRef<Path>) -> PathBuf {
148 MetadataCommand::new()
149 .manifest_path(manifest_path.as_ref())
150 .no_deps()
151 .exec()
152 .expect("cargo metadata command failed")
153 .workspace_root
154 .into()
155}
156
157pub fn get_dir_with_profile(
159 target_dir: impl AsRef<Path>,
160 profile: &str,
161 examples: bool,
162) -> PathBuf {
163 let mut res = target_dir.as_ref().join(RUSTC_TARGET).to_path_buf();
164 if profile == "dev" || profile == "test" {
165 res.push("debug");
166 } else if profile == "bench" {
167 res.push("release");
168 } else {
169 res.push(profile);
170 }
171 if examples {
172 res.join("examples")
173 } else {
174 res
175 }
176}
177
178pub fn current_package() -> Package {
180 get_package(env::var("CARGO_MANIFEST_DIR").unwrap())
181}
182
183pub fn is_skip_build() -> bool {
186 !get_env_var(SKIP_BUILD_ENV).is_empty()
187}
188
189fn get_env_var(name: &str) -> String {
190 println!("cargo:rerun-if-env-changed={name}");
191 env::var(name).unwrap_or_default()
192}
193
194pub fn guest_methods<S: AsRef<str>>(
196 pkg: &Package,
197 target_dir: impl AsRef<Path>,
198 guest_features: &[String],
199 profile: &Option<S>,
200) -> Vec<PathBuf> {
201 let profile = profile.as_ref().map(|s| s.as_ref()).unwrap_or("release");
202 pkg.targets
203 .iter()
204 .filter(|target| {
205 target
206 .kind
207 .iter()
208 .any(|kind| kind == "bin" || kind == "example")
209 })
210 .filter(|target| {
211 target
212 .required_features
213 .iter()
214 .all(|required_feature| guest_features.contains(required_feature))
215 })
216 .flat_map(|target| {
217 let path_prefix = target_dir.as_ref().join(RUSTC_TARGET).join(profile);
218 target
219 .kind
220 .iter()
221 .map(|target_kind| {
222 let mut path = path_prefix.clone();
223 if target_kind == "example" {
224 path.push(target_kind);
225 }
226 path.join(&target.name).to_path_buf()
227 })
228 .collect::<Vec<_>>()
229 })
230 .collect()
231}
232
233fn sanitized_cmd(tool: &str) -> Command {
236 let mut cmd = Command::new(tool);
237 for (key, _val) in env::vars()
238 .filter(|x| x.0.starts_with("CARGO") && !ALLOWED_CARGO_ENVS.contains(&x.0.as_str()))
239 {
240 cmd.env_remove(key);
241 }
242 cmd.env_remove("RUSTUP_TOOLCHAIN");
243 cmd
244}
245
246pub fn cargo_command(subcmd: &str, rust_flags: &[&str]) -> Command {
249 let toolchain = format!("+{}", get_rustup_toolchain_name());
250
251 let rustc = sanitized_cmd("rustup")
252 .args([&toolchain, "which", "rustc"])
253 .output()
254 .expect("rustup failed to find nightly toolchain")
255 .stdout;
256
257 let rustc = String::from_utf8(rustc).unwrap();
258 let rustc = rustc.trim();
259 println!("Using rustc: {rustc}");
260
261 let mut cmd = sanitized_cmd("cargo");
262 let mut args = vec![&toolchain, subcmd, "--target", RUSTC_TARGET];
263
264 if std::env::var(BUILD_LOCKED_ENV).is_ok() {
265 args.push("--locked");
266 }
267
268 args.extend_from_slice(&[
272 "-Z",
273 "build-std=alloc,core,proc_macro,panic_abort,std",
274 "-Z",
275 "build-std-features=compiler-builtins-mem",
276 ]);
277 println!("Building guest package: cargo {}", args.join(" "));
281
282 let encoded_rust_flags = encode_rust_flags(rust_flags);
283
284 cmd.env("RUSTC", rustc)
285 .env("CARGO_ENCODED_RUSTFLAGS", encoded_rust_flags)
286 .args(args);
287 cmd
288}
289
290pub(crate) fn encode_rust_flags(rustc_flags: &[&str]) -> String {
292 [
293 rustc_flags,
295 &[
296 "-C",
298 "passes=lower-atomic",
299 "-C",
305 &format!("link-arg=-Ttext=0x{:08X}", memory::TEXT_START),
306 "-C",
309 "link-arg=--fatal-warnings",
310 "-C",
311 "panic=abort",
312 "--cfg",
314 "getrandom_backend=\"custom\"",
315 ],
316 ]
317 .concat()
318 .join("\x1f")
319}
320
321fn tty_println(msg: &str) {
326 let tty_file = env::var(GUEST_LOGFILE_ENV).unwrap_or_else(|_| "/dev/tty".to_string());
327
328 let mut tty = fs::OpenOptions::new()
329 .read(true)
330 .write(true)
331 .create(true)
332 .truncate(false)
333 .open(tty_file)
334 .ok();
335
336 if let Some(tty) = &mut tty {
337 writeln!(tty, "{msg}").unwrap();
338 } else {
339 eprintln!("{msg}");
340 }
341}
342
343pub fn build_guest_package(
346 pkg: &Package,
347 guest_opts: &GuestOptions,
348 runtime_lib: Option<&str>,
349 target_filter: &Option<TargetFilter>,
350) -> Result<PathBuf, Option<i32>> {
351 let mut new_opts = guest_opts.clone();
352
353 if new_opts.target_dir.is_none() {
354 new_opts.target_dir = Some(get_target_dir(&pkg.manifest_path));
355 }
356
357 new_opts.options.extend(vec![
358 "--manifest-path".into(),
359 pkg.manifest_path.to_string(),
360 ]);
361
362 if let Some(runtime_lib) = runtime_lib {
363 new_opts.rustc_flags.extend(vec![
364 String::from("-C"),
365 format!("link_arg={}", runtime_lib),
366 ]);
367 }
368
369 let mut example = false;
370 if let Some(target_filter) = target_filter {
371 new_opts.options.extend(vec![
372 format!("--{}", target_filter.kind),
373 target_filter.name.clone(),
374 ]);
375 example = target_filter.kind == "example";
376 }
377
378 let res = build_generic(&new_opts);
379 res.map(|path| if example { path.join("examples") } else { path })
380}
381
382pub fn build_generic(guest_opts: &GuestOptions) -> Result<PathBuf, Option<i32>> {
384 if is_skip_build() || guest_opts.target_dir.is_none() {
385 eprintln!("Skipping build");
386 return Err(None);
387 }
388
389 if let Err(code) = ensure_toolchain_installed(&get_rustup_toolchain_name(), &["rust-src"]) {
392 eprintln!("rustup toolchain commands failed. Please ensure rustup is installed (https://www.rust-lang.org/tools/install)");
393 return Err(Some(code));
394 }
395
396 let target_dir = guest_opts.target_dir.as_ref().unwrap();
397 fs::create_dir_all(target_dir).unwrap();
398 let rust_flags: Vec<_> = guest_opts.rustc_flags.iter().map(|s| s.as_str()).collect();
399
400 let mut cmd = cargo_command("build", &rust_flags);
401
402 if !guest_opts.features.is_empty() {
403 cmd.args(["--features", guest_opts.features.join(",").as_str()]);
404 }
405 cmd.args(["--target-dir", target_dir.to_str().unwrap()]);
406
407 let profile = if let Some(profile) = &guest_opts.profile {
408 profile
409 } else {
410 "release"
411 };
412 cmd.args(["--profile", profile]);
413
414 cmd.args(&guest_opts.options);
415
416 let command_string = format!(
417 "{} {}",
418 cmd.get_program().to_string_lossy(),
419 cmd.get_args()
420 .map(|arg| arg.to_string_lossy())
421 .collect::<Vec<_>>()
422 .join(" ")
423 );
424 tty_println(&format!("cargo command: {command_string}"));
425
426 let mut child = cmd
427 .stderr(Stdio::piped())
428 .env("CARGO_TERM_COLOR", "always")
429 .spawn()
430 .expect("cargo build failed");
431 let stderr = child.stderr.take().unwrap();
432
433 tty_println(&format!("openvm build: Starting build for {RUSTC_TARGET}"));
434
435 for line in BufReader::new(stderr).lines() {
436 tty_println(&format!("openvm build: {}", line.unwrap()));
437 }
438
439 let res = child.wait().expect("Guest 'cargo build' failed");
440 if !res.success() {
441 Err(res.code())
442 } else {
443 Ok(get_dir_with_profile(target_dir, profile, false))
444 }
445}
446
447#[derive(Default)]
449pub struct TargetFilter {
450 pub name: String,
452 pub kind: String,
454}
455
456pub fn find_unique_executable<P: AsRef<Path>, Q: AsRef<Path>>(
459 pkg_dir: P,
460 target_dir: Q,
461 target_filter: &Option<TargetFilter>,
462) -> eyre::Result<PathBuf> {
463 let pkg = get_package(pkg_dir.as_ref());
464 let elf_paths = pkg
465 .targets
466 .into_iter()
467 .filter(move |target| {
468 if target.is_custom_build() || target.is_lib() {
470 return false;
471 }
472 if let Some(target_filter) = target_filter {
473 return target.kind.iter().any(|k| k == &target_filter.kind)
474 && target.name == target_filter.name;
475 }
476 true
477 })
478 .collect::<Vec<_>>();
479 if elf_paths.len() != 1 {
480 Err(eyre::eyre!(
481 "Expected 1 target, got {}: {:#?}",
482 elf_paths.len(),
483 elf_paths
484 ))
485 } else {
486 Ok(target_dir.as_ref().join(&elf_paths[0].name))
487 }
488}
489
490pub fn detect_toolchain(name: &str) {
492 let result = Command::new("rustup")
493 .args(["toolchain", "list", "--verbose"])
494 .stderr(Stdio::inherit())
495 .output()
496 .unwrap();
497 if !result.status.success() {
498 eprintln!("Failed to run: 'rustup toolchain list --verbose'");
499 std::process::exit(result.status.code().unwrap());
500 }
501
502 let stdout = String::from_utf8(result.stdout).unwrap();
503 if !stdout.lines().any(|line| line.trim().starts_with(name)) {
504 eprintln!("The '{name}' toolchain could not be found.");
505 std::process::exit(-1);
506 }
507}
508
509fn ensure_toolchain_installed(toolchain: &str, components: &[&str]) -> Result<(), i32> {
511 let output = Command::new("rustup")
513 .args(["toolchain", "list"])
514 .output()
515 .map_err(|e| {
516 tty_println(&format!("Failed to check toolchains: {}", e));
517 e.raw_os_error().unwrap_or(1)
518 })?;
519
520 let toolchain_installed = String::from_utf8_lossy(&output.stdout)
521 .lines()
522 .any(|line| line.trim().starts_with(toolchain));
523
524 if !toolchain_installed {
526 tty_println(&format!("Installing required toolchain: {}", toolchain));
527 let status = Command::new("rustup")
528 .args(["toolchain", "install", toolchain])
529 .status()
530 .map_err(|e| {
531 tty_println(&format!("Failed to install toolchain: {}", e));
532 e.raw_os_error().unwrap_or(1)
533 })?;
534
535 if !status.success() {
536 tty_println(&format!("Failed to install toolchain {}", toolchain));
537 return Err(status.code().unwrap_or(1));
538 }
539 }
540
541 for component in components {
543 let output = Command::new("rustup")
544 .args(["component", "list", "--toolchain", toolchain])
545 .output()
546 .map_err(|e| {
547 tty_println(&format!("Failed to check components: {}", e));
548 e.raw_os_error().unwrap_or(1)
549 })?;
550
551 let is_installed = String::from_utf8_lossy(&output.stdout)
552 .lines()
553 .any(|line| line.contains(component) && line.contains("(installed)"));
554
555 if !is_installed {
556 tty_println(&format!(
557 "Installing component {} for toolchain {}",
558 component, toolchain
559 ));
560 let status = Command::new("rustup")
561 .args(["component", "add", component, "--toolchain", toolchain])
562 .status()
563 .map_err(|e| {
564 tty_println(&format!("Failed to install component: {}", e));
565 e.raw_os_error().unwrap_or(1)
566 })?;
567
568 if !status.success() {
569 tty_println(&format!(
570 "Failed to install component {} for toolchain {}",
571 component, toolchain
572 ));
573 return Err(status.code().unwrap_or(1));
574 }
575 }
576 }
577
578 Ok(())
579}