stellar_scaffold_cli/commands/build/
mod.rs1#![allow(clippy::struct_excessive_bools)]
2use crate::commands::build::Error::EmptyPackageName;
3use crate::commands::version;
4use crate::extension;
5use cargo_metadata::camino::Utf8PathBuf;
6use cargo_metadata::{Metadata, MetadataCommand, Package};
7use clap::Parser;
8use clients::ScaffoldEnv;
9use serde_json::Value;
10use std::collections::BTreeMap;
11use std::{fmt::Debug, io, path::Path, process::ExitStatus};
12use stellar_cli::commands::contract::build::Cmd;
13use stellar_cli::commands::{contract::build, global};
14use stellar_cli::print::Print;
15use stellar_scaffold_ext_types::{CompileContext, HookName};
16
17pub mod clients;
18pub mod docker;
19pub mod env_toml;
20
21#[derive(Parser, Debug, Clone)]
31pub struct Command {
32 #[arg(long, visible_alias = "ls")]
34 pub list: bool,
35 #[command(flatten)]
36 pub build: build::Cmd,
37 #[arg(long)]
39 pub build_clients: bool,
40 #[command(flatten)]
41 pub build_clients_args: clients::Args,
42}
43
44#[derive(thiserror::Error, Debug)]
45pub enum Error {
46 #[error(transparent)]
47 Metadata(#[from] cargo_metadata::Error),
48 #[error(transparent)]
49 EnvironmentsToml(#[from] env_toml::Error),
50 #[error(transparent)]
51 CargoCmd(io::Error),
52 #[error("exit status {0}")]
53 Exit(ExitStatus),
54 #[error("package {package} not found")]
55 PackageNotFound { package: String },
56 #[error("creating out directory: {0}")]
57 CreatingOutDir(io::Error),
58 #[error("copying wasm file: {0}")]
59 CopyingWasmFile(io::Error),
60 #[error("getting the current directory: {0}")]
61 GettingCurrentDir(io::Error),
62 #[error(transparent)]
63 StellarBuild(#[from] stellar_build::deps::Error),
64 #[error(transparent)]
65 BuildClients(#[from] clients::Error),
66 #[error(transparent)]
67 Build(#[from] build::Error),
68 #[error("Failed to start docker container")]
69 DockerStart,
70 #[error("package name is empty: {0}")]
71 EmptyPackageName(Utf8PathBuf),
72}
73
74impl Command {
75 pub fn list_packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
76 let packages = self.packages(metadata)?;
77 Ok(stellar_build::deps::get_workspace(&packages)?)
78 }
79
80 async fn start_local_docker_if_needed(
81 &self,
82 workspace_root: &Path,
83 env: &ScaffoldEnv,
84 ) -> Result<(), Error> {
85 if let Some(current_env) = env_toml::Environment::get(workspace_root, env)?
86 && current_env.network.run_locally
87 {
88 docker::start_local_stellar().await.map_err(|e| {
89 eprintln!("Failed to start Stellar Docker container: {e:?}");
90 Error::DockerStart
91 })?;
92 }
93 Ok(())
94 }
95
96 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
97 let printer = Print::new(global_args.quiet);
98 let metadata = self.metadata()?;
99 let packages = self.list_packages(&metadata)?;
100 let workspace_root = metadata.workspace_root.as_std_path();
101
102 if let Some(env) = &self.build_clients_args.env
103 && env == &ScaffoldEnv::Development
104 {
105 printer.infoln("Starting local Stellar Docker container...");
106 self.start_local_docker_if_needed(workspace_root, env)
107 .await?;
108 printer.checkln("Local Stellar network is healthy and running.");
109 }
110
111 if self.list {
112 for p in packages {
113 println!("{}", p.name);
114 }
115 return Ok(());
116 }
117
118 let target_dir = &metadata.target_directory;
119
120 let scaffold_env = self
122 .build_clients_args
123 .env
124 .unwrap_or(ScaffoldEnv::Development);
125 let extensions = match env_toml::Environment::get(workspace_root, &scaffold_env)? {
126 Some(env_config) if !env_config.extensions.is_empty() => {
127 extension::discover(&env_config.extensions, &printer)
128 }
129 _ => vec![],
130 };
131 let wasm_out_dir =
133 self.build.out_dir.clone().unwrap_or_else(|| {
134 stellar_build::deps::stellar_wasm_out_dir(target_dir.as_std_path())
135 });
136 let source_dirs: Vec<std::path::PathBuf> = packages
137 .iter()
138 .filter_map(|p| p.manifest_path.parent())
139 .map(|p| p.as_std_path().to_path_buf())
140 .collect();
141 let pre_compile_ctx = CompileContext {
142 config: None,
143 project_root: workspace_root.to_path_buf(),
144 env: scaffold_env.to_string(),
145 wasm_out_dir: wasm_out_dir.clone(),
146 source_dirs: source_dirs.clone(),
147 wasm_paths: BTreeMap::new(),
148 };
149
150 extension::run_hook(
151 &extensions,
152 HookName::PreCompile,
153 &pre_compile_ctx,
154 &printer,
155 )
156 .await;
157
158 for p in &packages {
159 self.create_cmd(p, target_dir)?.run(global_args)?;
160 }
161
162 let wasm_paths: BTreeMap<String, std::path::PathBuf> = packages
164 .iter()
165 .map(|p| {
166 let name = p.name.replace('-', "_");
167 let path = stellar_build::stellar_wasm_out_file(target_dir.as_std_path(), &name);
168 (name, path)
169 })
170 .collect();
171 let post_compile_ctx = CompileContext {
172 config: None,
173 project_root: workspace_root.to_path_buf(),
174 env: scaffold_env.to_string(),
175 wasm_out_dir,
176 source_dirs,
177 wasm_paths,
178 };
179
180 extension::run_hook(
181 &extensions,
182 HookName::PostCompile,
183 &post_compile_ctx,
184 &printer,
185 )
186 .await;
187
188 if self.build_clients {
189 let mut build_clients_args = self.build_clients_args.clone();
190 build_clients_args.workspace_root = Some(metadata.workspace_root.into_std_path_buf());
192 build_clients_args.out_dir.clone_from(&self.build.out_dir);
193 build_clients_args.global_args = Some(global_args.clone());
194 build_clients_args.extensions = extensions;
195 build_clients_args.compile_ctx = Some(post_compile_ctx);
196 build_clients_args
197 .run(packages.iter().map(|p| p.name.replace('-', "_")).collect())
198 .await?;
199 }
200
201 Ok(())
202 }
203
204 fn packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
205 if let Some(package) = &self.build.package {
206 let package = metadata
207 .packages
208 .iter()
209 .find(|p| p.name == *package)
210 .ok_or_else(|| Error::PackageNotFound {
211 package: package.clone(),
212 })?
213 .clone();
214 let manifest_path = package.manifest_path.clone().into_std_path_buf();
215 let mut contracts = stellar_build::deps::contract(&manifest_path)?;
216 contracts.push(package);
217 return Ok(contracts);
218 }
219 Ok(metadata
220 .packages
221 .iter()
222 .filter(|p| {
223 p.targets
225 .iter()
226 .any(|t| t.crate_types.iter().any(|c| c == "cdylib"))
227 })
228 .cloned()
229 .collect())
230 }
231
232 pub(crate) fn metadata(&self) -> Result<Metadata, cargo_metadata::Error> {
233 let mut cmd = MetadataCommand::new();
234 cmd.no_deps();
235 if let Some(manifest_path) = &self.build.manifest_path {
239 cmd.manifest_path(manifest_path);
240 }
241 cmd.exec()
245 }
246
247 fn create_cmd(&self, p: &Package, target_dir: &Utf8PathBuf) -> Result<Cmd, Error> {
248 let mut cmd = self.build.clone();
249 cmd.out_dir = cmd.out_dir.or_else(|| {
250 Some(stellar_build::deps::stellar_wasm_out_dir(
251 target_dir.as_std_path(),
252 ))
253 });
254
255 if p.name.is_empty() {
257 return Err(EmptyPackageName(p.manifest_path.clone()));
258 }
259
260 cmd.package = Some(p.name.clone());
261
262 let mut meta_map = BTreeMap::new();
263
264 meta_map.insert("scaffold_version".to_string(), version::pkg().to_string());
265
266 if let Value::Object(map) = &p.metadata
267 && let Some(val) = &map.get("stellar")
268 && let Value::Object(stellar_meta) = val
269 {
270 if let Some(Value::Bool(true)) = stellar_meta.get("cargo_inherit") {
272 meta_map.insert("name".to_string(), p.name.clone());
273
274 if !p.version.to_string().is_empty() {
275 meta_map.insert("binver".to_string(), p.version.to_string());
276 }
277 if !p.authors.is_empty() {
278 meta_map.insert("authors".to_string(), p.authors.join(", "));
279 }
280 if let Some(homepage) = p.homepage.clone() {
281 meta_map.insert("homepage".to_string(), homepage);
282 }
283 if let Some(repository) = p.repository.clone() {
284 meta_map.insert("repository".to_string(), repository);
285 }
286 }
287 Self::rec_add_meta(String::new(), &mut meta_map, val);
288 meta_map.remove("rsver");
290 meta_map.remove("rssdkver");
291 meta_map.remove("cargo_inherit");
292 if let Some(version) = meta_map.remove("version") {
294 meta_map.insert("binver".to_string(), version);
295 }
296 if let Some(repository) = meta_map.remove("repository") {
297 meta_map.insert("source_repo".to_string(), repository);
298 }
299 if let Some(homepage) = meta_map.remove("homepage") {
300 meta_map.insert("home_domain".to_string(), homepage);
301 }
302 }
303 cmd.meta.extend(meta_map);
304 Ok(cmd)
305 }
306
307 fn rec_add_meta(prefix: String, meta_map: &mut BTreeMap<String, String>, value: &Value) {
308 match value {
309 Value::Null => {}
310 Value::Bool(bool) => {
311 meta_map.insert(prefix, bool.to_string());
312 }
313 Value::Number(n) => {
314 meta_map.insert(prefix, n.to_string());
315 }
316 Value::String(s) => {
317 meta_map.insert(prefix, s.clone());
318 }
319 Value::Array(array) => {
320 if array.iter().all(Self::is_simple) {
321 let s = array
322 .iter()
323 .map(|x| match x {
324 Value::String(str) => str.clone(),
325 _ => x.to_string(),
326 })
327 .collect::<Vec<_>>()
328 .join(",");
329 meta_map.insert(prefix, s);
330 } else {
331 for (pos, e) in array.iter().enumerate() {
332 Self::rec_add_meta(format!("{prefix}[{pos}]"), meta_map, e);
333 }
334 }
335 }
336 Value::Object(map) => {
337 let mut separator = "";
338 if !prefix.is_empty() {
339 separator = ".";
340 }
341 map.iter().for_each(|(k, v)| {
342 Self::rec_add_meta(format!("{prefix}{separator}{}", k.clone()), meta_map, v);
343 });
344 }
345 }
346 }
347
348 fn is_simple(val: &Value) -> bool {
349 !matches!(val, Value::Array(_) | Value::Object(_))
350 }
351}