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::{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";
24/// The default Rust toolchain name to use if OPENVM_RUST_TOOLCHAIN is not set
25pub const DEFAULT_RUSTUP_TOOLCHAIN_NAME: &str = "nightly-2025-02-14";
26
27/// Get the Rust toolchain name from environment variable or default
28pub 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
36/// Returns the given cargo Package from the metadata in the Cargo.toml manifest
37/// within the provided `manifest_dir`.
38pub 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
64/// Returns all packages from the Cargo.toml manifest at the given `manifest_dir`.
65pub 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
75/// Returns a single package if the manifest path matches exactly, otherwise returns all
76/// workspace packages.
77pub 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    // Check if any package has this exact manifest path
86    let matching = find_matching_packages(&manifest_meta, &manifest_path);
87
88    // If we found a package with this exact manifest path, return it
89    if !matching.is_empty() {
90        return matching;
91    }
92
93    // Otherwise return all workspace members
94    get_workspace_member_packages(manifest_meta)
95}
96
97/// Helper function to get cargo metadata for a manifest path
98fn 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
111/// Helper function to get workspace members
112fn 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
120/// Helper function to find packages matching a manifest path
121fn 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
133/// Determines and returns the build target directory from the Cargo manifest at
134/// the given `manifest_path`.
135pub 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
145/// Returns the workspace root directory from the Cargo manifest at
146/// the given `manifest_path`.
147pub 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
157/// Returns the target executable directory given `target_dir` and `profile`.
158pub 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
178/// When called from a build.rs, returns the current package being built.
179pub fn current_package() -> Package {
180    get_package(env::var("CARGO_MANIFEST_DIR").unwrap())
181}
182
183/// Reads the value of the environment variable `OPENVM_SKIP_BUILD` and returns true if it is set to
184/// 1.
185pub 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
194/// Returns all target ELF paths associated with the given guest crate.
195pub 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
233/// Build a [Command] with CARGO and RUSTUP_TOOLCHAIN environment variables
234/// removed.
235fn 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
246/// Creates a std::process::Command to execute the given cargo
247/// command in an environment suitable for targeting the zkvm guest.
248pub 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    // let rust_src = get_env_var("OPENVM_RUST_SRC");
269    // if !rust_src.is_empty() {
270    // TODO[jpw]: only do this for custom src once we make openvm toolchain
271    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    // cmd.env("__CARGO_TESTS_ONLY_SRC_ROOT", rust_src);
278    // }
279
280    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
290/// Returns a string that can be set as the value of CARGO_ENCODED_RUSTFLAGS when compiling guests
291pub(crate) fn encode_rust_flags(rustc_flags: &[&str]) -> String {
292    [
293        // Append other rust flags
294        rustc_flags,
295        &[
296            // Replace atomic ops with nonatomic versions since the guest is single threaded.
297            "-C",
298            "passes=lower-atomic",
299            // Specify where to start loading the program in
300            // memory.  The clang linker understands the same
301            // command line arguments as the GNU linker does; see
302            // https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html#SEC3
303            // for details.
304            "-C",
305            &format!("link-arg=-Ttext=0x{:08X}", memory::TEXT_START),
306            // Apparently not having an entry point is only a linker warning(!), so
307            // error out in this case.
308            "-C",
309            "link-arg=--fatal-warnings",
310            "-C",
311            "panic=abort",
312            // https://docs.rs/getrandom/0.3.2/getrandom/index.html#opt-in-backends
313            "--cfg",
314            "getrandom_backend=\"custom\"",
315        ],
316    ]
317    .concat()
318    .join("\x1f")
319}
320
321// HACK: Attempt to bypass the parent cargo output capture and
322// send directly to the tty, if available.  This way we get
323// progress messages from the inner cargo so the user doesn't
324// think it's just hanging.
325fn 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
343/// Builds a package that targets the riscv guest into the specified target
344/// directory.
345pub 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
382/// Generic wrapper call to cargo build
383pub 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    // Check if the required toolchain and rust-src component are installed, and if not, install
390    // them. This requires that `rustup` is installed.
391    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/// A filter for selecting a target from a package.
448#[derive(Default)]
449pub struct TargetFilter {
450    /// The target name to match.
451    pub name: String,
452    /// The kind of target to match.
453    pub kind: String,
454}
455
456/// Finds the unique executable target in the given package and target directory,
457/// using the given target filter.
458pub 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            // always filter out build script target
469            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
490/// Detect rust toolchain of given name
491pub 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
509/// Ensures the required toolchain and components are installed.
510fn ensure_toolchain_installed(toolchain: &str, components: &[&str]) -> Result<(), i32> {
511    // Check if toolchain is installed
512    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    // Install toolchain if missing
525    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    // Check and install missing components
542    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}