risc0_build/
docker.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
15use std::{fs, path::Path, process::Command};
16
17use anyhow::{bail, Context, Result};
18use cargo_metadata::Package;
19use docker_generate::DockerFile;
20use tempfile::tempdir;
21
22use crate::{
23    config::GuestInfo, encode_rust_flags, get_env_var, get_package, GuestOptions,
24    RISC0_TARGET_TRIPLE,
25};
26
27const DOCKER_IGNORE: &str = r#"
28**/Dockerfile
29**/.git
30**/node_modules
31**/target
32**/tmp
33"#;
34
35/// The target directory for the ELF binaries.
36pub const TARGET_DIR: &str = "target/riscv-guest/riscv32im-risc0-zkvm-elf/docker";
37
38/// Indicates whether the build was successful or skipped.
39pub enum BuildStatus {
40    /// The build was successful.
41    Success,
42    /// The build was skipped.
43    Skipped,
44}
45
46/// Build the package in the manifest path using a docker environment.
47pub fn docker_build(manifest_path: &Path, guest_opts: &GuestOptions) -> Result<BuildStatus> {
48    let manifest_dir = manifest_path.parent().unwrap().canonicalize().unwrap();
49    let pkg = get_package(manifest_dir);
50    let src_dir = guest_opts.use_docker.clone().unwrap_or_default().root_dir();
51    let guest_opts = guest_opts.clone();
52    let guest_info = GuestInfo {
53        options: guest_opts.clone(),
54        metadata: (&pkg).into(),
55    };
56    let pkg_name = pkg.name.replace('-', "_");
57    let target_dir = src_dir.join(TARGET_DIR).join(pkg_name);
58    build_guest_package_docker(&pkg, &target_dir, &guest_info)
59}
60
61pub(crate) fn build_guest_package_docker(
62    pkg: &Package,
63    target_dir: impl AsRef<Path>,
64    guest_info: &GuestInfo,
65) -> Result<BuildStatus> {
66    if !get_env_var("RISC0_SKIP_BUILD").is_empty() {
67        eprintln!("Skipping build because RISC0_SKIP_BUILD is set");
68        return Ok(BuildStatus::Skipped);
69    }
70
71    let src_dir = guest_info
72        .options
73        .use_docker
74        .clone()
75        .unwrap_or_default()
76        .root_dir()
77        .canonicalize()?;
78
79    eprintln!("Docker context: {src_dir:?}");
80    eprintln!(
81        "Building ELF binaries in {} for {RISC0_TARGET_TRIPLE} target...",
82        pkg.name
83    );
84
85    if !Command::new("docker")
86        .arg("--version")
87        .status()
88        .context("Could not find or execute docker")?
89        .success()
90    {
91        bail!("`docker --version` failed");
92    }
93
94    let manifest_path = pkg.manifest_path.as_std_path();
95    if let Err(err) = check_cargo_lock(manifest_path) {
96        eprintln!("{err}");
97    }
98
99    {
100        let temp_dir = tempdir()?;
101        let temp_path = temp_dir.path();
102        let rel_manifest_path = manifest_path.strip_prefix(&src_dir)?;
103        create_dockerfile(rel_manifest_path, temp_path, guest_info)?;
104        let target_dir = target_dir.as_ref();
105        let target_dir = target_dir.join(RISC0_TARGET_TRIPLE).join("docker");
106        build(&src_dir, temp_path, &target_dir)?;
107    }
108
109    Ok(BuildStatus::Success)
110}
111
112/// Create the dockerfile.
113///
114/// Overwrites if a dockerfile already exists.
115fn create_dockerfile(manifest_path: &Path, temp_dir: &Path, guest_info: &GuestInfo) -> Result<()> {
116    let manifest_env = &[("CARGO_MANIFEST_PATH", manifest_path.to_str().unwrap())];
117    let encoded_rust_flags = encode_rust_flags(&guest_info.metadata, true);
118    let rustflags_env = &[("CARGO_ENCODED_RUSTFLAGS", encoded_rust_flags.as_str())];
119
120    let common_args = vec![
121        "--locked",
122        "--target",
123        RISC0_TARGET_TRIPLE,
124        "--manifest-path",
125        "$CARGO_MANIFEST_PATH",
126    ];
127
128    let mut build_args = common_args.clone();
129    let features_str = guest_info.options.features.join(",");
130    if !guest_info.options.features.is_empty() {
131        build_args.push("--features");
132        build_args.push(&features_str);
133    }
134
135    let fetch_cmd = [&["cargo", "+risc0", "fetch"], common_args.as_slice()]
136        .concat()
137        .join(" ");
138    let build_cmd = [
139        &["cargo", "+risc0", "build", "--release"],
140        build_args.as_slice(),
141    ]
142    .concat()
143    .join(" ");
144
145    let docker_opts = guest_info.options.use_docker.clone().unwrap_or_default();
146    let docker_tag = format!(
147        "risczero/risc0-guest-builder:{}",
148        docker_opts.docker_container_tag()
149    );
150
151    let mut build = DockerFile::new()
152        .from_alias("build", &docker_tag)
153        .workdir("/src")
154        .copy(".", ".")
155        .env(manifest_env)
156        .env(rustflags_env)
157        .env(&[("CARGO_TARGET_DIR", "target")])
158        .env(&[("RISC0_FEATURE_bigint2", "")])
159        .env(&[(
160            "CC_riscv32im_risc0_zkvm_elf",
161            "/root/.risc0/cpp/bin/riscv32-unknown-elf-gcc",
162        )])
163        .env(&[("CFLAGS_riscv32im_risc0_zkvm_elf", "-march=rv32im -nostdlib")]);
164
165    let docker_env = docker_opts.env();
166    if !docker_env.is_empty() {
167        build = build.env(&docker_env);
168    }
169
170    build = build
171        // Fetching separately allows docker to cache the downloads, assuming the Cargo.lock
172        // doesn't change.
173        .run(&fetch_cmd)
174        .run(&build_cmd);
175
176    let src_dir = format!("/src/target/{RISC0_TARGET_TRIPLE}/release");
177    let binary = DockerFile::new()
178        .comment("export stage")
179        .from_alias("export", "scratch")
180        .copy_from("build", &src_dir, "/");
181
182    let file = DockerFile::new().dockerfile(build).dockerfile(binary);
183    fs::write(temp_dir.join("Dockerfile"), file.to_string())?;
184    fs::write(temp_dir.join("Dockerfile.dockerignore"), DOCKER_IGNORE)?;
185
186    Ok(())
187}
188
189/// Build the dockerfile and outputs the ELF.
190///
191/// Overwrites if an ELF with the same name already exists.
192fn build(src_dir: &Path, temp_dir: &Path, target_dir: &Path) -> Result<()> {
193    if Command::new("docker")
194        .arg("build")
195        .arg(format!("--output={}", target_dir.to_str().unwrap()))
196        .arg("-f")
197        .arg(temp_dir.join("Dockerfile"))
198        .arg(src_dir)
199        .status()
200        .context("docker failed to execute")?
201        .success()
202    {
203        Ok(())
204    } else {
205        Err(anyhow::anyhow!("docker build failed"))
206    }
207}
208
209fn check_cargo_lock(manifest_path: &Path) -> Result<()> {
210    let lock_file = manifest_path
211        .parent()
212        .context("invalid manifest path")?
213        .join("Cargo.lock");
214    fs::metadata(lock_file.clone()).context(format!(
215        "Cargo.lock not found in path {}",
216        lock_file.display()
217    ))?;
218    Ok(())
219}
220
221// requires Docker to be installed
222#[cfg(feature = "docker")]
223#[cfg(test)]
224mod test {
225    use crate::{build_package, DockerOptionsBuilder, GuestListEntry, GuestOptionsBuilder};
226
227    use super::*;
228
229    const SRC_DIR: &str = "../..";
230
231    fn build(target_dir: &Path, manifest_path: &str) -> Vec<GuestListEntry> {
232        let src_dir = Path::new(SRC_DIR).to_path_buf();
233        let manifest_path = Path::new(manifest_path);
234        let manifest_dir = manifest_path.parent().unwrap().canonicalize().unwrap();
235        let pkg = get_package(manifest_dir);
236        let docker_opts = DockerOptionsBuilder::default()
237            .root_dir(src_dir)
238            .build()
239            .unwrap();
240        let guest_opts = GuestOptionsBuilder::default()
241            .use_docker(docker_opts)
242            .build()
243            .unwrap();
244        build_package(&pkg, target_dir, guest_opts).unwrap()
245    }
246
247    fn compare_image_id(guest_list: &[GuestListEntry], name: &str, expected: &str) {
248        let guest = guest_list.iter().find(|x| x.name == name).unwrap();
249        assert_eq!(expected, guest.image_id.to_string());
250    }
251
252    // Test build reproducibility for risc0_zkvm_methods_guest.
253    // If the code of the package or any of its dependencies change,
254    // it may be required to recompute the expected image_ids.
255    // For that, run:
256    // `cargo run --bin cargo-risczero -- risczero build --manifest-path risc0/zkvm/methods/guest/Cargo.toml`
257    #[test]
258    fn test_reproducible_methods_guest() {
259        let temp_dir = tempdir().unwrap();
260        let temp_path = temp_dir.path();
261        let guest_list = build(temp_path, "../../risc0/zkvm/methods/guest/Cargo.toml");
262        compare_image_id(
263            &guest_list,
264            "hello_commit",
265            "5214467c35381b271ebcd373af45d508b86af9e4036b1a9db677caedd1afb290",
266        );
267    }
268}