Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include depdendency artifacts in build/contracts/ #878

Merged
merged 7 commits into from
Dec 5, 2020
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