1use 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
35pub const TARGET_DIR: &str = "target/riscv-guest/riscv32im-risc0-zkvm-elf/docker";
37
38pub enum BuildStatus {
40 Success,
42 Skipped,
44}
45
46pub 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
112fn 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 .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
189fn 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#[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]
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}