diff --git a/moccasin/__main__.py b/moccasin/__main__.py index 732429c..5c44434 100644 --- a/moccasin/__main__.py +++ b/moccasin/__main__.py @@ -115,11 +115,11 @@ def generate_main_parser_and_sub_parsers() -> ( # ------------------------------------------------------------------ # COMPILE COMMAND # ------------------------------------------------------------------ - sub_parsers.add_parser( + compile_parser = sub_parsers.add_parser( "compile", help="Compiles the project.", - description="""Compiles all Vyper contracts in the project. \n -This command will: + description="""Compiles a specific Vyper contract or all vyper contracts in the project. \n +If no contract or contract path is given, this command will: 1. Find all .vy files in the src/ directory 2. Compile each file using the Vyper compiler 3. Output the compiled artifacts to the out/ directory @@ -130,6 +130,12 @@ def generate_main_parser_and_sub_parsers() -> ( parents=[parent_parser], ) + compile_parser.add_argument( + "contract_or_contract_path", + nargs="?", + help="Optional argument to compile a specific contract.", + ) + # ------------------------------------------------------------------ # TEST COMMAND # ------------------------------------------------------------------ diff --git a/moccasin/commands/compile.py b/moccasin/commands/compile.py index b25827c..ad9b279 100644 --- a/moccasin/commands/compile.py +++ b/moccasin/commands/compile.py @@ -15,16 +15,24 @@ from argparse import Namespace -def main(_: Namespace) -> int: +def main(args: Namespace) -> int: initialize_global_config() config = get_config() project_path: Path = config.get_root() - compile_project( - project_path, - project_path.joinpath(config.out_folder), - project_path.joinpath(config.contracts_folder), - write_data=True, - ) + + if args.contract_or_contract_path: + contract_path = config._find_contract(args.contract_or_contract_path) + compile_( + contract_path, project_path.joinpath(config.out_folder), write_data=True + ) + logger.info(f"Done compiling {contract_path.stem}") + else: + compile_project( + project_path, + project_path.joinpath(config.out_folder), + project_path.joinpath(config.contracts_folder), + write_data=True, + ) return 0 @@ -49,7 +57,11 @@ def compile_project( logger.info(f"Compiling {len(contracts_to_compile)} contracts to {build_folder}...") for contract_path in contracts_to_compile: - compile_(contract_path, build_folder, write_data=write_data) + try: + compile_(contract_path, build_folder, write_data=write_data) + except vyper.exceptions.InitializerException: + logger.info(f"Skipping contract {contract_path.stem} due to uninitialized.") + continue logger.info("Done compiling project!") @@ -104,6 +116,6 @@ def compile_( json.dump(build_data, f, indent=4) logger.debug(f"Compilation data saved to {build_file}") - logger.debug("Done compiling {contract_name}") + logger.debug(f"Done compiling {contract_name}") return deployer diff --git a/tests/cli/test_cli_compile.py b/tests/cli/test_cli_compile.py index 439d072..dfb2b69 100644 --- a/tests/cli/test_cli_compile.py +++ b/tests/cli/test_cli_compile.py @@ -37,3 +37,19 @@ def test_compile_alias_build_project(complex_cleanup_out_folder, mox_path): os.chdir(current_dir) assert "Running compile command" in result.stderr assert result.returncode == 0 + + +def test_compile_one(complex_cleanup_out_folder, mox_path): + current_dir = Path.cwd() + try: + os.chdir(current_dir.joinpath(COMPLEX_PROJECT_PATH)) + result = subprocess.run( + [mox_path, "build", "BuyMeACoffee.vy"], + check=True, + capture_output=True, + text=True, + ) + finally: + os.chdir(current_dir) + assert "Done compiling BuyMeACoffee" in result.stderr + assert result.returncode == 0 diff --git a/tests/data/complex_project/contracts/Auth.vy b/tests/data/complex_project/contracts/Auth.vy new file mode 100644 index 0000000..14e6dfe --- /dev/null +++ b/tests/data/complex_project/contracts/Auth.vy @@ -0,0 +1,17 @@ +# pragma version ^0.4.0 + +# Not export to importing module? +owner: public(address) + +@deploy +def __init__(): + self.owner = msg.sender + +def _check_owner(): + assert self.owner == msg.sender + +# Must be exported by importing module +@external +def set_owner(owner: address): + self._check_owner() + self.owner = owner \ No newline at end of file diff --git a/tests/data/complex_project/contracts/InitializedAuth.vy b/tests/data/complex_project/contracts/InitializedAuth.vy new file mode 100644 index 0000000..8bcec33 --- /dev/null +++ b/tests/data/complex_project/contracts/InitializedAuth.vy @@ -0,0 +1,16 @@ +# pragma version ^0.4.0 + +import auth +import UninitializedAuth + +initializes: auth +# auth is dependency of auth_2_step +initializes: UninitializedAuth[auth := auth] + +# export all external functions +exports: UninitializedAuth.__interface__ + +@deploy +def __init__(): + auth.__init__() + UninitializedAuth.__init__() \ No newline at end of file diff --git a/tests/data/complex_project/contracts/UninitializedAuth.vy b/tests/data/complex_project/contracts/UninitializedAuth.vy new file mode 100644 index 0000000..5fd85d8 --- /dev/null +++ b/tests/data/complex_project/contracts/UninitializedAuth.vy @@ -0,0 +1,24 @@ +# pragma version ^0.4.0 +import auth + +# This contract is not a valid contract. auth.__init__() must be called +# by a contract that imports and uses this contract + +uses: auth + +pending_owner: address + +@deploy +def __init__(): + pass + +@external +def begin_transfer(new_owner: address): + auth._check_owner() + self.pending_owner = new_owner + +@external +def accept_transfer(): + assert msg.sender == self.pending_owner + auth.owner = msg.sender + self.pending_owner = empty(address) \ No newline at end of file