25
25
LOGGER = logging .getLogger ("CryticCompile" )
26
26
27
27
28
- class Vyper (AbstractPlatform ):
28
+ class VyperStandardJson (AbstractPlatform ):
29
29
"""
30
30
Vyper platform
31
31
"""
@@ -34,6 +34,28 @@ class Vyper(AbstractPlatform):
34
34
PROJECT_URL = "https://github.com/vyperlang/vyper"
35
35
TYPE = Type .VYPER
36
36
37
+ def __init__ (self , target : Optional [Path ] = None , ** _kwargs : str ):
38
+ super ().__init__ (str (target ), ** _kwargs )
39
+ self .standard_json_input = {
40
+ "language" : "Vyper" ,
41
+ "sources" : {},
42
+ "settings" : {
43
+ "outputSelection" : {
44
+ "*" : {
45
+ "*" : [
46
+ "abi" ,
47
+ "devdoc" ,
48
+ "userdoc" ,
49
+ "evm.bytecode" ,
50
+ "evm.deployedBytecode" ,
51
+ "evm.deployedBytecode.sourceMap" ,
52
+ ],
53
+ "" : ["ast" ],
54
+ }
55
+ }
56
+ },
57
+ }
58
+
37
59
def compile (self , crytic_compile : "CryticCompile" , ** kwargs : str ) -> None :
38
60
"""Compile the target
39
61
@@ -44,46 +66,67 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
44
66
45
67
"""
46
68
target = self ._target
69
+ # If the target was a directory `add_source_file` should have been called
70
+ # by `compile_all`. Otherwise, we should have a single file target.
71
+ if self ._target is not None and os .path .isfile (self ._target ):
72
+ self .add_source_files ([target ])
47
73
48
- vyper = kwargs .get ("vyper" , "vyper" )
49
-
50
- targets_json = _run_vyper (target , vyper )
74
+ vyper_bin = kwargs .get ("vyper" , "vyper" )
51
75
52
- assert "version" in targets_json
76
+ compilation_artifacts = _run_vyper_standard_json ( self . standard_json_input , vyper_bin )
53
77
compilation_unit = CompilationUnit (crytic_compile , str (target ))
54
78
79
+ compiler_version = compilation_artifacts ["compiler" ].split ("-" )[1 ]
80
+ if compiler_version != "0.3.7" :
81
+ LOGGER .info ("Vyper != 0.3.7 support is a best effort and might fail" )
55
82
compilation_unit .compiler_version = CompilerVersion (
56
- compiler = "vyper" , version = targets_json [ "version" ] , optimized = False
83
+ compiler = "vyper" , version = compiler_version , optimized = False
57
84
)
58
85
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 ]
86
+ for source_file , contract_info in compilation_artifacts ["contracts" ].items ():
87
+ filename = convert_filename (source_file , _relative_to_short , crytic_compile )
88
+ source_unit = compilation_unit .create_source_unit (filename )
89
+ for contract_name , contract_metadata in contract_info .items ():
90
+ source_unit .add_contract_name (contract_name )
91
+ compilation_unit .filename_to_contracts [filename ].add (contract_name )
92
+
93
+ source_unit .abis [contract_name ] = contract_metadata ["abi" ]
94
+ source_unit .bytecodes_init [contract_name ] = contract_metadata ["evm" ]["bytecode" ][
95
+ "object"
96
+ ].replace ("0x" , "" )
97
+ # Vyper does not provide the source mapping for the init bytecode
98
+ source_unit .srcmaps_init [contract_name ] = []
99
+ source_unit .srcmaps_runtime [contract_name ] = contract_metadata ["evm" ][
100
+ "deployedBytecode"
101
+ ]["sourceMap" ].split (";" )
102
+ source_unit .bytecodes_runtime [contract_name ] = contract_metadata ["evm" ][
103
+ "deployedBytecode"
104
+ ]["object" ].replace ("0x" , "" )
105
+ source_unit .natspec [contract_name ] = Natspec (
106
+ contract_metadata ["userdoc" ], contract_metadata ["devdoc" ]
107
+ )
108
+
109
+ for source_file , ast in compilation_artifacts ["sources" ].items ():
110
+ filename = convert_filename (source_file , _relative_to_short , crytic_compile )
111
+ source_unit = compilation_unit .create_source_unit (filename )
112
+ source_unit .ast = ast
113
+
114
+ def add_source_files (self , file_paths : List [str ]) -> None :
115
+ """
116
+ Append files
65
117
66
- source_unit = compilation_unit .create_source_unit (filename )
118
+ Args:
119
+ file_paths (List[str]): files to append
67
120
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" ]
121
+ Returns:
81
122
82
- # Natspec not yet handled for vyper
83
- source_unit .natspec [contract_name ] = Natspec ({}, {})
123
+ """
84
124
85
- ast = _get_vyper_ast (target , vyper )
86
- source_unit .ast = ast
125
+ for file_path in file_paths :
126
+ with open (file_path , "r" , encoding = "utf8" ) as f :
127
+ self .standard_json_input ["sources" ][file_path ] = { # type: ignore
128
+ "content" : f .read (),
129
+ }
87
130
88
131
def clean (self , ** _kwargs : str ) -> None :
89
132
"""Clean compilation artifacts
@@ -129,98 +172,45 @@ def _guessed_tests(self) -> List[str]:
129
172
return []
130
173
131
174
132
- def _run_vyper (
133
- filename : str , vyper : str , env : Optional [Dict ] = None , working_dir : Optional [ str ] = None
175
+ def _run_vyper_standard_json (
176
+ standard_json_input : Dict , vyper : str , env : Optional [Dict ] = None
134
177
) -> Dict :
135
- """Run vyper
178
+ """Run vyper and write compilation output to a file
136
179
137
180
Args:
138
- filename (str ): vyper file
181
+ standard_json_input (Dict ): Dict containing the vyper standard json input
139
182
vyper (str): vyper binary
140
183
env (Optional[Dict], optional): Environment variables. Defaults to None.
141
- working_dir (Optional[str], optional): Working directory. Defaults to None.
142
184
143
185
Raises:
144
186
InvalidCompilation: If vyper failed to run
145
187
146
188
Returns:
147
189
Dict: Vyper json compilation artifact
148
190
"""
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 )
191
+ cmd = [vyper , "--standard-json" ]
192
+
193
+ with subprocess .Popen (
194
+ cmd ,
195
+ stdin = subprocess .PIPE ,
196
+ stdout = subprocess .PIPE ,
197
+ stderr = subprocess .PIPE ,
198
+ env = env ,
199
+ executable = shutil .which (cmd [0 ]),
200
+ ) as process :
201
+
202
+ stdout_b , stderr_b = process .communicate (json .dumps (standard_json_input ).encode ("utf-8" ))
203
+ stdout , _stderr = (
204
+ stdout_b .decode (),
205
+ stderr_b .decode (errors = "backslashreplace" ),
206
+ ) # convert bytestrings to unicode strings
207
+
208
+ vyper_standard_output = json .loads (stdout )
209
+ if "errors" in vyper_standard_output :
210
+ # TODO format errors
211
+ raise InvalidCompilation (vyper_standard_output ["errors" ])
212
+
213
+ return vyper_standard_output
224
214
225
215
226
216
def _relative_to_short (relative : Path ) -> Path :
0 commit comments