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