diff --git a/docs/userguides/compile.md b/docs/userguides/compile.md index 2a555f212c..2ca36fd97e 100644 --- a/docs/userguides/compile.md +++ b/docs/userguides/compile.md @@ -111,6 +111,25 @@ solidity = compilers.get_compiler("solidity", settings=settings["solidity"]) vyper.compile([Path("path/to/contract.sol")]) ``` +### Settings Artifacts + +Compiler build artifacts can be found in the the `/.build/compilers.json` file. +Each compiler in your project has an associated [ethpm_types.source.Compiler](https://github.com/ApeWorX/ethpm-types/blob/main/ethpm_types/source.py) build artifact. +This data contains the versions and settings used for each contract in your project. +For example, assume you have`ape-solidity` creating a contract named `Test` from file `Test.sol`. +The structure of `.builds/compilers.json` file would be: + +```json +[ + { + "contractTypes": ["Test"], + "name": "solidity", + "settings": {"optimizer": {"enabled": true, "runs": 200}, "outputSelection": {"Test.sol": {"": ["ast"], "*": ["abi", "bin-runtime", "devdoc", "userdoc", "evm.bytecode.object", "evm.bytecode.sourceMap", "evm.deployedBytecode.object"]}}, "viaIR": false}, + "version": "0.8.17+commit.8df45f5f" + } +] +``` + ## Compile Source Code Instead of compiling project source files, you can compile code (str) directly: diff --git a/src/ape/api/projects.py b/src/ape/api/projects.py index ba7c45c484..a33af3d933 100644 --- a/src/ape/api/projects.py +++ b/src/ape/api/projects.py @@ -143,7 +143,7 @@ def contracts(self) -> Dict[str, ContractType]: contracts = {} for p in self._cache_folder.glob("*.json"): - if p == self.manifest_cachefile: + if p == self.manifest_cachefile or p.name.startswith(".") or not p.is_file(): continue contract_name = p.stem diff --git a/src/ape/managers/compilers.py b/src/ape/managers/compilers.py index ca4b7c557f..52579c46b8 100644 --- a/src/ape/managers/compilers.py +++ b/src/ape/managers/compilers.py @@ -1,8 +1,9 @@ +import json from pathlib import Path from typing import Any, Dict, List, Optional, Sequence, Set, Union from ethpm_types import ContractType -from ethpm_types.source import Content +from ethpm_types.source import Compiler, Content from ape.api import CompilerAPI from ape.contracts import ContractContainer @@ -42,6 +43,19 @@ def __getattr__(self, name: str) -> Any: raise ApeAttributeError(f"No attribute or compiler named '{name}'.") + @property + def compilers_data_file(self) -> Path: + # NOTE: Private file to avoid collision with contract type JSONs. + return self.project_manager.local_project._cache_folder / ".compilers.json" + + @property + def compiler_data(self) -> List[Compiler]: + return ( + [Compiler.parse_obj(x) for x in json.loads(self.compilers_data_file.read_text())] + if self.compilers_data_file.is_file() + else [] + ) + @property def registered_compilers(self) -> Dict[str, CompilerAPI]: """ diff --git a/src/ape/managers/project/manager.py b/src/ape/managers/project/manager.py index c214a41afb..8fb5da0f4f 100644 --- a/src/ape/managers/project/manager.py +++ b/src/ape/managers/project/manager.py @@ -12,11 +12,10 @@ from ape.api import DependencyAPI, ProjectAPI from ape.api.networks import LOCAL_NETWORK_NAME from ape.contracts import ContractContainer, ContractInstance, ContractNamespace -from ape.exceptions import ApeAttributeError, APINotImplementedError, ChainError, ProjectError +from ape.exceptions import ApeAttributeError, ChainError, ProjectError from ape.logging import logger from ape.managers.base import BaseManager from ape.managers.project.types import ApeProject, BrownieProject -from ape.utils import get_relative_path class ProjectManager(BaseManager): @@ -162,50 +161,18 @@ def compiler_data(self) -> List[Compiler]: """ return self._get_compiler_data() - def _get_compiler_data(self, compile_if_needed: bool = True): - contract_types: Iterable[ContractType] = ( - self.contracts.values() - if compile_if_needed - else self._get_cached_contract_types().values() - ) - compiler_list: List[Compiler] = [] - contracts_folder = self.config_manager.contracts_folder - for ext, compiler in self.compiler_manager.registered_compilers.items(): - sources = [x for x in self.source_paths if x.is_file() and x.suffix == ext] - if not sources: - continue + def _get_compiler_data(self, compile_if_needed: bool = True) -> List[Compiler]: + if not self.compiler_manager.compilers_data_file.is_file(): + if compile_if_needed: + self.load_contracts() - try: - version_map = compiler.get_version_map(sources, contracts_folder) - except APINotImplementedError: - versions = list(compiler.get_versions(sources)) - if len(versions) == 0: - # Skipping compilers that don't use versions - # These are unlikely to be part of the published manifest - continue - elif len(versions) > 1: - raise (ProjectError(f"Unable to create version map for '{ext}'.")) - - version = versions[0] - version_map = {version: sources} - - settings = compiler.get_compiler_settings(sources, base_path=contracts_folder) - for version, paths in version_map.items(): - version_settings = settings.get(version, {}) if version and settings else {} - source_ids = [str(get_relative_path(p, contracts_folder)) for p in paths] - filtered_contract_types = [ - ct for ct in contract_types if ct.source_id in source_ids - ] - contract_type_names = [ct.name for ct in filtered_contract_types if ct.name] - compiler_list.append( - Compiler( - name=compiler.name, - version=str(version), - settings=version_settings, - contractTypes=contract_type_names, - ) - ) - return compiler_list + if self.compiler_manager.compilers_data_file.is_file(): + # After compiling, settings files were generated. + return self._get_compiler_data(compile_if_needed=False) + + return [] + + return sorted(self.compiler_manager.compiler_data, key=lambda c: c.name) @property def meta(self) -> PackageMeta: diff --git a/tests/functional/test_project.py b/tests/functional/test_project.py index 1b81ca2a6d..a56890ce8e 100644 --- a/tests/functional/test_project.py +++ b/tests/functional/test_project.py @@ -1,3 +1,4 @@ +import json import os import shutil from pathlib import Path @@ -8,6 +9,7 @@ from ethpm_types import ContractInstance as EthPMContractInstance from ethpm_types import ContractType, Source from ethpm_types.manifest import PackageManifest +from ethpm_types.source import Compiler from ape import Contract from ape.exceptions import ProjectError @@ -101,6 +103,37 @@ def project_without_deployments(project): return project +@pytest.fixture +def solidity_compiler_artifact(project, compilers): + _ = project # Ensure this happens _in_ the root project. + compilers.compilers_data_file.unlink(missing_ok=True) + compiler_data = { + "contractTypes": ["Test"], + "name": "solidity", + "settings": { + "optimizer": {"enabled": True, "runs": 200}, + "outputSelection": { + "Test.sol": { + "": ["ast"], + "*": [ + "abi", + "bin-runtime", + "devdoc", + "userdoc", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + ], + } + }, + "viaIR": False, + }, + "version": "0.8.17+commit.8df45f5f", + } + compilers.compilers_data_file.write_text(json.dumps([compiler_data])) + return Compiler.parse_obj(compiler_data) + + def _make_new_contract(existing_contract: ContractType, name: str): source_text = existing_contract.json() source_text = source_text.replace(f"{existing_contract.name}.vy", f"{name}.json") @@ -328,10 +361,10 @@ def test_track_deployment_from_unknown_contract_given_txn_hash( assert actual.runtime_bytecode == contract.contract_type.runtime_bytecode -def test_compiler_data(config, project_path, contracts_folder): - # See ape-solidity / ape-vyper for better tests - with config.using_project(project_path, contracts_folder=contracts_folder) as project: - assert not project.compiler_data +def test_compiler_data(solidity_compiler_artifact, project): + actual = project.compiler_data + assert len(actual) == 1 + assert actual[0] == solidity_compiler_artifact def test_get_project_without_contracts_path(project):