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.88.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    // Canonicalize the manifest directory specified by the user.
245    let manifest_dir =
246        fs::canonicalize(manifest_dir.as_ref()).expect("could not canonicalize manifest path");
247    let manifest_path = manifest_dir.join("Cargo.toml");
248    let manifest_meta = MetadataCommand::new()
249        .manifest_path(&manifest_path)
250        .no_deps()
251        .exec()
252        .expect("cargo metadata command failed");
253    let mut matching: Vec<Package> = manifest_meta
254        .packages
255        .into_iter()
256        .filter(|pkg| {
257            let std_path: &Path = pkg.manifest_path.as_ref();
258            std_path == manifest_path
259        })
260        .collect();
261    if matching.is_empty() {
262        eprintln!("ERROR: No package found in {manifest_dir:?}");
263        std::process::exit(-1);
264    }
265    if matching.len() > 1 {
266        eprintln!("ERROR: Multiple packages found in {manifest_dir:?}",);
267        std::process::exit(-1);
268    }
269    matching.pop().unwrap()
270}
271
272/// Determines and returns the build target directory from the Cargo manifest at
273/// the given `manifest_path`.
274pub fn get_target_dir(manifest_path: impl AsRef<Path>) -> PathBuf {
275    MetadataCommand::new()
276        .manifest_path(manifest_path.as_ref())
277        .no_deps()
278        .exec()
279        .expect("cargo metadata command failed")
280        .target_directory
281        .into()
282}
283
284/// When called from a build.rs, returns the current package being built.
285fn current_package() -> Package {
286    get_package(env::var("CARGO_MANIFEST_DIR").unwrap())
287}
288
289/// Returns all inner packages specified the "methods" list inside
290/// "package.metadata.risc0".
291fn guest_packages(pkg: &Package) -> Vec<Package> {
292    let manifest_dir = pkg.manifest_path.parent().unwrap();
293    Risc0Metadata::from_package(pkg)
294        .unwrap()
295        .methods
296        .iter()
297        .map(|inner| get_package(manifest_dir.join(inner)))
298        .collect()
299}
300
301fn is_debug() -> bool {
302    get_env_var("RISC0_BUILD_DEBUG") == "1"
303}
304
305fn is_skip_build() -> bool {
306    !get_env_var("RISC0_SKIP_BUILD").is_empty()
307}
308
309fn get_env_var(name: &str) -> String {
310    let ret = env::var(name).unwrap_or_default();
311    if let Some(pkg) = env::var_os("CARGO_PKG_NAME") {
312        if pkg != "cargo-risczero" {
313            println!("cargo:rerun-if-env-changed={name}");
314        }
315    }
316    ret
317}
318
319/// Returns all methods associated with the given guest crate.
320fn guest_methods<G: GuestBuilder>(
321    pkg: &Package,
322    target_dir: impl AsRef<Path>,
323    guest_info: &GuestInfo,
324    profile: &str,
325) -> Vec<G> {
326    pkg.targets
327        .iter()
328        .filter(|target| target.is_bin())
329        .filter(|target| {
330            target
331                .required_features
332                .iter()
333                .all(|required_feature| guest_info.options.features.contains(required_feature))
334        })
335        .map(|target| {
336            G::build(
337                guest_info,
338                &target.name,
339                target_dir
340                    .as_ref()
341                    .join(RISC0_TARGET_TRIPLE)
342                    .join(profile)
343                    .join(&target.name)
344                    .to_str()
345                    .context("elf path contains invalid unicode")
346                    .unwrap(),
347            )
348            .unwrap()
349        })
350        .collect()
351}
352
353/// Build a [Command] with CARGO and RUSTUP_TOOLCHAIN environment variables
354/// removed.
355fn sanitized_cmd(tool: &str) -> Command {
356    let mut cmd = Command::new(tool);
357    for (key, _val) in env::vars().filter(|x| x.0.starts_with("CARGO")) {
358        cmd.env_remove(key);
359    }
360    cmd.env_remove("RUSTUP_TOOLCHAIN");
361    cmd
362}
363
364fn cpp_toolchain() -> Option<PathBuf> {
365    let rzup = rzup::Rzup::new().unwrap();
366    let (version, path) = rzup
367        .get_default_version(&rzup::Component::CppToolchain)
368        .unwrap()?;
369    println!("Using C++ toolchain version {version}");
370    Some(path)
371}
372
373fn rust_toolchain() -> PathBuf {
374    let rzup = rzup::Rzup::new().unwrap();
375    let Some((version, path)) = rzup
376        .get_default_version(&rzup::Component::RustToolchain)
377        .unwrap()
378    else {
379        panic!("Risc Zero Rust toolchain not found. Try running `rzup install rust`");
380    };
381    println!("Using Rust toolchain version {version}");
382    path
383}
384
385/// Creates a std::process::Command to execute the given cargo
386/// command in an environment suitable for targeting the zkvm guest.
387#[stability::unstable]
388pub fn cargo_command(subcmd: &str, rustc_flags: &[String]) -> Command {
389    let mut guest_info = GuestInfo::default();
390    guest_info.metadata.rustc_flags = Some(rustc_flags.to_vec());
391    cargo_command_internal(subcmd, &guest_info)
392}
393
394pub(crate) fn cargo_command_internal(subcmd: &str, guest_info: &GuestInfo) -> Command {
395    let rustc = rust_toolchain().join("bin/rustc");
396    println!("Using rustc: {}", rustc.display());
397
398    let mut cmd = sanitized_cmd("cargo");
399    let mut args = vec![subcmd, "--target", RISC0_TARGET_TRIPLE];
400
401    if !get_env_var("RISC0_BUILD_LOCKED").is_empty() {
402        args.push("--locked");
403    }
404
405    let rust_src = get_env_var("RISC0_RUST_SRC");
406    if !rust_src.is_empty() {
407        args.push("-Z");
408        args.push("build-std=alloc,core,proc_macro,panic_abort,std");
409        args.push("-Z");
410        args.push("build-std-features=compiler-builtins-mem");
411        cmd.env("__CARGO_TESTS_ONLY_SRC_ROOT", rust_src);
412    }
413
414    let encoded_rust_flags = encode_rust_flags(&guest_info.metadata, false);
415
416    if !cpp_toolchain_override() {
417        if let Some(toolchain_path) = cpp_toolchain() {
418            cmd.env("CC", toolchain_path.join("bin/riscv32-unknown-elf-gcc"));
419        } else {
420            // If you aren't compiling any C/C++ code, it might be just fine to not have a C++
421            // toolchain installed, but if you are then your compilation will surely fail. To avoid
422            // a potentially confusing error message, set the CC path to a bogus path that will
423            // hopefully make the issue obvious.
424            cmd.env(
425                "CC",
426                "/no_risc0_cpp_toolchain_installed_run_rzup_install_cpp",
427            );
428        }
429
430        cmd.env("CFLAGS_riscv32im_risc0_zkvm_elf", "-march=rv32im -nostdlib");
431
432        // Signal to dependencies, cryptography patches in particular, that the bigint2 zkVM
433        // feature is available.
434        cmd.env("RISC0_FEATURE_bigint2", "");
435    }
436
437    cmd.env("RUSTC", rustc)
438        .env("CARGO_ENCODED_RUSTFLAGS", encoded_rust_flags)
439        .args(args);
440    cmd
441}
442
443fn get_rust_toolchain_version() -> semver::Version {
444    let rzup = rzup::Rzup::new().unwrap();
445    let Some((version, _)) = rzup
446        .get_default_version(&rzup::Component::RustToolchain)
447        .unwrap()
448    else {
449        panic!("Risc Zero Rust toolchain not found. Try running `rzup install rust`");
450    };
451    version
452}
453
454/// Returns a string that can be set as the value of CARGO_ENCODED_RUSTFLAGS when compiling guests
455pub(crate) fn encode_rust_flags(guest_meta: &GuestMetadata, escape_special_chars: bool) -> String {
456    // llvm changed `loweratomic` to `lower-atomic`
457    let lower_atomic = if get_rust_toolchain_version() > semver::Version::new(1, 81, 0) {
458        "passes=lower-atomic"
459    } else {
460        "passes=loweratomic"
461    };
462    let rustc_flags = guest_meta.rustc_flags.clone().unwrap_or_default();
463    let rustc_flags: Vec<_> = rustc_flags.iter().map(|s| s.as_str()).collect();
464    let text_addr = if guest_meta.kernel {
465        KERNEL_START_ADDR.0
466    } else {
467        memory::TEXT_START
468    };
469    [
470        // Append other rust flags
471        rustc_flags.as_slice(),
472        &[
473            // Replace atomic ops with nonatomic versions since the guest is single threaded.
474            "-C",
475            lower_atomic,
476            // Specify where to start loading the program in
477            // memory.  The clang linker understands the same
478            // command line arguments as the GNU linker does; see
479            // https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html#SEC3
480            // for details.
481            "-C",
482            &format!("link-arg=-Ttext={text_addr:#010x}"),
483            // Apparently not having an entry point is only a linker warning(!), so
484            // error out in this case.
485            "-C",
486            "link-arg=--fatal-warnings",
487            "-C",
488            "panic=abort",
489            "--cfg",
490            "getrandom_backend=\"custom\"",
491        ],
492    ]
493    .concat()
494    .iter()
495    .map(|x| {
496        if escape_special_chars {
497            x.escape_default().to_string()
498        } else {
499            x.to_string()
500        }
501    })
502    .collect::<Vec<String>>()
503    .join("\x1f")
504}
505
506fn cpp_toolchain_override() -> bool {
507    // detect if there's an attempt to override the Cpp toolchain.
508    // Overriding the toolchain useful for troubleshooting crates.
509    !get_env_var("CC_riscv32im_risc0_zkvm_elf").is_empty()
510        || !get_env_var("CFLAGS_riscv32im_risc0_zkvm_elf").is_empty()
511}
512
513/// Builds a static library providing a rust runtime.
514///
515/// This can be used to build programs for the zkvm which don't depend on risc0_zkvm.
516pub fn build_rust_runtime() -> String {
517    build_rust_runtime_with_features(&[])
518}
519
520/// Builds a static library providing a rust runtime, with additional features given as arguments.
521///
522/// This can be used to build programs for the zkvm which don't depend on risc0_zkvm. Feature flags
523/// given will be pass when building risc0-zkvm-platform.
524pub fn build_rust_runtime_with_features(features: &[&str]) -> String {
525    build_staticlib(
526        "risc0-zkvm-platform",
527        &[&["rust-runtime", "panic-handler", "entrypoint"], features].concat(),
528    )
529}
530
531/// Builds a static library and returns the name of the resultant file.
532fn build_staticlib(guest_pkg: &str, features: &[&str]) -> String {
533    let guest_dir = get_guest_dir("static-lib", guest_pkg);
534
535    let guest_info = GuestInfo::default();
536    let mut cmd = cargo_command_internal("rustc", &guest_info);
537
538    if !is_debug() {
539        cmd.arg("--release");
540    }
541
542    // Add args to specify the package to be built, and to build is as a staticlib.
543    cmd.args([
544        "--package",
545        guest_pkg,
546        "--target-dir",
547        guest_dir.to_str().unwrap(),
548        "--lib",
549        "--message-format=json",
550        "--crate-type=staticlib",
551    ]);
552
553    for feature in features {
554        cmd.args(["--features", &(guest_pkg.to_owned() + "/" + feature)]);
555    }
556
557    eprintln!("Building staticlib: {cmd:?}");
558
559    // Run the build command and extract the name of the resulting staticlib
560    // artifact.
561    let mut child = cmd.stdout(Stdio::piped()).spawn().unwrap();
562    let reader = std::io::BufReader::new(child.stdout.take().unwrap());
563    let mut libs = Vec::new();
564    for message in Message::parse_stream(reader) {
565        match message.unwrap() {
566            Message::CompilerArtifact(artifact) => {
567                for filename in artifact.filenames {
568                    if let Some("a") = filename.extension() {
569                        libs.push(filename.to_string());
570                    }
571                }
572            }
573            Message::CompilerMessage(msg) => {
574                eprint!("{msg}");
575            }
576            _ => (),
577        }
578    }
579
580    let output = child.wait().expect("Couldn't get cargo's exit status");
581    if !output.success() {
582        panic!("Unable to build static library")
583    }
584
585    match libs.as_slice() {
586        [] => panic!("No static library was built"),
587        [lib] => lib.to_string(),
588        _ => panic!("Multiple static libraries found: {:?}", libs.as_slice()),
589    }
590}
591
592// HACK: Attempt to bypass the parent cargo output capture and
593// send directly to the tty, if available.  This way we get
594// progress messages from the inner cargo so the user doesn't
595// think it's just hanging.
596fn tty_println(msg: &str) {
597    let tty_file = env::var("RISC0_GUEST_LOGFILE").unwrap_or_else(|_| "/dev/tty".to_string());
598
599    let mut tty = fs::OpenOptions::new()
600        .read(true)
601        .write(true)
602        .create(true)
603        .truncate(false)
604        .open(tty_file)
605        .ok();
606
607    if let Some(tty) = &mut tty {
608        writeln!(tty, "{msg}").unwrap();
609    } else {
610        eprintln!("{msg}");
611    }
612}
613
614// Builds a package that targets the riscv guest into the specified target
615// directory.
616fn build_guest_package(pkg: &Package, target_dir: impl AsRef<Path>, guest_info: &GuestInfo) {
617    if is_skip_build() {
618        return;
619    }
620
621    let target_dir = target_dir.as_ref();
622    fs::create_dir_all(target_dir).unwrap();
623
624    let mut cmd = cargo_command_internal("build", guest_info);
625
626    let features_str = guest_info.options.features.join(",");
627    if !features_str.is_empty() {
628        cmd.args(["--features", &features_str]);
629    }
630
631    cmd.args([
632        "--manifest-path",
633        pkg.manifest_path.as_str(),
634        "--target-dir",
635        target_dir.to_str().unwrap(),
636    ]);
637
638    if !is_debug() {
639        cmd.args(["--release"]);
640    }
641
642    let mut child = cmd
643        .stderr(Stdio::piped())
644        .spawn()
645        .expect("cargo build failed");
646    let stderr = child.stderr.take().unwrap();
647
648    tty_println(&format!(
649        "{}: Starting build for {RISC0_TARGET_TRIPLE}",
650        pkg.name
651    ));
652
653    for line in BufReader::new(stderr).lines() {
654        tty_println(&format!("{}: {}", pkg.name, line.unwrap()));
655    }
656
657    let res = child.wait().expect("Guest 'cargo build' failed");
658    if !res.success() {
659        std::process::exit(res.code().unwrap());
660    }
661}
662
663fn get_out_dir() -> PathBuf {
664    // This code is based on https://docs.rs/cxx-build/latest/src/cxx_build/target.rs.html#10-49
665
666    if let Some(target_dir) = env::var_os("CARGO_TARGET_DIR").map(Into::<PathBuf>::into) {
667        if target_dir.is_absolute() {
668            return target_dir.join("riscv-guest");
669        }
670    }
671
672    let mut dir: PathBuf = env::var_os("OUT_DIR").unwrap().into();
673    loop {
674        if dir.join(".rustc_info.json").exists()
675            || dir.join("CACHEDIR.TAG").exists()
676            || dir.file_name() == Some(OsStr::new("target"))
677                && dir
678                    .parent()
679                    .is_some_and(|parent| parent.join("Cargo.toml").exists())
680        {
681            return dir.join("riscv-guest");
682        }
683        if dir.pop() {
684            continue;
685        }
686        panic!("Cannot find cargo target dir location")
687    }
688}
689
690fn get_guest_dir(host_pkg: impl AsRef<Path>, guest_pkg: impl AsRef<Path>) -> PathBuf {
691    get_out_dir().join(host_pkg).join(guest_pkg)
692}
693
694/// Embeds methods built for RISC-V for use by host-side dependencies.
695/// Specify custom options for a guest package by defining its [GuestOptions].
696/// See [embed_methods].
697pub fn embed_methods_with_options(
698    guest_pkg_to_options: HashMap<&str, GuestOptions>,
699) -> Vec<GuestListEntry> {
700    do_embed_methods(guest_pkg_to_options)
701}
702
703/// Build methods for RISC-V and embed minimal metadata - the `elf` name and path.
704/// To embed the full elf, use [embed_methods_with_options].
705///
706/// Use this option if you wish to import the guest `elf` into your prover at runtime
707/// rather than embedding it into the prover binary. This reduces build times for large
708/// binaries, but makes prover initialization fallible.
709///
710/// Specify custom options for the package by defining its [GuestOptions].
711pub fn embed_method_metadata_with_options(
712    guest_pkg_to_options: HashMap<&str, GuestOptions>,
713) -> Vec<MinGuestListEntry> {
714    do_embed_methods(guest_pkg_to_options)
715}
716
717struct GuestPackageWithOptions {
718    name: String,
719    pkg: Package,
720    opts: GuestOptions,
721    target_dir: PathBuf,
722}
723
724/// Embeds methods built for RISC-V for use by host-side dependencies.
725/// Specify custom options for a guest package by defining its [GuestOptions].
726/// See [embed_methods].
727fn do_embed_methods<G: GuestBuilder>(mut guest_opts: HashMap<&str, GuestOptions>) -> Vec<G> {
728    // Read the cargo metadata for info from `[package.metadata.risc0]`.
729    let pkg = current_package();
730    let guest_packages = guest_packages(&pkg);
731
732    let mut pkg_opts = vec![];
733    for guest_pkg in guest_packages {
734        let guest_dir = get_guest_dir(&pkg.name, &guest_pkg.name);
735        let opts = guest_opts
736            .remove(guest_pkg.name.as_str())
737            .unwrap_or_default();
738        pkg_opts.push(GuestPackageWithOptions {
739            name: format!("{}.{}", pkg.name, guest_pkg.name),
740            pkg: guest_pkg,
741            opts,
742            target_dir: guest_dir,
743        });
744    }
745
746    // If the user provided options for a package that wasn't built, abort.
747    if let Some(package) = guest_opts.keys().next() {
748        panic!("Error: guest options were provided for package {package:?} but the package was not built.");
749    }
750
751    build_methods(&pkg_opts)
752}
753
754fn build_methods<G: GuestBuilder>(guest_packages: &[GuestPackageWithOptions]) -> Vec<G> {
755    let out_dir_env = env::var_os("OUT_DIR").unwrap();
756    let out_dir = Path::new(&out_dir_env); // $ROOT/target/$profile/build/$crate/out
757
758    let methods_path = out_dir.join("methods.rs");
759    let mut methods_file = File::create(&methods_path).unwrap();
760
761    // NOTE: Codegen of the guest list is gated behind the "guest-list" feature flag,
762    // although the data structure are not, because when the `GuestListEntry` type
763    // is referenced in the generated code, this requires `risc0-build` be declared
764    // as a dependency of the methods crate.
765    #[cfg(feature = "guest-list")]
766    let mut guest_list_codegen = Vec::new();
767    #[cfg(feature = "guest-list")]
768    methods_file
769        .write_all(b"use risc0_build::GuestListEntry;\n")
770        .unwrap();
771
772    let profile = if is_debug() { "debug" } else { "release" };
773
774    let mut guest_list = vec![];
775    for guest in guest_packages {
776        println!("Building guest package: {}", guest.name);
777
778        let guest_info = GuestInfo {
779            options: guest.opts.clone(),
780            metadata: (&guest.pkg).into(),
781        };
782
783        let methods: Vec<G> = if guest.opts.use_docker.is_some() {
784            build_guest_package_docker(&guest.pkg, &guest.target_dir, &guest_info).unwrap();
785            guest_methods(&guest.pkg, &guest.target_dir, &guest_info, "docker")
786        } else {
787            build_guest_package(&guest.pkg, &guest.target_dir, &guest_info);
788            guest_methods(&guest.pkg, &guest.target_dir, &guest_info, profile)
789        };
790
791        for method in methods {
792            methods_file
793                .write_all(method.codegen_consts().as_bytes())
794                .unwrap();
795
796            #[cfg(feature = "guest-list")]
797            guest_list_codegen.push(method.codegen_list_entry());
798            guest_list.push(method);
799        }
800    }
801
802    #[cfg(feature = "guest-list")]
803    methods_file
804        .write_all(
805            format!(
806                "\npub const GUEST_LIST: &[{}] = &[{}];\n",
807                std::any::type_name::<G>(),
808                guest_list_codegen.join(",")
809            )
810            .as_bytes(),
811        )
812        .unwrap();
813
814    // HACK: It's not particularly practical to figure out all the
815    // files that all the guest crates transitively depend on.  So, we
816    // want to run the guest "cargo build" command each time we build.
817    //
818    // Since we generate methods.rs each time we run, it will always
819    // be changed.
820    println!("cargo:rerun-if-changed={}", methods_path.display());
821    println!("cargo:rerun-if-env-changed=RISC0_GUEST_LOGFILE");
822
823    guest_list
824}
825
826/// Embeds methods built for RISC-V for use by host-side dependencies.
827///
828/// This method should be called from a package with a
829/// [package.metadata.risc0] section including a "methods" property
830/// listing the relative paths that contain riscv guest method
831/// packages.
832///
833/// To access the generated image IDs and ELF filenames, include the
834/// generated methods.rs:
835///
836/// ```text
837/// include!(concat!(env!("OUT_DIR"), "/methods.rs"));
838/// ```
839///
840/// To conform to rust's naming conventions, the constants are mapped
841/// to uppercase.  For instance, if you have a method named
842/// "my_method", the image ID and elf contents will be defined as
843/// "MY_METHOD_ID" and "MY_METHOD_ELF" respectively.
844pub fn embed_methods() -> Vec<GuestListEntry> {
845    embed_methods_with_options(HashMap::new())
846}
847
848/// Build a guest package into the specified `target_dir` using the specified
849/// `GuestOptions`.
850pub fn build_package(
851    pkg: &Package,
852    target_dir: impl AsRef<Path>,
853    options: GuestOptions,
854) -> Result<Vec<GuestListEntry>> {
855    println!("Building guest package: {}", pkg.name);
856
857    let guest_info = GuestInfo {
858        options: options.clone(),
859        metadata: pkg.into(),
860    };
861
862    let profile = if is_debug() { "debug" } else { "release" };
863
864    if options.use_docker.is_some() {
865        build_guest_package_docker(pkg, target_dir.as_ref(), &guest_info)?;
866        Ok(guest_methods(pkg, &target_dir, &guest_info, "docker"))
867    } else {
868        build_guest_package(pkg, &target_dir, &guest_info);
869        Ok(guest_methods(pkg, &target_dir, &guest_info, profile))
870    }
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876
877    const RUSTC_FLAGS: &[&str] = &[
878        "--cfg",
879        "foo=\"bar\"",
880        "--cfg",
881        "foo='bar'",
882        "-C",
883        "link-args=--fatal-warnings",
884    ];
885
886    #[test]
887    fn encodes_rustc_flags() {
888        let guest_meta = GuestMetadata {
889            rustc_flags: Some(RUSTC_FLAGS.iter().map(ToString::to_string).collect()),
890            ..Default::default()
891        };
892        let encoded = encode_rust_flags(&guest_meta, false);
893        let expected = [
894            "--cfg",
895            "foo=\"bar\"",
896            "--cfg",
897            "foo='bar'",
898            "-C",
899            "link-args=--fatal-warnings",
900        ]
901        .join("\x1f");
902        assert!(encoded.contains(&expected));
903    }
904
905    #[test]
906    fn escapes_strings_when_encoding_when_requested() {
907        let guest_meta = GuestMetadata {
908            rustc_flags: Some(RUSTC_FLAGS.iter().map(ToString::to_string).collect()),
909            ..Default::default()
910        };
911        let encoded = encode_rust_flags(&guest_meta, true);
912        let expected = [
913            "--cfg",
914            "foo=\\\"bar\\\"",
915            "--cfg",
916            "foo=\\\'bar\\\'",
917            "-C",
918            "link-args=--fatal-warnings",
919        ]
920        .join("\x1f");
921        assert!(encoded.contains(&expected));
922    }
923}