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
LOGGER = <Logger CryticCompile (WARNING)>
def export_to_truffle( crytic_compile: crytic_compile.crytic_compile.CryticCompile, **kwargs: str) -> List[str]:
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

NAME: str = 'Truffle'
PROJECT_URL: str = 'https://github.com/trufflesuite/truffle'
TYPE: crytic_compile.platform.types.Type = <Type.TRUFFLE: 2>
def compile( self, crytic_compile: crytic_compile.crytic_compile.CryticCompile, **kwargs: str) -> None:
 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

def clean(self, **_kwargs: str) -> None:
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.

@staticmethod
def is_supported(target: str, **kwargs: str) -> bool:
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

def is_dependency(self, path: str) -> bool:
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