#!/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 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 process_qrc_file(target: str, filepath: str, base_dir: str = '') -> str: assert(target) resource_name = os.path.splitext(os.path.basename(filepath))[0] base_dir = os.path.join('' if base_dir == '.' else base_dir, os.path.dirname(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', '') 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', '') files[path] = alias 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 alias "{}")\n'.format(full_source, alias) params = '' if lang: params += ' LANG "{}"'.format(lang) if prefix: params += ' PREFIX "{}"'.format(prefix) if base_dir: params += ' BASE "{}"'.format(base_dir) output += 'add_qt_resource({} "{}"{} FILES\n {})\n'.format(target, full_resource_name, params, '\n '.join(sorted_files)) resource_count += 1 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) 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 '' if not isinstance(self._value, list): return '' result = [] for i in self._value: if not i: result.append('') 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.Mapping[str, typing.List[Operation]] = { 'QT_SOURCE_TREE': [SetOperation(['${PROJECT_SOURCE_DIR}'])], 'QT_BUILD_TREE': [SetOperation(['${PROJECT_BUILD_DIR}'])], }) -> None: if parent_scope: parent_scope._add_child(self) else: self._parent = None # type: typing.Optional[Scope] 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._condition = map_condition(condition) self._children = [] # type: typing.List[Scope] self._included_children = [] # type: typing.List[Scope] self._operations = copy.deepcopy(operations) 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 '') 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]: <>') 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 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], *, inherrit: bool = False) \ -> typing.List[str]: self._visited_keys.add(key) # Inherrit values from above: if self._parent and inherrit: 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, inherrit: bool = False) -> typing.List[str]: is_same_path = self.currentdir == self.basedir 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, [], inherrit=inherrit) def get_string(self, key: str, default: str = '') -> str: v = self.get(key) 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', inherrit=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: return self.get(match.group(1)) replacement = self.get(match.group(1)) 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.get_string('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('$')))) Value \ = add_element('Value', pp.NotAny(Else | pp.Literal('}') | EOL) \ + (pp.QuotedString(quoteChar='"', escChar='\\') | 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: ind = ' ' * indent for sd in scope.get_files('SUBDIRS'): if os.path.isdir(sd): cm_fh.write('{}add_subdirectory({})\n'.format(ind, sd)) elif os.path.isfile(sd): 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) elif sd.startswith('-'): cm_fh.write('{}### remove_subdirectory' '("{}")\n'.format(ind, sd[1:])) else: print(' XXXX: SUBDIR {} in {}: Not found.'.format(sd, scope)) for c in scope.children: cond = c.condition if cond == 'else': cm_fh.write('\n{}else()\n'.format(ind)) elif cond: cm_fh.write('\n{}if({})\n'.format(ind, cond)) handle_subdir(c, cm_fh, indent=indent + 1, is_example=is_example) if cond: cm_fh.write('{}endif()\n'.format(ind)) 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',]: 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] 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 = '' 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 ') try: # Generate and simplify condition using sympy: condition_expr = simplify_logic(condition) condition = str(_recursive_simplify(condition_expr)) # 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 = 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') qrc_output = '' if resources: qrc_only = True for r in resources: if r.endswith('.qrc'): qrc_output += process_qrc_file(target, r, scope.basedir) else: qrc_only = False if not qrc_only: print(' XXXX Ignoring non-QRC file resources.') 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_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) 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) 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) # Footer: cm_fh.write('{})\n'.format(spaces(indent))) write_resources(cm_fh, name, scope, indent) write_simd_part(cm_fh, name, scope, 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_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, *, indent: int = 0) -> None: test_name = scope.TARGET assert test_name write_main_part(cm_fh, test_name, 'Test', 'add_qt_test', scope, indent=indent, known_libraries={'Qt::Core', 'Qt::Test',}, 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 extra = ['GUI',] if gui else[] 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', 'add_qt_executable', 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') if plugin_type: extra.append('TYPE {}'.format(plugin_type)) 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', 'add_qt_plugin', scope, indent=indent, extra_lines=extra, known_libraries={}, extra_keys=[]) 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') is_lib = scope.TEMPLATE == 'lib' is_plugin = any('qt_plugin' == s for s in scope.get('_LOADED')) if is_lib or 'qt_module' in scope.get('_LOADED'): assert not is_example write_module(cm_fh, scope, indent=indent) elif is_plugin: assert not is_example write_plugin(cm_fh, scope, indent=indent) elif 'qt_tool' in scope.get('_LOADED'): assert not is_example write_tool(cm_fh, scope, indent=indent) else: if 'testcase' in scope.get('CONFIG') \ or 'testlib' in scope.get('CONFIG'): assert not is_example write_test(cm_fh, scope, indent=indent) else: config = scope.get('CONFIG') gui = all(val not in config for val in ['console', 'cmdline']) 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()