stellar_scaffold_cli/commands/build/
mod.rs

1#![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/// Build a contract from source
20///
21/// Builds all crates that are referenced by the cargo manifest (Cargo.toml)
22/// that have cdylib as their crate-type. Crates are built for the wasm32
23/// target. Unless configured otherwise, crates are built with their default
24/// features and with their release profile.
25///
26/// To view the commands that will be executed, without executing them, use the
27/// --print-commands-only option.
28#[derive(Parser, Debug, Clone)]
29pub struct Command {
30    /// List package names in order of build
31    #[arg(long, visible_alias = "ls")]
32    pub list: bool,
33    #[command(flatten)]
34    pub build: build::Cmd,
35    /// Build client code in addition to building the contract
36    #[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.to_string())? {
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            // Pass through the workspace_root, out_dir, global_args, and printer
125            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                // Filter crates by those that build to cdylib (wasm)
156                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        // Set the manifest path if one is provided, otherwise rely on the cargo
168        // commands default behavior of finding the nearest Cargo.toml in the
169        // current directory, or the parent directories above it.
170        if let Some(manifest_path) = &self.build.manifest_path {
171            cmd.manifest_path(manifest_path);
172        }
173        // Do not configure features on the metadata command, because we are
174        // only collecting non-dependency metadata, features have no impact on
175        // the output.
176        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        // Name is required in Cargo toml, so it should fail regardless
188        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                    // When cargo_inherit is set, copy meta from Cargo toml
202                    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 p.homepage.is_some() {
212                            meta_map.insert("homepage".to_string(), p.homepage.clone().unwrap());
213                        }
214                        if p.repository.is_some() {
215                            meta_map
216                                .insert("repository".to_string(), p.repository.clone().unwrap());
217                        }
218                    }
219
220                    Self::rec_add_meta(String::new(), &mut meta_map, val);
221
222                    // Reserved keys
223                    meta_map.remove("rsver");
224                    meta_map.remove("rssdkver");
225                    meta_map.remove("cargo_inherit");
226                    // Rename some fields
227                    if let Some(version) = meta_map.remove("version") {
228                        meta_map.insert("binver".to_string(), version);
229                    }
230                    if let Some(repository) = meta_map.remove("repository") {
231                        meta_map.insert("source_repo".to_string(), repository);
232                    }
233                    if let Some(homepage) = meta_map.remove("homepage") {
234                        meta_map.insert("home_domain".to_string(), homepage);
235                    }
236                }
237            }
238        }
239
240        meta_map
241            .iter()
242            .for_each(|(k, v)| cmd.meta.push((k.clone(), v.clone())));
243
244        Ok(cmd)
245    }
246
247    fn rec_add_meta(prefix: String, meta_map: &mut BTreeMap<String, String>, value: &Value) {
248        match value {
249            Value::Null => {}
250            Value::Bool(bool) => {
251                meta_map.insert(prefix, bool.to_string());
252            }
253            Value::Number(n) => {
254                meta_map.insert(prefix, n.to_string());
255            }
256            Value::String(s) => {
257                meta_map.insert(prefix, s.clone());
258            }
259            Value::Array(array) => {
260                if array.iter().all(Self::is_simple) {
261                    let s = array
262                        .iter()
263                        .map(|x| match x {
264                            Value::String(str) => str.clone(),
265                            _ => x.to_string(),
266                        })
267                        .collect::<Vec<_>>()
268                        .join(",");
269                    meta_map.insert(prefix, s);
270                } else {
271                    for (pos, e) in array.iter().enumerate() {
272                        Self::rec_add_meta(format!("{prefix}[{pos}]"), meta_map, e);
273                    }
274                }
275            }
276            Value::Object(map) => {
277                let mut separator = "";
278                if !prefix.is_empty() {
279                    separator = ".";
280                }
281                map.iter().for_each(|(k, v)| {
282                    Self::rec_add_meta(format!("{prefix}{separator}{}", k.clone()), meta_map, v);
283                });
284            }
285        }
286    }
287
288    fn is_simple(val: &Value) -> bool {
289        !matches!(val, Value::Array(_) | Value::Object(_))
290    }
291}