Skip to content

Commit

Permalink
feat: output compilers
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Nov 1, 2023
1 parent cab828b commit bf6cd4e
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 51 deletions.
19 changes: 19 additions & 0 deletions docs/userguides/compile.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<project>/.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:
Expand Down
2 changes: 1 addition & 1 deletion src/ape/api/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion src/ape/managers/compilers.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]:
"""
Expand Down
57 changes: 12 additions & 45 deletions src/ape/managers/project/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
41 changes: 37 additions & 4 deletions tests/functional/test_project.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
import shutil
from pathlib import Path
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit bf6cd4e

Please sign in to comment.