qt5base-lts/util/cmake/pro2cmake.py

2162 lines
82 KiB
Python
Raw Normal View History

#!/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()
Ugly fix for handling QT_SOURCE_TREE QT_SOURCE_TREE is a variable that is set in qtbase/.qmake.conf. In qtbase, it's used throughout various projects to find cpp sources when building standalone tests (among other things). Everything works fine with qmake, because even if qmake is invoked on the tests subfolder, qmake searches up the source directory tree until it finds a .qmake.conf file, and uses that. When building qttools with qmake, the qdoc project expects to have a QT_SOURCE_TREE value, but it's not actually set in the qttools/.qmake.conf file, so the generated include paths that use that value are incorrect. Curiously the build still succeeds. Now in CMake land we replaced QT_SOURCE_TREE with CMAKE_SOURCE_DIR, but that does not work properly when doing a standalone tests build, because the project in that case is the tests one, and not the qtbase one, so configuration fails in a developer build when trying to configure some private tests. So far I've found that only qtbase actively uses this value. A temporary fix is to save the qtbase source directory into a QT_SOURCE_TREE variable inside the generated BuildInternalsExtra.cmake file. The pro2cmake script is changed to handle presence of QT_SOURCE_TREE in a qrc file path. This is handled by finding the location of a .qmake.conf file starting from the project file absolute path. This is needed to stop the script from crashing when handling the mimedatabase test projects for example. The change also regenerates the relevant failing test projects, and thus standalone tests (when doing developer builds aka private_tests enabled) now configure and build successfully. Change-Id: I15adc6f4ab6e3056c43ed850196204e2229c4d98 Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
2019-07-26 16:59:53 +00:00
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 = '') -> str:
assert(target)
Ugly fix for handling QT_SOURCE_TREE QT_SOURCE_TREE is a variable that is set in qtbase/.qmake.conf. In qtbase, it's used throughout various projects to find cpp sources when building standalone tests (among other things). Everything works fine with qmake, because even if qmake is invoked on the tests subfolder, qmake searches up the source directory tree until it finds a .qmake.conf file, and uses that. When building qttools with qmake, the qdoc project expects to have a QT_SOURCE_TREE value, but it's not actually set in the qttools/.qmake.conf file, so the generated include paths that use that value are incorrect. Curiously the build still succeeds. Now in CMake land we replaced QT_SOURCE_TREE with CMAKE_SOURCE_DIR, but that does not work properly when doing a standalone tests build, because the project in that case is the tests one, and not the qtbase one, so configuration fails in a developer build when trying to configure some private tests. So far I've found that only qtbase actively uses this value. A temporary fix is to save the qtbase source directory into a QT_SOURCE_TREE variable inside the generated BuildInternalsExtra.cmake file. The pro2cmake script is changed to handle presence of QT_SOURCE_TREE in a qrc file path. This is handled by finding the location of a .qmake.conf file starting from the project file absolute path. This is needed to stop the script from crashing when handling the mimedatabase test projects for example. The change also regenerates the relevant failing test projects, and thus standalone tests (when doing developer builds aka private_tests enabled) now configure and build successfully. Change-Id: I15adc6f4ab6e3056c43ed850196204e2229c4d98 Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
2019-07-26 16:59:53 +00:00
# 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]
base_dir = os.path.join('' if base_dir == '.' else base_dir, os.path.dirname(filepath))
Ugly fix for handling QT_SOURCE_TREE QT_SOURCE_TREE is a variable that is set in qtbase/.qmake.conf. In qtbase, it's used throughout various projects to find cpp sources when building standalone tests (among other things). Everything works fine with qmake, because even if qmake is invoked on the tests subfolder, qmake searches up the source directory tree until it finds a .qmake.conf file, and uses that. When building qttools with qmake, the qdoc project expects to have a QT_SOURCE_TREE value, but it's not actually set in the qttools/.qmake.conf file, so the generated include paths that use that value are incorrect. Curiously the build still succeeds. Now in CMake land we replaced QT_SOURCE_TREE with CMAKE_SOURCE_DIR, but that does not work properly when doing a standalone tests build, because the project in that case is the tests one, and not the qtbase one, so configuration fails in a developer build when trying to configure some private tests. So far I've found that only qtbase actively uses this value. A temporary fix is to save the qtbase source directory into a QT_SOURCE_TREE variable inside the generated BuildInternalsExtra.cmake file. The pro2cmake script is changed to handle presence of QT_SOURCE_TREE in a qrc file path. This is handled by finding the location of a .qmake.conf file starting from the project file absolute path. This is needed to stop the script from crashing when handling the mimedatabase test projects for example. The change also regenerates the relevant failing test projects, and thus standalone tests (when doing developer builds aka private_tests enabled) now configure and build successfully. Change-Id: I15adc6f4ab6e3056c43ed850196204e2229c4d98 Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
2019-07-26 16:59:53 +00:00
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', '')
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)\n'.format(full_source, alias)
params = ''
if lang:
params += ' LANG\n "{}"\n'.format(lang)
if prefix:
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, 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 '<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
Ugly fix for handling QT_SOURCE_TREE QT_SOURCE_TREE is a variable that is set in qtbase/.qmake.conf. In qtbase, it's used throughout various projects to find cpp sources when building standalone tests (among other things). Everything works fine with qmake, because even if qmake is invoked on the tests subfolder, qmake searches up the source directory tree until it finds a .qmake.conf file, and uses that. When building qttools with qmake, the qdoc project expects to have a QT_SOURCE_TREE value, but it's not actually set in the qttools/.qmake.conf file, so the generated include paths that use that value are incorrect. Curiously the build still succeeds. Now in CMake land we replaced QT_SOURCE_TREE with CMAKE_SOURCE_DIR, but that does not work properly when doing a standalone tests build, because the project in that case is the tests one, and not the qtbase one, so configuration fails in a developer build when trying to configure some private tests. So far I've found that only qtbase actively uses this value. A temporary fix is to save the qtbase source directory into a QT_SOURCE_TREE variable inside the generated BuildInternalsExtra.cmake file. The pro2cmake script is changed to handle presence of QT_SOURCE_TREE in a qrc file path. This is handled by finding the location of a .qmake.conf file starting from the project file absolute path. This is needed to stop the script from crashing when handling the mimedatabase test projects for example. The change also regenerates the relevant failing test projects, and thus standalone tests (when doing developer builds aka private_tests enabled) now configure and build successfully. Change-Id: I15adc6f4ab6e3056c43ed850196204e2229c4d98 Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
2019-07-26 16:59:53 +00:00
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 ''
Ugly fix for handling QT_SOURCE_TREE QT_SOURCE_TREE is a variable that is set in qtbase/.qmake.conf. In qtbase, it's used throughout various projects to find cpp sources when building standalone tests (among other things). Everything works fine with qmake, because even if qmake is invoked on the tests subfolder, qmake searches up the source directory tree until it finds a .qmake.conf file, and uses that. When building qttools with qmake, the qdoc project expects to have a QT_SOURCE_TREE value, but it's not actually set in the qttools/.qmake.conf file, so the generated include paths that use that value are incorrect. Curiously the build still succeeds. Now in CMake land we replaced QT_SOURCE_TREE with CMAKE_SOURCE_DIR, but that does not work properly when doing a standalone tests build, because the project in that case is the tests one, and not the qtbase one, so configuration fails in a developer build when trying to configure some private tests. So far I've found that only qtbase actively uses this value. A temporary fix is to save the qtbase source directory into a QT_SOURCE_TREE variable inside the generated BuildInternalsExtra.cmake file. The pro2cmake script is changed to handle presence of QT_SOURCE_TREE in a qrc file path. This is handled by finding the location of a .qmake.conf file starting from the project file absolute path. This is needed to stop the script from crashing when handling the mimedatabase test projects for example. The change also regenerates the relevant failing test projects, and thus standalone tests (when doing developer builds aka private_tests enabled) now configure and build successfully. Change-Id: I15adc6f4ab6e3056c43ed850196204e2229c4d98 Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
2019-07-26 16:59:53 +00:00
@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.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
def find_all_remove_subdir(scope: Scope,
current_conditions: typing.FrozenSet[str]=None,
rm_subdir_conditions: typing.Dict[str, typing.Set[typing.FrozenSet[str]]]=None) -> typing.Dict[str, typing.Set[typing.FrozenSet[str]]]:
rm_subdir_conditions = rm_subdir_conditions if rm_subdir_conditions is not None else dict()
for sd in scope.get_files('SUBDIRS'):
if sd.startswith('-') and current_conditions is not None:
conditions = rm_subdir_conditions.get(sd[1:], set())
conditions.add(current_conditions)
rm_subdir_conditions[sd[1:]] = conditions
current_conditions = current_conditions if current_conditions is not None else frozenset()
for child_scope in scope.children:
assert child_scope.condition
find_all_remove_subdir(child_scope, frozenset((*current_conditions, child_scope.condition)), rm_subdir_conditions)
return rm_subdir_conditions
rm_subdir_conditions = find_all_remove_subdir(scope)
for sd in scope.get_files('SUBDIRS'):
if os.path.isdir(sd):
conditions = rm_subdir_conditions.get(sd)
cond_ind = ind
if conditions:
conditions_str = " OR ".join(sorted("(" + " AND ".join(condition) + ")" for condition in conditions))
conditions_str_wrapped = "NOT ({})".format(conditions_str)
conditions_simplified = simplify_condition(conditions_str_wrapped)
cm_fh.write(f'{ind}if({conditions_simplified})\n')
cond_ind += " "
cm_fh.write(f'{cond_ind}add_subdirectory({sd})\n')
if conditions:
cm_fh.write(f'{ind}endif()\n')
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('-'):
pass
else:
print(' XXXX: SUBDIR {} in {}: Not found.'.format(sd, scope))
for c in scope.children:
cond = c.condition
temp_buf = io.StringIO('') # we do not want to print empty conditions
handle_subdir(c, temp_buf, indent=indent + 1, is_example=is_example)
sub_call_str = temp_buf.getvalue()
if sub_call_str:
if cond == 'else':
cm_fh.write('\n{}else()\n'.format(ind))
elif cond:
cm_fh.write('\n{}if({})\n'.format(ind, cond))
cm_fh.write(sub_call_str)
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','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]
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')
qrc_output = ''
if resources:
qrc_only = True
for r in resources:
if r.endswith('.qrc'):
Ugly fix for handling QT_SOURCE_TREE QT_SOURCE_TREE is a variable that is set in qtbase/.qmake.conf. In qtbase, it's used throughout various projects to find cpp sources when building standalone tests (among other things). Everything works fine with qmake, because even if qmake is invoked on the tests subfolder, qmake searches up the source directory tree until it finds a .qmake.conf file, and uses that. When building qttools with qmake, the qdoc project expects to have a QT_SOURCE_TREE value, but it's not actually set in the qttools/.qmake.conf file, so the generated include paths that use that value are incorrect. Curiously the build still succeeds. Now in CMake land we replaced QT_SOURCE_TREE with CMAKE_SOURCE_DIR, but that does not work properly when doing a standalone tests build, because the project in that case is the tests one, and not the qtbase one, so configuration fails in a developer build when trying to configure some private tests. So far I've found that only qtbase actively uses this value. A temporary fix is to save the qtbase source directory into a QT_SOURCE_TREE variable inside the generated BuildInternalsExtra.cmake file. The pro2cmake script is changed to handle presence of QT_SOURCE_TREE in a qrc file path. This is handled by finding the location of a .qmake.conf file starting from the project file absolute path. This is needed to stop the script from crashing when handling the mimedatabase test projects for example. The change also regenerates the relevant failing test projects, and thus standalone tests (when doing developer builds aka private_tests enabled) now configure and build successfully. Change-Id: I15adc6f4ab6e3056c43ed850196204e2229c4d98 Reviewed-by: Simon Hausmann <simon.hausmann@qt.io>
2019-07-26 16:59:53 +00:00
qrc_output += process_qrc_file(target, r, scope.basedir, scope.file_absolute_path)
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_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')
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
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')
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('/','.')
extra_lines.append('URI "{}"'.format(uri))
# 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))
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))
import_name = scope.get_string('IMPORT_NAME')
if import_name:
extra_lines.append('NAME "{}"'.format(import_name))
plugindump_dep = scope.get_string('QML_PLUGINDUMP_DEPENDENCIES')
if plugindump_dep:
extra_lines.append('QML_PLUGINDUMP_DEPENDENCIES "{}"'.format(plugindump_dep))
# This is only required because of qmldir
extra_lines.append('RESOURCE_PREFIX "/qt-project.org/imports"')
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:
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
prefix = '/qt-project.org/imports/' + target_path
cm_fh.write('\n# QML Files\n')
cm_fh.write('{}add_qt_resource({} {}\n{}PREFIX\n{}"{}"\n{}FILES\n{}{}\n)\n'.format(
spaces(indent),
target,
resource_name,
spaces(indent + 1),
spaces(indent + 2),
prefix,
spaces(indent + 1),
spaces(indent + 2),
'\n{}'.format(spaces(indent + 2)).join(qml_files)))
if 'install_qml_files' in scope.get('CONFIG'):
cm_fh.write('\nqt_install_qml_files({}\n FILES\n {}\n)\n\n'.format(
target,
'\n '.join(qml_files)))
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_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
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:
config = scope.get('CONFIG')
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()