Skip to content

Commit e726dd0

Browse files
committed
support multiple compilation units for foundry
1 parent 99c0288 commit e726dd0

File tree

1 file changed

+95
-131
lines changed

1 file changed

+95
-131
lines changed

crytic_compile/platform/foundry.py

Lines changed: 95 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
from crytic_compile.platform.abstract_platform import AbstractPlatform
1515
from crytic_compile.platform.exceptions import InvalidCompilation
1616
from crytic_compile.platform.types import Type
17-
from crytic_compile.utils.naming import convert_filename
17+
from crytic_compile.utils.naming import convert_filename, extract_name
1818
from crytic_compile.utils.natspec import Natspec
1919

20+
from .solc import relative_to_short
21+
2022
# Handle cycle
2123
if TYPE_CHECKING:
2224
from crytic_compile import CryticCompile
@@ -60,14 +62,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
6062
cmd = [
6163
"forge",
6264
"build",
63-
"--extra-output",
64-
"abi",
65-
"--extra-output",
66-
"userdoc",
67-
"--extra-output",
68-
"devdoc",
69-
"--extra-output",
70-
"evm.methodIdentifiers",
65+
"--build-info",
7166
"--force",
7267
]
7368

@@ -94,68 +89,99 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
9489
if stderr:
9590
LOGGER.error(stderr)
9691

97-
filenames = Path(self._target, out_directory).rglob("*.json")
98-
99-
# foundry only support solc for now
100-
compiler = "solc"
101-
compilation_unit = CompilationUnit(crytic_compile, str(self._target))
102-
103-
for filename_txt in filenames:
104-
with open(filename_txt, encoding="utf8") as file_desc:
105-
target_loaded = json.load(file_desc)
106-
107-
userdoc = target_loaded.get("userdoc", {})
108-
devdoc = target_loaded.get("devdoc", {})
109-
natspec = Natspec(userdoc, devdoc)
110-
111-
if not "ast" in target_loaded:
112-
continue
113-
114-
filename_str = target_loaded["ast"]["absolutePath"]
115-
116-
try:
117-
filename = convert_filename(
118-
filename_str, lambda x: x, crytic_compile, working_dir=self._target
119-
)
120-
except InvalidCompilation as i:
121-
txt = str(i)
122-
txt += "\nSomething went wrong, please open an issue in https://github.com/crytic/crytic-compile"
123-
# pylint: disable=raise-missing-from
124-
raise InvalidCompilation(txt)
125-
126-
source_unit = compilation_unit.create_source_unit(filename)
127-
128-
source_unit.ast = target_loaded["ast"]
129-
130-
contract_name = filename_txt.parts[-1]
131-
contract_name = contract_name[: -len(".json")]
132-
133-
source_unit.natspec[contract_name] = natspec
134-
compilation_unit.filename_to_contracts[filename].add(contract_name)
135-
source_unit.contracts_names.add(contract_name)
136-
source_unit.abis[contract_name] = target_loaded["abi"]
137-
source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"][
138-
"object"
139-
].replace("0x", "")
140-
source_unit.bytecodes_runtime[contract_name] = target_loaded["deployedBytecode"][
141-
"object"
142-
].replace("0x", "")
143-
source_unit.srcmaps_init[contract_name] = (
144-
target_loaded["bytecode"]["sourceMap"].split(";")
145-
if target_loaded["bytecode"].get("sourceMap")
146-
else []
147-
)
148-
source_unit.srcmaps_runtime[contract_name] = (
149-
target_loaded["deployedBytecode"]["sourceMap"].split(";")
150-
if target_loaded["deployedBytecode"].get("sourceMap")
151-
else []
152-
)
92+
build_directory = Path(
93+
self._target,
94+
out_directory,
95+
"build-info",
96+
)
97+
files = sorted(
98+
os.listdir(build_directory), key=lambda x: os.path.getmtime(Path(build_directory, x))
99+
)
100+
files = [f for f in files if f.endswith(".json")]
101+
if not files:
102+
txt = f"`forge build` failed. Can you run it?\n{build_directory} is empty"
103+
raise InvalidCompilation(txt)
153104

154-
version, optimized, runs = _get_config_info(self._target)
105+
for file in files:
106+
build_info = Path(build_directory, file)
155107

156-
compilation_unit.compiler_version = CompilerVersion(
157-
compiler=compiler, version=version, optimized=optimized, optimize_runs=runs
158-
)
108+
# The file here should always ends .json, but just in case use ife
109+
uniq_id = file if ".json" not in file else file[0:-5]
110+
compilation_unit = CompilationUnit(crytic_compile, uniq_id)
111+
112+
with open(build_info, encoding="utf8") as file_desc:
113+
loaded_json = json.load(file_desc)
114+
115+
targets_json = loaded_json["output"]
116+
117+
version_from_config = loaded_json["solcVersion"] # TODO supper vyper
118+
input_json = loaded_json["input"]
119+
compiler = "solc" if input_json["language"] == "Solidity" else "vyper"
120+
optimized = input_json["settings"]["optimizer"]["enabled"]
121+
122+
compilation_unit.compiler_version = CompilerVersion(
123+
compiler=compiler, version=version_from_config, optimized=optimized
124+
)
125+
126+
skip_filename = compilation_unit.compiler_version.version in [
127+
f"0.4.{x}" for x in range(0, 10)
128+
]
129+
130+
if "contracts" in targets_json:
131+
for original_filename, contracts_info in targets_json["contracts"].items():
132+
133+
filename = convert_filename(
134+
original_filename,
135+
relative_to_short,
136+
crytic_compile,
137+
working_dir=self._target,
138+
)
139+
140+
source_unit = compilation_unit.create_source_unit(filename)
141+
142+
for original_contract_name, info in contracts_info.items():
143+
contract_name = extract_name(original_contract_name)
144+
145+
source_unit.contracts_names.add(contract_name)
146+
compilation_unit.filename_to_contracts[filename].add(contract_name)
147+
148+
source_unit.abis[contract_name] = info["abi"]
149+
source_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"][
150+
"object"
151+
]
152+
source_unit.bytecodes_runtime[contract_name] = info["evm"][
153+
"deployedBytecode"
154+
]["object"]
155+
source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][
156+
"sourceMap"
157+
].split(";")
158+
source_unit.srcmaps_runtime[contract_name] = info["evm"][
159+
"deployedBytecode"
160+
]["sourceMap"].split(";")
161+
userdoc = info.get("userdoc", {})
162+
devdoc = info.get("devdoc", {})
163+
natspec = Natspec(userdoc, devdoc)
164+
source_unit.natspec[contract_name] = natspec
165+
166+
if "sources" in targets_json:
167+
for path, info in targets_json["sources"].items():
168+
if skip_filename:
169+
path = convert_filename(
170+
self._target,
171+
relative_to_short,
172+
crytic_compile,
173+
working_dir=self._target,
174+
)
175+
else:
176+
path = convert_filename(
177+
path,
178+
relative_to_short,
179+
crytic_compile,
180+
working_dir=self._target,
181+
)
182+
183+
source_unit = compilation_unit.create_source_unit(path)
184+
source_unit.ast = info["ast"]
159185

160186
@staticmethod
161187
def is_supported(target: str, **kwargs: str) -> bool:
@@ -197,65 +223,3 @@ def _guessed_tests(self) -> List[str]:
197223
List[str]: The guessed unit tests commands
198224
"""
199225
return ["forge test"]
200-
201-
202-
def _get_config_info(target: str) -> Tuple[str, Optional[bool], Optional[int]]:
203-
"""get the compiler version from solidity-files-cache.json
204-
205-
Args:
206-
target (str): path to the project directory
207-
208-
Returns:
209-
(str, str, str): compiler version, optimized, runs
210-
211-
Raises:
212-
InvalidCompilation: If cache/solidity-files-cache.json cannot be parsed
213-
"""
214-
config = Path(target, "cache", "solidity-files-cache.json")
215-
if not config.exists():
216-
raise InvalidCompilation(
217-
"Could not find the cache/solidity-files-cache.json file."
218-
+ "If you are using 'cache = true' in foundry's config file, please remove it."
219-
+ " Otherwise please open an issue in https://github.com/crytic/crytic-compile"
220-
)
221-
with open(config, "r", encoding="utf8") as config_f:
222-
config_dict = json.load(config_f)
223-
224-
version: Optional[str] = None
225-
optimizer: Optional[bool] = None
226-
runs: Optional[int] = None
227-
228-
if "files" in config_dict:
229-
items = list(config_dict["files"].values())
230-
# On the form
231-
# { ..
232-
# "artifacts": {
233-
# "CONTRACT_NAME": {
234-
# "0.8.X+commit...": "filename"}
235-
#
236-
if len(items) >= 1:
237-
item = items[0]
238-
if "artifacts" in item:
239-
items_artifact = list(item["artifacts"].values())
240-
if len(items_artifact) >= 1:
241-
item_version = items_artifact[0]
242-
version = list(item_version.keys())[0]
243-
assert version
244-
plus_position = version.find("+")
245-
if plus_position > 0:
246-
version = version[:plus_position]
247-
if (
248-
"solcConfig" in item
249-
and "settings" in item["solcConfig"]
250-
and "optimizer" in item["solcConfig"]["settings"]
251-
):
252-
optimizer = item["solcConfig"]["settings"]["optimizer"]["enabled"]
253-
runs = item["solcConfig"]["settings"]["optimizer"].get("runs", None)
254-
255-
if version is None:
256-
raise InvalidCompilation(
257-
"Something went wrong with cache/solidity-files-cache.json parsing"
258-
+ ". Please open an issue in https://github.com/crytic/crytic-compile"
259-
)
260-
261-
return version, optimizer, runs

0 commit comments

Comments
 (0)