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
107/// 1.
108pub 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
117/// Returns all target ELF paths associated with the given guest crate.
118pub 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
156/// Build a [Command] with CARGO and RUSTUP_TOOLCHAIN environment variables
157/// removed.
158fn 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
167/// Creates a std::process::Command to execute the given cargo
168/// command in an environment suitable for targeting the zkvm guest.
169pub 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    // let rust_src = get_env_var("OPENVM_RUST_SRC");
190    // if !rust_src.is_empty() {
191    // TODO[jpw]: only do this for custom src once we make openvm toolchain
192    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    // cmd.env("__CARGO_TESTS_ONLY_SRC_ROOT", rust_src);
199    // }
200
201    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
211/// Returns a string that can be set as the value of CARGO_ENCODED_RUSTFLAGS when compiling guests
212pub(crate) fn encode_rust_flags(rustc_flags: &[&str]) -> String {
213    [
214        // Append other rust flags
215        rustc_flags,
216        &[
217            // Replace atomic ops with nonatomic versions since the guest is single threaded.
218            "-C",
219            "passes=lower-atomic",
220            // Specify where to start loading the program in
221            // memory.  The clang linker understands the same
222            // command line arguments as the GNU linker does; see
223            // https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html#SEC3
224            // for details.
225            "-C",
226            &format!("link-arg=-Ttext=0x{:08X}", memory::TEXT_START),
227            // Apparently not having an entry point is only a linker warning(!), so
228            // error out in this case.
229            "-C",
230            "link-arg=--fatal-warnings",
231            "-C",
232            "panic=abort",
233        ],
234    ]
235    .concat()
236    .join("\x1f")
237}
238
239// HACK: Attempt to bypass the parent cargo output capture and
240// send directly to the tty, if available.  This way we get
241// progress messages from the inner cargo so the user doesn't
242// think it's just hanging.
243fn 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
261/// Builds a package that targets the riscv guest into the specified target
262/// directory.
263pub 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    // Check if the required toolchain and rust-src component are installed, and if not, install
274    // them. This requires that `rustup` is installed.
275    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/// A filter for selecting a target from a package.
368#[derive(Default)]
369pub struct TargetFilter {
370    /// The target name to match.
371    pub name: String,
372    /// The kind of target to match.
373    pub kind: String,
374}
375
376/// Finds the unique executable target in the given package and target directory,
377/// using the given target filter.
378pub 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            // always filter out build script target
389            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
410/// Detect rust toolchain of given name
411pub 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
429/// Ensures the required toolchain and components are installed.
430fn ensure_toolchain_installed(toolchain: &str, components: &[&str]) -> Result<(), i32> {
431    // Check if toolchain is installed
432    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    // Install toolchain if missing
445    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    // Check and install missing components
462    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}