loam_cli/commands/build/
mod.rs1#![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#[derive(Parser, Debug, Clone)]
28pub struct Cmd {
29 #[arg(long, visible_alias = "ls")]
31 pub list: bool,
32 #[arg(long, default_value = "Cargo.toml")]
34 pub manifest_path: std::path::PathBuf,
35 #[arg(long)]
39 pub package: Option<String>,
40 #[arg(long)]
42 pub profile: Option<String>,
43 #[arg(long, help_heading = "Features")]
45 pub features: Option<String>,
46 #[arg(
48 long,
49 conflicts_with = "features",
50 conflicts_with = "no_default_features",
51 help_heading = "Features"
52 )]
53 pub all_features: bool,
54 #[arg(long, help_heading = "Features")]
56 pub no_default_features: bool,
57 #[arg(long)]
64 pub out_dir: Option<std::path::PathBuf>,
65 #[arg(long, conflicts_with = "out_dir", help_heading = "Other")]
67 pub print_commands_only: bool,
68 #[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 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 cmd.exec()
245 }
246}
247
248fn set_default_profile_flags(cmd: &mut Command) {
249 cmd.args([
250 "--",
251 "-C",
252 "opt-level=z", "-C",
254 "overflow-checks=yes", "-C",
256 "debuginfo=0", "-C",
258 "strip=symbols", "-C",
260 "debug-assertions=yes", "-C",
262 "panic=abort", "-C",
264 "codegen-units=1", "-C",
266 "lto=yes", ]);
268 cmd.env("RUSTFLAGS", "-C embed-bitcode=yes");
269}