openvm_build/
lib.rs

1// Initial cargo build commands taken from risc0 under Apache 2.0 license
2
3#![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
22/// The rustc compiler [target](https://doc.rust-lang.org/rustc/targets/index.html).
23pub 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
29/// Returns the given cargo Package from the metadata in the Cargo.toml manifest
30/// within the provided `manifest_dir`.
31pub 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
68/// Determines and returns the build target directory from the Cargo manifest at
69/// the given `manifest_path`.
70pub 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
80/// Returns the target executable directory given `target_dir` and `profile`.
81pub 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
101/// When called from a build.rs, returns the current package being built.
102pub fn current_package() -> Package {
103    get_package(env::var("CARGO_MANIFEST_DIR").unwrap())
104}
105
106/// Reads the value of the environment variable `OPENVM_SKIP_BUILD` and returns true if it is set to 1.
107pub 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
116/// Returns all target ELF paths associated with the given guest crate.
117pub 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
155/// Build a [Command] with CARGO and RUSTUP_TOOLCHAIN environment variables
156/// removed.
157fn 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
166/// Creates a std::process::Command to execute the given cargo
167/// command in an environment suitable for targeting the zkvm guest.
168pub 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    // let rust_src = get_env_var("OPENVM_RUST_SRC");
189    // if !rust_src.is_empty() {
190    // TODO[jpw]: only do this for custom src once we make openvm toolchain
191    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    // cmd.env("__CARGO_TESTS_ONLY_SRC_ROOT", rust_src);
198    // }
199
200    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
210/// Returns a string that can be set as the value of CARGO_ENCODED_RUSTFLAGS when compiling guests
211pub(crate) fn encode_rust_flags(rustc_flags: &[&str]) -> String {
212    [
213        // Append other rust flags
214        rustc_flags,
215        &[
216            // Replace atomic ops with nonatomic versions since the guest is single threaded.
217            "-C",
218            "passes=lower-atomic",
219            // Specify where to start loading the program in
220            // memory.  The clang linker understands the same
221            // command line arguments as the GNU linker does; see
222            // https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html#SEC3
223            // for details.
224            "-C",
225            &format!("link-arg=-Ttext=0x{:08X}", memory::TEXT_START),
226            // Apparently not having an entry point is only a linker warning(!), so
227            // error out in this case.
228            "-C",
229            "link-arg=--fatal-warnings",
230            "-C",
231            "panic=abort",
232        ],
233    ]
234    .concat()
235    .join("\x1f")
236}
237
238// HACK: Attempt to bypass the parent cargo output capture and
239// send directly to the tty, if available.  This way we get
240// progress messages from the inner cargo so the user doesn't
241// think it's just hanging.
242fn 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
260/// Builds a package that targets the riscv guest into the specified target
261/// directory.
262pub 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    // Check if the required toolchain and rust-src component are installed, and if not, install them.
273    // This requires that `rustup` is installed.
274    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/// A filter for selecting a target from a package.
367#[derive(Default)]
368pub struct TargetFilter {
369    /// The target name to match.
370    pub name: String,
371    /// The kind of target to match.
372    pub kind: String,
373}
374
375/// Finds the unique executable target in the given package and target directory,
376/// using the given target filter.
377pub 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            // always filter out build script target
388            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
409/// Detect rust toolchain of given name
410pub 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
428/// Ensures the required toolchain and components are installed.
429fn ensure_toolchain_installed(toolchain: &str, components: &[&str]) -> Result<(), i32> {
430    // Check if toolchain is installed
431    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    // Install toolchain if missing
444    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    // Check and install missing components
461    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}