cmake: Refactor CMakeExecutor and CMakeTraceParser

This moves most of the execution code from the CMakeInterpreter
into CMakeExecutor. Also, CMakeTraceParser is now responsible
for determining the trace cmd arguments.
pull/6432/head
Daniel Mensinger 5 years ago
parent b74ece344f
commit 3607f50d7f
No known key found for this signature in database
GPG Key ID: 54DD94C131E277D4
  1. 95
      mesonbuild/cmake/executor.py
  2. 51
      mesonbuild/cmake/interpreter.py
  3. 31
      mesonbuild/cmake/traceparser.py
  4. 30
      mesonbuild/dependencies/base.py
  5. 3
      mesonbuild/dependencies/dev.py

@ -15,8 +15,9 @@
# This class contains the basic functionality needed to run any interpreter
# or an interpreter-based tool.
import subprocess
import subprocess as S
from pathlib import Path
from threading import Thread
import typing as T
import re
import os
@ -30,19 +31,22 @@ from ..environment import Environment
if T.TYPE_CHECKING:
from ..dependencies.base import ExternalProgram
TYPE_result = T.Tuple[int, T.Optional[str], T.Optional[str]]
class CMakeExecutor:
# The class's copy of the CMake path. Avoids having to search for it
# multiple times in the same Meson invocation.
class_cmakebin = PerMachine(None, None)
class_cmakevers = PerMachine(None, None)
class_cmake_cache = {}
class_cmake_cache = {} # type: T.Dict[T.Any, TYPE_result]
def __init__(self, environment: Environment, version: str, for_machine: MachineChoice, silent: bool = False):
self.min_version = version
self.environment = environment
self.for_machine = for_machine
self.cmakebin, self.cmakevers = self.find_cmake_binary(self.environment, silent=silent)
self.always_capture_stderr = True
self.print_cmout = False
if self.cmakebin is False:
self.cmakebin = None
return
@ -130,17 +134,77 @@ class CMakeExecutor:
cmvers = re.sub(r'\s*cmake version\s*', '', out.split('\n')[0]).strip()
return cmvers
def set_exec_mode(self, print_cmout: T.Optional[bool] = None, always_capture_stderr: T.Optional[bool] = None) -> None:
if print_cmout is not None:
self.print_cmout = print_cmout
if always_capture_stderr is not None:
self.always_capture_stderr = always_capture_stderr
def _cache_key(self, args: T.List[str], build_dir: str, env):
fenv = frozenset(env.items()) if env is not None else None
targs = tuple(args)
return (self.cmakebin, targs, build_dir, fenv)
def _call_real(self, args: T.List[str], build_dir: str, env) -> T.Tuple[int, str, str]:
def _call_cmout_stderr(self, args: T.List[str], build_dir: str, env) -> TYPE_result:
cmd = self.cmakebin.get_command() + args
proc = S.Popen(cmd, stdout=S.PIPE, stderr=S.PIPE, cwd=build_dir, env=env)
# stdout and stderr MUST be read at the same time to avoid pipe
# blocking issues. The easiest way to do this is with a separate
# thread for one of the pipes.
def print_stdout():
while True:
line = proc.stdout.readline()
if not line:
break
mlog.log(line.decode(errors='ignore').strip('\n'))
proc.stdout.close()
t = Thread(target=print_stdout)
t.start()
try:
# Read stderr line by line and log non trace lines
raw_trace = ''
tline_start_reg = re.compile(r'^\s*(.*\.(cmake|txt))\(([0-9]+)\):\s*(\w+)\(.*$')
inside_multiline_trace = False
while True:
line = proc.stderr.readline()
if not line:
break
line = line.decode(errors='ignore')
if tline_start_reg.match(line):
raw_trace += line
inside_multiline_trace = not line.endswith(' )\n')
elif inside_multiline_trace:
raw_trace += line
else:
mlog.warning(line.strip('\n'))
finally:
proc.stderr.close()
t.join()
proc.wait()
return proc.returncode, None, raw_trace
def _call_cmout(self, args: T.List[str], build_dir: str, env) -> TYPE_result:
cmd = self.cmakebin.get_command() + args
proc = S.Popen(cmd, stdout=S.PIPE, stderr=S.STDOUT, cwd=build_dir, env=env)
while True:
line = proc.stdout.readline()
if not line:
break
mlog.log(line.decode(errors='ignore').strip('\n'))
proc.stdout.close()
proc.wait()
return proc.returncode, None, None
def _call_quiet(self, args: T.List[str], build_dir: str, env) -> TYPE_result:
os.makedirs(build_dir, exist_ok=True)
cmd = self.cmakebin.get_command() + args
ret = subprocess.run(cmd, env=env, cwd=build_dir, close_fds=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=False)
ret = S.run(cmd, env=env, cwd=build_dir, close_fds=False,
stdout=S.PIPE, stderr=S.PIPE, universal_newlines=False)
rc = ret.returncode
out = ret.stdout.decode(errors='ignore')
err = ret.stderr.decode(errors='ignore')
@ -148,21 +212,30 @@ class CMakeExecutor:
mlog.debug("Called `{}` in {} -> {}".format(call, build_dir, rc))
return rc, out, err
def call(self, args: T.List[str], build_dir: str, env=None, disable_cache: bool = False):
def _call_impl(self, args: T.List[str], build_dir: str, env) -> TYPE_result:
if not self.print_cmout:
return self._call_quiet(args, build_dir, env)
else:
if self.always_capture_stderr:
return self._call_cmout_stderr(args, build_dir, env)
else:
return self._call_cmout(args, build_dir, env)
def call(self, args: T.List[str], build_dir: str, env=None, disable_cache: bool = False) -> TYPE_result:
if env is None:
env = os.environ
if disable_cache:
return self._call_real(args, build_dir, env)
return self._call_impl(args, build_dir, env)
# First check if cached, if not call the real cmake function
cache = CMakeExecutor.class_cmake_cache
key = self._cache_key(args, build_dir, env)
if key not in cache:
cache[key] = self._call_real(args, build_dir, env)
cache[key] = self._call_impl(args, build_dir, env)
return cache[key]
def call_with_fake_build(self, args: T.List[str], build_dir: str, env=None):
def call_with_fake_build(self, args: T.List[str], build_dir: str, env=None) -> TYPE_result:
# First check the cache
cache = CMakeExecutor.class_cmake_cache
key = self._cache_key(args, build_dir, env)
@ -282,7 +355,7 @@ set(CMAKE_SIZEOF_VOID_P "{}")
def executable_path(self) -> str:
return self.cmakebin.get_path()
def get_command(self):
def get_command(self) -> T.List[str]:
return self.cmakebin.get_command()
def machine_choice(self) -> MachineChoice:

@ -24,8 +24,6 @@ from .. import mlog
from ..environment import Environment
from ..mesonlib import MachineChoice, version_compare
from ..compilers.compilers import lang_suffixes, header_suffixes, obj_suffixes, lib_suffixes, is_header
from subprocess import Popen, PIPE
from threading import Thread
from enum import Enum
from functools import lru_cache
import typing as T
@ -741,7 +739,7 @@ class CMakeInterpreter:
self.languages = []
self.targets = []
self.custom_targets = [] # type: T.List[ConverterCustomTarget]
self.trace = CMakeTraceParser()
self.trace = CMakeTraceParser('', '') # Will be replaced in analyse
self.output_target_map = OutputTargetMap(self.build_dir)
# Generated meson data
@ -754,10 +752,11 @@ class CMakeInterpreter:
cmake_exe = CMakeExecutor(self.env, '>=3.7', for_machine)
if not cmake_exe.found():
raise CMakeException('Unable to find CMake')
self.trace = CMakeTraceParser(cmake_exe.version(), self.build_dir, permissive=True)
generator = backend_generator_map[self.backend_name]
cmake_args = cmake_exe.get_command()
trace_args = ['--trace', '--trace-expand', '--no-warn-unused-cli']
cmake_args = []
trace_args = self.trace.trace_args()
cmcmp_args = ['-DCMAKE_POLICY_WARNING_{}=OFF'.format(x) for x in disable_policy_warnings]
if version_compare(cmake_exe.version(), '>=3.14'):
@ -795,46 +794,15 @@ class CMakeInterpreter:
os.makedirs(self.build_dir, exist_ok=True)
os_env = os.environ.copy()
os_env['LC_ALL'] = 'C'
final_command = cmake_args + trace_args + cmcmp_args + [self.src_dir]
proc = Popen(final_command, stdout=PIPE, stderr=PIPE, cwd=self.build_dir, env=os_env)
def print_stdout():
while True:
line = proc.stdout.readline()
if not line:
break
mlog.log(line.decode('utf-8').strip('\n'))
proc.stdout.close()
t = Thread(target=print_stdout)
t.start()
# Read stderr line by line and log non trace lines
self.raw_trace = ''
tline_start_reg = re.compile(r'^\s*(.*\.(cmake|txt))\(([0-9]+)\):\s*(\w+)\(.*$')
inside_multiline_trace = False
while True:
line = proc.stderr.readline()
if not line:
break
line = line.decode('utf-8')
if tline_start_reg.match(line):
self.raw_trace += line
inside_multiline_trace = not line.endswith(' )\n')
elif inside_multiline_trace:
self.raw_trace += line
else:
mlog.warning(line.strip('\n'))
proc.stderr.close()
proc.wait()
final_args = cmake_args + trace_args + cmcmp_args + [self.src_dir]
t.join()
cmake_exe.set_exec_mode(print_cmout=True)
rc, _, self.raw_trace = cmake_exe.call(final_args, self.build_dir, env=os_env, disable_cache=True)
mlog.log()
h = mlog.green('SUCCEEDED') if proc.returncode == 0 else mlog.red('FAILED')
h = mlog.green('SUCCEEDED') if rc == 0 else mlog.red('FAILED')
mlog.log('CMake configuration:', h)
if proc.returncode != 0:
if rc != 0:
raise CMakeException('Failed to configure the CMake subproject')
def initialise(self, extra_cmake_options: T.List[str]) -> None:
@ -889,7 +857,6 @@ class CMakeInterpreter:
self.languages = []
self.targets = []
self.custom_targets = []
self.trace = CMakeTraceParser(permissive=True)
# Parse the trace
self.trace.parse(self.raw_trace)

@ -18,8 +18,10 @@
from .common import CMakeException
from .generator import parse_generator_expressions
from .. import mlog
from ..mesonlib import version_compare
import typing as T
from pathlib import Path
import re
import os
@ -60,7 +62,7 @@ class CMakeGeneratorTarget(CMakeTarget):
self.working_dir = None # type: T.Optional[str]
class CMakeTraceParser:
def __init__(self, permissive: bool = False):
def __init__(self, cmake_version: str, build_dir: str, permissive: bool = False):
# Dict of CMake variables: '<var_name>': ['list', 'of', 'values']
self.vars = {}
@ -71,10 +73,27 @@ class CMakeTraceParser:
self.custom_targets = [] # type: T.List[CMakeGeneratorTarget]
self.permissive = permissive # type: bool
self.cmake_version = cmake_version # type: str
self.trace_format = 'human'
def parse(self, trace: str) -> None:
# First parse the trace
lexer1 = self._lex_trace(trace)
def trace_args(self) -> T.List[str]:
arg_map = {
'human': ['--trace', '--trace-expand'],
}
base_args = ['--no-warn-unused-cli']
return arg_map[self.trace_format] + base_args
def parse(self, trace: T.Optional[str] = None) -> None:
if not trace:
raise CMakeException('CMake: The CMake trace was not provided or is empty')
# Second parse the trace
lexer1 = None
if self.trace_format == 'human':
lexer1 = self._lex_trace_human(trace)
else:
raise CMakeException('CMake: Internal error: Invalid trace format {}. Expected [human]'.format(self.trace_format))
# All supported functions
functions = {
@ -481,7 +500,7 @@ class CMakeTraceParser:
self.targets[target].properties[i[0]] += i[1]
def _lex_trace(self, trace):
def _lex_trace_human(self, trace):
# The trace format is: '<file>(<line>): <func>(<args -- can contain \n> )\n'
reg_tline = re.compile(r'\s*(.*\.(cmake|txt))\(([0-9]+)\):\s*(\w+)\(([\s\S]*?) ?\)\s*\n', re.MULTILINE)
reg_other = re.compile(r'[^\n]*\n')
@ -510,7 +529,7 @@ class CMakeTraceParser:
yield CMakeTraceLine(file, line, func, args)
def _guess_files(self, broken_list: T.List[str]) -> T.List[str]:
#Try joining file paths that contain spaces
# Try joining file paths that contain spaces
reg_start = re.compile(r'^([A-Za-z]:)?/.*/[^./]+$')
reg_end = re.compile(r'^.*\.[a-zA-Z]+$')

@ -1089,7 +1089,6 @@ class CMakeDependency(ExternalDependency):
# stored in the pickled coredata and recovered.
self.cmakebin = None
self.cmakeinfo = None
self.traceparser = CMakeTraceParser()
# Where all CMake "build dirs" are located
self.cmake_root_dir = environment.scratch_dir
@ -1097,6 +1096,10 @@ class CMakeDependency(ExternalDependency):
# T.List of successfully found modules
self.found_modules = []
# Initialize with None before the first return to avoid
# AttributeError exceptions in derived classes
self.traceparser = None # type: CMakeTraceParser
self.cmakebin = CMakeExecutor(environment, CMakeDependency.class_cmake_version, self.for_machine, silent=self.silent)
if not self.cmakebin.found():
self.cmakebin = None
@ -1106,6 +1109,9 @@ class CMakeDependency(ExternalDependency):
mlog.debug(msg)
return
# Setup the trace parser
self.traceparser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir())
if CMakeDependency.class_cmakeinfo[self.for_machine] is None:
CMakeDependency.class_cmakeinfo[self.for_machine] = self._get_cmake_info()
self.cmakeinfo = CMakeDependency.class_cmakeinfo[self.for_machine]
@ -1151,11 +1157,13 @@ class CMakeDependency(ExternalDependency):
gen_list += [CMakeDependency.class_working_generator]
gen_list += CMakeDependency.class_cmake_generators
temp_parser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir())
for i in gen_list:
mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto'))
# Prepare options
cmake_opts = ['--trace-expand', '.']
cmake_opts = temp_parser.trace_args() + ['.']
if len(i) > 0:
cmake_opts = ['-G', i] + cmake_opts
@ -1175,7 +1183,6 @@ class CMakeDependency(ExternalDependency):
return None
try:
temp_parser = CMakeTraceParser()
temp_parser.parse(err1)
except MesonException:
return None
@ -1328,7 +1335,8 @@ class CMakeDependency(ExternalDependency):
mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto'))
# Prepare options
cmake_opts = ['--trace-expand', '-DNAME={}'.format(name), '-DARCHS={}'.format(';'.join(self.cmakeinfo['archs']))] + args + ['.']
cmake_opts = ['-DNAME={}'.format(name), '-DARCHS={}'.format(';'.join(self.cmakeinfo['archs']))] + args + ['.']
cmake_opts += self.traceparser.trace_args()
cmake_opts += self._extra_cmake_opts()
if len(i) > 0:
cmake_opts = ['-G', i] + cmake_opts
@ -1499,10 +1507,14 @@ class CMakeDependency(ExternalDependency):
self.compile_args = compileOptions + compileDefinitions + ['-I{}'.format(x) for x in incDirs]
self.link_args = libraries
def _setup_cmake_dir(self, cmake_file: str) -> str:
# Setup the CMake build environment and return the "build" directory
def _get_build_dir(self) -> str:
build_dir = Path(self.cmake_root_dir) / 'cmake_{}'.format(self.name)
build_dir.mkdir(parents=True, exist_ok=True)
return str(build_dir)
def _setup_cmake_dir(self, cmake_file: str) -> str:
# Setup the CMake build environment and return the "build" directory
build_dir = self._get_build_dir()
# Insert language parameters into the CMakeLists.txt and write new CMakeLists.txt
src_cmake = Path(__file__).parent / 'data' / cmake_file
@ -1525,11 +1537,11 @@ cmake_minimum_required(VERSION ${{CMAKE_VERSION}})
project(MesonTemp LANGUAGES {})
""".format(' '.join(cmake_language)) + cmake_txt
cm_file = build_dir / 'CMakeLists.txt'
cm_file = Path(build_dir) / 'CMakeLists.txt'
cm_file.write_text(cmake_txt)
mlog.cmd_ci_include(cm_file.absolute().as_posix())
return str(build_dir)
return build_dir
def _call_cmake(self, args, cmake_file: str, env=None):
build_dir = self._setup_cmake_dir(cmake_file)
@ -1552,7 +1564,7 @@ project(MesonTemp LANGUAGES {})
def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
configtool: T.Optional[str] = None, default_value: T.Optional[str] = None,
pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]:
if cmake:
if cmake and self.traceparser is not None:
try:
v = self.traceparser.vars[cmake]
except KeyError:

@ -396,6 +396,9 @@ class LLVMDependencyCMake(CMakeDependency):
self.llvm_opt_modules = stringlistify(extract_as_list(kwargs, 'optional_modules'))
super().__init__(name='LLVM', environment=env, language='cpp', kwargs=kwargs)
if self.traceparser is None:
return
# Extract extra include directories and definitions
inc_dirs = self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS')
defs = self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS')

Loading…
Cancel
Save