#![allow(clippy::struct_excessive_bools)]
use crate::commands::build::Error::EmptyPackageName;
use crate::commands::version;
use crate::extension;
use cargo_metadata::camino::Utf8PathBuf;
use cargo_metadata::{Metadata, MetadataCommand, Package};
use clap::Parser;
use clients::ScaffoldEnv;
use serde_json::Value;
use std::collections::BTreeMap;
use std::{fmt::Debug, io, path::Path, process::ExitStatus};
use stellar_cli::commands::contract::build::Cmd;
use stellar_cli::commands::{contract::build, global};
use stellar_cli::print::Print;
use stellar_scaffold_ext_types::{CompileContext, HookName};
pub mod clients;
pub mod docker;
pub mod env_toml;
#[derive(Parser, Debug, Clone)]
pub struct Command {
#[arg(long, visible_alias = "ls")]
pub list: bool,
#[command(flatten)]
pub build: build::Cmd,
#[arg(long)]
pub build_clients: bool,
#[command(flatten)]
pub build_clients_args: clients::Args,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Metadata(#[from] cargo_metadata::Error),
#[error(transparent)]
EnvironmentsToml(#[from] env_toml::Error),
#[error(transparent)]
CargoCmd(io::Error),
#[error("exit status {0}")]
Exit(ExitStatus),
#[error("package {package} not found")]
PackageNotFound { package: String },
#[error("creating out directory: {0}")]
CreatingOutDir(io::Error),
#[error("copying wasm file: {0}")]
CopyingWasmFile(io::Error),
#[error("getting the current directory: {0}")]
GettingCurrentDir(io::Error),
#[error(transparent)]
StellarBuild(#[from] stellar_build::deps::Error),
#[error(transparent)]
BuildClients(#[from] clients::Error),
#[error(transparent)]
Build(#[from] build::Error),
#[error("Failed to start docker container")]
DockerStart,
#[error("package name is empty: {0}")]
EmptyPackageName(Utf8PathBuf),
}
impl Command {
pub fn list_packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
let packages = self.packages(metadata)?;
Ok(stellar_build::deps::get_workspace(&packages)?)
}
async fn start_local_docker_if_needed(
&self,
workspace_root: &Path,
env: &ScaffoldEnv,
) -> Result<(), Error> {
if let Some(current_env) = env_toml::Environment::get(workspace_root, env)?
&& current_env.network.run_locally
{
docker::start_local_stellar().await.map_err(|e| {
eprintln!("Failed to start Stellar Docker container: {e:?}");
Error::DockerStart
})?;
}
Ok(())
}
pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
let printer = Print::new(global_args.quiet);
let metadata = self.metadata()?;
let packages = self.list_packages(&metadata)?;
let workspace_root = metadata.workspace_root.as_std_path();
if let Some(env) = &self.build_clients_args.env
&& env == &ScaffoldEnv::Development
{
printer.infoln("Starting local Stellar Docker container...");
self.start_local_docker_if_needed(workspace_root, env)
.await?;
printer.checkln("Local Stellar network is healthy and running.");
}
if self.list {
for p in packages {
println!("{}", p.name);
}
return Ok(());
}
let target_dir = &metadata.target_directory;
let scaffold_env = self
.build_clients_args
.env
.unwrap_or(ScaffoldEnv::Development);
let extensions = match env_toml::Environment::get(workspace_root, &scaffold_env)? {
Some(env_config) if !env_config.extensions.is_empty() => {
extension::discover(&env_config.extensions, &printer)
}
_ => vec![],
};
let wasm_out_dir =
self.build.out_dir.clone().unwrap_or_else(|| {
stellar_build::deps::stellar_wasm_out_dir(target_dir.as_std_path())
});
let source_dirs: Vec<std::path::PathBuf> = packages
.iter()
.filter_map(|p| p.manifest_path.parent())
.map(|p| p.as_std_path().to_path_buf())
.collect();
let pre_compile_ctx = CompileContext {
config: None,
project_root: workspace_root.to_path_buf(),
env: scaffold_env.to_string(),
wasm_out_dir: wasm_out_dir.clone(),
source_dirs: source_dirs.clone(),
wasm_paths: BTreeMap::new(),
};
extension::run_hook(
&extensions,
HookName::PreCompile,
&pre_compile_ctx,
&printer,
)
.await;
for p in &packages {
self.create_cmd(p, target_dir)?.run(global_args)?;
}
let wasm_paths: BTreeMap<String, std::path::PathBuf> = packages
.iter()
.map(|p| {
let name = p.name.replace('-', "_");
let path = stellar_build::stellar_wasm_out_file(target_dir.as_std_path(), &name);
(name, path)
})
.collect();
let post_compile_ctx = CompileContext {
config: None,
project_root: workspace_root.to_path_buf(),
env: scaffold_env.to_string(),
wasm_out_dir,
source_dirs,
wasm_paths,
};
extension::run_hook(
&extensions,
HookName::PostCompile,
&post_compile_ctx,
&printer,
)
.await;
if self.build_clients {
let mut build_clients_args = self.build_clients_args.clone();
build_clients_args.workspace_root = Some(metadata.workspace_root.into_std_path_buf());
build_clients_args.out_dir.clone_from(&self.build.out_dir);
build_clients_args.global_args = Some(global_args.clone());
build_clients_args.extensions = extensions;
build_clients_args.compile_ctx = Some(post_compile_ctx);
build_clients_args
.run(packages.iter().map(|p| p.name.replace('-', "_")).collect())
.await?;
}
Ok(())
}
fn packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
if let Some(package) = &self.build.package {
let package = metadata
.packages
.iter()
.find(|p| p.name == *package)
.ok_or_else(|| Error::PackageNotFound {
package: package.clone(),
})?
.clone();
let manifest_path = package.manifest_path.clone().into_std_path_buf();
let mut contracts = stellar_build::deps::contract(&manifest_path)?;
contracts.push(package);
return Ok(contracts);
}
Ok(metadata
.packages
.iter()
.filter(|p| {
p.targets
.iter()
.any(|t| t.crate_types.iter().any(|c| c == "cdylib"))
})
.cloned()
.collect())
}
pub(crate) fn metadata(&self) -> Result<Metadata, cargo_metadata::Error> {
let mut cmd = MetadataCommand::new();
cmd.no_deps();
if let Some(manifest_path) = &self.build.manifest_path {
cmd.manifest_path(manifest_path);
}
cmd.exec()
}
fn create_cmd(&self, p: &Package, target_dir: &Utf8PathBuf) -> Result<Cmd, Error> {
let mut cmd = self.build.clone();
cmd.out_dir = cmd.out_dir.or_else(|| {
Some(stellar_build::deps::stellar_wasm_out_dir(
target_dir.as_std_path(),
))
});
if p.name.is_empty() {
return Err(EmptyPackageName(p.manifest_path.clone()));
}
cmd.package = Some(p.name.clone());
let mut meta_map = BTreeMap::new();
meta_map.insert("scaffold_version".to_string(), version::pkg().to_string());
if let Value::Object(map) = &p.metadata
&& let Some(val) = &map.get("stellar")
&& let Value::Object(stellar_meta) = val
{
if let Some(Value::Bool(true)) = stellar_meta.get("cargo_inherit") {
meta_map.insert("name".to_string(), p.name.clone());
if !p.version.to_string().is_empty() {
meta_map.insert("binver".to_string(), p.version.to_string());
}
if !p.authors.is_empty() {
meta_map.insert("authors".to_string(), p.authors.join(", "));
}
if let Some(homepage) = p.homepage.clone() {
meta_map.insert("homepage".to_string(), homepage);
}
if let Some(repository) = p.repository.clone() {
meta_map.insert("repository".to_string(), repository);
}
}
Self::rec_add_meta(String::new(), &mut meta_map, val);
meta_map.remove("rsver");
meta_map.remove("rssdkver");
meta_map.remove("cargo_inherit");
if let Some(version) = meta_map.remove("version") {
meta_map.insert("binver".to_string(), version);
}
if let Some(repository) = meta_map.remove("repository") {
meta_map.insert("source_repo".to_string(), repository);
}
if let Some(homepage) = meta_map.remove("homepage") {
meta_map.insert("home_domain".to_string(), homepage);
}
}
cmd.meta.extend(meta_map);
Ok(cmd)
}
fn rec_add_meta(prefix: String, meta_map: &mut BTreeMap<String, String>, value: &Value) {
match value {
Value::Null => {}
Value::Bool(bool) => {
meta_map.insert(prefix, bool.to_string());
}
Value::Number(n) => {
meta_map.insert(prefix, n.to_string());
}
Value::String(s) => {
meta_map.insert(prefix, s.clone());
}
Value::Array(array) => {
if array.iter().all(Self::is_simple) {
let s = array
.iter()
.map(|x| match x {
Value::String(str) => str.clone(),
_ => x.to_string(),
})
.collect::<Vec<_>>()
.join(",");
meta_map.insert(prefix, s);
} else {
for (pos, e) in array.iter().enumerate() {
Self::rec_add_meta(format!("{prefix}[{pos}]"), meta_map, e);
}
}
}
Value::Object(map) => {
let mut separator = "";
if !prefix.is_empty() {
separator = ".";
}
map.iter().for_each(|(k, v)| {
Self::rec_add_meta(format!("{prefix}{separator}{}", k.clone()), meta_map, v);
});
}
}
}
fn is_simple(val: &Value) -> bool {
!matches!(val, Value::Array(_) | Value::Object(_))
}
}