Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions python/ruff/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from __future__ import annotations

from ._find_ruff import find_ruff_bin

__all__ = ["find_ruff_bin"]
87 changes: 13 additions & 74 deletions python/ruff/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <prefix>/pip-build-env-<rand>/overlay/bin/ruff
# Expect to find a "normal" folder at <prefix>/pip-build-env-<rand>/normal
#
# See: https://github.com/pypa/pip/blob/102d8187a1f5a4cd5de7a549fd8a9af34e89a54f/src/pip/_internal/build_env.py#L87
Comment on lines -38 to -43
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started porting this over to uv then noticed we support it already

See astral-sh/uv#18095

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally added per #13321

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()
104 changes: 104 additions & 0 deletions python/ruff/_find_ruff.py
Original file line number Diff line number Diff line change
@@ -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 `<prefix>/Lib/site-packages/ruff`
_join(
_matching_parents(_module_path(), "Lib/site-packages/ruff"), "Scripts"
)
if sys.platform == "win32"
# On Unix, with module path `<prefix>/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 `<target>/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