diff --git a/python/ruff/__init__.py b/python/ruff/__init__.py index e69de29bb2d1d6..07f41420d184b7 100644 --- a/python/ruff/__init__.py +++ b/python/ruff/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from ._find_ruff import find_ruff_bin + +__all__ = ["find_ruff_bin"] diff --git a/python/ruff/__main__.py b/python/ruff/__main__.py index 8569844c4ed345..875131923e2f69 100644 --- a/python/ruff/__main__.py +++ b/python/ruff/__main__.py @@ -2,87 +2,26 @@ import os import sys -import sysconfig +from ruff import find_ruff_bin -def find_ruff_bin() -> str: - """Return the ruff binary path.""" - ruff_exe = "ruff" + sysconfig.get_config_var("EXE") - - scripts_path = os.path.join(sysconfig.get_path("scripts"), ruff_exe) - if os.path.isfile(scripts_path): - return scripts_path - - if sys.version_info >= (3, 10): - user_scheme = sysconfig.get_preferred_scheme("user") - elif os.name == "nt": - user_scheme = "nt_user" - elif sys.platform == "darwin" and sys._framework: - user_scheme = "osx_framework_user" - else: - user_scheme = "posix_user" - - user_path = os.path.join( - sysconfig.get_path("scripts", scheme=user_scheme), ruff_exe - ) - if os.path.isfile(user_path): - return user_path - - # Search in `bin` adjacent to package root (as created by `pip install --target`). - pkg_root = os.path.dirname(os.path.dirname(__file__)) - target_path = os.path.join(pkg_root, "bin", ruff_exe) - if os.path.isfile(target_path): - return target_path - - # Search for pip-specific build environments. - # - # Expect to find ruff in /pip-build-env-/overlay/bin/ruff - # Expect to find a "normal" folder at /pip-build-env-/normal - # - # See: https://github.com/pypa/pip/blob/102d8187a1f5a4cd5de7a549fd8a9af34e89a54f/src/pip/_internal/build_env.py#L87 - paths = os.environ.get("PATH", "").split(os.pathsep) - if len(paths) >= 2: - - def get_last_three_path_parts(path: str) -> list[str]: - """Return a list of up to the last three parts of a path.""" - parts = [] - - while len(parts) < 3: - head, tail = os.path.split(path) - if tail or head != path: - parts.append(tail) - path = head - else: - parts.append(path) - break - - return parts - - maybe_overlay = get_last_three_path_parts(paths[0]) - maybe_normal = get_last_three_path_parts(paths[1]) - if ( - len(maybe_normal) >= 3 - and maybe_normal[-1].startswith("pip-build-env-") - and maybe_normal[-2] == "normal" - and len(maybe_overlay) >= 3 - and maybe_overlay[-1].startswith("pip-build-env-") - and maybe_overlay[-2] == "overlay" - ): - # The overlay must contain the ruff binary. - candidate = os.path.join(paths[0], ruff_exe) - if os.path.isfile(candidate): - return candidate - - raise FileNotFoundError(scripts_path) - - -if __name__ == "__main__": +def _run() -> None: ruff = find_ruff_bin() + if sys.platform == "win32": import subprocess - completed_process = subprocess.run([ruff, *sys.argv[1:]]) + # Avoid emitting a traceback on interrupt + try: + completed_process = subprocess.run([ruff, *sys.argv[1:]]) + except KeyboardInterrupt: + sys.exit(2) + sys.exit(completed_process.returncode) else: os.execvp(ruff, [ruff, *sys.argv[1:]]) + + +if __name__ == "__main__": + _run() diff --git a/python/ruff/_find_ruff.py b/python/ruff/_find_ruff.py new file mode 100644 index 00000000000000..c0213bb23fde26 --- /dev/null +++ b/python/ruff/_find_ruff.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import os +import sys +import sysconfig + + +class RuffNotFound(FileNotFoundError): ... + + +def find_ruff_bin() -> str: + """Return the ruff binary path.""" + + ruff_exe = "ruff" + sysconfig.get_config_var("EXE") + + targets = [ + # The scripts directory for the current Python + sysconfig.get_path("scripts"), + # The scripts directory for the base prefix + sysconfig.get_path("scripts", vars={"base": sys.base_prefix}), + # Above the package root, e.g., from `pip install --prefix` or `uv run --with` + ( + # On Windows, with module path `/Lib/site-packages/ruff` + _join( + _matching_parents(_module_path(), "Lib/site-packages/ruff"), "Scripts" + ) + if sys.platform == "win32" + # On Unix, with module path `/lib/python3.13/site-packages/ruff` + else _join( + _matching_parents(_module_path(), "lib/python*/site-packages/ruff"), + "bin", + ) + ), + # Adjacent to the package root, e.g., from `pip install --target` + # with module path `/ruff` + _join(_matching_parents(_module_path(), "ruff"), "bin"), + # The user scheme scripts directory, e.g., `~/.local/bin` + sysconfig.get_path("scripts", scheme=_user_scheme()), + ] + + seen = [] + for target in targets: + if not target: + continue + if target in seen: + continue + seen.append(target) + path = os.path.join(target, ruff_exe) + if os.path.isfile(path): + return path + + locations = "\n".join(f" - {target}" for target in seen) + raise RuffNotFound( + f"Could not find the ruff binary in any of the following locations:\n{locations}\n" + ) + + +def _module_path() -> str | None: + path = os.path.dirname(__file__) + return path + + +def _matching_parents(path: str | None, match: str) -> str | None: + """ + Return the parent directory of `path` after trimming a `match` from the end. + The match is expected to contain `/` as a path separator, while the `path` + is expected to use the platform's path separator (e.g., `os.sep`). The path + components are compared case-insensitively and a `*` wildcard can be used + in the `match`. + """ + from fnmatch import fnmatch + + if not path: + return None + parts = path.split(os.sep) + match_parts = match.split("/") + if len(parts) < len(match_parts): + return None + + if not all( + fnmatch(part, match_part) + for part, match_part in zip(reversed(parts), reversed(match_parts)) + ): + return None + + return os.sep.join(parts[: -len(match_parts)]) + + +def _join(path: str | None, *parts: str) -> str | None: + if not path: + return None + return os.path.join(path, *parts) + + +def _user_scheme() -> str: + if sys.version_info >= (3, 10): + user_scheme = sysconfig.get_preferred_scheme("user") + elif os.name == "nt": + user_scheme = "nt_user" + elif sys.platform == "darwin" and sys._framework: # ty: ignore[unresolved-attribute] + user_scheme = "osx_framework_user" + else: + user_scheme = "posix_user" + return user_scheme