loam_cli/commands/build/
mod.rs

1#![allow(clippy::struct_excessive_bools)]
2use cargo_metadata::{Metadata, MetadataCommand, Package};
3use clap::Parser;
4use itertools::Itertools;
5use std::{
6    collections::HashSet,
7    env,
8    ffi::OsStr,
9    fmt::Debug,
10    fs, io,
11    path::Path,
12    process::{Command, ExitStatus, Stdio},
13};
14
15pub mod clients;
16pub mod env_toml;
17
18/// Build a contract from source
19///
20/// Builds all crates that are referenced by the cargo manifest (Cargo.toml)
21/// that have cdylib as their crate-type. Crates are built for the wasm32
22/// target. Unless configured otherwise, crates are built with their default
23/// features and with their release profile.
24///
25/// To view the commands that will be executed, without executing them, use the
26/// --print-commands-only option.
27#[derive(Parser, Debug, Clone)]
28pub struct Cmd {
29    /// List package names
30    #[arg(long, visible_alias = "ls")]
31    pub list: bool,
32    /// Path to Cargo.toml
33    #[arg(long, default_value = "Cargo.toml")]
34    pub manifest_path: std::path::PathBuf,
35    /// Package to build
36    ///
37    /// If omitted, all packages that build for crate-type cdylib are built.
38    #[arg(long)]
39    pub package: Option<String>,
40    /// Build with the specified profile
41    #[arg(long)]
42    pub profile: Option<String>,
43    /// Build with the list of features activated, space or comma separated
44    #[arg(long, help_heading = "Features")]
45    pub features: Option<String>,
46    /// Build with the all features activated
47    #[arg(
48        long,
49        conflicts_with = "features",
50        conflicts_with = "no_default_features",
51        help_heading = "Features"
52    )]
53    pub all_features: bool,
54    /// Build with the default feature not activated
55    #[arg(long, help_heading = "Features")]
56    pub no_default_features: bool,
57    /// Directory to copy wasm files to
58    ///
59    /// If provided, wasm files can be found in the cargo target directory, and
60    /// the specified directory.
61    ///
62    /// If ommitted, wasm files are written only to `target/loam`.
63    #[arg(long)]
64    pub out_dir: Option<std::path::PathBuf>,
65    /// Print commands to build without executing them
66    #[arg(long, conflicts_with = "out_dir", help_heading = "Other")]
67    pub print_commands_only: bool,
68    /// Build client code in addition to building the contract
69    #[arg(long)]
70    pub build_clients: bool,
71    #[command(flatten)]
72    pub build_clients_args: clients::Args,
73}
74
75#[derive(thiserror::Error, Debug)]
76pub enum Error {
77    #[error(transparent)]
78    Metadata(#[from] cargo_metadata::Error),
79    #[error(transparent)]
80    CargoCmd(io::Error),
81    #[error("exit status {0}")]
82    Exit(ExitStatus),
83    #[error("package {package} not found")]
84    PackageNotFound { package: String },
85    #[error("creating out directory: {0}")]
86    CreatingOutDir(io::Error),
87    #[error("copying wasm file: {0}")]
88    CopyingWasmFile(io::Error),
89    #[error("getting the current directory: {0}")]
90    GettingCurrentDir(io::Error),
91    #[error(transparent)]
92    Loam(#[from] loam_build::deps::Error),
93    #[error(transparent)]
94    BuildClients(#[from] clients::Error),
95}
96
97impl Cmd {
98    pub fn list_packages(&self) -> Result<Vec<Package>, Error> {
99        let metadata = self.metadata()?;
100        let packages = self.packages(&metadata)?;
101        Ok(loam_build::deps::get_workspace(&packages)?)
102    }
103
104    pub async fn run(&self) -> Result<(), Error> {
105        let working_dir = env::current_dir().map_err(Error::GettingCurrentDir)?;
106        let metadata = self.metadata()?;
107        let packages = self.list_packages()?;
108        if self.list {
109            for p in packages {
110                println!("{}", p.name);
111            }
112            return Ok(());
113        }
114        let target_dir = &metadata.target_directory;
115
116        if let Some(package) = &self.package {
117            if packages.is_empty() {
118                return Err(Error::PackageNotFound {
119                    package: package.clone(),
120                });
121            }
122        }
123
124        let mut package_names: Vec<String> = Vec::new();
125        for p in packages {
126            package_names.push(p.name.clone().replace('-', "_"));
127            let mut cmd = Command::new("cargo");
128            cmd.stdout(Stdio::piped());
129            cmd.arg("rustc");
130            let manifest_path = pathdiff::diff_paths(&p.manifest_path, &working_dir)
131                .unwrap_or(p.manifest_path.clone().into());
132            cmd.arg(format!(
133                "--manifest-path={}",
134                manifest_path.to_string_lossy()
135            ));
136            cmd.arg("--crate-type=cdylib");
137            cmd.arg("--target=wasm32-unknown-unknown");
138            let profile = self.profile.as_deref().unwrap_or("release");
139            if profile == "release" {
140                cmd.arg("--release");
141            } else if profile != "debug" {
142                cmd.arg(format!("--profile={profile}"));
143            }
144            if self.all_features {
145                cmd.arg("--all-features");
146            }
147            if self.no_default_features {
148                cmd.arg("--no-default-features");
149            }
150            if let Some(features) = self.features() {
151                let requested: HashSet<String> = features.iter().cloned().collect();
152                let available = p.features.iter().map(|f| f.0).cloned().collect();
153                let activate = requested.intersection(&available).join(",");
154                if !activate.is_empty() {
155                    cmd.arg(format!("--features={activate}"));
156                }
157            }
158            if self.profile.is_none() {
159                set_default_profile_flags(&mut cmd);
160            }
161            let cmd_str = format!(
162                "cargo {}",
163                cmd.get_args().map(OsStr::to_string_lossy).join(" ")
164            );
165
166            if self.print_commands_only {
167                println!("{cmd_str}");
168            } else {
169                eprintln!("{cmd_str}");
170                let status = cmd.status().map_err(Error::CargoCmd)?;
171                if !status.success() {
172                    return Err(Error::Exit(status));
173                }
174
175                let out_dir = self
176                    .out_dir
177                    .clone()
178                    .unwrap_or_else(|| Path::new(target_dir).join("loam"));
179
180                fs::create_dir_all(&out_dir).map_err(Error::CreatingOutDir)?;
181                let file = format!("{}.wasm", p.name.replace('-', "_"));
182                let target_file_path = Path::new(target_dir)
183                    .join("wasm32-unknown-unknown")
184                    .join(profile)
185                    .join(&file);
186                let out_file_path = out_dir.join(&file);
187                if !out_file_path.exists() {
188                    symlink::symlink_file(target_file_path, out_file_path)
189                        .map_err(Error::CopyingWasmFile)?;
190                }
191            }
192        }
193
194        if self.build_clients {
195            self.build_clients_args
196                .run(&metadata.workspace_root.into_std_path_buf(), package_names)
197                .await?;
198        }
199
200        Ok(())
201    }
202
203    fn features(&self) -> Option<Vec<String>> {
204        self.features
205            .as_ref()
206            .map(|f| f.split(&[',', ' ']).map(String::from).collect())
207    }
208
209    fn packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
210        if let Some(package) = &self.package {
211            let package = metadata
212                .packages
213                .iter()
214                .find(|p| p.name == *package)
215                .ok_or_else(|| Error::PackageNotFound {
216                    package: package.clone(),
217                })?
218                .clone();
219            let manifest_path = package.manifest_path.clone().into_std_path_buf();
220            let mut contracts = loam_build::deps::contract(&manifest_path)?;
221            contracts.push(package);
222            return Ok(contracts);
223        }
224        Ok(metadata
225            .packages
226            .iter()
227            .filter(|p| {
228                // Filter crates by those that build to cdylib (wasm)
229                p.targets
230                    .iter()
231                    .any(|t| t.crate_types.iter().any(|c| c == "cdylib"))
232            })
233            .cloned()
234            .collect())
235    }
236
237    fn metadata(&self) -> Result<Metadata, cargo_metadata::Error> {
238        let mut cmd = MetadataCommand::new();
239        cmd.no_deps();
240        cmd.manifest_path(&self.manifest_path);
241        // Do not configure features on the metadata command, because we are
242        // only collecting non-dependency metadata, features have no impact on
243        // the output.
244        cmd.exec()
245    }
246}
247
248fn set_default_profile_flags(cmd: &mut Command) {
249    cmd.args([
250        "--",
251        "-C",
252        "opt-level=z", // Sets the optimization level to "z", which is equivalent to the opt-level = "z" in the Cargo profile.
253        "-C",
254        "overflow-checks=yes", // Enables overflow checks, equivalent to overflow-checks = true.
255        "-C",
256        "debuginfo=0", // Disables debug information, equivalent to debug = 0.
257        "-C",
258        "strip=symbols", // Strips symbols from the binary, equivalent to strip = "symbols".
259        "-C",
260        "debug-assertions=yes", // Enables debug assertions, equivalent to debug-assertions = true.
261        "-C",
262        "panic=abort", // Sets the panic strategy to "abort", equivalent to panic = "abort".
263        "-C",
264        "codegen-units=1", // Sets the number of codegen units to 1, equivalent to codegen-units = 1.
265        "-C",
266        "lto=yes", // Enables link-time optimization, equivalent to lto = true.
267    ]);
268    cmd.env("RUSTFLAGS", "-C embed-bitcode=yes");
269}