Skip to content

Commit

Permalink
Merge pull request #878 from eth-brownie/feat-depdendency-artifacts
Browse files Browse the repository at this point in the history
Include depdendency artifacts in `build/contracts/`
  • Loading branch information
iamdefinitelyahuman authored Dec 5, 2020
2 parents 8ae835a + 615e538 commit 5cfaac2
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 51 deletions.
9 changes: 6 additions & 3 deletions brownie/project/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ def __init__(self, sources: Sources) -> None:
self._contracts: Dict = {}
self._interfaces: Dict = {}

def _add_contract(self, build_json: Dict) -> None:
contract_name = build_json["contractName"]
def _add_contract(self, build_json: Dict, alias: str = None) -> None:
contract_name = alias or build_json["contractName"]
if contract_name in self._contracts and build_json["type"] == "interface":
return
self._contracts[contract_name] = build_json
Expand Down Expand Up @@ -115,7 +115,10 @@ def _remove_interface(self, contract_name: str) -> None:

def get(self, contract_name: str) -> Dict:
"""Returns build data for the given contract name."""
return self._contracts[self._stem(contract_name)]
key = self._stem(contract_name)
if key in self._contracts:
return self._contracts[key]
return self._interfaces[key]

def items(self, path: Optional[str] = None) -> Union[ItemsView, List]:
"""Provides an list of tuples as (key,value), similar to calling dict.items.
Expand Down
88 changes: 48 additions & 40 deletions brownie/project/compiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
from typing import Dict, Optional

import solcast
from eth_utils import remove_0x_prefix
from semantic_version import Version

Expand All @@ -18,7 +19,7 @@
install_solc,
set_solc_version,
)
from brownie.project.compiler.utils import merge_natspec
from brownie.project.compiler.utils import _get_alias, merge_natspec
from brownie.project.compiler.vyper import find_vyper_versions, set_vyper_version
from brownie.utils import notify

Expand Down Expand Up @@ -138,12 +139,8 @@ def compile_and_format(
)

output_json = compile_from_input_json(input_json, silent, allow_paths)

output_json["contracts"] = {
k: v for k, v in output_json["contracts"].items() if k in path_list
}

build_json.update(generate_build_json(input_json, output_json, compiler_data, silent))

return build_json


Expand Down Expand Up @@ -280,33 +277,40 @@ def generate_build_json(
compiler_data = {}
compiler_data["evm_version"] = input_json["settings"]["evmVersion"]
build_json: Dict = {}
path_list = list(input_json["sources"])

if input_json["language"] == "Solidity":
compiler_data["optimizer"] = input_json["settings"]["optimizer"]
source_nodes, statement_nodes, branch_nodes = solidity._get_nodes(output_json)

for path_str, contract_name in [
(k, v) for k in path_list for v in output_json["contracts"].get(k, {})
(k, x) for k, v in output_json["contracts"].items() for x in v.keys()
]:
contract_alias = contract_name

if path_str in input_json["sources"]:
source = input_json["sources"][path_str]["content"]
else:
with Path(path_str).open() as fp:
source = fp.read()
contract_alias = _get_alias(contract_name, path_str)

if not silent:
print(f" - {contract_name}...")
print(f" - {contract_alias}")

abi = output_json["contracts"][path_str][contract_name]["abi"]
natspec = merge_natspec(
output_json["contracts"][path_str][contract_name].get("devdoc", {}),
output_json["contracts"][path_str][contract_name].get("userdoc", {}),
)
output_evm = output_json["contracts"][path_str][contract_name]["evm"]
if contract_name in build_json and not output_evm["deployedBytecode"]["object"]:
if contract_alias in build_json and not output_evm["deployedBytecode"]["object"]:
continue

if input_json["language"] == "Solidity":
contract_node = next(
i[contract_name] for i in source_nodes if i.absolutePath == path_str
)
build_json[contract_name] = solidity._get_unique_build_json(
build_json[contract_alias] = solidity._get_unique_build_json(
output_evm,
contract_node,
statement_nodes,
Expand All @@ -316,16 +320,16 @@ def generate_build_json(

else:
if contract_name == "<stdin>":
contract_name = "Vyper"
build_json[contract_name] = vyper._get_unique_build_json(
contract_name = contract_alias = "Vyper"
build_json[contract_alias] = vyper._get_unique_build_json(
output_evm,
path_str,
contract_name,
contract_alias,
output_json["sources"][path_str]["ast"],
(0, len(input_json["sources"][path_str]["content"])),
(0, len(source)),
)

build_json[contract_name].update(
build_json[contract_alias].update(
{
"abi": abi,
"ast": output_json["sources"][path_str]["ast"],
Expand All @@ -336,8 +340,8 @@ def generate_build_json(
"language": input_json["language"],
"natspec": natspec,
"opcodes": output_evm["deployedBytecode"]["opcodes"],
"sha1": sha1(input_json["sources"][path_str]["content"].encode()).hexdigest(),
"source": input_json["sources"][path_str]["content"],
"sha1": sha1(source.encode()).hexdigest(),
"source": source,
"sourceMap": output_evm["bytecode"].get("sourceMap", ""),
"sourcePath": path_str,
}
Expand Down Expand Up @@ -398,6 +402,8 @@ def get_abi(
"abi": json.loads(v),
"contractName": Path(k).stem,
"type": "interface",
"source": None,
"offset": None,
"sha1": sha1(v.encode()).hexdigest(),
}
for k, v in contract_sources.items()
Expand All @@ -418,36 +424,38 @@ def get_abi(
"abi": output_json["contracts"][path][name]["abi"],
"contractName": name,
"type": "interface",
"source": source,
"offset": [0, len(source)],
"sha1": sha1(contract_sources[path].encode()).hexdigest(),
}

solc_sources = {k: v for k, v in contract_sources.items() if Path(k).suffix == ".sol"}

if solc_sources:
if not solc_sources:
return final_output

compiler_targets = find_solc_versions(solc_sources, install_needed=True, silent=silent)
compiler_targets = find_solc_versions(solc_sources, install_needed=True, silent=silent)

for version, path_list in compiler_targets.items():
to_compile = {k: v for k, v in contract_sources.items() if k in path_list}
for version, path_list in compiler_targets.items():
to_compile = {k: v for k, v in contract_sources.items() if k in path_list}

set_solc_version(version)
input_json = generate_input_json(to_compile, language="Solidity", remappings=remappings)
input_json["settings"]["outputSelection"]["*"] = {"*": ["abi"]}
set_solc_version(version)
input_json = generate_input_json(to_compile, language="Solidity", remappings=remappings)
input_json["settings"]["outputSelection"]["*"] = {"*": ["abi"], "": ["ast"]}

output_json = compile_from_input_json(input_json, silent, allow_paths)
output_json = {k: v for k, v in output_json["contracts"].items() if k in path_list}

final_output.update(
{
name: {
"abi": data["abi"],
"contractName": name,
"type": "interface",
"sha1": sha1(contract_sources[path].encode()).hexdigest(),
}
for path, v in output_json.items()
for name, data in v.items()
}
)
output_json = compile_from_input_json(input_json, silent, allow_paths)
source_nodes = solcast.from_standard_output(output_json)
output_json = {k: v for k, v in output_json["contracts"].items() if k in path_list}

for path, name, data in [(k, x, y) for k, v in output_json.items() for x, y in v.items()]:
contract_node = next(i[name] for i in source_nodes if i.absolutePath == path)
final_output[name] = {
"abi": data["abi"],
"contractName": name,
"type": "interface",
"source": contract_sources[path],
"offset": contract_node.offset,
"sha1": sha1(contract_sources[path].encode()).hexdigest(),
}

return final_output
15 changes: 11 additions & 4 deletions brownie/project/compiler/solidity.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from brownie._config import EVM_EQUIVALENTS
from brownie.exceptions import CompilerError, IncompatibleSolcVersion
from brownie.project.compiler.utils import expand_source_map
from brownie.project.compiler.utils import _get_alias, expand_source_map

from . import sources

Expand Down Expand Up @@ -252,14 +252,21 @@ def _get_unique_build_json(
has_fallback,
instruction_count,
)

dependencies = []
for node in [i for i in contract_node.dependencies if i.nodeType == "ContractDefinition"]:
# use contract aliases when recording dependencies, to avoid
# potential namespace collisions when importing across projects
name = node.name
path_str = node.parent().absolutePath
dependencies.append(_get_alias(name, path_str))

return {
"allSourcePaths": paths,
"bytecode": bytecode,
"bytecodeSha1": sha1(_remove_metadata(bytecode).encode()).hexdigest(),
"coverageMap": {"statements": statement_map, "branches": branch_map},
"dependencies": [
i.name for i in contract_node.dependencies if i.nodeType == "ContractDefinition"
],
"dependencies": dependencies,
"offset": contract_node.offset,
"pcMap": pc_map,
"type": contract_node.contractKind,
Expand Down
17 changes: 17 additions & 0 deletions brownie/project/compiler/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#!/usr/bin/python3

from pathlib import Path
from typing import Dict, List

from brownie._config import _get_data_folder


def expand_source_map(source_map_str: str) -> List:
# Expands the compressed sourceMap supplied by solc into a list of lists
Expand Down Expand Up @@ -52,3 +55,17 @@ def merge_natspec(devdoc: Dict, userdoc: Dict) -> Dict:
# sometimes Solidity has inconsistent NatSpec formatting ¯\_(ツ)_/¯
pass
return natspec


def _get_alias(contract_name: str, path_str: str) -> str:
# Generate an alias for a contract, used when tracking dependencies.
# For a contract within the project, the alias == the name. For contracts
# imported from a dependency, the alias is set as [PACKAGE]/[NAME]
# to avoid namespace collisions.
data_path = _get_data_folder().parts
path_parts = Path(path_str).parts
if path_parts[: len(data_path)] == data_path:
idx = len(data_path) + 1
return f"{path_parts[idx]}/{path_parts[idx+1]}/{contract_name}"
else:
return contract_name
29 changes: 26 additions & 3 deletions brownie/project/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,22 @@ def _compile(self, contract_sources: Dict, compiler_config: Dict, silent: bool)
finally:
os.chdir(cwd)

for data in build_json.values():
for alias, data in build_json.items():
if self._build_path is not None:
path = self._build_path.joinpath(f"contracts/{data['contractName']}.json")
if alias == data["contractName"]:
# if the alias == contract name, this is a part of the core project
path = self._build_path.joinpath(f"contracts/{alias}.json")
else:
# otherwise, this is an artifact from an external dependency
path = self._build_path.joinpath(f"contracts/dependencies/{alias}.json")
for parent in list(path.parents)[::-1]:
parent.mkdir(exist_ok=True)
with path.open("w") as fp:
json.dump(data, fp, sort_keys=True, indent=2, default=sorted)
self._build._add_contract(data)

if alias == data["contractName"]:
# only add artifacts from the core project for now
self._build._add_contract(data)

def _create_containers(self) -> None:
# create container objects
Expand Down Expand Up @@ -216,6 +226,8 @@ def load(self) -> None:
changed = self._get_changed_contracts(interface_hashes)
self._compile(changed, self._compiler_config, False)
self._compile_interfaces(interface_hashes)
self._load_dependency_artifacts()

self._create_containers()
self._load_deployments()

Expand Down Expand Up @@ -305,6 +317,17 @@ def _compile_interfaces(self, compiled_hashes: Dict) -> None:
json.dump(abi, fp, sort_keys=True, indent=2, default=sorted)
self._build._add_interface(abi)

def _load_dependency_artifacts(self) -> None:
dep_build_path = self._build_path.joinpath("contracts/dependencies/")
for path in list(dep_build_path.glob("**/*.json")):
contract_alias = path.relative_to(dep_build_path).with_suffix("").as_posix()
if self._build.get_dependents(contract_alias):
with path.open() as fp:
build_json = json.load(fp)
self._build._add_contract(build_json, contract_alias)
else:
path.unlink()

def _load_deployments(self) -> None:
if CONFIG.network_type != "live" and not CONFIG.settings["dev_deployment_artifacts"]:
return
Expand Down
2 changes: 1 addition & 1 deletion tests/project/main/test_interfaces_solc.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_contract_requires_interface(newproject, contract_type, import_path):
with newproject._path.joinpath("contracts/Bar.sol").open("w") as fp:
fp.write(CONTRACT.format(import_path))
newproject.load()
assert not newproject._path.joinpath("build/contracts/Foo.json").exists()
assert newproject._path.joinpath("build/contracts/Foo.json").exists()
assert not hasattr(newproject, "Foo")


Expand Down

0 comments on commit 5cfaac2

Please sign in to comment.