stellar_scaffold_cli/commands/build/
mod.rs

1#![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/// Build a contract from source
19///
20/// Builds all crates that are referenced by the cargo manifest (Cargo.toml)
21/// that have cdylib as their crate-type. Crates are built for the wasm32
22/// target. Unless configured otherwise, crates are built with their default
23/// features and with their release profile.
24///
25/// To view the commands that will be executed, without executing them, use the
26/// --print-commands-only option.
27#[derive(Parser, Debug, Clone)]
28pub struct Command {
29    /// List package names in order of build
30    #[arg(long, visible_alias = "ls")]
31    pub list: bool,
32    #[command(flatten)]
33    pub build: build::Cmd,
34    /// Build client code in addition to building the contract
35    #[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            // Pass through the workspace_root, out_dir, global_args, and printer
124            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                // Filter crates by those that build to cdylib (wasm)
155                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        // Set the manifest path if one is provided, otherwise rely on the cargo
167        // commands default behavior of finding the nearest Cargo.toml in the
168        // current directory, or the parent directories above it.
169        if let Some(manifest_path) = &self.build.manifest_path {
170            cmd.manifest_path(manifest_path);
171        }
172        // Do not configure features on the metadata command, because we are
173        // only collecting non-dependency metadata, features have no impact on
174        // the output.
175        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        // Name is required in Cargo toml, so it should fail regardless
187        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                    // When cargo_inherit is set, copy meta from Cargo toml
199                    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                    // Reserved keys
220                    meta_map.remove("rsver");
221                    meta_map.remove("rssdkver");
222                    meta_map.remove("cargo_inherit");
223                    // Rename some fields
224                    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}