Skip to content

Commit

Permalink
Update contract init (#1625)
Browse files Browse the repository at this point in the history
* contract init: add removal memo

* Rework contract init

* Update impl

* Improve messages
  • Loading branch information
Ifropc authored Oct 11, 2024
1 parent 8f8d2ac commit a041c68
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 31 deletions.
9 changes: 7 additions & 2 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 project
* `inspect` — (Deprecated in favor of `contract info` subcommands) 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
Expand Down Expand Up @@ -607,7 +607,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 project.

This command will create a Cargo workspace project and add a sample Stellar contract. The name of the contract can be specified by `--name`. It can be run multiple times with different names in order to generate multiple contracts, and files won't be overwritten unless `--overwrite` is passed.

**Usage:** `stellar contract init [OPTIONS] <PROJECT_PATH>`

Expand All @@ -617,6 +619,9 @@ Initialize a Soroban project with an example contract

###### **Options:**

* `--name <NAME>` — An optional flag to specify a new contract's name.

Default value: `hello-world`
* `--overwrite` — Overwrite all existing files.


Expand Down
63 changes: 62 additions & 1 deletion cmd/crates/soroban-test/tests/it/init.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use assert_fs::prelude::*;
use predicates::prelude::predicate;
use soroban_test::TestEnv;
use soroban_test::{AssertExt, TestEnv};

#[test]
fn init() {
Expand All @@ -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();
}
106 changes: 80 additions & 26 deletions cmd/soroban-cli/src/commands/contract/init.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::{
fs::{create_dir_all, metadata, write, Metadata},
io,
Expand All @@ -21,12 +22,19 @@ such as `soroban-template` or `soroban-frontend-template`";
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 future version (23+) https://github.com/stellar/stellar-cli/issues/1586
#[arg(
short,
long,
hide = true,
display_order = 100,
display_order = 100,
value_parser = error_on_use_of_removed_arg!(String, EXAMPLE_REMOVAL_NOTICE)
)]
pub with_example: Option<String>,
Expand Down Expand Up @@ -79,8 +87,12 @@ impl Cmd {
}

#[derive(RustEmbed)]
#[folder = "src/utils/contract-init-template"]
struct TemplateFiles;
#[folder = "src/utils/contract-workspace-template"]
struct WorkspaceTemplateFiles;

#[derive(RustEmbed)]
#[folder = "src/utils/contract-template"]
struct ContractTemplateFiles;

struct Runner {
args: Cmd,
Expand All @@ -91,19 +103,49 @@ 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:?}"));
.infoln(format!("Initializing workspace 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)?;
self.copy_template_files()?;
self.copy_template_files(
project_path.as_path(),
&mut WorkspaceTemplateFiles::iter(),
WorkspaceTemplateFiles::get,
)?;

let contract_path = project_path.join("contracts").join(&self.args.name);
self.print
.infoln(format!("Initializing contract at {contract_path:?}"));

Self::create_dir_all(contract_path.as_path())?;
self.copy_template_files(
contract_path.as_path(),
&mut ContractTemplateFiles::iter(),
ContractTemplateFiles::get,
)?;

Ok(())
}

fn copy_template_files(&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());
fn copy_template_files(
&self,
root_path: &Path,
files: &mut dyn Iterator<Item = Cow<str>>,
getter: fn(&str) -> Option<rust_embed::EmbeddedFile>,
) -> Result<(), Error> {
for item in &mut *files {
let mut to = root_path.join(item.as_ref());
// 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());
let is_toml = item_path.file_name().unwrap() == "Cargo.toml.removeextension";
if is_toml {
let item_parent_path = item_path.parent().unwrap();
to = root_path.join(item_parent_path).join("Cargo.toml");
}

let exists = Self::file_exists(&to);
if exists && !self.args.overwrite {
self.print
Expand All @@ -113,20 +155,19 @@ impl Runner {

Self::create_dir_all(to.parent().unwrap())?;

let Some(file) = TemplateFiles::get(item.as_ref()) else {
let Some(file) = getter(item.as_ref()) else {
self.print
.warnln(format!("Failed to read file: {}", item.as_ref()));
continue;
};

let file_contents =
str::from_utf8(file.data.as_ref()).map_err(Error::ConvertBytesToString)?;
let mut file_contents = str::from_utf8(file.data.as_ref())
.map_err(Error::ConvertBytesToString)?
.to_string();

// 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" {
let item_parent_path = item_path.parent().unwrap();
to = project_path.join(item_parent_path).join("Cargo.toml");
if is_toml {
let new_content = file_contents.replace("%contract-template%", &self.args.name);
file_contents = new_content;
}

if exists {
Expand All @@ -135,11 +176,9 @@ impl Runner {
} else {
self.print.plusln(format!("Writing {to:?}"));
}
Self::write(&to, file_contents)?;
Self::write(&to, &file_contents)?;
}

Self::create_dir_all(project_path.join("contracts").as_path())?;

Ok(())
}

Expand Down Expand Up @@ -177,6 +216,7 @@ mod tests {
let runner = Runner {
args: Cmd {
project_path: project_dir.to_string_lossy().to_string(),
name: "hello_world".to_string(),
with_example: None,
frontend_template: None,
overwrite: false,
Expand All @@ -186,11 +226,29 @@ mod tests {
runner.run().unwrap();

assert_base_template_files_exist(&project_dir);
assert_default_hello_world_contract_files_exist(&project_dir);

assert_contract_files_exist(&project_dir, "hello_world");
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);

let runner = Runner {
args: Cmd {
project_path: project_dir.to_string_lossy().to_string(),
name: "contract2".to_string(),
with_example: None,
frontend_template: None,
overwrite: false,
},
print: print::Print::new(false),
};
runner.run().unwrap();

assert_contract_files_exist(&project_dir, "contract2");
assert_excluded_paths_do_not_exist(&project_dir);

assert_contract_cargo_file_is_well_formed(&project_dir, "contract2");
assert_excluded_paths_do_not_exist(&project_dir);

temp_dir.close().unwrap();
Expand All @@ -204,10 +262,6 @@ mod tests {
}
}

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);

Expand Down Expand Up @@ -283,7 +337,7 @@ mod tests {
let filepath = project_dir.join(path);
assert!(!filepath.exists(), "{filepath:?} should not exist");
}
let contract_excluded_paths = ["Makefile", "target", "Cargo.lock"];
let contract_excluded_paths = ["target", "Cargo.lock"];
let contract_dirs = fs::read_dir(project_dir.join("contracts"))
.unwrap()
.map(|entry| entry.unwrap().path());
Expand Down
7 changes: 6 additions & 1 deletion cmd/soroban-cli/src/commands/contract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ pub enum Cmd {
#[command(subcommand)]
Info(info::Cmd),

/// Initialize a Soroban project with an example contract
/// Initialize a Soroban contract project.
///
/// This command will create a Cargo workspace project and add a sample Stellar contract.
/// The name of the contract can be specified by `--name`. It can be run multiple times
/// with different names in order to generate multiple contracts, and files won't
/// be overwritten unless `--overwrite` is passed.
Init(init::Cmd),

/// (Deprecated in favor of `contract info` subcommands) Inspect a WASM file listing contract functions, meta, etc
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "hello-world"
name = "%contract-template%"
version = "0.0.0"
edition = "2021"
publish = false
Expand Down
16 changes: 16 additions & 0 deletions cmd/soroban-cli/src/utils/contract-template/Makefile
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a041c68

Please sign in to comment.