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
246pub fn build(build_args: &BuildArgs, cargo_args: &BuildCargoArgs) -> Result<PathBuf> {
249 println!("[openvm] Building the package...");
250
251 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 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 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 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 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 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 let elf_targets = packages
360 .iter()
361 .flat_map(|pkg| pkg.targets.iter())
362 .filter(|target| {
363 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 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}