diff options
author | Angelos Mouzakitis <a.mouzakitis@virtualopensystems.com> | 2023-10-10 14:33:42 +0000 |
---|---|---|
committer | Angelos Mouzakitis <a.mouzakitis@virtualopensystems.com> | 2023-10-10 14:33:42 +0000 |
commit | af1a266670d040d2f4083ff309d732d648afba2a (patch) | |
tree | 2fc46203448ddcc6f81546d379abfaeb323575e9 /meson/mesonbuild/cmake/client.py | |
parent | e02cda008591317b1625707ff8e115a4841aa889 (diff) |
Change-Id: Iaf8d18082d3991dec7c0ebbea540f092188eb4ec
Diffstat (limited to 'meson/mesonbuild/cmake/client.py')
-rw-r--r-- | meson/mesonbuild/cmake/client.py | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/meson/mesonbuild/cmake/client.py b/meson/mesonbuild/cmake/client.py new file mode 100644 index 000000000..bcbb52ef9 --- /dev/null +++ b/meson/mesonbuild/cmake/client.py @@ -0,0 +1,373 @@ +# Copyright 2019 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This class contains the basic functionality needed to run any interpreter +# or an interpreter-based tool. + +from .common import CMakeException, CMakeConfiguration, CMakeBuildFile +from .. import mlog +from contextlib import contextmanager +from subprocess import Popen, PIPE, TimeoutExpired +from pathlib import Path +import typing as T +import json + +if T.TYPE_CHECKING: + from ..environment import Environment + from .executor import CMakeExecutor + +CMAKE_SERVER_BEGIN_STR = '[== "CMake Server" ==[' +CMAKE_SERVER_END_STR = ']== "CMake Server" ==]' + +CMAKE_MESSAGE_TYPES = { + 'error': ['cookie', 'errorMessage'], + 'hello': ['supportedProtocolVersions'], + 'message': ['cookie', 'message'], + 'progress': ['cookie'], + 'reply': ['cookie', 'inReplyTo'], + 'signal': ['cookie', 'name'], +} # type: T.Dict[str, T.List[str]] + +CMAKE_REPLY_TYPES = { + 'handshake': [], + 'configure': [], + 'compute': [], + 'cmakeInputs': ['buildFiles', 'cmakeRootDirectory', 'sourceDirectory'], + 'codemodel': ['configurations'] +} # type: T.Dict[str, T.List[str]] + +# Base CMake server message classes + +class MessageBase: + def __init__(self, msg_type: str, cookie: str) -> None: + self.type = msg_type + self.cookie = cookie + + def to_dict(self) -> T.Dict[str, T.Union[str, T.List[str], T.Dict[str, int]]]: + return {'type': self.type, 'cookie': self.cookie} + + def log(self) -> None: + mlog.warning('CMake server message of type', mlog.bold(type(self).__name__), 'has no log function') + +class RequestBase(MessageBase): + cookie_counter = 0 + + def __init__(self, msg_type: str) -> None: + super().__init__(msg_type, self.gen_cookie()) + + @staticmethod + def gen_cookie() -> str: + RequestBase.cookie_counter += 1 + return f'meson_{RequestBase.cookie_counter}' + +class ReplyBase(MessageBase): + def __init__(self, cookie: str, in_reply_to: str) -> None: + super().__init__('reply', cookie) + self.in_reply_to = in_reply_to + +class SignalBase(MessageBase): + def __init__(self, cookie: str, signal_name: str) -> None: + super().__init__('signal', cookie) + self.signal_name = signal_name + + def log(self) -> None: + mlog.log(mlog.bold('CMake signal:'), mlog.yellow(self.signal_name)) + +# Special Message classes + +class Error(MessageBase): + def __init__(self, cookie: str, message: str) -> None: + super().__init__('error', cookie) + self.message = message + + def log(self) -> None: + mlog.error(mlog.bold('CMake server error:'), mlog.red(self.message)) + +class Message(MessageBase): + def __init__(self, cookie: str, message: str) -> None: + super().__init__('message', cookie) + self.message = message + + def log(self) -> None: + #mlog.log(mlog.bold('CMake:'), self.message) + pass + +class Progress(MessageBase): + def __init__(self, cookie: str) -> None: + super().__init__('progress', cookie) + + def log(self) -> None: + pass + +class MessageHello(MessageBase): + def __init__(self, supported_protocol_versions: T.List[T.Dict[str, int]]) -> None: + super().__init__('hello', '') + self.supported_protocol_versions = supported_protocol_versions + + def supports(self, major: int, minor: T.Optional[int] = None) -> bool: + for i in self.supported_protocol_versions: + assert 'major' in i + assert 'minor' in i + if major == i['major']: + if minor is None or minor == i['minor']: + return True + return False + +# Request classes + +class RequestHandShake(RequestBase): + def __init__(self, src_dir: Path, build_dir: Path, generator: str, vers_major: int, vers_minor: T.Optional[int] = None) -> None: + super().__init__('handshake') + self.src_dir = src_dir + self.build_dir = build_dir + self.generator = generator + self.vers_major = vers_major + self.vers_minor = vers_minor + + def to_dict(self) -> T.Dict[str, T.Union[str, T.List[str], T.Dict[str, int]]]: + vers = {'major': self.vers_major} + if self.vers_minor is not None: + vers['minor'] = self.vers_minor + + # Old CMake versions (3.7) want '/' even on Windows + self.src_dir = self.src_dir.resolve() + self.build_dir = self.build_dir.resolve() + + return { + **super().to_dict(), + 'sourceDirectory': self.src_dir.as_posix(), + 'buildDirectory': self.build_dir.as_posix(), + 'generator': self.generator, + 'protocolVersion': vers + } + +class RequestConfigure(RequestBase): + def __init__(self, args: T.Optional[T.List[str]] = None): + super().__init__('configure') + self.args = args + + def to_dict(self) -> T.Dict[str, T.Union[str, T.List[str], T.Dict[str, int]]]: + res = super().to_dict() + if self.args: + res['cacheArguments'] = self.args + return res + +class RequestCompute(RequestBase): + def __init__(self) -> None: + super().__init__('compute') + +class RequestCMakeInputs(RequestBase): + def __init__(self) -> None: + super().__init__('cmakeInputs') + +class RequestCodeModel(RequestBase): + def __init__(self) -> None: + super().__init__('codemodel') + +# Reply classes + +class ReplyHandShake(ReplyBase): + def __init__(self, cookie: str) -> None: + super().__init__(cookie, 'handshake') + +class ReplyConfigure(ReplyBase): + def __init__(self, cookie: str) -> None: + super().__init__(cookie, 'configure') + +class ReplyCompute(ReplyBase): + def __init__(self, cookie: str) -> None: + super().__init__(cookie, 'compute') + +class ReplyCMakeInputs(ReplyBase): + def __init__(self, cookie: str, cmake_root: Path, src_dir: Path, build_files: T.List[CMakeBuildFile]) -> None: + super().__init__(cookie, 'cmakeInputs') + self.cmake_root = cmake_root + self.src_dir = src_dir + self.build_files = build_files + + def log(self) -> None: + mlog.log('CMake root: ', mlog.bold(self.cmake_root.as_posix())) + mlog.log('Source dir: ', mlog.bold(self.src_dir.as_posix())) + mlog.log('Build files:', mlog.bold(str(len(self.build_files)))) + with mlog.nested(): + for i in self.build_files: + mlog.log(str(i)) + +class ReplyCodeModel(ReplyBase): + def __init__(self, data: T.Dict[str, T.Any]) -> None: + super().__init__(data['cookie'], 'codemodel') + self.configs = [] + for i in data['configurations']: + self.configs += [CMakeConfiguration(i)] + + def log(self) -> None: + mlog.log('CMake code mode:') + for idx, i in enumerate(self.configs): + mlog.log(f'Configuration {idx}:') + with mlog.nested(): + i.log() + +# Main client class + +class CMakeClient: + def __init__(self, env: 'Environment') -> None: + self.env = env + self.proc = None # type: T.Optional[Popen] + self.type_map = { + 'error': lambda data: Error(data['cookie'], data['errorMessage']), + 'hello': lambda data: MessageHello(data['supportedProtocolVersions']), + 'message': lambda data: Message(data['cookie'], data['message']), + 'progress': lambda data: Progress(data['cookie']), + 'reply': self.resolve_type_reply, + 'signal': lambda data: SignalBase(data['cookie'], data['name']) + } # type: T.Dict[str, T.Callable[[T.Dict[str, T.Any]], MessageBase]] + + self.reply_map = { + 'handshake': lambda data: ReplyHandShake(data['cookie']), + 'configure': lambda data: ReplyConfigure(data['cookie']), + 'compute': lambda data: ReplyCompute(data['cookie']), + 'cmakeInputs': self.resolve_reply_cmakeInputs, + 'codemodel': lambda data: ReplyCodeModel(data), + } # type: T.Dict[str, T.Callable[[T.Dict[str, T.Any]], ReplyBase]] + + def readMessageRaw(self) -> T.Dict[str, T.Any]: + assert self.proc is not None + rawData = [] + begin = False + while self.proc.poll() is None: + line = self.proc.stdout.readline() + if not line: + break + line = line.decode('utf-8') + line = line.strip() + + if begin and line == CMAKE_SERVER_END_STR: + break # End of the message + elif begin: + rawData += [line] + elif line == CMAKE_SERVER_BEGIN_STR: + begin = True # Begin of the message + + if rawData: + res = json.loads('\n'.join(rawData)) + assert isinstance(res, dict) + for i in res.keys(): + assert isinstance(i, str) + return res + raise CMakeException('Failed to read data from the CMake server') + + def readMessage(self) -> MessageBase: + raw_data = self.readMessageRaw() + if 'type' not in raw_data: + raise CMakeException('The "type" attribute is missing from the message') + msg_type = raw_data['type'] + func = self.type_map.get(msg_type, None) + if not func: + raise CMakeException(f'Recieved unknown message type "{msg_type}"') + for i in CMAKE_MESSAGE_TYPES[msg_type]: + if i not in raw_data: + raise CMakeException(f'Key "{i}" is missing from CMake server message type {msg_type}') + return func(raw_data) + + def writeMessage(self, msg: MessageBase) -> None: + raw_data = '\n{}\n{}\n{}\n'.format(CMAKE_SERVER_BEGIN_STR, json.dumps(msg.to_dict(), indent=2), CMAKE_SERVER_END_STR) + self.proc.stdin.write(raw_data.encode('ascii')) + self.proc.stdin.flush() + + def query(self, request: RequestBase) -> MessageBase: + self.writeMessage(request) + while True: + reply = self.readMessage() + if reply.cookie == request.cookie and reply.type in ['reply', 'error']: + return reply + + reply.log() + + def query_checked(self, request: RequestBase, message: str) -> MessageBase: + reply = self.query(request) + h = mlog.green('SUCCEEDED') if reply.type == 'reply' else mlog.red('FAILED') + mlog.log(message + ':', h) + if reply.type != 'reply': + reply.log() + raise CMakeException('CMake server query failed') + return reply + + def do_handshake(self, src_dir: Path, build_dir: Path, generator: str, vers_major: int, vers_minor: T.Optional[int] = None) -> None: + # CMake prints the hello message on startup + msg = self.readMessage() + if not isinstance(msg, MessageHello): + raise CMakeException('Recieved an unexpected message from the CMake server') + + request = RequestHandShake(src_dir, build_dir, generator, vers_major, vers_minor) + self.query_checked(request, 'CMake server handshake') + + def resolve_type_reply(self, data: T.Dict[str, T.Any]) -> ReplyBase: + reply_type = data['inReplyTo'] + func = self.reply_map.get(reply_type, None) + if not func: + raise CMakeException(f'Recieved unknown reply type "{reply_type}"') + for i in ['cookie'] + CMAKE_REPLY_TYPES[reply_type]: + if i not in data: + raise CMakeException(f'Key "{i}" is missing from CMake server message type {type}') + return func(data) + + def resolve_reply_cmakeInputs(self, data: T.Dict[str, T.Any]) -> ReplyCMakeInputs: + files = [] + for i in data['buildFiles']: + for j in i['sources']: + files += [CMakeBuildFile(Path(j), i['isCMake'], i['isTemporary'])] + return ReplyCMakeInputs(data['cookie'], Path(data['cmakeRootDirectory']), Path(data['sourceDirectory']), files) + + @contextmanager + def connect(self, cmake_exe: 'CMakeExecutor') -> T.Generator[None, None, None]: + self.startup(cmake_exe) + try: + yield + finally: + self.shutdown() + + def startup(self, cmake_exe: 'CMakeExecutor') -> None: + if self.proc is not None: + raise CMakeException('The CMake server was already started') + assert cmake_exe.found() + + mlog.debug('Starting CMake server with CMake', mlog.bold(' '.join(cmake_exe.get_command())), 'version', mlog.cyan(cmake_exe.version())) + self.proc = Popen(cmake_exe.get_command() + ['-E', 'server', '--experimental', '--debug'], stdin=PIPE, stdout=PIPE) + + def shutdown(self) -> None: + if self.proc is None: + return + + mlog.debug('Shutting down the CMake server') + + # Close the pipes to exit + self.proc.stdin.close() + self.proc.stdout.close() + + # Wait for CMake to finish + try: + self.proc.wait(timeout=2) + except TimeoutExpired: + # Terminate CMake if there is a timeout + # terminate() may throw a platform specific exception if the process has already + # terminated. This may be the case if there is a race condition (CMake exited after + # the timeout but before the terminate() call). Additionally, this behavior can + # also be triggered on cygwin if CMake crashes. + # See https://github.com/mesonbuild/meson/pull/4969#issuecomment-499413233 + try: + self.proc.terminate() + except Exception: + pass + + self.proc = None |