stellar_scaffold_cli/commands/build/
mod.rs

1#![allow(clippy::struct_excessive_bools)]
2use std::{fmt::Debug, io, path::Path, process::ExitStatus};
3
4use cargo_metadata::{Metadata, MetadataCommand, Package};
5use clap::Parser;
6use stellar_cli::commands::{contract::build, global};
7
8use clients::ScaffoldEnv;
9
10pub mod clients;
11pub mod docker;
12pub mod env_toml;
13
14/// Build a contract from source
15///
16/// Builds all crates that are referenced by the cargo manifest (Cargo.toml)
17/// that have cdylib as their crate-type. Crates are built for the wasm32
18/// target. Unless configured otherwise, crates are built with their default
19/// features and with their release profile.
20///
21/// To view the commands that will be executed, without executing them, use the
22/// --print-commands-only option.
23#[derive(Parser, Debug, Clone)]
24pub struct Command {
25    /// List package names in order of build
26    #[arg(long, visible_alias = "ls")]
27    pub list: bool,
28    #[command(flatten)]
29    pub build: build::Cmd,
30    /// Build client code in addition to building the contract
31    #[arg(long)]
32    pub build_clients: bool,
33    #[command(flatten)]
34    pub build_clients_args: clients::Args,
35}
36
37#[derive(thiserror::Error, Debug)]
38pub enum Error {
39    #[error(transparent)]
40    Metadata(#[from] cargo_metadata::Error),
41    #[error(transparent)]
42    EnvironmentsToml(#[from] env_toml::Error),
43    #[error(transparent)]
44    CargoCmd(io::Error),
45    #[error("exit status {0}")]
46    Exit(ExitStatus),
47    #[error("package {package} not found")]
48    PackageNotFound { package: String },
49    #[error("creating out directory: {0}")]
50    CreatingOutDir(io::Error),
51    #[error("copying wasm file: {0}")]
52    CopyingWasmFile(io::Error),
53    #[error("getting the current directory: {0}")]
54    GettingCurrentDir(io::Error),
55    #[error(transparent)]
56    StellarBuild(#[from] stellar_build::deps::Error),
57    #[error(transparent)]
58    BuildClients(#[from] clients::Error),
59    #[error(transparent)]
60    Build(#[from] build::Error),
61    #[error("Failed to start docker container")]
62    DockerStart,
63}
64
65impl Command {
66    pub fn list_packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
67        let packages = self.packages(metadata)?;
68        Ok(stellar_build::deps::get_workspace(&packages)?)
69    }
70
71    async fn start_local_docker_if_needed(
72        &self,
73        workspace_root: &Path,
74        env: &ScaffoldEnv,
75    ) -> Result<(), Error> {
76        if let Some(current_env) = env_toml::Environment::get(workspace_root, &env.to_string())? {
77            if current_env.network.run_locally {
78                eprintln!("Starting local Stellar Docker container...");
79                docker::start_local_stellar().await.map_err(|e| {
80                    eprintln!("Failed to start Stellar Docker container: {e:?}");
81                    Error::DockerStart
82                })?;
83                eprintln!("Local Stellar network is healthy and running.");
84            }
85        }
86        Ok(())
87    }
88
89    pub async fn run(&self) -> Result<(), Error> {
90        let metadata = self.metadata()?;
91        let packages = self.list_packages(&metadata)?;
92        let workspace_root = metadata.workspace_root.as_std_path();
93
94        if let Some(env) = &self.build_clients_args.env {
95            if env == &ScaffoldEnv::Development {
96                self.start_local_docker_if_needed(workspace_root, env)
97                    .await?;
98            }
99        }
100
101        if self.list {
102            for p in packages {
103                println!("{}", p.name);
104            }
105            return Ok(());
106        }
107
108        let target_dir = &metadata.target_directory;
109
110        let global_args = global::Args::default();
111
112        for p in &packages {
113            let mut cmd = self.build.clone();
114            cmd.out_dir = cmd.out_dir.or_else(|| {
115                Some(stellar_build::deps::stellar_wasm_out_dir(
116                    target_dir.as_std_path(),
117                ))
118            });
119            cmd.package = Some(p.name.clone());
120            if !p.name.is_empty() {
121                cmd.meta.push(("name".to_string(), p.name.clone()));
122            }
123            if !p.version.to_string().is_empty() {
124                cmd.meta.push(("binver".to_string(), p.version.to_string()));
125            }
126            if p.homepage.is_some() {
127                cmd.meta
128                    .push(("home_domain".to_string(), p.homepage.clone().unwrap()));
129            }
130            if !p.authors.is_empty() {
131                cmd.meta.push(("authors".to_string(), p.authors.join(", ")));
132            }
133            if p.repository.is_some() {
134                cmd.meta
135                    .push(("source_repo".to_string(), p.repository.clone().unwrap()));
136            }
137            cmd.run(&global_args)?;
138        }
139
140        if self.build_clients {
141            let mut build_clients_args = self.build_clients_args.clone();
142            // Pass through the workspace_root and out_dir from the build command
143            build_clients_args.workspace_root = Some(metadata.workspace_root.into_std_path_buf());
144            build_clients_args.out_dir.clone_from(&self.build.out_dir);
145            build_clients_args
146                .run(packages.iter().map(|p| p.name.replace('-', "_")).collect())
147                .await?;
148        }
149
150        Ok(())
151    }
152
153    fn packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
154        if let Some(package) = &self.build.package {
155            let package = metadata
156                .packages
157                .iter()
158                .find(|p| p.name == *package)
159                .ok_or_else(|| Error::PackageNotFound {
160                    package: package.clone(),
161                })?
162                .clone();
163            let manifest_path = package.manifest_path.clone().into_std_path_buf();
164            let mut contracts = stellar_build::deps::contract(&manifest_path)?;
165            contracts.push(package);
166            return Ok(contracts);
167        }
168        Ok(metadata
169            .packages
170            .iter()
171            .filter(|p| {
172                // Filter crates by those that build to cdylib (wasm)
173                p.targets
174                    .iter()
175                    .any(|t| t.crate_types.iter().any(|c| c == "cdylib"))
176            })
177            .cloned()
178            .collect())
179    }
180
181    pub(crate) fn metadata(&self) -> Result<Metadata, cargo_metadata::Error> {
182        let mut cmd = MetadataCommand::new();
183        cmd.no_deps();
184        // Set the manifest path if one is provided, otherwise rely on the cargo
185        // commands default behavior of finding the nearest Cargo.toml in the
186        // current directory, or the parent directories above it.
187        if let Some(manifest_path) = &self.build.manifest_path {
188            cmd.manifest_path(manifest_path);
189        }
190        // Do not configure features on the metadata command, because we are
191        // only collecting non-dependency metadata, features have no impact on
192        // the output.
193        cmd.exec()
194    }
195}