risc0_build/
lib.rs

1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(clippy::needless_doctest_main)]
16#![doc = include_str!("../README.md")]
17#![deny(missing_docs)]
18#![deny(rustdoc::broken_intra_doc_links)]
19#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
20
21mod config;
22mod docker;
23
24use std::{
25    borrow::Cow,
26    collections::HashMap,
27    default::Default,
28    env,
29    ffi::OsStr,
30    fmt::Write as _,
31    fs::{self, File},
32    io::{BufRead, BufReader, Write},
33    path::{Path, PathBuf},
34    process::{Command, Stdio},
35    str::FromStr,
36};
37
38use anyhow::{anyhow, Context, Result};
39use cargo_metadata::{Message, MetadataCommand, Package};
40use config::GuestMetadata;
41use risc0_binfmt::{ProgramBinary, KERNEL_START_ADDR};
42use risc0_zkp::core::digest::Digest;
43use risc0_zkvm_platform::memory;
44use serde::Deserialize;
45
46use self::{config::GuestInfo, docker::build_guest_package_docker};
47
48pub use self::{
49    config::{
50        DockerOptions, DockerOptionsBuilder, DockerOptionsBuilderError, GuestOptions,
51        GuestOptionsBuilder, GuestOptionsBuilderError,
52    },
53    docker::{docker_build, BuildStatus, TARGET_DIR},
54};
55
56const RISC0_TARGET_TRIPLE: &str = "riscv32im-risc0-zkvm-elf";
57const DEFAULT_DOCKER_TAG: &str = "r0.1.85.0";
58
59#[derive(Debug, Deserialize)]
60struct Risc0Metadata {
61    methods: Vec<String>,
62}
63
64impl Risc0Metadata {
65    fn from_package(pkg: &Package) -> Option<Risc0Metadata> {
66        let obj = pkg.metadata.get("risc0").unwrap();
67        serde_json::from_value(obj.clone()).unwrap()
68    }
69}
70
71trait GuestBuilder: Sized {
72    fn build(guest_info: &GuestInfo, name: &str, elf_path: &str) -> Result<Self>;
73
74    fn codegen_consts(&self) -> String;
75
76    #[cfg(feature = "guest-list")]
77    fn codegen_list_entry(&self) -> String;
78}
79
80/// Represents an item in the generated list of compiled guest binaries
81#[derive(Debug, Clone)]
82pub struct MinGuestListEntry {
83    /// The name of the guest binary
84    pub name: Cow<'static, str>,
85
86    /// The path to the ELF binary
87    pub path: Cow<'static, str>,
88}
89
90impl GuestBuilder for MinGuestListEntry {
91    fn build(_guest_info: &GuestInfo, name: &str, elf_path: &str) -> Result<Self> {
92        Ok(Self {
93            name: Cow::Owned(name.to_owned()),
94            path: Cow::Owned(elf_path.to_owned()),
95        })
96    }
97
98    fn codegen_consts(&self) -> String {
99        // Quick check for '#' to avoid injection of arbitrary Rust code into the
100        // method.rs file. This would not be a serious issue since it would only
101        // affect the user that set the path, but it's good to add a check.
102        if self.path.contains('#') {
103            panic!("method path cannot include #: {}", self.path);
104        }
105
106        let upper = self.name.to_uppercase().replace('-', "_");
107        let elf_path: &str = &self.path;
108
109        format!(r##"pub const {upper}_PATH: &str = r#"{elf_path}"#;"##)
110    }
111
112    #[cfg(feature = "guest-list")]
113    fn codegen_list_entry(&self) -> String {
114        let upper = self.name.to_uppercase().replace('-', "_");
115        format!(
116            r##"
117    MinGuestListEntry {{
118        name: std::borrow::Cow::Borrowed("{upper}"),
119        path: std::borrow::Cow::Borrowed({upper}_PATH),
120    }}"##
121        )
122    }
123}
124
125/// Represents an item in the generated list of compiled guest binaries
126#[derive(Debug, Clone)]
127pub struct GuestListEntry {
128    /// The name of the guest binary
129    pub name: Cow<'static, str>,
130
131    /// The combined user ELF & kernel ELF guest binary
132    pub elf: Cow<'static, [u8]>,
133
134    /// The image id of the guest program.
135    pub image_id: Digest,
136
137    /// The path to the ELF binary
138    pub path: Cow<'static, str>,
139}
140
141fn r0vm_image_id(path: &str, flag: &str) -> Result<Digest> {
142    use hex::FromHex;
143    let output = Command::new("r0vm")
144        .env_remove("RUST_LOG")
145        .args(["--elf", path, flag])
146        .output()?;
147    if output.status.success() {
148        let stdout = String::from_utf8(output.stdout)?;
149        let digest = stdout.trim();
150        Ok(Digest::from_hex(digest).context("expecting a hex string")?)
151    } else {
152        let stderr = String::from_utf8(output.stderr)?;
153        Err(anyhow!("{stderr}"))
154    }
155}
156
157fn compute_image_id(elf: &[u8], elf_path: &str) -> Result<Digest> {
158    Ok(match r0vm_image_id(elf_path, "--id") {
159        Ok(image_id) => image_id,
160        Err(err) => {
161            tty_println("Falling back to slow ImageID computation. Updating to the latest r0vm will speed this up.");
162            tty_println(&format!("  error: {err}"));
163            risc0_binfmt::compute_image_id(elf)?
164        }
165    })
166}
167
168impl GuestBuilder for GuestListEntry {
169    /// Builds the [GuestListEntry] by reading the ELF from disk, and calculating the associated
170    /// image ID.
171    fn build(guest_info: &GuestInfo, name: &str, elf_path: &str) -> Result<Self> {
172        let mut elf = vec![];
173        let mut elf_path = elf_path.to_owned();
174        let mut image_id = Digest::default();
175        let is_kernel = guest_info.metadata.kernel;
176
177        if !is_skip_build() {
178            if is_kernel {
179                elf = std::fs::read(&elf_path)?;
180            } else {
181                let user_elf = std::fs::read(&elf_path)?;
182                let kernel_elf = guest_info.options.kernel();
183                let binary = ProgramBinary::new(&user_elf, &kernel_elf);
184                elf = binary.encode();
185                let combined_path = PathBuf::from_str(&(elf_path + ".bin"))?;
186                std::fs::write(&combined_path, &elf)?;
187                elf_path = combined_path.to_str().unwrap().to_owned();
188                image_id = compute_image_id(&elf, &elf_path)?;
189            }
190        }
191
192        Ok(Self {
193            name: Cow::Owned(name.to_owned()),
194            elf: Cow::Owned(elf),
195            image_id,
196            path: Cow::Owned(elf_path),
197        })
198    }
199
200    fn codegen_consts(&self) -> String {
201        // Quick check for '#' to avoid injection of arbitrary Rust code into the
202        // method.rs file. This would not be a serious issue since it would only
203        // affect the user that set the path, but it's good to add a check.
204        if self.path.contains('#') {
205            panic!("method path cannot include #: {}", self.path);
206        }
207
208        let upper = self.name.to_uppercase().replace('-', "_");
209
210        let image_id = self.image_id.as_words();
211        let elf = if is_skip_build() {
212            "&[]".to_string()
213        } else {
214            format!("include_bytes!({:?})", self.path)
215        };
216
217        let mut str = String::new();
218
219        writeln!(&mut str, "pub const {upper}_ELF: &[u8] = {elf};").unwrap();
220        writeln!(&mut str, "pub const {upper}_PATH: &str = {:?};", self.path).unwrap();
221        writeln!(&mut str, "pub const {upper}_ID: [u32; 8] = {image_id:?};").unwrap();
222
223        str
224    }
225
226    #[cfg(feature = "guest-list")]
227    fn codegen_list_entry(&self) -> String {
228        let upper = self.name.to_uppercase().replace('-', "_");
229        format!(
230            r##"
231    GuestListEntry {{
232        name: std::borrow::Cow::Borrowed("{upper}"),
233        elf: std::borrow::Cow::Borrowed({upper}_ELF),
234        image_id: {upper}_ID,
235        path: std::borrow::Cow::Borrowed({upper}_PATH),
236    }}"##
237        )
238    }
239}
240
241/// Returns the given cargo Package from the metadata in the Cargo.toml manifest
242/// within the provided `manifest_dir`.
243pub fn get_package(manifest_dir: impl AsRef<Path>) -> Package {
244    let manifest_dir = manifest_dir.as_ref();
245    let manifest_path = manifest_dir.join("Cargo.toml");
246    let manifest_meta = MetadataCommand::new()
247        .manifest_path(&manifest_path)
248        .no_deps()
249        .exec()
250        .expect("cargo metadata command failed");
251    let mut matching: Vec<Package> = manifest_meta
252        .packages
253        .into_iter()
254        .filter(|pkg| {
255            let std_path: &Path = pkg.manifest_path.as_ref();
256            std_path == manifest_path
257        })
258        .collect();
259    if matching.is_empty() {
260        eprintln!("ERROR: No package found in {manifest_dir:?}");
261        std::process::exit(-1);
262    }
263    if matching.len() > 1 {
264        eprintln!("ERROR: Multiple packages found in {manifest_dir:?}",);
265        std::process::exit(-1);
266    }
267    matching.pop().unwrap()
268}
269
270/// Determines and returns the build target directory from the Cargo manifest at
271/// the given `manifest_path`.
272pub fn get_target_dir(manifest_path: impl AsRef<Path>) -> PathBuf {
273    MetadataCommand::new()
274        .manifest_path(manifest_path.as_ref())
275        .no_deps()
276        .exec()
277        .expect("cargo metadata command failed")
278        .target_directory
279        .into()
280}
281
282/// When called from a build.rs, returns the current package being built.
283fn current_package() -> Package {
284    get_package(env::var("CARGO_MANIFEST_DIR").unwrap())
285}
286
287/// Returns all inner packages specified the "methods" list inside
288/// "package.metadata.risc0".
289fn guest_packages(pkg: &Package) -> Vec<Package> {
290    let manifest_dir = pkg.manifest_path.parent().unwrap();
291    Risc0Metadata::from_package(pkg)
292        .unwrap()
293        .methods
294        .iter()
295        .map(|inner| get_package(manifest_dir.join(inner)))
296        .collect()
297}
298
299fn is_debug() -> bool {
300    get_env_var("RISC0_BUILD_DEBUG") == "1"
301}
302
303fn is_skip_build() -> bool {
304    !get_env_var("RISC0_SKIP_BUILD").is_empty()
305}
306
307fn get_env_var(name: &str) -> String {
308    let ret = env::var(name).unwrap_or_default();
309    if let Some(pkg) = env::var_os("CARGO_PKG_NAME") {
310        if pkg != "cargo-risczero" {
311            println!("cargo:rerun-if-env-changed={name}");
312        }
313    }
314    ret
315}
316
317/// Returns all methods associated with the given guest crate.
318fn guest_methods<G: GuestBuilder>(
319    pkg: &Package,
320    target_dir: impl AsRef<Path>,
321    guest_info: &GuestInfo,
322    profile: &str,
323) -> Vec<G> {
324    pkg.targets
325        .iter()
326        .filter(|target| target.is_bin())
327        .filter(|target| {
328            target
329                .required_features
330                .iter()
331                .all(|required_feature| guest_info.options.features.contains(required_feature))
332        })
333        .map(|target| {
334            G::build(
335                guest_info,
336                &target.name,
337                target_dir
338                    .as_ref()
339                    .join(RISC0_TARGET_TRIPLE)
340                    .join(profile)
341                    .join(&target.name)
342                    .to_str()
343                    .context("elf path contains invalid unicode")
344                    .unwrap(),
345            )
346            .unwrap()
347        })
348        .collect()
349}
350
351/// Build a [Command] with CARGO and RUSTUP_TOOLCHAIN environment variables
352/// removed.
353fn sanitized_cmd(tool: &str) -> Command {
354    let mut cmd = Command::new(tool);
355    for (key, _val) in env::vars().filter(|x| x.0.starts_with("CARGO")) {
356        cmd.env_remove(key);
357    }
358    cmd.env_remove("RUSTUP_TOOLCHAIN");
359    cmd
360}
361
362fn cpp_toolchain() -> Option<PathBuf> {
363    let rzup = rzup::Rzup::new().unwrap();
364    let (version, path) = rzup
365        .get_default_version(&rzup::Component::CppToolchain)
366        .unwrap()?;
367    println!("Using C++ toolchain version {version}");
368    Some(path)
369}
370
371fn rust_toolchain() -> PathBuf {
372    let rzup = rzup::Rzup::new().unwrap();
373    let Some((version, path)) = rzup
374        .get_default_version(&rzup::Component::RustToolchain)
375        .unwrap()
376    else {
377        panic!("Risc Zero Rust toolchain not found. Try running `rzup install rust`");
378    };
379    println!("Using Rust toolchain version {version}");
380    path
381}
382
383/// Creates a std::process::Command to execute the given cargo
384/// command in an environment suitable for targeting the zkvm guest.
385#[stability::unstable]
386pub fn cargo_command(subcmd: &str, rustc_flags: &[String]) -> Command {
387    let mut guest_info = GuestInfo::default();
388    guest_info.metadata.rustc_flags = Some(rustc_flags.to_vec());
389    cargo_command_internal(subcmd, &guest_info)
390}
391
392pub(crate) fn cargo_command_internal(subcmd: &str, guest_info: &GuestInfo) -> Command {
393    let rustc = rust_toolchain().join("bin/rustc");
394    println!("Using rustc: {}", rustc.display());
395
396    let mut cmd = sanitized_cmd("cargo");
397    let mut args = vec![subcmd, "--target", RISC0_TARGET_TRIPLE];
398
399    if !get_env_var("RISC0_BUILD_LOCKED").is_empty() {
400        args.push("--locked");
401    }
402
403    let rust_src = get_env_var("RISC0_RUST_SRC");
404    if !rust_src.is_empty() {
405        args.push("-Z");
406        args.push("build-std=alloc,core,proc_macro,panic_abort,std");
407        args.push("-Z");
408        args.push("build-std-features=compiler-builtins-mem");
409        cmd.env("__CARGO_TESTS_ONLY_SRC_ROOT", rust_src);
410    }
411
412    let encoded_rust_flags = encode_rust_flags(&guest_info.metadata, false);
413
414    if !cpp_toolchain_override() {
415        if let Some(toolchain_path) = cpp_toolchain() {
416            cmd.env("CC", toolchain_path.join("bin/riscv32-unknown-elf-gcc"));
417        } else {
418            // If you aren't compiling any C/C++ code, it might be just fine to not have a C++
419            // toolchain installed, but if you are then your compilation will surely fail. To avoid
420            // a potentially confusing error message, set the CC path to a bogus path that will
421            // hopefully make the issue obvious.
422            cmd.env(
423                "CC",
424                "/no_risc0_cpp_toolchain_installed_run_rzup_install_cpp",
425            );
426        }
427
428        cmd.env("CFLAGS_riscv32im_risc0_zkvm_elf", "-march=rv32im -nostdlib");
429
430        // Signal to dependencies, cryptography patches in particular, that the bigint2 zkVM
431        // feature is available. Gated behind unstable to match risc0-zkvm-platform. Note that this
432        // would be seamless if there was a reliable way to tell whether it is enabled in
433        // risc0-zkvm-platform, however, this problem is also temporary.
434        #[cfg(feature = "unstable")]
435        cmd.env("RISC0_FEATURE_bigint2", "");
436    }
437
438    cmd.env("RUSTC", rustc)
439        .env("CARGO_ENCODED_RUSTFLAGS", encoded_rust_flags)
440        .args(args);
441    cmd
442}
443
444fn get_rust_toolchain_version() -> semver::Version {
445    let rzup = rzup::Rzup::new().unwrap();
446    let Some((version, _)) = rzup
447        .get_default_version(&rzup::Component::RustToolchain)
448        .unwrap()
449    else {
450        panic!("Risc Zero Rust toolchain not found. Try running `rzup install rust`");
451    };
452    version
453}
454
455/// Returns a string that can be set as the value of CARGO_ENCODED_RUSTFLAGS when compiling guests
456pub(crate) fn encode_rust_flags(guest_meta: &GuestMetadata, escape_special_chars: bool) -> String {
457    // llvm changed `loweratomic` to `lower-atomic`
458    let lower_atomic = if get_rust_toolchain_version() > semver::Version::new(1, 81, 0) {
459        "passes=lower-atomic"
460    } else {
461        "passes=loweratomic"
462    };
463    let rustc_flags = guest_meta.rustc_flags.clone().unwrap_or_default();
464    let rustc_flags: Vec<_> = rustc_flags.iter().map(|s| s.as_str()).collect();
465    let text_addr = if guest_meta.kernel {
466        KERNEL_START_ADDR.0
467    } else {
468        memory::TEXT_START
469    };
470    [
471        // Append other rust flags
472        rustc_flags.as_slice(),
473        &[
474            // Replace atomic ops with nonatomic versions since the guest is single threaded.
475            "-C",
476            lower_atomic,
477            // Specify where to start loading the program in
478            // memory.  The clang linker understands the same
479            // command line arguments as the GNU linker does; see
480            // https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html#SEC3
481            // for details.
482            "-C",
483            &format!("link-arg=-Ttext={text_addr:#010x}"),
484            // Apparently not having an entry point is only a linker warning(!), so
485            // error out in this case.
486            "-C",
487            "link-arg=--fatal-warnings",
488            "-C",
489            "panic=abort",
490            "--cfg",
491            "getrandom_backend=\"custom\"",
492        ],
493    ]
494    .concat()
495    .iter()
496    .map(|x| {
497        if escape_special_chars {
498            x.escape_default().to_string()
499        } else {
500            x.to_string()
501        }
502    })
503    .collect::<Vec<String>>()
504    .join("\x1f")
505}
506
507fn cpp_toolchain_override() -> bool {
508    // detect if there's an attempt to override the Cpp toolchain.
509    // Overriding the toolchain useful for troubleshooting crates.
510    !get_env_var("CC_riscv32im_risc0_zkvm_elf").is_empty()
511        || !get_env_var("CFLAGS_riscv32im_risc0_zkvm_elf").is_empty()
512}
513
514/// Builds a static library providing a rust runtime.
515///
516/// This can be used to build programs for the zkvm which don't depend on risc0_zkvm.
517pub fn build_rust_runtime() -> String {
518    build_rust_runtime_with_features(&[])
519}
520
521/// Builds a static library providing a rust runtime, with additional features given as arguments.
522///
523/// This can be used to build programs for the zkvm which don't depend on risc0_zkvm. Feature flags
524/// given will be pass when building risc0-zkvm-platform.
525pub fn build_rust_runtime_with_features(features: &[&str]) -> String {
526    build_staticlib(
527        "risc0-zkvm-platform",
528        &[&["rust-runtime", "panic-handler", "entrypoint"], features].concat(),
529    )
530}
531
532/// Builds a static library and returns the name of the resultant file.
533fn build_staticlib(guest_pkg: &str, features: &[&str]) -> String {
534    let guest_dir = get_guest_dir("static-lib", guest_pkg);
535
536    let guest_info = GuestInfo::default();
537    let mut cmd = cargo_command_internal("rustc", &guest_info);
538
539    if !is_debug() {
540        cmd.arg("--release");
541    }
542
543    // Add args to specify the package to be built, and to build is as a staticlib.
544    cmd.args([
545        "--package",
546        guest_pkg,
547        "--target-dir",
548        guest_dir.to_str().unwrap(),
549        "--lib",
550        "--message-format=json",
551        "--crate-type=staticlib",
552    ]);
553
554    for feature in features {
555        cmd.args(["--features", &(guest_pkg.to_owned() + "/" + feature)]);
556    }
557
558    eprintln!("Building staticlib: {:?}", cmd);
559
560    // Run the build command and extract the name of the resulting staticlib
561    // artifact.
562    let mut child = cmd.stdout(Stdio::piped()).spawn().unwrap();
563    let reader = std::io::BufReader::new(child.stdout.take().unwrap());
564    let mut libs = Vec::new();
565    for message in Message::parse_stream(reader) {
566        match message.unwrap() {
567            Message::CompilerArtifact(artifact) => {
568                for filename in artifact.filenames {
569                    if let Some("a") = filename.extension() {
570                        libs.push(filename.to_string());
571                    }
572                }
573            }
574            Message::CompilerMessage(msg) => {
575                eprint!("{}", msg);
576            }
577            _ => (),
578        }
579    }
580
581    let output = child.wait().expect("Couldn't get cargo's exit status");
582    if !output.success() {
583        panic!("Unable to build static library")
584    }
585
586    match libs.as_slice() {
587        [] => panic!("No static library was built"),
588        [lib] => lib.to_string(),
589        _ => panic!("Multiple static libraries found: {:?}", libs.as_slice()),
590    }
591}
592
593// HACK: Attempt to bypass the parent cargo output capture and
594// send directly to the tty, if available.  This way we get
595// progress messages from the inner cargo so the user doesn't
596// think it's just hanging.
597fn tty_println(msg: &str) {
598    let tty_file = env::var("RISC0_GUEST_LOGFILE").unwrap_or_else(|_| "/dev/tty".to_string());
599
600    let mut tty = fs::OpenOptions::new()
601        .read(true)
602        .write(true)
603        .create(true)
604        .truncate(false)
605        .open(tty_file)
606        .ok();
607
608    if let Some(tty) = &mut tty {
609        writeln!(tty, "{msg}").unwrap();
610    } else {
611        eprintln!("{msg}");
612    }
613}
614
615// Builds a package that targets the riscv guest into the specified target
616// directory.
617fn build_guest_package(pkg: &Package, target_dir: impl AsRef<Path>, guest_info: &GuestInfo) {
618    if is_skip_build() {
619        return;
620    }
621
622    let target_dir = target_dir.as_ref();
623    fs::create_dir_all(target_dir).unwrap();
624
625    let mut cmd = cargo_command_internal("build", guest_info);
626
627    let features_str = guest_info.options.features.join(",");
628    if !features_str.is_empty() {
629        cmd.args(["--features", &features_str]);
630    }
631
632    cmd.args([
633        "--manifest-path",
634        pkg.manifest_path.as_str(),
635        "--target-dir",
636        target_dir.to_str().unwrap(),
637    ]);
638
639    if !is_debug() {
640        cmd.args(["--release"]);
641    }
642
643    let mut child = cmd
644        .stderr(Stdio::piped())
645        .spawn()
646        .expect("cargo build failed");
647    let stderr = child.stderr.take().unwrap();
648
649    tty_println(&format!(
650        "{}: Starting build for {RISC0_TARGET_TRIPLE}",
651        pkg.name
652    ));
653
654    for line in BufReader::new(stderr).lines() {
655        tty_println(&format!("{}: {}", pkg.name, line.unwrap()));
656    }
657
658    let res = child.wait().expect("Guest 'cargo build' failed");
659    if !res.success() {
660        std::process::exit(res.code().unwrap());
661    }
662}
663
664fn get_out_dir() -> PathBuf {
665    // This code is based on https://docs.rs/cxx-build/latest/src/cxx_build/target.rs.html#10-49
666
667    if let Some(target_dir) = env::var_os("CARGO_TARGET_DIR").map(Into::<PathBuf>::into) {
668        if target_dir.is_absolute() {
669            return target_dir.join("riscv-guest");
670        }
671    }
672
673    let mut dir: PathBuf = env::var_os("OUT_DIR").unwrap().into();
674    loop {
675        if dir.join(".rustc_info.json").exists()
676            || dir.join("CACHEDIR.TAG").exists()
677            || dir.file_name() == Some(OsStr::new("target"))
678                && dir
679                    .parent()
680                    .is_some_and(|parent| parent.join("Cargo.toml").exists())
681        {
682            return dir.join("riscv-guest");
683        }
684        if dir.pop() {
685            continue;
686        }
687        panic!("Cannot find cargo target dir location")
688    }
689}
690
691fn get_guest_dir(host_pkg: impl AsRef<Path>, guest_pkg: impl AsRef<Path>) -> PathBuf {
692    get_out_dir().join(host_pkg).join(guest_pkg)
693}
694
695/// Embeds methods built for RISC-V for use by host-side dependencies.
696/// Specify custom options for a guest package by defining its [GuestOptions].
697/// See [embed_methods].
698pub fn embed_methods_with_options(
699    guest_pkg_to_options: HashMap<&str, GuestOptions>,
700) -> Vec<GuestListEntry> {
701    do_embed_methods(guest_pkg_to_options)
702}
703
704/// Build methods for RISC-V and embed minimal metadata - the `elf` name and path.
705/// To embed the full elf, use [embed_methods_with_options].
706///
707/// Use this option if you wish to import the guest `elf` into your prover at runtime
708/// rather than embedding it into the prover binary. This reduces build times for large
709/// binaries, but makes prover initialization fallible.
710///
711/// Specify custom options for the package by defining its [GuestOptions].
712pub fn embed_method_metadata_with_options(
713    guest_pkg_to_options: HashMap<&str, GuestOptions>,
714) -> Vec<MinGuestListEntry> {
715    do_embed_methods(guest_pkg_to_options)
716}
717
718struct GuestPackageWithOptions {
719    name: String,
720    pkg: Package,
721    opts: GuestOptions,
722    target_dir: PathBuf,
723}
724
725/// Embeds methods built for RISC-V for use by host-side dependencies.
726/// Specify custom options for a guest package by defining its [GuestOptions].
727/// See [embed_methods].
728fn do_embed_methods<G: GuestBuilder>(mut guest_opts: HashMap<&str, GuestOptions>) -> Vec<G> {
729    // Read the cargo metadata for info from `[package.metadata.risc0]`.
730    let pkg = current_package();
731    let guest_packages = guest_packages(&pkg);
732
733    let mut pkg_opts = vec![];
734    for guest_pkg in guest_packages {
735        let guest_dir = get_guest_dir(&pkg.name, &guest_pkg.name);
736        let opts = guest_opts
737            .remove(guest_pkg.name.as_str())
738            .unwrap_or_default();
739        pkg_opts.push(GuestPackageWithOptions {
740            name: format!("{}.{}", pkg.name, guest_pkg.name),
741            pkg: guest_pkg,
742            opts,
743            target_dir: guest_dir,
744        });
745    }
746
747    // If the user provided options for a package that wasn't built, abort.
748    if let Some(package) = guest_opts.keys().next() {
749        panic!("Error: guest options were provided for package {package:?} but the package was not built.");
750    }
751
752    build_methods(&pkg_opts)
753}
754
755fn build_methods<G: GuestBuilder>(guest_packages: &[GuestPackageWithOptions]) -> Vec<G> {
756    let out_dir_env = env::var_os("OUT_DIR").unwrap();
757    let out_dir = Path::new(&out_dir_env); // $ROOT/target/$profile/build/$crate/out
758
759    let methods_path = out_dir.join("methods.rs");
760    let mut methods_file = File::create(&methods_path).unwrap();
761
762    // NOTE: Codegen of the guest list is gated behind the "guest-list" feature flag,
763    // although the data structure are not, because when the `GuestListEntry` type
764    // is referenced in the generated code, this requires `risc0-build` be declared
765    // as a dependency of the methods crate.
766    #[cfg(feature = "guest-list")]
767    let mut guest_list_codegen = Vec::new();
768    #[cfg(feature = "guest-list")]
769    methods_file
770        .write_all(b"use risc0_build::GuestListEntry;\n")
771        .unwrap();
772
773    let profile = if is_debug() { "debug" } else { "release" };
774
775    let mut guest_list = vec![];
776    for guest in guest_packages {
777        println!("Building guest package: {}", guest.name);
778
779        let guest_info = GuestInfo {
780            options: guest.opts.clone(),
781            metadata: (&guest.pkg).into(),
782        };
783
784        let methods: Vec<G> = if guest.opts.use_docker.is_some() {
785            build_guest_package_docker(&guest.pkg, &guest.target_dir, &guest_info).unwrap();
786            guest_methods(&guest.pkg, &guest.target_dir, &guest_info, "docker")
787        } else {
788            build_guest_package(&guest.pkg, &guest.target_dir, &guest_info);
789            guest_methods(&guest.pkg, &guest.target_dir, &guest_info, profile)
790        };
791
792        for method in methods {
793            methods_file
794                .write_all(method.codegen_consts().as_bytes())
795                .unwrap();
796
797            #[cfg(feature = "guest-list")]
798            guest_list_codegen.push(method.codegen_list_entry());
799            guest_list.push(method);
800        }
801    }
802
803    #[cfg(feature = "guest-list")]
804    methods_file
805        .write_all(
806            format!(
807                "\npub const GUEST_LIST: &[{}] = &[{}];\n",
808                std::any::type_name::<G>(),
809                guest_list_codegen.join(",")
810            )
811            .as_bytes(),
812        )
813        .unwrap();
814
815    // HACK: It's not particularly practical to figure out all the
816    // files that all the guest crates transitively depend on.  So, we
817    // want to run the guest "cargo build" command each time we build.
818    //
819    // Since we generate methods.rs each time we run, it will always
820    // be changed.
821    println!("cargo:rerun-if-changed={}", methods_path.display());
822    println!("cargo:rerun-if-env-changed=RISC0_GUEST_LOGFILE");
823
824    guest_list
825}
826
827/// Embeds methods built for RISC-V for use by host-side dependencies.
828///
829/// This method should be called from a package with a
830/// [package.metadata.risc0] section including a "methods" property
831/// listing the relative paths that contain riscv guest method
832/// packages.
833///
834/// To access the generated image IDs and ELF filenames, include the
835/// generated methods.rs:
836///
837/// ```text
838/// include!(concat!(env!("OUT_DIR"), "/methods.rs"));
839/// ```
840///
841/// To conform to rust's naming conventions, the constants are mapped
842/// to uppercase.  For instance, if you have a method named
843/// "my_method", the image ID and elf contents will be defined as
844/// "MY_METHOD_ID" and "MY_METHOD_ELF" respectively.
845pub fn embed_methods() -> Vec<GuestListEntry> {
846    embed_methods_with_options(HashMap::new())
847}
848
849/// Build a guest package into the specified `target_dir` using the specified
850/// `GuestOptions`.
851pub fn build_package(
852    pkg: &Package,
853    target_dir: impl AsRef<Path>,
854    options: GuestOptions,
855) -> Result<Vec<GuestListEntry>> {
856    println!("Building guest package: {}", pkg.name);
857
858    let guest_info = GuestInfo {
859        options: options.clone(),
860        metadata: pkg.into(),
861    };
862
863    let profile = if is_debug() { "debug" } else { "release" };
864
865    if options.use_docker.is_some() {
866        build_guest_package_docker(pkg, target_dir.as_ref(), &guest_info)?;
867        Ok(guest_methods(pkg, &target_dir, &guest_info, "docker"))
868    } else {
869        build_guest_package(pkg, &target_dir, &guest_info);
870        Ok(guest_methods(pkg, &target_dir, &guest_info, profile))
871    }
872}
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877
878    const RUSTC_FLAGS: &[&str] = &[
879        "--cfg",
880        "foo=\"bar\"",
881        "--cfg",
882        "foo='bar'",
883        "-C",
884        "link-args=--fatal-warnings",
885    ];
886
887    #[test]
888    fn encodes_rustc_flags() {
889        let guest_meta = GuestMetadata {
890            rustc_flags: Some(RUSTC_FLAGS.iter().map(ToString::to_string).collect()),
891            ..Default::default()
892        };
893        let encoded = encode_rust_flags(&guest_meta, false);
894        let expected = [
895            "--cfg",
896            "foo=\"bar\"",
897            "--cfg",
898            "foo='bar'",
899            "-C",
900            "link-args=--fatal-warnings",
901        ]
902        .join("\x1f");
903        assert!(encoded.contains(&expected));
904    }
905
906    #[test]
907    fn escapes_strings_when_encoding_when_requested() {
908        let guest_meta = GuestMetadata {
909            rustc_flags: Some(RUSTC_FLAGS.iter().map(ToString::to_string).collect()),
910            ..Default::default()
911        };
912        let encoded = encode_rust_flags(&guest_meta, true);
913        let expected = [
914            "--cfg",
915            "foo=\\\"bar\\\"",
916            "--cfg",
917            "foo=\\\'bar\\\'",
918            "-C",
919            "link-args=--fatal-warnings",
920        ]
921        .join("\x1f");
922        assert!(encoded.contains(&expected));
923    }
924}