| # 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 .executor import CMakeExecutor |
| from ..environment import Environment |
| from ..mesonlib import MachineChoice |
| from .. import mlog |
| from contextlib import contextmanager |
| from subprocess import Popen, PIPE, TimeoutExpired |
| import typing as T |
| import json |
| import os |
| |
| 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'], |
| } |
| |
| CMAKE_REPLY_TYPES = { |
| 'handshake': [], |
| 'configure': [], |
| 'compute': [], |
| 'cmakeInputs': ['buildFiles', 'cmakeRootDirectory', 'sourceDirectory'], |
| 'codemodel': ['configurations'] |
| } |
| |
| # Base CMake server message classes |
| |
| class MessageBase: |
| def __init__(self, msg_type: str, cookie: str): |
| self.type = msg_type |
| self.cookie = cookie |
| |
| def to_dict(self) -> dict: |
| 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): |
| super().__init__(msg_type, self.gen_cookie()) |
| |
| @staticmethod |
| def gen_cookie(): |
| RequestBase.cookie_counter += 1 |
| return 'meson_{}'.format(RequestBase.cookie_counter) |
| |
| class ReplyBase(MessageBase): |
| def __init__(self, cookie: str, in_reply_to: str): |
| super().__init__('reply', cookie) |
| self.in_reply_to = in_reply_to |
| |
| class SignalBase(MessageBase): |
| def __init__(self, cookie: str, signal_name: str): |
| 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): |
| 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): |
| 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): |
| super().__init__('progress', cookie) |
| |
| def log(self) -> None: |
| pass |
| |
| class MessageHello(MessageBase): |
| def __init__(self, supported_protocol_versions: T.List[dict]): |
| 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: |
| 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: str, build_dir: str, generator: str, vers_major: int, vers_minor: T.Optional[int] = 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) -> dict: |
| 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 |
| src_list = os.path.normpath(self.src_dir).split(os.sep) |
| bld_list = os.path.normpath(self.build_dir).split(os.sep) |
| |
| return { |
| **super().to_dict(), |
| 'sourceDirectory': '/'.join(src_list), |
| 'buildDirectory': '/'.join(bld_list), |
| '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) -> dict: |
| res = super().to_dict() |
| if self.args: |
| res['cacheArguments'] = self.args |
| return res |
| |
| class RequestCompute(RequestBase): |
| def __init__(self): |
| super().__init__('compute') |
| |
| class RequestCMakeInputs(RequestBase): |
| def __init__(self): |
| super().__init__('cmakeInputs') |
| |
| class RequestCodeModel(RequestBase): |
| def __init__(self): |
| super().__init__('codemodel') |
| |
| # Reply classes |
| |
| class ReplyHandShake(ReplyBase): |
| def __init__(self, cookie: str): |
| super().__init__(cookie, 'handshake') |
| |
| class ReplyConfigure(ReplyBase): |
| def __init__(self, cookie: str): |
| super().__init__(cookie, 'configure') |
| |
| class ReplyCompute(ReplyBase): |
| def __init__(self, cookie: str): |
| super().__init__(cookie, 'compute') |
| |
| class ReplyCMakeInputs(ReplyBase): |
| def __init__(self, cookie: str, cmake_root: str, src_dir: str, build_files: T.List[CMakeBuildFile]): |
| 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)) |
| mlog.log('Source dir: ', mlog.bold(self.src_dir)) |
| 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: dict): |
| 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('Configuration {}:'.format(idx)) |
| with mlog.nested(): |
| i.log() |
| |
| # Main client class |
| |
| class CMakeClient: |
| def __init__(self, env: Environment): |
| self.env = env |
| self.proc = None |
| 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']) |
| } |
| |
| 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), |
| } |
| |
| def readMessageRaw(self) -> dict: |
| 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: |
| return json.loads('\n'.join(rawData)) |
| 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('Recieved unknown message type "{}"'.format(msg_type)) |
| for i in CMAKE_MESSAGE_TYPES[msg_type]: |
| if i not in raw_data: |
| raise CMakeException('Key "{}" is missing from CMake server message type {}'.format(i, 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) -> ReplyBase: |
| 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: str, build_dir: str, 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: dict) -> ReplyBase: |
| reply_type = data['inReplyTo'] |
| func = self.reply_map.get(reply_type, None) |
| if not func: |
| raise CMakeException('Recieved unknown reply type "{}"'.format(reply_type)) |
| for i in ['cookie'] + CMAKE_REPLY_TYPES[reply_type]: |
| if i not in data: |
| raise CMakeException('Key "{}" is missing from CMake server message type {}'.format(i, type)) |
| return func(data) |
| |
| def resolve_reply_cmakeInputs(self, data: dict) -> ReplyCMakeInputs: |
| files = [] |
| for i in data['buildFiles']: |
| for j in i['sources']: |
| files += [CMakeBuildFile(j, i['isCMake'], i['isTemporary'])] |
| return ReplyCMakeInputs(data['cookie'], data['cmakeRootDirectory'], data['sourceDirectory'], files) |
| |
| @contextmanager |
| def connect(self): |
| self.startup() |
| try: |
| yield |
| finally: |
| self.shutdown() |
| |
| def startup(self) -> None: |
| if self.proc is not None: |
| raise CMakeException('The CMake server was already started') |
| for_machine = MachineChoice.HOST # TODO make parameter |
| cmake_exe = CMakeExecutor(self.env, '>=3.7', for_machine) |
| if not cmake_exe.found(): |
| raise CMakeException('Unable to find CMake') |
| |
| 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 |