Skip to content

Commit

Permalink
Merge pull request #342 from multiversx/contract-template
Browse files Browse the repository at this point in the history
Contract templates and contract new from template using sc-meta
  • Loading branch information
popenta authored Oct 3, 2023
2 parents 4cfcf50 + 68abe21 commit c3caf06
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 437 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ venv.bak/

# Typings
typings

multiversx_sdk_cli/tests/testdata-out
21 changes: 15 additions & 6 deletions multiversx_sdk_cli/cli_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from multiversx_sdk_cli.docker import is_docker_installed, run_docker
from multiversx_sdk_cli.errors import DockerMissingError, NoWalletProvided
from multiversx_sdk_cli.projects.core import get_project_paths_recursively
from multiversx_sdk_cli.projects.templates import Contract
from multiversx_sdk_cli.ux import show_message

logger = logging.getLogger("cli.contracts")

Expand All @@ -28,14 +30,16 @@ def setup_parser(args: List[str], subparsers: Any) -> Any:

sub = cli_shared.add_command_subparser(subparsers, "contract", "new",
"Create a new Smart Contract project based on a template.")
sub.add_argument("name")
sub.add_argument("--name", help="The name of the contract. If missing, the name of the template will be used.")
sub.add_argument("--template", required=True, help="the template to use")
sub.add_argument("--directory", type=str, default=os.getcwd(),
help="🗀 the parent directory of the project (default: current directory)")
sub.add_argument("--tag", help="the framework version on which the contract should be created")
sub.add_argument("--path", type=str, default=os.getcwd(),
help="the parent directory of the project (default: current directory)")
sub.set_defaults(func=create)

sub = cli_shared.add_command_subparser(subparsers, "contract", "templates",
"List the available Smart Contract templates.")
sub.add_argument("--tag", help="the sc-meta framework version referred to")
sub.set_defaults(func=list_templates)

sub = cli_shared.add_command_subparser(subparsers, "contract", "build",
Expand Down Expand Up @@ -242,15 +246,20 @@ def _add_metadata_arg(sub: Any):


def list_templates(args: Any):
projects.list_project_templates()
tag = args.tag
contract = Contract(tag)
templates = contract.get_contract_templates()
show_message(templates)


def create(args: Any):
name = args.name
template = args.template
directory = Path(args.directory)
tag = args.tag
path = Path(args.path)

projects.create_from_template(name, template, directory)
contract = Contract(tag, name, template, path)
contract.create_from_template()


def get_project_paths(args: Any) -> List[Path]:
Expand Down
9 changes: 4 additions & 5 deletions multiversx_sdk_cli/projects/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from multiversx_sdk_cli.projects.core import (build_project, clean_project,
get_projects_in_workspace, load_project,
run_tests)
get_projects_in_workspace,
load_project, run_tests)
from multiversx_sdk_cli.projects.project_base import Project
from multiversx_sdk_cli.projects.project_clang import ProjectClang
from multiversx_sdk_cli.projects.project_cpp import ProjectCpp
from multiversx_sdk_cli.projects.project_rust import ProjectRust
from multiversx_sdk_cli.projects.project_sol import ProjectSol
from multiversx_sdk_cli.projects.report.do_report import do_report
from multiversx_sdk_cli.projects.templates import (create_from_template,
list_project_templates)
from multiversx_sdk_cli.projects.templates import Contract

__all__ = ["build_project", "clean_project", "do_report", "run_tests", "get_projects_in_workspace", "load_project", "Project", "ProjectClang", "ProjectCpp", "ProjectRust", "ProjectSol", "create_from_template", "list_project_templates"]
__all__ = ["build_project", "clean_project", "do_report", "run_tests", "get_projects_in_workspace", "load_project", "Project", "ProjectClang", "ProjectCpp", "ProjectRust", "ProjectSol", "Contract"]
273 changes: 37 additions & 236 deletions multiversx_sdk_cli/projects/templates.py
Original file line number Diff line number Diff line change
@@ -1,254 +1,55 @@
import json
import logging
import shutil
from pathlib import Path
from typing import Any, List, Tuple
from typing import Union

from multiversx_sdk_cli import errors, utils
from multiversx_sdk_cli.projects import shared
from multiversx_sdk_cli.projects.project_rust import CargoFile
from multiversx_sdk_cli.projects.templates_config import \
get_templates_repositories
from multiversx_sdk_cli.projects.templates_repository import \
TemplatesRepository
from multiversx_sdk_cli import myprocess
from multiversx_sdk_cli.dependencies.install import install_module

logger = logging.getLogger("projects.templates")


def list_project_templates():
summaries: List[TemplateSummary] = []

for repository in get_templates_repositories():
repository.download()
for template in repository.get_templates():
summaries.append(TemplateSummary(template, repository))

summaries = sorted(summaries, key=lambda item: item.name)

pretty_json = json.dumps([item.__dict__ for item in summaries], indent=4)
print(pretty_json)


class TemplateSummary():
def __init__(self, name: str, repository: TemplatesRepository):
class Contract:
def __init__(self,
tag: Union[str, None] = None,
name: Union[str, None] = None,
template: str = "",
path: Path = Path()
) -> None:
self.tag = tag
self.name = name
self.github = repository.github
self.language = repository.get_language(name)


def create_from_template(project_name: str, template_name: str, directory: Path):
directory = directory.expanduser()

logger.info("create_from_template.project_name: %s", project_name)
logger.info("create_from_template.template_name: %s", template_name)
logger.info("create_from_template.directory: %s", directory)

if not directory:
logger.info("Using current directory")
directory = Path.cwd()

project_directory = Path(directory) / project_name
if project_directory.exists():
raise errors.BadDirectory(str(project_directory))

_download_templates_repositories()
_copy_template(template_name, project_directory, project_name)

template = _load_as_template(project_directory)
template.apply(template_name, project_name)

logger.info("Project created, template applied.")


def _download_templates_repositories():
for repo in get_templates_repositories():
repo.download()


def _copy_template(template: str, destination_path: Path, project_name: str):
for repo in get_templates_repositories():
if repo.has_template(template):
source_path = repo.get_template_folder(template)
shutil.copytree(source_path, destination_path)
return

raise errors.TemplateMissingError(template)


def _load_as_template(directory: Path):
if shared.is_source_rust(directory):
return TemplateRust(directory)
raise errors.BadTemplateError(directory)


class Template:
def __init__(self, directory: Path):
self.directory = directory

def apply(self, template_name: str, project_name: str):
self.template_name = template_name
self.project_name = project_name
self._patch()

def _patch(self):
"""Implemented by derived classes"""
pass


class TemplateRust(Template):
CARGO_TOML = "Cargo.toml"

def _patch(self):
logger.info("Patching cargo files...")
self._patch_cargo()
self._patch_sub_crate("wasm")
self._patch_sub_crate("abi")
self._patch_sub_crate("meta")
template_name = self._with_underscores(self.template_name)

tests = (self.directory / "tests").glob("*.rs")

source_code_files = [
self.directory / "src" / f"{template_name}.rs",
self.directory / "src" / "lib.rs",
self.directory / "abi" / "src" / "main.rs",
self.directory / "wasm" / "src" / "lib.rs",
self.directory / "meta" / "src" / "main.rs",
]

source_code_files.extend(tests)

logger.info("Patching source code...")
self._patch_source_code_files(source_code_files, ignore_missing=True)
self._patch_source_code_tests()

logger.info("Patching test files...")
self._patch_scenarios_tests()

def _patch_cargo(self):
cargo_path = self.directory / TemplateRust.CARGO_TOML

cargo_file = CargoFile(cargo_path)
cargo_file.package_name = self.project_name
cargo_file.version = "0.0.0"
cargo_file.authors = ["you"]
cargo_file.edition = "2018"
cargo_file.publish = False

remove_path_from_dependencies(cargo_file)

cargo_file.save()

def _patch_sub_crate(self, sub_name: str) -> None:
cargo_path = self.directory / sub_name / TemplateRust.CARGO_TOML
if not cargo_path.is_file():
return

cargo_file = CargoFile(cargo_path)
cargo_file.package_name = f"{self.project_name}-{sub_name}"
cargo_file.version = "0.0.0"
cargo_file.authors = ["you"]
cargo_file.edition = "2018"
cargo_file.publish = False

remove_path_from_dependencies(cargo_file)

# Patch the path towards the project crate (one folder above):
cargo_file.get_dependency(self.template_name)["path"] = ".."

cargo_file.save()

self._replace_in_files(
[cargo_path],
[
(f"[dependencies.{self.template_name}]", f"[dependencies.{self.project_name}]")
],
ignore_missing=False
)

def _with_underscores(self, name: str) -> str:
return name.replace('-', '_')

def _contract_name(self, name: str) -> str:
chars = name.replace("-", " ").replace("_", " ").split()
return ''.join(i.capitalize() for i in chars[0:])

def _patch_source_code_files(self, source_paths: List[Path], ignore_missing: bool) -> None:
template_name = self._with_underscores(self.template_name)
project_name = self._with_underscores(self.project_name)
template_contract_name = self._contract_name(self.template_name)
project_contract_name = self._contract_name(self.project_name)

self._replace_in_files(
source_paths,
[
# Example: replace contract name "pub trait SimpleERC20" to "pub trait MyContract"
(f"pub trait {template_contract_name}", f"pub trait {project_contract_name}"),
# Example: replace "simple_erc20.wasm" to "my_token.wasm"
(f"{self.template_name}.wasm", f"{self.project_name}.wasm"),
# Example: replace "use simple_erc20::*" to "use my_token::*"
(f"use {template_name}::*", f"use {project_name}::*"),
# Example: replace "<simple_erc20::AbiProvider>()" to "<my_token::AbiProvider>()"
(f"<{template_name}::AbiProvider>()", f"<{project_name}::AbiProvider>()"),
# Example: replace "extern crate adder;" to "extern crate myadder;"
(f"extern crate {template_name};", f"extern crate {project_name};"),
# Example: replace "empty::ContractObj" to "foo_bar::ContractObj"
(f"{template_name}::ContractObj", f"{project_name}::ContractObj"),
(f"{template_name}::ContractBuilder", f"{project_name}::ContractBuilder"),
(f"{template_name}::contract_obj", f"{project_name}::contract_obj"),
],
ignore_missing
)

def _patch_source_code_tests(self):
test_dir_path = self.directory / "tests"
if not test_dir_path.is_dir():
return

test_paths = utils.list_files(test_dir_path)
self._patch_source_code_files(test_paths, ignore_missing=False)
self.template = template
self.path = path

def _patch_scenarios_tests(self):
test_dir_path = self.directory / "scenarios"
if not test_dir_path.is_dir():
return
def get_contract_templates(self) -> str:
self._ensure_dependencies_installed()
args = self._prepare_args_to_list_templates()
templates = myprocess.run_process(args=args, dump_to_stdout=False)
return templates

test_paths = utils.list_files(test_dir_path, suffix=".json")
self._replace_in_files(
test_paths,
[
(f"{self.template_name}.wasm", f"{self.project_name}.wasm")
],
ignore_missing=False
)
def create_from_template(self) -> None:
self._ensure_dependencies_installed()
args = self._prepare_args_to_create_new_contract_from_template()
myprocess.run_process(args)

for file in test_paths:
data = utils.read_json_file(file)
# Patch fields
data["name"] = data.get("name", "").replace(self.template_name, self.project_name)
utils.write_json_file(str(file), data)
def _ensure_dependencies_installed(self):
logger.info("Checking if the necessarry dependencies are installed.")
install_module("rust")

def _replace_in_files(self, files: List[Path], replacements: List[Tuple[str, str]], ignore_missing: bool) -> None:
for file in files:
if ignore_missing and not file.exists():
continue
content = file.read_text()
def _prepare_args_to_list_templates(self) -> list[str]:
args = ["sc-meta", "templates"]

for to_replace, replacement in replacements:
content = content.replace(to_replace, replacement)
if self.tag:
args.extend(["--tag", self.tag])

utils.write_file(file, content)
return args

def _prepare_args_to_create_new_contract_from_template(self) -> list[str]:
args = ["sc-meta", "new", "--template", self.template, "--path", str(self.path)]

def remove_path(dependency: Any) -> None:
try:
del dependency["path"]
except TypeError:
pass
if self.name:
args.extend(["--name", self.name])

if self.tag:
args.extend(["--tag", self.tag])

def remove_path_from_dependencies(cargo_file: CargoFile) -> None:
for dependency in cargo_file.get_dependencies().values():
remove_path(dependency)
for dependency in cargo_file.get_dev_dependencies().values():
remove_path(dependency)
return args
Loading

0 comments on commit c3caf06

Please sign in to comment.