From 783e13a5b725a04cf668141d505110f151284ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Thu, 10 Oct 2024 16:52:20 +0200 Subject: [PATCH 1/8] Add experimental container mode in the CLI --- rpm_spec_language_server/main.py | 270 ++++++++++++++++++++++++++++- rpm_spec_language_server/server.py | 28 ++- 2 files changed, 291 insertions(+), 7 deletions(-) diff --git a/rpm_spec_language_server/main.py b/rpm_spec_language_server/main.py index f6944a7..316dd6e 100644 --- a/rpm_spec_language_server/main.py +++ b/rpm_spec_language_server/main.py @@ -1,4 +1,96 @@ +"""This is the main entrypoint into the RPM spec language server, it provides +its CLI interface. + +The default running mode is not in container mode, which is pretty much what +you'd expect: we collect the CLI arguments and launch the language server in the +requested mode (TCP or stdio). + +The other running mode is container-mode. This is a hack/workaround for rpm +being pretty much tied to the OS you're running, which means that you cannot +really expand macros for a different distribution. To work around this, we +package the language server into container images of various popular RPM based +distributions and launch it in TCP mode. + +Container mode requires various workarounds: + +1. The container runtime (namely rootless podman) will open ports for + communication way before the container is up and running. This causes a + rather ugly problem: the editor sends an initialization request once the port + is open and expects a reply. However, in container mode, the port is already + open, but the language server in the container is not yet ready and the + initial request is lost, causing the editor to wait indefinitely. + + We work around this by launching the container, waiting for it to be up and + running and afterwards, we setup a port forward from the requested port into + the container. + +2. Jump to macro definitions cannot work in a straightforward way in the + container: the macros are defined in files in the container, which are + probably not existing on the host or are different. + + Unfortunately we cannot mount a container directory on the host + (easily). Therefore we have another hack: the language server will copy the + contents of :file:`/usr/lib/rpm` into :py:const:`_MACROS_COPY_DIR` if it + exists. :py:const:`_MACROS_COPY_DIR` is a bind-mount to a temporary directory + on the host and the language server in the container will then remap the + found macro file with the temporary directory on the host. This should allow + the editor to still find the macro definitions. + +3. Currently you can only have one project open, as the language server bind + mounts cwd into :file:`/src/` in the container. The language server expects + all files to be there. + + We might be able to work around this in the future by relying exclusively on + the in-memory representation or have a side-channel between container and the + process running on the host to read arbitrary files. + +""" + +import asyncio import logging +import socket +import sys +from os import getpid +from typing import NoReturn, Tuple + +_DEFAULT_PORT = 2087 +_MACROS_COPY_DIR = "/rpmmacros/" + + +async def _forward_data( + loop: asyncio.AbstractEventLoop, + receiving_socket: socket.socket, + sending_socket: socket.socket, +) -> NoReturn: + """Reads data from the ``receiving_socket`` and sends them to + ``sending_socket`` in an endless loop. + + """ + while True: + data, _ = await loop.sock_recvfrom(receiving_socket, 1024) + await loop.sock_sendall(sending_socket, data) + + +async def _create_forwarder(lsp_port: int, ctr_addr: str, ctr_port: int) -> NoReturn: + loop = asyncio.get_event_loop() + + lsp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + lsp_sock.bind(("", lsp_port)) + lsp_sock.listen(8) + lsp_sock.setblocking(False) + + while True: + client_sock, _ = await loop.sock_accept(lsp_sock) + + ctr_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ctr_sock.setblocking(False) + + await loop.sock_connect(ctr_sock, (ctr_addr, ctr_port)) + + await asyncio.gather( + _forward_data(loop, client_sock, ctr_sock), + _forward_data(loop, ctr_sock, client_sock), + ) def main() -> None: @@ -18,27 +110,174 @@ def main() -> None: parser.add_argument( "--host", type=str, default="127.0.0.1", help="Bind to this address" ) - parser.add_argument("--port", type=int, default=2087, help="Bind to this port") + parser.add_argument( + "--port", type=int, default=_DEFAULT_PORT, help="Bind to this port" + ) parser.add_argument( "--runtime-type-checks", action="store_true", help="Add typeguard runtime type checking", ) + + parser.add_argument( + "--container-mode", + action="store_true", + help="Run the language server in a container", + ) + parser.add_argument( + "--distribution", + type=str, + nargs=1, + choices=["fedora", "centos", "tumbleweed", "leap-15.6", "leap-15.5"], + help="The distribution to use for container-mode", + ) + parser.add_argument( + "--container-runtime", + type=str, + nargs=1, + choices=["docker", "podman"], + help="The container runtime to use in container-mode", + ) + parser.add_argument( + "--container-image", + type=str, + nargs=1, + help=( + "The container image to use to run the language server in (the " + " server MUST be pre-installed)" + ), + default=["ghcr.io/dcermak/rpm-spec-language-server"], + ) parser.add_argument( "--ctr-mount-path", type=str, nargs=1, - help="Directory that is mounted ", + help=( + "Directory in on the container where the directory with the spec is " + "mounted into the container (internal flag)" + ), default=[""], ) + parser.add_argument( + "--ctr-macros-mount-path", + type=str, + nargs=1, + help=( + "Path where the rpm macros directory from the container is mounted " + " on the host (internal flag)" + ), + default=[None], + ) args = parser.parse_args() + if args.container_mode: + import subprocess + import tempfile + from time import sleep + + # Communicating via stdio does not work in container mode, as we face + # the same caching issue as with TCP mode. I.e. we'd require a second + # forwarding implementation akin to the TCP port + # forwarder. Additionally, --stdio has weird glitches in container mode, + # where it needs to be ctrl-c'd twice to die. This causes problems for + # editors to properly terminate the language server. + if args.stdio: + raise ValueError("Container mode does not support stdio") + + # typeguard is not installed in the container images + if args.runtime_type_checks: + raise ValueError("Container mode does not support runtime-type-checks") + + # we need the logs for our poor man's healthcheck and we don't really + # want to have _another_ mount to pull the logs out of the container + if args.log_file: + raise ValueError( + "Log files not supported in container mode, " + "use $ctr_runtime logs $ctr_id" + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + _ctr_mount_path = "/src/" + + launch_args: Tuple[str, ...] = ( + (ctr_runtime := args.container_runtime[0]), + "run", + "--rm", + "-d", + # mount the cwd + "-v", + f".:{_ctr_mount_path}:z", + # mount the directory where the container copies the contents of + # /usr/lib/rpm/ + "-v", + f"{tmp_dir}:{_MACROS_COPY_DIR}:z", + # expose the TCP port + "-p", + (private_port := f"{_DEFAULT_PORT}/tcp"), + # container image + f"{args.container_image[0]}:{args.distribution[0]}", + # pass the macros path as + f"--ctr-macros-mount-path={tmp_dir}", + ) + + if args.verbose: + # forward verbosity arguments, but require at least INFO logging + # level so that our poor man's healthcheck works + launch_args += ("-" + ("v" * max(args.verbose, 1)),) + + launch_res = subprocess.check_output(launch_args) + ctr_id = launch_res.decode("utf-8").strip().splitlines()[-1] + + # run $ctr port $id 2087/tcp + # returns: + # 0.0.0.0:32768 + # [::]:32768 + # + # take first entry => split by rightmost : + addr, _, port = ( + subprocess.check_output((ctr_runtime, "port", ctr_id, private_port)) + .decode() + .strip() + .splitlines()[0] + .rpartition(":") + ) + + # poor man's healthcheck: wait for the container to be up and running + while True: + # read the logs from the container + log_output = ( + subprocess.check_output( + (ctr_runtime, "logs", ctr_id), stderr=subprocess.STDOUT + ) + .decode() + .strip() + ) + + # the server is up and running if the last line is: + # INFO:start_tcp:Starting TCP server on 127.0.0.1:2087 + if ( + log_lines := log_output.splitlines() + ) and "INFO:start_tcp:Starting TCP server on" in log_lines[-1]: + break + + sleep(1) + + try: + loop = asyncio.get_event_loop() + loop.run_until_complete(_create_forwarder(args.port, addr, int(port))) + finally: + subprocess.run((ctr_runtime, "rm", "-f", ctr_id)) + + return + if args.runtime_type_checks: from typeguard import install_import_hook install_import_hook("rpm_spec_language_server") + import os.path + from rpm_spec_language_server.logging import LOG_LEVELS, LOGGER from rpm_spec_language_server.server import create_rpm_lang_server @@ -50,7 +289,32 @@ def main() -> None: LOGGER.setLevel(log_level) - server = create_rpm_lang_server(args.ctr_mount_path[0]) + if args.ctr_macros_mount_path[0] and os.path.exists(_MACROS_COPY_DIR): + import shutil + + shutil.copytree( + "/usr/lib/rpm/", + os.path.join(_MACROS_COPY_DIR, "usr/lib/rpm"), + # need to ignore dangling symlinks as some scripts are symlinked + # into /usr/lib/rpm/ from directories outside of that + ignore_dangling_symlinks=True, + ) + + server = create_rpm_lang_server( + args.ctr_mount_path[0], args.ctr_macros_mount_path[0] + ) + + # if we're running in the container, then we need to add a signal handler + # for SIGTERM, otherwise the container will not terminate cleanly on SIGTERM + # and wait until SIGKILL + # see: https://stackoverflow.com/a/62871549 + if getpid() == 1: + from signal import SIGTERM, signal + + def terminate(signal, frame): + sys.exit(0) + + signal(SIGTERM, terminate) if args.stdio: server.start_io() diff --git a/rpm_spec_language_server/server.py b/rpm_spec_language_server/server.py index d2e336a..cd9feea 100644 --- a/rpm_spec_language_server/server.py +++ b/rpm_spec_language_server/server.py @@ -94,7 +94,11 @@ class RpmSpecLanguageServer(LanguageServer): "%elif", ] - def __init__(self, container_mount_path: Optional[str] = None) -> None: + def __init__( + self, + container_mount_path: Optional[str] = None, + container_macro_mount_path: Optional[str] = None, + ) -> None: super().__init__( name := "rpm_spec_language_server", metadata.version(name), @@ -106,6 +110,7 @@ def __init__(self, container_mount_path: Optional[str] = None) -> None: retrieve_spec_md() or "" ) self._container_path: str = container_mount_path or "" + self._container_macro_mount_path: str = container_macro_mount_path or "" @property def is_vscode_connected(self) -> bool: @@ -174,6 +179,18 @@ def _spec_path_from_uri(self, uri: str) -> Optional[str]: return path + def _macro_uri(self, macro_file_location: str) -> str: + if self._container_macro_mount_path: + return "file://" + os.path.join( + self._container_macro_mount_path, + # remove leading slashes from the location as os.path.join will + # otherwise return *only* macro_file_location and omit + # self._container_macro_mount_path + macro_file_location.lstrip("/"), + ) + else: + return f"file://{macro_file_location}" + def spec_from_text_document( self, text_document: Union[TextDocumentIdentifier, TextDocumentItem], @@ -267,8 +284,11 @@ def get_macro_under_cursor( def create_rpm_lang_server( container_mount_path: Optional[str] = None, + container_macro_mount_path: Optional[str] = None, ) -> RpmSpecLanguageServer: - rpm_spec_server = RpmSpecLanguageServer(container_mount_path) + rpm_spec_server = RpmSpecLanguageServer( + container_mount_path, container_macro_mount_path + ) def did_open_or_save( server: RpmSpecLanguageServer, @@ -501,7 +521,7 @@ def find_preamble_definition_in_spec( if define_matches := find_macro_in_macro_file( macro_file_f.read(-1) ): - file_uri = f"file://{f.name}" + file_uri = server._macro_uri(f.name) break # we didn't find a match @@ -513,7 +533,7 @@ def find_preamble_definition_in_spec( if define_matches := find_macro_in_macro_file( macro_file_f.read(-1) ): - file_uri = f"file://{fname}" + file_uri = server._macro_uri(fname) if define_matches and file_uri: return [ From 3f3e1966a211c5b91a52509617ae0e12372cf3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Fri, 11 Oct 2024 18:00:08 +0200 Subject: [PATCH 2/8] Improve logging --- rpm_spec_language_server/server.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rpm_spec_language_server/server.py b/rpm_spec_language_server/server.py index cd9feea..7643595 100644 --- a/rpm_spec_language_server/server.py +++ b/rpm_spec_language_server/server.py @@ -294,7 +294,6 @@ def did_open_or_save( server: RpmSpecLanguageServer, param: Union[DidOpenTextDocumentParams, DidSaveTextDocumentParams], ) -> None: - LOGGER.debug("open or save event") if not (spec := server.spec_from_text_document(param.text_document)): return None @@ -424,6 +423,7 @@ def find_macro_definition( param.text_document ) ): + LOGGER.debug("spec sections of %s are unavailable", param.text_document.uri) return None macro_under_cursor = server.get_macro_under_cursor( @@ -431,6 +431,7 @@ def find_macro_definition( ) if not macro_under_cursor: + LOGGER.debug("did not find macro under cursor") return None macro_name = ( @@ -444,6 +445,8 @@ def find_macro_definition( else macro_under_cursor.level ) + LOGGER.debug("Got macro %s, level: %s", macro_name, macro_level) + def find_macro_define_in_spec(file_contents: str) -> list[re.Match[str]]: """Searches for the definition of the macro ``macro_under_cursor`` as it would appear in a spec file, i.e.: ``%global macro`` or @@ -490,6 +493,8 @@ def find_preamble_definition_in_spec( # macro is something like %version, %release, etc. elif macro_level == MacroLevel.SPEC: + LOGGER.debug("looking for macro %s in the spec", macro_name) + # try the preamble values if not ( define_matches := find_preamble_definition_in_spec( str(spec_sections.spec) @@ -510,14 +515,18 @@ def find_preamble_definition_in_spec( # builtin macros file of rpm (_should_ be in %_rpmconfigdir/macros) so # we retry the search in that file. elif macro_level == MacroLevel.MACROFILES: + LOGGER.debug("looking for %s in macro files", macro_name) MACROS_DIR = rpm.expandMacro("%_rpmmacrodir") + LOGGER.debug("%%_rpmmacrodir: %s", MACROS_DIR) ts = rpm.TransactionSet() # search in packages for pkg in ts.dbMatch("provides", f"rpm_macro({macro_name})"): + LOGGER.debug("Package %s provides rpm_macro(%s)", pkg.name, macro_name) for f in rpm.files(pkg): if f.name.startswith(MACROS_DIR): with open(f.name) as macro_file_f: + LOGGER.debug("Looking for macro in %s", f.name) if define_matches := find_macro_in_macro_file( macro_file_f.read(-1) ): From a5718acaf49fe8dc07930adf60c86eecedbd02ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Fri, 11 Oct 2024 18:00:20 +0200 Subject: [PATCH 3/8] Fix the regex for macro files The general syntax for macro defines in macro files is: %$macro_name something but you can also have a () after the macro name like in: %py3_build() \ which would not get matched by the original regex as it expected the macro name to be followed by whitespace. We therefore need to extend it and also match open braces --- rpm_spec_language_server/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpm_spec_language_server/server.py b/rpm_spec_language_server/server.py index 7643595..1508350 100644 --- a/rpm_spec_language_server/server.py +++ b/rpm_spec_language_server/server.py @@ -465,7 +465,7 @@ def find_macro_in_macro_file(file_contents: str) -> list[re.Match[str]]: """ regex = re.compile( - rf"^([\t \f]*)(%{macro_name})([\t \f]+)(\S+)", re.MULTILINE + rf"^([\t \f]*)(%{macro_name})([\t \f]+|\()(\S+)", re.MULTILINE ) return list(regex.finditer(file_contents)) From 206bfcfb9d799656ecde896e849a0862e3979b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Fri, 11 Oct 2024 18:04:57 +0200 Subject: [PATCH 4/8] Also look for macros with SPEC level as if they were %defined We were so far only assuming that SPEC level macros are from the preamble, but these can be also %define or %global macros from the current spec. Hence we look for them via the macro define find function as well, in case we find nothing in the preamble --- rpm_spec_language_server/server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rpm_spec_language_server/server.py b/rpm_spec_language_server/server.py index 1508350..cf878de 100644 --- a/rpm_spec_language_server/server.py +++ b/rpm_spec_language_server/server.py @@ -500,7 +500,11 @@ def find_preamble_definition_in_spec( str(spec_sections.spec) ) ): - return None + # or maybe it's defined via %global or %define? + if not ( + define_matches := find_macro_define_in_spec(str(spec_sections.spec)) + ): + return None file_uri = param.text_document.uri # the macro comes from a macro file From 11fd55899aacfff7a69b2101df6f6c8f07fb2f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Fri, 11 Oct 2024 23:16:32 +0200 Subject: [PATCH 5/8] Move regex searches for macro definitions to util module and add tests --- rpm_spec_language_server/server.py | 55 +++++++--------------------- rpm_spec_language_server/util.py | 54 ++++++++++++++++++++++++++- tests/test_util.py | 59 +++++++++++++++++++++++++++++- 3 files changed, 124 insertions(+), 44 deletions(-) diff --git a/rpm_spec_language_server/server.py b/rpm_spec_language_server/server.py index cf878de..2106086 100644 --- a/rpm_spec_language_server/server.py +++ b/rpm_spec_language_server/server.py @@ -1,5 +1,4 @@ import os.path -import re from importlib import metadata from typing import Optional, Union, cast, overload from urllib.parse import unquote, urlparse @@ -57,6 +56,9 @@ get_macro_string_at_position, ) from rpm_spec_language_server.util import ( + find_macro_define_in_spec, + find_macro_matches_in_macro_file, + find_preamble_definition_in_spec, position_from_match, spec_from_text, ) @@ -447,45 +449,14 @@ def find_macro_definition( LOGGER.debug("Got macro %s, level: %s", macro_name, macro_level) - def find_macro_define_in_spec(file_contents: str) -> list[re.Match[str]]: - """Searches for the definition of the macro ``macro_under_cursor`` - as it would appear in a spec file, i.e.: ``%global macro`` or - ``%define macro``. - - """ - regex = re.compile( - rf"^([\t \f]*)(%(?:global|define))([\t \f]+)({macro_name})", - re.MULTILINE, - ) - return list(regex.finditer(file_contents)) - - def find_macro_in_macro_file(file_contents: str) -> list[re.Match[str]]: - """Searches for the definition of the macro ``macro_under_cursor`` - as it would appear in a rpm macros file, i.e.: ``%macro …``. - - """ - regex = re.compile( - rf"^([\t \f]*)(%{macro_name})([\t \f]+|\()(\S+)", re.MULTILINE - ) - return list(regex.finditer(file_contents)) - - def find_preamble_definition_in_spec( - file_contents: str, - ) -> list[re.Match[str]]: - regex = re.compile( - rf"^([\t \f]*)({macro_name}):([\t \f]+)(\S*)", - re.MULTILINE | re.IGNORECASE, - ) - if (m := regex.search(file_contents)) is None: - return [] - return [m] - define_matches, file_uri = [], None # macro is defined in the spec file if macro_level == MacroLevel.GLOBAL: if not ( - define_matches := find_macro_define_in_spec(str(spec_sections.spec)) + define_matches := find_macro_define_in_spec( + macro_name, str(spec_sections.spec) + ) ): return None @@ -497,12 +468,14 @@ def find_preamble_definition_in_spec( # try the preamble values if not ( define_matches := find_preamble_definition_in_spec( - str(spec_sections.spec) + macro_name, str(spec_sections.spec) ) ): # or maybe it's defined via %global or %define? if not ( - define_matches := find_macro_define_in_spec(str(spec_sections.spec)) + define_matches := find_macro_define_in_spec( + macro_name, str(spec_sections.spec) + ) ): return None file_uri = param.text_document.uri @@ -531,8 +504,8 @@ def find_preamble_definition_in_spec( if f.name.startswith(MACROS_DIR): with open(f.name) as macro_file_f: LOGGER.debug("Looking for macro in %s", f.name) - if define_matches := find_macro_in_macro_file( - macro_file_f.read(-1) + if define_matches := find_macro_matches_in_macro_file( + macro_name, macro_file_f.read(-1) ): file_uri = server._macro_uri(f.name) break @@ -543,8 +516,8 @@ def find_preamble_definition_in_spec( if not define_matches: fname = rpm.expandMacro("%_rpmconfigdir") + "/macros" with open(fname) as macro_file_f: - if define_matches := find_macro_in_macro_file( - macro_file_f.read(-1) + if define_matches := find_macro_matches_in_macro_file( + macro_name, macro_file_f.read(-1) ): file_uri = server._macro_uri(fname) diff --git a/rpm_spec_language_server/util.py b/rpm_spec_language_server/util.py index 50af328..8da198d 100644 --- a/rpm_spec_language_server/util.py +++ b/rpm_spec_language_server/util.py @@ -1,7 +1,7 @@ from functools import reduce -from re import Match +from re import IGNORECASE, MULTILINE, Match, finditer, search from tempfile import TemporaryDirectory -from typing import Optional +from typing import List, Optional from lsprotocol.types import Position from specfile.exceptions import RPMException @@ -51,3 +51,53 @@ def spec_from_text( except RPMException as rpm_exc: LOGGER.debug("Failed to parse spec, got %s", rpm_exc) return None + + +def find_macro_matches_in_macro_file( + macro_name: str, macro_file_contents: str +) -> List[Match[str]]: + """Searches for definitions of ``macro_name`` in a rpm macro file with the + contents ``macro_file_contents``. + + A macro can be defined in a macro file like: ``%macro `` or ``%macro($args)``. + + """ + return list( + finditer( + rf"^([\t \f]*)(%{macro_name})([\t \f]+|\()(\S+)", + macro_file_contents, + flags=MULTILINE, + ) + ) + + +def find_preamble_definition_in_spec( + macro_name: str, spec_file_contents: str +) -> list[Match[str]]: + """Find the definition of a "macro" like ``%version`` in the preamble of a + spec file with the supplied contents. If any matches are found, then they + are returned as a list of matches. + + """ + m = search( + rf"^([\t \f]*)({macro_name}):([\t \f]+)(\S*)", + spec_file_contents, + flags=MULTILINE | IGNORECASE, + ) + return [] if m is None else [m] + + +def find_macro_define_in_spec( + macro_name: str, spec_file_contents: str +) -> list[Match[str]]: + """Searches for the definition of the macro ``macro_name`` as it would + appear in a spec file, i.e.: ``%global macro`` or ``%define macro``. + + """ + return list( + finditer( + rf"^([\t \f]*)(%(?:global|define))([\t \f]+)({macro_name})", + spec_file_contents, + flags=MULTILINE, + ) + ) diff --git a/tests/test_util.py b/tests/test_util.py index 8396ab5..729bba3 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,11 +1,17 @@ +# ruff: noqa: E501 +# don't care about long line warnings in copied together strings import re from pathlib import Path +from typing import List from urllib.parse import quote import pytest from lsprotocol.types import Position, TextDocumentIdentifier from rpm_spec_language_server.server import create_rpm_lang_server -from rpm_spec_language_server.util import position_from_match +from rpm_spec_language_server.util import ( + find_macro_matches_in_macro_file, + position_from_match, +) from tests.data import NOTMUCH_SPEC @@ -66,3 +72,54 @@ def test_spec_from_text_with_special_path(tmp_path: Path) -> None: assert spec assert spec.name == "notmuch" + + +# copied together out of macros.python & macros.cmake +_FAKE_MACROS_FILE = r"""# Use the slashes after expand so that the command starts on the same line as +# the macro +%py3_build() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python3} %{py_setup} %{?py_setup_args} build --executable="%{__python3} %{py3_shbang_opts}" %{?*} +} + +%py3_build_wheel() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python3} %{py_setup} %{?py_setup_args} bdist_wheel %{?*} +} + +%cmake \ + %{set_build_flags} \ + %__cmake \\\ + %{!?__cmake_in_source_build:-S "%{_vpath_srcdir}"} \\\ + %{!?__cmake_in_source_build:-B "%{__cmake_builddir}"} \\\ + -DCMAKE_C_FLAGS_RELEASE:STRING="-DNDEBUG" \\\ + -DCMAKE_CXX_FLAGS_RELEASE:STRING="-DNDEBUG" \\\ + -DCMAKE_Fortran_FLAGS_RELEASE:STRING="-DNDEBUG" \\\ + -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON \\\ + -DCMAKE_INSTALL_DO_STRIP:BOOL=OFF \\\ + -DCMAKE_INSTALL_PREFIX:PATH=%{_prefix} \\\ + -DINCLUDE_INSTALL_DIR:PATH=%{_includedir} \\\ + -DLIB_INSTALL_DIR:PATH=%{_libdir} \\\ + -DSYSCONF_INSTALL_DIR:PATH=%{_sysconfdir} \\\ + -DSHARE_INSTALL_PREFIX:PATH=%{_datadir} \\\ +%if "%{?_lib}" == "lib64" \ + %{?_cmake_lib_suffix64} \\\ +%endif \ + %{?_cmake_shared_libs} + +%cmake_build \ + %__cmake --build "%{__cmake_builddir}" %{?_smp_mflags} --verbose + +""" + + +@pytest.mark.parametrize( + "macro_name, starts", + [("py3_build", [90]), ("cmake", [481]), ("cmake_build", [1280])], +) +def test_macro_matches_in_macro_file(macro_name: str, starts: List[int]) -> None: + matches = find_macro_matches_in_macro_file(macro_name, _FAKE_MACROS_FILE) + + assert len(matches) == len(starts) + for i, pos in enumerate(starts): + assert matches[i].start() == pos From 7f5f0182ac9525a3596e8d70289bc30238d58a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Sat, 12 Oct 2024 15:40:43 +0200 Subject: [PATCH 6/8] Fix CLIENT_SERVER_T type --- tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 95d368d..bb28bec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ import asyncio import os import threading -from typing import Generator +from typing import Generator, Tuple import pytest from _pytest.fixtures import SubRequest @@ -95,11 +95,11 @@ def __iter__(self) -> Generator[LanguageServer, None, None]: yield self.server -CLIENT_SERVER_T = Generator[tuple[LanguageServer, RpmSpecLanguageServer], None, None] +CLIENT_SERVER_T = Tuple[LanguageServer, RpmSpecLanguageServer] @pytest.fixture -def client_server(request: SubRequest) -> CLIENT_SERVER_T: +def client_server(request: SubRequest) -> Generator[CLIENT_SERVER_T, None, None]: if (param := getattr(request, "param", None)) and isinstance(param, str): cs = ClientServer(client_name=param) else: From f53763db40b98572f062b1c2066b4107e582b0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Sat, 12 Oct 2024 17:14:42 +0200 Subject: [PATCH 7/8] Add urls to container images to README --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 7796db5..6711740 100644 --- a/README.rst +++ b/README.rst @@ -105,11 +105,11 @@ In the example above, replace ``$distri`` with the desired distribution. Supported distributions/tags ---------------------------- -- ``fedora``: based on ``fedora:latest`` -- ``tumbleweed``: based on ``tumbleweed:latest`` -- ``centos``: based on ``centos:stream9`` -- ``leap-15.5``: based on ``leap:15.5`` -- ``leap-15.6``: based on ``leap:15.6`` +- ``fedora``: based on ``registry.fedoraproject.org/fedora:latest`` +- ``tumbleweed``: based on ``registry.opensuse.org/opensuse/tumbleweed:latest`` +- ``centos``: based on ``quay.io/centos/centos:stream9`` +- ``leap-15.5``: based on ``registry.opensuse.org/opensuse/leap:15.5`` +- ``leap-15.6``: based on ``registry.opensuse.org/opensuse/leap:15.6`` Clients From ac330cd56270dd83576e2f9b87b7411847ad1ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Sat, 12 Oct 2024 17:16:18 +0200 Subject: [PATCH 8/8] Correct documentation for container mode --- README.rst | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index 6711740..13bc949 100644 --- a/README.rst +++ b/README.rst @@ -64,7 +64,7 @@ The server requires the `spec.md file. It can either use the locally installed copy from the ``rpm`` package or (if the documentation has not been installed) from a locally cached version in ``~/.cache/rpm/spec.md``. The language server will fetch the ``spec.md`` from -the upstream github repository if neither of the previous options. +the upstream github repository if neither of the previous options works. Container Mode @@ -75,32 +75,26 @@ container mode. In this mode, the server is launched inside a container with the package directory mounted into the running container. This allows you to have access to a different distribution than your current one. -The container mode can currently handle only one package open. The RPM spec file -**must** be in the top-level directory. Additionally, the server **must** -communicate via TCP. This means that you might have to reconfigure your +The container mode can currently handle only having one package open. The RPM +spec file **must** be in the top-level directory. Additionally, the server +**must** communicate via TCP. This means that you might have to reconfigure your lsp-client, if it assumes to communicate via stdio. -To enable the container mode with Podman, proceed as follows: +To run the language server in container mode, launch the language server with +the following additional flags: .. code-block:: shell-session $ cd ~/path/to/my/package $ # ensure that the spec file is in the current working directory! - $ podman container runlabel run \ - ghcr.io/dcermak/rpm-spec-lang-server:$distri + $ python -m rpm_spec_language_server -vvv \ + --distribution $distri \ + --container-mode \ + --container-runtime=$runtime \ where you replace ``$distri`` with one of ``tumbleweed``, ``leap-15.5``, -``leap-15.6``, ``fedora`` or ``centos``. - -To use Docker, get the exact launch command as shown below: - -.. code-block:: shell-session - - $ docker inspect -f '{{index .Config.Labels "run"}}' \ - ghcr.io/dcermak/rpm-spec-lang-server:$distri | \ - sed -e 's/podman/docker/' -e 's|$IMAGE|ghcr.io/dcermak/rpm-spec-lang-server:$distri|' - -In the example above, replace ``$distri`` with the desired distribution. +``leap-15.6``, ``fedora`` or ``centos`` and ``$runtime`` with either ``docker`` +or ``podman``. Supported distributions/tags ----------------------------