diff options
Diffstat (limited to 'util/cmake/pro2cmake.py')
-rwxr-xr-x | util/cmake/pro2cmake.py | 2342 |
1 files changed, 2342 insertions, 0 deletions
diff --git a/util/cmake/pro2cmake.py b/util/cmake/pro2cmake.py new file mode 100755 index 0000000000..26c346eef9 --- /dev/null +++ b/util/cmake/pro2cmake.py @@ -0,0 +1,2342 @@ +#!/usr/bin/env python3 +############################################################################# +## +## Copyright (C) 2018 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the plugins of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:GPL-EXCEPT$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 3 as published by the Free Software +## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + + +from __future__ import annotations + +from argparse import ArgumentParser +from textwrap import dedent +import copy +import xml.etree.ElementTree as ET +from itertools import chain +import os.path +import re +import io +import typing + +from sympy.logic import (simplify_logic, And, Or, Not,) +import pyparsing as pp +from helper import _set_up_py_parsing_nicer_debug_output +_set_up_py_parsing_nicer_debug_output(pp) + +from helper import map_qt_library, map_3rd_party_library, is_known_3rd_party_library, \ + featureName, map_platform, find_library_info_for_target, generate_find_package_info, \ + LibraryMapping + +from shutil import copyfile +from special_case_helper import SpecialCaseHandler + + +def _parse_commandline(): + parser = ArgumentParser(description='Generate CMakeLists.txt files from .' + 'pro files.') + parser.add_argument('--debug', dest='debug', action='store_true', + help='Turn on all debug output') + parser.add_argument('--debug-parser', dest='debug_parser', + action='store_true', + help='Print debug output from qmake parser.') + parser.add_argument('--debug-parse-result', dest='debug_parse_result', + action='store_true', + help='Dump the qmake parser result.') + parser.add_argument('--debug-parse-dictionary', + dest='debug_parse_dictionary', action='store_true', + help='Dump the qmake parser result as dictionary.') + parser.add_argument('--debug-pro-structure', dest='debug_pro_structure', + action='store_true', + help='Dump the structure of the qmake .pro-file.') + parser.add_argument('--debug-full-pro-structure', + dest='debug_full_pro_structure', action='store_true', + help='Dump the full structure of the qmake .pro-file ' + '(with includes).') + parser.add_argument('--debug-special-case-preservation', + dest='debug_special_case_preservation', action='store_true', + help='Show all git commands and file copies.') + + parser.add_argument('--is-example', action='store_true', + dest="is_example", + help='Treat the input .pro file as an example.') + parser.add_argument('-s', '--skip-special-case-preservation', + dest='skip_special_case_preservation', action='store_true', + help='Skips behavior to reapply ' + 'special case modifications (requires git in PATH)') + parser.add_argument('-k', '--keep-temporary-files', + dest='keep_temporary_files', action='store_true', + help='Don\'t automatically remove CMakeLists.gen.txt and other ' + 'intermediate files.') + + parser.add_argument('files', metavar='<.pro/.pri file>', type=str, + nargs='+', help='The .pro/.pri file to process') + + return parser.parse_args() + + +def find_qmake_conf(project_file_path: str = '') -> typing.Optional[str]: + if not os.path.isabs(project_file_path): + print('Warning: could not find .qmake.conf file, given path is not an absolute path: {}' + .format(project_file_path)) + return None + + cwd = os.path.dirname(project_file_path) + file_name = '.qmake.conf' + + while os.path.isdir(cwd): + maybe_file = os.path.join(cwd, file_name) + if os.path.isfile(maybe_file): + return maybe_file + else: + cwd = os.path.dirname(cwd) + + return None + + +def process_qrc_file(target: str, filepath: str, base_dir: str = '', project_file_path: str = '', skip_qtquick_compiler: bool = False, + retain_qtquick_compiler: bool = False) -> str: + assert(target) + + # Hack to handle QT_SOURCE_TREE. Assume currently that it's the same + # as the qtbase source path. + qt_source_tree_literal = '${QT_SOURCE_TREE}' + if qt_source_tree_literal in filepath: + qmake_conf = find_qmake_conf(project_file_path) + + if qmake_conf: + qt_source_tree = os.path.dirname(qmake_conf) + filepath = filepath.replace(qt_source_tree_literal, qt_source_tree) + else: + print('Warning, could not determine QT_SOURCE_TREE location while trying to find: {}' + .format(filepath)) + + + resource_name = os.path.splitext(os.path.basename(filepath))[0] + dir_name = os.path.dirname(filepath) + base_dir = os.path.join('' if base_dir == '.' else base_dir, dir_name) + + # Small not very thorough check to see if this a shared qrc resource + # pattern is mostly used by the tests. + is_parent_path = dir_name.startswith('..') + if not os.path.isfile(filepath): + raise RuntimeError('Invalid file path given to process_qrc_file: {}'.format(filepath)) + + tree = ET.parse(filepath) + root = tree.getroot() + assert(root.tag == 'RCC') + + output = '' + + resource_count = 0 + for resource in root: + assert(resource.tag == 'qresource') + lang = resource.get('lang', '') + prefix = resource.get('prefix', '/') + if not prefix.startswith('/'): + prefix = '/' + prefix + + full_resource_name = resource_name + (str(resource_count) if resource_count > 0 else '') + + files: typing.Dict[str, str] = {} + for file in resource: + path = file.text + assert path + + # Get alias: + alias = file.get('alias', '') + # In cases where examples use shared resources, we set the alias + # too the same name of the file, or the applications won't be + # be able to locate the resource + if not alias and is_parent_path: + alias = path + files[path] = alias + + output += write_add_qt_resource_call(target, full_resource_name, prefix, base_dir, lang, files, skip_qtquick_compiler, retain_qtquick_compiler) + resource_count += 1 + + return output + + +def write_add_qt_resource_call(target: str, resource_name: str, prefix: typing.Optional[str], base_dir: typing.Optional[str], + lang: typing.Optional[str], files: typing.Dict[str, str], skip_qtquick_compiler: bool, retain_qtquick_compiler: bool) -> str: + output = '' + + sorted_files = sorted(files.keys()) + + assert(sorted_files) + + for source in sorted_files: + alias = files[source] + if alias: + full_source = os.path.join(base_dir, source) + output += 'set_source_files_properties("{}"\n' \ + ' PROPERTIES QT_RESOURCE_ALIAS "{}"\n)\n'.format(full_source, alias) + + # Quote file paths in case there are spaces. + sorted_files = ['"{}"'.format(f) for f in sorted_files] + + file_list = '\n '.join(sorted_files) + output += 'set({}_resource_files\n {}\n)\n\n'.format(resource_name, file_list) + file_list = "${{{}_resource_files}}".format(resource_name) + if skip_qtquick_compiler: + output += 'set_source_files_properties(${{{}_resource_files}} PROPERTIES QT_SKIP_QUICKCOMPILER 1)\n\n'.format(resource_name) + + if retain_qtquick_compiler: + output += 'set_source_files_properties(${{{}_resource_files}} PROPERTIES QT_RETAIN_QUICKCOMPILER 1)\n\n'.format(resource_name) + + params = '' + if lang: + params += ' LANG\n "{}"\n'.format(lang) + params += ' PREFIX\n "{}"\n'.format(prefix) + if base_dir: + params += ' BASE\n "{}"\n'.format(base_dir) + output += 'add_qt_resource({} "{}"\n{} FILES\n {}\n)\n'.format(target, resource_name, + params, file_list) + + return output + + +def fixup_linecontinuation(contents: str) -> str: + # Remove all line continuations, aka a backslash followed by + # a newline character with an arbitrary amount of whitespace + # between the backslash and the newline. + # This greatly simplifies the qmake parsing grammar. + contents = re.sub(r'([^\t ])\\[ \t]*\n', '\\1 ', contents) + contents = re.sub(r'\\[ \t]*\n', '', contents) + return contents + + +def fixup_comments(contents: str) -> str: + # Get rid of completely commented out lines. + # So any line which starts with a '#' char and ends with a new line + # will be replaced by a single new line. + # + # This is needed because qmake syntax is weird. In a multi line + # assignment (separated by backslashes and newlines aka + # # \\\n ), if any of the lines are completely commented out, in + # principle the assignment should fail. + # + # It should fail because you would have a new line separating + # the previous value from the next value, and the next value would + # not be interpreted as a value, but as a new token / operation. + # qmake is lenient though, and accepts that, so we need to take + # care of it as well, as if the commented line didn't exist in the + # first place. + + contents = re.sub(r'\n#[^\n]*?\n', '\n', contents, re.DOTALL) + return contents + + +def spaces(indent: int) -> str: + return ' ' * indent + + +def trim_leading_dot(file: str) -> str: + while file.startswith('./'): + file = file[2:] + return file + + +def map_to_file(f: str, scope: Scope, *, is_include: bool = False) -> str: + assert('$$' not in f) + + if f.startswith('${'): # Some cmake variable is prepended + return f + + base_dir = scope.currentdir if is_include else scope.basedir + f = os.path.join(base_dir, f) + + return trim_leading_dot(f) + + +def handle_vpath(source: str, base_dir: str, vpath: typing.List[str]) -> str: + assert('$$' not in source) + + if not source: + return '' + + if not vpath: + return source + + if os.path.exists(os.path.join(base_dir, source)): + return source + + variable_pattern = re.compile(r'\$\{[A-Za-z0-9_]+\}') + match = re.match(variable_pattern, source) + if match: + # a complex, variable based path, skipping validation + # or resolving + return source + + for v in vpath: + fullpath = os.path.join(v, source) + if os.path.exists(fullpath): + return trim_leading_dot(os.path.relpath(fullpath, base_dir)) + + print(' XXXX: Source {}: Not found.'.format(source)) + return '{}-NOTFOUND'.format(source) + + +def handle_function_value(group: pp.ParseResults): + function_name = group[0] + function_args = group[1] + if function_name == 'qtLibraryTarget': + if len(function_args) > 1: + raise RuntimeError('Don\'t know what to with more than one function argument for $$qtLibraryTarget().') + return str(function_args[0]) + + if function_name == 'quote': + # Do nothing, just return a string result + return str(group) + + # Return the whole expression as a string. + if function_name in ['join', 'cmakeRelativePath', 'shell_quote', 'shadowed']: + return 'join({})'.format(''.join(function_args)) + + raise RuntimeError('No logic to handle function "{}", please add one in handle_function_value().'.format(function_name)) + +class Operation: + def __init__(self, value: typing.Union[typing.List[str], str]): + if isinstance(value, list): + self._value = value + else: + self._value = [str(value), ] + + def process(self, key: str, input: typing.List[str], + transformer: typing.Callable[[typing.List[str]], typing.List[str]]) -> typing.List[str]: + assert(False) + + def __repr__(self): + assert(False) + + def _dump(self): + if not self._value: + return '<NOTHING>' + + if not isinstance(self._value, list): + return '<NOT A LIST>' + + result = [] + for i in self._value: + if not i: + result.append('<NONE>') + else: + result.append(str(i)) + return '"' + '", "'.join(result) + '"' + + +class AddOperation(Operation): + def process(self, key: str, input: typing.List[str], + transformer: typing.Callable[[typing.List[str]], typing.List[str]]) -> typing.List[str]: + return input + transformer(self._value) + + def __repr__(self): + return '+({})'.format(self._dump()) + + +class UniqueAddOperation(Operation): + def process(self, key: str, input: typing.List[str], + transformer: typing.Callable[[typing.List[str]], typing.List[str]]) -> typing.List[str]: + result = input + for v in transformer(self._value): + if v not in result: + result.append(v) + return result + + def __repr__(self): + return '*({})'.format(self._dump()) + + +class SetOperation(Operation): + def process(self, key: str, input: typing.List[str], + transformer: typing.Callable[[typing.List[str]], typing.List[str]]) -> typing.List[str]: + values = [] # typing.List[str] + for v in self._value: + if v != '$${}'.format(key): + values.append(v) + else: + values += input + + if transformer: + return list(transformer(values)) + else: + return values + + def __repr__(self): + return '=({})'.format(self._dump()) + + +class RemoveOperation(Operation): + def __init__(self, value): + super().__init__(value) + + def process(self, key: str, input: typing.List[str], + transformer: typing.Callable[[typing.List[str]], typing.List[str]]) -> typing.List[str]: + input_set = set(input) + value_set = set(self._value) + result = [] + + # Add everything that is not going to get removed: + for v in input: + if v not in value_set: + result += [v,] + + # Add everything else with removal marker: + for v in transformer(self._value): + if v not in input_set: + result += ['-{}'.format(v), ] + + return result + + def __repr__(self): + return '-({})'.format(self._dump()) + + +class Scope(object): + + SCOPE_ID: int = 1 + + def __init__(self, *, + parent_scope: typing.Optional[Scope], + file: typing.Optional[str] = None, condition: str = '', + base_dir: str = '', + operations: typing.Union[ + typing.Mapping[str, typing.List[Operation]], None] = None) -> None: + if operations is None: + operations = { + 'QT_SOURCE_TREE': [SetOperation(['${QT_SOURCE_TREE}'])], + 'QT_BUILD_TREE': [SetOperation(['${PROJECT_BUILD_DIR}'])], + } + + self._operations = copy.deepcopy(operations) + if parent_scope: + parent_scope._add_child(self) + else: + self._parent = None # type: typing.Optional[Scope] + # Only add the "QT = core gui" Set operation once, on the + # very top-level .pro scope, aka it's basedir is empty. + if not base_dir: + self._operations['QT'] = [SetOperation(['core', 'gui'])] + + self._basedir = base_dir + if file: + self._currentdir = os.path.dirname(file) + if not self._currentdir: + self._currentdir = '.' + if not self._basedir: + self._basedir = self._currentdir + + self._scope_id = Scope.SCOPE_ID + Scope.SCOPE_ID += 1 + self._file = file + self._file_absolute_path = os.path.abspath(file) + self._condition = map_condition(condition) + self._children = [] # type: typing.List[Scope] + self._included_children = [] # type: typing.List[Scope] + self._visited_keys = set() # type: typing.Set[str] + self._total_condition = None # type: typing.Optional[str] + + def __repr__(self): + return '{}:{}:{}:{}:{}'.format(self._scope_id, + self._basedir, self._currentdir, + self._file, self._condition or '<TRUE>') + + def reset_visited_keys(self): + self._visited_keys = set() + + def merge(self, other: 'Scope') -> None: + assert self != other + self._included_children.append(other) + + @property + def scope_debug(self) -> bool: + merge = self.get_string('PRO2CMAKE_SCOPE_DEBUG').lower() + return merge == '1' or merge == 'on' or merge == 'yes' or merge == 'true' + + @property + def parent(self) -> typing.Optional[Scope]: + return self._parent + + @property + def basedir(self) -> str: + return self._basedir + + @property + def currentdir(self) -> str: + return self._currentdir + + def can_merge_condition(self): + if self._condition == 'else': + return False + if self._operations: + return False + + child_count = len(self._children) + if child_count == 0 or child_count > 2: + return False + assert child_count != 1 or self._children[0]._condition != 'else' + return child_count == 1 or self._children[1]._condition == 'else' + + def settle_condition(self): + new_children: typing.List[Scope] = [] + for c in self._children: + c.settle_condition() + + if c.can_merge_condition(): + child = c._children[0] + child._condition = '({}) AND ({})'.format(c._condition, child._condition) + new_children += c._children + else: + new_children.append(c) + self._children = new_children + + @staticmethod + def FromDict(parent_scope: typing.Optional['Scope'], + file: str, statements, cond: str = '', base_dir: str = '') -> Scope: + scope = Scope(parent_scope=parent_scope, file=file, condition=cond, base_dir=base_dir) + for statement in statements: + if isinstance(statement, list): # Handle skipped parts... + assert not statement + continue + + operation = statement.get('operation', None) + if operation: + key = statement.get('key', '') + value = statement.get('value', []) + assert key != '' + + if operation == '=': + scope._append_operation(key, SetOperation(value)) + elif operation == '-=': + scope._append_operation(key, RemoveOperation(value)) + elif operation == '+=': + scope._append_operation(key, AddOperation(value)) + elif operation == '*=': + scope._append_operation(key, UniqueAddOperation(value)) + else: + print('Unexpected operation "{}" in scope "{}".' + .format(operation, scope)) + assert(False) + + continue + + condition = statement.get('condition', None) + if condition: + Scope.FromDict(scope, file, + statement.get('statements'), condition, + scope.basedir) + + else_statements = statement.get('else_statements') + if else_statements: + Scope.FromDict(scope, file, else_statements, + 'else', scope.basedir) + continue + + loaded = statement.get('loaded') + if loaded: + scope._append_operation('_LOADED', UniqueAddOperation(loaded)) + continue + + option = statement.get('option', None) + if option: + scope._append_operation('_OPTION', UniqueAddOperation(option)) + continue + + included = statement.get('included', None) + if included: + scope._append_operation('_INCLUDED', + UniqueAddOperation(included)) + continue + + scope.settle_condition() + + if scope.scope_debug: + print('..... [SCOPE_DEBUG]: Created scope {}:'.format(scope)) + scope.dump(indent=1) + print('..... [SCOPE_DEBUG]: <<END OF SCOPE>>') + return scope + + def _append_operation(self, key: str, op: Operation) -> None: + if key in self._operations: + self._operations[key].append(op) + else: + self._operations[key] = [op, ] + + @property + def file(self) -> str: + return self._file or '' + + @property + def file_absolute_path(self) -> str: + return self._file_absolute_path or '' + + @property + def generated_cmake_lists_path(self) -> str: + assert self.basedir + return os.path.join(self.basedir, 'CMakeLists.gen.txt') + + @property + def original_cmake_lists_path(self) -> str: + assert self.basedir + return os.path.join(self.basedir, 'CMakeLists.txt') + + @property + def condition(self) -> str: + return self._condition + + @property + def total_condition(self) -> typing.Optional[str]: + return self._total_condition + + @total_condition.setter + def total_condition(self, condition: str) -> None: + self._total_condition = condition + + def _add_child(self, scope: 'Scope') -> None: + scope._parent = self + self._children.append(scope) + + @property + def children(self) -> typing.List['Scope']: + result = list(self._children) + for include_scope in self._included_children: + result += include_scope.children + return result + + def dump(self, *, indent: int = 0) -> None: + ind = ' ' * indent + print('{}Scope "{}":'.format(ind, self)) + if self.total_condition: + print('{} Total condition = {}'.format(ind, self.total_condition)) + print('{} Keys:'.format(ind)) + keys = self._operations.keys() + if not keys: + print('{} -- NONE --'.format(ind)) + else: + for k in sorted(keys): + print('{} {} = "{}"' + .format(ind, k, self._operations.get(k, []))) + print('{} Children:'.format(ind)) + if not self._children: + print('{} -- NONE --'.format(ind)) + else: + for c in self._children: + c.dump(indent=indent + 1) + print('{} Includes:'.format(ind)) + if not self._included_children: + print('{} -- NONE --'.format(ind)) + else: + for c in self._included_children: + c.dump(indent=indent + 1) + + def dump_structure(self, *, type: str = 'ROOT', indent: int = 0) -> None: + print('{}{}: {}'.format(spaces(indent), type, self)) + for i in self._included_children: + i.dump_structure(type='INCL', indent=indent + 1) + for i in self._children: + i.dump_structure(type='CHLD', indent=indent + 1) + + @property + def keys(self): + return self._operations.keys() + + @property + def visited_keys(self): + return self._visited_keys + + def _evalOps(self, key: str, + transformer: typing.Optional[typing.Callable[[Scope, typing.List[str]], typing.List[str]]], + result: typing.List[str], *, inherit: bool = False) \ + -> typing.List[str]: + self._visited_keys.add(key) + + # Inherrit values from above: + if self._parent and inherit: + result = self._parent._evalOps(key, transformer, result) + + if transformer: + op_transformer = lambda files: transformer(self, files) + else: + op_transformer = lambda files: files + + for op in self._operations.get(key, []): + result = op.process(key, result, op_transformer) + + for ic in self._included_children: + result = list(ic._evalOps(key, transformer, result)) + + return result + + def get(self, key: str, *, ignore_includes: bool = False, inherit: bool = False) -> typing.List[str]: + + is_same_path = self.currentdir == self.basedir + + if key == '_PRO_FILE_PWD_': + return ['${CMAKE_CURRENT_SOURCE_DIR}'] + if key == 'PWD': + if is_same_path: + return ['${CMAKE_CURRENT_SOURCE_DIR}'] + else: + return ['${CMAKE_CURRENT_SOURCE_DIR}/' + os.path.relpath(self.currentdir, self.basedir),] + if key == 'OUT_PWD': + if is_same_path: + return ['${CMAKE_CURRENT_BINARY_DIR}'] + else: + return ['${CMAKE_CURRENT_BINARY_DIR}/' + os.path.relpath(self.currentdir, self.basedir),] + + return self._evalOps(key, None, [], inherit=inherit) + + def get_string(self, key: str, default: str = '', inherit : bool = False) -> str: + v = self.get(key, inherit = inherit) + if len(v) == 0: + return default + assert len(v) == 1 + return v[0] + + def _map_files(self, files: typing.List[str], *, + use_vpath: bool = True, is_include: bool = False) -> typing.List[str]: + + expanded_files = [] # type: typing.List[str] + for f in files: + r = self._expand_value(f) + expanded_files += r + + mapped_files = list(map(lambda f: map_to_file(f, self, is_include=is_include), expanded_files)) + + if use_vpath: + result = list(map(lambda f: handle_vpath(f, self.basedir, self.get('VPATH', inherit=True)), mapped_files)) + else: + result = mapped_files + + # strip ${CMAKE_CURRENT_SOURCE_DIR}: + result = list(map(lambda f: f[28:] if f.startswith('${CMAKE_CURRENT_SOURCE_DIR}/') else f, result)) + + # strip leading ./: + result = list(map(lambda f: trim_leading_dot(f), result)) + + return result + + def get_files(self, key: str, *, use_vpath: bool = False, + is_include: bool = False) -> typing.List[str]: + transformer = lambda scope, files: scope._map_files(files, use_vpath=use_vpath, is_include=is_include) + return list(self._evalOps(key, transformer, [])) + + def _expand_value(self, value: str) -> typing.List[str]: + result = value + pattern = re.compile(r'\$\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?') + match = re.search(pattern, result) + while match: + old_result = result + if match.group(0) == value: + get_result = self.get(match.group(1), inherit = True) + if len(get_result) == 1: + result = get_result[0] + else: + return get_result + else: + replacement = self.get(match.group(1), inherit = True) + replacement_str = replacement[0] if replacement else '' + result = result[:match.start()] \ + + replacement_str \ + + result[match.end():] + + if result == old_result: + return [result,] # Do not go into infinite loop + + match = re.search(pattern, result) + return [result,] + + def expand(self, key: str) -> typing.List[str]: + value = self.get(key) + result: typing.List[str] = [] + assert isinstance(value, list) + for v in value: + result += self._expand_value(v) + return result + + def expandString(self, key: str) -> str: + result = self._expand_value(self.get_string(key)) + assert len(result) == 1 + return result[0] + + @property + def TEMPLATE(self) -> str: + return self.get_string('TEMPLATE', 'app') + + def _rawTemplate(self) -> str: + return self.get_string('TEMPLATE') + + @property + def TARGET(self) -> str: + return self.expandString('TARGET') \ + or os.path.splitext(os.path.basename(self.file))[0] + @property + def _INCLUDED(self) -> typing.List[str]: + return self.get('_INCLUDED') + + +class QmakeParser: + def __init__(self, *, debug: bool = False) -> None: + self.debug = debug + self._Grammar = self._generate_grammar() + + def _generate_grammar(self): + # Define grammar: + pp.ParserElement.setDefaultWhitespaceChars(' \t') + + def add_element(name: str, value: pp.ParserElement): + nonlocal self + if self.debug: + value.setName(name) + value.setDebug() + return value + + EOL = add_element('EOL', pp.Suppress(pp.LineEnd())) + Else = add_element('Else', pp.Keyword('else')) + Identifier = add_element('Identifier', pp.Word(pp.alphas + '_', + bodyChars=pp.alphanums+'_-./')) + BracedValue = add_element('BracedValue', + pp.nestedExpr( + ignoreExpr=pp.quotedString | + pp.QuotedString(quoteChar='$(', + endQuoteChar=')', + escQuote='\\', + unquoteResults=False) + ).setParseAction(lambda s, l, t: ['(', *t[0], ')'])) + + Substitution \ + = add_element('Substitution', + pp.Combine(pp.Literal('$') + + (((pp.Literal('$') + Identifier + + pp.Optional(pp.nestedExpr())) + | (pp.Literal('(') + Identifier + pp.Literal(')')) + | (pp.Literal('{') + Identifier + pp.Literal('}')) + | (pp.Literal('$') + pp.Literal('{') + + Identifier + pp.Optional(pp.nestedExpr()) + + pp.Literal('}')) + | (pp.Literal('$') + pp.Literal('[') + Identifier + + pp.Literal(']')) + )))) + LiteralValuePart = add_element('LiteralValuePart', + pp.Word(pp.printables, excludeChars='$#{}()')) + SubstitutionValue \ + = add_element('SubstitutionValue', + pp.Combine(pp.OneOrMore(Substitution + | LiteralValuePart + | pp.Literal('$')))) + FunctionValue \ + = add_element('FunctionValue', + pp.Group(pp.Suppress(pp.Literal('$') + pp.Literal('$')) + + Identifier + + pp.nestedExpr() #.setParseAction(lambda s, l, t: ['(', *t[0], ')']) + ).setParseAction(lambda s, l, t: handle_function_value(*t))) + Value \ + = add_element('Value', + pp.NotAny(Else | pp.Literal('}') | EOL) \ + + (pp.QuotedString(quoteChar='"', escChar='\\') + | FunctionValue + | SubstitutionValue + | BracedValue)) + + Values = add_element('Values', pp.ZeroOrMore(Value)('value')) + + Op = add_element('OP', + pp.Literal('=') | pp.Literal('-=') | pp.Literal('+=') \ + | pp.Literal('*=')) + + Key = add_element('Key', Identifier) + + Operation = add_element('Operation', Key('key') + Op('operation') + Values('value')) + CallArgs = add_element('CallArgs', pp.nestedExpr()) + + def parse_call_args(results): + out = '' + for item in chain(*results): + if isinstance(item, str): + out += item + else: + out += "(" + parse_call_args(item) + ")" + return out + + CallArgs.setParseAction(parse_call_args) + + Load = add_element('Load', pp.Keyword('load') + CallArgs('loaded')) + Include = add_element('Include', pp.Keyword('include') + CallArgs('included')) + Option = add_element('Option', pp.Keyword('option') + CallArgs('option')) + + # ignore the whole thing... + DefineTestDefinition = add_element( + 'DefineTestDefinition', + pp.Suppress(pp.Keyword('defineTest') + CallArgs + + pp.nestedExpr(opener='{', closer='}', ignoreExpr=pp.LineEnd()))) + + # ignore the whole thing... + ForLoop = add_element( + 'ForLoop', + pp.Suppress(pp.Keyword('for') + CallArgs + + pp.nestedExpr(opener='{', closer='}', ignoreExpr=pp.LineEnd()))) + + # ignore the whole thing... + ForLoopSingleLine = add_element( + 'ForLoopSingleLine', + pp.Suppress(pp.Keyword('for') + CallArgs + pp.Literal(':') + pp.SkipTo(EOL))) + + # ignore the whole thing... + FunctionCall = add_element('FunctionCall', pp.Suppress(Identifier + pp.nestedExpr())) + + Scope = add_element('Scope', pp.Forward()) + + Statement = add_element('Statement', + pp.Group(Load | Include | Option | ForLoop | ForLoopSingleLine + | DefineTestDefinition | FunctionCall | Operation)) + StatementLine = add_element('StatementLine', Statement + (EOL | pp.FollowedBy('}'))) + StatementGroup = add_element('StatementGroup', + pp.ZeroOrMore(StatementLine | Scope | pp.Suppress(EOL))) + + Block = add_element('Block', + pp.Suppress('{') + pp.Optional(EOL) + + StatementGroup + pp.Optional(EOL) + + pp.Suppress('}') + pp.Optional(EOL)) + + ConditionEnd = add_element('ConditionEnd', + pp.FollowedBy((pp.Optional(pp.White()) + + (pp.Literal(':') + | pp.Literal('{') + | pp.Literal('|'))))) + + ConditionPart1 = add_element('ConditionPart1', + (pp.Optional('!') + Identifier + pp.Optional(BracedValue))) + ConditionPart2 = add_element('ConditionPart2', pp.CharsNotIn('#{}|:=\\\n')) + ConditionPart = add_element( + 'ConditionPart', + (ConditionPart1 ^ ConditionPart2) + ConditionEnd) + + ConditionOp = add_element('ConditionOp', pp.Literal('|') ^ pp.Literal(':')) + ConditionWhiteSpace = add_element('ConditionWhiteSpace', + pp.Suppress(pp.Optional(pp.White(' ')))) + + ConditionRepeated = add_element('ConditionRepeated', + pp.ZeroOrMore(ConditionOp + + ConditionWhiteSpace + ConditionPart)) + + Condition = add_element('Condition', pp.Combine(ConditionPart + ConditionRepeated)) + Condition.setParseAction(lambda x: ' '.join(x).strip().replace(':', ' && ').strip(' && ')) + + # Weird thing like write_file(a)|error() where error() is the alternative condition + # which happens to be a function call. In this case there is no scope, but our code expects + # a scope with a list of statements, so create a fake empty statement. + ConditionEndingInFunctionCall = add_element( + 'ConditionEndingInFunctionCall', pp.Suppress(ConditionOp) + FunctionCall + + pp.Empty().setParseAction(lambda x: [[]]) + .setResultsName('statements')) + + SingleLineScope = add_element('SingleLineScope', + pp.Suppress(pp.Literal(':')) + + pp.Group(Block | (Statement + EOL))('statements')) + MultiLineScope = add_element('MultiLineScope', Block('statements')) + + SingleLineElse = add_element('SingleLineElse', + pp.Suppress(pp.Literal(':')) + + (Scope | Block | (Statement + pp.Optional(EOL)))) + MultiLineElse = add_element('MultiLineElse', Block) + ElseBranch = add_element('ElseBranch', pp.Suppress(Else) + (SingleLineElse | MultiLineElse)) + + # Scope is already add_element'ed in the forward declaration above. + Scope <<= \ + pp.Group(Condition('condition') + + (SingleLineScope | MultiLineScope | ConditionEndingInFunctionCall) + + pp.Optional(ElseBranch)('else_statements')) + + Grammar = StatementGroup('statements') + Grammar.ignore(pp.pythonStyleComment()) + + return Grammar + + def parseFile(self, file: str): + print('Parsing \"{}\"...'.format(file)) + try: + with open(file, 'r') as file_fd: + contents = file_fd.read() + + old_contents = contents + contents = fixup_comments(contents) + contents = fixup_linecontinuation(contents) + + if old_contents != contents: + print('Warning: Fixed line continuation in .pro-file!\n' + ' Position information in Parsing output might be wrong!') + result = self._Grammar.parseString(contents, parseAll=True) + except pp.ParseException as pe: + print(pe.line) + print(' '*(pe.col-1) + '^') + print(pe) + raise pe + return result + + +def parseProFile(file: str, *, debug=False): + parser = QmakeParser(debug=debug) + return parser.parseFile(file) + + +def map_condition(condition: str) -> str: + # Some hardcoded cases that are too bothersome to generalize. + condition = re.sub(r'qtConfig\(opengl\(es1\|es2\)\?\)', + r'QT_FEATURE_opengl OR QT_FEATURE_opengles2 OR QT_FEATURE_opengles3', + condition) + condition = re.sub(r'qtConfig\(opengl\.\*\)', r'QT_FEATURE_opengl', condition) + condition = re.sub(r'^win\*$', r'win', condition) + + def gcc_version_handler(match_obj: re.Match): + operator = match_obj.group(1) + version_type = match_obj.group(2) + if operator == 'equals': + operator = 'STREQUAL' + elif operator == 'greaterThan': + operator = 'STRGREATER' + elif operator == 'lessThan': + operator = 'STRLESS' + + version = match_obj.group(3) + return '(QT_COMPILER_VERSION_{} {} {})'.format(version_type, operator, version) + + # TODO: Possibly fix for other compilers. + pattern = r'(equals|greaterThan|lessThan)\(QT_GCC_([A-Z]+)_VERSION,[ ]*([0-9]+)\)' + condition = re.sub(pattern, gcc_version_handler, condition) + + # TODO: the current if(...) replacement makes the parentheses + # unbalanced when there are nested expressions. + # Need to fix this either with pypi regex recursive regexps, + # using pyparsing, or some other proper means of handling + # balanced parentheses. + condition = re.sub(r'\bif\s*\((.*?)\)', r'\1', condition) + + condition = re.sub(r'\bisEmpty\s*\((.*?)\)', r'\1_ISEMPTY', condition) + condition = re.sub(r'\bcontains\s*\((.*?),\s*"?(.*?)"?\)', + r'\1___contains___\2', condition) + condition = re.sub(r'\bequals\s*\((.*?),\s*"?(.*?)"?\)', + r'\1___equals___\2', condition) + condition = re.sub(r'\bisEqual\s*\((.*?),\s*"?(.*?)"?\)', + r'\1___equals___\2', condition) + condition = re.sub(r'\s*==\s*', '___STREQUAL___', condition) + condition = re.sub(r'\bexists\s*\((.*?)\)', r'EXISTS \1', condition) + + pattern = r'CONFIG\((debug|release),debug\|release\)' + match_result = re.match(pattern, condition) + if match_result: + build_type = match_result.group(1) + if build_type == 'debug': + build_type = 'Debug' + elif build_type == 'release': + build_type = 'Release' + condition = re.sub(pattern, '(CMAKE_BUILD_TYPE STREQUAL {})'.format(build_type), condition) + + condition = condition.replace('*', '_x_') + condition = condition.replace('.$$', '__ss_') + condition = condition.replace('$$', '_ss_') + + condition = condition.replace('!', 'NOT ') + condition = condition.replace('&&', ' AND ') + condition = condition.replace('|', ' OR ') + + cmake_condition = '' + for part in condition.split(): + # some features contain e.g. linux, that should not be + # turned upper case + feature = re.match(r"(qtConfig|qtHaveModule)\(([a-zA-Z0-9_-]+)\)", + part) + if feature: + if (feature.group(1) == "qtHaveModule"): + part = 'TARGET {}'.format(map_qt_library(feature.group(2))) + else: + feature_name = featureName(feature.group(2)) + if feature_name.startswith('system_') and is_known_3rd_party_library(feature_name[7:]): + part = 'ON' + elif feature == 'dlopen': + part = 'ON' + else: + part = 'QT_FEATURE_' + feature_name + else: + part = map_platform(part) + + part = part.replace('true', 'ON') + part = part.replace('false', 'OFF') + cmake_condition += ' ' + part + return cmake_condition.strip() + + +def handle_subdir(scope: Scope, + cm_fh: typing.IO[str], + *, + indent: int = 0, + is_example: bool = False) -> None: + + # Global nested dictionary that will contain sub_dir assignments and their conditions. + # Declared as a global in order not to pollute the nested function signatures with giant + # type hints. + sub_dirs: typing.Dict[str, typing.Dict[str, typing.Set[typing.FrozenSet[str]]]] = {} + + # Collects assignment conditions into global sub_dirs dict. + def collect_subdir_info(sub_dir_assignment: str, + *, + current_conditions: typing.FrozenSet[str] = None): + subtraction = sub_dir_assignment.startswith('-') + if subtraction: + subdir_name = sub_dir_assignment[1:] + else: + subdir_name = sub_dir_assignment + if subdir_name not in sub_dirs: + sub_dirs[subdir_name] = {} + additions = sub_dirs[subdir_name].get('additions', set()) + subtractions = sub_dirs[subdir_name].get('subtractions', set()) + if current_conditions: + if subtraction: + subtractions.add(current_conditions) + else: + additions.add(current_conditions) + if additions: + sub_dirs[subdir_name]['additions'] = additions + if subtractions: + sub_dirs[subdir_name]['subtractions'] = subtractions + + # Recursive helper that collects subdir info for given scope, + # and the children of the given scope. + def handle_subdir_helper(scope: Scope, + cm_fh: typing.IO[str], + *, + indent: int = 0, + current_conditions: typing.FrozenSet[str] = None, + is_example: bool = False): + for sd in scope.get_files('SUBDIRS'): + # Collect info about conditions and SUBDIR assignments in the + # current scope. + if os.path.isdir(sd) or sd.startswith('-'): + collect_subdir_info(sd, current_conditions=current_conditions) + # For the file case, directly write into the file handle. + elif os.path.isfile(sd): + # Handle cases with SUBDIRS += Foo/bar/z.pro. We want to be able + # to generate add_subdirectory(Foo/bar) instead of parsing the full + # .pro file in the current CMakeLists.txt. This causes issues + # with relative paths in certain projects otherwise. + dirname = os.path.dirname(sd) + if dirname: + collect_subdir_info(dirname, current_conditions=current_conditions) + else: + subdir_result = parseProFile(sd, debug=False) + subdir_scope \ + = Scope.FromDict(scope, sd, + subdir_result.asDict().get('statements'), + '', scope.basedir) + + do_include(subdir_scope) + cmakeify_scope(subdir_scope, cm_fh, indent=indent, is_example=is_example) + else: + print(' XXXX: SUBDIR {} in {}: Not found.'.format(sd, scope)) + + # Collect info about conditions and SUBDIR assignments in child + # scopes, aka recursively call the same function, but with an + # updated current_conditions frozen set. + for c in scope.children: + handle_subdir_helper(c, cm_fh, + indent=indent + 1, + is_example=is_example, + current_conditions=frozenset((*current_conditions, c.condition))) + + def group_and_print_sub_dirs(indent: int = 0): + # Simplify conditions, and group + # subdirectories with the same conditions. + grouped_sub_dirs = {} + for subdir_name in sub_dirs: + additions = sub_dirs[subdir_name].get('additions', set()) + subtractions = sub_dirs[subdir_name].get('subtractions', set()) + + # An empty condition key represents the group of sub dirs + # that should be added unconditionally. + condition_key = '' + if additions or subtractions: + addition_str = '' + subtraction_str = '' + if additions: + addition_str = " OR ".join(sorted("(" + " AND ".join(condition) + ")" + for condition in additions)) + if addition_str: + addition_str = '({})'.format(addition_str) + if subtractions: + subtraction_str = " OR ".join(sorted("(" + " AND ".join(condition) + ")" + for condition in subtractions)) + if subtraction_str: + subtraction_str = 'NOT ({})'.format(subtraction_str) + + condition_str = addition_str + if condition_str and subtraction_str: + condition_str += ' AND ' + condition_str += subtraction_str + condition_simplified = simplify_condition(condition_str) + condition_key = condition_simplified + + sub_dir_list_by_key = grouped_sub_dirs.get(condition_key, []) + sub_dir_list_by_key.append(subdir_name) + grouped_sub_dirs[condition_key] = sub_dir_list_by_key + + # Print the groups. + ind = ' ' * indent + for condition_key in grouped_sub_dirs: + cond_ind = ind + if condition_key: + cm_fh.write(f'{ind}if({condition_key})\n') + cond_ind += " " + + sub_dir_list_by_key = grouped_sub_dirs.get(condition_key, []) + for subdir_name in sub_dir_list_by_key: + cm_fh.write(f'{cond_ind}add_subdirectory({subdir_name})\n') + if condition_key: + cm_fh.write(f'{ind}endif()\n') + + # A set of conditions which will be ANDed together. The set is recreated with more conditions + # as the scope deepens. + current_conditions = frozenset() + + # Do the work. + handle_subdir_helper(scope, cm_fh, + indent=indent, + current_conditions=current_conditions, + is_example=is_example) + group_and_print_sub_dirs(indent=indent) + + +def sort_sources(sources: typing.List[str]) -> typing.List[str]: + to_sort = {} # type: typing.Dict[str, typing.List[str]] + for s in sources: + if s is None: + continue + + dir = os.path.dirname(s) + base = os.path.splitext(os.path.basename(s))[0] + if base.endswith('_p'): + base = base[:-2] + sort_name = os.path.join(dir, base) + + array = to_sort.get(sort_name, []) + array.append(s) + + to_sort[sort_name] = array + + lines = [] + for k in sorted(to_sort.keys()): + lines.append(' '.join(sorted(to_sort[k]))) + + return lines + + +def _map_libraries_to_cmake(libraries: typing.List[str], + known_libraries: typing.Set[str]) -> typing.List[str]: + result = [] # type: typing.List[str] + is_framework = False + + for l in libraries: + if l == '-framework': + is_framework = True + continue + if is_framework: + l = '${FW%s}' % l + if l.startswith('-l'): + l = l[2:] + + if l.startswith('-'): + l = '# Remove: {}'.format(l[1:]) + else: + l = map_3rd_party_library(l) + + if not l or l in result or l in known_libraries: + continue + + result.append(l) + is_framework = False + + return result + + +def extract_cmake_libraries(scope: Scope, *, known_libraries: typing.Set[str]=set()) \ + -> typing.Tuple[typing.List[str], typing.List[str]]: + public_dependencies = [] # type: typing.List[str] + private_dependencies = [] # type: typing.List[str] + + for key in ['QMAKE_USE', 'LIBS',]: + public_dependencies += scope.expand(key) + for key in ['QMAKE_USE_PRIVATE', 'QMAKE_USE_FOR_PRIVATE', 'LIBS_PRIVATE',]: + private_dependencies += scope.expand(key) + + for key in ['QT_FOR_PRIVATE','QT_PRIVATE']: + private_dependencies += [map_qt_library(q) for q in scope.expand(key)] + + for key in ['QT',]: + # Qt public libs: These may include FooPrivate in which case we get + # a private dependency on FooPrivate as well as a public dependency on Foo + for lib in scope.expand(key): + mapped_lib = map_qt_library(lib) + + if mapped_lib.endswith('Private'): + private_dependencies.append(mapped_lib) + public_dependencies.append(mapped_lib[:-7]) + else: + public_dependencies.append(mapped_lib) + + return (_map_libraries_to_cmake(public_dependencies, known_libraries), + _map_libraries_to_cmake(private_dependencies, known_libraries)) + + +def write_header(cm_fh: typing.IO[str], name: str, + typename: str, *, indent: int = 0): + cm_fh.write('{}###########################################' + '##########################\n'.format(spaces(indent))) + cm_fh.write('{}## {} {}:\n'.format(spaces(indent), name, typename)) + cm_fh.write('{}###########################################' + '##########################\n\n'.format(spaces(indent))) + + +def write_scope_header(cm_fh: typing.IO[str], *, indent: int = 0): + cm_fh.write('\n{}## Scopes:\n'.format(spaces(indent))) + cm_fh.write('{}###########################################' + '##########################\n'.format(spaces(indent))) + + +def write_list(cm_fh: typing.IO[str], entries: typing.List[str], + cmake_parameter: str, + indent: int = 0, *, + header: str = '', footer: str = ''): + if not entries: + return + + ind = spaces(indent) + extra_indent = '' + + if header: + cm_fh.write('{}{}'.format(ind, header)) + extra_indent += ' ' + if cmake_parameter: + cm_fh.write('{}{}{}\n'.format(ind, extra_indent, cmake_parameter)) + extra_indent += ' ' + for s in sort_sources(entries): + cm_fh.write('{}{}{}\n'.format(ind, extra_indent, s)) + if footer: + cm_fh.write('{}{}\n'.format(ind, footer)) + + +def write_source_file_list(cm_fh: typing.IO[str], scope, cmake_parameter: str, + keys: typing.List[str], indent: int = 0, *, + header: str = '', footer: str = ''): + # collect sources + sources: typing.List[str] = [] + for key in keys: + sources += scope.get_files(key, use_vpath=True) + + write_list(cm_fh, sources, cmake_parameter, indent, header=header, footer=footer) + + +def write_all_source_file_lists(cm_fh: typing.IO[str], scope: Scope, header: str, *, + indent: int = 0, footer: str = '', + extra_keys: typing.Optional[typing.List[str]] = None): + if extra_keys is None: + extra_keys = [] + write_source_file_list(cm_fh, scope, header, + ['SOURCES', 'HEADERS', 'OBJECTIVE_SOURCES', 'NO_PCH_SOURCES', 'FORMS'] + extra_keys, + indent, footer=footer) + + +def write_defines(cm_fh: typing.IO[str], scope: Scope, cmake_parameter: str, *, + indent: int = 0, footer: str = ''): + defines = scope.expand('DEFINES') + defines += [d[2:] for d in scope.expand('QMAKE_CXXFLAGS') if d.startswith('-D')] + defines = [d.replace('=\\\\\\"$$PWD/\\\\\\"', + '="${CMAKE_CURRENT_SOURCE_DIR}/"') for d in defines] + + if 'qml_debug' in scope.get('CONFIG'): + defines.append('QT_QML_DEBUG') + + write_list(cm_fh, defines, cmake_parameter, indent, footer=footer) + + +def write_include_paths(cm_fh: typing.IO[str], scope: Scope, cmake_parameter: str, *, + indent: int = 0, footer: str = ''): + includes = [i.rstrip('/') or ('/') for i in scope.get_files('INCLUDEPATH')] + + write_list(cm_fh, includes, cmake_parameter, indent, footer=footer) + + +def write_compile_options(cm_fh: typing.IO[str], scope: Scope, cmake_parameter: str, *, + indent: int = 0, footer: str = ''): + compile_options = [d for d in scope.expand('QMAKE_CXXFLAGS') if not d.startswith('-D')] + + write_list(cm_fh, compile_options, cmake_parameter, indent, footer=footer) + + +def write_library_section(cm_fh: typing.IO[str], scope: Scope, *, + indent: int = 0, known_libraries: typing.Set[str]=set()): + (public_dependencies, private_dependencies) \ + = extract_cmake_libraries(scope, known_libraries=known_libraries) + + write_list(cm_fh, private_dependencies, 'LIBRARIES', indent + 1) + write_list(cm_fh, public_dependencies, 'PUBLIC_LIBRARIES', indent + 1) + + +def write_autogen_section(cm_fh: typing.IO[str], scope: Scope, *, + indent: int = 0): + forms = scope.get_files('FORMS') + if forms: + write_list(cm_fh, ['uic'], 'ENABLE_AUTOGEN_TOOLS', indent) + + +def write_sources_section(cm_fh: typing.IO[str], scope: Scope, *, + indent: int = 0, known_libraries=set()): + ind = spaces(indent) + + # mark RESOURCES as visited: + scope.get('RESOURCES') + + write_all_source_file_lists(cm_fh, scope, 'SOURCES', indent=indent + 1) + + write_source_file_list(cm_fh, scope, 'DBUS_ADAPTOR_SOURCES', ['DBUS_ADAPTORS',], indent + 1) + dbus_adaptor_flags = scope.expand('QDBUSXML2CPP_ADAPTOR_HEADER_FLAGS') + if dbus_adaptor_flags: + cm_fh.write('{} DBUS_ADAPTOR_FLAGS\n'.format(ind)) + cm_fh.write('{} "{}"\n'.format(ind, '" "'.join(dbus_adaptor_flags))) + + write_source_file_list(cm_fh, scope, 'DBUS_INTERFACE_SOURCES', ['DBUS_INTERFACES',], indent + 1) + dbus_interface_flags = scope.expand('QDBUSXML2CPP_INTERFACE_HEADER_FLAGS') + if dbus_interface_flags: + cm_fh.write('{} DBUS_INTERFACE_FLAGS\n'.format(ind)) + cm_fh.write('{} "{}"\n'.format(ind, '" "'.join(dbus_interface_flags))) + + write_defines(cm_fh, scope, 'DEFINES', indent=indent + 1) + + write_include_paths(cm_fh, scope, 'INCLUDE_DIRECTORIES', indent=indent + 1) + + write_library_section(cm_fh, scope, indent=indent, known_libraries=known_libraries) + + write_compile_options(cm_fh, scope, 'COMPILE_OPTIONS', indent=indent + 1) + + write_autogen_section(cm_fh, scope, indent=indent + 1) + + link_options = scope.get('QMAKE_LFLAGS') + if link_options: + cm_fh.write('{} LINK_OPTIONS\n'.format(ind)) + for lo in link_options: + cm_fh.write('{} "{}"\n'.format(ind, lo)) + + moc_options = scope.get('QMAKE_MOC_OPTIONS') + if moc_options: + cm_fh.write('{} MOC_OPTIONS\n'.format(ind)) + for mo in moc_options: + cm_fh.write('{} "{}"\n'.format(ind, mo)) + + +def is_simple_condition(condition: str) -> bool: + return ' ' not in condition \ + or (condition.startswith('NOT ') and ' ' not in condition[4:]) + + +def write_ignored_keys(scope: Scope, indent: str) -> str: + result = '' + ignored_keys = scope.keys - scope.visited_keys + for k in sorted(ignored_keys): + if k == '_INCLUDED' or k == 'TARGET' or k == 'QMAKE_DOCS' or k == 'QT_SOURCE_TREE' \ + or k == 'QT_BUILD_TREE' or k == 'TRACEPOINT_PROVIDER': + # All these keys are actually reported already + continue + values = scope.get(k) + value_string = '<EMPTY>' if not values \ + else '"' + '" "'.join(scope.get(k)) + '"' + result += '{}# {} = {}\n'.format(indent, k, value_string) + + if result: + result = '\n#### Keys ignored in scope {}:\n{}'.format(scope, result) + + return result + + +def _iterate_expr_tree(expr, op, matches): + assert expr.func == op + keepers = () + for arg in expr.args: + if arg in matches: + matches = tuple(x for x in matches if x != arg) + elif arg == op: + (matches, extra_keepers) = _iterate_expr_tree(arg, op, matches) + keepers = (*keepers, *extra_keepers) + else: + keepers = (*keepers, arg) + return matches, keepers + + +def _simplify_expressions(expr, op, matches, replacement): + for arg in expr.args: + expr = expr.subs(arg, _simplify_expressions(arg, op, matches, + replacement)) + + if expr.func == op: + (to_match, keepers) = tuple(_iterate_expr_tree(expr, op, matches)) + if len(to_match) == 0: + # build expression with keepers and replacement: + if keepers: + start = replacement + current_expr = None + last_expr = keepers[-1] + for repl_arg in keepers[:-1]: + current_expr = op(start, repl_arg) + start = current_expr + top_expr = op(start, last_expr) + else: + top_expr = replacement + + expr = expr.subs(expr, top_expr) + + return expr + + +def _simplify_flavors_in_condition(base: str, flavors, expr): + ''' Simplify conditions based on the knownledge of which flavors + belong to which OS. ''' + base_expr = simplify_logic(base) + false_expr = simplify_logic('false') + for flavor in flavors: + flavor_expr = simplify_logic(flavor) + expr = _simplify_expressions(expr, And, (base_expr, flavor_expr,), + flavor_expr) + expr = _simplify_expressions(expr, Or, (base_expr, flavor_expr), + base_expr) + expr = _simplify_expressions(expr, And, (Not(base_expr), flavor_expr,), + false_expr) + return expr + + +def _simplify_os_families(expr, family_members, other_family_members): + for family in family_members: + for other in other_family_members: + if other in family_members: + continue # skip those in the sub-family + + f_expr = simplify_logic(family) + o_expr = simplify_logic(other) + + expr = _simplify_expressions(expr, And, (f_expr, Not(o_expr)), f_expr) + expr = _simplify_expressions(expr, And, (Not(f_expr), o_expr), o_expr) + expr = _simplify_expressions(expr, And, (f_expr, o_expr), simplify_logic('false')) + return expr + + +def _recursive_simplify(expr): + ''' Simplify the expression as much as possible based on + domain knowledge. ''' + input_expr = expr + + # Simplify even further, based on domain knowledge: + windowses = ('WIN32', 'WINRT') + apples = ('APPLE_OSX', 'APPLE_UIKIT', 'APPLE_IOS', + 'APPLE_TVOS', 'APPLE_WATCHOS',) + bsds = ('FREEBSD', 'OPENBSD', 'NETBSD',) + androids = ('ANDROID', 'ANDROID_EMBEDDED') + unixes = ('APPLE', *apples, 'BSD', *bsds, 'LINUX', + *androids, 'HAIKU', + 'INTEGRITY', 'VXWORKS', 'QNX', 'WASM') + + unix_expr = simplify_logic('UNIX') + win_expr = simplify_logic('WIN32') + false_expr = simplify_logic('false') + true_expr = simplify_logic('true') + + expr = expr.subs(Not(unix_expr), win_expr) # NOT UNIX -> WIN32 + expr = expr.subs(Not(win_expr), unix_expr) # NOT WIN32 -> UNIX + + # UNIX [OR foo ]OR WIN32 -> ON [OR foo] + expr = _simplify_expressions(expr, Or, (unix_expr, win_expr,), true_expr) + # UNIX [AND foo ]AND WIN32 -> OFF [AND foo] + expr = _simplify_expressions(expr, And, (unix_expr, win_expr,), false_expr) + + expr = _simplify_flavors_in_condition('WIN32', ('WINRT',), expr) + expr = _simplify_flavors_in_condition('APPLE', apples, expr) + expr = _simplify_flavors_in_condition('BSD', bsds, expr) + expr = _simplify_flavors_in_condition('UNIX', unixes, expr) + expr = _simplify_flavors_in_condition('ANDROID', ('ANDROID_EMBEDDED',), expr) + + # Simplify families of OSes against other families: + expr = _simplify_os_families(expr, ('WIN32', 'WINRT'), unixes) + expr = _simplify_os_families(expr, androids, unixes) + expr = _simplify_os_families(expr, ('BSD', *bsds), unixes) + + for family in ('HAIKU', 'QNX', 'INTEGRITY', 'LINUX', 'VXWORKS'): + expr = _simplify_os_families(expr, (family,), unixes) + + # Now simplify further: + expr = simplify_logic(expr) + + while expr != input_expr: + input_expr = expr + expr = _recursive_simplify(expr) + + return expr + + +def simplify_condition(condition: str) -> str: + input_condition = condition.strip() + + # Map to sympy syntax: + condition = ' ' + input_condition + ' ' + condition = condition.replace('(', ' ( ') + condition = condition.replace(')', ' ) ') + + tmp = '' + while tmp != condition: + tmp = condition + + condition = condition.replace(' NOT ', ' ~ ') + condition = condition.replace(' AND ', ' & ') + condition = condition.replace(' OR ', ' | ') + condition = condition.replace(' ON ', ' true ') + condition = condition.replace(' OFF ', ' false ') + + # SymPy chokes on expressions that contain two tokens one next to + # the other delimited by a space, which are not an operation. + # So a CMake condition like "TARGET Foo::Bar" fails the whole + # expression simplifying process. + # Turn these conditions into a single token so that SymPy can parse + # the expression, and thus simplify it. + # Do this by replacing and keeping a map of conditions to single + # token symbols. + pattern = re.compile(r'(TARGET [a-zA-Z]+::[a-zA-Z]+)') + target_symbol_mapping = {} + all_target_conditions = re.findall(pattern, condition) + for target_condition in all_target_conditions: + # Replace spaces and colons with underscores. + target_condition_symbol_name = re.sub('[ :]', '_', target_condition) + target_symbol_mapping[target_condition_symbol_name] = target_condition + condition = re.sub(target_condition, target_condition_symbol_name, condition) + + try: + # Generate and simplify condition using sympy: + condition_expr = simplify_logic(condition) + condition = str(_recursive_simplify(condition_expr)) + + # Restore the target conditions. + for symbol_name in target_symbol_mapping: + condition = re.sub(symbol_name, target_symbol_mapping[symbol_name], condition) + + # Map back to CMake syntax: + condition = condition.replace('~', 'NOT ') + condition = condition.replace('&', 'AND') + condition = condition.replace('|', 'OR') + condition = condition.replace('True', 'ON') + condition = condition.replace('False', 'OFF') + except: + # sympy did not like our input, so leave this condition alone: + condition = input_condition + + return condition or 'ON' + + +def recursive_evaluate_scope(scope: Scope, parent_condition: str = '', + previous_condition: str = '') -> str: + current_condition = scope.condition + total_condition = current_condition + if total_condition == 'else': + assert previous_condition, \ + "Else branch without previous condition in: %s" % scope.file + total_condition = 'NOT ({})'.format(previous_condition) + if parent_condition: + if not total_condition: + total_condition = parent_condition + else: + total_condition = '({}) AND ({})'.format(parent_condition, + total_condition) + + scope.total_condition = simplify_condition(total_condition) + + prev_condition = '' + for c in scope.children: + prev_condition = recursive_evaluate_scope(c, total_condition, + prev_condition) + + return current_condition + + +def map_to_cmake_condition(condition: typing.Optional[str]) -> str: + condition = condition.replace("QTDIR_build", "QT_BUILDING_QT") + condition = re.sub(r'\bQT_ARCH___equals___([a-zA-Z_0-9]*)', + r'(TEST_architecture_arch STREQUAL "\1")', condition or '') + condition = re.sub(r'\bQT_ARCH___contains___([a-zA-Z_0-9]*)', + r'(TEST_architecture_arch STREQUAL "\1")', condition or '') + return condition + + +def write_resources(cm_fh: typing.IO[str], target: str, scope: Scope, indent: int = 0): + vpath = scope.expand('VPATH') + + # Handle QRC files by turning them into add_qt_resource: + resources = scope.get_files('RESOURCES') + qtquickcompiler_skipped = scope.get_files('QTQUICK_COMPILER_SKIPPED_RESOURCES') + qtquickcompiler_retained = scope.get_files('QTQUICK_COMPILER_RETAINED_RESOURCES') + qrc_output = '' + if resources: + standalone_files: typing.List[str] = [] + for r in resources: + skip_qtquick_compiler = r in qtquickcompiler_skipped + retain_qtquick_compiler = r in qtquickcompiler_retained + if r.endswith('.qrc'): + qrc_output += process_qrc_file(target, r, scope.basedir, scope.file_absolute_path, + skip_qtquick_compiler, retain_qtquick_compiler) + else: + immediate_files = {f:"" for f in scope.get_files(r + ".files")} + if immediate_files: + immediate_prefix = scope.get(r + ".prefix") + if immediate_prefix: + immediate_prefix = immediate_prefix[0] + else: + immediate_prefix = "/" + immediate_base = scope.get(r + ".base") + immediate_lang = None + immediate_name = "qmake_" + r + qrc_output += write_add_qt_resource_call(target, immediate_name, immediate_prefix, immediate_base, immediate_lang, + immediate_files, skip_qtquick_compiler, retain_qtquick_compiler) + else: + # stadalone source file properties need to be set as they + # are parsed. + if skip_qtquick_compiler: + output += 'set_source_files_properties("{}" PROPERTIES QT_SKIP_QUICKCOMPILER 1)\n\n'.format(r) + + if retain_qtquick_compiler: + output += 'set_source_files_properties("{}" PROPERTIES QT_RETAIN_QUICKCOMPILER 1)\n\n'.format(r) + standalone_files.append(r) + + if standalone_files: + name = "qmake_immediate" + prefix = "/" + base = None + lang = None + files = {f:"" for f in standalone_files} + skip_qtquick_compiler = False + qrc_output += write_add_qt_resource_call(target, name, prefix, base, lang, files, + skip_qtquick_compiler = False, retain_qtquick_compiler = False) + + + if qrc_output: + cm_fh.write('\n# Resources:\n') + for line in qrc_output.split('\n'): + cm_fh.write(' ' * indent + line + '\n') + + +def write_extend_target(cm_fh: typing.IO[str], target: str, + scope: Scope, indent: int = 0): + ind = spaces(indent) + extend_qt_io_string = io.StringIO() + write_sources_section(extend_qt_io_string, scope) + extend_qt_string = extend_qt_io_string.getvalue() + + extend_scope = '\n{}extend_target({} CONDITION {}\n' \ + '{}{})\n'.format(ind, target, + map_to_cmake_condition(scope.total_condition), + extend_qt_string, ind) + + if not extend_qt_string: + extend_scope = '' # Nothing to report, so don't! + + cm_fh.write(extend_scope) + + write_resources(cm_fh, target, scope, indent) + +def flatten_scopes(scope: Scope) -> typing.List[Scope]: + result = [scope] # type: typing.List[Scope] + for c in scope.children: + result += flatten_scopes(c) + return result + + +def merge_scopes(scopes: typing.List[Scope]) -> typing.List[Scope]: + result = [] # type: typing.List[Scope] + + # Merge scopes with their parents: + known_scopes = {} # type: typing.Mapping[str, Scope] + for scope in scopes: + total_condition = scope.total_condition + assert total_condition + if total_condition == 'OFF': + # ignore this scope entirely! + pass + elif total_condition in known_scopes: + known_scopes[total_condition].merge(scope) + else: + # Keep everything else: + result.append(scope) + known_scopes[total_condition] = scope + + return result + + +def write_simd_part(cm_fh: typing.IO[str], target: str, scope: Scope, indent: int = 0): + simd_options = [ 'sse2', 'sse3', 'ssse3', 'sse4_1', 'sse4_2', 'aesni', 'shani', 'avx', 'avx2', + 'avx512f', 'avx512cd', 'avx512er', 'avx512pf', 'avx512dq', 'avx512bw', + 'avx512vl', 'avx512ifma', 'avx512vbmi', 'f16c', 'rdrnd', 'neon', 'mips_dsp', + 'mips_dspr2', + 'arch_haswell', 'avx512common', 'avx512core']; + for simd in simd_options: + SIMD = simd.upper(); + write_source_file_list(cm_fh, scope, 'SOURCES', + ['{}_HEADERS'.format(SIMD), + '{}_SOURCES'.format(SIMD), + '{}_C_SOURCES'.format(SIMD), + '{}_ASM'.format(SIMD)], + indent, + header = 'add_qt_simd_part({} SIMD {}\n'.format(target, simd), + footer = ')\n\n') + +def write_android_part(cm_fh: typing.IO[str], target: str, scope:Scope, indent: int = 0): + keys = [ 'ANDROID_BUNDLED_JAR_DEPENDENCIES' + , 'ANDROID_LIB_DEPENDENCIES' + , 'ANDROID_JAR_DEPENDENCIES' + , 'ANDROID_LIB_DEPENDENCY_REPLACEMENTS' + , 'ANDROID_BUNDLED_FILES' + , 'ANDROID_PERMISSIONS' ] + + has_no_values = True + for key in keys: + value = scope.get(key) + if len(value) != 0: + if has_no_values: + if scope.condition: + cm_fh.write('\n{}if(ANDROID AND ({}))\n'.format(spaces(indent), scope.condition)) + else: + cm_fh.write('\n{}if(ANDROID)\n'.format(spaces(indent))) + indent += 1 + has_no_values = False + cm_fh.write('{}set_property(TARGET {} APPEND PROPERTY QT_{}\n'.format(spaces(indent), target, key)) + write_list(cm_fh, value, '', indent + 1) + cm_fh.write('{})\n'.format(spaces(indent))) + indent -= 1 + + if not has_no_values: + cm_fh.write('{}endif()\n'.format(spaces(indent))) + +def write_main_part(cm_fh: typing.IO[str], name: str, typename: str, + cmake_function: str, scope: Scope, *, + extra_lines: typing.List[str] = [], + indent: int = 0, extra_keys: typing.List[str], + **kwargs: typing.Any): + # Evaluate total condition of all scopes: + recursive_evaluate_scope(scope) + + is_qml_plugin = any('qml_plugin' == s for s in scope.get('_LOADED')) + + if 'exceptions' in scope.get('CONFIG'): + extra_lines.append('EXCEPTIONS') + + # Get a flat list of all scopes but the main one: + scopes = flatten_scopes(scope) + total_scopes = len(scopes) + # Merge scopes based on their conditions: + scopes = merge_scopes(scopes) + + assert len(scopes) + assert scopes[0].total_condition == 'ON' + + scopes[0].reset_visited_keys() + for k in extra_keys: + scopes[0].get(k) + + # Now write out the scopes: + write_header(cm_fh, name, typename, indent=indent) + + # collect all testdata and insert globbing commands + has_test_data = False + if typename == 'Test': + test_data = scope.expand('TESTDATA') + if test_data: + has_test_data = True + cm_fh.write('# Collect test data\n') + for data in test_data: + if '*' in data: + cm_fh.write(dedent(""" + {indent}file(GLOB test_data_glob + {indent1}LIST_DIRECTORIES true + {indent1}RELATIVE ${{CMAKE_CURRENT_SOURCE_DIR}} + {indent1}"{}") + """).format( + data, + indent=spaces(indent), + indent1=spaces(indent + 1) + )) + cm_fh.write('{}list(APPEND test_data ${{test_data_glob}})\n'.format(spaces(indent))) + else: + cm_fh.write('{}list(APPEND test_data "{}")\n'.format(spaces(indent), data)) + cm_fh.write('\n') + + # Check for DESTDIR override + destdir = scope.get_string('DESTDIR') + if destdir: + if destdir.startswith('./') or destdir.startswith('../'): + destdir = '${CMAKE_CURRENT_BINARY_DIR}/' + destdir + extra_lines.append('OUTPUT_DIRECTORY "{}"'.format(destdir)) + + cm_fh.write('{}{}({}\n'.format(spaces(indent), cmake_function, name)) + for extra_line in extra_lines: + cm_fh.write('{} {}\n'.format(spaces(indent), extra_line)) + + write_sources_section(cm_fh, scopes[0], indent=indent, **kwargs) + + if has_test_data: + cm_fh.write('{} TESTDATA ${{test_data}}\n'.format(spaces(indent))) + # Footer: + cm_fh.write('{})\n'.format(spaces(indent))) + + write_resources(cm_fh, name, scope, indent) + + write_simd_part(cm_fh, name, scope, indent) + + write_android_part(cm_fh, name, scopes[0], indent) + + if is_qml_plugin: + write_qml_plugin_qml_files(cm_fh, name, scopes[0], indent) + + ignored_keys_report = write_ignored_keys(scopes[0], spaces(indent)) + if ignored_keys_report: + cm_fh.write(ignored_keys_report) + + + # Scopes: + if len(scopes) == 1: + return + + write_scope_header(cm_fh, indent=indent) + + for c in scopes[1:]: + c.reset_visited_keys() + write_android_part(cm_fh, name, c, indent=indent) + write_extend_target(cm_fh, name, c, indent=indent) + ignored_keys_report = write_ignored_keys(c, spaces(indent)) + if ignored_keys_report: + cm_fh.write(ignored_keys_report) + +def write_module(cm_fh: typing.IO[str], scope: Scope, *, + indent: int = 0) -> None: + module_name = scope.TARGET + if not module_name.startswith('Qt'): + print('XXXXXX Module name {} does not start with Qt!'.format(module_name)) + + extra = [] + + # A module should be static when 'static' is in CONFIG + # or when option(host_build) is used, as described in qt_module.prf. + is_static = 'static' in scope.get('CONFIG') or 'host_build' in scope.get('_OPTION') + + if is_static: + extra.append('STATIC') + if 'internal_module' in scope.get('CONFIG'): + extra.append('INTERNAL_MODULE') + if 'no_module_headers' in scope.get('CONFIG'): + extra.append('NO_MODULE_HEADERS') + if 'minimal_syncqt' in scope.get('CONFIG'): + extra.append('NO_SYNC_QT') + + module_config = scope.get("MODULE_CONFIG") + if len(module_config): + extra.append('QMAKE_MODULE_CONFIG {}'.format(" ".join(module_config))) + + module_plugin_types = scope.get_files('MODULE_PLUGIN_TYPES') + if module_plugin_types: + extra.append('PLUGIN_TYPES {}'.format(" ".join(module_plugin_types))) + + write_main_part(cm_fh, module_name[2:], 'Module', 'add_qt_module', scope, + extra_lines=extra, indent=indent, + known_libraries={}, extra_keys=[]) + + if 'qt_tracepoints' in scope.get('CONFIG'): + tracepoints = scope.get_files('TRACEPOINT_PROVIDER') + cm_fh.write('\n\n{}qt_create_tracepoints({} {})\n' + .format(spaces(indent), module_name[2:], ' '.join(tracepoints))) + + +def write_tool(cm_fh: typing.IO[str], scope: Scope, *, + indent: int = 0) -> None: + tool_name = scope.TARGET + + extra = ['BOOTSTRAP'] if 'force_bootstrap' in scope.get('CONFIG') else [] + + write_main_part(cm_fh, tool_name, 'Tool', 'add_qt_tool', scope, + indent=indent, known_libraries={'Qt::Core', }, + extra_lines=extra, extra_keys=['CONFIG']) + + +def write_test(cm_fh: typing.IO[str], scope: Scope, + gui: bool = False, *, indent: int = 0) -> None: + test_name = scope.TARGET + assert test_name + + extra = ['GUI',] if gui else [] + libraries={'Qt::Core', 'Qt::Test'} + + if 'qmltestcase' in scope.get('CONFIG'): + libraries.add('Qt::QmlTest') + extra.append('QMLTEST') + importpath = scope.get('IMPORTPATH') + if importpath: + qml_importpath = scope.expandString(importpath) + if qml_importpath: + extra.append('QML_IMPORTPATH "{}"'.format(qml_importpath)) + + write_main_part(cm_fh, test_name, 'Test', 'add_qt_test', scope, + indent=indent, known_libraries=libraries, + extra_lines=extra, extra_keys=[]) + + +def write_binary(cm_fh: typing.IO[str], scope: Scope, + gui: bool = False, *, indent: int = 0) -> None: + binary_name = scope.TARGET + assert binary_name + + is_qt_test_helper = 'qt_test_helper' in scope.get('_LOADED') + + extra = ['GUI'] if gui and not is_qt_test_helper else [] + cmake_function_call = 'add_qt_executable' + + if is_qt_test_helper: + binary_name += '_helper' + cmake_function_call = 'add_qt_test_helper' + + target_path = scope.get_string('target.path') + if target_path: + target_path = target_path.replace('$$[QT_INSTALL_EXAMPLES]', '${INSTALL_EXAMPLESDIR}') + extra.append('OUTPUT_DIRECTORY "{}"'.format(target_path)) + if 'target' in scope.get('INSTALLS'): + extra.append('INSTALL_DIRECTORY "{}"'.format(target_path)) + + write_main_part(cm_fh, binary_name, 'Binary', cmake_function_call, scope, + extra_lines=extra, indent=indent, + known_libraries={'Qt::Core', }, extra_keys=['target.path', 'INSTALLS']) + + +def write_find_package_section(cm_fh: typing.IO[str], + public_libs: typing.List[str], + private_libs: typing.List[str], *, indent: int=0): + packages = [] # type: typing.List[LibraryMapping] + all_libs = public_libs + private_libs + + for l in all_libs: + info = find_library_info_for_target(l) + if info and info not in packages: + packages.append(info) + + ind = spaces(indent) + + for p in packages: + cm_fh.write(generate_find_package_info(p, use_qt_find_package=False, indent=indent)) + + if packages: + cm_fh.write('\n') + + +def write_example(cm_fh: typing.IO[str], scope: Scope, + gui: bool = False, *, indent: int = 0) -> None: + binary_name = scope.TARGET + assert binary_name + + cm_fh.write('cmake_minimum_required(VERSION 3.14)\n' + + 'project({} LANGUAGES CXX)\n\n'.format(binary_name) + + 'set(CMAKE_INCLUDE_CURRENT_DIR ON)\n\n' + + 'set(CMAKE_AUTOMOC ON)\n' + + 'set(CMAKE_AUTORCC ON)\n' + + 'set(CMAKE_AUTOUIC ON)\n\n' + + 'set(INSTALL_EXAMPLEDIR "examples")\n\n') + + (public_libs, private_libs) = extract_cmake_libraries(scope) + write_find_package_section(cm_fh, public_libs, private_libs, indent=indent) + + add_executable = 'add_{}executable({}'.format("qt_gui_" if gui else "", binary_name); + + write_all_source_file_lists(cm_fh, scope, add_executable, indent=0, extra_keys=['RESOURCES']) + + cm_fh.write(')\n') + + write_include_paths(cm_fh, scope, 'target_include_directories({} PUBLIC'.format(binary_name), + indent=0, footer=')') + write_defines(cm_fh, scope, 'target_compile_definitions({} PUBLIC'.format(binary_name), + indent=0, footer=')') + write_list(cm_fh, private_libs, '', indent=indent, + header='target_link_libraries({} PRIVATE\n'.format(binary_name), footer=')') + write_list(cm_fh, public_libs, '', indent=indent, + header='target_link_libraries({} PUBLIC\n'.format(binary_name), footer=')') + write_compile_options(cm_fh, scope, 'target_compile_options({}'.format(binary_name), + indent=0, footer=')') + + cm_fh.write('\ninstall(TARGETS {}\n'.format(binary_name) + + ' RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"\n' + + ' BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"\n' + + ' LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"\n' + + ')\n') + + +def write_plugin(cm_fh, scope, *, indent: int = 0): + plugin_name = scope.TARGET + assert plugin_name + + extra = [] + + plugin_type = scope.get_string('PLUGIN_TYPE') + is_qml_plugin = any('qml_plugin' == s for s in scope.get('_LOADED')) + plugin_function_name = 'add_qt_plugin' + if plugin_type: + extra.append('TYPE {}'.format(plugin_type)) + elif is_qml_plugin: + plugin_function_name = 'add_qml_module' + write_qml_plugin(cm_fh, plugin_name, scope, indent=indent, extra_lines=extra) + + plugin_class_name = scope.get_string('PLUGIN_CLASS_NAME') + if plugin_class_name: + extra.append('CLASS_NAME {}'.format(plugin_class_name)) + + write_main_part(cm_fh, plugin_name, 'Plugin', plugin_function_name, scope, + indent=indent, extra_lines=extra, known_libraries={}, extra_keys=[]) + + +def write_qml_plugin(cm_fh: typing.IO[str], + target: str, + scope: Scope, *, + extra_lines: typing.List[str] = [], + indent: int = 0, + **kwargs: typing.Any): + # Collect other args if available + indent += 2 + scope_config = scope.get('CONFIG') + is_embedding_qml_files = False + + + sources = scope.get_files('SOURCES') + if len(sources) != 0: + extra_lines.append('CPP_PLUGIN') + + target_path = scope.get_string('TARGETPATH') + if target_path: + uri = target_path.replace('/','.') + import_name = scope.get_string('IMPORT_NAME') + # Catch special cases such as foo.QtQuick.2.bar, which when converted + # into a target path via cmake will result in foo/QtQuick/2/bar, which is + # not what we want. So we supply the target path override. + target_path_from_uri = uri.replace('.', '/') + if target_path != target_path_from_uri: + extra_lines.append('TARGET_PATH "{}"'.format(target_path)) + if import_name: + extra_lines.append('URI "{}"'.format(import_name)) + else: + uri = re.sub('\\.\\d+', '', uri) + extra_lines.append('URI "{}"'.format(uri)) + + import_version = scope.get_string('IMPORT_VERSION') + if import_version: + import_version = import_version.replace("$$QT_MINOR_VERSION","${CMAKE_PROJECT_VERSION_MINOR}") + extra_lines.append('VERSION "{}"'.format(import_version)) + + plugindump_dep = scope.get_string('QML_PLUGINDUMP_DEPENDENCIES') + + if plugindump_dep: + extra_lines.append('QML_PLUGINDUMP_DEPENDENCIES "{}"'.format(plugindump_dep)) + + +def write_qml_plugin_qml_files(cm_fh: typing.IO[str], + target: str, + scope: Scope, + indent: int = 0): + + qml_files = scope.get_files('QML_FILES', use_vpath=True) + if qml_files: + + # Quote file paths in case there are spaces. + qml_files = ['"{}"'.format(f) for f in qml_files] + + cm_fh.write('\n{}set(qml_files\n{}{}\n)\n'.format( + spaces(indent), + spaces(indent + 1), + '\n{}'.format(spaces(indent + 1)).join(qml_files))) + + target_path = scope.get_string('TARGETPATH', inherit=True) + target_path_mangled = target_path.replace('/', '_') + target_path_mangled = target_path_mangled.replace('.', '_') + resource_name = 'qmake_' + target_path_mangled + cm_fh.write('\n{}add_qt_resource({} {}\n{}FILES\n{}${{qml_files}}\n)\n'.format( + spaces(indent), + target, + resource_name, + spaces(indent + 1), + spaces(indent + 2))) + + cm_fh.write('\nqt_install_qml_files({}\n FILES ${{qml_files}}\n)\n\n'.format( + target)) + + +def handle_app_or_lib(scope: Scope, cm_fh: typing.IO[str], *, + indent: int = 0, is_example: bool=False) -> None: + assert scope.TEMPLATE in ('app', 'lib') + + config = scope.get('CONFIG') + is_lib = scope.TEMPLATE == 'lib' + is_qml_plugin = any('qml_plugin' == s for s in scope.get('_LOADED')) + is_plugin = any('qt_plugin' == s for s in scope.get('_LOADED')) or is_qml_plugin or 'plugin' in config + + if is_plugin: + assert not is_example + write_plugin(cm_fh, scope, indent=indent) + elif is_lib or 'qt_module' in scope.get('_LOADED'): + assert not is_example + write_module(cm_fh, scope, indent=indent) + elif 'qt_tool' in scope.get('_LOADED'): + assert not is_example + write_tool(cm_fh, scope, indent=indent) + else: + gui = all(val not in config for val in ['console', 'cmdline']) + if 'testcase' in config \ + or 'testlib' in config \ + or 'qmltestcase' in config: + assert not is_example + write_test(cm_fh, scope, gui, indent=indent) + else: + if is_example: + write_example(cm_fh, scope, gui, indent=indent) + else: + write_binary(cm_fh, scope, gui, indent=indent) + + ind = spaces(indent) + write_source_file_list(cm_fh, scope, '', + ['QMAKE_DOCS',], + indent, + header = 'add_qt_docs(\n', + footer = ')\n') + + +def cmakeify_scope(scope: Scope, cm_fh: typing.IO[str], *, + indent: int = 0, is_example: bool=False) -> None: + template = scope.TEMPLATE + if template == 'subdirs': + handle_subdir(scope, cm_fh, indent=indent, is_example=is_example) + elif template in ('app', 'lib'): + handle_app_or_lib(scope, cm_fh, indent=indent, is_example=is_example) + else: + print(' XXXX: {}: Template type {} not yet supported.' + .format(scope.file, template)) + + +def generate_new_cmakelists(scope: Scope, *, is_example: bool=False) -> None: + print('Generating CMakeLists.gen.txt') + with open(scope.generated_cmake_lists_path, 'w') as cm_fh: + assert scope.file + cm_fh.write('# Generated from {}.\n\n' + .format(os.path.basename(scope.file))) + cmakeify_scope(scope, cm_fh, is_example=is_example) + + +def do_include(scope: Scope, *, debug: bool = False) -> None: + for c in scope.children: + do_include(c) + + for include_file in scope.get_files('_INCLUDED', is_include=True): + if not include_file: + continue + if not os.path.isfile(include_file): + print(' XXXX: Failed to include {}.'.format(include_file)) + continue + + include_result = parseProFile(include_file, debug=debug) + include_scope \ + = Scope.FromDict(None, include_file, + include_result.asDict().get('statements'), + '', scope.basedir) # This scope will be merged into scope! + + do_include(include_scope) + + scope.merge(include_scope) + + +def copy_generated_file_to_final_location(scope: Scope, keep_temporary_files=False) -> None: + print('Copying {} to {}'.format(scope.generated_cmake_lists_path, + scope.original_cmake_lists_path)) + copyfile(scope.generated_cmake_lists_path, scope.original_cmake_lists_path) + if not keep_temporary_files: + os.remove(scope.generated_cmake_lists_path) + + +def main() -> None: + args = _parse_commandline() + + debug_parsing = args.debug_parser or args.debug + + backup_current_dir = os.getcwd() + + for file in args.files: + new_current_dir = os.path.dirname(file) + file_relative_path = os.path.basename(file) + if new_current_dir: + os.chdir(new_current_dir) + + parseresult = parseProFile(file_relative_path, debug=debug_parsing) + + if args.debug_parse_result or args.debug: + print('\n\n#### Parser result:') + print(parseresult) + print('\n#### End of parser result.\n') + if args.debug_parse_dictionary or args.debug: + print('\n\n####Parser result dictionary:') + print(parseresult.asDict()) + print('\n#### End of parser result dictionary.\n') + + file_scope = Scope.FromDict(None, file_relative_path, + parseresult.asDict().get('statements')) + + if args.debug_pro_structure or args.debug: + print('\n\n#### .pro/.pri file structure:') + file_scope.dump() + print('\n#### End of .pro/.pri file structure.\n') + + do_include(file_scope, debug=debug_parsing) + + if args.debug_full_pro_structure or args.debug: + print('\n\n#### Full .pro/.pri file structure:') + file_scope.dump() + print('\n#### End of full .pro/.pri file structure.\n') + + generate_new_cmakelists(file_scope, is_example=args.is_example) + + copy_generated_file = True + if not args.skip_special_case_preservation: + debug_special_case = args.debug_special_case_preservation or args.debug + handler = SpecialCaseHandler(file_scope.original_cmake_lists_path, + file_scope.generated_cmake_lists_path, + file_scope.basedir, + keep_temporary_files=args.keep_temporary_files, + debug=debug_special_case) + + copy_generated_file = handler.handle_special_cases() + + if copy_generated_file: + copy_generated_file_to_final_location(file_scope, + keep_temporary_files=args.keep_temporary_files) + os.chdir(backup_current_dir) + + +if __name__ == '__main__': + main() |