15
15
from crytic_compile .platform .exceptions import InvalidCompilation
16
16
from crytic_compile .platform .types import Type
17
17
from crytic_compile .utils .naming import convert_filename
18
+ from crytic_compile .utils .subprocess import run
18
19
19
20
# Handle cycle
20
21
from crytic_compile .utils .natspec import Natspec
25
26
LOGGER = logging .getLogger ("CryticCompile" )
26
27
27
28
28
- class Vyper (AbstractPlatform ):
29
+ class VyperStandardJson (AbstractPlatform ):
29
30
"""
30
31
Vyper platform
31
32
"""
32
33
33
34
NAME = "vyper"
34
35
PROJECT_URL = "https://github.com/vyperlang/vyper"
35
36
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 )
36
59
37
60
def compile (self , crytic_compile : "CryticCompile" , ** kwargs : str ) -> None :
38
61
"""Compile the target
@@ -44,46 +67,61 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
44
67
45
68
"""
46
69
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 ])
47
74
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 ))
49
80
50
- targets_json = _run_vyper ( target , vyper )
81
+ compilation_artifacts = _run_vyper_standard_json ( output_file . as_posix (), vyper_bin )
51
82
52
- assert "version" in targets_json
53
83
compilation_unit = CompilationUnit (crytic_compile , str (target ))
54
84
85
+ compiler_version = compilation_artifacts ["compiler" ].split ("-" )[1 ]
86
+ assert compiler_version == "0.3.7"
55
87
compilation_unit .compiler_version = CompilerVersion (
56
- compiler = "vyper" , version = targets_json [ "version" ] , optimized = False
88
+ compiler = "vyper" , version = compiler_version , optimized = False
57
89
)
58
90
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
+ }
87
125
88
126
def clean (self , ** _kwargs : str ) -> None :
89
127
"""Clean compilation artifacts
@@ -129,13 +167,16 @@ def _guessed_tests(self) -> List[str]:
129
167
return []
130
168
131
169
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 ,
134
175
) -> Dict :
135
- """Run vyper
176
+ """Run vyper and write compilation output to a file
136
177
137
178
Args:
138
- filename (str): vyper file
179
+ standard_input_path (str): path to the standard input json file
139
180
vyper (str): vyper binary
140
181
env (Optional[Dict], optional): Environment variables. Defaults to None.
141
182
working_dir (Optional[str], optional): Working directory. Defaults to None.
@@ -146,81 +187,12 @@ def _run_vyper(
146
187
Returns:
147
188
Dict: Vyper json compilation artifact
148
189
"""
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 )
224
196
225
197
226
198
def _relative_to_short (relative : Path ) -> Path :
0 commit comments