Files
modulus/modulus/clangd.py
T
khodowany fb93acca1a
publish / publish (push) Successful in 11m4s
Cleanup
2026-10-08 01:31:14 -05:00

177 lines
7.3 KiB
Python

import fcntl
import hashlib
import json
import os
import pathlib
import shutil
import signal
import subprocess
import sys
import tarfile
import tempfile
import uuid
import modulus.config
import modulus.container
cache_version = 1
def settings(context):
return {**modulus.config.clangd_defaults, **context.lsp_settings.get("clangd", {})}
def cache_directory(context, image):
cache_home = pathlib.Path(os.environ.get("XDG_CACHE_HOME", pathlib.Path.home() / ".cache")).resolve()
config = settings(context)
key = {name: config[name] for name in ("executable", "query_drivers", "header_roots")}
key["version"] = cache_version
digest = hashlib.sha256(json.dumps(key, sort_keys=True).encode()).hexdigest()[:20]
return cache_home / "modulus" / "clangd" / image.removeprefix("sha256:") / digest
def read_manifest(cache):
data = json.loads((cache / "manifest.json").read_text(encoding="utf-8"))
roots = data["roots"]
if data["version"] != cache_version or not isinstance(roots, list) or not roots:
raise ValueError("invalid header cache manifest")
for root in roots:
path = pathlib.PurePosixPath(root)
if not path.is_absolute() or root == "/" or ".." in path.parts:
raise ValueError("invalid header root")
if not (cache / "rootfs" / root.lstrip("/")).is_dir():
raise ValueError("incomplete header cache")
return roots
def extract_headers(stream, destination, roots):
# Only dereferenced regular files/directories below the discovered header roots.
allowed = [pathlib.PurePosixPath(root.lstrip("/")) for root in roots]
with tarfile.open(fileobj=stream, mode="r|") as archive:
for member in archive:
path = pathlib.PurePosixPath(member.name)
if path.is_absolute() or ".." in path.parts or not any(path.is_relative_to(root) for root in allowed):
raise ValueError(f"unsafe header archive path: {member.name}")
target = destination / path
if member.isdir():
target.mkdir(parents=True, exist_ok=True)
elif member.isfile():
target.parent.mkdir(parents=True, exist_ok=True)
with archive.extractfile(member) as source, target.open("wb") as output:
shutil.copyfileobj(source, output)
target.chmod(0o444)
else:
raise ValueError(f"non-regular header archive entry: {member.name}")
def prepare(context):
try:
image = modulus.container.current_image(context, modulus.container.docker_platform_arch())
if image is None:
raise RuntimeError("build image is missing or stale; run configure first")
cache = cache_directory(context, image)
cache.parent.mkdir(parents=True, exist_ok=True)
with (cache.parent / ".lock").open("a") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
if cache.exists():
read_manifest(cache)
return 0
probe = context.tools_dir / "clangd_headers.py"
result = subprocess.run(
[*modulus.container.isolated_run(image), "--volume", f"{probe}:/modulus_headers.py:ro", image,
"python3", "/modulus_headers.py", json.dumps(settings(context))],
stdout=subprocess.PIPE, text=True, check=True,
)
roots = json.loads(result.stdout)
with tempfile.TemporaryDirectory(prefix=".headers_", dir=cache.parent) as temporary:
stage = pathlib.Path(temporary) / "cache"
stage.mkdir()
command = [*modulus.container.isolated_run(image), image, "tar", "--dereference", "--hard-dereference",
"--exclude=*/.git", "--exclude=*/__pycache__",
"-C", "/", "-cf", "-", *[root.lstrip("/") for root in roots]]
with subprocess.Popen(command, stdout=subprocess.PIPE) as process:
try:
extract_headers(process.stdout, stage / "rootfs", roots)
finally:
process.stdout.close()
if process.wait() != 0:
raise RuntimeError("could not copy container headers")
(stage / "manifest.json").write_text(json.dumps({"version": cache_version, "roots": roots}) + "\n")
read_manifest(stage)
stage.rename(cache)
print(f"Prepared clangd headers: {cache}", file=sys.stderr)
return 0
except (OSError, ValueError, KeyError, RuntimeError, subprocess.SubprocessError, tarfile.TarError) as error:
print(f"Modulus clangd preparation: {error}", file=sys.stderr)
return 1
def path_mappings(cache, roots):
mappings = []
for root in sorted(roots, key=len, reverse=True):
client = str(cache / "rootfs" / root.lstrip("/"))
if any(c in client + root for c in ",=\n\r"):
raise ValueError("header paths cannot contain clangd mapping delimiters")
mappings.append(f"{client}={root}")
return "--path-mappings=" + ",".join(mappings)
def server_arguments(context, argv):
config = settings(context)
return [config["executable"], *config["arguments"], *argv,
"--query-driver=" + ",".join(config["query_drivers"])]
def launch_command(context, argv, image, cache, roots, name):
root = context.repo_root
return [
*modulus.container.isolated_run(image), "--interactive", "--name", name,
"--volume", f"{root}:{root}:ro",
"--volume", f"{context.build_root}:{context.build_root}:rw",
"--env", f"XDG_CACHE_HOME={context.common_dir / 'clangd'}",
"--workdir", str(root), image, *server_arguments(context, argv), path_mappings(cache, roots),
]
def launch(context, argv):
try:
if context.docker_marker.exists():
os.execv(settings(context)["executable"], server_arguments(context, argv))
image = modulus.container.current_image(context, modulus.container.docker_platform_arch())
if image is None:
raise RuntimeError("missing or stale build image; run ./modulus.py configure (Modulus: ./tools/configure.py)")
databases = [context.default_target.build_dir, *[
context.repo_root / database["build_dir"] for database in context.lsp_settings.get("compile_databases", [])
]]
for database in databases:
if not (database / "compile_commands.json").is_file():
raise RuntimeError(f"missing {database / 'compile_commands.json'}; run the lsp command")
cache = cache_directory(context, image)
try:
roots = read_manifest(cache)
except (OSError, ValueError, KeyError) as error:
raise RuntimeError("header cache is missing or invalid; run the lsp command") from error
name = "modulus_clangd_" + uuid.uuid4().hex
command = launch_command(context, argv, image, cache, roots, name)
return run_server(command, name)
except (OSError, ValueError, RuntimeError) as error:
print(f"Modulus clangd: {error}", file=sys.stderr)
return 1
def run_server(command, name):
# Inherit stdio verbatim; never send build logs or a terminal over the LSP pipe.
def interrupted(signum, frame):
raise SystemExit(128 + signum)
previous = {sig: signal.signal(sig, interrupted) for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP)}
process = None
try:
process = subprocess.Popen(command)
return process.wait()
finally:
for sig, handler in previous.items():
signal.signal(sig, handler)
if process is not None:
try:
subprocess.run(["docker", "rm", "--force", name], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, check=False, timeout=15)
except (OSError, subprocess.TimeoutExpired) as error:
print(f"Modulus clangd cleanup: {error}", file=sys.stderr)
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait()