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
132 let wasm_out_dir =
134 self.build.out_dir.clone().unwrap_or_else(|| {
135 stellar_build::deps::stellar_wasm_out_dir(target_dir.as_std_path())
136 });
137 let source_dirs: Vec<std::path::PathBuf> = packages
138 .iter()
139 .filter_map(|p| p.manifest_path.parent())
140 .map(|p| p.as_std_path().to_path_buf())
141 .collect();
142 let pre_compile_ctx = CompileContext {
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 project_root: workspace_root.to_path_buf(),
173 env: scaffold_env.to_string(),
174 wasm_out_dir,
175 source_dirs,
176 wasm_paths,
177 };
178
179 extension::run_hook(
180 &extensions,
181 HookName::PostCompile,
182 &post_compile_ctx,
183 &printer,
184 )
185 .await;
186
187 if self.build_clients {
188 let mut build_clients_args = self.build_clients_args.clone();
189 build_clients_args.workspace_root = Some(metadata.workspace_root.into_std_path_buf());
191 build_clients_args.out_dir.clone_from(&self.build.out_dir);
192 build_clients_args.global_args = Some(global_args.clone());
193 build_clients_args.extensions = extensions;
194 build_clients_args.compile_ctx = Some(post_compile_ctx);
195 build_clients_args
196 .run(packages.iter().map(|p| p.name.replace('-', "_")).collect())
197 .await?;
198 }
199
200 Ok(())
201 }
202
203 fn packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
204 if let Some(package) = &self.build.package {
205 let package = metadata
206 .packages
207 .iter()
208 .find(|p| p.name == *package)
209 .ok_or_else(|| Error::PackageNotFound {
210 package: package.clone(),
211 })?
212 .clone();
213 let manifest_path = package.manifest_path.clone().into_std_path_buf();
214 let mut contracts = stellar_build::deps::contract(&manifest_path)?;
215 contracts.push(package);
216 return Ok(contracts);
217 }
218 Ok(metadata
219 .packages
220 .iter()
221 .filter(|p| {
222 p.targets
224 .iter()
225 .any(|t| t.crate_types.iter().any(|c| c == "cdylib"))
226 })
227 .cloned()
228 .collect())
229 }
230
231 pub(crate) fn metadata(&self) -> Result<Metadata, cargo_metadata::Error> {
232 let mut cmd = MetadataCommand::new();
233 cmd.no_deps();
234 if let Some(manifest_path) = &self.build.manifest_path {
238 cmd.manifest_path(manifest_path);
239 }
240 cmd.exec()
244 }
245
246 fn create_cmd(&self, p: &Package, target_dir: &Utf8PathBuf) -> Result<Cmd, Error> {
247 let mut cmd = self.build.clone();
248 cmd.out_dir = cmd.out_dir.or_else(|| {
249 Some(stellar_build::deps::stellar_wasm_out_dir(
250 target_dir.as_std_path(),
251 ))
252 });
253
254 if p.name.is_empty() {
256 return Err(EmptyPackageName(p.manifest_path.clone()));
257 }
258
259 cmd.package = Some(p.name.clone());
260
261 let mut meta_map = BTreeMap::new();
262
263 meta_map.insert("scaffold_version".to_string(), version::pkg().to_string());
264
265 if let Value::Object(map) = &p.metadata
266 && let Some(val) = &map.get("stellar")
267 && let Value::Object(stellar_meta) = val
268 {
269 if let Some(Value::Bool(true)) = stellar_meta.get("cargo_inherit") {
271 meta_map.insert("name".to_string(), p.name.clone());
272
273 if !p.version.to_string().is_empty() {
274 meta_map.insert("binver".to_string(), p.version.to_string());
275 }
276 if !p.authors.is_empty() {
277 meta_map.insert("authors".to_string(), p.authors.join(", "));
278 }
279 if let Some(homepage) = p.homepage.clone() {
280 meta_map.insert("homepage".to_string(), homepage);
281 }
282 if let Some(repository) = p.repository.clone() {
283 meta_map.insert("repository".to_string(), repository);
284 }
285 }
286 Self::rec_add_meta(String::new(), &mut meta_map, val);
287 meta_map.remove("rsver");
289 meta_map.remove("rssdkver");
290 meta_map.remove("cargo_inherit");
291 if let Some(version) = meta_map.remove("version") {
293 meta_map.insert("binver".to_string(), version);
294 }
295 if let Some(repository) = meta_map.remove("repository") {
296 meta_map.insert("source_repo".to_string(), repository);
297 }
298 if let Some(homepage) = meta_map.remove("homepage") {
299 meta_map.insert("home_domain".to_string(), homepage);
300 }
301 }
302 cmd.meta.extend(meta_map);
303 Ok(cmd)
304 }
305
306 fn rec_add_meta(prefix: String, meta_map: &mut BTreeMap<String, String>, value: &Value) {
307 match value {
308 Value::Null => {}
309 Value::Bool(bool) => {
310 meta_map.insert(prefix, bool.to_string());
311 }
312 Value::Number(n) => {
313 meta_map.insert(prefix, n.to_string());
314 }
315 Value::String(s) => {
316 meta_map.insert(prefix, s.clone());
317 }
318 Value::Array(array) => {
319 if array.iter().all(Self::is_simple) {
320 let s = array
321 .iter()
322 .map(|x| match x {
323 Value::String(str) => str.clone(),
324 _ => x.to_string(),
325 })
326 .collect::<Vec<_>>()
327 .join(",");
328 meta_map.insert(prefix, s);
329 } else {
330 for (pos, e) in array.iter().enumerate() {
331 Self::rec_add_meta(format!("{prefix}[{pos}]"), meta_map, e);
332 }
333 }
334 }
335 Value::Object(map) => {
336 let mut separator = "";
337 if !prefix.is_empty() {
338 separator = ".";
339 }
340 map.iter().for_each(|(k, v)| {
341 Self::rec_add_meta(format!("{prefix}{separator}{}", k.clone()), meta_map, v);
342 });
343 }
344 }
345 }
346
347 fn is_simple(val: &Value) -> bool {
348 !matches!(val, Value::Array(_) | Value::Object(_))
349 }
350}