aboutsummaryrefslogtreecommitdiffstats
path: root/meson/mesonbuild/cmake/client.py
diff options
context:
space:
mode:
authorAngelos Mouzakitis <a.mouzakitis@virtualopensystems.com>2023-10-10 14:33:42 +0000
committerAngelos Mouzakitis <a.mouzakitis@virtualopensystems.com>2023-10-10 14:33:42 +0000
commitaf1a266670d040d2f4083ff309d732d648afba2a (patch)
tree2fc46203448ddcc6f81546d379abfaeb323575e9 /meson/mesonbuild/cmake/client.py
parente02cda008591317b1625707ff8e115a4841aa889 (diff)
Add submodule dependency filesHEADmaster
Change-Id: Iaf8d18082d3991dec7c0ebbea540f092188eb4ec
Diffstat (limited to 'meson/mesonbuild/cmake/client.py')
-rw-r--r--meson/mesonbuild/cmake/client.py373
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