diff --git a/mesonbuild/cmake/__init__.py b/mesonbuild/cmake/__init__.py index f9835a12b..5520ae369 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 f77e0cc15..9cb7f74bd 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 217247e34..70ef622ef 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 000000000..df7c73a7a --- /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 92df462cc..1c672beb4 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 7db0cd375..4d6892494 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 4bfab0c0c..ca0779ca1 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)