diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 33911725a..a878ca0cc 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -84,7 +84,7 @@ Tools for smart contract developers * `fetch` — Fetch a contract's Wasm binary * `id` — Generate the contract id for a given contract or asset * `info` — Access info about contracts -* `init` — Initialize a Soroban project with an example contract +* `init` — Initialize a Soroban contract * `inspect` — Inspect a WASM file listing contract functions, meta, etc * `install` — Install a WASM file to the ledger without creating a contract instance * `invoke` — Invoke a contract function @@ -611,7 +611,9 @@ Outputs no data when no data is present in the contract. ## `stellar contract init` -Initialize a Soroban project with an example contract +Initialize a Soroban contract. + +When running with empty or non-existent `--project-path`, this command will generate a template Cargo workspace project and add a sample contract package. When running in the existing Cargo project, it will add a new package for a sample contract with a given `--name`. **Usage:** `stellar contract init [OPTIONS] ` @@ -621,9 +623,15 @@ Initialize a Soroban project with an example contract ###### **Options:** -* `-w`, `--with-example` — This argument has been removed and will be not be recognized by the future versions of CLI. You can still clone examples from the repo https://github.com/stellar/soroban-examples -* `--frontend-template` — This argument has been removed and will be not be recognized by the future versions of CLI. You can search for frontend templates using github tags, such as soroban-template or soroban-frontend-template -* `--overwrite` — Overwrite all existing files. +* `--name ` — An optional flag to specify a new contract's name. + + Default value: `hello-world` +* `-w`, `--with-example` — This argument has been deprecated and will be removed in the future versions of CLI. You can still clone examples from the repo https://github.com/stellar/soroban-examples +* `--frontend-template` — This argument has been deprecated and will be removed in the future versions of CLI. You can search for frontend templates using github tags, such as soroban-template or soroban-frontend-template +* `--overwrite` — This argument has been deprecated and will be removed in the future versions of CLI. init command no longer overwrites existing files. + + Possible values: `true`, `false` + diff --git a/cmd/crates/soroban-test/tests/it/init.rs b/cmd/crates/soroban-test/tests/it/init.rs index 6eada4ce6..c3cc9b694 100644 --- a/cmd/crates/soroban-test/tests/it/init.rs +++ b/cmd/crates/soroban-test/tests/it/init.rs @@ -1,6 +1,6 @@ use assert_fs::prelude::*; use predicates::prelude::predicate; -use soroban_test::TestEnv; +use soroban_test::{AssertExt, TestEnv}; #[test] fn init() { @@ -24,3 +24,64 @@ fn init() { == Some(&format!("{major}.0.0")) })); } + +#[test] +fn init_and_deploy() { + let name = "hello_world"; + let sandbox = TestEnv::default(); + + sandbox + .new_assert_cmd("contract") + .arg("init") + .arg("--name") + .arg(name) + .arg("project") + .assert() + .success(); + + let manifest_path = sandbox + .dir() + .join(format!("project/contracts/{name}/Cargo.toml")); + assert!(manifest_path.exists()); + + sandbox + .new_assert_cmd("contract") + .arg("build") + .arg("--manifest-path") + .arg(manifest_path) + .assert() + .success(); + + let target_dir = sandbox + .dir() + .join("project/target/wasm32-unknown-unknown/release"); + assert!(target_dir.exists()); + + let assert = sandbox + .new_assert_cmd("contract") + .arg("deploy") + .arg("--wasm") + .arg(target_dir.join(format!("{name}.wasm"))) + .assert(); + + let contract = assert.stdout_as_str(); + + assert.success(); + + let assert = sandbox + .new_assert_cmd("contract") + .arg("invoke") + .arg("--id") + .arg(contract) + .arg("--") + .arg("hello") + .arg("--to") + .arg("bar") + .assert(); + + let output = assert.stdout_as_str(); + + assert_eq!(output, r#"["Hello","bar"]"#); + + assert.success(); +} diff --git a/cmd/soroban-cli/src/commands/contract/init.rs b/cmd/soroban-cli/src/commands/contract/init.rs index 941e5f13e..46197ec99 100644 --- a/cmd/soroban-cli/src/commands/contract/init.rs +++ b/cmd/soroban-cli/src/commands/contract/init.rs @@ -1,5 +1,5 @@ use std::{ - fs::{create_dir_all, metadata, write, Metadata}, + fs::{create_dir_all, write}, io, path::{Path, PathBuf}, str, @@ -8,6 +8,9 @@ use std::{ use clap::Parser; use rust_embed::RustEmbed; +use crate::commands::contract::init::Error::{ + AlreadyExists, PathExistsNotCargoProject, PathExistsNotDir, +}; use crate::{commands::global, print}; #[derive(Parser, Debug, Clone)] @@ -15,12 +18,19 @@ use crate::{commands::global, print}; pub struct Cmd { pub project_path: String, + #[arg( + long, + default_value = "hello-world", + long_help = "An optional flag to specify a new contract's name." + )] + pub name: String, + // TODO: remove in 23.0 #[arg( short, long, action = clap::ArgAction::HelpLong, - long_help = "This argument has been removed and will be not be recognized by the future versions of CLI. You can still clone examples from the repo https://github.com/stellar/soroban-examples", + long_help = "This argument has been deprecated and will be removed in the future versions of CLI. You can still clone examples from the repo https://github.com/stellar/soroban-examples", )] pub with_example: Option, @@ -28,12 +38,17 @@ pub struct Cmd { #[arg( long, action = clap::ArgAction::HelpLong, - long_help = "This argument has been removed and will be not be recognized by the future versions of CLI. You can search for frontend templates using github tags, such as soroban-template or soroban-frontend-template", + long_help = "This argument has been deprecated and will be removed in the future versions of CLI. You can search for frontend templates using github tags, such as soroban-template or soroban-frontend-template", )] pub frontend_template: Option, - #[arg(long, long_help = "Overwrite all existing files.")] - pub overwrite: bool, + // TODO: remove in 23.0 + #[arg( + long, + action = clap::ArgAction::HelpLong, + long_help = "This argument has been deprecated and will be removed in the future versions of CLI. init command no longer overwrites existing files." + )] + pub overwrite: Option, } #[derive(thiserror::Error, Debug)] @@ -71,8 +86,12 @@ impl Cmd { } #[derive(RustEmbed)] -#[folder = "src/utils/contract-init-template"] -struct TemplateFiles; +#[folder = "src/utils/contract-workspace-template"] +struct WorkspaceTemplate; + +#[derive(RustEmbed)] +#[folder = "src/utils/contract-template"] +struct ContractTemplate; struct Runner { args: Cmd, @@ -82,30 +101,37 @@ struct Runner { impl Runner { fn run(&self) -> Result<(), Error> { let project_path = PathBuf::from(&self.args.project_path); - self.print - .infoln(format!("Initializing project at {project_path:?}")); - // create a project dir, and copy the contents of the base template (contract-init-template) into it - Self::create_dir_all(&project_path)?; + if project_path.exists() { + if project_path.is_dir() { + if project_path.read_dir()?.next().is_none() { + self.init_workspace()?; + } else if !project_path.join("Cargo.toml").exists() { + return Err(PathExistsNotCargoProject); + } + } else { + return Err(PathExistsNotDir); + } + } else { + self.init_workspace()?; + } + self.copy_template_files()?; Ok(()) } - fn copy_template_files(&self) -> Result<(), Error> { + fn init_workspace(&self) -> Result<(), Error> { let project_path = Path::new(&self.args.project_path); - for item in TemplateFiles::iter() { - let mut to = project_path.join(item.as_ref()); - let exists = Self::file_exists(&to); - if exists && !self.args.overwrite { - self.print - .infoln(format!("Skipped creating {to:?} as it already exists")); - continue; - } + self.print + .infoln(format!("Initializing workspace at {project_path:?}")); + + for item in WorkspaceTemplate::iter() { + let to = project_path.join(item.as_ref()); Self::create_dir_all(to.parent().unwrap())?; - let Some(file) = TemplateFiles::get(item.as_ref()) else { + let Some(file) = WorkspaceTemplate::get(item.as_ref()) else { self.print .warnln(format!("Failed to read file: {}", item.as_ref())); continue; @@ -114,6 +140,40 @@ impl Runner { let file_contents = str::from_utf8(file.data.as_ref()).map_err(Error::ConvertBytesToString)?; + Self::write(&to, file_contents)?; + } + + Self::create_dir_all(project_path.join("contracts").as_path())?; + + Ok(()) + } + + fn copy_template_files(&self) -> Result<(), Error> { + let binding = Path::new(&self.args.project_path) + .join("contracts") + .join(&self.args.name); + let project_path = binding.as_path(); + + self.print.infoln(format!( + "Adding package to the workspace at {project_path:?}" + )); + + if project_path.exists() { + return Err(AlreadyExists(self.args.name.clone())); + } + + Self::create_dir_all(project_path)?; + + for item in ContractTemplate::iter() { + let mut to = project_path.join(item.as_ref()); + Self::create_dir_all(to.parent().unwrap())?; + + let Some(file) = ContractTemplate::get(item.as_ref()) else { + self.print + .warnln(format!("Failed to read file: {}", item.as_ref())); + continue; + }; + // We need to include the Cargo.toml file as Cargo.toml.removeextension in the template so that it will be included the package. This is making sure that the Cargo file is written as Cargo.toml in the new project. This is a workaround for this issue: https://github.com/rust-lang/cargo/issues/8597. let item_path = Path::new(item.as_ref()); if item_path.file_name().unwrap() == "Cargo.toml.removeextension" { @@ -121,27 +181,26 @@ impl Runner { to = project_path.join(item_parent_path).join("Cargo.toml"); } - if exists { - self.print - .plusln(format!("Writing {to:?} (overwriting existing file)")); - } else { - self.print.plusln(format!("Writing {to:?}")); + let file_contents = + str::from_utf8(file.data.as_ref()).map_err(Error::ConvertBytesToString)?; + + if let Some(file_name) = to.file_name() { + if file_name.to_str().unwrap_or("").contains("Cargo.toml") { + Self::write( + &to, + file_contents + .replace("contract-template", &self.args.name) + .as_str(), + )?; + continue; + } } + Self::write(&to, file_contents)?; } - - Self::create_dir_all(project_path.join("contracts").as_path())?; - Ok(()) } - fn file_exists(file_path: &Path) -> bool { - metadata(file_path) - .as_ref() - .map(Metadata::is_file) - .unwrap_or(false) - } - fn create_dir_all(path: &Path) -> Result<(), Error> { create_dir_all(path).map_err(|e| Error::Io(format!("creating directory: {path:?}"), e)) } @@ -153,64 +212,42 @@ impl Runner { #[cfg(test)] mod tests { - use std::fs; use std::fs::read_to_string; - use itertools::Itertools; + use tempfile::TempDir; use super::*; const TEST_PROJECT_NAME: &str = "test-project"; - #[test] - fn test_init() { - let temp_dir = tempfile::tempdir().unwrap(); + // Runs init command and checks that project has correct structure + fn run_init(temp_dir: &TempDir, name: &str) { let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); let runner = Runner { args: Cmd { project_path: project_dir.to_string_lossy().to_string(), + name: name.to_string(), with_example: None, frontend_template: None, - overwrite: false, + overwrite: None, }, print: print::Print::new(false), }; runner.run().unwrap(); - assert_base_template_files_exist(&project_dir); - assert_default_hello_world_contract_files_exist(&project_dir); - assert_excluded_paths_do_not_exist(&project_dir); - - assert_contract_cargo_file_is_well_formed(&project_dir, "hello_world"); - - assert_excluded_paths_do_not_exist(&project_dir); - - temp_dir.close().unwrap(); - } - - // test helpers - fn assert_base_template_files_exist(project_dir: &Path) { let expected_paths = ["contracts", "Cargo.toml", "README.md"]; for path in &expected_paths { assert!(project_dir.join(path).exists()); } - } - - fn assert_default_hello_world_contract_files_exist(project_dir: &Path) { - assert_contract_files_exist(project_dir, "hello_world"); - } - fn assert_contract_files_exist(project_dir: &Path, contract_name: &str) { - let contract_dir = project_dir.join("contracts").join(contract_name); + let contract_dir = project_dir.join("contracts").join(name); assert!(contract_dir.exists()); assert!(contract_dir.as_path().join("Cargo.toml").exists()); assert!(contract_dir.as_path().join("src").join("lib.rs").exists()); assert!(contract_dir.as_path().join("src").join("test.rs").exists()); - } - fn assert_contract_cargo_file_is_well_formed(project_dir: &Path, contract_name: &str) { - let contract_dir = project_dir.join("contracts").join(contract_name); + let contract_dir = project_dir.join("contracts").join(name); let cargo_toml_path = contract_dir.as_path().join("Cargo.toml"); let cargo_toml_str = read_to_string(cargo_toml_path.clone()).unwrap(); let doc = cargo_toml_str.parse::().unwrap(); @@ -269,21 +306,23 @@ mod tests { ); } - fn assert_excluded_paths_do_not_exist(project_dir: &Path) { - let base_excluded_paths = [".git", ".github", "Makefile", ".vscode", "target"]; - for path in &base_excluded_paths { - let filepath = project_dir.join(path); - assert!(!filepath.exists(), "{filepath:?} should not exist"); - } - let contract_excluded_paths = ["Makefile", "target", "Cargo.lock"]; - let contract_dirs = fs::read_dir(project_dir.join("contracts")) - .unwrap() - .map(|entry| entry.unwrap().path()); - contract_dirs - .cartesian_product(contract_excluded_paths.iter()) - .for_each(|(contract_dir, excluded_path)| { - let filepath = contract_dir.join(excluded_path); - assert!(!filepath.exists(), "{filepath:?} should not exist"); - }); + #[test] + fn test_init() { + let temp_dir = tempfile::tempdir().unwrap(); + + run_init(&temp_dir, "hello_world"); + + temp_dir.close().unwrap(); + } + + #[test] + fn test_add() { + let temp_dir = tempfile::tempdir().unwrap(); + + // Running init twice should add new member in the workspace + run_init(&temp_dir, "hello_world"); + run_init(&temp_dir, "hello_world_2"); + + temp_dir.close().unwrap(); } } diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index 761784de9..e39c3eadf 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -53,7 +53,12 @@ pub enum Cmd { #[command(subcommand)] Info(info::Cmd), - /// Initialize a Soroban project with an example contract + /// Initialize a Soroban contract. + /// + /// When running with empty or non-existent `--project-path`, this command will + /// generate a template Cargo workspace project and add a sample contract package. + /// When running in the existing Cargo project, it will add a new package for a sample contract + /// with a given `--name`. Init(init::Cmd), /// Inspect a WASM file listing contract functions, meta, etc diff --git a/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/Cargo.toml.removeextension b/cmd/soroban-cli/src/utils/contract-template/Cargo.toml.removeextension similarity index 89% rename from cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/Cargo.toml.removeextension rename to cmd/soroban-cli/src/utils/contract-template/Cargo.toml.removeextension index 2d8b3ac4e..bb090987c 100644 --- a/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/Cargo.toml.removeextension +++ b/cmd/soroban-cli/src/utils/contract-template/Cargo.toml.removeextension @@ -1,5 +1,5 @@ [package] -name = "hello-world" +name = "contract-template" version = "0.0.0" edition = "2021" publish = false diff --git a/cmd/soroban-cli/src/utils/contract-template/Makefile b/cmd/soroban-cli/src/utils/contract-template/Makefile new file mode 100644 index 000000000..7f774ad12 --- /dev/null +++ b/cmd/soroban-cli/src/utils/contract-template/Makefile @@ -0,0 +1,16 @@ +default: build + +all: test + +test: build + cargo test + +build: + stellar contract build + @ls -l target/wasm32-unknown-unknown/release/*.wasm + +fmt: + cargo fmt --all + +clean: + cargo clean diff --git a/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/lib.rs b/cmd/soroban-cli/src/utils/contract-template/src/lib.rs similarity index 100% rename from cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/lib.rs rename to cmd/soroban-cli/src/utils/contract-template/src/lib.rs diff --git a/cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/test.rs b/cmd/soroban-cli/src/utils/contract-template/src/test.rs similarity index 100% rename from cmd/soroban-cli/src/utils/contract-init-template/contracts/hello_world/src/test.rs rename to cmd/soroban-cli/src/utils/contract-template/src/test.rs diff --git a/cmd/soroban-cli/src/utils/contract-init-template/.gitignore b/cmd/soroban-cli/src/utils/contract-workspace-template/.gitignore similarity index 100% rename from cmd/soroban-cli/src/utils/contract-init-template/.gitignore rename to cmd/soroban-cli/src/utils/contract-workspace-template/.gitignore diff --git a/cmd/soroban-cli/src/utils/contract-init-template/Cargo.toml.removeextension b/cmd/soroban-cli/src/utils/contract-workspace-template/Cargo.toml.removeextension similarity index 100% rename from cmd/soroban-cli/src/utils/contract-init-template/Cargo.toml.removeextension rename to cmd/soroban-cli/src/utils/contract-workspace-template/Cargo.toml.removeextension diff --git a/cmd/soroban-cli/src/utils/contract-init-template/README.md b/cmd/soroban-cli/src/utils/contract-workspace-template/README.md similarity index 100% rename from cmd/soroban-cli/src/utils/contract-init-template/README.md rename to cmd/soroban-cli/src/utils/contract-workspace-template/README.md