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 {
109 !get_env_var(SKIP_BUILD_ENV).is_empty()
110}
111
112fn get_env_var(name: &str) -> String {
113 println!("cargo:rerun-if-env-changed={name}");
114 env::var(name).unwrap_or_default()
115}
116
117pub fn guest_methods<S: AsRef<str>>(
119 pkg: &Package,
120 target_dir: impl AsRef<Path>,
121 guest_features: &[String],
122 profile: &Option<S>,
123) -> Vec<PathBuf> {
124 let profile = profile.as_ref().map(|s| s.as_ref()).unwrap_or("release");
125 pkg.targets
126 .iter()
127 .filter(|target| {
128 target
129 .kind
130 .iter()
131 .any(|kind| kind == "bin" || kind == "example")
132 })
133 .filter(|target| {
134 target
135 .required_features
136 .iter()
137 .all(|required_feature| guest_features.contains(required_feature))
138 })
139 .flat_map(|target| {
140 let path_prefix = target_dir.as_ref().join(RUSTC_TARGET).join(profile);
141 target
142 .kind
143 .iter()
144 .map(|target_kind| {
145 let mut path = path_prefix.clone();
146 if target_kind == "example" {
147 path.push(target_kind);
148 }
149 path.join(&target.name).to_path_buf()
150 })
151 .collect::<Vec<_>>()
152 })
153 .collect()
154}
155
156fn sanitized_cmd(tool: &str) -> Command {
159 let mut cmd = Command::new(tool);
160 for (key, _val) in env::vars().filter(|x| x.0.starts_with("CARGO")) {
161 cmd.env_remove(key);
162 }
163 cmd.env_remove("RUSTUP_TOOLCHAIN");
164 cmd
165}
166
167pub fn cargo_command(subcmd: &str, rust_flags: &[&str]) -> Command {
170 let toolchain = format!("+{RUSTUP_TOOLCHAIN_NAME}");
171
172 let rustc = sanitized_cmd("rustup")
173 .args([&toolchain, "which", "rustc"])
174 .output()
175 .expect("rustup failed to find nightly toolchain")
176 .stdout;
177
178 let rustc = String::from_utf8(rustc).unwrap();
179 let rustc = rustc.trim();
180 println!("Using rustc: {rustc}");
181
182 let mut cmd = sanitized_cmd("cargo");
183 let mut args = vec![&toolchain, subcmd, "--target", RUSTC_TARGET];
184
185 if std::env::var(BUILD_LOCKED_ENV).is_ok() {
186 args.push("--locked");
187 }
188
189 args.extend_from_slice(&[
193 "-Z",
194 "build-std=alloc,core,proc_macro,panic_abort,std",
195 "-Z",
196 "build-std-features=compiler-builtins-mem",
197 ]);
198 println!("Building guest package: cargo {}", args.join(" "));
202
203 let encoded_rust_flags = encode_rust_flags(rust_flags);
204
205 cmd.env("RUSTC", rustc)
206 .env("CARGO_ENCODED_RUSTFLAGS", encoded_rust_flags)
207 .args(args);
208 cmd
209}
210
211pub(crate) fn encode_rust_flags(rustc_flags: &[&str]) -> String {
213 [
214 rustc_flags,
216 &[
217 "-C",
219 "passes=lower-atomic",
220 "-C",
226 &format!("link-arg=-Ttext=0x{:08X}", memory::TEXT_START),
227 "-C",
230 "link-arg=--fatal-warnings",
231 "-C",
232 "panic=abort",
233 ],
234 ]
235 .concat()
236 .join("\x1f")
237}
238
239fn tty_println(msg: &str) {
244 let tty_file = env::var(GUEST_LOGFILE_ENV).unwrap_or_else(|_| "/dev/tty".to_string());
245
246 let mut tty = fs::OpenOptions::new()
247 .read(true)
248 .write(true)
249 .create(true)
250 .truncate(false)
251 .open(tty_file)
252 .ok();
253
254 if let Some(tty) = &mut tty {
255 writeln!(tty, "{msg}").unwrap();
256 } else {
257 eprintln!("{msg}");
258 }
259}
260
261pub fn build_guest_package(
264 pkg: &Package,
265 guest_opts: &GuestOptions,
266 runtime_lib: Option<&str>,
267 target_filter: &Option<TargetFilter>,
268) -> Result<PathBuf, Option<i32>> {
269 if is_skip_build() {
270 return Err(None);
271 }
272
273 if let Err(code) = ensure_toolchain_installed(RUSTUP_TOOLCHAIN_NAME, &["rust-src"]) {
276 eprintln!("rustup toolchain commands failed. Please ensure rustup is installed (https://www.rust-lang.org/tools/install)");
277 return Err(Some(code));
278 }
279
280 let target_dir = guest_opts
281 .target_dir
282 .clone()
283 .unwrap_or_else(|| get_target_dir(pkg.manifest_path.clone()));
284
285 fs::create_dir_all(&target_dir).unwrap();
286
287 let runtime_rust_flags = runtime_lib
288 .map(|lib| vec![String::from("-C"), format!("link_arg={}", lib)])
289 .unwrap_or_default();
290 let rust_flags: Vec<_> = [
291 runtime_rust_flags
292 .iter()
293 .map(|s| s.as_str())
294 .collect::<Vec<_>>(),
295 guest_opts.rustc_flags.iter().map(|s| s.as_str()).collect(),
296 ]
297 .concat();
298
299 let mut cmd = cargo_command("build", &rust_flags);
300
301 let features_str = guest_opts.features.join(",");
302 if !features_str.is_empty() {
303 cmd.args(["--features", &features_str]);
304 }
305
306 cmd.args([
307 "--manifest-path",
308 pkg.manifest_path.as_str(),
309 "--target-dir",
310 target_dir.to_str().unwrap(),
311 ]);
312
313 if let Some(target_filter) = target_filter {
314 cmd.args([
315 format!("--{}", target_filter.kind).as_str(),
316 target_filter.name.as_str(),
317 ]);
318 }
319
320 let profile = if let Some(profile) = &guest_opts.profile {
321 profile
322 } else {
323 "release"
324 };
325 cmd.args(["--profile", profile]);
326
327 cmd.args(&guest_opts.options);
328
329 let command_string = format!(
330 "{} {}",
331 cmd.get_program().to_string_lossy(),
332 cmd.get_args()
333 .map(|arg| arg.to_string_lossy())
334 .collect::<Vec<_>>()
335 .join(" ")
336 );
337 tty_println(&format!("cargo command: {command_string}"));
338
339 let mut child = cmd
340 .stderr(Stdio::piped())
341 .env("CARGO_TERM_COLOR", "always")
342 .spawn()
343 .expect("cargo build failed");
344 let stderr = child.stderr.take().unwrap();
345
346 tty_println(&format!("{}: Starting build for {RUSTC_TARGET}", pkg.name));
347
348 for line in BufReader::new(stderr).lines() {
349 tty_println(&format!("{}: {}", pkg.name, line.unwrap()));
350 }
351
352 let res = child.wait().expect("Guest 'cargo build' failed");
353 if !res.success() {
354 Err(res.code())
355 } else {
356 Ok(get_dir_with_profile(
357 &target_dir,
358 profile,
359 target_filter
360 .as_ref()
361 .map(|t| t.kind == "example")
362 .unwrap_or(false),
363 ))
364 }
365}
366
367#[derive(Default)]
369pub struct TargetFilter {
370 pub name: String,
372 pub kind: String,
374}
375
376pub fn find_unique_executable<P: AsRef<Path>, Q: AsRef<Path>>(
379 pkg_dir: P,
380 target_dir: Q,
381 target_filter: &Option<TargetFilter>,
382) -> eyre::Result<PathBuf> {
383 let pkg = get_package(pkg_dir.as_ref());
384 let elf_paths = pkg
385 .targets
386 .into_iter()
387 .filter(move |target| {
388 if target.is_custom_build() || target.is_lib() {
390 return false;
391 }
392 if let Some(target_filter) = target_filter {
393 return target.kind.iter().any(|k| k == &target_filter.kind)
394 && target.name == target_filter.name;
395 }
396 true
397 })
398 .collect::<Vec<_>>();
399 if elf_paths.len() != 1 {
400 Err(eyre::eyre!(
401 "Expected 1 target, got {}: {:#?}",
402 elf_paths.len(),
403 elf_paths
404 ))
405 } else {
406 Ok(target_dir.as_ref().join(&elf_paths[0].name))
407 }
408}
409
410pub fn detect_toolchain(name: &str) {
412 let result = Command::new("rustup")
413 .args(["toolchain", "list", "--verbose"])
414 .stderr(Stdio::inherit())
415 .output()
416 .unwrap();
417 if !result.status.success() {
418 eprintln!("Failed to run: 'rustup toolchain list --verbose'");
419 std::process::exit(result.status.code().unwrap());
420 }
421
422 let stdout = String::from_utf8(result.stdout).unwrap();
423 if !stdout.lines().any(|line| line.trim().starts_with(name)) {
424 eprintln!("The '{name}' toolchain could not be found.");
425 std::process::exit(-1);
426 }
427}
428
429fn ensure_toolchain_installed(toolchain: &str, components: &[&str]) -> Result<(), i32> {
431 let output = Command::new("rustup")
433 .args(["toolchain", "list"])
434 .output()
435 .map_err(|e| {
436 tty_println(&format!("Failed to check toolchains: {}", e));
437 e.raw_os_error().unwrap_or(1)
438 })?;
439
440 let toolchain_installed = String::from_utf8_lossy(&output.stdout)
441 .lines()
442 .any(|line| line.trim().starts_with(toolchain));
443
444 if !toolchain_installed {
446 tty_println(&format!("Installing required toolchain: {}", toolchain));
447 let status = Command::new("rustup")
448 .args(["toolchain", "install", toolchain])
449 .status()
450 .map_err(|e| {
451 tty_println(&format!("Failed to install toolchain: {}", e));
452 e.raw_os_error().unwrap_or(1)
453 })?;
454
455 if !status.success() {
456 tty_println(&format!("Failed to install toolchain {}", toolchain));
457 return Err(status.code().unwrap_or(1));
458 }
459 }
460
461 for component in components {
463 let output = Command::new("rustup")
464 .args(["component", "list", "--toolchain", toolchain])
465 .output()
466 .map_err(|e| {
467 tty_println(&format!("Failed to check components: {}", e));
468 e.raw_os_error().unwrap_or(1)
469 })?;
470
471 let is_installed = String::from_utf8_lossy(&output.stdout)
472 .lines()
473 .any(|line| line.contains(component) && line.contains("(installed)"));
474
475 if !is_installed {
476 tty_println(&format!(
477 "Installing component {} for toolchain {}",
478 component, toolchain
479 ));
480 let status = Command::new("rustup")
481 .args(["component", "add", component, "--toolchain", toolchain])
482 .status()
483 .map_err(|e| {
484 tty_println(&format!("Failed to install component: {}", e));
485 e.raw_os_error().unwrap_or(1)
486 })?;
487
488 if !status.success() {
489 tty_println(&format!(
490 "Failed to install component {} for toolchain {}",
491 component, toolchain
492 ));
493 return Err(status.code().unwrap_or(1));
494 }
495 }
496 }
497
498 Ok(())
499}