1use std::{
2 env::var,
3 fs::{copy, create_dir_all, read},
4 path::PathBuf,
5};
6
7use clap::Parser;
8use eyre::Result;
9use itertools::izip;
10use openvm_build::{
11 build_generic, get_package, get_workspace_packages, get_workspace_root, GuestOptions,
12};
13use openvm_circuit::arch::{
14 instructions::exe::VmExe, InitFileGenerator, OPENVM_DEFAULT_INIT_FILE_NAME,
15};
16use openvm_sdk::{config::TranspilerConfig, fs::write_object_to_file};
17use openvm_transpiler::{elf::Elf, openvm_platform::memory::MEM_SIZE, FromElf};
18
19use crate::util::{
20 get_manifest_path_and_dir, get_target_dir, get_target_output_dir, read_config_toml_or_default,
21};
22
23#[derive(Parser)]
24#[command(name = "build", about = "Compile an OpenVM program")]
25pub struct BuildCmd {
26 #[clap(flatten)]
27 build_args: BuildArgs,
28
29 #[clap(flatten)]
30 cargo_args: BuildCargoArgs,
31}
32
33impl BuildCmd {
34 pub fn run(&self) -> Result<()> {
35 build(&self.build_args, &self.cargo_args)?;
36 Ok(())
37 }
38}
39
40#[derive(Clone, Parser)]
41pub struct BuildArgs {
42 #[arg(
43 long,
44 help = "Skips transpilation into exe when set",
45 help_heading = "OpenVM Options"
46 )]
47 pub no_transpile: bool,
48
49 #[arg(
50 long,
51 help = "Path to the OpenVM config .toml file that specifies the VM extensions, by default will search for the file at ${manifest_dir}/openvm.toml",
52 help_heading = "OpenVM Options"
53 )]
54 pub config: Option<PathBuf>,
55
56 #[arg(
57 long,
58 help = "Output directory that OpenVM proving artifacts will be copied to",
59 help_heading = "OpenVM Options"
60 )]
61 pub output_dir: Option<PathBuf>,
62
63 #[arg(
64 long,
65 default_value = OPENVM_DEFAULT_INIT_FILE_NAME,
66 help = "Name of the init file",
67 help_heading = "OpenVM Options"
68 )]
69 pub init_file_name: String,
70}
71
72impl Default for BuildArgs {
73 fn default() -> Self {
74 Self {
75 no_transpile: false,
76 config: None,
77 output_dir: None,
78 init_file_name: OPENVM_DEFAULT_INIT_FILE_NAME.to_string(),
79 }
80 }
81}
82
83#[derive(Clone, Parser)]
84pub struct BuildCargoArgs {
85 #[arg(
86 long,
87 short = 'p',
88 value_name = "PACKAGES",
89 help = "Build only specified packages",
90 help_heading = "Package Selection"
91 )]
92 pub package: Vec<String>,
93
94 #[arg(
95 long,
96 alias = "all",
97 help = "Build all members of the workspace",
98 help_heading = "Package Selection"
99 )]
100 pub workspace: bool,
101
102 #[arg(
103 long,
104 value_name = "PACKAGES",
105 help = "Exclude specified packages",
106 help_heading = "Package Selection"
107 )]
108 pub exclude: Vec<String>,
109
110 #[arg(
111 long,
112 help = "Build the package library",
113 help_heading = "Target Selection"
114 )]
115 pub lib: bool,
116
117 #[arg(
118 long,
119 value_name = "BIN",
120 help = "Build the specified binary",
121 help_heading = "Target Selection"
122 )]
123 pub bin: Vec<String>,
124
125 #[arg(
126 long,
127 help = "Build all binary targets",
128 help_heading = "Target Selection"
129 )]
130 pub bins: bool,
131
132 #[arg(
133 long,
134 value_name = "EXAMPLE",
135 help = "Build the specified example",
136 help_heading = "Target Selection"
137 )]
138 pub example: Vec<String>,
139
140 #[arg(
141 long,
142 help = "Build all example targets",
143 help_heading = "Target Selection"
144 )]
145 pub examples: bool,
146
147 #[arg(
148 long,
149 help = "Build all package targets",
150 help_heading = "Target Selection"
151 )]
152 pub all_targets: bool,
153
154 #[arg(
155 long,
156 short = 'F',
157 value_name = "FEATURES",
158 value_delimiter = ',',
159 help = "Space/comma separated list of features to activate",
160 help_heading = "Feature Selection"
161 )]
162 pub features: Vec<String>,
163
164 #[arg(
165 long,
166 help = "Activate all available features of all selected packages",
167 help_heading = "Feature Selection"
168 )]
169 pub all_features: bool,
170
171 #[arg(
172 long,
173 help = "Do not activate the `default` feature of the selected packages",
174 help_heading = "Feature Selection"
175 )]
176 pub no_default_features: bool,
177
178 #[arg(
179 long,
180 value_name = "NAME",
181 default_value = "release",
182 help = "Build with the given profile",
183 help_heading = "Compilation Options"
184 )]
185 pub profile: String,
186
187 #[arg(
188 long,
189 value_name = "DIR",
190 help = "Directory for all generated artifacts and intermediate files",
191 help_heading = "Output Options"
192 )]
193 pub target_dir: Option<PathBuf>,
194
195 #[arg(
196 long,
197 short = 'v',
198 help = "Use verbose output",
199 help_heading = "Display Options"
200 )]
201 pub verbose: bool,
202
203 #[arg(
204 long,
205 short = 'q',
206 help = "Do not print cargo log messages",
207 help_heading = "Display Options"
208 )]
209 pub quiet: bool,
210
211 #[arg(
212 long,
213 value_name = "WHEN",
214 default_value = "always",
215 help = "Control when colored output is used",
216 help_heading = "Display Options"
217 )]
218 pub color: String,
219
220 #[arg(
221 long,
222 value_name = "PATH",
223 help = "Path to the Cargo.toml file, by default searches for the file in the current or any parent directory",
224 help_heading = "Manifest Options"
225 )]
226 pub manifest_path: Option<PathBuf>,
227
228 #[arg(
229 long,
230 help = "Ignore rust-version specification in packages",
231 help_heading = "Manifest Options"
232 )]
233 pub ignore_rust_version: bool,
234
235 #[arg(
236 long,
237 help = "Asserts same dependencies and versions are used as when the existing Cargo.lock file was originally generated",
238 help_heading = "Manifest Options"
239 )]
240 pub locked: bool,
241
242 #[arg(
243 long,
244 help = "Prevents Cargo from accessing the network for any reason",
245 help_heading = "Manifest Options"
246 )]
247 pub offline: bool,
248
249 #[arg(
250 long,
251 help = "Equivalent to specifying both --locked and --offline",
252 help_heading = "Manifest Options"
253 )]
254 pub frozen: bool,
255}
256
257impl Default for BuildCargoArgs {
258 fn default() -> Self {
259 Self {
260 package: vec![],
261 workspace: false,
262 exclude: vec![],
263 lib: false,
264 bin: vec![],
265 bins: false,
266 example: vec![],
267 examples: false,
268 all_targets: false,
269 features: vec![],
270 all_features: false,
271 no_default_features: false,
272 profile: "release".to_string(),
273 target_dir: None,
274 verbose: false,
275 quiet: false,
276 color: "always".to_string(),
277 manifest_path: None,
278 ignore_rust_version: false,
279 locked: false,
280 offline: false,
281 frozen: false,
282 }
283 }
284}
285
286pub fn build(build_args: &BuildArgs, cargo_args: &BuildCargoArgs) -> Result<PathBuf> {
289 println!("[openvm] Building the package...");
290
291 let (manifest_path, manifest_dir) = get_manifest_path_and_dir(&cargo_args.manifest_path)?;
293 let target_dir = get_target_dir(&cargo_args.target_dir, &manifest_path);
294
295 let mut guest_options = GuestOptions::default()
297 .with_features(cargo_args.features.clone())
298 .with_profile(cargo_args.profile.clone())
299 .with_rustc_flags(var("RUSTFLAGS").unwrap_or_default().split_whitespace());
300
301 guest_options.target_dir = Some(target_dir.clone());
302 guest_options
303 .options
304 .push(format!("--color={}", cargo_args.color));
305 guest_options.options.push("--manifest-path".to_string());
306 guest_options
307 .options
308 .push(manifest_path.to_string_lossy().to_string());
309
310 for pkg in &cargo_args.package {
311 guest_options.options.push("--package".to_string());
312 guest_options.options.push(pkg.clone());
313 }
314 for pkg in &cargo_args.exclude {
315 guest_options.options.push("--exclude".to_string());
316 guest_options.options.push(pkg.clone());
317 }
318 for target in &cargo_args.bin {
319 guest_options.options.push("--bin".to_string());
320 guest_options.options.push(target.clone());
321 }
322 for example in &cargo_args.example {
323 guest_options.options.push("--example".to_string());
324 guest_options.options.push(example.clone());
325 }
326
327 let all_bins = cargo_args.bins || cargo_args.all_targets;
328 let all_examples = cargo_args.examples || cargo_args.all_targets;
329
330 let boolean_flags = [
331 ("--workspace", cargo_args.workspace),
332 ("--lib", cargo_args.lib || cargo_args.all_targets),
333 ("--bins", all_bins),
334 ("--examples", all_examples),
335 ("--all-features", cargo_args.all_features),
336 ("--no-default-features", cargo_args.no_default_features),
337 ("--verbose", cargo_args.verbose),
338 ("--quiet", cargo_args.quiet),
339 ("--ignore-rust-version", cargo_args.ignore_rust_version),
340 ("--locked", cargo_args.locked),
341 ("--offline", cargo_args.offline),
342 ("--frozen", cargo_args.frozen),
343 ];
344 for (flag, enabled) in boolean_flags {
345 if enabled {
346 guest_options.options.push(flag.to_string());
347 }
348 }
349
350 let app_config = read_config_toml_or_default(
352 build_args
353 .config
354 .to_owned()
355 .unwrap_or_else(|| manifest_dir.join("openvm.toml")),
356 )?;
357 app_config
358 .app_vm_config
359 .write_to_init_file(&manifest_dir, Some(&build_args.init_file_name))?;
360
361 let elf_target_dir = match build_generic(&guest_options) {
363 Ok(raw_target_dir) => raw_target_dir,
364 Err(None) => {
365 return Err(eyre::eyre!("Failed to build guest"));
366 }
367 Err(Some(code)) => {
368 return Err(eyre::eyre!("Failed to build guest: code = {}", code));
369 }
370 };
371 println!("[openvm] Successfully built the packages");
372
373 if build_args.no_transpile {
375 if build_args.output_dir.is_some() {
376 println!("[openvm] WARNING: Output directory set but transpilation skipped");
377 }
378 return Ok(elf_target_dir);
379 }
380
381 let workspace_root = get_workspace_root(&manifest_path);
383 let packages = if cargo_args.workspace || manifest_dir == workspace_root {
384 get_workspace_packages(manifest_dir)
385 .into_iter()
386 .filter(|pkg| {
387 (cargo_args.package.is_empty() || cargo_args.package.contains(&pkg.name))
388 && !cargo_args.exclude.contains(&pkg.name)
389 })
390 .collect()
391 } else {
392 vec![get_package(manifest_dir)]
393 };
394
395 let elf_targets = packages
397 .iter()
398 .flat_map(|pkg| pkg.targets.iter())
399 .filter(|target| {
400 if target.is_example() {
404 all_examples || cargo_args.example.contains(&target.name)
405 } else if target.is_bin() {
406 all_bins
407 || cargo_args.bin.contains(&target.name)
408 || (!cargo_args.examples
409 && !cargo_args.lib
410 && cargo_args.bin.is_empty()
411 && cargo_args.example.is_empty())
412 } else {
413 false
414 }
415 })
416 .collect::<Vec<_>>();
417 let elf_paths = elf_targets
418 .iter()
419 .map(|target| {
420 if target.is_example() {
421 elf_target_dir.join("examples")
422 } else {
423 elf_target_dir.clone()
424 }
425 .join(&target.name)
426 })
427 .collect::<Vec<_>>();
428
429 let target_output_dir = get_target_output_dir(&target_dir, &cargo_args.profile);
431
432 println!("[openvm] Transpiling the package...");
433 for (elf_path, target) in izip!(&elf_paths, &elf_targets) {
434 let transpiler = app_config.app_vm_config.transpiler();
435 let data = read(elf_path.clone())?;
436 let elf = Elf::decode(&data, MEM_SIZE as u32)?;
437 let exe = VmExe::from_elf(elf, transpiler)?;
438
439 let target_name = if target.is_example() {
440 PathBuf::from("examples").join(&target.name)
441 } else {
442 PathBuf::from(&target.name)
443 };
444 let file_name = target_name.with_extension("vmexe");
445 let file_path = target_output_dir.join(&file_name);
446
447 write_object_to_file(&file_path, exe)?;
448 if let Some(output_dir) = &build_args.output_dir {
449 create_dir_all(output_dir)?;
450 copy(file_path, output_dir.join(file_name))?;
451 }
452 }
453
454 let final_output_dir = if let Some(output_dir) = &build_args.output_dir {
455 output_dir
456 } else {
457 &target_output_dir
458 };
459 println!(
460 "[openvm] Successfully transpiled to {}",
461 final_output_dir.display()
462 );
463 Ok(final_output_dir.clone())
464}