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