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::{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";
24const RUSTUP_TOOLCHAIN_NAME: &str = "nightly-2025-02-14";
25const BUILD_LOCKED_ENV: &str = "OPENVM_BUILD_LOCKED";
26const SKIP_BUILD_ENV: &str = "OPENVM_SKIP_BUILD";
27const GUEST_LOGFILE_ENV: &str = "OPENVM_GUEST_LOGFILE";
28
29pub fn get_package(manifest_dir: impl AsRef<Path>) -> Package {
32 let manifest_path = fs::canonicalize(manifest_dir.as_ref().join("Cargo.toml")).unwrap();
33 let manifest_meta = MetadataCommand::new()
34 .manifest_path(&manifest_path)
35 .no_deps()
36 .exec()
37 .unwrap_or_else(|e| {
38 panic!(
39 "cargo metadata command failed for manifest path: {}: {e:?}",
40 manifest_path.display()
41 )
42 });
43 let mut matching: Vec<Package> = manifest_meta
44 .packages
45 .into_iter()
46 .filter(|pkg| {
47 let std_path: &Path = pkg.manifest_path.as_ref();
48 std_path == manifest_path
49 })
50 .collect();
51 if matching.is_empty() {
52 eprintln!(
53 "ERROR: No package found in {}",
54 manifest_dir.as_ref().display()
55 );
56 std::process::exit(-1);
57 }
58 if matching.len() > 1 {
59 eprintln!(
60 "ERROR: Multiple packages found in {}",
61 manifest_dir.as_ref().display()
62 );
63 std::process::exit(-1);
64 }
65 matching.pop().unwrap()
66}
67
68pub fn get_target_dir(manifest_path: impl AsRef<Path>) -> PathBuf {
71 MetadataCommand::new()
72 .manifest_path(manifest_path.as_ref())
73 .no_deps()
74 .exec()
75 .expect("cargo metadata command failed")
76 .target_directory
77 .into()
78}
79
80pub fn get_dir_with_profile(
82 target_dir: impl AsRef<Path>,
83 profile: &str,
84 examples: bool,
85) -> PathBuf {
86 let mut res = target_dir.as_ref().join(RUSTC_TARGET).to_path_buf();
87 if profile == "dev" || profile == "test" {
88 res.push("debug");
89 } else if profile == "bench" {
90 res.push("release");
91 } else {
92 res.push(profile);
93 }
94 if examples {
95 res.join("examples")
96 } else {
97 res
98 }
99}
100
101pub fn current_package() -> Package {
103 get_package(env::var("CARGO_MANIFEST_DIR").unwrap())
104}
105
106pub fn is_skip_build() -> bool {
108 !get_env_var(SKIP_BUILD_ENV).is_empty()
109}
110
111fn get_env_var(name: &str) -> String {
112 println!("cargo:rerun-if-env-changed={name}");
113 env::var(name).unwrap_or_default()
114}
115
116pub fn guest_methods<S: AsRef<str>>(
118 pkg: &Package,
119 target_dir: impl AsRef<Path>,
120 guest_features: &[String],
121 profile: &Option<S>,
122) -> Vec<PathBuf> {
123 let profile = profile.as_ref().map(|s| s.as_ref()).unwrap_or("release");
124 pkg.targets
125 .iter()
126 .filter(|target| {
127 target
128 .kind
129 .iter()
130 .any(|kind| kind == "bin" || kind == "example")
131 })
132 .filter(|target| {
133 target
134 .required_features
135 .iter()
136 .all(|required_feature| guest_features.contains(required_feature))
137 })
138 .flat_map(|target| {
139 let path_prefix = target_dir.as_ref().join(RUSTC_TARGET).join(profile);
140 target
141 .kind
142 .iter()
143 .map(|target_kind| {
144 let mut path = path_prefix.clone();
145 if target_kind == "example" {
146 path.push(target_kind);
147 }
148 path.join(&target.name).to_path_buf()
149 })
150 .collect::<Vec<_>>()
151 })
152 .collect()
153}
154
155fn sanitized_cmd(tool: &str) -> Command {
158 let mut cmd = Command::new(tool);
159 for (key, _val) in env::vars().filter(|x| x.0.starts_with("CARGO")) {
160 cmd.env_remove(key);
161 }
162 cmd.env_remove("RUSTUP_TOOLCHAIN");
163 cmd
164}
165
166pub fn cargo_command(subcmd: &str, rust_flags: &[&str]) -> Command {
169 let toolchain = format!("+{RUSTUP_TOOLCHAIN_NAME}");
170
171 let rustc = sanitized_cmd("rustup")
172 .args([&toolchain, "which", "rustc"])
173 .output()
174 .expect("rustup failed to find nightly toolchain")
175 .stdout;
176
177 let rustc = String::from_utf8(rustc).unwrap();
178 let rustc = rustc.trim();
179 println!("Using rustc: {rustc}");
180
181 let mut cmd = sanitized_cmd("cargo");
182 let mut args = vec![&toolchain, subcmd, "--target", RUSTC_TARGET];
183
184 if std::env::var(BUILD_LOCKED_ENV).is_ok() {
185 args.push("--locked");
186 }
187
188 args.extend_from_slice(&[
192 "-Z",
193 "build-std=alloc,core,proc_macro,panic_abort,std",
194 "-Z",
195 "build-std-features=compiler-builtins-mem",
196 ]);
197 println!("Building guest package: cargo {}", args.join(" "));
201
202 let encoded_rust_flags = encode_rust_flags(rust_flags);
203
204 cmd.env("RUSTC", rustc)
205 .env("CARGO_ENCODED_RUSTFLAGS", encoded_rust_flags)
206 .args(args);
207 cmd
208}
209
210pub(crate) fn encode_rust_flags(rustc_flags: &[&str]) -> String {
212 [
213 rustc_flags,
215 &[
216 "-C",
218 "passes=lower-atomic",
219 "-C",
225 &format!("link-arg=-Ttext=0x{:08X}", memory::TEXT_START),
226 "-C",
229 "link-arg=--fatal-warnings",
230 "-C",
231 "panic=abort",
232 ],
233 ]
234 .concat()
235 .join("\x1f")
236}
237
238fn tty_println(msg: &str) {
243 let tty_file = env::var(GUEST_LOGFILE_ENV).unwrap_or_else(|_| "/dev/tty".to_string());
244
245 let mut tty = fs::OpenOptions::new()
246 .read(true)
247 .write(true)
248 .create(true)
249 .truncate(false)
250 .open(tty_file)
251 .ok();
252
253 if let Some(tty) = &mut tty {
254 writeln!(tty, "{msg}").unwrap();
255 } else {
256 eprintln!("{msg}");
257 }
258}
259
260pub fn build_guest_package(
263 pkg: &Package,
264 guest_opts: &GuestOptions,
265 runtime_lib: Option<&str>,
266 target_filter: &Option<TargetFilter>,
267) -> Result<PathBuf, Option<i32>> {
268 if is_skip_build() {
269 return Err(None);
270 }
271
272 if let Err(code) = ensure_toolchain_installed(RUSTUP_TOOLCHAIN_NAME, &["rust-src"]) {
275 eprintln!("rustup toolchain commands failed. Please ensure rustup is installed (https://www.rust-lang.org/tools/install)");
276 return Err(Some(code));
277 }
278
279 let target_dir = guest_opts
280 .target_dir
281 .clone()
282 .unwrap_or_else(|| get_target_dir(pkg.manifest_path.clone()));
283
284 fs::create_dir_all(&target_dir).unwrap();
285
286 let runtime_rust_flags = runtime_lib
287 .map(|lib| vec![String::from("-C"), format!("link_arg={}", lib)])
288 .unwrap_or_default();
289 let rust_flags: Vec<_> = [
290 runtime_rust_flags
291 .iter()
292 .map(|s| s.as_str())
293 .collect::<Vec<_>>(),
294 guest_opts.rustc_flags.iter().map(|s| s.as_str()).collect(),
295 ]
296 .concat();
297
298 let mut cmd = cargo_command("build", &rust_flags);
299
300 let features_str = guest_opts.features.join(",");
301 if !features_str.is_empty() {
302 cmd.args(["--features", &features_str]);
303 }
304
305 cmd.args([
306 "--manifest-path",
307 pkg.manifest_path.as_str(),
308 "--target-dir",
309 target_dir.to_str().unwrap(),
310 ]);
311
312 if let Some(target_filter) = target_filter {
313 cmd.args([
314 format!("--{}", target_filter.kind).as_str(),
315 target_filter.name.as_str(),
316 ]);
317 }
318
319 let profile = if let Some(profile) = &guest_opts.profile {
320 profile
321 } else {
322 "release"
323 };
324 cmd.args(["--profile", profile]);
325
326 cmd.args(&guest_opts.options);
327
328 let command_string = format!(
329 "{} {}",
330 cmd.get_program().to_string_lossy(),
331 cmd.get_args()
332 .map(|arg| arg.to_string_lossy())
333 .collect::<Vec<_>>()
334 .join(" ")
335 );
336 tty_println(&format!("cargo command: {command_string}"));
337
338 let mut child = cmd
339 .stderr(Stdio::piped())
340 .env("CARGO_TERM_COLOR", "always")
341 .spawn()
342 .expect("cargo build failed");
343 let stderr = child.stderr.take().unwrap();
344
345 tty_println(&format!("{}: Starting build for {RUSTC_TARGET}", pkg.name));
346
347 for line in BufReader::new(stderr).lines() {
348 tty_println(&format!("{}: {}", pkg.name, line.unwrap()));
349 }
350
351 let res = child.wait().expect("Guest 'cargo build' failed");
352 if !res.success() {
353 Err(res.code())
354 } else {
355 Ok(get_dir_with_profile(
356 &target_dir,
357 profile,
358 target_filter
359 .as_ref()
360 .map(|t| t.kind == "example")
361 .unwrap_or(false),
362 ))
363 }
364}
365
366#[derive(Default)]
368pub struct TargetFilter {
369 pub name: String,
371 pub kind: String,
373}
374
375pub fn find_unique_executable<P: AsRef<Path>, Q: AsRef<Path>>(
378 pkg_dir: P,
379 target_dir: Q,
380 target_filter: &Option<TargetFilter>,
381) -> eyre::Result<PathBuf> {
382 let pkg = get_package(pkg_dir.as_ref());
383 let elf_paths = pkg
384 .targets
385 .into_iter()
386 .filter(move |target| {
387 if target.is_custom_build() || target.is_lib() {
389 return false;
390 }
391 if let Some(target_filter) = target_filter {
392 return target.kind.iter().any(|k| k == &target_filter.kind)
393 && target.name == target_filter.name;
394 }
395 true
396 })
397 .collect::<Vec<_>>();
398 if elf_paths.len() != 1 {
399 Err(eyre::eyre!(
400 "Expected 1 target, got {}: {:#?}",
401 elf_paths.len(),
402 elf_paths
403 ))
404 } else {
405 Ok(target_dir.as_ref().join(&elf_paths[0].name))
406 }
407}
408
409pub fn detect_toolchain(name: &str) {
411 let result = Command::new("rustup")
412 .args(["toolchain", "list", "--verbose"])
413 .stderr(Stdio::inherit())
414 .output()
415 .unwrap();
416 if !result.status.success() {
417 eprintln!("Failed to run: 'rustup toolchain list --verbose'");
418 std::process::exit(result.status.code().unwrap());
419 }
420
421 let stdout = String::from_utf8(result.stdout).unwrap();
422 if !stdout.lines().any(|line| line.trim().starts_with(name)) {
423 eprintln!("The '{name}' toolchain could not be found.");
424 std::process::exit(-1);
425 }
426}
427
428fn ensure_toolchain_installed(toolchain: &str, components: &[&str]) -> Result<(), i32> {
430 let output = Command::new("rustup")
432 .args(["toolchain", "list"])
433 .output()
434 .map_err(|e| {
435 tty_println(&format!("Failed to check toolchains: {}", e));
436 e.raw_os_error().unwrap_or(1)
437 })?;
438
439 let toolchain_installed = String::from_utf8_lossy(&output.stdout)
440 .lines()
441 .any(|line| line.trim().starts_with(toolchain));
442
443 if !toolchain_installed {
445 tty_println(&format!("Installing required toolchain: {}", toolchain));
446 let status = Command::new("rustup")
447 .args(["toolchain", "install", toolchain])
448 .status()
449 .map_err(|e| {
450 tty_println(&format!("Failed to install toolchain: {}", e));
451 e.raw_os_error().unwrap_or(1)
452 })?;
453
454 if !status.success() {
455 tty_println(&format!("Failed to install toolchain {}", toolchain));
456 return Err(status.code().unwrap_or(1));
457 }
458 }
459
460 for component in components {
462 let output = Command::new("rustup")
463 .args(["component", "list", "--toolchain", toolchain])
464 .output()
465 .map_err(|e| {
466 tty_println(&format!("Failed to check components: {}", e));
467 e.raw_os_error().unwrap_or(1)
468 })?;
469
470 let is_installed = String::from_utf8_lossy(&output.stdout)
471 .lines()
472 .any(|line| line.contains(component) && line.contains("(installed)"));
473
474 if !is_installed {
475 tty_println(&format!(
476 "Installing component {} for toolchain {}",
477 component, toolchain
478 ));
479 let status = Command::new("rustup")
480 .args(["component", "add", component, "--toolchain", toolchain])
481 .status()
482 .map_err(|e| {
483 tty_println(&format!("Failed to install component: {}", e));
484 e.raw_os_error().unwrap_or(1)
485 })?;
486
487 if !status.success() {
488 tty_println(&format!(
489 "Failed to install component {} for toolchain {}",
490 component, toolchain
491 ));
492 return Err(status.code().unwrap_or(1));
493 }
494 }
495 }
496
497 Ok(())
498}