aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mesonbuild/cmake/__init__.py2
-rw-r--r--mesonbuild/cmake/client.py143
-rw-r--r--mesonbuild/cmake/common.py143
-rw-r--r--mesonbuild/cmake/fileapi.py318
-rw-r--r--mesonbuild/cmake/interpreter.py57
-rw-r--r--mesonbuild/cmake/traceparser.py18
-rwxr-xr-xrun_project_tests.py33
7 files changed, 557 insertions, 157 deletions
diff --git a/mesonbuild/cmake/__init__.py b/mesonbuild/cmake/__init__.py
index f9835a1..5520ae3 100644
--- a/mesonbuild/cmake/__init__.py
+++ b/mesonbuild/cmake/__init__.py
@@ -19,6 +19,7 @@ __all__ = [
'CMakeClient',
'CMakeExecutor',
'CMakeException',
+ 'CMakeFileAPI',
'CMakeInterpreter',
'CMakeTarget',
'CMakeTraceLine',
@@ -29,6 +30,7 @@ __all__ = [
from .common import CMakeException
from .client import CMakeClient
from .executor import CMakeExecutor
+from .fileapi import CMakeFileAPI
from .generator import parse_generator_expressions
from .interpreter import CMakeInterpreter
from .traceparser import CMakeTarget, CMakeTraceLine, CMakeTraceParser
diff --git a/mesonbuild/cmake/client.py b/mesonbuild/cmake/client.py
index f77e0cc..9cb7f74 100644
--- a/mesonbuild/cmake/client.py
+++ b/mesonbuild/cmake/client.py
@@ -15,7 +15,7 @@
# This class contains the basic functionality needed to run any interpreter
# or an interpreter-based tool.
-from .common import CMakeException
+from .common import CMakeException, CMakeConfiguration, CMakeBuildFile
from .executor import CMakeExecutor
from ..environment import Environment
from ..mesonlib import MachineChoice
@@ -186,15 +186,6 @@ class ReplyCompute(ReplyBase):
def __init__(self, cookie: str):
super().__init__(cookie, 'compute')
-class CMakeBuildFile:
- def __init__(self, file: str, is_cmake: bool, is_temp: bool):
- self.file = file
- self.is_cmake = is_cmake
- self.is_temp = is_temp
-
- def __repr__(self):
- return '<{}: {}; cmake={}; temp={}>'.format(self.__class__.__name__, self.file, self.is_cmake, self.is_temp)
-
class ReplyCMakeInputs(ReplyBase):
def __init__(self, cookie: str, cmake_root: str, src_dir: str, build_files: List[CMakeBuildFile]):
super().__init__(cookie, 'cmakeInputs')
@@ -210,138 +201,6 @@ class ReplyCMakeInputs(ReplyBase):
for i in self.build_files:
mlog.log(str(i))
-def _flags_to_list(raw: str) -> List[str]:
- # Convert a raw commandline string into a list of strings
- res = []
- curr = ''
- escape = False
- in_string = False
- for i in raw:
- if escape:
- # If the current char is not a quote, the '\' is probably important
- if i not in ['"', "'"]:
- curr += '\\'
- curr += i
- escape = False
- elif i == '\\':
- escape = True
- elif i in ['"', "'"]:
- in_string = not in_string
- elif i in [' ', '\n']:
- if in_string:
- curr += i
- else:
- res += [curr]
- curr = ''
- else:
- curr += i
- res += [curr]
- res = list(filter(lambda x: len(x) > 0, res))
- return res
-
-class CMakeFileGroup:
- def __init__(self, data: dict):
- self.defines = data.get('defines', '')
- self.flags = _flags_to_list(data.get('compileFlags', ''))
- self.includes = data.get('includePath', [])
- self.is_generated = data.get('isGenerated', False)
- self.language = data.get('language', 'C')
- self.sources = data.get('sources', [])
-
- # Fix the include directories
- tmp = []
- for i in self.includes:
- if isinstance(i, dict) and 'path' in i:
- tmp += [i['path']]
- elif isinstance(i, str):
- tmp += [i]
- self.includes = tmp
-
- def log(self) -> None:
- mlog.log('flags =', mlog.bold(', '.join(self.flags)))
- mlog.log('defines =', mlog.bold(', '.join(self.defines)))
- mlog.log('includes =', mlog.bold(', '.join(self.includes)))
- mlog.log('is_generated =', mlog.bold('true' if self.is_generated else 'false'))
- mlog.log('language =', mlog.bold(self.language))
- mlog.log('sources:')
- for i in self.sources:
- with mlog.nested():
- mlog.log(i)
-
-class CMakeTarget:
- def __init__(self, data: dict):
- self.artifacts = data.get('artifacts', [])
- self.src_dir = data.get('sourceDirectory', '')
- self.build_dir = data.get('buildDirectory', '')
- self.name = data.get('name', '')
- self.full_name = data.get('fullName', '')
- self.install = data.get('hasInstallRule', False)
- self.install_paths = list(set(data.get('installPaths', [])))
- self.link_lang = data.get('linkerLanguage', '')
- self.link_libraries = _flags_to_list(data.get('linkLibraries', ''))
- self.link_flags = _flags_to_list(data.get('linkFlags', ''))
- self.link_lang_flags = _flags_to_list(data.get('linkLanguageFlags', ''))
- self.link_path = data.get('linkPath', '')
- self.type = data.get('type', 'EXECUTABLE')
- self.is_generator_provided = data.get('isGeneratorProvided', False)
- self.files = []
-
- for i in data.get('fileGroups', []):
- self.files += [CMakeFileGroup(i)]
-
- def log(self) -> None:
- mlog.log('artifacts =', mlog.bold(', '.join(self.artifacts)))
- mlog.log('src_dir =', mlog.bold(self.src_dir))
- mlog.log('build_dir =', mlog.bold(self.build_dir))
- mlog.log('name =', mlog.bold(self.name))
- mlog.log('full_name =', mlog.bold(self.full_name))
- mlog.log('install =', mlog.bold('true' if self.install else 'false'))
- mlog.log('install_paths =', mlog.bold(', '.join(self.install_paths)))
- mlog.log('link_lang =', mlog.bold(self.link_lang))
- mlog.log('link_libraries =', mlog.bold(', '.join(self.link_libraries)))
- mlog.log('link_flags =', mlog.bold(', '.join(self.link_flags)))
- mlog.log('link_lang_flags =', mlog.bold(', '.join(self.link_lang_flags)))
- mlog.log('link_path =', mlog.bold(self.link_path))
- mlog.log('type =', mlog.bold(self.type))
- mlog.log('is_generator_provided =', mlog.bold('true' if self.is_generator_provided else 'false'))
- for idx, i in enumerate(self.files):
- mlog.log('Files {}:'.format(idx))
- with mlog.nested():
- i.log()
-
-class CMakeProject:
- def __init__(self, data: dict):
- self.src_dir = data.get('sourceDirectory', '')
- self.build_dir = data.get('buildDirectory', '')
- self.name = data.get('name', '')
- self.targets = []
-
- for i in data.get('targets', []):
- self.targets += [CMakeTarget(i)]
-
- def log(self) -> None:
- mlog.log('src_dir =', mlog.bold(self.src_dir))
- mlog.log('build_dir =', mlog.bold(self.build_dir))
- mlog.log('name =', mlog.bold(self.name))
- for idx, i in enumerate(self.targets):
- mlog.log('Target {}:'.format(idx))
- with mlog.nested():
- i.log()
-
-class CMakeConfiguration:
- def __init__(self, data: dict):
- self.name = data.get('name', '')
- self.projects = []
- for i in data.get('projects', []):
- self.projects += [CMakeProject(i)]
-
- def log(self) -> None:
- mlog.log('name =', mlog.bold(self.name))
- for idx, i in enumerate(self.projects):
- mlog.log('Project {}:'.format(idx))
- with mlog.nested():
- i.log()
-
class ReplyCodeModel(ReplyBase):
def __init__(self, data: dict):
super().__init__(data['cookie'], 'codemodel')
diff --git a/mesonbuild/cmake/common.py b/mesonbuild/cmake/common.py
index 217247e..70ef622 100644
--- a/mesonbuild/cmake/common.py
+++ b/mesonbuild/cmake/common.py
@@ -16,6 +16,149 @@
# or an interpreter-based tool.
from ..mesonlib import MesonException
+from .. import mlog
+from typing import List
class CMakeException(MesonException):
pass
+
+class CMakeBuildFile:
+ def __init__(self, file: str, is_cmake: bool, is_temp: bool):
+ self.file = file
+ self.is_cmake = is_cmake
+ self.is_temp = is_temp
+
+ def __repr__(self):
+ return '<{}: {}; cmake={}; temp={}>'.format(self.__class__.__name__, self.file, self.is_cmake, self.is_temp)
+
+def _flags_to_list(raw: str) -> List[str]:
+ # Convert a raw commandline string into a list of strings
+ res = []
+ curr = ''
+ escape = False
+ in_string = False
+ for i in raw:
+ if escape:
+ # If the current char is not a quote, the '\' is probably important
+ if i not in ['"', "'"]:
+ curr += '\\'
+ curr += i
+ escape = False
+ elif i == '\\':
+ escape = True
+ elif i in ['"', "'"]:
+ in_string = not in_string
+ elif i in [' ', '\n']:
+ if in_string:
+ curr += i
+ else:
+ res += [curr]
+ curr = ''
+ else:
+ curr += i
+ res += [curr]
+ res = list(filter(lambda x: len(x) > 0, res))
+ return res
+
+class CMakeFileGroup:
+ def __init__(self, data: dict):
+ self.defines = data.get('defines', '')
+ self.flags = _flags_to_list(data.get('compileFlags', ''))
+ self.includes = data.get('includePath', [])
+ self.is_generated = data.get('isGenerated', False)
+ self.language = data.get('language', 'C')
+ self.sources = data.get('sources', [])
+
+ # Fix the include directories
+ tmp = []
+ for i in self.includes:
+ if isinstance(i, dict) and 'path' in i:
+ tmp += [i['path']]
+ elif isinstance(i, str):
+ tmp += [i]
+ self.includes = tmp
+
+ def log(self) -> None:
+ mlog.log('flags =', mlog.bold(', '.join(self.flags)))
+ mlog.log('defines =', mlog.bold(', '.join(self.defines)))
+ mlog.log('includes =', mlog.bold(', '.join(self.includes)))
+ mlog.log('is_generated =', mlog.bold('true' if self.is_generated else 'false'))
+ mlog.log('language =', mlog.bold(self.language))
+ mlog.log('sources:')
+ for i in self.sources:
+ with mlog.nested():
+ mlog.log(i)
+
+class CMakeTarget:
+ def __init__(self, data: dict):
+ self.artifacts = data.get('artifacts', [])
+ self.src_dir = data.get('sourceDirectory', '')
+ self.build_dir = data.get('buildDirectory', '')
+ self.name = data.get('name', '')
+ self.full_name = data.get('fullName', '')
+ self.install = data.get('hasInstallRule', False)
+ self.install_paths = list(set(data.get('installPaths', [])))
+ self.link_lang = data.get('linkerLanguage', '')
+ self.link_libraries = _flags_to_list(data.get('linkLibraries', ''))
+ self.link_flags = _flags_to_list(data.get('linkFlags', ''))
+ self.link_lang_flags = _flags_to_list(data.get('linkLanguageFlags', ''))
+ # self.link_path = data.get('linkPath', '')
+ self.type = data.get('type', 'EXECUTABLE')
+ # self.is_generator_provided = data.get('isGeneratorProvided', False)
+ self.files = []
+
+ for i in data.get('fileGroups', []):
+ self.files += [CMakeFileGroup(i)]
+
+ def log(self) -> None:
+ mlog.log('artifacts =', mlog.bold(', '.join(self.artifacts)))
+ mlog.log('src_dir =', mlog.bold(self.src_dir))
+ mlog.log('build_dir =', mlog.bold(self.build_dir))
+ mlog.log('name =', mlog.bold(self.name))
+ mlog.log('full_name =', mlog.bold(self.full_name))
+ mlog.log('install =', mlog.bold('true' if self.install else 'false'))
+ mlog.log('install_paths =', mlog.bold(', '.join(self.install_paths)))
+ mlog.log('link_lang =', mlog.bold(self.link_lang))
+ mlog.log('link_libraries =', mlog.bold(', '.join(self.link_libraries)))
+ mlog.log('link_flags =', mlog.bold(', '.join(self.link_flags)))
+ mlog.log('link_lang_flags =', mlog.bold(', '.join(self.link_lang_flags)))
+ # mlog.log('link_path =', mlog.bold(self.link_path))
+ mlog.log('type =', mlog.bold(self.type))
+ # mlog.log('is_generator_provided =', mlog.bold('true' if self.is_generator_provided else 'false'))
+ for idx, i in enumerate(self.files):
+ mlog.log('Files {}:'.format(idx))
+ with mlog.nested():
+ i.log()
+
+class CMakeProject:
+ def __init__(self, data: dict):
+ self.src_dir = data.get('sourceDirectory', '')
+ self.build_dir = data.get('buildDirectory', '')
+ self.name = data.get('name', '')
+ self.targets = []
+
+ for i in data.get('targets', []):
+ self.targets += [CMakeTarget(i)]
+
+ def log(self) -> None:
+ mlog.log('src_dir =', mlog.bold(self.src_dir))
+ mlog.log('build_dir =', mlog.bold(self.build_dir))
+ mlog.log('name =', mlog.bold(self.name))
+ for idx, i in enumerate(self.targets):
+ mlog.log('Target {}:'.format(idx))
+ with mlog.nested():
+ i.log()
+
+class CMakeConfiguration:
+ def __init__(self, data: dict):
+ self.name = data.get('name', '')
+ self.projects = []
+ for i in data.get('projects', []):
+ self.projects += [CMakeProject(i)]
+
+ def log(self) -> None:
+ mlog.log('name =', mlog.bold(self.name))
+ for idx, i in enumerate(self.projects):
+ mlog.log('Project {}:'.format(idx))
+ with mlog.nested():
+ i.log()
diff --git a/mesonbuild/cmake/fileapi.py b/mesonbuild/cmake/fileapi.py
new file mode 100644
index 0000000..df7c73a
--- /dev/null
+++ b/mesonbuild/cmake/fileapi.py
@@ -0,0 +1,318 @@
+# 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.
+
+from .common import CMakeException, CMakeBuildFile, CMakeConfiguration
+from typing import Any, List, Tuple
+import os
+import json
+import re
+
+STRIP_KEYS = ['cmake', 'reply', 'backtrace', 'backtraceGraph', 'version']
+
+class CMakeFileAPI:
+ def __init__(self, build_dir: str):
+ self.build_dir = build_dir
+ self.api_base_dir = os.path.join(self.build_dir, '.cmake', 'api', 'v1')
+ self.request_dir = os.path.join(self.api_base_dir, 'query', 'client-meson')
+ self.reply_dir = os.path.join(self.api_base_dir, 'reply')
+ self.cmake_sources = []
+ self.cmake_configurations = []
+ self.kind_resolver_map = {
+ 'codemodel': self._parse_codemodel,
+ 'cmakeFiles': self._parse_cmakeFiles,
+ }
+
+ def get_cmake_sources(self) -> List[CMakeBuildFile]:
+ return self.cmake_sources
+
+ def get_cmake_configurations(self) -> List[CMakeConfiguration]:
+ return self.cmake_configurations
+
+ def setup_request(self) -> None:
+ os.makedirs(self.request_dir, exist_ok=True)
+
+ query = {
+ 'requests': [
+ {'kind': 'codemodel', 'version': {'major': 2, 'minor': 0}},
+ {'kind': 'cmakeFiles', 'version': {'major': 1, 'minor': 0}},
+ ]
+ }
+
+ with open(os.path.join(self.request_dir, 'query.json'), 'w') as fp:
+ json.dump(query, fp, indent=2)
+
+ def load_reply(self) -> None:
+ if not os.path.isdir(self.reply_dir):
+ raise CMakeException('No response from the CMake file API')
+
+ files = os.listdir(self.reply_dir)
+ root = None
+ reg_index = re.compile(r'^index-.*\.json$')
+ for i in files:
+ if reg_index.match(i):
+ root = i
+ break
+
+ if not root:
+ raise CMakeException('Failed to find the CMake file API index')
+
+ index = self._reply_file_content(root) # Load the root index
+ index = self._strip_data(index) # Avoid loading duplicate files
+ index = self._resolve_references(index) # Load everything
+ index = self._strip_data(index) # Strip unused data (again for loaded files)
+
+ # Debug output
+ debug_json = os.path.normpath(os.path.join(self.build_dir, '..', 'fileAPI.json'))
+ with open(debug_json, 'w') as fp:
+ json.dump(index, fp, indent=2)
+
+ # parse the JSON
+ for i in index['objects']:
+ assert(isinstance(i, dict))
+ assert('kind' in i)
+ assert(i['kind'] in self.kind_resolver_map)
+
+ self.kind_resolver_map[i['kind']](i)
+
+ def _parse_codemodel(self, data: dict) -> None:
+ assert('configurations' in data)
+ assert('paths' in data)
+
+ source_dir = data['paths']['source']
+ build_dir = data['paths']['build']
+
+ # The file API output differs quite a bit from the server
+ # output. It is more flat than the server output and makes
+ # heavy use of references. Here these references are
+ # resolved and the resulting data structure is identical
+ # to the CMake serve output.
+
+ def helper_parse_dir(dir_entry: dict) -> Tuple[str, str]:
+ src_dir = dir_entry.get('source', '.')
+ bld_dir = dir_entry.get('build', '.')
+ src_dir = src_dir if os.path.isabs(src_dir) else os.path.join(source_dir, src_dir)
+ bld_dir = bld_dir if os.path.isabs(bld_dir) else os.path.join(source_dir, bld_dir)
+ src_dir = os.path.normpath(src_dir)
+ bld_dir = os.path.normpath(bld_dir)
+
+ return src_dir, bld_dir
+
+ def parse_sources(comp_group: dict, tgt: dict) -> Tuple[List[str], List[str], List[int]]:
+ gen = []
+ src = []
+ idx = []
+
+ src_list_raw = tgt.get('sources', [])
+ for i in comp_group.get('sourceIndexes', []):
+ if i >= len(src_list_raw) or 'path' not in src_list_raw[i]:
+ continue
+ if src_list_raw[i].get('isGenerated', False):
+ gen += [src_list_raw[i]['path']]
+ else:
+ src += [src_list_raw[i]['path']]
+ idx += [i]
+
+ return src, gen, idx
+
+ def parse_target(tgt: dict) -> dict:
+ src_dir, bld_dir = helper_parse_dir(cnf.get('paths', {}))
+
+ # Parse install paths (if present)
+ install_paths = []
+ if 'install' in tgt:
+ prefix = tgt['install']['prefix']['path']
+ install_paths = [os.path.join(prefix, x['path']) for x in tgt['install']['destinations']]
+ install_paths = list(set(install_paths))
+
+ # On the first look, it looks really nice that the CMake devs have
+ # decided to use arrays for the linker flags. However, this feeling
+ # soon turns into despair when you realize that there only one entry
+ # per type in most cases, and we still have to do manual string splitting.
+ link_flags = []
+ link_libs = []
+ for i in tgt.get('link', {}).get('commandFragments', []):
+ if i['role'] == 'flags':
+ link_flags += [i['fragment']]
+ elif i['role'] == 'libraries':
+ link_libs += [i['fragment']]
+ elif i['role'] == 'libraryPath':
+ link_flags += ['-L{}'.format(i['fragment'])]
+ elif i['role'] == 'frameworkPath':
+ link_flags += ['-F{}'.format(i['fragment'])]
+ for i in tgt.get('archive', {}).get('commandFragments', []):
+ if i['role'] == 'flags':
+ link_flags += [i['fragment']]
+
+ # TODO The `dependencies` entry is new in the file API.
+ # maybe we can make use of that in addtion to the
+ # implicit dependency detection
+ tgt_data = {
+ 'artifacts': [x.get('path', '') for x in tgt.get('artifacts', [])],
+ 'sourceDirectory': src_dir,
+ 'buildDirectory': bld_dir,
+ 'name': tgt.get('name', ''),
+ 'fullName': tgt.get('nameOnDisk', ''),
+ 'hasInstallRule': 'install' in tgt,
+ 'installPaths': install_paths,
+ 'linkerLanguage': tgt.get('link', {}).get('language', 'CXX'),
+ 'linkLibraries': ' '.join(link_libs), # See previous comment block why we join the array
+ 'linkFlags': ' '.join(link_flags), # See previous comment block why we join the array
+ 'type': tgt.get('type', 'EXECUTABLE'),
+ 'fileGroups': [],
+ }
+
+ processed_src_idx = []
+ for cg in tgt.get('compileGroups', []):
+ # Again, why an array, when there is usually only one element
+ # and arguments are seperated with spaces...
+ flags = []
+ for i in cg.get('compileCommandFragments', []):
+ flags += [i['fragment']]
+
+ cg_data = {
+ 'defines': [x.get('define', '') for x in cg.get('defines', [])],
+ 'compileFlags': ' '.join(flags),
+ 'language': cg.get('language', 'C'),
+ 'isGenerated': None, # Set later, flag is stored per source file
+ 'sources': [],
+
+ # TODO handle isSystem
+ 'includePath': [x.get('path', '') for x in cg.get('includes', [])],
+ }
+
+ normal_src, generated_src, src_idx = parse_sources(cg, tgt)
+ if normal_src:
+ cg_data = dict(cg_data)
+ cg_data['isGenerated'] = False
+ cg_data['sources'] = normal_src
+ tgt_data['fileGroups'] += [cg_data]
+ if generated_src:
+ cg_data = dict(cg_data)
+ cg_data['isGenerated'] = True
+ cg_data['sources'] = generated_src
+ tgt_data['fileGroups'] += [cg_data]
+ processed_src_idx += src_idx
+
+ # Object libraries have no compile groups, only source groups.
+ # So we add all the source files to a dummy source group that were
+ # not found in the previous loop
+ normal_src = []
+ generated_src = []
+ for idx, src in enumerate(tgt.get('sources', [])):
+ if idx in processed_src_idx:
+ continue
+
+ if src.get('isGenerated', False):
+ generated_src += [src['path']]
+ else:
+ normal_src += [src['path']]
+
+ if normal_src:
+ tgt_data['fileGroups'] += [{
+ 'isGenerated': False,
+ 'sources': normal_src,
+ }]
+ if generated_src:
+ tgt_data['fileGroups'] += [{
+ 'isGenerated': True,
+ 'sources': generated_src,
+ }]
+ return tgt_data
+
+ def parse_project(pro: dict) -> dict:
+ # Only look at the first directory specified in directoryIndexes
+ # TODO Figure out what the other indexes are there for
+ p_src_dir = source_dir
+ p_bld_dir = build_dir
+ try:
+ p_src_dir, p_bld_dir = helper_parse_dir(cnf['directories'][pro['directoryIndexes'][0]])
+ except (IndexError, KeyError):
+ pass
+
+ pro_data = {
+ 'name': pro.get('name', ''),
+ 'sourceDirectory': p_src_dir,
+ 'buildDirectory': p_bld_dir,
+ 'targets': [],
+ }
+
+ for ref in pro.get('targetIndexes', []):
+ tgt = {}
+ try:
+ tgt = cnf['targets'][ref]
+ except (IndexError, KeyError):
+ pass
+ pro_data['targets'] += [parse_target(tgt)]
+
+ return pro_data
+
+ for cnf in data.get('configurations', []):
+ cnf_data = {
+ 'name': cnf.get('name', ''),
+ 'projects': [],
+ }
+
+ for pro in cnf.get('projects', []):
+ cnf_data['projects'] += [parse_project(pro)]
+
+ self.cmake_configurations += [CMakeConfiguration(cnf_data)]
+
+ def _parse_cmakeFiles(self, data: dict) -> None:
+ assert('inputs' in data)
+ assert('paths' in data)
+
+ src_dir = data['paths']['source']
+
+ for i in data['inputs']:
+ path = i['path']
+ path = path if os.path.isabs(path) else os.path.join(src_dir, path)
+ self.cmake_sources += [CMakeBuildFile(path, i.get('isCMake', False), i.get('isGenerated', False))]
+
+ def _strip_data(self, data: Any) -> Any:
+ if isinstance(data, list):
+ for idx, i in enumerate(data):
+ data[idx] = self._strip_data(i)
+
+ elif isinstance(data, dict):
+ new = {}
+ for key, val in data.items():
+ if key not in STRIP_KEYS:
+ new[key] = self._strip_data(val)
+ data = new
+
+ return data
+
+ def _resolve_references(self, data: Any) -> Any:
+ if isinstance(data, list):
+ for idx, i in enumerate(data):
+ data[idx] = self._resolve_references(i)
+
+ elif isinstance(data, dict):
+ # Check for the "magic" reference entry and insert
+ # it into the root data dict
+ if 'jsonFile' in data:
+ data.update(self._reply_file_content(data['jsonFile']))
+
+ for key, val in data.items():
+ data[key] = self._resolve_references(val)
+
+ return data
+
+ def _reply_file_content(self, filename: str) -> dict:
+ real_path = os.path.join(self.reply_dir, filename)
+ if not os.path.exists(real_path):
+ raise CMakeException('File "{}" does not exist'.format(real_path))
+
+ with open(real_path, 'r') as fp:
+ return json.load(fp)
diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py
index 92df462..1c672be 100644
--- a/mesonbuild/cmake/interpreter.py
+++ b/mesonbuild/cmake/interpreter.py
@@ -15,17 +15,19 @@
# This class contains the basic functionality needed to run any interpreter
# or an interpreter-based tool.
-from .common import CMakeException
-from .client import CMakeClient, RequestCMakeInputs, RequestConfigure, RequestCompute, RequestCodeModel, CMakeTarget
+from .common import CMakeException, CMakeTarget
+from .client import CMakeClient, RequestCMakeInputs, RequestConfigure, RequestCompute, RequestCodeModel
+from .fileapi import CMakeFileAPI
from .executor import CMakeExecutor
from .traceparser import CMakeTraceParser, CMakeGeneratorTarget
from .. import mlog
from ..environment import Environment
-from ..mesonlib import MachineChoice
+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 typing import Any, List, Dict, Optional, TYPE_CHECKING
from threading import Thread
+from enum import Enum
import os, re
from ..mparser import (
@@ -458,6 +460,10 @@ class ConverterCustomTarget:
mlog.log(' -- inputs: ', mlog.bold(str(self.inputs)))
mlog.log(' -- depends: ', mlog.bold(str(self.depends)))
+class CMakeAPI(Enum):
+ SERVER = 1
+ FILE = 2
+
class CMakeInterpreter:
def __init__(self, build: 'Build', subdir: str, src_dir: str, install_prefix: str, env: Environment, backend: 'Backend'):
assert(hasattr(backend, 'name'))
@@ -469,11 +475,13 @@ class CMakeInterpreter:
self.install_prefix = install_prefix
self.env = env
self.backend_name = backend.name
+ self.cmake_api = CMakeAPI.SERVER
self.client = CMakeClient(self.env)
+ self.fileapi = CMakeFileAPI(self.build_dir)
# Raw CMake results
self.bs_files = []
- self.codemodel = None
+ self.codemodel_configs = None
self.raw_trace = None
# Analysed data
@@ -496,6 +504,10 @@ class CMakeInterpreter:
generator = backend_generator_map[self.backend_name]
cmake_args = cmake_exe.get_command()
+ if version_compare(cmake_exe.version(), '>=3.14'):
+ self.cmake_api = CMakeAPI.FILE
+ self.fileapi.setup_request()
+
# Map meson compiler to CMake variables
for lang, comp in self.env.coredata.compilers[for_machine].items():
if lang not in language_map:
@@ -552,8 +564,24 @@ class CMakeInterpreter:
def initialise(self, extra_cmake_options: List[str]) -> None:
# Run configure the old way becuse doing it
# with the server doesn't work for some reason
+ # Aditionally, the File API requires a configure anyway
self.configure(extra_cmake_options)
+ # Continue with the file API If supported
+ if self.cmake_api is CMakeAPI.FILE:
+ # Parse the result
+ self.fileapi.load_reply()
+
+ # Load the buildsystem file list
+ cmake_files = self.fileapi.get_cmake_sources()
+ self.bs_files = [x.file for x in cmake_files if not x.is_cmake and not x.is_temp]
+ self.bs_files = [os.path.relpath(x, self.env.get_source_dir()) for x in self.bs_files]
+ self.bs_files = list(set(self.bs_files))
+
+ # Load the codemodel configurations
+ self.codemodel_configs = self.fileapi.get_cmake_configurations()
+ return
+
with self.client.connect():
generator = backend_generator_map[self.backend_name]
self.client.do_handshake(self.src_dir, self.build_dir, generator, 1)
@@ -574,10 +602,10 @@ class CMakeInterpreter:
self.bs_files = [x.file for x in bs_reply.build_files if not x.is_cmake and not x.is_temp]
self.bs_files = [os.path.relpath(os.path.join(src_dir, x), self.env.get_source_dir()) for x in self.bs_files]
self.bs_files = list(set(self.bs_files))
- self.codemodel = cm_reply
+ self.codemodel_configs = cm_reply.configs
def analyse(self) -> None:
- if self.codemodel is None:
+ if self.codemodel_configs is None:
raise CMakeException('CMakeInterpreter was not initialized')
# Clear analyser data
@@ -591,7 +619,7 @@ class CMakeInterpreter:
self.trace.parse(self.raw_trace)
# Find all targets
- for i in self.codemodel.configs:
+ for i in self.codemodel_configs:
for j in i.projects:
if not self.project_name:
self.project_name = j.name
@@ -599,6 +627,21 @@ class CMakeInterpreter:
if k.type not in skip_targets:
self.targets += [ConverterTarget(k, self.env)]
+ # Add interface targets from trace, if not already present.
+ # This step is required because interface targets were removed from
+ # the CMake file API output.
+ api_target_name_list = [x.name for x in self.targets]
+ for i in self.trace.targets.values():
+ if i.type != 'INTERFACE' or i.name in api_target_name_list or i.imported:
+ continue
+ dummy = CMakeTarget({
+ 'name': i.name,
+ 'type': 'INTERFACE_LIBRARY',
+ 'sourceDirectory': self.src_dir,
+ 'buildDirectory': self.build_dir,
+ })
+ self.targets += [ConverterTarget(dummy, self.env)]
+
for i in self.trace.custom_targets:
self.custom_targets += [ConverterCustomTarget(i)]
diff --git a/mesonbuild/cmake/traceparser.py b/mesonbuild/cmake/traceparser.py
index 7db0cd3..4d68924 100644
--- a/mesonbuild/cmake/traceparser.py
+++ b/mesonbuild/cmake/traceparser.py
@@ -35,19 +35,21 @@ class CMakeTraceLine:
return s.format(self.file, self.line, self.func, self.args)
class CMakeTarget:
- def __init__(self, name, target_type, properties=None):
+ def __init__(self, name, target_type, properties=None, imported: bool = False, tline: Optional[CMakeTraceLine] = None):
if properties is None:
properties = {}
self.name = name
self.type = target_type
self.properties = properties
+ self.imported = imported
+ self.tline = tline
def __repr__(self):
- s = 'CMake TARGET:\n -- name: {}\n -- type: {}\n -- properties: {{\n{} }}'
+ s = 'CMake TARGET:\n -- name: {}\n -- type: {}\n -- imported: {}\n -- properties: {{\n{} }}\n -- tline: {}'
propSTR = ''
for i in self.properties:
propSTR += " '{}': {}\n".format(i, self.properties[i])
- return s.format(self.name, self.type, propSTR)
+ return s.format(self.name, self.type, self.imported, propSTR, self.tline)
class CMakeGeneratorTarget:
def __init__(self):
@@ -210,7 +212,7 @@ class CMakeTraceParser:
if len(args) < 1:
return self._gen_exception('add_library', 'interface library name not specified', tline)
- self.targets[args[0]] = CMakeTarget(args[0], 'INTERFACE', {})
+ self.targets[args[0]] = CMakeTarget(args[0], 'INTERFACE', {}, tline=tline, imported='IMPORTED' in args)
elif 'IMPORTED' in args:
args.remove('IMPORTED')
@@ -218,7 +220,7 @@ class CMakeTraceParser:
if len(args) < 2:
return self._gen_exception('add_library', 'requires at least 2 arguments', tline)
- self.targets[args[0]] = CMakeTarget(args[0], args[1], {})
+ self.targets[args[0]] = CMakeTarget(args[0], args[1], {}, tline=tline, imported=True)
elif 'ALIAS' in args:
args.remove('ALIAS')
@@ -227,11 +229,11 @@ class CMakeTraceParser:
return self._gen_exception('add_library', 'requires at least 2 arguments', tline)
# Simulate the ALIAS with INTERFACE_LINK_LIBRARIES
- self.targets[args[0]] = CMakeTarget(args[0], 'ALIAS', {'INTERFACE_LINK_LIBRARIES': [args[1]]})
+ self.targets[args[0]] = CMakeTarget(args[0], 'ALIAS', {'INTERFACE_LINK_LIBRARIES': [args[1]]}, tline=tline)
elif 'OBJECT' in args:
return self._gen_exception('add_library', 'OBJECT libraries are not supported', tline)
else:
- self.targets[args[0]] = CMakeTarget(args[0], 'NORMAL', {})
+ self.targets[args[0]] = CMakeTarget(args[0], 'NORMAL', {}, tline=tline)
def _cmake_add_custom_command(self, tline: CMakeTraceLine):
# DOC: https://cmake.org/cmake/help/latest/command/add_custom_command.html
@@ -300,7 +302,7 @@ class CMakeTraceParser:
if len(tline.args) < 1:
return self._gen_exception('add_custom_target', 'requires at least one argument', tline)
- self.targets[tline.args[0]] = CMakeTarget(tline.args[0], 'CUSTOM', {})
+ self.targets[tline.args[0]] = CMakeTarget(tline.args[0], 'CUSTOM', {}, tline=tline)
def _cmake_set_property(self, tline: CMakeTraceLine) -> None:
# DOC: https://cmake.org/cmake/help/latest/command/set_property.html
diff --git a/run_project_tests.py b/run_project_tests.py
index 4bfab0c0..ca0779c 100755
--- a/run_project_tests.py
+++ b/run_project_tests.py
@@ -867,6 +867,38 @@ def detect_system_compiler():
raise RuntimeError("Could not find C compiler.")
print()
+def print_tool_versions():
+ tools = [
+ {
+ 'tool': 'cmake',
+ 'args': ['--version'],
+ 'regex': re.compile(r'^cmake version ([0-9]+(\.[0-9]+)*)$'),
+ 'match_group': 1,
+ },
+ ]
+
+ def get_version(t: dict) -> str:
+ exe = shutil.which(t['tool'])
+ if not exe:
+ return 'not found'
+
+ args = [t['tool']] + t['args']
+ pc, o, e = Popen_safe(args)
+ if pc.returncode != 0:
+ return '{} (invalid {} executable)'.format(exe, t['tool'])
+ for i in o.split('\n'):
+ i = i.strip('\n\r\t ')
+ m = t['regex'].match(i)
+ if m is not None:
+ return '{} ({})'.format(exe, m.group(t['match_group']))
+
+ return '{} (unknown)'.format(exe)
+
+ max_width = max([len(x['tool']) for x in tools] + [7])
+ for tool in tools:
+ print('{0:<{2}}: {1}'.format(tool['tool'], get_version(tool), max_width))
+ print()
+
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Run the test suite of Meson.")
parser.add_argument('extra_args', nargs='*',
@@ -882,6 +914,7 @@ if __name__ == '__main__':
setup_commands(options.backend)
detect_system_compiler()
+ print_tool_versions()
script_dir = os.path.split(__file__)[0]
if script_dir != '':
os.chdir(script_dir)