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