stellar_scaffold_cli/commands/build/
mod.rs1#![allow(clippy::struct_excessive_bools)]
2use crate::commands::build::Error::EmptyPackageName;
3use cargo_metadata::camino::Utf8PathBuf;
4use cargo_metadata::{Metadata, MetadataCommand, Package};
5use clap::Parser;
6use clients::ScaffoldEnv;
7use serde_json::Value;
8use std::collections::BTreeMap;
9use std::{fmt::Debug, io, path::Path, process::ExitStatus};
10use stellar_cli::commands::contract::build::Cmd;
11use stellar_cli::commands::{contract::build, global};
12use stellar_cli::print::Print;
13
14pub mod clients;
15pub mod docker;
16pub mod env_toml;
17
18#[derive(Parser, Debug, Clone)]
28pub struct Command {
29 #[arg(long, visible_alias = "ls")]
31 pub list: bool,
32 #[command(flatten)]
33 pub build: build::Cmd,
34 #[arg(long)]
36 pub build_clients: bool,
37 #[command(flatten)]
38 pub build_clients_args: clients::Args,
39}
40
41#[derive(thiserror::Error, Debug)]
42pub enum Error {
43 #[error(transparent)]
44 Metadata(#[from] cargo_metadata::Error),
45 #[error(transparent)]
46 EnvironmentsToml(#[from] env_toml::Error),
47 #[error(transparent)]
48 CargoCmd(io::Error),
49 #[error("exit status {0}")]
50 Exit(ExitStatus),
51 #[error("package {package} not found")]
52 PackageNotFound { package: String },
53 #[error("creating out directory: {0}")]
54 CreatingOutDir(io::Error),
55 #[error("copying wasm file: {0}")]
56 CopyingWasmFile(io::Error),
57 #[error("getting the current directory: {0}")]
58 GettingCurrentDir(io::Error),
59 #[error(transparent)]
60 StellarBuild(#[from] stellar_build::deps::Error),
61 #[error(transparent)]
62 BuildClients(#[from] clients::Error),
63 #[error(transparent)]
64 Build(#[from] build::Error),
65 #[error("Failed to start docker container")]
66 DockerStart,
67 #[error("package name is empty: {0}")]
68 EmptyPackageName(Utf8PathBuf),
69}
70
71impl Command {
72 pub fn list_packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
73 let packages = self.packages(metadata)?;
74 Ok(stellar_build::deps::get_workspace(&packages)?)
75 }
76
77 async fn start_local_docker_if_needed(
78 &self,
79 workspace_root: &Path,
80 env: &ScaffoldEnv,
81 ) -> Result<(), Error> {
82 if let Some(current_env) = env_toml::Environment::get(workspace_root, &env.to_string())? {
83 if current_env.network.run_locally {
84 docker::start_local_stellar().await.map_err(|e| {
85 eprintln!("Failed to start Stellar Docker container: {e:?}");
86 Error::DockerStart
87 })?;
88 }
89 }
90 Ok(())
91 }
92
93 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
94 let printer = Print::new(global_args.quiet);
95 let metadata = self.metadata()?;
96 let packages = self.list_packages(&metadata)?;
97 let workspace_root = metadata.workspace_root.as_std_path();
98
99 if let Some(env) = &self.build_clients_args.env {
100 if env == &ScaffoldEnv::Development {
101 printer.infoln("Starting local Stellar Docker container...");
102 self.start_local_docker_if_needed(workspace_root, env)
103 .await?;
104 printer.checkln("Local Stellar network is healthy and running.");
105 }
106 }
107
108 if self.list {
109 for p in packages {
110 println!("{}", p.name);
111 }
112 return Ok(());
113 }
114
115 let target_dir = &metadata.target_directory;
116
117 for p in &packages {
118 self.create_cmd(p, target_dir)?.run(global_args)?;
119 }
120
121 if self.build_clients {
122 let mut build_clients_args = self.build_clients_args.clone();
123 build_clients_args.workspace_root = Some(metadata.workspace_root.into_std_path_buf());
125 build_clients_args.out_dir.clone_from(&self.build.out_dir);
126 build_clients_args.global_args = Some(global_args.clone());
127 build_clients_args
128 .run(packages.iter().map(|p| p.name.replace('-', "_")).collect())
129 .await?;
130 }
131
132 Ok(())
133 }
134
135 fn packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
136 if let Some(package) = &self.build.package {
137 let package = metadata
138 .packages
139 .iter()
140 .find(|p| p.name == *package)
141 .ok_or_else(|| Error::PackageNotFound {
142 package: package.clone(),
143 })?
144 .clone();
145 let manifest_path = package.manifest_path.clone().into_std_path_buf();
146 let mut contracts = stellar_build::deps::contract(&manifest_path)?;
147 contracts.push(package);
148 return Ok(contracts);
149 }
150 Ok(metadata
151 .packages
152 .iter()
153 .filter(|p| {
154 p.targets
156 .iter()
157 .any(|t| t.crate_types.iter().any(|c| c == "cdylib"))
158 })
159 .cloned()
160 .collect())
161 }
162
163 pub(crate) fn metadata(&self) -> Result<Metadata, cargo_metadata::Error> {
164 let mut cmd = MetadataCommand::new();
165 cmd.no_deps();
166 if let Some(manifest_path) = &self.build.manifest_path {
170 cmd.manifest_path(manifest_path);
171 }
172 cmd.exec()
176 }
177
178 fn create_cmd(&self, p: &Package, target_dir: &Utf8PathBuf) -> Result<Cmd, Error> {
179 let mut cmd = self.build.clone();
180 cmd.out_dir = cmd.out_dir.or_else(|| {
181 Some(stellar_build::deps::stellar_wasm_out_dir(
182 target_dir.as_std_path(),
183 ))
184 });
185
186 if p.name.is_empty() {
188 return Err(EmptyPackageName(p.manifest_path.clone()));
189 }
190
191 cmd.package = Some(p.name.clone());
192
193 if let Value::Object(map) = &p.metadata {
194 if let Some(val) = &map.get("stellar") {
195 if let Value::Object(stellar_meta) = val {
196 let mut meta_map = BTreeMap::new();
197
198 if let Some(Value::Bool(true)) = stellar_meta.get("cargo_inherit") {
200 meta_map.insert("name".to_string(), p.name.clone());
201
202 if !p.version.to_string().is_empty() {
203 meta_map.insert("binver".to_string(), p.version.to_string());
204 }
205 if !p.authors.is_empty() {
206 meta_map.insert("authors".to_string(), p.authors.join(", "));
207 }
208 if p.homepage.is_some() {
209 meta_map.insert("homepage".to_string(), p.homepage.clone().unwrap());
210 }
211 if p.repository.is_some() {
212 meta_map
213 .insert("repository".to_string(), p.repository.clone().unwrap());
214 }
215 }
216
217 Self::rec_add_meta(String::new(), &mut meta_map, val);
218
219 meta_map.remove("rsver");
221 meta_map.remove("rssdkver");
222 meta_map.remove("cargo_inherit");
223 if let Some(version) = meta_map.remove("version") {
225 meta_map.insert("binver".to_string(), version);
226 }
227 if let Some(repository) = meta_map.remove("repository") {
228 meta_map.insert("source_repo".to_string(), repository);
229 }
230 if let Some(homepage) = meta_map.remove("homepage") {
231 meta_map.insert("home_domain".to_string(), homepage);
232 }
233
234 meta_map
235 .iter()
236 .for_each(|(k, v)| cmd.meta.push((k.clone(), v.clone())));
237 }
238 }
239 }
240
241 Ok(cmd)
242 }
243
244 fn rec_add_meta(prefix: String, meta_map: &mut BTreeMap<String, String>, value: &Value) {
245 match value {
246 Value::Null => {}
247 Value::Bool(bool) => {
248 meta_map.insert(prefix, bool.to_string());
249 }
250 Value::Number(n) => {
251 meta_map.insert(prefix, n.to_string());
252 }
253 Value::String(s) => {
254 meta_map.insert(prefix, s.clone());
255 }
256 Value::Array(array) => {
257 if array.iter().all(Self::is_simple) {
258 let s = array
259 .iter()
260 .map(|x| match x {
261 Value::String(str) => str.clone(),
262 _ => x.to_string(),
263 })
264 .collect::<Vec<_>>()
265 .join(",");
266 meta_map.insert(prefix, s);
267 } else {
268 for (pos, e) in array.iter().enumerate() {
269 Self::rec_add_meta(format!("{prefix}[{pos}]"), meta_map, e);
270 }
271 }
272 }
273 Value::Object(map) => {
274 let mut separator = "";
275 if !prefix.is_empty() {
276 separator = ".";
277 }
278 map.iter().for_each(|(k, v)| {
279 Self::rec_add_meta(format!("{prefix}{separator}{}", k.clone()), meta_map, v);
280 });
281 }
282 }
283 }
284
285 fn is_simple(val: &Value) -> bool {
286 !matches!(val, Value::Array(_) | Value::Object(_))
287 }
288}