cargo_openvm/commands/
build.rs

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
286// Returns either a) the default transpilation output directory or b) the ELF output
287// directory if no_transpile is set to true.
288pub fn build(build_args: &BuildArgs, cargo_args: &BuildCargoArgs) -> Result<PathBuf> {
289    println!("[openvm] Building the package...");
290
291    // Find manifest_path, manifest_dir, and target_dir
292    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    // Set guest options using build arguments; use found manifest directory for consistency
296    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    // Write to init file
351    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    // Build (allowing passed options to decide what gets built)
362    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 transpilation is skipped, return the raw target directory
374    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    // Get all built packages
382    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    // Find elf paths of all targets for all built packages
396    let elf_targets = packages
397        .iter()
398        .flat_map(|pkg| pkg.targets.iter())
399        .filter(|target| {
400            // We only build bin and example targets (note they are mutually exclusive
401            // types). If no target selection flags are set, then all bin targets are
402            // built by default.
403            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    // Transpile, storing in ${target_dir}/openvm/${profile} by default
430    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}