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
90 changes: 90 additions & 0 deletions bundled/tool/ruff_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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"))


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

# This is subject to change in the future
RUFF_VERSION_REQUIREMENT = SpecifierSet(">=0.3.5")


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,
) -> 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)


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

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"

path = Path(sysconfig.get_path("scripts", scheme=user_scheme)) / RUFF_EXE
if path.is_file():
return path

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)
completed_process = subprocess.run([ruff, *sys.argv[1:]], check=False)
sys.exit(completed_process.returncode)
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 EXPERIMENTAL_SERVER_SCRIPT_PATH = path.join(
BUNDLED_PYTHON_SCRIPTS_DIR,
"tool",
"ruff_server.py",
);
export const RUFF_SERVER_CMD = "server";
export const RUFF_SERVER_REQUIRED_ARGS = ["--preview"];
41 changes: 29 additions & 12 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,
EXPERIMENTAL_SERVER_SCRIPT_PATH,
} from "./constants";
import { traceError, traceInfo, traceVerbose } from "./log/logging";
import { getDebuggerPath } from "./python";
Expand All @@ -39,17 +39,34 @@ 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];

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

traceInfo(`Server run command: ${[command, ...args].join(" ")}`);
let serverOptions: ServerOptions;
// If the user provided a binary path, we'll try to call that path directly.
if (settings.path[0]) {
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 {
// Otherwise, we'll call a Python script that tries to locate
// a binary, falling back to the bundled version if no local executable is found.
const command = settings.interpreter[0];
const cwd = settings.cwd;
const args = [EXPERIMENTAL_SERVER_SCRIPT_PATH, RUFF_SERVER_CMD, ...RUFF_SERVER_REQUIRED_ARGS];

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

traceInfo(`Server run command: ${[command, ...args].join(" ")}`);
}

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