crytic_compile.platform.truffle
Truffle platform
1""" 2Truffle platform 3""" 4import glob 5import json 6import logging 7import os 8import platform 9import re 10import shutil 11import subprocess 12import uuid 13from pathlib import Path 14from typing import TYPE_CHECKING, Dict, List, Optional, Tuple 15 16from crytic_compile.compilation_unit import CompilationUnit 17from crytic_compile.compiler.compiler import CompilerVersion 18from crytic_compile.platform import solc 19from crytic_compile.platform.abstract_platform import AbstractPlatform 20from crytic_compile.platform.exceptions import InvalidCompilation 21from crytic_compile.platform.types import Type 22from crytic_compile.utils.naming import convert_filename 23from crytic_compile.utils.natspec import Natspec 24 25# Handle cycle 26if TYPE_CHECKING: 27 from crytic_compile import CryticCompile 28 29LOGGER = logging.getLogger("CryticCompile") 30 31 32def export_to_truffle(crytic_compile: "CryticCompile", **kwargs: str) -> List[str]: 33 """Export to the truffle format 34 35 Args: 36 crytic_compile (CryticCompile): CryticCompile object to export 37 **kwargs: optional arguments. Used: "export_dir" 38 39 Raises: 40 InvalidCompilation: If there are more than 1 compilation unit 41 42 Returns: 43 List[str]: Singleton with the generated directory 44 """ 45 # Get our export directory, if it's set, we create the path. 46 export_dir = kwargs.get("export_dir", "crytic-export") 47 if export_dir and not os.path.exists(export_dir): 48 os.makedirs(export_dir) 49 50 compilation_units = list(crytic_compile.compilation_units.values()) 51 if len(compilation_units) != 1: 52 raise InvalidCompilation("Truffle export require 1 compilation unit") 53 compilation_unit = compilation_units[0] 54 55 # Loop for each contract filename. 56 57 libraries = compilation_unit.crytic_compile.libraries 58 59 results: List[Dict] = [] 60 for source_unit in compilation_unit.source_units.values(): 61 for contract_name in source_unit.contracts_names: 62 # Create the informational object to output for this contract 63 output = { 64 "contractName": contract_name, 65 "abi": source_unit.abi(contract_name), 66 "bytecode": "0x" + source_unit.bytecode_init(contract_name, libraries), 67 "deployedBytecode": "0x" + source_unit.bytecode_runtime(contract_name, libraries), 68 "ast": source_unit.ast, 69 "userdoc": source_unit.natspec[contract_name].userdoc.export(), 70 "devdoc": source_unit.natspec[contract_name].devdoc.export(), 71 } 72 results.append(output) 73 74 # If we have an export directory, export it. 75 76 path = os.path.join(export_dir, contract_name + ".json") 77 with open(path, "w", encoding="utf8") as file_desc: 78 json.dump(output, file_desc) 79 80 return [export_dir] 81 82 83class Truffle(AbstractPlatform): 84 """ 85 Truffle platform 86 """ 87 88 NAME = "Truffle" 89 PROJECT_URL = "https://github.com/trufflesuite/truffle" 90 TYPE = Type.TRUFFLE 91 92 # pylint: disable=too-many-locals,too-many-statements,too-many-branches 93 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 94 """Compile 95 96 Args: 97 crytic_compile (CryticCompile): CryticCompile object to populate 98 **kwargs: optional arguments. Used "truffle_build_directory", "truffle_ignore_compile", "ignore_compile", 99 "truffle_version", "npx_disable" 100 101 Raises: 102 InvalidCompilation: If truffle failed to run 103 """ 104 105 build_directory = kwargs.get("truffle_build_directory", os.path.join("build", "contracts")) 106 truffle_ignore_compile = kwargs.get("truffle_ignore_compile", False) or kwargs.get( 107 "ignore_compile", False 108 ) 109 truffle_version = kwargs.get("truffle_version", None) 110 # crytic_compile.type = Type.TRUFFLE 111 # Truffle on windows has naming conflicts where it will invoke truffle.js directly instead 112 # of truffle.cmd (unless in powershell or git bash). 113 # The cleanest solution is to explicitly call 114 # truffle.cmd. Reference: 115 # https://truffleframework.com/docs/truffle/reference/configuration#resolving-naming-conflicts-on-windows 116 117 truffle_overwrite_config = kwargs.get("truffle_overwrite_config", False) 118 119 if platform.system() == "Windows": 120 base_cmd = ["truffle.cmd"] 121 elif kwargs.get("npx_disable", False): 122 base_cmd = ["truffle"] 123 else: 124 base_cmd = ["npx", "truffle"] 125 if truffle_version: 126 if truffle_version.startswith("truffle"): 127 base_cmd = ["npx", truffle_version] 128 else: 129 base_cmd = ["npx", f"truffle@{truffle_version}"] 130 elif os.path.isfile(os.path.join(self._target, "package.json")): 131 with open(os.path.join(self._target, "package.json"), encoding="utf8") as file_desc: 132 package = json.load(file_desc) 133 if "devDependencies" in package: 134 if "truffle" in package["devDependencies"]: 135 version = package["devDependencies"]["truffle"] 136 if version.startswith("^"): 137 version = version[1:] 138 truffle_version = f"truffle@{version}" 139 base_cmd = ["npx", truffle_version] 140 if "dependencies" in package: 141 if "truffle" in package["dependencies"]: 142 version = package["dependencies"]["truffle"] 143 if version.startswith("^"): 144 version = version[1:] 145 truffle_version = f"truffle@{version}" 146 base_cmd = ["npx", truffle_version] 147 148 if not truffle_ignore_compile: 149 cmd = base_cmd + ["compile", "--all"] 150 151 LOGGER.info( 152 "'%s' running (use --truffle-version truffle@x.x.x to use specific version)", 153 " ".join(cmd), 154 ) 155 156 config_used = None 157 config_saved = None 158 if truffle_overwrite_config: 159 overwritten_version = kwargs.get("truffle_overwrite_version", None) 160 # If the version is not provided, we try to guess it with the config file 161 if overwritten_version is None: 162 version_from_config = _get_version_from_config(self._target) 163 if version_from_config: 164 overwritten_version, _ = version_from_config 165 166 # Save the config file, and write our temporary config 167 config_used, config_saved = _save_config(Path(self._target)) 168 if config_used is None: 169 config_used = Path("truffle-config.js") 170 _write_config(Path(self._target), config_used, overwritten_version) 171 172 with subprocess.Popen( 173 cmd, 174 stdout=subprocess.PIPE, 175 stderr=subprocess.PIPE, 176 cwd=self._target, 177 executable=shutil.which(cmd[0]), 178 ) as process: 179 180 stdout_bytes, stderr_bytes = process.communicate() 181 stdout, stderr = ( 182 stdout_bytes.decode(errors="backslashreplace"), 183 stderr_bytes.decode(errors="backslashreplace"), 184 ) # convert bytestrings to unicode strings 185 186 if truffle_overwrite_config: 187 assert config_used 188 _reload_config(Path(self._target), config_saved, config_used) 189 190 LOGGER.info(stdout) 191 if stderr: 192 LOGGER.error(stderr) 193 if not os.path.isdir(os.path.join(self._target, build_directory)): 194 if os.path.isdir(os.path.join(self._target, "node_modules")): 195 raise InvalidCompilation( 196 f"External dependencies {build_directory} {self._target} not found, please install them. (npm install)" 197 ) 198 raise InvalidCompilation("`truffle compile` failed. Can you run it?") 199 filenames = glob.glob(os.path.join(self._target, build_directory, "*.json")) 200 201 optimized = None 202 203 version = None 204 compiler = None 205 compilation_unit = CompilationUnit(crytic_compile, str(self._target)) 206 207 for filename_txt in filenames: 208 with open(filename_txt, encoding="utf8") as file_desc: 209 target_loaded = json.load(file_desc) 210 # pylint: disable=too-many-nested-blocks 211 if optimized is None: 212 if "metadata" in target_loaded: 213 metadata = target_loaded["metadata"] 214 try: 215 metadata = json.loads(metadata) 216 if "settings" in metadata: 217 if "optimizer" in metadata["settings"]: 218 if "enabled" in metadata["settings"]["optimizer"]: 219 optimized = metadata["settings"]["optimizer"]["enabled"] 220 except json.decoder.JSONDecodeError: 221 pass 222 223 userdoc = target_loaded.get("userdoc", {}) 224 devdoc = target_loaded.get("devdoc", {}) 225 natspec = Natspec(userdoc, devdoc) 226 227 if not "ast" in target_loaded: 228 continue 229 230 filename = target_loaded["ast"]["absolutePath"] 231 232 # Since truffle 5.3.14, the filenames start with "project:" 233 # See https://github.com/crytic/crytic-compile/issues/199 234 if filename.startswith("project:"): 235 filename = "." + filename[len("project:") :] 236 237 try: 238 filename = convert_filename( 239 filename, _relative_to_short, crytic_compile, working_dir=self._target 240 ) 241 except InvalidCompilation as i: 242 txt = str(i) 243 txt += "\nConsider removing the build/contracts content (rm build/contracts/*)" 244 # pylint: disable=raise-missing-from 245 raise InvalidCompilation(txt) 246 247 source_unit = compilation_unit.create_source_unit(filename) 248 249 source_unit.ast = target_loaded["ast"] 250 251 contract_name = target_loaded["contractName"] 252 source_unit.natspec[contract_name] = natspec 253 compilation_unit.filename_to_contracts[filename].add(contract_name) 254 source_unit.add_contract_name(contract_name) 255 source_unit.abis[contract_name] = target_loaded["abi"] 256 source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( 257 "0x", "" 258 ) 259 source_unit.bytecodes_runtime[contract_name] = target_loaded[ 260 "deployedBytecode" 261 ].replace("0x", "") 262 source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") 263 source_unit.srcmaps_runtime[contract_name] = target_loaded[ 264 "deployedSourceMap" 265 ].split(";") 266 267 if compiler is None: 268 compiler = target_loaded.get("compiler", {}).get("name", None) 269 if version is None: 270 version = target_loaded.get("compiler", {}).get("version", None) 271 if "+" in version: 272 version = version[0 : version.find("+")] 273 274 if version is None or compiler is None: 275 version_from_config = _get_version_from_config(self._target) 276 if version_from_config: 277 version, compiler = version_from_config 278 else: 279 version, compiler = _get_version(base_cmd, cwd=self._target) 280 281 compilation_unit.compiler_version = CompilerVersion( 282 compiler=compiler, version=version, optimized=optimized 283 ) 284 285 def clean(self, **_kwargs: str) -> None: 286 """Clean compilation artifacts 287 288 Args: 289 **_kwargs: unused. 290 """ 291 return 292 293 @staticmethod 294 def is_supported(target: str, **kwargs: str) -> bool: 295 """Check if the target is a truffle project 296 297 Args: 298 target (str): path to the target 299 **kwargs: optional arguments. Used: "truffle_ignore" 300 301 Returns: 302 bool: True if the target is a truffle project 303 """ 304 truffle_ignore = kwargs.get("truffle_ignore", False) 305 if truffle_ignore: 306 return False 307 308 return os.path.isfile(os.path.join(target, "truffle.js")) or os.path.isfile( 309 os.path.join(target, "truffle-config.js") 310 ) 311 312 # pylint: disable=no-self-use 313 def is_dependency(self, path: str) -> bool: 314 """Check if the path is a dependency 315 316 Args: 317 path (str): path to the target 318 319 Returns: 320 bool: True if the target is a dependency 321 """ 322 if path in self._cached_dependencies: 323 return self._cached_dependencies[path] 324 ret = "node_modules" in Path(path).parts 325 self._cached_dependencies[path] = ret 326 return ret 327 328 # pylint: disable=no-self-use 329 def _guessed_tests(self) -> List[str]: 330 """Guess the potential unit tests commands 331 332 Returns: 333 List[str]: The guessed unit tests commands 334 """ 335 return ["truffle test"] 336 337 338def _get_version_from_config(target: str) -> Optional[Tuple[str, str]]: 339 """Naive check on the truffleconfig file to get the version 340 341 Args: 342 target (str): path to the project directory 343 344 Returns: 345 Optional[Tuple[str, str]]: (compiler version, compiler name) 346 """ 347 config = Path(target, "truffle-config.js") 348 if not config.exists(): 349 config = Path(target, "truffle.js") 350 if not config.exists(): 351 return None 352 with open(config, "r", encoding="utf8") as config_f: 353 config_buffer = config_f.read() 354 355 # The config is a javascript file 356 # Use a naive regex to match the solc version 357 match = re.search(r'solc: {[ ]*\n[ ]*version: "([0-9\.]*)', config_buffer) 358 if match: 359 if match.groups(): 360 version = match.groups()[0] 361 return version, "solc-js" 362 return None 363 364 365def _get_version(truffle_call: List[str], cwd: str) -> Tuple[str, str]: 366 """Get the compiler version 367 368 Args: 369 truffle_call (List[str]): Command to run truffle 370 cwd (str): Working directory to run truffle 371 372 Raises: 373 InvalidCompilation: If truffle failed, or the solidity version was not found 374 375 Returns: 376 Tuple[str, str]: (compiler version, compiler name) 377 """ 378 cmd = truffle_call + ["version"] 379 try: 380 with subprocess.Popen( 381 cmd, 382 stdout=subprocess.PIPE, 383 stderr=subprocess.PIPE, 384 cwd=cwd, 385 executable=shutil.which(cmd[0]), 386 ) as process: 387 sstdout, _ = process.communicate() 388 ssstdout = sstdout.decode() # convert bytestrings to unicode strings 389 if not ssstdout: 390 raise InvalidCompilation("Truffle failed to run: 'truffle version'") 391 stdout = ssstdout.split("\n") 392 for line in stdout: 393 if "Solidity" in line: 394 if "native" in line: 395 return solc.get_version("solc", {}), "solc-native" 396 version = re.findall(r"\d+\.\d+\.\d+", line)[0] 397 compiler = re.findall(r"(solc[a-z\-]*)", line) 398 if len(compiler) > 0: 399 return version, compiler[0] 400 401 raise InvalidCompilation(f"Solidity version not found {stdout}") 402 except OSError as error: 403 # pylint: disable=raise-missing-from 404 raise InvalidCompilation(f"Truffle failed: {error}") 405 406 407def _save_config(cwd: Path) -> Tuple[Optional[Path], Optional[Path]]: 408 """Save truffle-config.js / truffle.js to a temporary file. 409 410 Args: 411 cwd (Path): Working directory 412 413 Returns: 414 Tuple[Optional[Path], Optional[Path]]: (original_config_name, temporary_file). None if there was no config file 415 """ 416 unique_filename = str(uuid.uuid4()) 417 while Path(cwd, unique_filename).exists(): 418 unique_filename = str(uuid.uuid4()) 419 420 if Path(cwd, "truffle-config.js").exists(): 421 shutil.move(str(Path(cwd, "truffle-config.js")), str(Path(cwd, unique_filename))) 422 return Path("truffle-config.js"), Path(unique_filename) 423 424 if Path(cwd, "truffle.js").exists(): 425 shutil.move(str(Path(cwd, "truffle.js")), str(Path(cwd, unique_filename))) 426 return Path("truffle.js"), Path(unique_filename) 427 return None, None 428 429 430def _reload_config(cwd: Path, original_config: Optional[Path], tmp_config: Path) -> None: 431 """Restore the original config 432 433 Args: 434 cwd (Path): Working directory 435 original_config (Optional[Path]): Original config saved 436 tmp_config (Path): Temporary config 437 """ 438 os.remove(Path(cwd, tmp_config)) 439 if original_config is not None: 440 shutil.move(str(Path(cwd, original_config)), str(Path(cwd, tmp_config))) 441 442 443def _write_config(cwd: Path, original_config: Path, version: Optional[str]) -> None: 444 """Write the config file 445 446 Args: 447 cwd (Path): Working directory 448 original_config (Path): Original config saved 449 version (Optional[str]): Solc version 450 """ 451 txt = "" 452 if version: 453 txt = f""" 454 module.exports = {{ 455 compilers: {{ 456 solc: {{ 457 version: "{version}" 458 }} 459 }} 460 }} 461 """ 462 with open(Path(cwd, original_config), "w", encoding="utf8") as f: 463 f.write(txt) 464 465 466def _relative_to_short(relative: Path) -> Path: 467 """Convert the relative path to its short version 468 469 Args: 470 relative (Path): Path to convert 471 472 Returns: 473 Path: Converted path 474 """ 475 short = relative 476 try: 477 short = short.relative_to(Path("contracts")) 478 except ValueError: 479 try: 480 short = short.relative_to("node_modules") 481 except ValueError: 482 pass 483 return short
33def export_to_truffle(crytic_compile: "CryticCompile", **kwargs: str) -> List[str]: 34 """Export to the truffle format 35 36 Args: 37 crytic_compile (CryticCompile): CryticCompile object to export 38 **kwargs: optional arguments. Used: "export_dir" 39 40 Raises: 41 InvalidCompilation: If there are more than 1 compilation unit 42 43 Returns: 44 List[str]: Singleton with the generated directory 45 """ 46 # Get our export directory, if it's set, we create the path. 47 export_dir = kwargs.get("export_dir", "crytic-export") 48 if export_dir and not os.path.exists(export_dir): 49 os.makedirs(export_dir) 50 51 compilation_units = list(crytic_compile.compilation_units.values()) 52 if len(compilation_units) != 1: 53 raise InvalidCompilation("Truffle export require 1 compilation unit") 54 compilation_unit = compilation_units[0] 55 56 # Loop for each contract filename. 57 58 libraries = compilation_unit.crytic_compile.libraries 59 60 results: List[Dict] = [] 61 for source_unit in compilation_unit.source_units.values(): 62 for contract_name in source_unit.contracts_names: 63 # Create the informational object to output for this contract 64 output = { 65 "contractName": contract_name, 66 "abi": source_unit.abi(contract_name), 67 "bytecode": "0x" + source_unit.bytecode_init(contract_name, libraries), 68 "deployedBytecode": "0x" + source_unit.bytecode_runtime(contract_name, libraries), 69 "ast": source_unit.ast, 70 "userdoc": source_unit.natspec[contract_name].userdoc.export(), 71 "devdoc": source_unit.natspec[contract_name].devdoc.export(), 72 } 73 results.append(output) 74 75 # If we have an export directory, export it. 76 77 path = os.path.join(export_dir, contract_name + ".json") 78 with open(path, "w", encoding="utf8") as file_desc: 79 json.dump(output, file_desc) 80 81 return [export_dir]
Export to the truffle format
Args: crytic_compile (CryticCompile): CryticCompile object to export **kwargs: optional arguments. Used: "export_dir"
Raises: InvalidCompilation: If there are more than 1 compilation unit
Returns: List[str]: Singleton with the generated directory
84class Truffle(AbstractPlatform): 85 """ 86 Truffle platform 87 """ 88 89 NAME = "Truffle" 90 PROJECT_URL = "https://github.com/trufflesuite/truffle" 91 TYPE = Type.TRUFFLE 92 93 # pylint: disable=too-many-locals,too-many-statements,too-many-branches 94 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 95 """Compile 96 97 Args: 98 crytic_compile (CryticCompile): CryticCompile object to populate 99 **kwargs: optional arguments. Used "truffle_build_directory", "truffle_ignore_compile", "ignore_compile", 100 "truffle_version", "npx_disable" 101 102 Raises: 103 InvalidCompilation: If truffle failed to run 104 """ 105 106 build_directory = kwargs.get("truffle_build_directory", os.path.join("build", "contracts")) 107 truffle_ignore_compile = kwargs.get("truffle_ignore_compile", False) or kwargs.get( 108 "ignore_compile", False 109 ) 110 truffle_version = kwargs.get("truffle_version", None) 111 # crytic_compile.type = Type.TRUFFLE 112 # Truffle on windows has naming conflicts where it will invoke truffle.js directly instead 113 # of truffle.cmd (unless in powershell or git bash). 114 # The cleanest solution is to explicitly call 115 # truffle.cmd. Reference: 116 # https://truffleframework.com/docs/truffle/reference/configuration#resolving-naming-conflicts-on-windows 117 118 truffle_overwrite_config = kwargs.get("truffle_overwrite_config", False) 119 120 if platform.system() == "Windows": 121 base_cmd = ["truffle.cmd"] 122 elif kwargs.get("npx_disable", False): 123 base_cmd = ["truffle"] 124 else: 125 base_cmd = ["npx", "truffle"] 126 if truffle_version: 127 if truffle_version.startswith("truffle"): 128 base_cmd = ["npx", truffle_version] 129 else: 130 base_cmd = ["npx", f"truffle@{truffle_version}"] 131 elif os.path.isfile(os.path.join(self._target, "package.json")): 132 with open(os.path.join(self._target, "package.json"), encoding="utf8") as file_desc: 133 package = json.load(file_desc) 134 if "devDependencies" in package: 135 if "truffle" in package["devDependencies"]: 136 version = package["devDependencies"]["truffle"] 137 if version.startswith("^"): 138 version = version[1:] 139 truffle_version = f"truffle@{version}" 140 base_cmd = ["npx", truffle_version] 141 if "dependencies" in package: 142 if "truffle" in package["dependencies"]: 143 version = package["dependencies"]["truffle"] 144 if version.startswith("^"): 145 version = version[1:] 146 truffle_version = f"truffle@{version}" 147 base_cmd = ["npx", truffle_version] 148 149 if not truffle_ignore_compile: 150 cmd = base_cmd + ["compile", "--all"] 151 152 LOGGER.info( 153 "'%s' running (use --truffle-version truffle@x.x.x to use specific version)", 154 " ".join(cmd), 155 ) 156 157 config_used = None 158 config_saved = None 159 if truffle_overwrite_config: 160 overwritten_version = kwargs.get("truffle_overwrite_version", None) 161 # If the version is not provided, we try to guess it with the config file 162 if overwritten_version is None: 163 version_from_config = _get_version_from_config(self._target) 164 if version_from_config: 165 overwritten_version, _ = version_from_config 166 167 # Save the config file, and write our temporary config 168 config_used, config_saved = _save_config(Path(self._target)) 169 if config_used is None: 170 config_used = Path("truffle-config.js") 171 _write_config(Path(self._target), config_used, overwritten_version) 172 173 with subprocess.Popen( 174 cmd, 175 stdout=subprocess.PIPE, 176 stderr=subprocess.PIPE, 177 cwd=self._target, 178 executable=shutil.which(cmd[0]), 179 ) as process: 180 181 stdout_bytes, stderr_bytes = process.communicate() 182 stdout, stderr = ( 183 stdout_bytes.decode(errors="backslashreplace"), 184 stderr_bytes.decode(errors="backslashreplace"), 185 ) # convert bytestrings to unicode strings 186 187 if truffle_overwrite_config: 188 assert config_used 189 _reload_config(Path(self._target), config_saved, config_used) 190 191 LOGGER.info(stdout) 192 if stderr: 193 LOGGER.error(stderr) 194 if not os.path.isdir(os.path.join(self._target, build_directory)): 195 if os.path.isdir(os.path.join(self._target, "node_modules")): 196 raise InvalidCompilation( 197 f"External dependencies {build_directory} {self._target} not found, please install them. (npm install)" 198 ) 199 raise InvalidCompilation("`truffle compile` failed. Can you run it?") 200 filenames = glob.glob(os.path.join(self._target, build_directory, "*.json")) 201 202 optimized = None 203 204 version = None 205 compiler = None 206 compilation_unit = CompilationUnit(crytic_compile, str(self._target)) 207 208 for filename_txt in filenames: 209 with open(filename_txt, encoding="utf8") as file_desc: 210 target_loaded = json.load(file_desc) 211 # pylint: disable=too-many-nested-blocks 212 if optimized is None: 213 if "metadata" in target_loaded: 214 metadata = target_loaded["metadata"] 215 try: 216 metadata = json.loads(metadata) 217 if "settings" in metadata: 218 if "optimizer" in metadata["settings"]: 219 if "enabled" in metadata["settings"]["optimizer"]: 220 optimized = metadata["settings"]["optimizer"]["enabled"] 221 except json.decoder.JSONDecodeError: 222 pass 223 224 userdoc = target_loaded.get("userdoc", {}) 225 devdoc = target_loaded.get("devdoc", {}) 226 natspec = Natspec(userdoc, devdoc) 227 228 if not "ast" in target_loaded: 229 continue 230 231 filename = target_loaded["ast"]["absolutePath"] 232 233 # Since truffle 5.3.14, the filenames start with "project:" 234 # See https://github.com/crytic/crytic-compile/issues/199 235 if filename.startswith("project:"): 236 filename = "." + filename[len("project:") :] 237 238 try: 239 filename = convert_filename( 240 filename, _relative_to_short, crytic_compile, working_dir=self._target 241 ) 242 except InvalidCompilation as i: 243 txt = str(i) 244 txt += "\nConsider removing the build/contracts content (rm build/contracts/*)" 245 # pylint: disable=raise-missing-from 246 raise InvalidCompilation(txt) 247 248 source_unit = compilation_unit.create_source_unit(filename) 249 250 source_unit.ast = target_loaded["ast"] 251 252 contract_name = target_loaded["contractName"] 253 source_unit.natspec[contract_name] = natspec 254 compilation_unit.filename_to_contracts[filename].add(contract_name) 255 source_unit.add_contract_name(contract_name) 256 source_unit.abis[contract_name] = target_loaded["abi"] 257 source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( 258 "0x", "" 259 ) 260 source_unit.bytecodes_runtime[contract_name] = target_loaded[ 261 "deployedBytecode" 262 ].replace("0x", "") 263 source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") 264 source_unit.srcmaps_runtime[contract_name] = target_loaded[ 265 "deployedSourceMap" 266 ].split(";") 267 268 if compiler is None: 269 compiler = target_loaded.get("compiler", {}).get("name", None) 270 if version is None: 271 version = target_loaded.get("compiler", {}).get("version", None) 272 if "+" in version: 273 version = version[0 : version.find("+")] 274 275 if version is None or compiler is None: 276 version_from_config = _get_version_from_config(self._target) 277 if version_from_config: 278 version, compiler = version_from_config 279 else: 280 version, compiler = _get_version(base_cmd, cwd=self._target) 281 282 compilation_unit.compiler_version = CompilerVersion( 283 compiler=compiler, version=version, optimized=optimized 284 ) 285 286 def clean(self, **_kwargs: str) -> None: 287 """Clean compilation artifacts 288 289 Args: 290 **_kwargs: unused. 291 """ 292 return 293 294 @staticmethod 295 def is_supported(target: str, **kwargs: str) -> bool: 296 """Check if the target is a truffle project 297 298 Args: 299 target (str): path to the target 300 **kwargs: optional arguments. Used: "truffle_ignore" 301 302 Returns: 303 bool: True if the target is a truffle project 304 """ 305 truffle_ignore = kwargs.get("truffle_ignore", False) 306 if truffle_ignore: 307 return False 308 309 return os.path.isfile(os.path.join(target, "truffle.js")) or os.path.isfile( 310 os.path.join(target, "truffle-config.js") 311 ) 312 313 # pylint: disable=no-self-use 314 def is_dependency(self, path: str) -> bool: 315 """Check if the path is a dependency 316 317 Args: 318 path (str): path to the target 319 320 Returns: 321 bool: True if the target is a dependency 322 """ 323 if path in self._cached_dependencies: 324 return self._cached_dependencies[path] 325 ret = "node_modules" in Path(path).parts 326 self._cached_dependencies[path] = ret 327 return ret 328 329 # pylint: disable=no-self-use 330 def _guessed_tests(self) -> List[str]: 331 """Guess the potential unit tests commands 332 333 Returns: 334 List[str]: The guessed unit tests commands 335 """ 336 return ["truffle test"]
Truffle platform
94 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 95 """Compile 96 97 Args: 98 crytic_compile (CryticCompile): CryticCompile object to populate 99 **kwargs: optional arguments. Used "truffle_build_directory", "truffle_ignore_compile", "ignore_compile", 100 "truffle_version", "npx_disable" 101 102 Raises: 103 InvalidCompilation: If truffle failed to run 104 """ 105 106 build_directory = kwargs.get("truffle_build_directory", os.path.join("build", "contracts")) 107 truffle_ignore_compile = kwargs.get("truffle_ignore_compile", False) or kwargs.get( 108 "ignore_compile", False 109 ) 110 truffle_version = kwargs.get("truffle_version", None) 111 # crytic_compile.type = Type.TRUFFLE 112 # Truffle on windows has naming conflicts where it will invoke truffle.js directly instead 113 # of truffle.cmd (unless in powershell or git bash). 114 # The cleanest solution is to explicitly call 115 # truffle.cmd. Reference: 116 # https://truffleframework.com/docs/truffle/reference/configuration#resolving-naming-conflicts-on-windows 117 118 truffle_overwrite_config = kwargs.get("truffle_overwrite_config", False) 119 120 if platform.system() == "Windows": 121 base_cmd = ["truffle.cmd"] 122 elif kwargs.get("npx_disable", False): 123 base_cmd = ["truffle"] 124 else: 125 base_cmd = ["npx", "truffle"] 126 if truffle_version: 127 if truffle_version.startswith("truffle"): 128 base_cmd = ["npx", truffle_version] 129 else: 130 base_cmd = ["npx", f"truffle@{truffle_version}"] 131 elif os.path.isfile(os.path.join(self._target, "package.json")): 132 with open(os.path.join(self._target, "package.json"), encoding="utf8") as file_desc: 133 package = json.load(file_desc) 134 if "devDependencies" in package: 135 if "truffle" in package["devDependencies"]: 136 version = package["devDependencies"]["truffle"] 137 if version.startswith("^"): 138 version = version[1:] 139 truffle_version = f"truffle@{version}" 140 base_cmd = ["npx", truffle_version] 141 if "dependencies" in package: 142 if "truffle" in package["dependencies"]: 143 version = package["dependencies"]["truffle"] 144 if version.startswith("^"): 145 version = version[1:] 146 truffle_version = f"truffle@{version}" 147 base_cmd = ["npx", truffle_version] 148 149 if not truffle_ignore_compile: 150 cmd = base_cmd + ["compile", "--all"] 151 152 LOGGER.info( 153 "'%s' running (use --truffle-version truffle@x.x.x to use specific version)", 154 " ".join(cmd), 155 ) 156 157 config_used = None 158 config_saved = None 159 if truffle_overwrite_config: 160 overwritten_version = kwargs.get("truffle_overwrite_version", None) 161 # If the version is not provided, we try to guess it with the config file 162 if overwritten_version is None: 163 version_from_config = _get_version_from_config(self._target) 164 if version_from_config: 165 overwritten_version, _ = version_from_config 166 167 # Save the config file, and write our temporary config 168 config_used, config_saved = _save_config(Path(self._target)) 169 if config_used is None: 170 config_used = Path("truffle-config.js") 171 _write_config(Path(self._target), config_used, overwritten_version) 172 173 with subprocess.Popen( 174 cmd, 175 stdout=subprocess.PIPE, 176 stderr=subprocess.PIPE, 177 cwd=self._target, 178 executable=shutil.which(cmd[0]), 179 ) as process: 180 181 stdout_bytes, stderr_bytes = process.communicate() 182 stdout, stderr = ( 183 stdout_bytes.decode(errors="backslashreplace"), 184 stderr_bytes.decode(errors="backslashreplace"), 185 ) # convert bytestrings to unicode strings 186 187 if truffle_overwrite_config: 188 assert config_used 189 _reload_config(Path(self._target), config_saved, config_used) 190 191 LOGGER.info(stdout) 192 if stderr: 193 LOGGER.error(stderr) 194 if not os.path.isdir(os.path.join(self._target, build_directory)): 195 if os.path.isdir(os.path.join(self._target, "node_modules")): 196 raise InvalidCompilation( 197 f"External dependencies {build_directory} {self._target} not found, please install them. (npm install)" 198 ) 199 raise InvalidCompilation("`truffle compile` failed. Can you run it?") 200 filenames = glob.glob(os.path.join(self._target, build_directory, "*.json")) 201 202 optimized = None 203 204 version = None 205 compiler = None 206 compilation_unit = CompilationUnit(crytic_compile, str(self._target)) 207 208 for filename_txt in filenames: 209 with open(filename_txt, encoding="utf8") as file_desc: 210 target_loaded = json.load(file_desc) 211 # pylint: disable=too-many-nested-blocks 212 if optimized is None: 213 if "metadata" in target_loaded: 214 metadata = target_loaded["metadata"] 215 try: 216 metadata = json.loads(metadata) 217 if "settings" in metadata: 218 if "optimizer" in metadata["settings"]: 219 if "enabled" in metadata["settings"]["optimizer"]: 220 optimized = metadata["settings"]["optimizer"]["enabled"] 221 except json.decoder.JSONDecodeError: 222 pass 223 224 userdoc = target_loaded.get("userdoc", {}) 225 devdoc = target_loaded.get("devdoc", {}) 226 natspec = Natspec(userdoc, devdoc) 227 228 if not "ast" in target_loaded: 229 continue 230 231 filename = target_loaded["ast"]["absolutePath"] 232 233 # Since truffle 5.3.14, the filenames start with "project:" 234 # See https://github.com/crytic/crytic-compile/issues/199 235 if filename.startswith("project:"): 236 filename = "." + filename[len("project:") :] 237 238 try: 239 filename = convert_filename( 240 filename, _relative_to_short, crytic_compile, working_dir=self._target 241 ) 242 except InvalidCompilation as i: 243 txt = str(i) 244 txt += "\nConsider removing the build/contracts content (rm build/contracts/*)" 245 # pylint: disable=raise-missing-from 246 raise InvalidCompilation(txt) 247 248 source_unit = compilation_unit.create_source_unit(filename) 249 250 source_unit.ast = target_loaded["ast"] 251 252 contract_name = target_loaded["contractName"] 253 source_unit.natspec[contract_name] = natspec 254 compilation_unit.filename_to_contracts[filename].add(contract_name) 255 source_unit.add_contract_name(contract_name) 256 source_unit.abis[contract_name] = target_loaded["abi"] 257 source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( 258 "0x", "" 259 ) 260 source_unit.bytecodes_runtime[contract_name] = target_loaded[ 261 "deployedBytecode" 262 ].replace("0x", "") 263 source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") 264 source_unit.srcmaps_runtime[contract_name] = target_loaded[ 265 "deployedSourceMap" 266 ].split(";") 267 268 if compiler is None: 269 compiler = target_loaded.get("compiler", {}).get("name", None) 270 if version is None: 271 version = target_loaded.get("compiler", {}).get("version", None) 272 if "+" in version: 273 version = version[0 : version.find("+")] 274 275 if version is None or compiler is None: 276 version_from_config = _get_version_from_config(self._target) 277 if version_from_config: 278 version, compiler = version_from_config 279 else: 280 version, compiler = _get_version(base_cmd, cwd=self._target) 281 282 compilation_unit.compiler_version = CompilerVersion( 283 compiler=compiler, version=version, optimized=optimized 284 )
Compile
Args: crytic_compile (CryticCompile): CryticCompile object to populate **kwargs: optional arguments. Used "truffle_build_directory", "truffle_ignore_compile", "ignore_compile", "truffle_version", "npx_disable"
Raises: InvalidCompilation: If truffle failed to run
286 def clean(self, **_kwargs: str) -> None: 287 """Clean compilation artifacts 288 289 Args: 290 **_kwargs: unused. 291 """ 292 return
Clean compilation artifacts
Args: **_kwargs: unused.
294 @staticmethod 295 def is_supported(target: str, **kwargs: str) -> bool: 296 """Check if the target is a truffle project 297 298 Args: 299 target (str): path to the target 300 **kwargs: optional arguments. Used: "truffle_ignore" 301 302 Returns: 303 bool: True if the target is a truffle project 304 """ 305 truffle_ignore = kwargs.get("truffle_ignore", False) 306 if truffle_ignore: 307 return False 308 309 return os.path.isfile(os.path.join(target, "truffle.js")) or os.path.isfile( 310 os.path.join(target, "truffle-config.js") 311 )
Check if the target is a truffle project
Args: target (str): path to the target **kwargs: optional arguments. Used: "truffle_ignore"
Returns: bool: True if the target is a truffle project
314 def is_dependency(self, path: str) -> bool: 315 """Check if the path is a dependency 316 317 Args: 318 path (str): path to the target 319 320 Returns: 321 bool: True if the target is a dependency 322 """ 323 if path in self._cached_dependencies: 324 return self._cached_dependencies[path] 325 ret = "node_modules" in Path(path).parts 326 self._cached_dependencies[path] = ret 327 return ret
Check if the path is a dependency
Args: path (str): path to the target
Returns: bool: True if the target is a dependency