Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental ruff server now uses local ruff binaries when available #443

Merged
merged 8 commits into from
Apr 11, 2024
89 changes: 89 additions & 0 deletions bundled/tool/ruff_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import os
Copy link

@T-256 T-256 May 23, 2024

Choose a reason for hiding this comment

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

IF we could convert this file into typescript, then extension wouldn't need Python installation/activation on target system at all.
Currently Ruff extension needs python interpreter to start the server.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, our eventual plan is to move this logic to Typescript 😄

Copy link

Choose a reason for hiding this comment

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

Created #479 to track it.

Copy link
Member

Choose a reason for hiding this comment

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

Would be an awesome change.

import shutil
import site
import subprocess
import sys
import sysconfig
from pathlib import Path

RUFF_EXE = "ruff.exe" if sys.platform == "win32" else "ruff"

BUNDLE_DIR = Path(__file__).parent.parent


def update_sys_path(path_to_add: str) -> None:
"""Add given path to `sys.path`."""
if os.path.isdir(path_to_add):
# The `site` module adds the directory at the end, if not yet present; we want
# it to be at the beginning, so that it takes precedence over any other
# installed versions.
sys.path.insert(0, path_to_add)

# Allow development versions of libraries to be imported.
site.addsitedir(path_to_add)


if __name__ == "__main__":
# Ensure that we can import bundled libraries like `packaging`
update_sys_path(os.fspath(BUNDLE_DIR / "libs"))

snowsignal marked this conversation as resolved.
Show resolved Hide resolved

from packaging.specifiers import SpecifierSet
from packaging.version import Version

# This is the first release that included `ruff server`.
# The minimum version may change in the future.
RUFF_VERSION_REQUIREMENT = SpecifierSet(">=0.3.3")
snowsignal marked this conversation as resolved.
Show resolved Hide resolved
# These versions have major bugs or broken integration, and should be avoided.
FORBIDDEN_RUFF_VERSIONS = [Version("0.3.4")]


def executable_version(executable: str) -> Version:
"""Return the version of the executable at the given path."""
output = subprocess.check_output([executable, "--version"]).decode().strip()
version = output.replace("ruff ", "")
return Version(version)


def check_compatibility(
executable: str,
requirement: SpecifierSet,
forbidden_requirements: SpecifierSet,
) -> None:
"""Check the executable for compatibility against various version specifiers."""
version = executable_version(executable)
if not requirement.contains(version, prereleases=True):
message = f"Ruff {requirement} required, but found {version} at {executable}"
raise RuntimeError(message)
for forbidden in forbidden_requirements:
if version == forbidden:
message = (
f"Tried to use Ruff version {version} at {executable}, which is not allowed.\n"
"This version of the server has incompatibilities and/or integration-breaking bugs.\n"
"Please upgrade to the latest version and try again."
)
raise RuntimeError(message)


def find_ruff_bin(fallback: Path) -> Path:
"""Return the ruff binary path."""
path = Path(sysconfig.get_path("scripts")) / RUFF_EXE
if path.is_file():
return path
snowsignal marked this conversation as resolved.
Show resolved Hide resolved

path = shutil.which("ruff")
if path:
return path

return fallback


if __name__ == "__main__":
ruff = os.fsdecode(
find_ruff_bin(
Path(BUNDLE_DIR / "libs" / "bin" / RUFF_EXE),
),
)
check_compatibility(ruff, RUFF_VERSION_REQUIREMENT, FORBIDDEN_RUFF_VERSIONS)
completed_process = subprocess.run([ruff, *sys.argv[1:]], check=False)
sys.exit(completed_process.returncode)
snowsignal marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 5 additions & 1 deletion src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export const DEBUG_SERVER_SCRIPT_PATH = path.join(
"tool",
`_debug_server.py`,
);
export const RUFF_BIN_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, "libs", "bin", "ruff");
export const NEW_SERVER_SCRIPT_PATH = path.join(
snowsignal marked this conversation as resolved.
Show resolved Hide resolved
BUNDLED_PYTHON_SCRIPTS_DIR,
"tool",
"ruff_server.py",
);
export const RUFF_SERVER_CMD = "server";
export const RUFF_SERVER_REQUIRED_ARGS = ["--preview"];
34 changes: 24 additions & 10 deletions src/common/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
import {
BUNDLED_PYTHON_SCRIPTS_DIR,
DEBUG_SERVER_SCRIPT_PATH,
RUFF_BIN_PATH,
RUFF_SERVER_REQUIRED_ARGS,
RUFF_SERVER_CMD,
SERVER_SCRIPT_PATH,
NEW_SERVER_SCRIPT_PATH,
} from "./constants";
import { traceError, traceInfo, traceVerbose } from "./log/logging";
import { getDebuggerPath } from "./python";
Expand All @@ -39,17 +39,31 @@ async function createExperimentalServer(
outputChannel: LogOutputChannel,
initializationOptions: IInitOptions,
): Promise<LanguageClient> {
const command = RUFF_BIN_PATH;
const cwd = settings.cwd;
const args = [RUFF_SERVER_CMD, ...RUFF_SERVER_REQUIRED_ARGS];
let serverOptions: ServerOptions;
if (settings.path.length > 0 && settings.path[0]) {
snowsignal marked this conversation as resolved.
Show resolved Hide resolved
const command = settings.path[0];
const cwd = settings.cwd;
const args = [RUFF_SERVER_CMD, ...RUFF_SERVER_REQUIRED_ARGS];
serverOptions = {
command,
args,
options: { cwd, env: process.env },
};

traceInfo(`Server run command: ${[command, ...args].join(" ")}`);
} else {
const command = settings.interpreter[0];
const cwd = settings.cwd;
const args = [NEW_SERVER_SCRIPT_PATH, RUFF_SERVER_CMD, ...RUFF_SERVER_REQUIRED_ARGS];

const serverOptions: ServerOptions = {
command,
args,
options: { cwd, env: process.env },
};
serverOptions = {
command,
args,
options: { cwd, env: process.env },
};

traceInfo(`Server run command: ${[command, ...args].join(" ")}`);
traceInfo(`Server run command: ${[command, ...args].join(" ")}`);
}
snowsignal marked this conversation as resolved.
Show resolved Hide resolved

const clientOptions = {
// Register the server for python documents
Expand Down