Skip to content

Commit a6a33a8

Browse files
committed
feat: compile vyper 0.3.7 via standard json input
1 parent 7d6d7aa commit a6a33a8

File tree

3 files changed

+107
-132
lines changed

3 files changed

+107
-132
lines changed

crytic_compile/crytic_compile.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union
1616

1717
from crytic_compile.compilation_unit import CompilationUnit
18-
from crytic_compile.platform import all_platforms, solc_standard_json
18+
from crytic_compile.platform import all_platforms
19+
from crytic_compile.platform.solc_standard_json import SolcStandardJson
20+
from crytic_compile.platform.vyper import VyperStandardJson
1921
from crytic_compile.platform.abstract_platform import AbstractPlatform
2022
from crytic_compile.platform.all_export import PLATFORMS_EXPORT
2123
from crytic_compile.platform.solc import Solc
@@ -628,11 +630,7 @@ def compile_all(target: str, **kwargs: str) -> List[CryticCompile]:
628630
"""
629631
use_solc_standard_json = kwargs.get("solc_standard_json", False)
630632

631-
# Attempt to perform glob expansion of target/filename
632-
globbed_targets = glob.glob(target, recursive=True)
633-
634633
# Check if the target refers to a valid target already.
635-
# If it does not, we assume it's a glob pattern.
636634
compilations: List[CryticCompile] = []
637635
if os.path.isfile(target) or is_supported(target):
638636
if target.endswith(".zip"):
@@ -644,28 +642,33 @@ def compile_all(target: str, **kwargs: str) -> List[CryticCompile]:
644642
compilations = load_from_zip(tmp.name)
645643
else:
646644
compilations.append(CryticCompile(target, **kwargs))
647-
elif os.path.isdir(target) or len(globbed_targets) > 0:
648-
# We create a new glob to find solidity files at this path (in case this is a directory)
649-
filenames = glob.glob(os.path.join(target, "*.sol"))
650-
if not filenames:
651-
filenames = glob.glob(os.path.join(target, "*.vy"))
652-
if not filenames:
653-
filenames = globbed_targets
654-
645+
elif os.path.isdir(target):
646+
solidity_filenames = glob.glob(os.path.join(target, "*.sol"))
647+
vyper_filenames = glob.glob(os.path.join(target, "*.vy"))
655648
# Determine if we're using --standard-solc option to
656649
# aggregate many files into a single compilation.
657650
if use_solc_standard_json:
658651
# If we're using standard solc, then we generated our
659652
# input to create a single compilation with all files
660-
standard_json = solc_standard_json.SolcStandardJson()
661-
for filename in filenames:
662-
standard_json.add_source_file(filename)
663-
compilations.append(CryticCompile(standard_json, **kwargs))
653+
solc_standard_json = SolcStandardJson()
654+
solc_standard_json.add_source_files(solidity_filenames)
655+
compilations.append(CryticCompile(solc_standard_json, **kwargs))
664656
else:
665657
# We compile each file and add it to our compilations.
666-
for filename in filenames:
658+
for filename in solidity_filenames:
667659
compilations.append(CryticCompile(filename, **kwargs))
660+
661+
if vyper_filenames:
662+
vyper_standard_json = VyperStandardJson()
663+
vyper_standard_json.add_source_files(vyper_filenames)
664+
compilations.append(CryticCompile(vyper_standard_json, **kwargs))
668665
else:
669-
raise ValueError(f"{str(target)} is not a file or directory.")
666+
raise NotImplementedError()
667+
# TODO split glob into language
668+
# # Attempt to perform glob expansion of target/filename
669+
# globbed_targets = glob.glob(target, recursive=True)
670+
# print(globbed_targets)
671+
672+
# raise ValueError(f"{str(target)} is not a file or directory.")
670673

671674
return compilations

crytic_compile/platform/all_platforms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
from .solc_standard_json import SolcStandardJson
1515
from .standard import Standard
1616
from .truffle import Truffle
17-
from .vyper import Vyper
17+
from .vyper import VyperStandardJson
1818
from .waffle import Waffle
1919
from .foundry import Foundry

crytic_compile/platform/vyper.py

Lines changed: 84 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from crytic_compile.platform.exceptions import InvalidCompilation
1616
from crytic_compile.platform.types import Type
1717
from crytic_compile.utils.naming import convert_filename
18+
from crytic_compile.utils.subprocess import run
1819

1920
# Handle cycle
2021
from crytic_compile.utils.natspec import Natspec
@@ -25,14 +26,36 @@
2526
LOGGER = logging.getLogger("CryticCompile")
2627

2728

28-
class Vyper(AbstractPlatform):
29+
class VyperStandardJson(AbstractPlatform):
2930
"""
3031
Vyper platform
3132
"""
3233

3334
NAME = "vyper"
3435
PROJECT_URL = "https://github.com/vyperlang/vyper"
3536
TYPE = Type.VYPER
37+
standard_json_input: Dict = {
38+
"language": "Vyper",
39+
"sources": {},
40+
"settings": {
41+
"outputSelection": {
42+
"*": {
43+
"*": [
44+
"abi",
45+
"devdoc",
46+
"userdoc",
47+
"evm.bytecode",
48+
"evm.deployedBytecode",
49+
"evm.deployedBytecode.sourceMap",
50+
],
51+
"": ["ast"],
52+
}
53+
}
54+
},
55+
}
56+
57+
def __init__(self, target: Optional[Path] = None, **_kwargs: str):
58+
super().__init__(target, **_kwargs)
3659

3760
def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
3861
"""Compile the target
@@ -44,46 +67,61 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
4467
4568
"""
4669
target = self._target
70+
# If the target was a directory `add_source_file` should have been called
71+
# by `compile_all`. Otherwise, we should have a single file target.
72+
if self._target is not None and os.path.isfile(self._target):
73+
self.add_source_files([target])
4774

48-
vyper = kwargs.get("vyper", "vyper")
75+
vyper_bin = kwargs.get("vyper", "vyper")
76+
output_file = Path("crytic-export/standard_input.json")
77+
output_file.parent.mkdir(exist_ok=True, parents=True)
78+
with open(output_file, "w") as f:
79+
f.write(json.dumps(self.standard_json_input))
4980

50-
targets_json = _run_vyper(target, vyper)
81+
compilation_artifacts = _run_vyper_standard_json(output_file.as_posix(), vyper_bin)
5182

52-
assert "version" in targets_json
5383
compilation_unit = CompilationUnit(crytic_compile, str(target))
5484

85+
compiler_version = compilation_artifacts["compiler"].split("-")[1]
86+
assert compiler_version == "0.3.7"
5587
compilation_unit.compiler_version = CompilerVersion(
56-
compiler="vyper", version=targets_json["version"], optimized=False
88+
compiler="vyper", version=compiler_version, optimized=False
5789
)
5890

59-
assert target in targets_json
60-
61-
info = targets_json[target]
62-
filename = convert_filename(target, _relative_to_short, crytic_compile)
63-
64-
contract_name = Path(target).parts[-1]
65-
66-
source_unit = compilation_unit.create_source_unit(filename)
67-
68-
source_unit.add_contract_name(contract_name)
69-
compilation_unit.filename_to_contracts[filename].add(contract_name)
70-
source_unit.abis[contract_name] = info["abi"]
71-
source_unit.bytecodes_init[contract_name] = info["bytecode"].replace("0x", "")
72-
source_unit.bytecodes_runtime[contract_name] = info["bytecode_runtime"].replace("0x", "")
73-
# Vyper does not provide the source mapping for the init bytecode
74-
source_unit.srcmaps_init[contract_name] = []
75-
# info["source_map"]["pc_pos_map"] contains the source mapping in a simpler format
76-
# However pc_pos_map_compressed" seems to follow solc's format, so for convenience
77-
# We store the same
78-
# TODO: create SourceMapping class, so that srcmaps_runtime would store an class
79-
# That will give more flexebility to different compilers
80-
source_unit.srcmaps_runtime[contract_name] = info["source_map"]["pc_pos_map_compressed"]
81-
82-
# Natspec not yet handled for vyper
83-
source_unit.natspec[contract_name] = Natspec({}, {})
84-
85-
ast = _get_vyper_ast(target, vyper)
86-
source_unit.ast = ast
91+
for source_file, contract_info in compilation_artifacts["contracts"].items():
92+
filename = convert_filename(source_file, _relative_to_short, crytic_compile)
93+
source_unit = compilation_unit.create_source_unit(filename)
94+
for contract_name, contract_metadata in contract_info.items():
95+
source_unit.add_contract_name(contract_name)
96+
compilation_unit.filename_to_contracts[filename].add(contract_name)
97+
98+
source_unit.abis[contract_name] = contract_metadata["abi"]
99+
source_unit.bytecodes_init[contract_name] = contract_metadata["evm"]["bytecode"][
100+
"object"
101+
].replace("0x", "")
102+
# Vyper does not provide the source mapping for the init bytecode
103+
source_unit.srcmaps_init[contract_name] = []
104+
source_unit.srcmaps_runtime[contract_name] = contract_metadata["evm"][
105+
"deployedBytecode"
106+
]["sourceMap"]
107+
source_unit.bytecodes_runtime[contract_name] = contract_metadata["evm"][
108+
"deployedBytecode"
109+
]["object"].replace("0x", "")
110+
source_unit.natspec[contract_name] = Natspec(
111+
contract_metadata["userdoc"], contract_metadata["devdoc"]
112+
)
113+
114+
for source_file, ast in compilation_artifacts["sources"].items():
115+
filename = convert_filename(source_file, _relative_to_short, crytic_compile)
116+
source_unit = compilation_unit.create_source_unit(filename)
117+
source_unit.ast = ast
118+
119+
def add_source_files(self, file_paths: List[str]) -> None:
120+
for file_path in file_paths:
121+
with open(file_path, "r") as f:
122+
self.standard_json_input["sources"][file_path] = {
123+
"content": f.read(),
124+
}
87125

88126
def clean(self, **_kwargs: str) -> None:
89127
"""Clean compilation artifacts
@@ -129,13 +167,16 @@ def _guessed_tests(self) -> List[str]:
129167
return []
130168

131169

132-
def _run_vyper(
133-
filename: str, vyper: str, env: Optional[Dict] = None, working_dir: Optional[str] = None
170+
def _run_vyper_standard_json(
171+
standard_input_path: str,
172+
vyper: str,
173+
env: Optional[Dict] = None,
174+
working_dir: Optional[str] = None,
134175
) -> Dict:
135-
"""Run vyper
176+
"""Run vyper and write compilation output to a file
136177
137178
Args:
138-
filename (str): vyper file
179+
standard_input_path (str): path to the standard input json file
139180
vyper (str): vyper binary
140181
env (Optional[Dict], optional): Environment variables. Defaults to None.
141182
working_dir (Optional[str], optional): Working directory. Defaults to None.
@@ -146,81 +187,12 @@ def _run_vyper(
146187
Returns:
147188
Dict: Vyper json compilation artifact
148189
"""
149-
if not os.path.isfile(filename):
150-
raise InvalidCompilation(f"{filename} does not exist (are you in the correct directory?)")
151-
152-
cmd = [vyper, filename, "-f", "combined_json"]
153-
154-
additional_kwargs: Dict = {"cwd": working_dir} if working_dir else {}
155-
stderr = ""
156-
LOGGER.info(
157-
"'%s' running",
158-
" ".join(cmd),
159-
)
160-
try:
161-
with subprocess.Popen(
162-
cmd,
163-
stdout=subprocess.PIPE,
164-
stderr=subprocess.PIPE,
165-
env=env,
166-
executable=shutil.which(cmd[0]),
167-
**additional_kwargs,
168-
) as process:
169-
stdout, stderr = process.communicate()
170-
res = stdout.split(b"\n")
171-
res = res[-2]
172-
return json.loads(res)
173-
except OSError as error:
174-
# pylint: disable=raise-missing-from
175-
raise InvalidCompilation(error)
176-
except json.decoder.JSONDecodeError:
177-
# pylint: disable=raise-missing-from
178-
raise InvalidCompilation(f"Invalid vyper compilation\n{stderr}")
179-
180-
181-
def _get_vyper_ast(
182-
filename: str, vyper: str, env: Optional[Dict] = None, working_dir: Optional[str] = None
183-
) -> Dict:
184-
"""Get ast from vyper
185-
186-
Args:
187-
filename (str): vyper file
188-
vyper (str): vyper binary
189-
env (Dict, optional): Environment variables. Defaults to None.
190-
working_dir (str, optional): Working directory. Defaults to None.
191-
192-
Raises:
193-
InvalidCompilation: If vyper failed to run
194-
195-
Returns:
196-
Dict: [description]
197-
"""
198-
if not os.path.isfile(filename):
199-
raise InvalidCompilation(f"{filename} does not exist (are you in the correct directory?)")
200-
201-
cmd = [vyper, filename, "-f", "ast"]
202-
203-
additional_kwargs: Dict = {"cwd": working_dir} if working_dir else {}
204-
stderr = ""
205-
try:
206-
with subprocess.Popen(
207-
cmd,
208-
stdout=subprocess.PIPE,
209-
stderr=subprocess.PIPE,
210-
env=env,
211-
executable=shutil.which(cmd[0]),
212-
**additional_kwargs,
213-
) as process:
214-
stdout, stderr = process.communicate()
215-
res = stdout.split(b"\n")
216-
res = res[-2]
217-
return json.loads(res)
218-
except json.decoder.JSONDecodeError:
219-
# pylint: disable=raise-missing-from
220-
raise InvalidCompilation(f"Invalid vyper compilation\n{stderr}")
221-
except Exception as exception:
222-
# pylint: disable=raise-missing-from
223-
raise InvalidCompilation(exception)
190+
cmd = [vyper, standard_input_path, "--standard-json", "-o", "crytic-export/artifacts.json"]
191+
success = run(cmd, cwd=working_dir, extra_env=env)
192+
if success is None:
193+
raise InvalidCompilation("Vyper compilation failed")
194+
with open("crytic-export/artifacts.json", "r") as f:
195+
return json.load(f)
224196

225197

226198
def _relative_to_short(relative: Path) -> Path:

0 commit comments

Comments
 (0)