From 157c70b97d90306dd520fbe448307dfd6444ba81 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Tue, 24 Sep 2024 14:34:16 -0400 Subject: [PATCH] feat(cli): build contracts in specified order (#160) --- Cargo.lock | 2 + crates/loam-cli/Cargo.toml | 3 +- crates/loam-cli/src/commands/build/clients.rs | 39 +++++- .../loam-cli/src/commands/build/env_toml.rs | 53 ++++++-- .../tests/it/build_clients/init_script.rs | 114 ++++++++++++++++++ 5 files changed, 199 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d4a253f..59640884 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2718,6 +2718,7 @@ dependencies = [ "heck 0.5.0", "hex", "ignore", + "indexmap 1.9.3", "itertools 0.12.1", "loam-build", "notify", @@ -4795,6 +4796,7 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" dependencies = [ + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", diff --git a/crates/loam-cli/Cargo.toml b/crates/loam-cli/Cargo.toml index 3868ce45..0f7f4b94 100644 --- a/crates/loam-cli/Cargo.toml +++ b/crates/loam-cli/Cargo.toml @@ -56,7 +56,7 @@ sha2 = "0.10.7" hex = "0.4.3" shlex = "1.1.0" symlink = "0.1.0" -toml = { version = "0.8.12", features = ["parse"] } +toml = { version = "0.8.12", features = ["parse", "preserve_order"] } rand = "0.8.5" wasm-gen = { version = "0.1.4" } notify = "6.1.1" @@ -65,6 +65,7 @@ stellar-xdr = "21.0.0" rust-embed = { version = "8.2.0", features = ["debug-embed"] } regex = "1.10.5" toml_edit = "0.22.16" +indexmap = { version = "1.9", features = ["serde"] } [dev-dependencies] assert_cmd = "2.0.4" diff --git a/crates/loam-cli/src/commands/build/clients.rs b/crates/loam-cli/src/commands/build/clients.rs index f4a50242..63f7bf2c 100644 --- a/crates/loam-cli/src/commands/build/clients.rs +++ b/crates/loam-cli/src/commands/build/clients.rs @@ -1,12 +1,12 @@ #![allow(clippy::struct_excessive_bools)] use crate::commands::build::env_toml; +use indexmap::IndexMap; use regex::Regex; use serde_json; use shlex::split; use soroban_cli::commands::NetworkRunnable; use soroban_cli::utils::contract_hash; use soroban_cli::{commands as cli, CommandParser}; -use std::collections::BTreeMap as Map; use std::fmt::Debug; use std::hash::Hash; use std::process::Command; @@ -328,10 +328,39 @@ export default new Client.Client({{ Ok(()) } + fn reorder_package_names( + package_names: &[String], + contracts: Option<&IndexMap, env_toml::Contract>>, + ) -> Vec { + contracts.map_or_else( + || package_names.to_vec(), + |contracts| { + let mut reordered: Vec = contracts + .keys() + .filter_map(|contract_name| { + package_names + .iter() + .find(|&name| name == contract_name.as_ref()) + .cloned() + }) + .collect(); + + reordered.extend( + package_names + .iter() + .filter(|name| !contracts.contains_key(name.as_str())) + .cloned(), + ); + + reordered + }, + ) + } + async fn handle_production_contracts( &self, workspace_root: &std::path::Path, - contracts: &Map, env_toml::Contract>, + contracts: &IndexMap, env_toml::Contract>, ) -> Result<(), Error> { for (name, contract) in contracts.iter().filter(|(_, settings)| settings.client) { if let Some(id) = &contract.id { @@ -350,7 +379,7 @@ export default new Client.Client({{ async fn handle_contracts( self, workspace_root: &std::path::Path, - contracts: Option<&Map, env_toml::Contract>>, + contracts: Option<&IndexMap, env_toml::Contract>>, package_names: Vec, network: &Network, ) -> Result<(), Error> { @@ -375,7 +404,9 @@ export default new Client.Client({{ } } } - for name in package_names { + // Reorder package_names based on contracts order + let reordered_names = Self::reorder_package_names(&package_names, contracts); + for name in reordered_names { let settings = match contracts { Some(contracts) => contracts.get(&name as &str), None => None, diff --git a/crates/loam-cli/src/commands/build/env_toml.rs b/crates/loam-cli/src/commands/build/env_toml.rs index f0bd4450..dc2c8de1 100644 --- a/crates/loam-cli/src/commands/build/env_toml.rs +++ b/crates/loam-cli/src/commands/build/env_toml.rs @@ -1,26 +1,28 @@ +use indexmap::IndexMap; use serde::Deserialize; use std::collections::BTreeMap as Map; -use std::io; use std::path::Path; +use toml::value::Table; pub const ENV_FILE: &str = "environments.toml"; #[derive(thiserror::Error, Debug)] pub enum Error { #[error("⛔ ️parsing environments.toml: {0}")] - ParsingToml(io::Error), + ParsingToml(#[from] toml::de::Error), #[error("⛔ ️no settings for current LOAM_ENV ({0:?}) found in environments.toml")] NoSettingsForCurrentEnv(String), + #[error("⛔ ️reading environments.toml as a string: {0}")] + ParsingString(#[from] std::io::Error), } type Environments = Map, Environment>; -#[derive(Debug, serde::Deserialize, Clone)] +#[derive(Debug, Clone)] pub struct Environment { - #[serde(default, deserialize_with = "deserialize_accounts")] pub accounts: Option>, pub network: Network, - pub contracts: Option, Contract>>, + pub contracts: Option, Contract>>, } fn deserialize_accounts<'de, D>(deserializer: D) -> Result>, D::Error> @@ -31,6 +33,43 @@ where Ok(opt.map(|vec| vec.into_iter().map(Account::from).collect())) } +impl<'de> Deserialize<'de> for Environment { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct EnvironmentHelper { + #[serde(default, deserialize_with = "deserialize_accounts")] + accounts: Option>, + network: Network, + contracts: Option, + } + + let helper = EnvironmentHelper::deserialize(deserializer)?; + + let contracts = helper + .contracts + .map(|contracts_table| { + contracts_table + .into_iter() + .map(|(key, value)| { + let contract: Contract = + Contract::deserialize(value).map_err(serde::de::Error::custom)?; + Ok((key.into_boxed_str(), contract)) + }) + .collect::, D::Error>>() + }) + .transpose()?; + + Ok(Environment { + accounts: helper.accounts, + network: helper.network, + contracts, + }) + } +} + #[derive(Debug, serde::Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct Network { @@ -90,8 +129,8 @@ impl Environment { return Ok(None); } - let toml_str = std::fs::read_to_string(env_toml).map_err(Error::ParsingToml)?; - let mut parsed_toml: Environments = toml::from_str(&toml_str).unwrap(); + let toml_str = std::fs::read_to_string(env_toml)?; + let mut parsed_toml: Environments = toml::from_str(&toml_str)?; let current_env = parsed_toml.remove(loam_env); if current_env.is_none() { return Err(Error::NoSettingsForCurrentEnv(loam_env.to_string())); diff --git a/crates/loam-cli/tests/it/build_clients/init_script.rs b/crates/loam-cli/tests/it/build_clients/init_script.rs index 16a5ea64..7cd032b4 100644 --- a/crates/loam-cli/tests/it/build_clients/init_script.rs +++ b/crates/loam-cli/tests/it/build_clients/init_script.rs @@ -142,3 +142,117 @@ fn init_handles_quotations_and_subcommands_in_script() { )); }) } + +#[test] +fn init_scripts_run_in_specified_order() { + TestEnv::from("soroban-init-boilerplate", |env| { + let binary_path = env + .find_binary("stellar") + .expect("Stellar binary not found. Test cannot proceed."); + let binary_path_str = binary_path.to_string_lossy(); + // First configuration: custom_types then token + env.set_environments_toml(format!( + r#" +development.accounts = [ +{{ name = "alice" }}, +{{ name = "bob" }}, +] + +[development.network] +rpc-url = "http://localhost:8000/rpc" +network-passphrase = "Standalone Network ; February 2017" + +[development.contracts] +hello_world.client = false +soroban_increment_contract.client = false +soroban_auth_contract.client = false + +[development.contracts.soroban_custom_types_contract] +client = true +init = """ +test_init --resolution 300000 --assets '[{{"Stellar": "$({} contract id asset --asset native)"}} ]' --decimals 14 --base '{{"Stellar":"$({} contract id asset --asset native)"}}' +""" + +[development.contracts.soroban_token_contract] +client = true +init = """ +STELLAR_ACCOUNT=bob initialize --symbol ABND --decimal 7 --name abundance --admin bob +STELLAR_ACCOUNT=bob mint --amount 2000000 --to bob +""" +"#, + binary_path_str, binary_path_str + )); + + let output = env + .loam_env("development", true) + .output() + .expect("Failed to execute command"); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success()); + + // Check order of initialization + let custom_types_index = stderr + .find("Running initialization script for \"soroban_custom") + .expect("Custom types init not found"); + let token_index = stderr + .find("Running initialization script for \"soroban_token") + .expect("Token init not found"); + assert!( + custom_types_index < token_index, + "Custom types should be initialized before token" + ); + + // Second configuration: token then custom_types + env.set_environments_toml(format!( + r#" +development.accounts = [ +{{ name = "alice" }}, +{{ name = "bob" }}, +] + +[development.network] +rpc-url = "http://localhost:8000/rpc" +network-passphrase = "Standalone Network ; February 2017" + +[development.contracts] +hello_world.client = false +soroban_increment_contract.client = false +soroban_auth_contract.client = false + +[development.contracts.soroban_token_contract] +client = true +init = """ +STELLAR_ACCOUNT=bob initialize --symbol ABND --decimal 7 --name abundance --admin bob +STELLAR_ACCOUNT=bob mint --amount 2000000 --to bob +""" + +[development.contracts.soroban_custom_types_contract] +client = true +init = """ +test_init --resolution 300000 --assets '[{{"Stellar": "$({} contract id asset --asset native)"}} ]' --decimals 14 --base '{{"Stellar":"$({} contract id asset --asset native)"}}' +""" +"#, + binary_path_str, binary_path_str)); + + let output = env + .loam_env("development", true) + .output() + .expect("Failed to execute command"); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success()); + + // Check order of initialization + let token_index = stderr + .find("Running initialization script for \"soroban_token") + .expect("Token init not found"); + let custom_types_index = stderr + .find("Running initialization script for \"soroban_custom") + .expect("Custom types init not found"); + assert!( + token_index < custom_types_index, + "Token should be initialized before custom types" + ); + }) +}