diff --git a/.github/workflows/doctor.yml b/.github/workflows/doctor.yml new file mode 100644 index 0000000000..b6124216a1 --- /dev/null +++ b/.github/workflows/doctor.yml @@ -0,0 +1,87 @@ +--- +name: CI + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + pull_request: + paths: + - 'slither/tools/doctor/**' + - '.github/workflows/doctor.yml' + +jobs: + slither-doctor: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "windows-2022"] + python: ["3.8", "3.9", "3.10", "3.11"] + exclude: + # strange failure + - os: windows-2022 + python: 3.8 + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Try system-wide Slither + run: | + echo "::group::Install slither" + pip3 install . + echo "::endgroup::" + + # escape cwd so python doesn't pick up local module + cd / + + echo "::group::Via module" + python3 -m slither.tools.doctor . + echo "::endgroup::" + + echo "::group::Via binary" + slither-doctor . + echo "::endgroup::" + + - name: Try user Slither + run: | + echo "::group::Install slither" + pip3 install --user . + echo "::endgroup::" + + # escape cwd so python doesn't pick up local module + cd / + + echo "::group::Via module" + python3 -m slither.tools.doctor . + echo "::endgroup::" + + echo "::group::Via binary" + slither-doctor . + echo "::endgroup::" + + - name: Try venv Slither + run: | + echo "::group::Install slither" + python3 -m venv venv + source venv/bin/activate || source venv/Scripts/activate + hash -r + pip3 install . + echo "::endgroup::" + + # escape cwd so python doesn't pick up local module + cd / + + echo "::group::Via module" + python3 -m slither.tools.doctor . + echo "::endgroup::" + + echo "::group::Via binary" + slither-doctor . + echo "::endgroup::" diff --git a/setup.py b/setup.py index 89adf7f4ab..cc125011e5 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ packages=find_packages(), python_requires=">=3.8", install_requires=[ + "packaging", "prettytable>=0.7.2", "pycryptodome>=3.4.6", # "crytic-compile>=0.2.4", diff --git a/slither/tools/doctor/__main__.py b/slither/tools/doctor/__main__.py index 94ae865ec2..b9b4c54977 100644 --- a/slither/tools/doctor/__main__.py +++ b/slither/tools/doctor/__main__.py @@ -1,4 +1,6 @@ import argparse +import logging +import sys from crytic_compile import cryticparser @@ -25,6 +27,9 @@ def parse_args() -> argparse.Namespace: def main(): + # log on stdout to keep output in order + logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, force=True) + args = parse_args() kwargs = vars(args) diff --git a/slither/tools/doctor/checks/__init__.py b/slither/tools/doctor/checks/__init__.py index 762c60b5d0..8a07419406 100644 --- a/slither/tools/doctor/checks/__init__.py +++ b/slither/tools/doctor/checks/__init__.py @@ -1,6 +1,7 @@ from typing import Callable, List from dataclasses import dataclass +from slither.tools.doctor.checks.paths import check_slither_path from slither.tools.doctor.checks.platform import compile_project, detect_platform from slither.tools.doctor.checks.versions import show_versions @@ -12,6 +13,7 @@ class Check: ALL_CHECKS: List[Check] = [ + Check("PATH configuration", check_slither_path), Check("Software versions", show_versions), Check("Project platform", detect_platform), Check("Project compilation", compile_project), diff --git a/slither/tools/doctor/checks/paths.py b/slither/tools/doctor/checks/paths.py new file mode 100644 index 0000000000..d388847ef4 --- /dev/null +++ b/slither/tools/doctor/checks/paths.py @@ -0,0 +1,85 @@ +from pathlib import Path +from typing import List, Optional, Tuple +import shutil +import sys +import sysconfig + +from slither.utils.colors import yellow, green, red + + +def path_is_relative_to(path: Path, relative_to: Path) -> bool: + """ + Check if a path is relative to another one. + + Compatibility wrapper for Path.is_relative_to + """ + if sys.version_info >= (3, 9, 0): + return path.is_relative_to(relative_to) + + path_parts = path.resolve().parts + relative_to_parts = relative_to.resolve().parts + + if len(path_parts) < len(relative_to_parts): + return False + + for (a, b) in zip(path_parts, relative_to_parts): + if a != b: + return False + + return True + + +def check_path_config(name: str) -> Tuple[bool, Optional[Path], List[Path]]: + """ + Check if a given Python binary/script is in PATH. + :return: Returns if the binary on PATH corresponds to this installation, + its path (if present), and a list of possible paths where this + binary might be found. + """ + binary_path = shutil.which(name) + possible_paths = [] + + for scheme in sysconfig.get_scheme_names(): + script_path = Path(sysconfig.get_path("scripts", scheme)) + purelib_path = Path(sysconfig.get_path("purelib", scheme)) + script_binary_path = shutil.which(name, path=script_path) + if script_binary_path is not None: + possible_paths.append((script_path, purelib_path)) + + binary_here = False + if binary_path is not None: + binary_path = Path(binary_path) + this_code = Path(__file__) + this_binary = list(filter(lambda x: path_is_relative_to(this_code, x[1]), possible_paths)) + binary_here = len(this_binary) > 0 and all( + path_is_relative_to(binary_path, script) for script, _ in this_binary + ) + + return binary_here, binary_path, list(set(script for script, _ in possible_paths)) + + +def check_slither_path(**_kwargs) -> None: + binary_here, binary_path, possible_paths = check_path_config("slither") + show_paths = False + + if binary_path: + print(green(f"`slither` found in PATH at `{binary_path}`.")) + if binary_here: + print(green("Its location matches this slither-doctor installation.")) + else: + print( + yellow( + "This path does not correspond to this slither-doctor installation.\n" + + "Double-check the order of directories in PATH if you have several Slither installations." + ) + ) + show_paths = True + else: + print(red("`slither` was not found in PATH.")) + show_paths = True + + if show_paths: + print() + print("Consider adding one of the following directories to PATH:") + for path in possible_paths: + print(f" * {path}") diff --git a/slither/tools/doctor/checks/versions.py b/slither/tools/doctor/checks/versions.py index 909bccf55b..ec7ef1d1f3 100644 --- a/slither/tools/doctor/checks/versions.py +++ b/slither/tools/doctor/checks/versions.py @@ -3,19 +3,19 @@ from typing import Optional import urllib -from packaging.version import parse, LegacyVersion, Version +from packaging.version import parse, Version from slither.utils.colors import yellow, green -def get_installed_version(name: str) -> Optional[LegacyVersion | Version]: +def get_installed_version(name: str) -> Optional[Version]: try: return parse(metadata.version(name)) except metadata.PackageNotFoundError: return None -def get_github_version(name: str) -> Optional[LegacyVersion | Version]: +def get_github_version(name: str) -> Optional[Version]: try: with urllib.request.urlopen( f"https://api.github.com/repos/crytic/{name}/releases/latest" @@ -45,7 +45,9 @@ def show_versions(**_kwargs) -> None: for name, (installed, latest) in versions.items(): color = yellow if name in outdated else green - print(f"{name + ':':<16}{color(installed or 'N/A'):<16} (latest is {latest or 'Unknown'})") + print( + f"{name + ':':<16}{color(str(installed) or 'N/A'):<16} (latest is {str(latest) or 'Unknown'})" + ) if len(outdated) > 0: print()