summaryrefslogtreecommitdiffstats
path: root/util/cmake
diff options
context:
space:
mode:
Diffstat (limited to 'util/cmake')
-rw-r--r--util/cmake/Pipfile15
-rwxr-xr-xutil/cmake/cmakeconversionrate.py127
-rwxr-xr-xutil/cmake/configurejson2cmake.py976
-rwxr-xr-xutil/cmake/generate_module_map.sh38
-rw-r--r--util/cmake/helper.py462
-rw-r--r--util/cmake/json_parser.py100
-rwxr-xr-xutil/cmake/pro2cmake.py1903
-rwxr-xr-xutil/cmake/pro_conversion_rate.py218
-rwxr-xr-xutil/cmake/run_pro2cmake.py167
-rw-r--r--util/cmake/special_case_helper.py397
-rw-r--r--util/cmake/tests/__init__.py0
-rw-r--r--util/cmake/tests/data/comment_scope.pro6
-rw-r--r--util/cmake/tests/data/complex_assign.pro2
-rw-r--r--util/cmake/tests/data/complex_condition.pro4
-rw-r--r--util/cmake/tests/data/complex_values.pro22
-rw-r--r--util/cmake/tests/data/condition_without_scope.pro2
-rw-r--r--util/cmake/tests/data/contains_scope.pro4
-rw-r--r--util/cmake/tests/data/definetest.pro6
-rw-r--r--util/cmake/tests/data/else.pro6
-rw-r--r--util/cmake/tests/data/else2.pro4
-rw-r--r--util/cmake/tests/data/else3.pro7
-rw-r--r--util/cmake/tests/data/else4.pro6
-rw-r--r--util/cmake/tests/data/else5.pro10
-rw-r--r--util/cmake/tests/data/else6.pro11
-rw-r--r--util/cmake/tests/data/else7.pro2
-rw-r--r--util/cmake/tests/data/else8.pro5
-rw-r--r--util/cmake/tests/data/escaped_value.pro2
-rw-r--r--util/cmake/tests/data/for.pro11
-rw-r--r--util/cmake/tests/data/function_if.pro4
-rw-r--r--util/cmake/tests/data/include.pro3
-rw-r--r--util/cmake/tests/data/lc.pro10
-rw-r--r--util/cmake/tests/data/lc_with_comment.pro22
-rw-r--r--util/cmake/tests/data/load.pro3
-rw-r--r--util/cmake/tests/data/multi_condition_divided_by_lc.pro3
-rw-r--r--util/cmake/tests/data/multiline_assign.pro4
-rw-r--r--util/cmake/tests/data/nested_function_calls.pro2
-rw-r--r--util/cmake/tests/data/quoted.pro5
-rw-r--r--util/cmake/tests/data/single_line_for.pro4
-rw-r--r--util/cmake/tests/data/sql.pro3
-rw-r--r--util/cmake/tests/data/standardpaths.pro17
-rw-r--r--util/cmake/tests/data/unset.pro2
-rwxr-xr-xutil/cmake/tests/test_lc_fixup.py46
-rwxr-xr-xutil/cmake/tests/test_logic_mapping.py186
-rwxr-xr-xutil/cmake/tests/test_operations.py57
-rwxr-xr-xutil/cmake/tests/test_parsing.py345
-rwxr-xr-xutil/cmake/tests/test_scope_handling.py338
46 files changed, 5567 insertions, 0 deletions
diff --git a/util/cmake/Pipfile b/util/cmake/Pipfile
new file mode 100644
index 0000000000..7fbf716eb8
--- /dev/null
+++ b/util/cmake/Pipfile
@@ -0,0 +1,15 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+pytest = "*"
+mypy = "*"
+pyparsing = "*"
+sympy = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.7"
diff --git a/util/cmake/cmakeconversionrate.py b/util/cmake/cmakeconversionrate.py
new file mode 100755
index 0000000000..3496ed1b91
--- /dev/null
+++ b/util/cmake/cmakeconversionrate.py
@@ -0,0 +1,127 @@
+#!/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 argparse import ArgumentParser
+
+import os
+import re
+import subprocess
+import sys
+import typing
+
+
+def _parse_commandline():
+ parser = ArgumentParser(description='Calculate the conversion rate to cmake.')
+ parser.add_argument('--debug', dest='debug', action='store_true',
+ help='Turn on debug output')
+ parser.add_argument('source_directory', metavar='<Source Directory>', type=str,
+ help='The Qt module source directory')
+ parser.add_argument('binary_directory', metavar='<CMake build direcotry>', type=str,
+ help='The CMake build directory (might be empty)')
+
+ return parser.parse_args()
+
+
+def calculate_baseline(source_directory: str, *, debug: bool=False) -> int:
+ if debug:
+ print('Scanning "{}" for qmake-based tests.'.format(source_directory))
+ result = subprocess.run('/usr/bin/git grep -E "^\\s*CONFIG\\s*\\+?=.*\\btestcase\\b" | sort -u | wc -l',
+ shell=True, capture_output=True, cwd=source_directory)
+ return int(result.stdout)
+
+
+def build(source_directory: str, binary_directory: str, *, debug=False) -> None:
+ abs_source = os.path.abspath(source_directory)
+ if not os.path.isdir(binary_directory):
+ os.makedirs(binary_directory)
+ if not os.path.exists(os.path.join(binary_directory, 'CMakeCache.txt')):
+
+ if debug:
+ print('Running cmake in "{}".'.format(binary_directory))
+ result = subprocess.run(['/usr/bin/cmake', '-GNinja', abs_source], cwd=binary_directory)
+ if debug:
+ print('CMake return code: {}.'.format(result.returncode))
+
+ assert result.returncode == 0
+
+ if debug:
+ print('Running ninja in "{}".'.format(binary_directory))
+ result = subprocess.run('/usr/bin/ninja', cwd=binary_directory)
+ if debug:
+ print('Ninja return code: {}.'.format(result.returncode))
+
+ assert result.returncode == 0
+
+
+def test(binary_directory: str, *, debug=False) -> typing.Tuple[int, int]:
+ if debug:
+ print('Running ctest in "{}".'.format(binary_directory))
+ result = subprocess.run('/usr/bin/ctest -j 250 | grep "tests passed, "',
+ shell=True, capture_output=True, cwd=binary_directory)
+ summary = result.stdout.decode('utf-8').replace('\n', '')
+ if debug:
+ print('Test summary: {} ({}).'.format(summary, result.returncode))
+
+ matches = re.fullmatch(r'\d+% tests passed, (\d+) tests failed out of (\d+)', summary)
+ if matches:
+ if debug:
+ print('Matches: failed {}, total {}.'.format(matches.group(1), matches.group(2)))
+ return (int(matches.group(2)), int(matches.group(2)) - int(matches.group(1)), )
+
+ return (0, 0,)
+
+
+def main() -> int:
+ args = _parse_commandline()
+
+ base_line = calculate_baseline(args.source_directory, debug=args.debug)
+ if base_line <= 0:
+ print('Could not find the qmake baseline in {}.'.format(args.source_directory))
+ return 1
+
+ if args.debug:
+ print('qmake baseline: {} test binaries.'.format(base_line))
+
+ cmake_total = 0
+ cmake_success = 0
+ try:
+ build(args.source_directory, args.binary_directory, debug=args.debug)
+ (cmake_total, cmake_success, ) = test(args.binary_directory, debug=args.debug)
+ finally:
+ if cmake_total == 0:
+ print('\n\n\nCould not calculate the cmake state.')
+ return 2
+ else:
+ print('\n\n\nCMake test conversion rate: {:.2%}.'.format(cmake_total / base_line))
+ print('CMake test success rate : {:.2%}.'.format(cmake_success / base_line))
+ return 0
+
+
+if __name__ == '__main__':
+ main()
diff --git a/util/cmake/configurejson2cmake.py b/util/cmake/configurejson2cmake.py
new file mode 100755
index 0000000000..f929ac142d
--- /dev/null
+++ b/util/cmake/configurejson2cmake.py
@@ -0,0 +1,976 @@
+#!/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$
+##
+#############################################################################
+
+import json_parser
+import os.path
+import re
+import sys
+from typing import Set, Union, List, Dict
+
+from helper import map_qt_library, featureName, map_platform, \
+ find_3rd_party_library_mapping, generate_find_package_info
+
+knownTests = set() # type: Set[str]
+
+
+class LibraryMapping:
+ def __init__(self, package: str, resultVariable: str, appendFoundSuffix: bool = True) -> None:
+ self.package = package
+ self.resultVariable = resultVariable
+ self.appendFoundSuffix = appendFoundSuffix
+
+def map_tests(test: str) -> str:
+ testmap = {
+ 'c++11': '$<COMPILE_FEATURES:cxx_std_11>',
+ 'c++14': '$<COMPILE_FEATURES:cxx_std_14>',
+ 'c++1z': '$<COMPILE_FEATURES:cxx_std_17>',
+ 'c99': '$<COMPILE_FEATURES:c_std_99>',
+ 'c11': '$<COMPILE_FEATURES:c_std_11>',
+
+ 'x86SimdAlways': 'ON', # FIXME: Make this actually do a compile test.
+
+ 'aesni': 'TEST_subarch_aes',
+ 'avx': 'TEST_subarch_avx',
+ 'avx2': 'TEST_subarch_avx2',
+ 'avx512f': 'TEST_subarch_avx512f',
+ 'avx512cd': 'TEST_subarch_avx512cd',
+ 'avx512dq': 'TEST_subarch_avx512dq',
+ 'avx512bw': 'TEST_subarch_avx512bw',
+ 'avx512er': 'TEST_subarch_avx512er',
+ 'avx512pf': 'TEST_subarch_avx512pf',
+ 'avx512vl': 'TEST_subarch_avx512vl',
+ 'avx512ifma': 'TEST_subarch_avx512ifma',
+ 'avx512vbmi': 'TEST_subarch_avx512vbmi',
+ 'avx512vbmi2': 'TEST_subarch_avx512vbmi2',
+ 'avx512vpopcntdq': 'TEST_subarch_avx512vpopcntdq',
+ 'avx5124fmaps': 'TEST_subarch_avx5124fmaps',
+ 'avx5124vnniw': 'TEST_subarch_avx5124vnniw',
+ 'bmi': 'TEST_subarch_bmi',
+ 'bmi2': 'TEST_subarch_bmi2',
+ 'cx16': 'TEST_subarch_cx16',
+ 'f16c': 'TEST_subarch_f16c',
+ 'fma': 'TEST_subarch_fma',
+ 'fma4': 'TEST_subarch_fma4',
+ 'fsgsbase': 'TEST_subarch_fsgsbase',
+ 'gfni': 'TEST_subarch_gfni',
+ 'ibt': 'TEST_subarch_ibt',
+ 'lwp': 'TEST_subarch_lwp',
+ 'lzcnt': 'TEST_subarch_lzcnt',
+ 'mmx': 'TEST_subarch_mmx',
+ 'movbe': 'TEST_subarch_movbe',
+ 'mpx': 'TEST_subarch_mpx',
+ 'no-sahf': 'TEST_subarch_no_shaf',
+ 'pclmul': 'TEST_subarch_pclmul',
+ 'popcnt': 'TEST_subarch_popcnt',
+ 'prefetchwt1': 'TEST_subarch_prefetchwt1',
+ 'prfchw': 'TEST_subarch_prfchw',
+ 'pdpid': 'TEST_subarch_rdpid',
+ 'rdpid': 'TEST_subarch_rdpid',
+ 'rdseed': 'TEST_subarch_rdseed',
+ 'rdrnd': 'TEST_subarch_rdseed', # FIXME: Is this the right thing?
+ 'rtm': 'TEST_subarch_rtm',
+ 'shani': 'TEST_subarch_sha',
+ 'shstk': 'TEST_subarch_shstk',
+ 'sse2': 'TEST_subarch_sse2',
+ 'sse3': 'TEST_subarch_sse3',
+ 'ssse3': 'TEST_subarch_ssse3',
+ 'sse4a': 'TEST_subarch_sse4a',
+ 'sse4_1': 'TEST_subarch_sse4_1',
+ 'sse4_2': 'TEST_subarch_sse4_2',
+ 'tbm': 'TEST_subarch_tbm',
+ 'xop': 'TEST_subarch_xop',
+
+ 'neon': 'TEST_subarch_neon',
+ 'iwmmxt': 'TEST_subarch_iwmmxt',
+ 'crc32': 'TEST_subarch_crc32',
+
+ 'vis': 'TEST_subarch_vis',
+ 'vis2': 'TEST_subarch_vis2',
+ 'vis3': 'TEST_subarch_vis3',
+
+ 'dsp': 'TEST_subarch_dsp',
+ 'dspr2': 'TEST_subarch_dspr2',
+
+ 'altivec': 'TEST_subarch_altivec',
+ 'spe': 'TEST_subarch_spe',
+ 'vsx': 'TEST_subarch_vsx',
+
+ 'posix-iconv': 'TEST_posix_iconv',
+ 'sun-iconv': 'TEST_sun_iconv',
+
+ 'openssl11': '(OPENSSL_VERSION VERSION_GREATER_EQUAL "1.1.0")',
+
+ 'reduce_exports': 'CMAKE_CXX_COMPILE_OPTIONS_VISIBILITY',
+
+ 'libinput_axis_api': 'ON',
+ "xlib": "X11_FOUND",
+ }
+ if test in testmap:
+ return testmap.get(test, None)
+ if test in knownTests:
+ return 'TEST_{}'.format(featureName(test))
+ return None
+
+
+def cm(ctx, *output):
+ txt = ctx['output']
+ if txt != '' and not txt.endswith('\n'):
+ txt += '\n'
+ txt += '\n'.join(output)
+
+ ctx['output'] = txt
+ return ctx
+
+
+def readJsonFromDir(dir):
+ path = os.path.join(dir, 'configure.json')
+
+ print('Reading {}...'.format(path))
+ assert os.path.exists(path)
+
+ parser = json_parser.QMakeSpecificJSONParser()
+ return parser.parse(path)
+
+
+def processFiles(ctx, data):
+ print(' files:')
+ if 'files' in data:
+ for (k, v) in data['files'].items():
+ ctx[k] = v
+ return ctx
+
+def parseLib(ctx, lib, data, cm_fh, cmake_find_packages_set):
+ newlib = find_3rd_party_library_mapping(lib)
+ if not newlib:
+ print(' XXXX Unknown library "{}".'.format(lib))
+ return
+
+ if newlib.packageName is None:
+ print(' **** Skipping library "{}" -- was masked.'.format(lib))
+ return
+
+ print(' mapped library {} to {}.'.format(lib, newlib.targetName))
+
+ # Avoid duplicate find_package calls.
+ if newlib.targetName in cmake_find_packages_set:
+ return
+
+ # If certain libraries are used within a feature, but the feature
+ # is only emitted conditionally with a simple condition (like
+ # 'on Windows' or 'on Linux'), we should enclose the find_package
+ # call for the library into the same condition.
+ emit_if = newlib.emit_if
+
+ # Only look through features if a custom emit_if wasn't provided.
+ if not emit_if:
+ for feature in data['features']:
+ feature_data = data['features'][feature]
+ if 'condition' in feature_data and \
+ 'libs.{}'.format(lib) in feature_data['condition'] and \
+ 'emitIf' in feature_data and \
+ 'config.' in feature_data['emitIf']:
+ emit_if = feature_data['emitIf']
+ break
+
+ if emit_if:
+ emit_if = map_condition(emit_if)
+
+ cmake_find_packages_set.add(newlib.targetName)
+
+ cm_fh.write(generate_find_package_info(newlib, emit_if=emit_if))
+
+
+def lineify(label, value, quote=True):
+ if value:
+ if quote:
+ return ' {} "{}"\n'.format(label, value.replace('"', '\\"'))
+ return ' {} {}\n'.format(label, value)
+ return ''
+
+def map_condition(condition):
+ # Handle NOT:
+ if isinstance(condition, list):
+ condition = '(' + ') AND ('.join(condition) + ')'
+ if isinstance(condition, bool):
+ if condition:
+ return 'ON'
+ else:
+ return 'OFF'
+ assert isinstance(condition, str)
+
+ mapped_features = {
+ 'gbm': 'gbm_FOUND',
+ "system-xcb": "ON",
+ "system-freetype": "ON",
+ 'system-pcre2': 'ON',
+ }
+
+ # Turn foo != "bar" into (NOT foo STREQUAL 'bar')
+ condition = re.sub(r"(.+)\s*!=\s*('.+')", '(! \\1 == \\2)', condition)
+
+ condition = condition.replace('!', 'NOT ')
+ condition = condition.replace('&&', ' AND ')
+ condition = condition.replace('||', ' OR ')
+ condition = condition.replace('==', ' STREQUAL ')
+
+ # explicitly handle input.sdk == '':
+ condition = re.sub(r"input\.sdk\s*==\s*''", 'NOT INPUT_SDK', condition)
+
+ last_pos = 0
+ mapped_condition = ''
+ has_failed = False
+ for match in re.finditer(r'([a-zA-Z0-9_]+)\.([a-zA-Z0-9_+-]+)', condition):
+ substitution = None
+ appendFoundSuffix = True
+ if match.group(1) == 'libs':
+ libmapping = find_3rd_party_library_mapping(match.group(2))
+
+ if libmapping and libmapping.packageName:
+ substitution = libmapping.packageName
+ if libmapping.resultVariable:
+ substitution = libmapping.resultVariable
+ if libmapping.appendFoundSuffix:
+ substitution += '_FOUND'
+
+ elif match.group(1) == 'features':
+ feature = match.group(2)
+ if feature in mapped_features:
+ substitution = mapped_features.get(feature)
+ else:
+ substitution = 'QT_FEATURE_{}'.format(featureName(match.group(2)))
+
+ elif match.group(1) == 'subarch':
+ substitution = 'TEST_arch_{}_subarch_{}'.format("${TEST_architecture_arch}",
+ match.group(2))
+
+ elif match.group(1) == 'call':
+ if match.group(2) == 'crossCompile':
+ substitution = 'CMAKE_CROSSCOMPILING'
+
+ elif match.group(1) == 'tests':
+ substitution = map_tests(match.group(2))
+
+ elif match.group(1) == 'input':
+ substitution = 'INPUT_{}'.format(featureName(match.group(2)))
+
+ elif match.group(1) == 'config':
+ substitution = map_platform(match.group(2))
+ elif match.group(1) == 'module':
+ substitution = 'TARGET {}'.format(map_qt_library(match.group(2)))
+
+ elif match.group(1) == 'arch':
+ if match.group(2) == 'i386':
+ # FIXME: Does this make sense?
+ substitution = '(TEST_architecture_arch STREQUAL i386)'
+ elif match.group(2) == 'x86_64':
+ substitution = '(TEST_architecture_arch STREQUAL x86_64)'
+ elif match.group(2) == 'arm':
+ # FIXME: Does this make sense?
+ substitution = '(TEST_architecture_arch STREQUAL arm)'
+ elif match.group(2) == 'arm64':
+ # FIXME: Does this make sense?
+ substitution = '(TEST_architecture_arch STREQUAL arm64)'
+ elif match.group(2) == 'mips':
+ # FIXME: Does this make sense?
+ substitution = '(TEST_architecture_arch STREQUAL mips)'
+
+ if substitution is None:
+ print(' XXXX Unknown condition "{}".'.format(match.group(0)))
+ has_failed = True
+ else:
+ mapped_condition += condition[last_pos:match.start(1)] + substitution
+ last_pos = match.end(2)
+
+ mapped_condition += condition[last_pos:]
+
+ # Space out '(' and ')':
+ mapped_condition = mapped_condition.replace('(', ' ( ')
+ mapped_condition = mapped_condition.replace(')', ' ) ')
+
+ # Prettify:
+ condition = re.sub('\\s+', ' ', mapped_condition)
+ condition = condition.strip()
+
+ if has_failed:
+ condition += ' OR FIXME'
+
+ return condition
+
+
+def parseInput(ctx, input, data, cm_fh):
+ skip_inputs = {
+ "prefix", "hostprefix", "extprefix",
+
+ "archdatadir", "bindir", "datadir", "docdir",
+ "examplesdir", "external-hostbindir", "headerdir",
+ "hostbindir", "hostdatadir", "hostlibdir",
+ "importdir", "libdir", "libexecdir",
+ "plugindir", "qmldir", "settingsdir",
+ "sysconfdir", "testsdir", "translationdir",
+
+ "android-arch", "android-ndk", "android-ndk-host",
+ "android-ndk-platform", "android-sdk",
+ "android-toolchain-version", "android-style-assets",
+
+ "appstore-compliant",
+
+ "avx", "avx2", "avx512", "c++std", "ccache", "commercial",
+ "compile-examples", "confirm-license",
+ "dbus",
+ "dbus-runtime",
+
+ "debug", "debug-and-release",
+
+ "developer-build",
+
+ "device", "device-option",
+
+ "f16c",
+
+ "force-asserts", "force-debug-info", "force-pkg-config",
+ "framework",
+
+ "gc-binaries",
+
+ "gdb-index",
+
+ "gcc-sysroot",
+
+ "gcov",
+
+ "gnumake",
+
+ "gui",
+
+ "harfbuzz",
+
+ "headersclean",
+
+ "incredibuild-xge",
+
+ "libudev",
+ "ltcg",
+ "make",
+ "make-tool",
+
+ "mips_dsp",
+ "mips_dspr2",
+ "mp",
+
+ "nomake",
+
+ "opensource",
+
+ "optimize-debug", "optimize-size", "optimized-qmake", "optimized-tools",
+
+ "pch",
+
+ "pkg-config",
+
+ "platform",
+
+ "plugin-manifests",
+ "profile",
+ "qreal",
+
+ "reduce-exports", "reduce-relocations",
+
+ "release",
+
+ "rpath",
+
+ "sanitize",
+
+ "sdk",
+
+ "separate-debug-info",
+
+ "shared",
+
+ "silent",
+
+ "qdbus",
+
+ "sse2",
+ "sse3",
+ "sse4.1",
+ "sse4.2",
+ "ssse3",
+ "static",
+ "static-runtime",
+ "strip",
+ "syncqt",
+ "sysroot",
+ "testcocoon",
+ "use-gold-linker",
+ "warnings-are-errors",
+ "Werror",
+ "widgets",
+ "xplatform",
+ "zlib",
+
+ "doubleconversion",
+
+ "eventfd",
+ "glib",
+ "icu",
+ "inotify",
+ "journald",
+ "pcre",
+ "posix-ipc",
+ "pps",
+ "slog2",
+ "syslog",
+
+ "sqlite",
+ }
+
+ if input in skip_inputs:
+ print(' **** Skipping input {}: masked.'.format(input))
+ return
+
+ type = data
+ if isinstance(data, dict):
+ type = data["type"]
+
+ if type == "boolean":
+ print(' **** Skipping boolean input {}: masked.'.format(input))
+ return
+
+ if type == "enum":
+ cm_fh.write("# input {}\n".format(input))
+ cm_fh.write('set(INPUT_{} "undefined" CACHE STRING "")\n'.format(featureName(input)))
+ cm_fh.write('set_property(CACHE INPUT_{} PROPERTY STRINGS undefined {})\n\n'.format(featureName(input), " ".join(data["values"])))
+ return
+
+ print(' XXXX UNHANDLED INPUT TYPE {} in input description'.format(type))
+ return
+
+
+# "tests": {
+# "cxx11_future": {
+# "label": "C++11 <future>",
+# "type": "compile",
+# "test": {
+# "include": "future",
+# "main": [
+# "std::future<int> f = std::async([]() { return 42; });",
+# "(void)f.get();"
+# ],
+# "qmake": "unix:LIBS += -lpthread"
+# }
+# },
+def parseTest(ctx, test, data, cm_fh):
+ skip_tests = {
+ 'c++11', 'c++14', 'c++1y', 'c++1z',
+ 'c11', 'c99',
+ 'gc_binaries',
+ 'posix-iconv', "sun-iconv",
+ 'precomile_header',
+ 'reduce_exports',
+ 'separate_debug_info', # FIXME: see if cmake can do this
+ 'gc_binaries',
+ 'libinput_axis_api',
+ 'xlib',
+ }
+
+ if test in skip_tests:
+ print(' **** Skipping features {}: masked.'.format(test))
+ return
+
+ if data["type"] == "compile":
+ knownTests.add(test)
+
+ details = data["test"]
+
+ if isinstance(details, str):
+ print(' XXXX UNHANDLED TEST SUB-TYPE {} in test description'.format(details))
+ return
+
+ head = details.get("head", "")
+ if isinstance(head, list):
+ head = "\n".join(head)
+
+ sourceCode = head + '\n'
+
+ include = details.get("include", "")
+ if isinstance(include, list):
+ include = '#include <' + '>\n#include <'.join(include) + '>'
+ elif include:
+ include = '#include <{}>'.format(include)
+
+ sourceCode += include + '\n'
+
+ tail = details.get("tail", "")
+ if isinstance(tail, list):
+ tail = "\n".join(tail)
+
+ sourceCode += tail + '\n'
+
+ sourceCode += "int main(int argc, char **argv)\n"
+ sourceCode += "{\n"
+ sourceCode += " (void)argc; (void)argv;\n"
+ sourceCode += " /* BEGIN TEST: */\n"
+
+ main = details.get("main", "")
+ if isinstance(main, list):
+ main = "\n".join(main)
+
+ sourceCode += main + '\n'
+
+ sourceCode += " /* END TEST: */\n"
+ sourceCode += " return 0;\n"
+ sourceCode += "}\n"
+
+ sourceCode = sourceCode.replace('"', '\\"')
+
+ librariesCmakeName = ""
+ qmakeFixme = ""
+
+ cm_fh.write("# {}\n".format(test))
+ if "qmake" in details: # We don't really have many so we can just enumerate them all
+ if details["qmake"] == "unix:LIBS += -lpthread":
+ librariesCmakeName = format(featureName(test)) + "_TEST_LIBRARIES"
+ cm_fh.write("if (UNIX)\n")
+ cm_fh.write(" set(" + librariesCmakeName + " pthread)\n")
+ cm_fh.write("endif()\n")
+ elif details["qmake"] == "linux: LIBS += -lpthread -lrt":
+ librariesCmakeName = format(featureName(test)) + "_TEST_LIBRARIES"
+ cm_fh.write("if (LINUX)\n")
+ cm_fh.write(" set(" + librariesCmakeName + " pthread rt)\n")
+ cm_fh.write("endif()\n")
+ elif details["qmake"] == "CONFIG += c++11":
+ # do nothing we're always in c++11 mode
+ pass
+ else:
+ qmakeFixme = "# FIXME: qmake: {}\n".format(details["qmake"])
+
+ if "use" in data:
+ if data["use"] == "egl xcb_xlib":
+ librariesCmakeName = format(featureName(test)) + "_TEST_LIBRARIES"
+ cm_fh.write("if (HAVE_EGL AND X11_XCB_FOUND AND X11_FOUND)\n")
+ cm_fh.write(" set(" + librariesCmakeName + " EGL::EGL X11::X11 X11::XCB)\n")
+ cm_fh.write("endif()\n")
+ else:
+ qmakeFixme += "# FIXME: use: {}\n".format(data["use"])
+
+ cm_fh.write("qt_config_compile_test({}\n".format(featureName(test)))
+ cm_fh.write(lineify("LABEL", data.get("label", "")))
+ if librariesCmakeName != "":
+ cm_fh.write(lineify("LIBRARIES", "${"+librariesCmakeName+"}"))
+ cm_fh.write(" CODE\n")
+ cm_fh.write('"' + sourceCode + '"')
+ if qmakeFixme != "":
+ cm_fh.write(qmakeFixme)
+ cm_fh.write(")\n\n")
+
+ elif data["type"] == "x86Simd":
+ knownTests.add(test)
+
+ label = data["label"]
+
+ cm_fh.write("# {}\n".format(test))
+ cm_fh.write("qt_config_compile_test_x86simd({} \"{}\")\n".format(test, label))
+ cm_fh.write("\n")
+
+# "features": {
+# "android-style-assets": {
+# "label": "Android Style Assets",
+# "condition": "config.android",
+# "output": [ "privateFeature" ],
+# "comment": "This belongs into gui, but the license check needs it here already."
+# },
+ else:
+ print(' XXXX UNHANDLED TEST TYPE {} in test description'.format(data["type"]))
+
+
+def parseFeature(ctx, feature, data, cm_fh):
+ # This is *before* the feature name gets normalized! So keep - and + chars, etc.
+ feature_mapping = {
+ 'alloc_h': None, # handled by alloc target
+ 'alloc_malloc_h': None,
+ 'alloc_stdlib_h': None,
+ 'build_all': None,
+ 'c11': None,
+ 'c89': None,
+ 'c99': None,
+ 'ccache': None,
+ 'compiler-flags': None,
+ 'cross_compile': None,
+ 'debug_and_release': None,
+ 'debug': None,
+ 'dlopen': {
+ 'condition': 'UNIX',
+ },
+ 'doubleconversion': None,
+ 'enable_gdb_index': None,
+ 'enable_new_dtags': None,
+ 'force_debug_info': None,
+ 'framework': {
+ 'condition': 'APPLE AND BUILD_SHARED_LIBS',
+ },
+ 'gc_binaries': None,
+ 'gcc-sysroot': None,
+ 'gcov': None,
+ 'gnu-libiconv': {
+ 'condition': 'NOT WIN32 AND NOT QNX AND NOT ANDROID AND NOT APPLE AND TEST_posix_iconv AND NOT TEST_iconv_needlib',
+ 'enable': 'TEST_posix_iconv AND NOT TEST_iconv_needlib',
+ 'disable': 'NOT TEST_posix_iconv OR TEST_iconv_needlib',
+ },
+ 'GNUmake': None,
+ 'harfbuzz': {
+ 'condition': 'HARFBUZZ_FOUND'
+ },
+ 'host-dbus': None,
+ 'iconv': {
+ 'condition': 'NOT QT_FEATURE_icu AND QT_FEATURE_textcodec AND ( TEST_posix_iconv OR TEST_sun_iconv )'
+ },
+ 'incredibuild_xge': None,
+ 'jpeg': {
+ 'condition': 'QT_FEATURE_imageformatplugin AND JPEG_FOUND'
+ },
+ 'ltcg': None,
+ 'msvc_mp': None,
+ 'optimize_debug': None,
+ 'optimize_size': None,
+ # special case to enable implicit feature on WIN32, until ANGLE is ported
+ 'opengl-desktop': {
+ 'autoDetect': ''
+ },
+ # special case to disable implicit feature on WIN32, until ANGLE is ported
+ 'opengl-dynamic': {
+ 'autoDetect': 'OFF'
+ },
+ 'opengles2': { # special case to disable implicit feature on WIN32, until ANGLE is ported
+ 'condition': 'NOT WIN32 AND ( NOT APPLE_WATCHOS AND NOT QT_FEATURE_opengl_desktop AND GLESv2_FOUND )'
+ },
+ 'pkg-config': None,
+ 'posix_fallocate': None, # Only needed for sqlite, which we do not want to build
+ 'posix-libiconv': {
+ 'condition': 'NOT WIN32 AND NOT QNX AND NOT ANDROID AND NOT APPLE AND TEST_posix_iconv AND TEST_iconv_needlib',
+ 'enable': 'TEST_posix_iconv AND TEST_iconv_needlib',
+ 'disable': 'NOT TEST_posix_iconv OR NOT TEST_iconv_needlib',
+ },
+ 'precompile_header': None,
+ 'profile': None,
+ 'qmakeargs': None,
+ 'qpa_default_platform': None, # Not a bool!
+ 'reduce_relocations': None,
+ 'release': None,
+ 'release_tools': None,
+ 'rpath_dir': None, # rpath related
+ 'rpath': None,
+ 'sanitize_address': None, # sanitizer
+ 'sanitize_memory': None,
+ 'sanitizer': None,
+ 'sanitize_thread': None,
+ 'sanitize_undefined': None,
+ 'separate_debug_info': None,
+ 'shared': None,
+ 'silent': None,
+ 'sql-sqlite' : {
+ 'condition': 'QT_FEATURE_datestring AND SQLite3_FOUND',
+ },
+ 'stack-protector-strong': None,
+ 'static': None,
+ 'static_runtime': None,
+ 'stl': None, # Do we really need to test for this in 2018?!
+ 'strip': None,
+ 'sun-libiconv': {
+ 'condition': 'NOT WIN32 AND NOT QNX AND NOT ANDROID AND NOT APPLE AND TEST_sun_iconv',
+ 'enable': 'TEST_sun_iconv',
+ 'disable': 'NOT TEST_sun_iconv',
+ },
+ 'system-doubleconversion': None, # No system libraries anymore!
+ 'system-freetype': None,
+ 'system-harfbuzz': None,
+ 'system-jpeg': None,
+ 'system-pcre2': None,
+ 'system-png': None,
+ 'system-sqlite': None,
+ 'system-xcb': None,
+ 'system-zlib': None,
+ 'tiff': {
+ 'condition': 'QT_FEATURE_imageformatplugin AND TIFF_FOUND'
+ },
+ 'use_gold_linker': None,
+ 'verifyspec': None, # qmake specific...
+ 'warnings_are_errors': None, # FIXME: Do we need these?
+ 'webp': {
+ 'condition': 'QT_FEATURE_imageformatplugin AND WrapWebP_FOUND'
+ },
+ 'xkbcommon-system': None, # another system library, just named a bit different from the rest
+ }
+
+ mapping = feature_mapping.get(feature, {})
+
+ if mapping is None:
+ print(' **** Skipping features {}: masked.'.format(feature))
+ return
+
+ handled = { 'autoDetect', 'comment', 'condition', 'description', 'disable', 'emitIf', 'enable', 'label', 'output', 'purpose', 'section' }
+ label = mapping.get('label', data.get('label', ''))
+ purpose = mapping.get('purpose', data.get('purpose', data.get('description', label)))
+ autoDetect = map_condition(mapping.get('autoDetect', data.get('autoDetect', '')))
+ condition = map_condition(mapping.get('condition', data.get('condition', '')))
+ output = mapping.get('output', data.get('output', []))
+ comment = mapping.get('comment', data.get('comment', ''))
+ section = mapping.get('section', data.get('section', ''))
+ enable = map_condition(mapping.get('enable', data.get('enable', '')))
+ disable = map_condition(mapping.get('disable', data.get('disable', '')))
+ emitIf = map_condition(mapping.get('emitIf', data.get('emitIf', '')))
+
+ for k in [k for k in data.keys() if k not in handled]:
+ print(' XXXX UNHANDLED KEY {} in feature description'.format(k))
+
+ if not output:
+ # feature that is only used in the conditions of other features
+ output = ["internalFeature"]
+
+ publicFeature = False # #define QT_FEATURE_featurename in public header
+ privateFeature = False # #define QT_FEATURE_featurename in private header
+ negativeFeature = False # #define QT_NO_featurename in public header
+ internalFeature = False # No custom or QT_FEATURE_ defines
+ publicDefine = False # #define MY_CUSTOM_DEFINE in public header
+
+ for o in output:
+ outputType = o
+ outputArgs = {}
+ if isinstance(o, dict):
+ outputType = o['type']
+ outputArgs = o
+
+ if outputType in ['varAssign', 'varAppend', 'varRemove', 'publicQtConfig', 'privateConfig', 'publicConfig']:
+ continue
+ elif outputType == 'define':
+ publicDefine = True
+ elif outputType == 'feature':
+ negativeFeature = True
+ elif outputType == 'publicFeature':
+ publicFeature = True
+ elif outputType == 'privateFeature':
+ privateFeature = True
+ elif outputType == 'internalFeature':
+ internalFeature = True
+ else:
+ print(' XXXX UNHANDLED OUTPUT TYPE {} in feature {}.'.format(outputType, feature))
+ continue
+
+ if not any([publicFeature, privateFeature, internalFeature, publicDefine, negativeFeature]):
+ print(' **** Skipping feature {}: Not relevant for C++.'.format(feature))
+ return
+
+ cxxFeature = featureName(feature)
+
+ def writeFeature(name, publicFeature=False, privateFeature=False, labelAppend=''):
+ if comment:
+ cm_fh.write('# {}\n'.format(comment))
+
+ cm_fh.write('qt_feature("{}"'.format(name))
+ if publicFeature:
+ cm_fh.write(' PUBLIC')
+ if privateFeature:
+ cm_fh.write(' PRIVATE')
+ cm_fh.write('\n')
+
+ cm_fh.write(lineify('SECTION', section))
+ cm_fh.write(lineify('LABEL', label + labelAppend))
+ if purpose != label:
+ cm_fh.write(lineify('PURPOSE', purpose))
+ cm_fh.write(lineify('AUTODETECT', autoDetect, quote=False))
+ cm_fh.write(lineify('CONDITION', condition, quote=False))
+ cm_fh.write(lineify('ENABLE', enable, quote=False))
+ cm_fh.write(lineify('DISABLE', disable, quote=False))
+ cm_fh.write(lineify('EMIT_IF', emitIf, quote=False))
+ cm_fh.write(')\n')
+
+ # Write qt_feature() calls before any qt_feature_definition() calls
+
+ # Default internal feature case.
+ featureCalls = {}
+ featureCalls[cxxFeature] = {'name': cxxFeature, 'labelAppend': ''}
+
+ # Go over all outputs to compute the number of features that have to be declared
+ for o in output:
+ outputType = o
+ name = cxxFeature
+
+ # The label append is to provide a unique label for features that have more than one output
+ # with different names.
+ labelAppend = ''
+
+ if isinstance(o, dict):
+ outputType = o['type']
+ if 'name' in o:
+ name = o['name']
+ labelAppend = ': {}'.format(o['name'])
+
+ if outputType not in ['feature', 'publicFeature', 'privateFeature']:
+ continue
+ if name not in featureCalls:
+ featureCalls[name] = {'name': name, 'labelAppend': labelAppend}
+
+ if outputType in ['feature', 'publicFeature']:
+ featureCalls[name]['publicFeature'] = True
+ elif outputType == 'privateFeature':
+ featureCalls[name]['privateFeature'] = True
+
+ # Write the qt_feature() calls from the computed feature map
+ for _, args in featureCalls.items():
+ writeFeature(**args)
+
+ # Write qt_feature_definition() calls
+ for o in output:
+ outputType = o
+ outputArgs = {}
+ if isinstance(o, dict):
+ outputType = o['type']
+ outputArgs = o
+
+ # Map negative feature to define:
+ if outputType == 'feature':
+ outputType = 'define'
+ outputArgs = {'name': 'QT_NO_{}'.format(cxxFeature.upper()),
+ 'negative': True,
+ 'value': 1,
+ 'type': 'define'}
+
+ if outputType != 'define':
+ continue
+
+ if outputArgs.get('name') is None:
+ print(' XXXX DEFINE output without name in feature {}.'.format(feature))
+ continue
+
+ cm_fh.write('qt_feature_definition("{}" "{}"'.format(cxxFeature, outputArgs.get('name')))
+ if outputArgs.get('negative', False):
+ cm_fh.write(' NEGATE')
+ if outputArgs.get('value') is not None:
+ cm_fh.write(' VALUE "{}"'.format(outputArgs.get('value')))
+ cm_fh.write(')\n')
+
+
+def processInputs(ctx, data, cm_fh):
+ print(' inputs:')
+ if 'commandline' not in data:
+ return
+
+ commandLine = data['commandline']
+ if "options" not in commandLine:
+ return
+
+ for input in commandLine['options']:
+ parseInput(ctx, input, commandLine['options'][input], cm_fh)
+
+
+def processTests(ctx, data, cm_fh):
+ print(' tests:')
+ if 'tests' not in data:
+ return
+
+ for test in data['tests']:
+ parseTest(ctx, test, data['tests'][test], cm_fh)
+
+
+def processFeatures(ctx, data, cm_fh):
+ print(' features:')
+ if 'features' not in data:
+ return
+
+ for feature in data['features']:
+ parseFeature(ctx, feature, data['features'][feature], cm_fh)
+
+
+def processLibraries(ctx, data, cm_fh):
+ cmake_find_packages_set = set()
+ print(' libraries:')
+ if 'libraries' not in data:
+ return
+
+ for lib in data['libraries']:
+ parseLib(ctx, lib, data, cm_fh, cmake_find_packages_set)
+
+
+def processSubconfigs(dir, ctx, data):
+ assert ctx is not None
+ if 'subconfigs' in data:
+ for subconf in data['subconfigs']:
+ subconfDir = os.path.join(dir, subconf)
+ subconfData = readJsonFromDir(subconfDir)
+ subconfCtx = ctx
+ processJson(subconfDir, subconfCtx, subconfData)
+
+
+def processJson(dir, ctx, data):
+ ctx['module'] = data.get('module', 'global')
+
+ ctx = processFiles(ctx, data)
+
+ with open(os.path.join(dir, "configure.cmake"), 'w') as cm_fh:
+ cm_fh.write("\n\n#### Inputs\n\n")
+
+ processInputs(ctx, data, cm_fh)
+
+ cm_fh.write("\n\n#### Libraries\n\n")
+
+ processLibraries(ctx, data, cm_fh)
+
+ cm_fh.write("\n\n#### Tests\n\n")
+
+ processTests(ctx, data, cm_fh)
+
+ cm_fh.write("\n\n#### Features\n\n")
+
+ processFeatures(ctx, data, cm_fh)
+
+ if ctx.get('module') == 'global':
+ cm_fh.write('\nqt_extra_definition("QT_VERSION_STR" "\\\"${PROJECT_VERSION}\\\"" PUBLIC)\n')
+ cm_fh.write('qt_extra_definition("QT_VERSION_MAJOR" ${PROJECT_VERSION_MAJOR} PUBLIC)\n')
+ cm_fh.write('qt_extra_definition("QT_VERSION_MINOR" ${PROJECT_VERSION_MINOR} PUBLIC)\n')
+ cm_fh.write('qt_extra_definition("QT_VERSION_PATCH" ${PROJECT_VERSION_PATCH} PUBLIC)\n')
+
+ # do this late:
+ processSubconfigs(dir, ctx, data)
+
+
+def main():
+ if len(sys.argv) != 2:
+ print("This scripts needs one directory to process!")
+ quit(1)
+
+ dir = sys.argv[1]
+
+ print("Processing: {}.".format(dir))
+
+ data = readJsonFromDir(dir)
+ processJson(dir, {}, data)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/util/cmake/generate_module_map.sh b/util/cmake/generate_module_map.sh
new file mode 100755
index 0000000000..1ca0bfc43c
--- /dev/null
+++ b/util/cmake/generate_module_map.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/bash
+#############################################################################
+##
+## 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$
+##
+#############################################################################
+
+pro_files=$(find . -name \*.pro)
+
+for f in ${pro_files}; do
+ if grep "^load(qt_module)" "${f}" > /dev/null ; then
+ target=$(grep "TARGET" "${f}" | cut -d'=' -f2 | sed -e "s/\s*//g")
+ module=$(basename ${f})
+ echo "'${module%.pro}': '${target}',"
+ fi
+done
diff --git a/util/cmake/helper.py b/util/cmake/helper.py
new file mode 100644
index 0000000000..e80813a1f7
--- /dev/null
+++ b/util/cmake/helper.py
@@ -0,0 +1,462 @@
+#############################################################################
+##
+## 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$
+##
+#############################################################################
+
+import re
+import typing
+
+class LibraryMapping:
+ def __init__(self, soName: str,
+ packageName: typing.Optional[str],
+ targetName: typing.Optional[str], *,
+ resultVariable: typing.Optional[str] = None,
+ extra: typing.List[str] = [],
+ appendFoundSuffix: bool = True,
+ emit_if: str = '') -> None:
+ self.soName = soName
+ self.packageName = packageName
+ self.resultVariable = resultVariable
+ self.appendFoundSuffix = appendFoundSuffix
+ self.extra = extra
+ self.targetName = targetName
+
+ # if emit_if is non-empty, the generated find_package call
+ # for a library will be surrounded by this condition.
+ self.emit_if = emit_if
+
+ def is_qt(self) -> bool:
+ return self.packageName == 'Qt' \
+ or self.packageName == 'Qt5' \
+ or self.packageName == 'Qt6'
+
+_qt_library_map = [
+ # Qt:
+ LibraryMapping('accessibility_support', 'Qt6', 'Qt::AccessibilitySupport', extra = ['COMPONENTS', 'AccessibilitySupport']),
+ LibraryMapping('androidextras', 'Qt6', 'Qt::AndroidExtras', extra = ['COMPONENTS', 'AndroidExtras']),
+ LibraryMapping('animation', 'Qt6', 'Qt::3DAnimation', extra = ['COMPONENTS', '3DAnimation']),
+ LibraryMapping('application-lib', 'Qt6', 'Qt::AppManApplication', extra = ['COMPONENTS', 'AppManApplication']),
+ LibraryMapping('bluetooth', 'Qt6', 'Qt::Bluetooth', extra = ['COMPONENTS', 'Bluetooth']),
+ LibraryMapping('bootstrap', 'Qt6', 'Qt::Bootstrap', extra = ['COMPONENTS', 'Bootstrap']),
+ # bootstrap-dbus: Not needed in Qt6!
+ LibraryMapping('client', 'Qt6', 'Qt::WaylandClient', extra = ['COMPONENTS', 'WaylandClient']),
+ LibraryMapping('clipboard_support', 'Qt6', 'Qt::ClipboardSupport', extra = ['COMPONENTS', 'ClipboardSupport']),
+ LibraryMapping('common-lib', 'Qt6', 'Qt::AppManCommon', extra = ['COMPONENTS', 'AppManCommon']),
+ LibraryMapping('compositor', 'Qt6', 'Qt::WaylandCompositor', extra = ['COMPONENTS', 'WaylandCompositor']),
+ LibraryMapping('concurrent', 'Qt6', 'Qt::Concurrent', extra = ['COMPONENTS', 'Concurrent']),
+ LibraryMapping('container', 'Qt6', 'Qt::AxContainer', extra = ['COMPONENTS', 'AxContainer']),
+ LibraryMapping('control', 'Qt6', 'Qt::AxServer', extra = ['COMPONENTS', 'AxServer']),
+ LibraryMapping('core_headers', 'Qt6', 'Qt::WebEngineCore', extra = ['COMPONENTS', 'WebEngineCore']),
+ LibraryMapping('core', 'Qt6', 'Qt::Core', extra = ['COMPONENTS', 'Core']),
+ LibraryMapping('coretest', 'Qt6', 'Qt::3DCoreTest', extra = ['COMPONENTS', '3DCoreTest']),
+ LibraryMapping('crypto-lib', 'Qt6', 'Qt::AppManCrypto', extra = ['COMPONENTS', 'AppManCrypto']),
+ LibraryMapping('dbus', 'Qt6', 'Qt::DBus', extra = ['COMPONENTS', 'DBus']),
+ LibraryMapping('devicediscovery', 'Qt6', 'Qt::DeviceDiscoverySupport', extra = ['COMPONENTS', 'DeviceDiscoverySupport']),
+ LibraryMapping('devicediscovery_support', 'Qt6', 'Qt::DeviceDiscoverySupport', extra = ['COMPONENTS', 'DeviceDiscoverySupport']),
+ LibraryMapping('edid', 'Qt6', 'Qt::EdidSupport', extra = ['COMPONENTS', 'EdidSupport']),
+ LibraryMapping('edid_support', 'Qt6', 'Qt::EdidSupport', extra = ['COMPONENTS', 'EdidSupport']),
+ LibraryMapping('eglconvenience', 'Qt6', 'Qt::EglSupport', extra = ['COMPONENTS', 'EglSupport']),
+ LibraryMapping('eglfsdeviceintegration', 'Qt6', 'Qt::EglFSDeviceIntegration', extra = ['COMPONENTS', 'EglFSDeviceIntegration']),
+ LibraryMapping('eglfs_kms_support', 'Qt6', 'Qt::EglFsKmsSupport', extra = ['COMPONENTS', 'EglFsKmsSupport']),
+ LibraryMapping('egl_support', 'Qt6', 'Qt::EglSupport', extra = ['COMPONENTS', 'EglSupport']),
+ # enginio: Not needed in Qt6!
+ LibraryMapping('eventdispatchers', 'Qt6', 'Qt::EventDispatcherSupport', extra = ['COMPONENTS', 'EventDispatcherSupport']),
+ LibraryMapping('eventdispatcher_support', 'Qt6', 'Qt::EventDispatcherSupport', extra = ['COMPONENTS', 'EventDispatcherSupport']),
+ LibraryMapping('extras', 'Qt6', 'Qt::3DExtras', extra = ['COMPONENTS', '3DExtras']),
+ LibraryMapping('fbconvenience', 'Qt6', 'Qt::FbSupport', extra = ['COMPONENTS', 'FbSupport']),
+ LibraryMapping('fb_support', 'Qt6', 'Qt::FbSupport', extra = ['COMPONENTS', 'FbSupport']),
+ LibraryMapping('fontdatabase_support', 'Qt6', 'Qt::FontDatabaseSupport', extra = ['COMPONENTS', 'FontDatabaseSupport']),
+ LibraryMapping('gamepad', 'Qt6', 'Qt::Gamepad', extra = ['COMPONENTS', 'Gamepad']),
+ LibraryMapping('global', 'Qt6', 'Qt::Core', extra = ['COMPONENTS', 'Core']), # manually added special case
+ LibraryMapping('glx_support', 'Qt6', 'Qt::GlxSupport', extra = ['COMPONENTS', 'GlxSupport']),
+ LibraryMapping('graphics_support', 'Qt6', 'Qt::GraphicsSupport', extra = ['COMPONENTS', 'GraphicsSupport']),
+ LibraryMapping('gsttools', 'Qt6', 'Qt::MultimediaGstTools', extra = ['COMPONENTS', 'MultimediaGstTools']),
+ LibraryMapping('gui', 'Qt6', 'Qt::Gui', extra = ['COMPONENTS', 'Gui']),
+ LibraryMapping('help', 'Qt6', 'Qt::Help', extra = ['COMPONENTS', 'Help']),
+ LibraryMapping('hunspellinputmethod', 'Qt6', 'Qt::HunspellInputMethod', extra = ['COMPONENTS', 'HunspellInputMethod']),
+ LibraryMapping('input', 'Qt6', 'Qt::InputSupport', extra = ['COMPONENTS', 'InputSupport']),
+ LibraryMapping('input_support', 'Qt6', 'Qt::InputSupport', extra = ['COMPONENTS', 'InputSupport']),
+ LibraryMapping('installer-lib', 'Qt6', 'Qt::AppManInstaller', extra = ['COMPONENTS', 'AppManInstaller']),
+ LibraryMapping('kmsconvenience', 'Qt6', 'Qt::KmsSupport', extra = ['COMPONENTS', 'KmsSupport']),
+ LibraryMapping('kms_support', 'Qt6', 'Qt::KmsSupport', extra = ['COMPONENTS', 'KmsSupport']),
+ LibraryMapping('launcher-lib', 'Qt6', 'Qt::AppManLauncher', extra = ['COMPONENTS', 'AppManLauncher']),
+ LibraryMapping('lib', 'Qt6', 'Qt::Designer', extra = ['COMPONENTS', 'Designer']),
+ LibraryMapping('linuxaccessibility_support', 'Qt6', 'Qt::LinuxAccessibilitySupport', extra = ['COMPONENTS', 'LinuxAccessibilitySupport']),
+ LibraryMapping('location', 'Qt6', 'Qt::Location', extra = ['COMPONENTS', 'Location']),
+ LibraryMapping('logic', 'Qt6', 'Qt::3DLogic', extra = ['COMPONENTS', '3DLogic']),
+ LibraryMapping('macextras', 'Qt6', 'Qt::MacExtras', extra = ['COMPONENTS', 'MacExtras']),
+ LibraryMapping('main-lib', 'Qt6', 'Qt::AppManMain', extra = ['COMPONENTS', 'AppManMain']),
+ LibraryMapping('manager-lib', 'Qt6', 'Qt::AppManManager', extra = ['COMPONENTS', 'AppManManager']),
+ LibraryMapping('monitor-lib', 'Qt6', 'Qt::AppManMonitor', extra = ['COMPONENTS', 'AppManMonitor']),
+ LibraryMapping('multimedia', 'Qt6', 'Qt::Multimedia', extra = ['COMPONENTS', 'Multimedia']),
+ LibraryMapping('multimediawidgets', 'Qt6', 'Qt::MultimediaWidgets', extra = ['COMPONENTS', 'MultimediaWidgets']),
+ LibraryMapping('network', 'Qt6', 'Qt::Network', extra = ['COMPONENTS', 'Network']),
+ LibraryMapping('networkauth', 'Qt6', 'Qt::NetworkAuth', extra = ['COMPONENTS', 'NetworkAuth']),
+ LibraryMapping('nfc', 'Qt6', 'Qt::Nfc', extra = ['COMPONENTS', 'Nfc']),
+ LibraryMapping('oauth', 'Qt6', 'Qt::NetworkAuth', extra = ['COMPONENTS', 'NetworkAuth']),
+ LibraryMapping('openglextensions', 'Qt6', 'Qt::OpenGLExtensions', extra = ['COMPONENTS', 'OpenGLExtensions']),
+ LibraryMapping('opengl', 'Qt6', 'Qt::OpenGL', extra = ['COMPONENTS', 'OpenGL']),
+ LibraryMapping('package-lib', 'Qt6', 'Qt::AppManPackage', extra = ['COMPONENTS', 'AppManPackage']),
+ LibraryMapping('packetprotocol', 'Qt6', 'Qt::PacketProtocol', extra = ['COMPONENTS', 'PacketProtocol']),
+ LibraryMapping('particles', 'Qt6', 'Qt::QuickParticles', extra = ['COMPONENTS', 'QuickParticles']),
+ LibraryMapping('platformcompositor', 'Qt6', 'Qt::PlatformCompositorSupport', extra = ['COMPONENTS', 'PlatformCompositorSupport']),
+ LibraryMapping('platformcompositor_support', 'Qt6', 'Qt::PlatformCompositorSupport', extra = ['COMPONENTS', 'PlatformCompositorSupport']),
+ LibraryMapping('plugin-interfaces', 'Qt6', 'Qt::AppManPluginInterfaces', extra = ['COMPONENTS', 'AppManPluginInterfaces']),
+ LibraryMapping('positioning', 'Qt6', 'Qt::Positioning', extra = ['COMPONENTS', 'Positioning']),
+ LibraryMapping('positioningquick', 'Qt6', 'Qt::PositioningQuick', extra = ['COMPONENTS', 'PositioningQuick']),
+ LibraryMapping('printsupport', 'Qt6', 'Qt::PrintSupport', extra = ['COMPONENTS', 'PrintSupport']),
+ LibraryMapping('purchasing', 'Qt6', 'Qt::Purchasing', extra = ['COMPONENTS', 'Purchasing']),
+ LibraryMapping('qmldebug', 'Qt6', 'Qt::QmlDebug', extra = ['COMPONENTS', 'QmlDebug']),
+ LibraryMapping('qmldevtools', 'Qt6', 'Qt::QmlDevTools', extra = ['COMPONENTS', 'QmlDevTools']),
+ LibraryMapping('qml', 'Qt6', 'Qt::Qml', extra = ['COMPONENTS', 'Qml']),
+ LibraryMapping('qmlmodels', 'Qt6', 'Qt::QmlModels', extra = ['COMPONENTS', 'QmlModels']),
+ LibraryMapping('qmltest', 'Qt6', 'Qt::QuickTest', extra = ['COMPONENTS', 'QuickTest']),
+ LibraryMapping('qtmultimediaquicktools', 'Qt6', 'Qt::MultimediaQuick', extra = ['COMPONENTS', 'MultimediaQuick']),
+ LibraryMapping('quick3danimation', 'Qt6', 'Qt::3DQuickAnimation', extra = ['COMPONENTS', '3DQuickAnimation']),
+ LibraryMapping('quick3dextras', 'Qt6', 'Qt::3DQuickExtras', extra = ['COMPONENTS', '3DQuickExtras']),
+ LibraryMapping('quick3dinput', 'Qt6', 'Qt::3DQuickInput', extra = ['COMPONENTS', '3DQuickInput']),
+ LibraryMapping('quick3d', 'Qt6', 'Qt::3DQuick', extra = ['COMPONENTS', '3DQuick']),
+ LibraryMapping('quick3drender', 'Qt6', 'Qt::3DQuickRender', extra = ['COMPONENTS', '3DQuickRender']),
+ LibraryMapping('quick3dscene2d', 'Qt6', 'Qt::3DQuickScene2D', extra = ['COMPONENTS', '3DQuickScene2D']),
+ LibraryMapping('quickcontrols2', 'Qt6', 'Qt::QuickControls2', extra = ['COMPONENTS', 'QuickControls2']),
+ LibraryMapping('quick', 'Qt6', 'Qt::Quick', extra = ['COMPONENTS', 'Quick']),
+ LibraryMapping('quickshapes', 'Qt6', 'Qt::QuickShapes', extra = ['COMPONENTS', 'QuickShapes']),
+ LibraryMapping('quicktemplates2', 'Qt6', 'Qt::QuickTemplates2', extra = ['COMPONENTS', 'QuickTemplates2']),
+ LibraryMapping('quickwidgets', 'Qt6', 'Qt::QuickWidgets', extra = ['COMPONENTS', 'QuickWidgets']),
+ LibraryMapping('render', 'Qt6', 'Qt::3DRender', extra = ['COMPONENTS', '3DRender']),
+ LibraryMapping('script', 'Qt6', 'Qt::Script', extra = ['COMPONENTS', 'Script']),
+ LibraryMapping('scripttools', 'Qt6', 'Qt::ScriptTools', extra = ['COMPONENTS', 'ScriptTools']),
+ LibraryMapping('sensors', 'Qt6', 'Qt::Sensors', extra = ['COMPONENTS', 'Sensors']),
+ LibraryMapping('serialport', 'Qt6', 'Qt::SerialPort', extra = ['COMPONENTS', 'SerialPort']),
+ LibraryMapping('services', 'Qt6', 'Qt::ServiceSupport', extra = ['COMPONENTS', 'ServiceSupport']),
+ LibraryMapping('service_support', 'Qt6', 'Qt::ServiceSupport', extra = ['COMPONENTS', 'ServiceSupport']),
+ LibraryMapping('sql', 'Qt6', 'Qt::Sql', extra = ['COMPONENTS', 'Sql']),
+ LibraryMapping('svg', 'Qt6', 'Qt::Svg', extra = ['COMPONENTS', 'Svg']),
+ LibraryMapping('testlib', 'Qt6', 'Qt::Test', extra = ['COMPONENTS', 'Test']),
+ LibraryMapping('theme_support', 'Qt6', 'Qt::ThemeSupport', extra = ['COMPONENTS', 'ThemeSupport']),
+ LibraryMapping('tts', 'Qt6', 'Qt::TextToSpeech', extra = ['COMPONENTS', 'TextToSpeech']),
+ LibraryMapping('uiplugin', 'Qt6', 'Qt::UiPlugin', extra = ['COMPONENTS', 'UiPlugin']),
+ LibraryMapping('uitools', 'Qt6', 'Qt::UiTools', extra = ['COMPONENTS', 'UiTools']),
+ LibraryMapping('virtualkeyboard', 'Qt6', 'Qt::VirtualKeyboard', extra = ['COMPONENTS', 'VirtualKeyboard']),
+ LibraryMapping('vulkan_support', 'Qt6', 'Qt::VulkanSupport', extra = ['COMPONENTS', 'VulkanSupport']),
+ LibraryMapping('webchannel', 'Qt6', 'Qt::WebChannel', extra = ['COMPONENTS', 'WebChannel']),
+ LibraryMapping('webengine', 'Qt6', 'Qt::WebEngine', extra = ['COMPONENTS', 'WebEngine']),
+ LibraryMapping('webenginewidgets', 'Qt6', 'Qt::WebEngineWidgets', extra = ['COMPONENTS', 'WebEngineWidgets']),
+ LibraryMapping('websockets', 'Qt6', 'Qt::WebSockets', extra = ['COMPONENTS', 'WebSockets']),
+ LibraryMapping('webview', 'Qt6', 'Qt::WebView', extra = ['COMPONENTS', 'WebView']),
+ LibraryMapping('widgets', 'Qt6', 'Qt::Widgets', extra = ['COMPONENTS', 'Widgets']),
+ LibraryMapping('window-lib', 'Qt6', 'Qt::AppManWindow', extra = ['COMPONENTS', 'AppManWindow']),
+ LibraryMapping('windowsuiautomation_support', 'Qt6', 'Qt::WindowsUIAutomationSupport', extra = ['COMPONENTS', 'WindowsUIAutomationSupport']),
+ LibraryMapping('winextras', 'Qt6', 'Qt::WinExtras', extra = ['COMPONENTS', 'WinExtras']),
+ LibraryMapping('x11extras', 'Qt6', 'Qt::X11Extras', extra = ['COMPONENTS', 'X11Extras']),
+ LibraryMapping('xcb_qpa_lib', 'Qt6', 'Qt::XcbQpa', extra = ['COMPONENTS', 'XcbQpa']),
+ LibraryMapping('xkbcommon_support', 'Qt6', 'Qt::XkbCommonSupport', extra = ['COMPONENTS', 'XkbCommonSupport']),
+ LibraryMapping('xmlpatterns', 'Qt6', 'Qt::XmlPatterns', extra = ['COMPONENTS', 'XmlPatterns']),
+ LibraryMapping('xml', 'Qt6', 'Qt::Xml', extra = ['COMPONENTS', 'Xml']),
+ # qtzlib: No longer supported.
+]
+
+# Note that the library map is adjusted dynamically further down.
+_library_map = [
+ # 3rd party:
+ LibraryMapping('atspi', 'ATSPI2', 'PkgConfig::ATSPI2'),
+ LibraryMapping('corewlan', None, None),
+ LibraryMapping('cups', 'Cups', 'Cups::Cups'),
+ LibraryMapping('dbus', 'WrapDBus1', 'dbus-1', resultVariable="DBus1"),
+ LibraryMapping('doubleconversion', None, None),
+ LibraryMapping('drm', 'Libdrm', 'Libdrm::Libdrm'),
+ LibraryMapping('egl', 'EGL', 'EGL::EGL'),
+ LibraryMapping('fontconfig', 'Fontconfig', 'Fontconfig::Fontconfig', resultVariable="FONTCONFIG"),
+ LibraryMapping('freetype', 'WrapFreetype', 'WrapFreetype::WrapFreetype', extra=['REQUIRED']),
+ LibraryMapping('gbm', 'gbm', 'gbm::gbm'),
+ LibraryMapping('glib', 'GLIB2', 'GLIB2::GLIB2'),
+ LibraryMapping('gnu_iconv', None, None),
+ LibraryMapping('gtk3', 'GTK3', 'PkgConfig::GTK3'),
+ LibraryMapping('harfbuzz', 'harfbuzz', 'harfbuzz::harfbuzz'),
+ LibraryMapping('host_dbus', None, None),
+ LibraryMapping('icu', 'ICU', 'ICU::i18n ICU::uc ICU::data', extra=['COMPONENTS', 'i18n', 'uc', 'data']),
+ LibraryMapping('journald', 'Libsystemd', 'PkgConfig::Libsystemd'),
+ LibraryMapping('jpeg', 'JPEG', 'JPEG::JPEG'), # see also libjpeg
+ LibraryMapping('libatomic', 'Atomic', 'Atomic'),
+ LibraryMapping('libdl', None, '${CMAKE_DL_LIBS}'),
+ LibraryMapping('libinput', 'Libinput', 'Libinput::Libinput'),
+ LibraryMapping('libjpeg', 'JPEG', 'JPEG::JPEG'), # see also jpeg
+ LibraryMapping('libpng', 'PNG', 'PNG::PNG'),
+ LibraryMapping('libproxy', 'Libproxy', 'PkgConfig::Libproxy'),
+ LibraryMapping('librt', 'WrapRt','WrapRt'),
+ LibraryMapping('libudev', 'Libudev', 'PkgConfig::Libudev'),
+ LibraryMapping('lttng-ust', 'LTTngUST', 'LTTng::UST', resultVariable='LTTNGUST'),
+ LibraryMapping('mtdev', 'Mtdev', 'PkgConfig::Mtdev'),
+ LibraryMapping('odbc', 'ODBC', 'ODBC::ODBC'),
+ LibraryMapping('opengl_es2', 'GLESv2', 'GLESv2::GLESv2'),
+ LibraryMapping('opengl', 'OpenGL', 'OpenGL::GL', resultVariable='OpenGL_OpenGL'),
+ LibraryMapping('openssl_headers', 'OpenSSL', 'OpenSSL::SSL_nolink', resultVariable='OPENSSL_INCLUDE_DIR', appendFoundSuffix=False),
+ LibraryMapping('openssl', 'OpenSSL', 'OpenSSL::SSL'),
+ LibraryMapping('pcre2', 'WrapPCRE2', 'WrapPCRE2::WrapPCRE2', extra = ['REQUIRED']),
+ LibraryMapping('posix_iconv', None, None),
+ LibraryMapping('pps', 'PPS', 'PPS::PPS'),
+ LibraryMapping('psql', 'PostgreSQL', 'PostgreSQL::PostgreSQL'),
+ LibraryMapping('slog2', 'Slog2', 'Slog2::Slog2'),
+ LibraryMapping('sqlite2', None, None), # No more sqlite2 support in Qt6!
+ LibraryMapping('sqlite3', 'SQLite3', 'SQLite::SQLite3'),
+ LibraryMapping('sun_iconv', None, None),
+ LibraryMapping('tslib', 'Tslib', 'PkgConfig::Tslib'),
+ LibraryMapping('udev', 'Libudev', 'PkgConfig::Libudev'),
+ LibraryMapping('udev', 'Libudev', 'PkgConfig::Libudev'), # see also libudev!
+ LibraryMapping('vulkan', 'Vulkan', 'Vulkan::Vulkan'),
+ LibraryMapping('wayland_server', 'Wayland', 'Wayland::Server'),
+ LibraryMapping('x11sm', 'X11', '${X11_SM_LIB} ${X11_ICE_LIB}', resultVariable="X11_SM"),
+ LibraryMapping('xcb', 'XCB', 'XCB::XCB', extra = ['1.9']),
+ LibraryMapping('xcb_glx', 'XCB', 'XCB::GLX', extra = ['COMPONENTS', 'GLX'], resultVariable='XCB_GLX'),
+ LibraryMapping('xcb_icccm', 'XCB', 'XCB::ICCCM', extra = ['COMPONENTS', 'ICCCM'], resultVariable='XCB_ICCCM'),
+ LibraryMapping('xcb_image', 'XCB', 'XCB::IMAGE', extra = ['COMPONENTS', 'IMAGE'], resultVariable='XCB_IMAGE'),
+ LibraryMapping('xcb_keysyms', 'XCB', 'XCB::KEYSYMS', extra = ['COMPONENTS', 'KEYSYMS'], resultVariable='XCB_KEYSYMS'),
+ LibraryMapping('xcb_randr', 'XCB', 'XCB::RANDR', extra = ['COMPONENTS', 'RANDR'], resultVariable='XCB_RANDR'),
+ LibraryMapping('xcb_render', 'XCB', 'XCB::RENDER', extra = ['COMPONENTS', 'RENDER'], resultVariable='XCB_RENDER'),
+ LibraryMapping('xcb_renderutil', 'XCB', 'XCB::RENDERUTIL', extra = ['COMPONENTS', 'RENDERUTIL'], resultVariable='XCB_RENDERUTIL'),
+ LibraryMapping('xcb_shape', 'XCB', 'XCB::SHAPE', extra = ['COMPONENTS', 'SHAPE'], resultVariable='XCB_SHAPE'),
+ LibraryMapping('xcb_shm', 'XCB', 'XCB::SHM', extra = ['COMPONENTS', 'SHM'], resultVariable='XCB_SHM'),
+ LibraryMapping('xcb_sync', 'XCB', 'XCB::SYNC', extra = ['COMPONENTS', 'SYNC'], resultVariable='XCB_SYNC'),
+ LibraryMapping('xcb_xfixes', 'XCB', 'XCB::XFIXES', extra = ['COMPONENTS', 'XFIXES'], resultVariable='XCB_XFIXES'),
+ LibraryMapping('xcb_xinerama', 'XCB', 'XCB::XINERAMA', extra = ['COMPONENTS', 'XINERAMA'], resultVariable='XCB_XINERAMA'),
+ LibraryMapping('xcb_xinput', 'XCB', 'XCB::XINPUT', extra = ['COMPONENTS', 'XINPUT'], resultVariable='XCB_XINPUT'),
+ LibraryMapping('xcb_xkb', 'XCB', 'XCB::XKB', extra = ['COMPONENTS', 'XKB'], resultVariable='XCB_XKB'),
+ LibraryMapping('xcb_xlib', 'X11_XCB', 'X11::XCB'),
+ LibraryMapping('xkbcommon_evdev', 'XKB', 'XKB::XKB', extra = ['0.4.1']), # see also xkbcommon
+ LibraryMapping('xkbcommon_x11', 'XKB', 'XKB::XKB', extra = ['0.4.1']), # see also xkbcommon
+ LibraryMapping('xkbcommon', 'XKB', 'XKB::XKB', extra = ['0.4.1']),
+ LibraryMapping('xlib', 'X11', 'X11::XCB'), # FIXME: Is this correct?
+ LibraryMapping('xrender', 'XRender', 'PkgConfig::XRender'),
+ LibraryMapping('zlib', 'ZLIB', 'ZLIB::ZLIB', extra=['REQUIRED']),
+ LibraryMapping('zstd', 'ZSTD', 'ZSTD::ZSTD'),
+ LibraryMapping('tiff', 'TIFF', 'TIFF::TIFF'),
+ LibraryMapping('webp', 'WrapWebP', 'WrapWebP::WrapWebP'),
+ LibraryMapping('jasper', 'WrapJasper', 'WrapJasper::WrapJasper'),
+]
+
+
+def _adjust_library_map():
+ # Assign a Linux condition on all x and wayland related packages.
+ # We don't want to get pages of package not found messages on
+ # Windows and macOS, and this also improves configure time on
+ # those platforms.
+ linux_package_prefixes = ['xcb', 'x11', 'xkb', 'xrender', 'xlib', 'wayland']
+ for i, _ in enumerate(_library_map):
+ if any([_library_map[i].soName.startswith(p) for p in linux_package_prefixes]):
+ _library_map[i].emit_if = 'config.linux'
+
+
+_adjust_library_map()
+
+
+def find_3rd_party_library_mapping(soName: str) -> typing.Optional[LibraryMapping]:
+ for i in _library_map:
+ if i.soName == soName:
+ return i
+ return None
+
+
+def find_qt_library_mapping(soName: str) -> typing.Optional[LibraryMapping]:
+ for i in _qt_library_map:
+ if i.soName == soName:
+ return i
+ return None
+
+
+def find_library_info_for_target(targetName: str) -> typing.Optional[LibraryMapping]:
+ qt_target = targetName
+ if targetName.endswith('Private'):
+ qt_target = qt_target[:-7]
+
+ for i in _qt_library_map:
+ if i.targetName == qt_target:
+ return i
+
+ for i in _library_map:
+ if i.targetName == targetName:
+ return i
+
+ return None
+
+
+def featureName(input: str) -> str:
+ replacement_char = '_'
+ if input.startswith('c++'):
+ replacement_char = 'x'
+ return re.sub(r'[^a-zA-Z0-9_]', replacement_char, input)
+
+
+def map_qt_library(lib: str) -> str:
+ private = False
+ if lib.endswith('-private'):
+ private = True
+ lib = lib[:-8]
+ mapped = find_qt_library_mapping(lib)
+ qt_name = lib
+ if mapped:
+ assert mapped.targetName # Qt libs must have a target name set
+ qt_name = mapped.targetName
+ if private:
+ qt_name += 'Private'
+ return qt_name
+
+
+platform_mapping = {
+ 'win32': 'WIN32',
+ 'win': 'WIN32',
+ 'unix': 'UNIX',
+ 'darwin': 'APPLE',
+ 'linux': 'LINUX',
+ 'integrity': 'INTEGRITY',
+ 'qnx': 'QNX',
+ 'vxworks': 'VXWORKS',
+ 'hpux': 'HPUX',
+ 'nacl': 'NACL',
+ 'android': 'ANDROID',
+ 'android-embedded': 'ANDROID_EMBEDDED',
+ 'uikit': 'APPLE_UIKIT',
+ 'tvos': 'APPLE_TVOS',
+ 'watchos': 'APPLE_WATCHOS',
+ 'winrt': 'WINRT',
+ 'wasm': 'WASM',
+ 'msvc': 'MSVC',
+ 'clang': 'CLANG',
+ 'gcc': 'GCC',
+ 'icc': 'ICC',
+ 'intel_icc': 'ICC',
+ 'osx': 'APPLE_OSX',
+ 'ios': 'APPLE_IOS',
+ 'freebsd': 'FREEBSD',
+ 'openbsd': 'OPENBSD',
+ 'netbsd': 'NETBSD',
+ 'haiku': 'HAIKU',
+ 'netbsd': 'NETBSD',
+ 'mac': 'APPLE_OSX',
+ 'macx': 'APPLE_OSX',
+ 'macos': 'APPLE_OSX',
+ 'macx-icc': '(APPLE_OSX AND ICC)',
+}
+
+
+def map_platform(platform: str) -> str:
+ """ Return the qmake platform as cmake platform or the unchanged string. """
+ return platform_mapping.get(platform, platform)
+
+
+def is_known_3rd_party_library(lib: str) -> bool:
+ if lib.endswith('/nolink') or lib.endswith('_nolink'):
+ lib = lib[:-7]
+ mapping = find_3rd_party_library_mapping(lib)
+
+ return mapping is not None
+
+
+def map_3rd_party_library(lib: str) -> str:
+ libpostfix = ''
+ if lib.endswith('/nolink'):
+ lib = lib[:-7]
+ libpostfix = '_nolink'
+ mapping = find_3rd_party_library_mapping(lib)
+ if not mapping or not mapping.targetName:
+ return lib
+ return mapping.targetName + libpostfix
+
+
+def generate_find_package_info(lib: LibraryMapping,
+ use_qt_find_package: bool=True, *,
+ indent: int = 0,
+ emit_if: str = '') -> str:
+ isRequired = False
+
+ extra = lib.extra.copy()
+
+ if "REQUIRED" in extra and use_qt_find_package:
+ isRequired = True
+ extra.remove("REQUIRED")
+
+ cmake_target_name = lib.targetName
+ assert(cmake_target_name);
+
+ # _nolink or not does not matter at this point:
+ if cmake_target_name.endswith('_nolink') or cmake_target_name.endswith('/nolink'):
+ cmake_target_name = cmake_target_name[:-7]
+
+ if cmake_target_name and use_qt_find_package:
+ extra += ['PROVIDED_TARGETS', cmake_target_name]
+
+ result = ''
+ one_ind = ' '
+ ind = one_ind * indent
+
+ if use_qt_find_package:
+ if extra:
+ result = '{}qt_find_package({} {})\n'.format(ind, lib.packageName, ' '.join(extra))
+ else:
+ result = '{}qt_find_package({})\n'.format(ind, lib.packageName)
+
+ if isRequired:
+ result += '{}set_package_properties({} PROPERTIES TYPE REQUIRED)\n'.format(ind, lib.packageName)
+ else:
+ if extra:
+ result = '{}find_package({} {})\n'.format(ind, lib.packageName, ' '.join(extra))
+ else:
+ result = '{}find_package({})\n'.format(ind, lib.packageName)
+
+ # If a package should be found only in certain conditions, wrap
+ # the find_package call within that condition.
+ if emit_if:
+ result = "if(({emit_if}) OR QT_FIND_ALL_PACKAGES_ALWAYS)\n" \
+ "{ind}{result}endif()\n".format(emit_if=emit_if,
+ result=result,
+ ind=one_ind)
+
+ return result
+
+
+def _set_up_py_parsing_nicer_debug_output(pp):
+ indent = -1
+
+ def increase_indent(fn):
+ def wrapper_function(*args):
+ nonlocal indent
+ indent += 1
+ print("> " * indent, end="")
+ return fn(*args)
+
+ return wrapper_function
+
+ def decrease_indent(fn):
+ def wrapper_function(*args):
+ nonlocal indent
+ print("> " * indent, end="")
+ indent -= 1
+ return fn(*args)
+
+ return wrapper_function
+
+ pp._defaultStartDebugAction = increase_indent(pp._defaultStartDebugAction)
+ pp._defaultSuccessDebugAction = decrease_indent(pp._defaultSuccessDebugAction)
+ pp._defaultExceptionDebugAction = decrease_indent(pp._defaultExceptionDebugAction)
diff --git a/util/cmake/json_parser.py b/util/cmake/json_parser.py
new file mode 100644
index 0000000000..6ead008f08
--- /dev/null
+++ b/util/cmake/json_parser.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 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$
+##
+#############################################################################
+
+import pyparsing as pp
+import json
+import re
+from helper import _set_up_py_parsing_nicer_debug_output
+_set_up_py_parsing_nicer_debug_output(pp)
+
+
+class QMakeSpecificJSONParser:
+ def __init__(self, *, debug: bool = False) -> None:
+ self.debug = debug
+ self.grammar = self.create_py_parsing_grammar()
+
+ def create_py_parsing_grammar(self):
+ # Keep around all whitespace.
+ pp.ParserElement.setDefaultWhitespaceChars('')
+
+ def add_element(name: str, value: pp.ParserElement):
+ nonlocal self
+ if self.debug:
+ value.setName(name)
+ value.setDebug()
+ return value
+
+ # Our grammar is pretty simple. We want to remove all newlines
+ # inside quoted strings, to make the quoted strings JSON
+ # compliant. So our grammar should skip to the first quote while
+ # keeping everything before it as-is, process the quoted string
+ # skip to the next quote, and repeat that until the end of the
+ # file.
+
+ EOF = add_element('EOF', pp.StringEnd())
+ SkipToQuote = add_element('SkipToQuote', pp.SkipTo('"'))
+ SkipToEOF = add_element('SkipToEOF', pp.SkipTo(EOF))
+
+ def remove_newlines_and_whitespace_in_quoted_string(tokens):
+ first_string = tokens[0]
+ replaced_string = re.sub(r'\n[ ]*', ' ', first_string)
+ return replaced_string
+
+ QuotedString = add_element('QuotedString', pp.QuotedString(quoteChar='"',
+ multiline=True,
+ unquoteResults=False))
+ QuotedString.setParseAction(remove_newlines_and_whitespace_in_quoted_string)
+
+ QuotedTerm = add_element('QuotedTerm', pp.Optional(SkipToQuote) + QuotedString)
+ Grammar = add_element('Grammar', pp.OneOrMore(QuotedTerm) + SkipToEOF)
+
+ return Grammar
+
+ def parse_file_using_py_parsing(self, file: str):
+ print('Pre processing "{}" using py parsing to remove incorrect newlines.'.format(file))
+ try:
+ with open(file, 'r') as file_fd:
+ contents = file_fd.read()
+
+ parser_result = self.grammar.parseString(contents, parseAll=True)
+ token_list = parser_result.asList()
+ joined_string = ''.join(token_list)
+
+ return joined_string
+ except pp.ParseException as pe:
+ print(pe.line)
+ print(' '*(pe.col-1) + '^')
+ print(pe)
+ raise pe
+
+ def parse(self, file: str):
+ pre_processed_string = self.parse_file_using_py_parsing(file)
+ print('Parsing "{}" using json.loads().'.format(file))
+ json_parsed = json.loads(pre_processed_string)
+ return json_parsed
diff --git a/util/cmake/pro2cmake.py b/util/cmake/pro2cmake.py
new file mode 100755
index 0000000000..28724b1fc0
--- /dev/null
+++ b/util/cmake/pro2cmake.py
@@ -0,0 +1,1903 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2018 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the plugins of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+
+from __future__ import annotations
+
+from argparse import ArgumentParser
+import copy
+import xml.etree.ElementTree as ET
+from itertools import chain
+import os.path
+import re
+import io
+import typing
+
+from sympy.logic import (simplify_logic, And, Or, Not,)
+import pyparsing as pp
+from helper import _set_up_py_parsing_nicer_debug_output
+_set_up_py_parsing_nicer_debug_output(pp)
+
+from helper import map_qt_library, map_3rd_party_library, is_known_3rd_party_library, \
+ featureName, map_platform, find_library_info_for_target, generate_find_package_info, \
+ LibraryMapping
+
+from shutil import copyfile
+from special_case_helper import SpecialCaseHandler
+
+
+def _parse_commandline():
+ parser = ArgumentParser(description='Generate CMakeLists.txt files from .'
+ 'pro files.')
+ parser.add_argument('--debug', dest='debug', action='store_true',
+ help='Turn on all debug output')
+ parser.add_argument('--debug-parser', dest='debug_parser',
+ action='store_true',
+ help='Print debug output from qmake parser.')
+ parser.add_argument('--debug-parse-result', dest='debug_parse_result',
+ action='store_true',
+ help='Dump the qmake parser result.')
+ parser.add_argument('--debug-parse-dictionary',
+ dest='debug_parse_dictionary', action='store_true',
+ help='Dump the qmake parser result as dictionary.')
+ parser.add_argument('--debug-pro-structure', dest='debug_pro_structure',
+ action='store_true',
+ help='Dump the structure of the qmake .pro-file.')
+ parser.add_argument('--debug-full-pro-structure',
+ dest='debug_full_pro_structure', action='store_true',
+ help='Dump the full structure of the qmake .pro-file '
+ '(with includes).')
+ parser.add_argument('--debug-special-case-preservation',
+ dest='debug_special_case_preservation', action='store_true',
+ help='Show all git commands and file copies.')
+
+ parser.add_argument('--is-example', action='store_true',
+ dest="is_example",
+ help='Treat the input .pro file as an example.')
+ parser.add_argument('-s', '--skip-special-case-preservation',
+ dest='skip_special_case_preservation', action='store_true',
+ help='Skips behavior to reapply '
+ 'special case modifications (requires git in PATH)')
+ parser.add_argument('-k', '--keep-temporary-files',
+ dest='keep_temporary_files', action='store_true',
+ help='Don\'t automatically remove CMakeLists.gen.txt and other '
+ 'intermediate files.')
+
+ parser.add_argument('files', metavar='<.pro/.pri file>', type=str,
+ nargs='+', help='The .pro/.pri file to process')
+
+ return parser.parse_args()
+
+
+def process_qrc_file(target: str, filepath: str, base_dir: str = '') -> str:
+ assert(target)
+ resource_name = os.path.splitext(os.path.basename(filepath))[0]
+ base_dir = os.path.join('' if base_dir == '.' else base_dir, os.path.dirname(filepath))
+
+ tree = ET.parse(filepath)
+ root = tree.getroot()
+ assert(root.tag == 'RCC')
+
+ output = ''
+
+ resource_count = 0
+ for resource in root:
+ assert(resource.tag == 'qresource')
+ lang = resource.get('lang', '')
+ prefix = resource.get('prefix', '')
+
+ full_resource_name = resource_name + (str(resource_count) if resource_count > 0 else '')
+
+ files: typing.Dict[str, str] = {}
+ for file in resource:
+ path = file.text
+ assert path
+
+ # Get alias:
+ alias = file.get('alias', '')
+ files[path] = alias
+
+ sorted_files = sorted(files.keys())
+
+ assert(sorted_files)
+
+ for source in sorted_files:
+ alias = files[source]
+ if alias:
+ full_source = os.path.join(base_dir, source)
+ output += 'set_source_files_properties("{}"\n' \
+ ' PROPERTIES alias "{}")\n'.format(full_source, alias)
+
+ params = ''
+ if lang:
+ params += ' LANG "{}"'.format(lang)
+ if prefix:
+ params += ' PREFIX "{}"'.format(prefix)
+ if base_dir:
+ params += ' BASE "{}"'.format(base_dir)
+ output += 'add_qt_resource({} "{}"{} FILES\n {})\n'.format(target, full_resource_name,
+ params,
+ '\n '.join(sorted_files))
+
+ resource_count += 1
+
+ return output
+
+
+def fixup_linecontinuation(contents: str) -> str:
+ # Remove all line continuations, aka a backslash followed by
+ # a newline character with an arbitrary amount of whitespace
+ # between the backslash and the newline.
+ # This greatly simplifies the qmake parsing grammar.
+ contents = re.sub(r'([^\t ])\\[ \t]*\n', '\\1 ', contents)
+ contents = re.sub(r'\\[ \t]*\n', '', contents)
+ return contents
+
+
+def fixup_comments(contents: str) -> str:
+ # Get rid of completely commented out lines.
+ # So any line which starts with a '#' char and ends with a new line
+ # will be replaced by a single new line.
+ #
+ # This is needed because qmake syntax is weird. In a multi line
+ # assignment (separated by backslashes and newlines aka
+ # # \\\n ), if any of the lines are completely commented out, in
+ # principle the assignment should fail.
+ #
+ # It should fail because you would have a new line separating
+ # the previous value from the next value, and the next value would
+ # not be interpreted as a value, but as a new token / operation.
+ # qmake is lenient though, and accepts that, so we need to take
+ # care of it as well, as if the commented line didn't exist in the
+ # first place.
+
+ contents = re.sub(r'\n#[^\n]*?\n', '\n', contents, re.DOTALL)
+ return contents
+
+
+def spaces(indent: int) -> str:
+ return ' ' * indent
+
+
+def trim_leading_dot(file: str) -> str:
+ while file.startswith('./'):
+ file = file[2:]
+ return file
+
+
+def map_to_file(f: str, scope: Scope, *, is_include: bool = False) -> str:
+ assert('$$' not in f)
+
+ if f.startswith('${'): # Some cmake variable is prepended
+ return f
+
+ base_dir = scope.currentdir if is_include else scope.basedir
+ f = os.path.join(base_dir, f)
+
+ return trim_leading_dot(f)
+
+
+def handle_vpath(source: str, base_dir: str, vpath: typing.List[str]) -> str:
+ assert('$$' not in source)
+
+ if not source:
+ return ''
+
+ if not vpath:
+ return source
+
+ if os.path.exists(os.path.join(base_dir, source)):
+ return source
+
+ variable_pattern = re.compile(r'\$\{[A-Za-z0-9_]+\}')
+ match = re.match(variable_pattern, source)
+ if match:
+ # a complex, variable based path, skipping validation
+ # or resolving
+ return source
+
+ for v in vpath:
+ fullpath = os.path.join(v, source)
+ if os.path.exists(fullpath):
+ return trim_leading_dot(os.path.relpath(fullpath, base_dir))
+
+ print(' XXXX: Source {}: Not found.'.format(source))
+ return '{}-NOTFOUND'.format(source)
+
+
+class Operation:
+ def __init__(self, value: typing.Union[typing.List[str], str]):
+ if isinstance(value, list):
+ self._value = value
+ else:
+ self._value = [str(value), ]
+
+ def process(self, key: str, input: typing.List[str],
+ transformer: typing.Callable[[typing.List[str]], typing.List[str]]) -> typing.List[str]:
+ assert(False)
+
+ def __repr__(self):
+ assert(False)
+
+ def _dump(self):
+ if not self._value:
+ return '<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.Mapping[str, typing.List[Operation]] = {
+ 'QT_SOURCE_TREE': [SetOperation(['${PROJECT_SOURCE_DIR}'])],
+ 'QT_BUILD_TREE': [SetOperation(['${PROJECT_BUILD_DIR}'])],
+ }) -> None:
+ if parent_scope:
+ parent_scope._add_child(self)
+ else:
+ self._parent = None # type: typing.Optional[Scope]
+
+ self._basedir = base_dir
+ if file:
+ self._currentdir = os.path.dirname(file)
+ if not self._currentdir:
+ self._currentdir = '.'
+ if not self._basedir:
+ self._basedir = self._currentdir
+
+ self._scope_id = Scope.SCOPE_ID
+ Scope.SCOPE_ID += 1
+ self._file = file
+ self._condition = map_condition(condition)
+ self._children = [] # type: typing.List[Scope]
+ self._included_children = [] # type: typing.List[Scope]
+ self._operations = copy.deepcopy(operations)
+ self._visited_keys = set() # type: typing.Set[str]
+ self._total_condition = None # type: typing.Optional[str]
+
+ def __repr__(self):
+ return '{}:{}:{}:{}:{}'.format(self._scope_id,
+ self._basedir, self._currentdir,
+ self._file, self._condition or '<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 generated_cmake_lists_path(self) -> str:
+ assert self.basedir
+ return os.path.join(self.basedir, 'CMakeLists.gen.txt')
+
+ @property
+ def original_cmake_lists_path(self) -> str:
+ assert self.basedir
+ return os.path.join(self.basedir, 'CMakeLists.txt')
+
+ @property
+ def condition(self) -> str:
+ return self._condition
+
+ @property
+ def total_condition(self) -> typing.Optional[str]:
+ return self._total_condition
+
+ @total_condition.setter
+ def total_condition(self, condition: str) -> None:
+ self._total_condition = condition
+
+ def _add_child(self, scope: 'Scope') -> None:
+ scope._parent = self
+ self._children.append(scope)
+
+ @property
+ def children(self) -> typing.List['Scope']:
+ result = list(self._children)
+ for include_scope in self._included_children:
+ result += include_scope.children
+ return result
+
+ def dump(self, *, indent: int = 0) -> None:
+ ind = ' ' * indent
+ print('{}Scope "{}":'.format(ind, self))
+ if self.total_condition:
+ print('{} Total condition = {}'.format(ind, self.total_condition))
+ print('{} Keys:'.format(ind))
+ keys = self._operations.keys()
+ if not keys:
+ print('{} -- NONE --'.format(ind))
+ else:
+ for k in sorted(keys):
+ print('{} {} = "{}"'
+ .format(ind, k, self._operations.get(k, [])))
+ print('{} Children:'.format(ind))
+ if not self._children:
+ print('{} -- NONE --'.format(ind))
+ else:
+ for c in self._children:
+ c.dump(indent=indent + 1)
+ print('{} Includes:'.format(ind))
+ if not self._included_children:
+ print('{} -- NONE --'.format(ind))
+ else:
+ for c in self._included_children:
+ c.dump(indent=indent + 1)
+
+ def dump_structure(self, *, type: str = 'ROOT', indent: int = 0) -> None:
+ print('{}{}: {}'.format(spaces(indent), type, self))
+ for i in self._included_children:
+ i.dump_structure(type='INCL', indent=indent + 1)
+ for i in self._children:
+ i.dump_structure(type='CHLD', indent=indent + 1)
+
+ @property
+ def keys(self):
+ return self._operations.keys()
+
+ @property
+ def visited_keys(self):
+ return self._visited_keys
+
+ def _evalOps(self, key: str,
+ transformer: typing.Optional[typing.Callable[[Scope, typing.List[str]], typing.List[str]]],
+ result: typing.List[str], *, inherrit: bool = False) \
+ -> typing.List[str]:
+ self._visited_keys.add(key)
+
+ # Inherrit values from above:
+ if self._parent and inherrit:
+ result = self._parent._evalOps(key, transformer, result)
+
+ if transformer:
+ op_transformer = lambda files: transformer(self, files)
+ else:
+ op_transformer = lambda files: files
+
+ for op in self._operations.get(key, []):
+ result = op.process(key, result, op_transformer)
+
+ for ic in self._included_children:
+ result = list(ic._evalOps(key, transformer, result))
+
+ return result
+
+ def get(self, key: str, *, ignore_includes: bool = False, inherrit: bool = False) -> typing.List[str]:
+
+ is_same_path = self.currentdir == self.basedir
+
+ if key == 'PWD':
+ if is_same_path:
+ return ['${CMAKE_CURRENT_SOURCE_DIR}']
+ else:
+ return ['${CMAKE_CURRENT_SOURCE_DIR}/' + os.path.relpath(self.currentdir, self.basedir),]
+ if key == 'OUT_PWD':
+ if is_same_path:
+ return ['${CMAKE_CURRENT_BINARY_DIR}']
+ else:
+ return ['${CMAKE_CURRENT_BINARY_DIR}/' + os.path.relpath(self.currentdir, self.basedir),]
+
+ return self._evalOps(key, None, [], inherrit=inherrit)
+
+ def get_string(self, key: str, default: str = '') -> str:
+ v = self.get(key)
+ if len(v) == 0:
+ return default
+ assert len(v) == 1
+ return v[0]
+
+ def _map_files(self, files: typing.List[str], *,
+ use_vpath: bool = True, is_include: bool = False) -> typing.List[str]:
+
+ expanded_files = [] # type: typing.List[str]
+ for f in files:
+ r = self._expand_value(f)
+ expanded_files += r
+
+ mapped_files = list(map(lambda f: map_to_file(f, self, is_include=is_include), expanded_files))
+
+ if use_vpath:
+ result = list(map(lambda f: handle_vpath(f, self.basedir, self.get('VPATH', inherrit=True)), mapped_files))
+ else:
+ result = mapped_files
+
+ # strip ${CMAKE_CURRENT_SOURCE_DIR}:
+ result = list(map(lambda f: f[28:] if f.startswith('${CMAKE_CURRENT_SOURCE_DIR}/') else f, result))
+
+ # strip leading ./:
+ result = list(map(lambda f: trim_leading_dot(f), result))
+
+ return result
+
+ def get_files(self, key: str, *, use_vpath: bool = False,
+ is_include: bool = False) -> typing.List[str]:
+ transformer = lambda scope, files: scope._map_files(files, use_vpath=use_vpath, is_include=is_include)
+ return list(self._evalOps(key, transformer, []))
+
+ def _expand_value(self, value: str) -> typing.List[str]:
+ result = value
+ pattern = re.compile(r'\$\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?')
+ match = re.search(pattern, result)
+ while match:
+ old_result = result
+ if match.group(0) == value:
+ return self.get(match.group(1))
+
+ replacement = self.get(match.group(1))
+ replacement_str = replacement[0] if replacement else ''
+ result = result[:match.start()] \
+ + replacement_str \
+ + result[match.end():]
+
+ if result == old_result:
+ return [result,] # Do not go into infinite loop
+
+ match = re.search(pattern, result)
+ return [result,]
+
+ def expand(self, key: str) -> typing.List[str]:
+ value = self.get(key)
+ result: typing.List[str] = []
+ assert isinstance(value, list)
+ for v in value:
+ result += self._expand_value(v)
+ return result
+
+ def expandString(self, key: str) -> str:
+ result = self._expand_value(self.get_string(key))
+ assert len(result) == 1
+ return result[0]
+
+ @property
+ def TEMPLATE(self) -> str:
+ return self.get_string('TEMPLATE', 'app')
+
+ def _rawTemplate(self) -> str:
+ return self.get_string('TEMPLATE')
+
+ @property
+ def TARGET(self) -> str:
+ return self.get_string('TARGET') \
+ or os.path.splitext(os.path.basename(self.file))[0]
+
+ @property
+ def _INCLUDED(self) -> typing.List[str]:
+ return self.get('_INCLUDED')
+
+
+class QmakeParser:
+ def __init__(self, *, debug: bool = False) -> None:
+ self.debug = debug
+ self._Grammar = self._generate_grammar()
+
+ def _generate_grammar(self):
+ # Define grammar:
+ pp.ParserElement.setDefaultWhitespaceChars(' \t')
+
+ def add_element(name: str, value: pp.ParserElement):
+ nonlocal self
+ if self.debug:
+ value.setName(name)
+ value.setDebug()
+ return value
+
+ EOL = add_element('EOL', pp.Suppress(pp.LineEnd()))
+ Else = add_element('Else', pp.Keyword('else'))
+ Identifier = add_element('Identifier', pp.Word(pp.alphas + '_',
+ bodyChars=pp.alphanums+'_-./'))
+ BracedValue = add_element('BracedValue',
+ pp.nestedExpr(
+ ignoreExpr=pp.quotedString |
+ pp.QuotedString(quoteChar='$(',
+ endQuoteChar=')',
+ escQuote='\\',
+ unquoteResults=False)
+ ).setParseAction(lambda s, l, t: ['(', *t[0], ')']))
+
+ Substitution \
+ = add_element('Substitution',
+ pp.Combine(pp.Literal('$')
+ + (((pp.Literal('$') + Identifier
+ + pp.Optional(pp.nestedExpr()))
+ | (pp.Literal('(') + Identifier + pp.Literal(')'))
+ | (pp.Literal('{') + Identifier + pp.Literal('}'))
+ | (pp.Literal('$') + pp.Literal('{')
+ + Identifier + pp.Optional(pp.nestedExpr())
+ + pp.Literal('}'))
+ | (pp.Literal('$') + pp.Literal('[') + Identifier
+ + pp.Literal(']'))
+ ))))
+ LiteralValuePart = add_element('LiteralValuePart',
+ pp.Word(pp.printables, excludeChars='$#{}()'))
+ SubstitutionValue \
+ = add_element('SubstitutionValue',
+ pp.Combine(pp.OneOrMore(Substitution
+ | LiteralValuePart
+ | pp.Literal('$'))))
+ Value \
+ = add_element('Value',
+ pp.NotAny(Else | pp.Literal('}') | EOL) \
+ + (pp.QuotedString(quoteChar='"', escChar='\\')
+ | SubstitutionValue
+ | BracedValue))
+
+ Values = add_element('Values', pp.ZeroOrMore(Value)('value'))
+
+ Op = add_element('OP',
+ pp.Literal('=') | pp.Literal('-=') | pp.Literal('+=') \
+ | pp.Literal('*='))
+
+ Key = add_element('Key', Identifier)
+
+ Operation = add_element('Operation', Key('key') + Op('operation') + Values('value'))
+ CallArgs = add_element('CallArgs', pp.nestedExpr())
+
+ def parse_call_args(results):
+ out = ''
+ for item in chain(*results):
+ if isinstance(item, str):
+ out += item
+ else:
+ out += "(" + parse_call_args(item) + ")"
+ return out
+
+ CallArgs.setParseAction(parse_call_args)
+
+ Load = add_element('Load', pp.Keyword('load') + CallArgs('loaded'))
+ Include = add_element('Include', pp.Keyword('include') + CallArgs('included'))
+ Option = add_element('Option', pp.Keyword('option') + CallArgs('option'))
+
+ # ignore the whole thing...
+ DefineTestDefinition = add_element(
+ 'DefineTestDefinition',
+ pp.Suppress(pp.Keyword('defineTest') + CallArgs
+ + pp.nestedExpr(opener='{', closer='}', ignoreExpr=pp.LineEnd())))
+
+ # ignore the whole thing...
+ ForLoop = add_element(
+ 'ForLoop',
+ pp.Suppress(pp.Keyword('for') + CallArgs
+ + pp.nestedExpr(opener='{', closer='}', ignoreExpr=pp.LineEnd())))
+
+ # ignore the whole thing...
+ ForLoopSingleLine = add_element(
+ 'ForLoopSingleLine',
+ pp.Suppress(pp.Keyword('for') + CallArgs + pp.Literal(':') + pp.SkipTo(EOL)))
+
+ # ignore the whole thing...
+ FunctionCall = add_element('FunctionCall', pp.Suppress(Identifier + pp.nestedExpr()))
+
+ Scope = add_element('Scope', pp.Forward())
+
+ Statement = add_element('Statement',
+ pp.Group(Load | Include | Option | ForLoop | ForLoopSingleLine
+ | DefineTestDefinition | FunctionCall | Operation))
+ StatementLine = add_element('StatementLine', Statement + (EOL | pp.FollowedBy('}')))
+ StatementGroup = add_element('StatementGroup',
+ pp.ZeroOrMore(StatementLine | Scope | pp.Suppress(EOL)))
+
+ Block = add_element('Block',
+ pp.Suppress('{') + pp.Optional(EOL)
+ + StatementGroup + pp.Optional(EOL)
+ + pp.Suppress('}') + pp.Optional(EOL))
+
+ ConditionEnd = add_element('ConditionEnd',
+ pp.FollowedBy((pp.Optional(pp.White())
+ + (pp.Literal(':')
+ | pp.Literal('{')
+ | pp.Literal('|')))))
+
+ ConditionPart1 = add_element('ConditionPart1',
+ (pp.Optional('!') + Identifier + pp.Optional(BracedValue)))
+ ConditionPart2 = add_element('ConditionPart2', pp.CharsNotIn('#{}|:=\\\n'))
+ ConditionPart = add_element(
+ 'ConditionPart',
+ (ConditionPart1 ^ ConditionPart2) + ConditionEnd)
+
+ ConditionOp = add_element('ConditionOp', pp.Literal('|') ^ pp.Literal(':'))
+ ConditionWhiteSpace = add_element('ConditionWhiteSpace',
+ pp.Suppress(pp.Optional(pp.White(' '))))
+
+ ConditionRepeated = add_element('ConditionRepeated',
+ pp.ZeroOrMore(ConditionOp
+ + ConditionWhiteSpace + ConditionPart))
+
+ Condition = add_element('Condition', pp.Combine(ConditionPart + ConditionRepeated))
+ Condition.setParseAction(lambda x: ' '.join(x).strip().replace(':', ' && ').strip(' && '))
+
+ # Weird thing like write_file(a)|error() where error() is the alternative condition
+ # which happens to be a function call. In this case there is no scope, but our code expects
+ # a scope with a list of statements, so create a fake empty statement.
+ ConditionEndingInFunctionCall = add_element(
+ 'ConditionEndingInFunctionCall', pp.Suppress(ConditionOp) + FunctionCall
+ + pp.Empty().setParseAction(lambda x: [[]])
+ .setResultsName('statements'))
+
+ SingleLineScope = add_element('SingleLineScope',
+ pp.Suppress(pp.Literal(':'))
+ + pp.Group(Block | (Statement + EOL))('statements'))
+ MultiLineScope = add_element('MultiLineScope', Block('statements'))
+
+ SingleLineElse = add_element('SingleLineElse',
+ pp.Suppress(pp.Literal(':'))
+ + (Scope | Block | (Statement + pp.Optional(EOL))))
+ MultiLineElse = add_element('MultiLineElse', Block)
+ ElseBranch = add_element('ElseBranch', pp.Suppress(Else) + (SingleLineElse | MultiLineElse))
+
+ # Scope is already add_element'ed in the forward declaration above.
+ Scope <<= \
+ pp.Group(Condition('condition')
+ + (SingleLineScope | MultiLineScope | ConditionEndingInFunctionCall)
+ + pp.Optional(ElseBranch)('else_statements'))
+
+ Grammar = StatementGroup('statements')
+ Grammar.ignore(pp.pythonStyleComment())
+
+ return Grammar
+
+ def parseFile(self, file: str):
+ print('Parsing \"{}\"...'.format(file))
+ try:
+ with open(file, 'r') as file_fd:
+ contents = file_fd.read()
+
+ old_contents = contents
+ contents = fixup_comments(contents)
+ contents = fixup_linecontinuation(contents)
+
+ if old_contents != contents:
+ print('Warning: Fixed line continuation in .pro-file!\n'
+ ' Position information in Parsing output might be wrong!')
+ result = self._Grammar.parseString(contents, parseAll=True)
+ except pp.ParseException as pe:
+ print(pe.line)
+ print(' '*(pe.col-1) + '^')
+ print(pe)
+ raise pe
+ return result
+
+
+def parseProFile(file: str, *, debug=False):
+ parser = QmakeParser(debug=debug)
+ return parser.parseFile(file)
+
+
+def map_condition(condition: str) -> str:
+ # Some hardcoded cases that are too bothersome to generalize.
+ condition = re.sub(r'^qtConfig\(opengl\(es1\|es2\)\?\)$',
+ r'QT_FEATURE_opengl OR QT_FEATURE_opengles2 OR QT_FEATURE_opengles3',
+ condition)
+ condition = re.sub(r'^qtConfig\(opengl\.\*\)$', r'QT_FEATURE_opengl', condition)
+ condition = re.sub(r'^win\*$', r'win', condition)
+
+ def gcc_version_handler(match_obj: re.Match):
+ operator = match_obj.group(1)
+ version_type = match_obj.group(2)
+ if operator == 'equals':
+ operator = 'STREQUAL'
+ elif operator == 'greaterThan':
+ operator = 'STRGREATER'
+ elif operator == 'lessThan':
+ operator = 'STRLESS'
+
+ version = match_obj.group(3)
+ return '(QT_COMPILER_VERSION_{} {} {})'.format(version_type, operator, version)
+
+ # TODO: Possibly fix for other compilers.
+ pattern = r'(equals|greaterThan|lessThan)\(QT_GCC_([A-Z]+)_VERSION,[ ]*([0-9]+)\)'
+ condition = re.sub(pattern, gcc_version_handler, condition)
+
+ # TODO: the current if(...) replacement makes the parentheses
+ # unbalanced when there are nested expressions.
+ # Need to fix this either with pypi regex recursive regexps,
+ # using pyparsing, or some other proper means of handling
+ # balanced parentheses.
+ condition = re.sub(r'\bif\s*\((.*?)\)', r'\1', condition)
+
+ condition = re.sub(r'\bisEmpty\s*\((.*?)\)', r'\1_ISEMPTY', condition)
+ condition = re.sub(r'\bcontains\s*\((.*?),\s*"?(.*?)"?\)',
+ r'\1___contains___\2', condition)
+ condition = re.sub(r'\bequals\s*\((.*?),\s*"?(.*?)"?\)',
+ r'\1___equals___\2', condition)
+ condition = re.sub(r'\bisEqual\s*\((.*?),\s*"?(.*?)"?\)',
+ r'\1___equals___\2', condition)
+ condition = re.sub(r'\s*==\s*', '___STREQUAL___', condition)
+ condition = re.sub(r'\bexists\s*\((.*?)\)', r'EXISTS \1', condition)
+
+ pattern = r'CONFIG\((debug|release),debug\|release\)'
+ match_result = re.match(pattern, condition)
+ if match_result:
+ build_type = match_result.group(1)
+ if build_type == 'debug':
+ build_type = 'Debug'
+ elif build_type == 'release':
+ build_type = 'Release'
+ condition = re.sub(pattern, '(CMAKE_BUILD_TYPE STREQUAL {})'.format(build_type), condition)
+
+ condition = condition.replace('*', '_x_')
+ condition = condition.replace('.$$', '__ss_')
+ condition = condition.replace('$$', '_ss_')
+
+ condition = condition.replace('!', 'NOT ')
+ condition = condition.replace('&&', ' AND ')
+ condition = condition.replace('|', ' OR ')
+
+ cmake_condition = ''
+ for part in condition.split():
+ # some features contain e.g. linux, that should not be
+ # turned upper case
+ feature = re.match(r"(qtConfig|qtHaveModule)\(([a-zA-Z0-9_-]+)\)",
+ part)
+ if feature:
+ if (feature.group(1) == "qtHaveModule"):
+ part = 'TARGET {}'.format(map_qt_library(feature.group(2)))
+ else:
+ feature_name = featureName(feature.group(2))
+ if feature_name.startswith('system_') and is_known_3rd_party_library(feature_name[7:]):
+ part = 'ON'
+ elif feature == 'dlopen':
+ part = 'ON'
+ else:
+ part = 'QT_FEATURE_' + feature_name
+ else:
+ part = map_platform(part)
+
+ part = part.replace('true', 'ON')
+ part = part.replace('false', 'OFF')
+ cmake_condition += ' ' + part
+ return cmake_condition.strip()
+
+
+def handle_subdir(scope: Scope, cm_fh: typing.IO[str], *,
+ indent: int = 0, is_example: bool=False) -> None:
+ ind = ' ' * indent
+ for sd in scope.get_files('SUBDIRS'):
+ if os.path.isdir(sd):
+ cm_fh.write('{}add_subdirectory({})\n'.format(ind, sd))
+ elif os.path.isfile(sd):
+ subdir_result = parseProFile(sd, debug=False)
+ subdir_scope \
+ = Scope.FromDict(scope, sd,
+ subdir_result.asDict().get('statements'),
+ '', scope.basedir)
+
+ do_include(subdir_scope)
+ cmakeify_scope(subdir_scope, cm_fh, indent=indent, is_example=is_example)
+ elif sd.startswith('-'):
+ cm_fh.write('{}### remove_subdirectory'
+ '("{}")\n'.format(ind, sd[1:]))
+ else:
+ print(' XXXX: SUBDIR {} in {}: Not found.'.format(sd, scope))
+
+ for c in scope.children:
+ cond = c.condition
+ if cond == 'else':
+ cm_fh.write('\n{}else()\n'.format(ind))
+ elif cond:
+ cm_fh.write('\n{}if({})\n'.format(ind, cond))
+
+ handle_subdir(c, cm_fh, indent=indent + 1, is_example=is_example)
+
+ if cond:
+ cm_fh.write('{}endif()\n'.format(ind))
+
+
+def sort_sources(sources: typing.List[str]) -> typing.List[str]:
+ to_sort = {} # type: typing.Dict[str, typing.List[str]]
+ for s in sources:
+ if s is None:
+ continue
+
+ dir = os.path.dirname(s)
+ base = os.path.splitext(os.path.basename(s))[0]
+ if base.endswith('_p'):
+ base = base[:-2]
+ sort_name = os.path.join(dir, base)
+
+ array = to_sort.get(sort_name, [])
+ array.append(s)
+
+ to_sort[sort_name] = array
+
+ lines = []
+ for k in sorted(to_sort.keys()):
+ lines.append(' '.join(sorted(to_sort[k])))
+
+ return lines
+
+
+def _map_libraries_to_cmake(libraries: typing.List[str],
+ known_libraries: typing.Set[str]) -> typing.List[str]:
+ result = [] # type: typing.List[str]
+ is_framework = False
+
+ for l in libraries:
+ if l == '-framework':
+ is_framework = True
+ continue
+ if is_framework:
+ l = '${FW%s}' % l
+ if l.startswith('-l'):
+ l = l[2:]
+
+ if l.startswith('-'):
+ l = '# Remove: {}'.format(l[1:])
+ else:
+ l = map_3rd_party_library(l)
+
+ if not l or l in result or l in known_libraries:
+ continue
+
+ result.append(l)
+ is_framework = False
+
+ return result
+
+
+def extract_cmake_libraries(scope: Scope, *, known_libraries: typing.Set[str]=set()) \
+ -> typing.Tuple[typing.List[str], typing.List[str]]:
+ public_dependencies = [] # type: typing.List[str]
+ private_dependencies = [] # type: typing.List[str]
+
+ for key in ['QMAKE_USE', 'LIBS',]:
+ public_dependencies += scope.expand(key)
+ for key in ['QMAKE_USE_PRIVATE', 'QMAKE_USE_FOR_PRIVATE', 'LIBS_PRIVATE',]:
+ private_dependencies += scope.expand(key)
+
+ for key in ['QT_FOR_PRIVATE',]:
+ private_dependencies += [map_qt_library(q) for q in scope.expand(key)]
+
+ for key in ['QT',]:
+ # Qt public libs: These may include FooPrivate in which case we get
+ # a private dependency on FooPrivate as well as a public dependency on Foo
+ for lib in scope.expand(key):
+ mapped_lib = map_qt_library(lib)
+
+ if mapped_lib.endswith('Private'):
+ private_dependencies.append(mapped_lib)
+ public_dependencies.append(mapped_lib[:-7])
+ else:
+ public_dependencies.append(mapped_lib)
+
+ return (_map_libraries_to_cmake(public_dependencies, known_libraries),
+ _map_libraries_to_cmake(private_dependencies, known_libraries))
+
+
+def write_header(cm_fh: typing.IO[str], name: str,
+ typename: str, *, indent: int = 0):
+ cm_fh.write('{}###########################################'
+ '##########################\n'.format(spaces(indent)))
+ cm_fh.write('{}## {} {}:\n'.format(spaces(indent), name, typename))
+ cm_fh.write('{}###########################################'
+ '##########################\n\n'.format(spaces(indent)))
+
+
+def write_scope_header(cm_fh: typing.IO[str], *, indent: int = 0):
+ cm_fh.write('\n{}## Scopes:\n'.format(spaces(indent)))
+ cm_fh.write('{}###########################################'
+ '##########################\n'.format(spaces(indent)))
+
+
+def write_list(cm_fh: typing.IO[str], entries: typing.List[str],
+ cmake_parameter: str,
+ indent: int = 0, *,
+ header: str = '', footer: str = ''):
+ if not entries:
+ return
+
+ ind = spaces(indent)
+ extra_indent = ''
+
+ if header:
+ cm_fh.write('{}{}'.format(ind, header))
+ extra_indent += ' '
+ if cmake_parameter:
+ cm_fh.write('{}{}{}\n'.format(ind, extra_indent, cmake_parameter))
+ extra_indent += ' '
+ for s in sort_sources(entries):
+ cm_fh.write('{}{}{}\n'.format(ind, extra_indent, s))
+ if footer:
+ cm_fh.write('{}{}\n'.format(ind, footer))
+
+
+def write_source_file_list(cm_fh: typing.IO[str], scope, cmake_parameter: str,
+ keys: typing.List[str], indent: int = 0, *,
+ header: str = '', footer: str = ''):
+ # collect sources
+ sources: typing.List[str] = []
+ for key in keys:
+ sources += scope.get_files(key, use_vpath=True)
+
+ write_list(cm_fh, sources, cmake_parameter, indent, header=header, footer=footer)
+
+
+def write_all_source_file_lists(cm_fh: typing.IO[str], scope: Scope, header: str, *,
+ indent: int = 0, footer: str = '',
+ extra_keys: typing.Optional[typing.List[str]] = None):
+ if extra_keys is None:
+ extra_keys = []
+ write_source_file_list(cm_fh, scope, header,
+ ['SOURCES', 'HEADERS', 'OBJECTIVE_SOURCES', 'NO_PCH_SOURCES', 'FORMS'] + extra_keys,
+ indent, footer=footer)
+
+
+def write_defines(cm_fh: typing.IO[str], scope: Scope, cmake_parameter: str, *,
+ indent: int = 0, footer: str = ''):
+ defines = scope.expand('DEFINES')
+ defines += [d[2:] for d in scope.expand('QMAKE_CXXFLAGS') if d.startswith('-D')]
+ defines = [d.replace('=\\\\\\"$$PWD/\\\\\\"',
+ '="${CMAKE_CURRENT_SOURCE_DIR}/"') for d in defines]
+
+ write_list(cm_fh, defines, cmake_parameter, indent, footer=footer)
+
+
+def write_include_paths(cm_fh: typing.IO[str], scope: Scope, cmake_parameter: str, *,
+ indent: int = 0, footer: str = ''):
+ includes = [i.rstrip('/') or ('/') for i in scope.get_files('INCLUDEPATH')]
+
+ write_list(cm_fh, includes, cmake_parameter, indent, footer=footer)
+
+
+def write_compile_options(cm_fh: typing.IO[str], scope: Scope, cmake_parameter: str, *,
+ indent: int = 0, footer: str = ''):
+ compile_options = [d for d in scope.expand('QMAKE_CXXFLAGS') if not d.startswith('-D')]
+
+ write_list(cm_fh, compile_options, cmake_parameter, indent, footer=footer)
+
+
+def write_library_section(cm_fh: typing.IO[str], scope: Scope, *,
+ indent: int = 0, known_libraries: typing.Set[str]=set()):
+ (public_dependencies, private_dependencies) \
+ = extract_cmake_libraries(scope, known_libraries=known_libraries)
+
+ write_list(cm_fh, private_dependencies, 'LIBRARIES', indent + 1)
+ write_list(cm_fh, public_dependencies, 'PUBLIC_LIBRARIES', indent + 1)
+
+
+def write_autogen_section(cm_fh: typing.IO[str], scope: Scope, *,
+ indent: int = 0):
+ forms = scope.get_files('FORMS')
+ if forms:
+ write_list(cm_fh, ['uic'], 'ENABLE_AUTOGEN_TOOLS', indent)
+
+
+def write_sources_section(cm_fh: typing.IO[str], scope: Scope, *,
+ indent: int = 0, known_libraries=set()):
+ ind = spaces(indent)
+
+ # mark RESOURCES as visited:
+ scope.get('RESOURCES')
+
+ write_all_source_file_lists(cm_fh, scope, 'SOURCES', indent=indent + 1)
+
+ write_source_file_list(cm_fh, scope, 'DBUS_ADAPTOR_SOURCES', ['DBUS_ADAPTORS',], indent + 1)
+ dbus_adaptor_flags = scope.expand('QDBUSXML2CPP_ADAPTOR_HEADER_FLAGS')
+ if dbus_adaptor_flags:
+ cm_fh.write('{} DBUS_ADAPTOR_FLAGS\n'.format(ind))
+ cm_fh.write('{} "{}"\n'.format(ind, '" "'.join(dbus_adaptor_flags)))
+
+ write_source_file_list(cm_fh, scope, 'DBUS_INTERFACE_SOURCES', ['DBUS_INTERFACES',], indent + 1)
+ dbus_interface_flags = scope.expand('QDBUSXML2CPP_INTERFACE_HEADER_FLAGS')
+ if dbus_interface_flags:
+ cm_fh.write('{} DBUS_INTERFACE_FLAGS\n'.format(ind))
+ cm_fh.write('{} "{}"\n'.format(ind, '" "'.join(dbus_interface_flags)))
+
+ write_defines(cm_fh, scope, 'DEFINES', indent=indent + 1)
+
+ write_include_paths(cm_fh, scope, 'INCLUDE_DIRECTORIES', indent=indent + 1)
+
+ write_library_section(cm_fh, scope, indent=indent, known_libraries=known_libraries)
+
+ write_compile_options(cm_fh, scope, 'COMPILE_OPTIONS', indent=indent + 1)
+
+ write_autogen_section(cm_fh, scope, indent=indent + 1)
+
+ link_options = scope.get('QMAKE_LFLAGS')
+ if link_options:
+ cm_fh.write('{} LINK_OPTIONS\n'.format(ind))
+ for lo in link_options:
+ cm_fh.write('{} "{}"\n'.format(ind, lo))
+
+ moc_options = scope.get('QMAKE_MOC_OPTIONS')
+ if moc_options:
+ cm_fh.write('{} MOC_OPTIONS\n'.format(ind))
+ for mo in moc_options:
+ cm_fh.write('{} "{}"\n'.format(ind, mo))
+
+
+def is_simple_condition(condition: str) -> bool:
+ return ' ' not in condition \
+ or (condition.startswith('NOT ') and ' ' not in condition[4:])
+
+
+def write_ignored_keys(scope: Scope, indent: str) -> str:
+ result = ''
+ ignored_keys = scope.keys - scope.visited_keys
+ for k in sorted(ignored_keys):
+ if k == '_INCLUDED' or k == 'TARGET' or k == 'QMAKE_DOCS' or k == 'QT_SOURCE_TREE' \
+ or k == 'QT_BUILD_TREE' or k == 'TRACEPOINT_PROVIDER':
+ # All these keys are actually reported already
+ continue
+ values = scope.get(k)
+ value_string = '<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 ')
+
+ try:
+ # Generate and simplify condition using sympy:
+ condition_expr = simplify_logic(condition)
+ condition = str(_recursive_simplify(condition_expr))
+
+ # Map back to CMake syntax:
+ condition = condition.replace('~', 'NOT ')
+ condition = condition.replace('&', 'AND')
+ condition = condition.replace('|', 'OR')
+ condition = condition.replace('True', 'ON')
+ condition = condition.replace('False', 'OFF')
+ except:
+ # sympy did not like our input, so leave this condition alone:
+ condition = input_condition
+
+ return condition or 'ON'
+
+
+def recursive_evaluate_scope(scope: Scope, parent_condition: str = '',
+ previous_condition: str = '') -> str:
+ current_condition = scope.condition
+ total_condition = current_condition
+ if total_condition == 'else':
+ assert previous_condition, \
+ "Else branch without previous condition in: %s" % scope.file
+ total_condition = 'NOT ({})'.format(previous_condition)
+ if parent_condition:
+ if not total_condition:
+ total_condition = parent_condition
+ else:
+ total_condition = '({}) AND ({})'.format(parent_condition,
+ total_condition)
+
+ scope.total_condition = simplify_condition(total_condition)
+
+ prev_condition = ''
+ for c in scope.children:
+ prev_condition = recursive_evaluate_scope(c, total_condition,
+ prev_condition)
+
+ return current_condition
+
+
+def map_to_cmake_condition(condition: typing.Optional[str]) -> str:
+ condition = re.sub(r'\bQT_ARCH___equals___([a-zA-Z_0-9]*)',
+ r'(TEST_architecture_arch STREQUAL "\1")', condition or '')
+ condition = re.sub(r'\bQT_ARCH___contains___([a-zA-Z_0-9]*)',
+ r'(TEST_architecture_arch STREQUAL "\1")', condition or '')
+ return condition
+
+
+def write_resources(cm_fh: typing.IO[str], target: str, scope: Scope, indent: int = 0):
+ vpath = scope.expand('VPATH')
+
+ # Handle QRC files by turning them into add_qt_resource:
+ resources = scope.get_files('RESOURCES')
+ qrc_output = ''
+ if resources:
+ qrc_only = True
+ for r in resources:
+ if r.endswith('.qrc'):
+ qrc_output += process_qrc_file(target, r, scope.basedir)
+ else:
+ qrc_only = False
+
+ if not qrc_only:
+ print(' XXXX Ignoring non-QRC file resources.')
+
+ if qrc_output:
+ cm_fh.write('\n# Resources:\n')
+ for line in qrc_output.split('\n'):
+ cm_fh.write(' ' * indent + line + '\n')
+
+
+def write_extend_target(cm_fh: typing.IO[str], target: str,
+ scope: Scope, indent: int = 0):
+ ind = spaces(indent)
+ extend_qt_io_string = io.StringIO()
+ write_sources_section(extend_qt_io_string, scope)
+ extend_qt_string = extend_qt_io_string.getvalue()
+
+ extend_scope = '\n{}extend_target({} CONDITION {}\n' \
+ '{}{})\n'.format(ind, target,
+ map_to_cmake_condition(scope.total_condition),
+ extend_qt_string, ind)
+
+ if not extend_qt_string:
+ extend_scope = '' # Nothing to report, so don't!
+
+ cm_fh.write(extend_scope)
+
+ write_resources(cm_fh, target, scope, indent)
+
+
+def flatten_scopes(scope: Scope) -> typing.List[Scope]:
+ result = [scope] # type: typing.List[Scope]
+ for c in scope.children:
+ result += flatten_scopes(c)
+ return result
+
+
+def merge_scopes(scopes: typing.List[Scope]) -> typing.List[Scope]:
+ result = [] # type: typing.List[Scope]
+
+ # Merge scopes with their parents:
+ known_scopes = {} # type: typing.Mapping[str, Scope]
+ for scope in scopes:
+ total_condition = scope.total_condition
+ assert total_condition
+ if total_condition == 'OFF':
+ # ignore this scope entirely!
+ pass
+ elif total_condition in known_scopes:
+ known_scopes[total_condition].merge(scope)
+ else:
+ # Keep everything else:
+ result.append(scope)
+ known_scopes[total_condition] = scope
+
+ return result
+
+
+def write_simd_part(cm_fh: typing.IO[str], target: str, scope: Scope, indent: int = 0):
+ simd_options = [ 'sse2', 'sse3', 'ssse3', 'sse4_1', 'sse4_2', 'aesni', 'shani', 'avx', 'avx2',
+ 'avx512f', 'avx512cd', 'avx512er', 'avx512pf', 'avx512dq', 'avx512bw',
+ 'avx512vl', 'avx512ifma', 'avx512vbmi', 'f16c', 'rdrnd', 'neon', 'mips_dsp',
+ 'mips_dspr2',
+ 'arch_haswell', 'avx512common', 'avx512core'];
+ for simd in simd_options:
+ SIMD = simd.upper();
+ write_source_file_list(cm_fh, scope, 'SOURCES',
+ ['{}_HEADERS'.format(SIMD),
+ '{}_SOURCES'.format(SIMD),
+ '{}_C_SOURCES'.format(SIMD),
+ '{}_ASM'.format(SIMD)],
+ indent,
+ header = 'add_qt_simd_part({} SIMD {}\n'.format(target, simd),
+ footer = ')\n\n')
+
+
+def write_main_part(cm_fh: typing.IO[str], name: str, typename: str,
+ cmake_function: str, scope: Scope, *,
+ extra_lines: typing.List[str] = [],
+ indent: int = 0, extra_keys: typing.List[str],
+ **kwargs: typing.Any):
+ # Evaluate total condition of all scopes:
+ recursive_evaluate_scope(scope)
+
+ if 'exceptions' in scope.get('CONFIG'):
+ extra_lines.append('EXCEPTIONS')
+
+ # Get a flat list of all scopes but the main one:
+ scopes = flatten_scopes(scope)
+ total_scopes = len(scopes)
+ # Merge scopes based on their conditions:
+ scopes = merge_scopes(scopes)
+
+ assert len(scopes)
+ assert scopes[0].total_condition == 'ON'
+
+ scopes[0].reset_visited_keys()
+ for k in extra_keys:
+ scopes[0].get(k)
+
+ # Now write out the scopes:
+ write_header(cm_fh, name, typename, indent=indent)
+
+ cm_fh.write('{}{}({}\n'.format(spaces(indent), cmake_function, name))
+ for extra_line in extra_lines:
+ cm_fh.write('{} {}\n'.format(spaces(indent), extra_line))
+
+ write_sources_section(cm_fh, scopes[0], indent=indent, **kwargs)
+
+ # Footer:
+ cm_fh.write('{})\n'.format(spaces(indent)))
+
+ write_resources(cm_fh, name, scope, indent)
+
+ write_simd_part(cm_fh, name, scope, indent)
+
+ ignored_keys_report = write_ignored_keys(scopes[0], spaces(indent))
+ if ignored_keys_report:
+ cm_fh.write(ignored_keys_report)
+
+
+ # Scopes:
+ if len(scopes) == 1:
+ return
+
+ write_scope_header(cm_fh, indent=indent)
+
+ for c in scopes[1:]:
+ c.reset_visited_keys()
+ write_extend_target(cm_fh, name, c, indent=indent)
+ ignored_keys_report = write_ignored_keys(c, spaces(indent))
+ if ignored_keys_report:
+ cm_fh.write(ignored_keys_report)
+
+
+def write_module(cm_fh: typing.IO[str], scope: Scope, *,
+ indent: int = 0) -> None:
+ module_name = scope.TARGET
+ if not module_name.startswith('Qt'):
+ print('XXXXXX Module name {} does not start with Qt!'.format(module_name))
+
+ extra = []
+
+ # A module should be static when 'static' is in CONFIG
+ # or when option(host_build) is used, as described in qt_module.prf.
+ is_static = 'static' in scope.get('CONFIG') or 'host_build' in scope.get('_OPTION')
+
+ if is_static:
+ extra.append('STATIC')
+ if 'internal_module' in scope.get('CONFIG'):
+ extra.append('INTERNAL_MODULE')
+ if 'no_module_headers' in scope.get('CONFIG'):
+ extra.append('NO_MODULE_HEADERS')
+ if 'minimal_syncqt' in scope.get('CONFIG'):
+ extra.append('NO_SYNC_QT')
+
+ module_config = scope.get("MODULE_CONFIG")
+ if len(module_config):
+ extra.append('QMAKE_MODULE_CONFIG {}'.format(" ".join(module_config)))
+
+ module_plugin_types = scope.get_files('MODULE_PLUGIN_TYPES')
+ if module_plugin_types:
+ extra.append('PLUGIN_TYPES {}'.format(" ".join(module_plugin_types)))
+
+ write_main_part(cm_fh, module_name[2:], 'Module', 'add_qt_module', scope,
+ extra_lines=extra, indent=indent,
+ known_libraries={}, extra_keys=[])
+
+ if 'qt_tracepoints' in scope.get('CONFIG'):
+ tracepoints = scope.get_files('TRACEPOINT_PROVIDER')
+ cm_fh.write('\n\n{}qt_create_tracepoints({} {})\n'
+ .format(spaces(indent), module_name[2:], ' '.join(tracepoints)))
+
+
+def write_tool(cm_fh: typing.IO[str], scope: Scope, *,
+ indent: int = 0) -> None:
+ tool_name = scope.TARGET
+
+ extra = ['BOOTSTRAP'] if 'force_bootstrap' in scope.get('CONFIG') else []
+
+ write_main_part(cm_fh, tool_name, 'Tool', 'add_qt_tool', scope,
+ indent=indent, known_libraries={'Qt::Core', },
+ extra_lines=extra, extra_keys=['CONFIG'])
+
+
+def write_test(cm_fh: typing.IO[str], scope: Scope, *,
+ indent: int = 0) -> None:
+ test_name = scope.TARGET
+ assert test_name
+
+ write_main_part(cm_fh, test_name, 'Test', 'add_qt_test', scope,
+ indent=indent, known_libraries={'Qt::Core', 'Qt::Test',},
+ extra_keys=[])
+
+
+def write_binary(cm_fh: typing.IO[str], scope: Scope,
+ gui: bool = False, *, indent: int = 0) -> None:
+ binary_name = scope.TARGET
+ assert binary_name
+
+ extra = ['GUI',] if gui else[]
+
+ target_path = scope.get_string('target.path')
+ if target_path:
+ target_path = target_path.replace('$$[QT_INSTALL_EXAMPLES]', '${INSTALL_EXAMPLESDIR}')
+ extra.append('OUTPUT_DIRECTORY "{}"'.format(target_path))
+ if 'target' in scope.get('INSTALLS'):
+ extra.append('INSTALL_DIRECTORY "{}"'.format(target_path))
+
+ write_main_part(cm_fh, binary_name, 'Binary', 'add_qt_executable', scope,
+ extra_lines=extra, indent=indent,
+ known_libraries={'Qt::Core', }, extra_keys=['target.path', 'INSTALLS'])
+
+
+def write_find_package_section(cm_fh: typing.IO[str],
+ public_libs: typing.List[str],
+ private_libs: typing.List[str], *, indent: int=0):
+ packages = [] # type: typing.List[LibraryMapping]
+ all_libs = public_libs + private_libs
+
+ for l in all_libs:
+ info = find_library_info_for_target(l)
+ if info and info not in packages:
+ packages.append(info)
+
+ ind = spaces(indent)
+
+ for p in packages:
+ cm_fh.write(generate_find_package_info(p, use_qt_find_package=False, indent=indent))
+
+ if packages:
+ cm_fh.write('\n')
+
+
+def write_example(cm_fh: typing.IO[str], scope: Scope,
+ gui: bool = False, *, indent: int = 0) -> None:
+ binary_name = scope.TARGET
+ assert binary_name
+
+ cm_fh.write('cmake_minimum_required(VERSION 3.14)\n' +
+ 'project({} LANGUAGES CXX)\n\n'.format(binary_name) +
+ 'set(CMAKE_INCLUDE_CURRENT_DIR ON)\n\n' +
+ 'set(CMAKE_AUTOMOC ON)\n' +
+ 'set(CMAKE_AUTORCC ON)\n' +
+ 'set(CMAKE_AUTOUIC ON)\n\n' +
+ 'set(INSTALL_EXAMPLEDIR "examples")\n\n')
+
+ (public_libs, private_libs) = extract_cmake_libraries(scope)
+ write_find_package_section(cm_fh, public_libs, private_libs, indent=indent)
+
+ add_executable = 'add_{}executable({}'.format("qt_gui_" if gui else "", binary_name);
+
+ write_all_source_file_lists(cm_fh, scope, add_executable, indent=0, extra_keys=['RESOURCES'])
+
+ cm_fh.write(')\n')
+
+ write_include_paths(cm_fh, scope, 'target_include_directories({} PUBLIC'.format(binary_name),
+ indent=0, footer=')')
+ write_defines(cm_fh, scope, 'target_compile_definitions({} PUBLIC'.format(binary_name),
+ indent=0, footer=')')
+ write_list(cm_fh, private_libs, '', indent=indent,
+ header='target_link_libraries({} PRIVATE\n'.format(binary_name), footer=')')
+ write_list(cm_fh, public_libs, '', indent=indent,
+ header='target_link_libraries({} PUBLIC\n'.format(binary_name), footer=')')
+ write_compile_options(cm_fh, scope, 'target_compile_options({}'.format(binary_name),
+ indent=0, footer=')')
+
+ cm_fh.write('\ninstall(TARGETS {}\n'.format(binary_name) +
+ ' RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"\n' +
+ ' BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"\n' +
+ ' LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"\n' +
+ ')\n')
+
+
+def write_plugin(cm_fh, scope, *, indent: int = 0):
+ plugin_name = scope.TARGET
+ assert plugin_name
+
+ extra = []
+
+ plugin_type = scope.get_string('PLUGIN_TYPE')
+ if plugin_type:
+ extra.append('TYPE {}'.format(plugin_type))
+
+ plugin_class_name = scope.get_string('PLUGIN_CLASS_NAME')
+ if plugin_class_name:
+ extra.append('CLASS_NAME {}'.format(plugin_class_name))
+
+ write_main_part(cm_fh, plugin_name, 'Plugin', 'add_qt_plugin', scope,
+ indent=indent, extra_lines=extra, known_libraries={}, extra_keys=[])
+
+
+def handle_app_or_lib(scope: Scope, cm_fh: typing.IO[str], *,
+ indent: int = 0, is_example: bool=False) -> None:
+ assert scope.TEMPLATE in ('app', 'lib')
+
+ is_lib = scope.TEMPLATE == 'lib'
+ is_plugin = any('qt_plugin' == s for s in scope.get('_LOADED'))
+
+ if is_lib or 'qt_module' in scope.get('_LOADED'):
+ assert not is_example
+ write_module(cm_fh, scope, indent=indent)
+ elif is_plugin:
+ assert not is_example
+ write_plugin(cm_fh, scope, indent=indent)
+ elif 'qt_tool' in scope.get('_LOADED'):
+ assert not is_example
+ write_tool(cm_fh, scope, indent=indent)
+ else:
+ if 'testcase' in scope.get('CONFIG') \
+ or 'testlib' in scope.get('CONFIG'):
+ assert not is_example
+ write_test(cm_fh, scope, indent=indent)
+ else:
+ config = scope.get('CONFIG')
+ gui = all(val not in config for val in ['console', 'cmdline'])
+ if is_example:
+ write_example(cm_fh, scope, gui, indent=indent)
+ else:
+ write_binary(cm_fh, scope, gui, indent=indent)
+
+ ind = spaces(indent)
+ write_source_file_list(cm_fh, scope, '',
+ ['QMAKE_DOCS',],
+ indent,
+ header = 'add_qt_docs(\n',
+ footer = ')\n')
+
+
+def cmakeify_scope(scope: Scope, cm_fh: typing.IO[str], *,
+ indent: int = 0, is_example: bool=False) -> None:
+ template = scope.TEMPLATE
+ if template == 'subdirs':
+ handle_subdir(scope, cm_fh, indent=indent, is_example=is_example)
+ elif template in ('app', 'lib'):
+ handle_app_or_lib(scope, cm_fh, indent=indent, is_example=is_example)
+ else:
+ print(' XXXX: {}: Template type {} not yet supported.'
+ .format(scope.file, template))
+
+
+def generate_new_cmakelists(scope: Scope, *, is_example: bool=False) -> None:
+ print('Generating CMakeLists.gen.txt')
+ with open(scope.generated_cmake_lists_path, 'w') as cm_fh:
+ assert scope.file
+ cm_fh.write('# Generated from {}.\n\n'
+ .format(os.path.basename(scope.file)))
+ cmakeify_scope(scope, cm_fh, is_example=is_example)
+
+
+def do_include(scope: Scope, *, debug: bool = False) -> None:
+ for c in scope.children:
+ do_include(c)
+
+ for include_file in scope.get_files('_INCLUDED', is_include=True):
+ if not include_file:
+ continue
+ if not os.path.isfile(include_file):
+ print(' XXXX: Failed to include {}.'.format(include_file))
+ continue
+
+ include_result = parseProFile(include_file, debug=debug)
+ include_scope \
+ = Scope.FromDict(None, include_file,
+ include_result.asDict().get('statements'),
+ '', scope.basedir) # This scope will be merged into scope!
+
+ do_include(include_scope)
+
+ scope.merge(include_scope)
+
+
+def copy_generated_file_to_final_location(scope: Scope, keep_temporary_files=False) -> None:
+ print('Copying {} to {}'.format(scope.generated_cmake_lists_path,
+ scope.original_cmake_lists_path))
+ copyfile(scope.generated_cmake_lists_path, scope.original_cmake_lists_path)
+ if not keep_temporary_files:
+ os.remove(scope.generated_cmake_lists_path)
+
+
+def main() -> None:
+ args = _parse_commandline()
+
+ debug_parsing = args.debug_parser or args.debug
+
+ backup_current_dir = os.getcwd()
+
+ for file in args.files:
+ new_current_dir = os.path.dirname(file)
+ file_relative_path = os.path.basename(file)
+ if new_current_dir:
+ os.chdir(new_current_dir)
+
+ parseresult = parseProFile(file_relative_path, debug=debug_parsing)
+
+ if args.debug_parse_result or args.debug:
+ print('\n\n#### Parser result:')
+ print(parseresult)
+ print('\n#### End of parser result.\n')
+ if args.debug_parse_dictionary or args.debug:
+ print('\n\n####Parser result dictionary:')
+ print(parseresult.asDict())
+ print('\n#### End of parser result dictionary.\n')
+
+ file_scope = Scope.FromDict(None, file_relative_path,
+ parseresult.asDict().get('statements'))
+
+ if args.debug_pro_structure or args.debug:
+ print('\n\n#### .pro/.pri file structure:')
+ file_scope.dump()
+ print('\n#### End of .pro/.pri file structure.\n')
+
+ do_include(file_scope, debug=debug_parsing)
+
+ if args.debug_full_pro_structure or args.debug:
+ print('\n\n#### Full .pro/.pri file structure:')
+ file_scope.dump()
+ print('\n#### End of full .pro/.pri file structure.\n')
+
+ generate_new_cmakelists(file_scope, is_example=args.is_example)
+
+ copy_generated_file = True
+ if not args.skip_special_case_preservation:
+ debug_special_case = args.debug_special_case_preservation or args.debug
+ handler = SpecialCaseHandler(file_scope.original_cmake_lists_path,
+ file_scope.generated_cmake_lists_path,
+ file_scope.basedir,
+ keep_temporary_files=args.keep_temporary_files,
+ debug=debug_special_case)
+
+ copy_generated_file = handler.handle_special_cases()
+
+ if copy_generated_file:
+ copy_generated_file_to_final_location(file_scope,
+ keep_temporary_files=args.keep_temporary_files)
+ os.chdir(backup_current_dir)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/util/cmake/pro_conversion_rate.py b/util/cmake/pro_conversion_rate.py
new file mode 100755
index 0000000000..740e834ca5
--- /dev/null
+++ b/util/cmake/pro_conversion_rate.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 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
+
+"""
+This utility script shows statistics about
+converted .pro -> CMakeLists.txt files.
+
+To execute: python3 pro_conversion_rate.py <src dir>
+where <src dir> can be any qt source directory. For better statistics,
+specify a module root source dir (like ./qtbase or ./qtsvg).
+
+"""
+
+from argparse import ArgumentParser
+
+import os
+import typing
+from timeit import default_timer
+
+
+def _parse_commandline():
+ parser = ArgumentParser(description='Find pro files for which there are no CMakeLists.txt.')
+ parser.add_argument('source_directory', metavar='<src dir>', type=str,
+ help='The source directory')
+
+ return parser.parse_args()
+
+
+class Blacklist:
+ """ Class to check if a certain dir_name / dir_path is blacklisted """
+
+ def __init__(self, names: typing.List[str], path_parts: typing.List[str]):
+ self.names = names
+ self.path_parts = path_parts
+
+ # The lookup algorithm
+ self.lookup = self.is_blacklisted_part
+ self.tree = None
+
+ try:
+ # If package is available, use Aho-Corasick algorithm,
+ from ahocorapy.keywordtree import KeywordTree
+ self.tree = KeywordTree(case_insensitive=True)
+
+ for p in self.path_parts:
+ self.tree.add(p)
+ self.tree.finalize()
+
+ self.lookup = self.is_blacklisted_part_aho
+ except ImportError:
+ pass
+
+ def is_blacklisted(self, dir_name: str, dir_path: str) -> bool:
+ # First check if exact dir name is blacklisted.
+ if dir_name in self.names:
+ return True
+
+ # Check if a path part is blacklisted (e.g. util/cmake)
+ return self.lookup(dir_path)
+
+ def is_blacklisted_part(self, dir_path: str) -> bool:
+ if any(part in dir_path for part in self.path_parts):
+ return True
+ return False
+
+ def is_blacklisted_part_aho(self, dir_path: str) -> bool:
+ return self.tree.search(dir_path) is not None
+
+
+def recursive_scan(path: str, extension: str, result_paths: typing.List[str], blacklist: Blacklist):
+ """ Find files ending with a certain extension, filtering out blacklisted entries """
+ try:
+ for entry in os.scandir(path):
+ entry: os.DirEntry = entry
+
+ if entry.is_file() and entry.path.endswith(extension):
+ result_paths.append(entry.path)
+ elif entry.is_dir():
+ if blacklist.is_blacklisted(entry.name, entry.path):
+ continue
+ recursive_scan(entry.path, extension, result_paths, blacklist)
+ except Exception as e:
+ print(e)
+
+
+def check_for_cmake_project(pro_path: str) -> bool:
+ pro_dir_name = os.path.dirname(pro_path)
+ cmake_project_path = os.path.join(pro_dir_name, "CMakeLists.txt")
+ return os.path.exists(cmake_project_path)
+
+
+def compute_stats(src_path: str, pros_with_missing_project: typing.List[str],
+ total_pros: int, existing_pros: int, missing_pros: int) -> dict:
+ stats = {}
+ stats['total projects'] = {'label': 'Total pro files found',
+ 'value': total_pros}
+ stats['existing projects'] = {'label': 'Existing CMakeLists.txt files found',
+ 'value': existing_pros}
+ stats['missing projects'] = {'label': 'Missing CMakeLists.txt files found',
+ 'value': missing_pros}
+ stats['missing examples'] = {'label': 'Missing examples', 'value': 0}
+ stats['missing tests'] = {'label': 'Missing tests', 'value': 0}
+ stats['missing src'] = {'label': 'Missing src/**/**', 'value': 0}
+ stats['missing plugins'] = {'label': 'Missing plugins', 'value': 0}
+
+ for p in pros_with_missing_project:
+ rel_path = os.path.relpath(p, src_path)
+ if rel_path.startswith("examples"):
+ stats['missing examples']['value'] += 1
+ elif rel_path.startswith("tests"):
+ stats['missing tests']['value'] += 1
+ elif rel_path.startswith(os.path.join("src", "plugins")):
+ stats['missing plugins']['value'] += 1
+ elif rel_path.startswith("src"):
+ stats['missing src']['value'] += 1
+
+ for stat in stats:
+ if stats[stat]['value'] > 0:
+ stats[stat]['percentage'] = round(stats[stat]['value'] * 100 / total_pros, 2)
+ return stats
+
+
+def print_stats(src_path: str, pros_with_missing_project: typing.List[str], stats: dict,
+ scan_time: float, script_time: float):
+
+ if stats['total projects']['value'] == 0:
+ print("No .pro files found. Did you specify a correct source path?")
+ return
+
+ if stats['total projects']['value'] == stats['existing projects']['value']:
+ print("All projects were converted.")
+ else:
+ print("Missing CMakeLists.txt files for the following projects: \n")
+
+ for p in pros_with_missing_project:
+ rel_path = os.path.relpath(p, src_path)
+ print(rel_path)
+
+ print("\nStatistics: \n")
+
+ for stat in stats:
+ if stats[stat]['value'] > 0:
+ print("{:<40}: {} ({}%)".format(stats[stat]['label'],
+ stats[stat]['value'],
+ stats[stat]['percentage']))
+
+ print("\n{:<40}: {:.10f} seconds".format("Scan time", scan_time))
+ print("{:<40}: {:.10f} seconds".format("Total script time", script_time))
+
+
+def main():
+ args = _parse_commandline()
+ src_path = os.path.abspath(args.source_directory)
+ pro_paths = []
+
+ extension = ".pro"
+
+ blacklist_names = ["config.tests", "doc", "3rdparty", "angle"]
+ blacklist_path_parts = [
+ os.path.join("util", "cmake")
+ ]
+
+ script_start_time = default_timer()
+ blacklist = Blacklist(blacklist_names, blacklist_path_parts)
+
+ scan_time_start = default_timer()
+ recursive_scan(src_path, extension, pro_paths, blacklist)
+ scan_time_end = default_timer()
+ scan_time = scan_time_end - scan_time_start
+
+ total_pros = len(pro_paths)
+
+ pros_with_missing_project = []
+ for pro_path in pro_paths:
+ if not check_for_cmake_project(pro_path):
+ pros_with_missing_project.append(pro_path)
+
+ missing_pros = len(pros_with_missing_project)
+ existing_pros = total_pros - missing_pros
+
+ stats = compute_stats(src_path, pros_with_missing_project, total_pros, existing_pros,
+ missing_pros)
+ script_end_time = default_timer()
+ script_time = script_end_time - script_start_time
+
+ print_stats(src_path, pros_with_missing_project, stats, scan_time, script_time)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/util/cmake/run_pro2cmake.py b/util/cmake/run_pro2cmake.py
new file mode 100755
index 0000000000..bc64fb3fbb
--- /dev/null
+++ b/util/cmake/run_pro2cmake.py
@@ -0,0 +1,167 @@
+#!/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$
+##
+#############################################################################
+
+import glob
+import os
+import subprocess
+import concurrent.futures
+import typing
+import argparse
+from argparse import ArgumentParser
+
+
+def parse_command_line():
+ parser = ArgumentParser(description='Run pro2cmake on all .pro files recursively in given path.')
+ parser.add_argument('--only-existing', dest='only_existing', action='store_true',
+ help='Run pro2cmake only on .pro files that already have a CMakeLists.txt.')
+ parser.add_argument('--only-qtbase-main-modules', dest='only_qtbase_main_modules', action='store_true',
+ help='Run pro2cmake only on the main modules in qtbase.')
+ parser.add_argument('path', metavar='<path>', type=str,
+ help='The path where to look for .pro files.')
+
+ return parser.parse_args()
+
+
+def find_all_pro_files(base_path: str, args: argparse.Namespace):
+
+ def sorter(pro_file: str) -> str:
+ """ Sorter that tries to prioritize main pro files in a directory. """
+ pro_file_without_suffix = pro_file.rsplit('/', 1)[-1][:-4]
+ dir_name = os.path.dirname(pro_file)
+ if dir_name.endswith('/' + pro_file_without_suffix):
+ return dir_name
+ return dir_name + "/__" + pro_file
+
+ all_files = []
+ previous_dir_name: str = None
+
+ print('Finding .pro files.')
+ glob_result = glob.glob(os.path.join(base_path, '**/*.pro'), recursive=True)
+
+ def cmake_lists_exists_filter(path):
+ path_dir_name = os.path.dirname(path)
+ if os.path.exists(os.path.join(path_dir_name, 'CMakeLists.txt')):
+ return True
+ return False
+
+ def qtbase_main_modules_filter(path):
+ main_modules = [
+ 'corelib',
+ 'network',
+ 'gui',
+ 'widgets',
+ 'testlib',
+ 'printsupport',
+ 'opengl',
+ 'sql',
+ 'dbus',
+ 'concurrent',
+ 'xml',
+ ]
+ path_suffixes = ['src/{}/{}.pro'.format(m, m, '.pro') for m in main_modules]
+
+ for path_suffix in path_suffixes:
+ if path.endswith(path_suffix):
+ return True
+ return False
+
+ filter_result = glob_result
+ filter_func = None
+ if args.only_existing:
+ filter_func = cmake_lists_exists_filter
+ elif args.only_qtbase_main_modules:
+ filter_func = qtbase_main_modules_filter
+
+ if filter_func:
+ print('Filtering.')
+ filter_result = [p for p in filter_result if filter_func(p)]
+
+ for pro_file in sorted(filter_result, key=sorter):
+ dir_name = os.path.dirname(pro_file)
+ if dir_name == previous_dir_name:
+ print("Skipping:", pro_file)
+ else:
+ all_files.append(pro_file)
+ previous_dir_name = dir_name
+ return all_files
+
+
+def run(all_files: typing.List[str], pro2cmake: str, args: argparse.Namespace) -> typing.List[str]:
+ failed_files = []
+ files_count = len(all_files)
+ workers = (os.cpu_count() or 1)
+
+ if args.only_qtbase_main_modules:
+ # qtbase main modules take longer than usual to process.
+ workers = 2
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=workers, initializer=os.nice, initargs=(10,)) as pool:
+ print('Firing up thread pool executor.')
+
+ def _process_a_file(data: typing.Tuple[str, int, int]) -> typing.Tuple[int, str, str]:
+ filename, index, total = data
+ result = subprocess.run((pro2cmake, os.path.basename(filename)),
+ cwd=os.path.dirname(filename),
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ stdout = 'Converted[{}/{}]: {}\n'.format(index, total, filename)
+ return result.returncode, filename, stdout + result.stdout.decode()
+
+ for return_code, filename, stdout in pool.map(_process_a_file,
+ zip(all_files,
+ range(1, files_count + 1),
+ (files_count for _ in all_files))):
+ if return_code:
+ failed_files.append(filename)
+ print(stdout)
+
+ return failed_files
+
+
+def main() -> None:
+ args = parse_command_line()
+
+ script_path = os.path.dirname(os.path.abspath(__file__))
+ pro2cmake = os.path.join(script_path, 'pro2cmake.py')
+ base_path = args.path
+
+ all_files = find_all_pro_files(base_path, args)
+ files_count = len(all_files)
+ failed_files = run(all_files, pro2cmake, args)
+ if len(all_files) == 0:
+ print('No files found.')
+
+ if failed_files:
+ print('The following files were not successfully '
+ 'converted ({} of {}):'.format(len(failed_files), files_count))
+ for f in failed_files:
+ print(' "{}"'.format(f))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/util/cmake/special_case_helper.py b/util/cmake/special_case_helper.py
new file mode 100644
index 0000000000..b9cb93dce0
--- /dev/null
+++ b/util/cmake/special_case_helper.py
@@ -0,0 +1,397 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 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$
+##
+#############################################################################
+
+"""
+This is a helper script that takes care of reapplying special case
+modifications when regenerating a CMakeLists.txt file using
+pro2cmake.py.
+
+It has two modes of operation:
+1) Dumb "special case" block removal and re-application.
+2) Smart "special case" diff application, using a previously generated
+ "clean" CMakeLists.txt as a source. "clean" in this case means a
+ generated file which has no "special case" modifications.
+
+Both modes use a temporary git repository to compute and reapply
+"special case" diffs.
+
+For the first mode to work, the developer has to mark changes
+with "# special case" markers on every line they want to keep. Or
+enclose blocks of code they want to keep between "# special case begin"
+and "# special case end" markers.
+
+For example:
+
+SOURCES
+ foo.cpp
+ bar.cpp # special case
+
+SOURCES
+ foo1.cpp
+ foo2.cpp
+ # special case begin
+ foo3.cpp
+ foo4.cpp
+ # special case end
+
+The second mode, as mentioned, requires a previous "clean"
+CMakeLists.txt file.
+
+The script can then compute the exact diff between
+a "clean" and "modified" (with special cases) file, and reapply that
+diff to a newly generated "CMakeLists.txt" file.
+
+This implies that we always have to keep a "clean" file alongside the
+"modified" project file for each project (corelib, gui, etc.) So we
+have to commit both files to the repository.
+
+If there is no such "clean" file, we can use the first operation mode
+to generate one. After that, we only have to use the second operation
+mode for the project file in question.
+
+When the script is used, the developer only has to take care of fixing
+the newly generated "modified" file. The "clean" file is automatically
+handled and git add'ed by the script, and will be committed together
+with the "modified" file.
+
+
+"""
+
+import re
+import os
+import subprocess
+import filecmp
+import time
+import typing
+import stat
+
+from shutil import copyfile
+from shutil import rmtree
+
+
+def remove_special_cases(original: str) -> str:
+ # Remove content between the following markers
+ # '# special case begin' and '# special case end'.
+ # This also remove the markers.
+ replaced = re.sub(r'\n[^#\n]*?#[^\n]*?special case begin.*?#[^\n]*special case end[^\n]*?\n',
+ '\n',
+ original,
+ 0,
+ re.DOTALL)
+
+ # Remove individual lines that have the "# special case" marker.
+ replaced = re.sub(r'\n.*#.*special case[^\n]*\n', '\n', replaced)
+ return replaced
+
+
+def read_content_from_file(file_path: str) -> str:
+ with open(file_path, 'r') as file_fd:
+ content = file_fd.read()
+ return content
+
+
+def write_content_to_file(file_path: str, content: str) -> None:
+ with open(file_path, 'w') as file_fd:
+ file_fd.write(content)
+
+
+def resolve_simple_git_conflicts(file_path: str, debug=False) -> None:
+ content = read_content_from_file(file_path)
+ # If the conflict represents the addition of a new content hunk,
+ # keep the content and remove the conflict markers.
+ if debug:
+ print('Resolving simple conflicts automatically.')
+ replaced = re.sub(r'\n<<<<<<< HEAD\n=======(.+?)>>>>>>> master\n', r'\1', content, 0, re.DOTALL)
+ write_content_to_file(file_path, replaced)
+
+
+def copyfile_log(src: str, dst: str, debug=False):
+ if debug:
+ print('Copying {} to {}.'.format(src, dst))
+ copyfile(src, dst)
+
+
+def check_if_git_in_path() -> bool:
+ is_win = os.name == 'nt'
+ for path in os.environ['PATH'].split(os.pathsep):
+ git_path = os.path.join(path, 'git')
+ if is_win:
+ git_path += '.exe'
+ if os.path.isfile(git_path) and os.access(git_path, os.X_OK):
+ return True
+ return False
+
+
+def run_process_quiet(args_string: str, debug=False) -> bool:
+ if debug:
+ print('Running command: "{}\"'.format(args_string))
+ args_list = args_string.split()
+ try:
+ subprocess.run(args_list, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ # git merge with conflicts returns with exit code 1, but that's not
+ # an error for us.
+ if 'git merge' not in args_string:
+ print('Error while running: "{}"\n{}'.format(args_string, e.stdout))
+ return False
+ return True
+
+
+def does_file_have_conflict_markers(file_path: str, debug=False) -> bool:
+ if debug:
+ print('Checking if {} has no leftover conflict markers.'.format(file_path))
+ content_actual = read_content_from_file(file_path)
+ if '<<<<<<< HEAD' in content_actual:
+ print('Conflict markers found in {}. '
+ 'Please remove or solve them first.'.format(file_path))
+ return True
+ return False
+
+
+def create_file_with_no_special_cases(original_file_path: str, no_special_cases_file_path: str, debug=False):
+ """
+ Reads content of original CMakeLists.txt, removes all content
+ between "# special case" markers or lines, saves the result into a
+ new file.
+ """
+ content_actual = read_content_from_file(original_file_path)
+ if debug:
+ print('Removing special case blocks from {}.'.format(original_file_path))
+ content_no_special_cases = remove_special_cases(content_actual)
+
+ if debug:
+ print('Saving original contents of {} '
+ 'with removed special case blocks to {}'.format(original_file_path,
+ no_special_cases_file_path))
+ write_content_to_file(no_special_cases_file_path, content_no_special_cases)
+
+
+def rm_tree_on_error_handler(func: typing.Callable[..., None],
+ path: str, exception_info: tuple):
+ # If the path is read only, try to make it writable, and try
+ # to remove the path again.
+ if not os.access(path, os.W_OK):
+ os.chmod(path, stat.S_IWRITE)
+ func(path)
+ else:
+ print('Error while trying to remove path: {}. Exception: {}'.format(path, exception_info))
+
+
+class SpecialCaseHandler(object):
+
+ def __init__(self,
+ original_file_path: str,
+ generated_file_path: str,
+ base_dir: str,
+ keep_temporary_files=False,
+ debug=False) -> None:
+ self.base_dir = base_dir
+ self.original_file_path = original_file_path
+ self.generated_file_path = generated_file_path
+ self.keep_temporary_files = keep_temporary_files
+ self.use_heuristic = False
+ self.debug = debug
+
+ @property
+ def prev_file_path(self) -> str:
+ return os.path.join(self.base_dir, '.prev_CMakeLists.txt')
+
+ @property
+ def post_merge_file_path(self) -> str:
+ return os.path.join(self.base_dir, 'CMakeLists-post-merge.txt')
+
+ @property
+ def no_special_file_path(self) -> str:
+ return os.path.join(self.base_dir, 'CMakeLists.no-special.txt')
+
+ def apply_git_merge_magic(self, no_special_cases_file_path: str) -> None:
+ # Create new folder for temporary repo, and ch dir into it.
+ repo = os.path.join(self.base_dir, 'tmp_repo')
+ repo_absolute_path = os.path.abspath(repo)
+ txt = 'CMakeLists.txt'
+
+ try:
+ os.mkdir(repo)
+ current_dir = os.getcwd()
+ os.chdir(repo)
+ except Exception as e:
+ print('Failed to create temporary directory for temporary git repo. Exception: {}'
+ .format(e))
+ raise e
+
+ generated_file_path = os.path.join("..", self.generated_file_path)
+ original_file_path = os.path.join("..", self.original_file_path)
+ no_special_cases_file_path = os.path.join("..", no_special_cases_file_path)
+ post_merge_file_path = os.path.join("..", self.post_merge_file_path)
+
+ try:
+ # Create new repo with the "clean" CMakeLists.txt file.
+ run_process_quiet('git init .', debug=self.debug)
+ run_process_quiet('git config user.name fake', debug=self.debug)
+ run_process_quiet('git config user.email fake@fake', debug=self.debug)
+ copyfile_log(no_special_cases_file_path, txt, debug=self.debug)
+ run_process_quiet('git add {}'.format(txt), debug=self.debug)
+ run_process_quiet('git commit -m no_special', debug=self.debug)
+ run_process_quiet('git checkout -b no_special', debug=self.debug)
+
+ # Copy the original "modified" file (with the special cases)
+ # and make a new commit.
+ run_process_quiet('git checkout -b original', debug=self.debug)
+ copyfile_log(original_file_path, txt, debug=self.debug)
+ run_process_quiet('git add {}'.format(txt), debug=self.debug)
+ run_process_quiet('git commit -m original', debug=self.debug)
+
+ # Checkout the commit with "clean" file again, and create a
+ # new branch.
+ run_process_quiet('git checkout no_special', debug=self.debug)
+ run_process_quiet('git checkout -b newly_generated', debug=self.debug)
+
+ # Copy the new "modified" file and make a commit.
+ copyfile_log(generated_file_path, txt, debug=self.debug)
+ run_process_quiet('git add {}'.format(txt), debug=self.debug)
+ run_process_quiet('git commit -m newly_generated', debug=self.debug)
+
+ # Merge the "old" branch with modifications into the "new"
+ # branch with the newly generated file.
+ run_process_quiet('git merge original', debug=self.debug)
+
+ # Resolve some simple conflicts (just remove the markers)
+ # for cases that don't need intervention.
+ resolve_simple_git_conflicts(txt, debug=self.debug)
+
+ # Copy the resulting file from the merge.
+ copyfile_log(txt, post_merge_file_path)
+ except Exception as e:
+ print('Git merge conflict resolution process failed. Exception: {}'.format(e))
+ raise e
+ finally:
+ os.chdir(current_dir)
+
+ # Remove the temporary repo.
+ try:
+ if not self.keep_temporary_files:
+ rmtree(repo_absolute_path, onerror=rm_tree_on_error_handler)
+ except Exception as e:
+ print('Error removing temporary repo. Exception: {}'.format(e))
+
+ def save_next_clean_file(self):
+ files_are_equivalent = filecmp.cmp(self.generated_file_path, self.post_merge_file_path)
+
+ if not files_are_equivalent:
+ # Before overriding the generated file with the post
+ # merge result, save the new "clean" file for future
+ # regenerations.
+ copyfile_log(self.generated_file_path, self.prev_file_path, debug=self.debug)
+
+ # Attempt to git add until we succeed. It can fail when
+ # run_pro2cmake executes pro2cmake in multiple threads, and git
+ # has acquired the index lock.
+ success = False
+ failed_once = False
+ i = 0
+ while not success and i < 20:
+ success = run_process_quiet("git add {}".format(self.prev_file_path),
+ debug=self.debug)
+ if not success:
+ failed_once = True
+ i += 1
+ time.sleep(0.1)
+
+ if failed_once and not success:
+ print('Retrying git add, the index.lock was probably acquired.')
+ if failed_once and success:
+ print('git add succeeded.')
+ elif failed_once and not success:
+ print('git add failed. Make sure to git add {} yourself.'.format(
+ self.prev_file_path))
+
+ def handle_special_cases_helper(self) -> bool:
+ """
+ Uses git to reapply special case modifications to the "new"
+ generated CMakeLists.gen.txt file.
+
+ If use_heuristic is True, a new file is created from the
+ original file, with special cases removed.
+
+ If use_heuristic is False, an existing "clean" file with no
+ special cases is used from a previous conversion. The "clean"
+ file is expected to be in the same folder as the original one.
+ """
+ try:
+ if does_file_have_conflict_markers(self.original_file_path):
+ return False
+
+ if self.use_heuristic:
+ create_file_with_no_special_cases(self.original_file_path,
+ self.no_special_file_path)
+ no_special_cases_file_path = self.no_special_file_path
+ else:
+ no_special_cases_file_path = self.prev_file_path
+
+ if self.debug:
+ print('Using git to reapply special case modifications to newly generated {} '
+ 'file'.format(self.generated_file_path))
+
+ self.apply_git_merge_magic(no_special_cases_file_path)
+ self.save_next_clean_file()
+
+ copyfile_log(self.post_merge_file_path, self.generated_file_path)
+ if not self.keep_temporary_files:
+ os.remove(self.post_merge_file_path)
+
+ print('Special case reapplication using git is complete. '
+ 'Make sure to fix remaining conflict markers.')
+
+ except Exception as e:
+ print('Error occurred while trying to reapply special case modifications: {}'.format(e))
+ return False
+ finally:
+ if not self.keep_temporary_files and self.use_heuristic:
+ os.remove(self.no_special_file_path)
+
+ return True
+
+ def handle_special_cases(self) -> bool:
+ original_file_exists = os.path.isfile(self.original_file_path)
+ prev_file_exists = os.path.isfile(self.prev_file_path)
+ self.use_heuristic = not prev_file_exists
+
+ git_available = check_if_git_in_path()
+ keep_special_cases = original_file_exists and git_available
+
+ if not git_available:
+ print('You need to have git in PATH in order to reapply the special '
+ 'case modifications.')
+
+ copy_generated_file = True
+
+ if keep_special_cases:
+ copy_generated_file = self.handle_special_cases_helper()
+
+ return copy_generated_file
diff --git a/util/cmake/tests/__init__.py b/util/cmake/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/util/cmake/tests/__init__.py
diff --git a/util/cmake/tests/data/comment_scope.pro b/util/cmake/tests/data/comment_scope.pro
new file mode 100644
index 0000000000..be43cad37d
--- /dev/null
+++ b/util/cmake/tests/data/comment_scope.pro
@@ -0,0 +1,6 @@
+# QtCore can't be compiled with -Wl,-no-undefined because it uses the "environ"
+# variable and on FreeBSD and OpenBSD, this variable is in the final executable itself.
+# OpenBSD 6.0 will include environ in libc.
+freebsd|openbsd: QMAKE_LFLAGS_NOUNDEF =
+
+include(animation/animation.pri)
diff --git a/util/cmake/tests/data/complex_assign.pro b/util/cmake/tests/data/complex_assign.pro
new file mode 100644
index 0000000000..d251afcdd5
--- /dev/null
+++ b/util/cmake/tests/data/complex_assign.pro
@@ -0,0 +1,2 @@
+qmake-clean.commands += (cd qmake && $(MAKE) clean ":-(==)-:" '(Foo)' )
+
diff --git a/util/cmake/tests/data/complex_condition.pro b/util/cmake/tests/data/complex_condition.pro
new file mode 100644
index 0000000000..bc3369bd63
--- /dev/null
+++ b/util/cmake/tests/data/complex_condition.pro
@@ -0,0 +1,4 @@
+!system("dbus-send --session --type=signal / local.AutotestCheck.Hello >$$QMAKE_SYSTEM_NULL_DEVICE 2>&1") {
+ SOURCES = dbus.cpp
+}
+
diff --git a/util/cmake/tests/data/complex_values.pro b/util/cmake/tests/data/complex_values.pro
new file mode 100644
index 0000000000..4d747c1dd7
--- /dev/null
+++ b/util/cmake/tests/data/complex_values.pro
@@ -0,0 +1,22 @@
+linux:!static {
+ precompile_header {
+ # we'll get an error if we just use SOURCES +=
+ no_pch_assembler.commands = $$QMAKE_CC -c $(CFLAGS) $(INCPATH) ${QMAKE_FILE_IN} -o ${QMAKE_FILE_OUT}
+ no_pch_assembler.dependency_type = TYPE_C
+ no_pch_assembler.output = ${QMAKE_VAR_OBJECTS_DIR}${QMAKE_FILE_BASE}$${first(QMAKE_EXT_OBJ)}
+ no_pch_assembler.input = NO_PCH_ASM
+ no_pch_assembler.name = compiling[no_pch] ${QMAKE_FILE_IN}
+ silent: no_pch_assembler.commands = @echo compiling[no_pch] ${QMAKE_FILE_IN} && $$no_pch_assembler.commands
+ CMAKE_ANGLE_GLES2_IMPLIB_RELEASE = libGLESv2.$${QMAKE_EXTENSION_STATICLIB}
+ HOST_BINS = $$[QT_HOST_BINS]
+ CMAKE_HOST_DATA_DIR = $$[QT_HOST_DATA/src]/
+ TR_EXCLUDE += ../3rdparty/*
+
+ QMAKE_EXTRA_COMPILERS += no_pch_assembler
+ NO_PCH_ASM += global/minimum-linux.S
+ } else {
+ SOURCES += global/minimum-linux.S
+ }
+ HEADERS += global/minimum-linux_p.h
+}
+
diff --git a/util/cmake/tests/data/condition_without_scope.pro b/util/cmake/tests/data/condition_without_scope.pro
new file mode 100644
index 0000000000..2aa1237c12
--- /dev/null
+++ b/util/cmake/tests/data/condition_without_scope.pro
@@ -0,0 +1,2 @@
+write_file("a", contents)|error()
+
diff --git a/util/cmake/tests/data/contains_scope.pro b/util/cmake/tests/data/contains_scope.pro
new file mode 100644
index 0000000000..0f51350a45
--- /dev/null
+++ b/util/cmake/tests/data/contains_scope.pro
@@ -0,0 +1,4 @@
+contains(DEFINES,QT_EVAL):include(eval.pri)
+
+HOST_BINS = $$[QT_HOST_BINS]
+
diff --git a/util/cmake/tests/data/definetest.pro b/util/cmake/tests/data/definetest.pro
new file mode 100644
index 0000000000..76b63d239f
--- /dev/null
+++ b/util/cmake/tests/data/definetest.pro
@@ -0,0 +1,6 @@
+defineTest(pathIsAbsolute) {
+ p = $$clean_path($$1)
+ !isEmpty(p):isEqual(p, $$absolute_path($$p)): return(true)
+ return(false)
+}
+
diff --git a/util/cmake/tests/data/else.pro b/util/cmake/tests/data/else.pro
new file mode 100644
index 0000000000..bbf9c5ac9f
--- /dev/null
+++ b/util/cmake/tests/data/else.pro
@@ -0,0 +1,6 @@
+
+linux {
+ SOURCES += a.cpp
+} else {
+ SOURCES += b.cpp
+}
diff --git a/util/cmake/tests/data/else2.pro b/util/cmake/tests/data/else2.pro
new file mode 100644
index 0000000000..f2ef36ec28
--- /dev/null
+++ b/util/cmake/tests/data/else2.pro
@@ -0,0 +1,4 @@
+
+osx: A = 1
+else: win32: B = 2
+else: C = 3
diff --git a/util/cmake/tests/data/else3.pro b/util/cmake/tests/data/else3.pro
new file mode 100644
index 0000000000..0de9c2c1d9
--- /dev/null
+++ b/util/cmake/tests/data/else3.pro
@@ -0,0 +1,7 @@
+qtConfig(timezone) {
+ A = 1
+} else:win32 {
+ B = 2
+} else {
+ C = 3
+}
diff --git a/util/cmake/tests/data/else4.pro b/util/cmake/tests/data/else4.pro
new file mode 100644
index 0000000000..9ed676ccfa
--- /dev/null
+++ b/util/cmake/tests/data/else4.pro
@@ -0,0 +1,6 @@
+qtConfig(timezone) {
+ A = 1
+} else:win32: B = 2
+else {
+ C = 3
+}
diff --git a/util/cmake/tests/data/else5.pro b/util/cmake/tests/data/else5.pro
new file mode 100644
index 0000000000..3de76af50a
--- /dev/null
+++ b/util/cmake/tests/data/else5.pro
@@ -0,0 +1,10 @@
+# comments
+qtConfig(timezone) { # bar
+ A = 1
+} else:win32 {
+ B = 2 # foo
+} else { C = 3
+# baz
+ # foobar
+}
+# endcomment
diff --git a/util/cmake/tests/data/else6.pro b/util/cmake/tests/data/else6.pro
new file mode 100644
index 0000000000..9eaa834a19
--- /dev/null
+++ b/util/cmake/tests/data/else6.pro
@@ -0,0 +1,11 @@
+qtConfig(timezone) \
+{
+ A = \
+1
+} \
+else:win32: \
+B = 2
+else: \
+ C \
+= 3
+
diff --git a/util/cmake/tests/data/else7.pro b/util/cmake/tests/data/else7.pro
new file mode 100644
index 0000000000..e663b1c05e
--- /dev/null
+++ b/util/cmake/tests/data/else7.pro
@@ -0,0 +1,2 @@
+msvc:equals(QT_ARCH, i386): QMAKE_LFLAGS += /BASE:0x65000000
+
diff --git a/util/cmake/tests/data/else8.pro b/util/cmake/tests/data/else8.pro
new file mode 100644
index 0000000000..6d4d5f01ed
--- /dev/null
+++ b/util/cmake/tests/data/else8.pro
@@ -0,0 +1,5 @@
+qtConfig(timezone) { A = 1 } else:win32: {\
+B = 2 \
+} else: \
+ C \
+= 3 \
diff --git a/util/cmake/tests/data/escaped_value.pro b/util/cmake/tests/data/escaped_value.pro
new file mode 100644
index 0000000000..7c95b1fc30
--- /dev/null
+++ b/util/cmake/tests/data/escaped_value.pro
@@ -0,0 +1,2 @@
+MODULE_AUX_INCLUDES = \
+ \$\$QT_MODULE_INCLUDE_BASE/QtANGLE
diff --git a/util/cmake/tests/data/for.pro b/util/cmake/tests/data/for.pro
new file mode 100644
index 0000000000..5751432980
--- /dev/null
+++ b/util/cmake/tests/data/for.pro
@@ -0,0 +1,11 @@
+SOURCES = main.cpp
+for (config, SIMD) {
+ uc = $$upper($$config)
+ DEFINES += QT_COMPILER_SUPPORTS_$${uc}
+
+ add_cflags {
+ cflags = QMAKE_CFLAGS_$${uc}
+ !defined($$cflags, var): error("This compiler does not support $${uc}")
+ QMAKE_CXXFLAGS += $$eval($$cflags)
+ }
+}
diff --git a/util/cmake/tests/data/function_if.pro b/util/cmake/tests/data/function_if.pro
new file mode 100644
index 0000000000..9af018f864
--- /dev/null
+++ b/util/cmake/tests/data/function_if.pro
@@ -0,0 +1,4 @@
+pathIsAbsolute($$CMAKE_HOST_DATA_DIR) {
+ CMAKE_HOST_DATA_DIR = $$[QT_HOST_DATA/src]/
+}
+
diff --git a/util/cmake/tests/data/include.pro b/util/cmake/tests/data/include.pro
new file mode 100644
index 0000000000..22d8a40919
--- /dev/null
+++ b/util/cmake/tests/data/include.pro
@@ -0,0 +1,3 @@
+A = 42
+include(foo) # load foo
+B=23
diff --git a/util/cmake/tests/data/lc.pro b/util/cmake/tests/data/lc.pro
new file mode 100644
index 0000000000..def80e7c95
--- /dev/null
+++ b/util/cmake/tests/data/lc.pro
@@ -0,0 +1,10 @@
+TEMPLATE=subdirs
+SUBDIRS=\
+ qmacstyle \
+ qstyle \
+ qstyleoption \
+ qstylesheetstyle \
+
+!qtConfig(private_tests): SUBDIRS -= \
+ qstylesheetstyle \
+
diff --git a/util/cmake/tests/data/lc_with_comment.pro b/util/cmake/tests/data/lc_with_comment.pro
new file mode 100644
index 0000000000..176913dfc8
--- /dev/null
+++ b/util/cmake/tests/data/lc_with_comment.pro
@@ -0,0 +1,22 @@
+SUBDIRS = \
+# dds \
+ tga \
+ wbmp
+
+MYVAR = foo # comment
+MYVAR = foo2# comment
+MYVAR = foo3# comment #
+
+MYVAR = foo4# comment #
+
+##
+#
+#
+##
+
+ #
+ #
+#
+ # #
+
+MYVAR = foo5# comment # #
diff --git a/util/cmake/tests/data/load.pro b/util/cmake/tests/data/load.pro
new file mode 100644
index 0000000000..c9717e9832
--- /dev/null
+++ b/util/cmake/tests/data/load.pro
@@ -0,0 +1,3 @@
+A = 42
+load(foo)# load foo
+B=23
diff --git a/util/cmake/tests/data/multi_condition_divided_by_lc.pro b/util/cmake/tests/data/multi_condition_divided_by_lc.pro
new file mode 100644
index 0000000000..23254231df
--- /dev/null
+++ b/util/cmake/tests/data/multi_condition_divided_by_lc.pro
@@ -0,0 +1,3 @@
+equals(a): \
+ greaterThan(a):flags += 1
+
diff --git a/util/cmake/tests/data/multiline_assign.pro b/util/cmake/tests/data/multiline_assign.pro
new file mode 100644
index 0000000000..42a3d0a674
--- /dev/null
+++ b/util/cmake/tests/data/multiline_assign.pro
@@ -0,0 +1,4 @@
+A = 42 \
+ 43 \
+ 44
+B=23
diff --git a/util/cmake/tests/data/nested_function_calls.pro b/util/cmake/tests/data/nested_function_calls.pro
new file mode 100644
index 0000000000..5ecc53f1cc
--- /dev/null
+++ b/util/cmake/tests/data/nested_function_calls.pro
@@ -0,0 +1,2 @@
+requires(qtConfig(dlopen))
+
diff --git a/util/cmake/tests/data/quoted.pro b/util/cmake/tests/data/quoted.pro
new file mode 100644
index 0000000000..61682aa0d0
--- /dev/null
+++ b/util/cmake/tests/data/quoted.pro
@@ -0,0 +1,5 @@
+if(linux*|hurd*):!cross_compile:!static:!*-armcc* {
+ prog=$$quote(if (/program interpreter: (.*)]/) { print $1; })
+ DEFINES += ELF_INTERPRETER=\\\"$$system(LC_ALL=C readelf -l /bin/ls | perl -n -e \'$$prog\')\\\"
+}
+
diff --git a/util/cmake/tests/data/single_line_for.pro b/util/cmake/tests/data/single_line_for.pro
new file mode 100644
index 0000000000..806d08a49c
--- /dev/null
+++ b/util/cmake/tests/data/single_line_for.pro
@@ -0,0 +1,4 @@
+for(d, sd): \
+ exists($$d/$${d}.pro): \
+ SUBDIRS += $$d
+
diff --git a/util/cmake/tests/data/sql.pro b/util/cmake/tests/data/sql.pro
new file mode 100644
index 0000000000..a9d7fc7c5a
--- /dev/null
+++ b/util/cmake/tests/data/sql.pro
@@ -0,0 +1,3 @@
+TEMPLATE = subdirs
+SUBDIRS = \
+ kernel \
diff --git a/util/cmake/tests/data/standardpaths.pro b/util/cmake/tests/data/standardpaths.pro
new file mode 100644
index 0000000000..4b45788e4f
--- /dev/null
+++ b/util/cmake/tests/data/standardpaths.pro
@@ -0,0 +1,17 @@
+win32 {
+ !winrt {
+ SOURCES +=io/qstandardpaths_win.cpp
+ } else {
+ SOURCES +=io/qstandardpaths_winrt.cpp
+ }
+} else:unix {
+ mac {
+ OBJECTIVE_SOURCES += io/qstandardpaths_mac.mm
+ } else:android:!android-embedded {
+ SOURCES += io/qstandardpaths_android.cpp
+ } else:haiku {
+ SOURCES += io/qstandardpaths_haiku.cpp
+ } else {
+ SOURCES += io/qstandardpaths_unix.cpp
+ }
+}
diff --git a/util/cmake/tests/data/unset.pro b/util/cmake/tests/data/unset.pro
new file mode 100644
index 0000000000..7ffb0582f1
--- /dev/null
+++ b/util/cmake/tests/data/unset.pro
@@ -0,0 +1,2 @@
+unset(f16c_cxx)
+
diff --git a/util/cmake/tests/test_lc_fixup.py b/util/cmake/tests/test_lc_fixup.py
new file mode 100755
index 0000000000..841e11615e
--- /dev/null
+++ b/util/cmake/tests/test_lc_fixup.py
@@ -0,0 +1,46 @@
+#!/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 pro2cmake import fixup_linecontinuation
+
+
+def test_no_change():
+ input = "test \\\nline2\n line3"
+ output = "test line2\n line3"
+ result = fixup_linecontinuation(input)
+ assert output == result
+
+
+def test_fix():
+ input = "test \\\t\nline2\\\n line3\\ \nline4 \\ \t\nline5\\\n\n\n"
+ output = "test line2 line3 line4 line5 \n\n"
+ result = fixup_linecontinuation(input)
+ assert output == result
+
+
diff --git a/util/cmake/tests/test_logic_mapping.py b/util/cmake/tests/test_logic_mapping.py
new file mode 100755
index 0000000000..c477aa8351
--- /dev/null
+++ b/util/cmake/tests/test_logic_mapping.py
@@ -0,0 +1,186 @@
+#!/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 pro2cmake import simplify_condition
+
+
+def validate_simplify(input: str, expected: str) -> None:
+ output = simplify_condition(input)
+ assert output == expected
+
+
+def validate_simplify_unchanged(input: str) -> None:
+ validate_simplify(input, input)
+
+
+def test_simplify_on():
+ validate_simplify_unchanged('ON')
+
+
+def test_simplify_off():
+ validate_simplify_unchanged('OFF')
+
+
+def test_simplify_not_on():
+ validate_simplify('NOT ON', 'OFF')
+
+
+def test_simplify_not_off():
+ validate_simplify('NOT OFF', 'ON')
+
+
+def test_simplify_isEmpty():
+ validate_simplify_unchanged('isEmpty(foo)')
+
+
+def test_simplify_not_isEmpty():
+ validate_simplify_unchanged('NOT isEmpty(foo)')
+
+
+def test_simplify_simple_and():
+ validate_simplify_unchanged('QT_FEATURE_bar AND QT_FEATURE_foo')
+
+
+def test_simplify_simple_or():
+ validate_simplify_unchanged('QT_FEATURE_bar OR QT_FEATURE_foo')
+
+
+def test_simplify_simple_not():
+ validate_simplify_unchanged('NOT QT_FEATURE_foo')
+
+
+def test_simplify_simple_and_reorder():
+ validate_simplify('QT_FEATURE_foo AND QT_FEATURE_bar', 'QT_FEATURE_bar AND QT_FEATURE_foo')
+
+
+def test_simplify_simple_or_reorder():
+ validate_simplify('QT_FEATURE_foo OR QT_FEATURE_bar', 'QT_FEATURE_bar OR QT_FEATURE_foo')
+
+
+def test_simplify_unix_or_win32():
+ validate_simplify('WIN32 OR UNIX', 'ON')
+
+
+def test_simplify_unix_or_win32_or_foobar_or_barfoo():
+ validate_simplify('WIN32 OR UNIX OR foobar OR barfoo', 'ON')
+
+
+def test_simplify_not_not_bar():
+ validate_simplify(' NOT NOT bar ', 'bar')
+
+
+def test_simplify_not_unix():
+ validate_simplify('NOT UNIX', 'WIN32')
+
+
+def test_simplify_not_win32():
+ validate_simplify('NOT WIN32', 'UNIX')
+
+
+def test_simplify_unix_and_win32():
+ validate_simplify('WIN32 AND UNIX', 'OFF')
+
+
+def test_simplify_unix_or_win32():
+ validate_simplify('WIN32 OR UNIX', 'ON')
+
+
+def test_simplify_unix_and_win32_or_foobar_or_barfoo():
+ validate_simplify('WIN32 AND foobar AND UNIX AND barfoo', 'OFF')
+
+
+def test_simplify_watchos_and_win32():
+ validate_simplify('APPLE_WATCHOS AND WIN32', 'OFF')
+
+
+def test_simplify_win32_and_watchos():
+ validate_simplify('WIN32 AND APPLE_WATCHOS', 'OFF')
+
+
+def test_simplify_apple_and_appleosx():
+ validate_simplify('APPLE AND APPLE_OSX', 'APPLE_OSX')
+
+
+def test_simplify_apple_or_appleosx():
+ validate_simplify('APPLE OR APPLE_OSX', 'APPLE')
+
+
+def test_simplify_apple_or_appleosx_level1():
+ validate_simplify('foobar AND (APPLE OR APPLE_OSX )', 'APPLE AND foobar')
+
+
+def test_simplify_apple_or_appleosx_level1_double():
+ validate_simplify('foobar AND (APPLE OR APPLE_OSX )', 'APPLE AND foobar')
+
+
+def test_simplify_apple_or_appleosx_level1_double_with_extra_spaces():
+ validate_simplify('foobar AND (APPLE OR APPLE_OSX ) '
+ 'AND ( APPLE_OSX OR APPLE )', 'APPLE AND foobar')
+
+
+def test_simplify_apple_or_appleosx_level2():
+ validate_simplify('foobar AND ( ( APPLE OR APPLE_WATCHOS ) '
+ 'OR APPLE_OSX ) AND ( APPLE_OSX OR APPLE ) '
+ 'AND ( (WIN32 OR WINRT) OR UNIX) ', 'APPLE AND foobar')
+
+
+def test_simplify_not_apple_and_appleosx():
+ validate_simplify('NOT APPLE AND APPLE_OSX', 'OFF')
+
+
+def test_simplify_unix_and_bar_or_win32():
+ validate_simplify('WIN32 AND bar AND UNIX', 'OFF')
+
+
+def test_simplify_unix_or_bar_or_win32():
+ validate_simplify('WIN32 OR bar OR UNIX', 'ON')
+
+
+def test_simplify_complex_true():
+ validate_simplify('WIN32 OR ( APPLE OR UNIX)', 'ON')
+
+
+def test_simplify_apple_unix_freebsd():
+ validate_simplify('( APPLE OR ( UNIX OR FREEBSD ))', 'UNIX')
+
+
+def test_simplify_apple_unix_freebsd_foobar():
+ validate_simplify('( APPLE OR ( UNIX OR FREEBSD ) OR foobar)',
+ 'UNIX OR foobar')
+
+
+def test_simplify_complex_false():
+ validate_simplify('WIN32 AND foobar AND ( '
+ 'APPLE OR ( UNIX OR FREEBSD ))',
+ 'OFF')
+
+
+def test_simplify_android_not_apple():
+ validate_simplify('ANDROID AND NOT ANDROID_EMBEDDED AND NOT APPLE_OSX',
+ 'ANDROID AND NOT ANDROID_EMBEDDED')
diff --git a/util/cmake/tests/test_operations.py b/util/cmake/tests/test_operations.py
new file mode 100755
index 0000000000..c1e5f1b250
--- /dev/null
+++ b/util/cmake/tests/test_operations.py
@@ -0,0 +1,57 @@
+#!/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 pro2cmake import AddOperation, SetOperation, UniqueAddOperation, RemoveOperation
+
+def test_add_operation():
+ op = AddOperation(['bar', 'buz'])
+
+ result = op.process(['foo', 'bar'], ['foo', 'bar'], lambda x: x)
+ assert ['foo', 'bar', 'bar', 'buz'] == result
+
+
+def test_uniqueadd_operation():
+ op = UniqueAddOperation(['bar', 'buz'])
+
+ result = op.process(['foo', 'bar'], ['foo', 'bar'], lambda x: x)
+ assert ['foo', 'bar', 'buz'] == result
+
+
+def test_set_operation():
+ op = SetOperation(['bar', 'buz'])
+
+ result = op.process(['foo', 'bar'], ['foo', 'bar'], lambda x: x)
+ assert ['bar', 'buz'] == result
+
+
+def test_remove_operation():
+ op = RemoveOperation(['bar', 'buz'])
+
+ result = op.process(['foo', 'bar'], ['foo', 'bar'], lambda x: x)
+ assert ['foo', '-buz'] == result
diff --git a/util/cmake/tests/test_parsing.py b/util/cmake/tests/test_parsing.py
new file mode 100755
index 0000000000..f924b13913
--- /dev/null
+++ b/util/cmake/tests/test_parsing.py
@@ -0,0 +1,345 @@
+#!/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$
+##
+#############################################################################
+
+import os
+from pro2cmake import QmakeParser
+
+
+_tests_path = os.path.dirname(os.path.abspath(__file__))
+
+
+def validate_op(key, op, value, to_validate):
+ assert key == to_validate['key']
+ assert op == to_validate['operation']
+ assert value == to_validate.get('value', None)
+
+
+def validate_single_op(key, op, value, to_validate):
+ assert len(to_validate) == 1
+ validate_op(key, op, value, to_validate[0])
+
+
+def evaluate_condition(to_validate):
+ assert 'condition' in to_validate
+ assert 'statements' in to_validate
+
+ return (to_validate['condition'],
+ to_validate['statements'],
+ to_validate.get('else_statements', {}))
+
+
+def validate_default_else_test(file_name):
+ result = parse_file(file_name)
+ assert len(result) == 1
+
+ (cond, if_branch, else_branch) = evaluate_condition(result[0])
+ assert cond == 'qtConfig(timezone)'
+ validate_single_op('A', '=', ['1'], if_branch)
+
+ assert len(else_branch) == 1
+ (cond2, if2_branch, else2_branch) = evaluate_condition(else_branch[0])
+ assert cond2 == 'win32'
+ validate_single_op('B', '=', ['2'], if2_branch)
+ validate_single_op('C', '=', ['3'], else2_branch)
+
+
+def parse_file(file):
+ p = QmakeParser(debug=True)
+ result = p.parseFile(file)
+
+ print('\n\n#### Parser result:')
+ print(result)
+ print('\n#### End of parser result.\n')
+
+ print('\n\n####Parser result dictionary:')
+ print(result.asDict())
+ print('\n#### End of parser result dictionary.\n')
+
+ result_dictionary = result.asDict()
+
+ assert len(result_dictionary) == 1
+
+ return result_dictionary['statements']
+
+
+def test_else():
+ result = parse_file(_tests_path + '/data/else.pro')
+ assert len(result) == 1
+
+ (cond, if_branch, else_branch) = evaluate_condition(result[0])
+
+ assert cond == 'linux'
+ validate_single_op('SOURCES', '+=', ['a.cpp'], if_branch)
+ validate_single_op('SOURCES', '+=', ['b.cpp'], else_branch)
+
+
+def test_else2():
+ result = parse_file(_tests_path + '/data/else2.pro')
+ assert len(result) == 1
+
+ (cond, if_branch, else_branch) = evaluate_condition(result[0])
+ assert cond == 'osx'
+ validate_single_op('A', '=', ['1'], if_branch)
+
+ assert len(else_branch) == 1
+ (cond2, if2_branch, else2_branch) = evaluate_condition(else_branch[0])
+ assert cond2 == 'win32'
+ validate_single_op('B', '=', ['2'], if2_branch)
+
+ validate_single_op('C', '=', ['3'], else2_branch)
+
+
+def test_else3():
+ validate_default_else_test(_tests_path + '/data/else3.pro')
+
+
+def test_else4():
+ validate_default_else_test(_tests_path + '/data/else4.pro')
+
+
+def test_else5():
+ validate_default_else_test(_tests_path + '/data/else5.pro')
+
+
+def test_else6():
+ validate_default_else_test(_tests_path + '/data/else6.pro')
+
+
+def test_else7():
+ result = parse_file(_tests_path + '/data/else7.pro')
+ assert len(result) == 1
+
+
+def test_else8():
+ validate_default_else_test(_tests_path + '/data/else8.pro')
+
+
+def test_multiline_assign():
+ result = parse_file(_tests_path + '/data/multiline_assign.pro')
+ assert len(result) == 2
+ validate_op('A', '=', ['42', '43', '44'], result[0])
+ validate_op('B', '=', ['23'], result[1])
+
+
+def test_include():
+ result = parse_file(_tests_path + '/data/include.pro')
+ assert len(result) == 3
+ validate_op('A', '=', ['42'], result[0])
+ include = result[1]
+ assert len(include) == 1
+ assert include.get('included', '') == 'foo'
+ validate_op('B', '=', ['23'], result[2])
+
+
+def test_load():
+ result = parse_file(_tests_path + '/data/load.pro')
+ assert len(result) == 3
+ validate_op('A', '=', ['42'], result[0])
+ load = result[1]
+ assert len(load) == 1
+ assert load.get('loaded', '') == 'foo'
+ validate_op('B', '=', ['23'], result[2])
+
+
+def test_definetest():
+ result = parse_file(_tests_path + '/data/definetest.pro')
+ assert len(result) == 1
+ assert result[0] == []
+
+
+def test_for():
+ result = parse_file(_tests_path + '/data/for.pro')
+ assert len(result) == 2
+ validate_op('SOURCES', '=', ['main.cpp'], result[0])
+ assert result[1] == []
+
+
+def test_single_line_for():
+ result = parse_file(_tests_path + '/data/single_line_for.pro')
+ assert len(result) == 1
+ assert result[0] == []
+
+
+def test_unset():
+ result = parse_file(_tests_path + '/data/unset.pro')
+ assert len(result) == 1
+ assert result[0] == []
+
+
+def test_quoted():
+ result = parse_file(_tests_path + '/data/quoted.pro')
+ assert len(result) == 1
+
+
+def test_complex_values():
+ result = parse_file(_tests_path + '/data/complex_values.pro')
+ assert len(result) == 1
+
+
+def test_function_if():
+ result = parse_file(_tests_path + '/data/function_if.pro')
+ assert len(result) == 1
+
+
+def test_realworld_standardpaths():
+ result = parse_file(_tests_path + '/data/standardpaths.pro')
+
+ (cond, if_branch, else_branch) = evaluate_condition(result[0])
+ assert cond == 'win32'
+ assert len(if_branch) == 1
+ assert len(else_branch) == 1
+
+ # win32:
+ (cond1, if_branch1, else_branch1) = evaluate_condition(if_branch[0])
+ assert cond1 == '!winrt'
+ assert len(if_branch1) == 1
+ validate_op('SOURCES', '+=', ['io/qstandardpaths_win.cpp'], if_branch1[0])
+ assert len(else_branch1) == 1
+ validate_op('SOURCES', '+=', ['io/qstandardpaths_winrt.cpp'], else_branch1[0])
+
+ # unix:
+ (cond2, if_branch2, else_branch2) = evaluate_condition(else_branch[0])
+ assert cond2 == 'unix'
+ assert len(if_branch2) == 1
+ assert len(else_branch2) == 0
+
+ # mac / else:
+ (cond3, if_branch3, else_branch3) = evaluate_condition(if_branch2[0])
+ assert cond3 == 'mac'
+ assert len(if_branch3) == 1
+ validate_op('OBJECTIVE_SOURCES', '+=', ['io/qstandardpaths_mac.mm'], if_branch3[0])
+ assert len(else_branch3) == 1
+
+ # android / else:
+ (cond4, if_branch4, else_branch4) = evaluate_condition(else_branch3[0])
+ assert cond4 == 'android && !android-embedded'
+ assert len(if_branch4) == 1
+ validate_op('SOURCES', '+=', ['io/qstandardpaths_android.cpp'], if_branch4[0])
+ assert len(else_branch4) == 1
+
+ # haiku / else:
+ (cond5, if_branch5, else_branch5) = evaluate_condition(else_branch4[0])
+ assert cond5 == 'haiku'
+ assert len(if_branch5) == 1
+ validate_op('SOURCES', '+=', ['io/qstandardpaths_haiku.cpp'], if_branch5[0])
+ assert len(else_branch5) == 1
+ validate_op('SOURCES', '+=', ['io/qstandardpaths_unix.cpp'], else_branch5[0])
+
+
+def test_realworld_comment_scope():
+ result = parse_file(_tests_path + '/data/comment_scope.pro')
+ assert len(result) == 2
+ (cond, if_branch, else_branch) = evaluate_condition(result[0])
+ assert cond == 'freebsd|openbsd'
+ assert len(if_branch) == 1
+ validate_op('QMAKE_LFLAGS_NOUNDEF', '=', None, if_branch[0])
+
+ assert result[1].get('included', '') == 'animation/animation.pri'
+
+
+def test_realworld_contains_scope():
+ result = parse_file(_tests_path + '/data/contains_scope.pro')
+ assert len(result) == 2
+
+
+def test_realworld_complex_assign():
+ result = parse_file(_tests_path + '/data/complex_assign.pro')
+ assert len(result) == 1
+ validate_op('qmake-clean.commands', '+=', '( cd qmake && $(MAKE) clean ":-(==)-:" \'(Foo)\' )'.split(),
+ result[0])
+
+
+def test_realworld_complex_condition():
+ result = parse_file(_tests_path + '/data/complex_condition.pro')
+ assert len(result) == 1
+ (cond, if_branch, else_branch) = evaluate_condition(result[0])
+ assert cond == '!system("dbus-send --session --type=signal / ' \
+ 'local.AutotestCheck.Hello >$$QMAKE_SYSTEM_NULL_DEVICE ' \
+ '2>&1")'
+ assert len(if_branch) == 1
+ validate_op('SOURCES', '=', ['dbus.cpp'], if_branch[0])
+
+ assert len(else_branch) == 0
+
+
+def test_realworld_sql():
+ result = parse_file(_tests_path + '/data/sql.pro')
+ assert len(result) == 2
+ validate_op('TEMPLATE', '=', ['subdirs'], result[0])
+ validate_op('SUBDIRS', '=', ['kernel'], result[1])
+
+
+def test_realworld_qtconfig():
+ result = parse_file(_tests_path + '/data/escaped_value.pro')
+ assert len(result) == 1
+ validate_op('MODULE_AUX_INCLUDES', '=', ['\\$\\$QT_MODULE_INCLUDE_BASE/QtANGLE'], result[0])
+
+
+def test_realworld_lc():
+ result = parse_file(_tests_path + '/data/lc.pro')
+ assert len(result) == 3
+
+
+def test_realworld_lc_with_comment_in_between():
+ result = parse_file(_tests_path + '/data/lc_with_comment.pro')
+
+ my_var = result[1]['value'][0]
+ assert my_var == 'foo'
+
+ my_var = result[2]['value'][0]
+ assert my_var == 'foo2'
+
+ my_var = result[3]['value'][0]
+ assert my_var == 'foo3'
+
+ my_var = result[4]['value'][0]
+ assert my_var == 'foo4'
+
+ my_var = result[5]['value'][0]
+ assert my_var == 'foo5'
+
+ sub_dirs = result[0]['value']
+ assert sub_dirs[0] == 'tga'
+ assert sub_dirs[1] == 'wbmp'
+ assert len(result) == 6
+
+
+def test_condition_without_scope():
+ result = parse_file(_tests_path + '/data/condition_without_scope.pro')
+ assert len(result) == 1
+
+
+def test_multi_condition_divided_by_lc():
+ result = parse_file(_tests_path + '/data/multi_condition_divided_by_lc.pro')
+ assert len(result) == 1
+
+
+def test_nested_function_calls():
+ result = parse_file(_tests_path + '/data/nested_function_calls.pro')
+ assert len(result) == 1
diff --git a/util/cmake/tests/test_scope_handling.py b/util/cmake/tests/test_scope_handling.py
new file mode 100755
index 0000000000..c0b553fabd
--- /dev/null
+++ b/util/cmake/tests/test_scope_handling.py
@@ -0,0 +1,338 @@
+#!/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 pro2cmake import Scope, SetOperation, merge_scopes, recursive_evaluate_scope
+
+import pytest
+import typing
+
+ScopeList = typing.List[Scope]
+
+def _map_to_operation(**kwargs):
+ result = {} # type: typing.Mapping[str, typing.List[SetOperation]]
+ for (key, value) in kwargs.items():
+ result[key] = [SetOperation([value])]
+ return result
+
+
+def _new_scope(*, parent_scope=None, condition='', **kwargs) -> Scope:
+ return Scope(parent_scope=parent_scope,
+ file='file1', condition=condition, operations=_map_to_operation(**kwargs))
+
+
+def _evaluate_scopes(scopes: ScopeList) -> ScopeList:
+ for s in scopes:
+ if not s.parent:
+ recursive_evaluate_scope(s)
+ return scopes
+
+
+def _validate(input_scopes: ScopeList, output_scopes: ScopeList):
+ merged_scopes = merge_scopes(input_scopes)
+ assert merged_scopes == output_scopes
+
+
+def test_evaluate_one_scope():
+ scope = _new_scope(condition='QT_FEATURE_foo', test1='bar')
+
+ input_scope = scope
+ recursive_evaluate_scope(scope)
+ assert scope == input_scope
+
+
+def test_evaluate_child_scope():
+ scope = _new_scope(condition='QT_FEATURE_foo', test1='bar')
+ _new_scope(parent_scope=scope, condition='QT_FEATURE_bar', test2='bar')
+
+ input_scope = scope
+ recursive_evaluate_scope(scope)
+
+ assert scope.total_condition == 'QT_FEATURE_foo'
+ assert len(scope.children) == 1
+ assert scope.get_string('test1') == 'bar'
+ assert scope.get_string('test2', 'not found') == 'not found'
+
+ child = scope.children[0]
+ assert child.total_condition == 'QT_FEATURE_bar AND QT_FEATURE_foo'
+ assert child.get_string('test1', 'not found') == 'not found'
+ assert child.get_string('test2') == 'bar'
+
+
+def test_evaluate_two_child_scopes():
+ scope = _new_scope(condition='QT_FEATURE_foo', test1='bar')
+ _new_scope(parent_scope=scope, condition='QT_FEATURE_bar', test2='bar')
+ _new_scope(parent_scope=scope, condition='QT_FEATURE_buz', test3='buz')
+
+ input_scope = scope
+ recursive_evaluate_scope(scope)
+
+ assert scope.total_condition == 'QT_FEATURE_foo'
+ assert len(scope.children) == 2
+ assert scope.get_string('test1') == 'bar'
+ assert scope.get_string('test2', 'not found') == 'not found'
+ assert scope.get_string('test3', 'not found') == 'not found'
+
+ child1 = scope.children[0]
+ assert child1.total_condition == 'QT_FEATURE_bar AND QT_FEATURE_foo'
+ assert child1.get_string('test1', 'not found') == 'not found'
+ assert child1.get_string('test2') == 'bar'
+ assert child1.get_string('test3', 'not found') == 'not found'
+
+ child2 = scope.children[1]
+ assert child2.total_condition == 'QT_FEATURE_buz AND QT_FEATURE_foo'
+ assert child2.get_string('test1', 'not found') == 'not found'
+ assert child2.get_string('test2') == ''
+ assert child2.get_string('test3', 'not found') == 'buz'
+
+
+def test_evaluate_else_child_scopes():
+ scope = _new_scope(condition='QT_FEATURE_foo', test1='bar')
+ _new_scope(parent_scope=scope, condition='QT_FEATURE_bar', test2='bar')
+ _new_scope(parent_scope=scope, condition='else', test3='buz')
+
+ input_scope = scope
+ recursive_evaluate_scope(scope)
+
+ assert scope.total_condition == 'QT_FEATURE_foo'
+ assert len(scope.children) == 2
+ assert scope.get_string('test1') == 'bar'
+ assert scope.get_string('test2', 'not found') == 'not found'
+ assert scope.get_string('test3', 'not found') == 'not found'
+
+ child1 = scope.children[0]
+ assert child1.total_condition == 'QT_FEATURE_bar AND QT_FEATURE_foo'
+ assert child1.get_string('test1', 'not found') == 'not found'
+ assert child1.get_string('test2') == 'bar'
+ assert child1.get_string('test3', 'not found') == 'not found'
+
+ child2 = scope.children[1]
+ assert child2.total_condition == 'QT_FEATURE_foo AND NOT QT_FEATURE_bar'
+ assert child2.get_string('test1', 'not found') == 'not found'
+ assert child2.get_string('test2') == ''
+ assert child2.get_string('test3', 'not found') == 'buz'
+
+
+def test_evaluate_invalid_else_child_scopes():
+ scope = _new_scope(condition='QT_FEATURE_foo', test1='bar')
+ _new_scope(parent_scope=scope, condition='else', test3='buz')
+ _new_scope(parent_scope=scope, condition='QT_FEATURE_bar', test2='bar')
+
+ input_scope = scope
+ with pytest.raises(AssertionError):
+ recursive_evaluate_scope(scope)
+
+
+def test_merge_empty_scope_list():
+ _validate([], [])
+
+
+def test_merge_one_scope():
+ scopes = [_new_scope(test='foo')]
+
+ recursive_evaluate_scope(scopes[0])
+
+ _validate(scopes, scopes)
+
+
+def test_merge_one_on_scope():
+ scopes = [_new_scope(condition='ON', test='foo')]
+
+ recursive_evaluate_scope(scopes[0])
+
+ _validate(scopes, scopes)
+
+
+def test_merge_one_off_scope():
+ scopes = [_new_scope(condition='OFF', test='foo')]
+
+ recursive_evaluate_scope(scopes[0])
+
+ _validate(scopes, [])
+
+
+def test_merge_one_conditioned_scope():
+ scopes = [_new_scope(condition='QT_FEATURE_foo', test='foo')]
+
+ recursive_evaluate_scope(scopes[0])
+
+ _validate(scopes, scopes)
+
+
+def test_merge_two_scopes_with_same_condition():
+ scopes = [_new_scope(condition='QT_FEATURE_bar', test='foo'),
+ _new_scope(condition='QT_FEATURE_bar', test2='bar')]
+
+ recursive_evaluate_scope(scopes[0])
+ recursive_evaluate_scope(scopes[1])
+
+ result = merge_scopes(scopes)
+
+ assert len(result) == 1
+ r0 = result[0]
+ assert r0.total_condition == 'QT_FEATURE_bar'
+ assert r0.get_string('test') == 'foo'
+ assert r0.get_string('test2') == 'bar'
+
+
+def test_merge_three_scopes_two_with_same_condition():
+ scopes = [_new_scope(condition='QT_FEATURE_bar', test='foo'),
+ _new_scope(condition='QT_FEATURE_baz', test1='buz'),
+ _new_scope(condition='QT_FEATURE_bar', test2='bar')]
+
+ recursive_evaluate_scope(scopes[0])
+ recursive_evaluate_scope(scopes[1])
+ recursive_evaluate_scope(scopes[2])
+
+ result = merge_scopes(scopes)
+
+ assert len(result) == 2
+ r0 = result[0]
+ assert r0.total_condition == 'QT_FEATURE_bar'
+ assert r0.get_string('test') == 'foo'
+ assert r0.get_string('test2') == 'bar'
+
+ assert result[1] == scopes[1]
+
+
+def test_merge_two_unrelated_on_off_scopes():
+ scopes = [_new_scope(condition='ON', test='foo'),
+ _new_scope(condition='OFF', test2='bar')]
+
+ recursive_evaluate_scope(scopes[0])
+ recursive_evaluate_scope(scopes[1])
+
+ _validate(scopes, [scopes[0]])
+
+
+def test_merge_two_unrelated_on_off_scopes():
+ scopes = [_new_scope(condition='OFF', test='foo'),
+ _new_scope(condition='ON', test2='bar')]
+
+ recursive_evaluate_scope(scopes[0])
+ recursive_evaluate_scope(scopes[1])
+
+ _validate(scopes, [scopes[1]])
+
+
+def test_merge_parent_child_scopes_with_different_conditions():
+ scope = _new_scope(condition='FOO', test1='parent')
+ scopes = [scope, _new_scope(parent_scope=scope, condition='bar', test2='child')]
+
+ recursive_evaluate_scope(scope)
+
+ _validate(scopes, scopes)
+
+
+def test_merge_parent_child_scopes_with_same_conditions():
+ scope = _new_scope(condition='FOO AND bar', test1='parent')
+ scopes = [scope, _new_scope(parent_scope=scope, condition='FOO AND bar', test2='child')]
+
+ recursive_evaluate_scope(scope)
+
+ result = merge_scopes(scopes)
+
+ assert len(result) == 1
+ r0 = result[0]
+ assert r0.parent == None
+ assert r0.total_condition == 'FOO AND bar'
+ assert r0.get_string('test1') == 'parent'
+ assert r0.get_string('test2') == 'child'
+
+
+def test_merge_parent_child_scopes_with_on_child_condition():
+ scope = _new_scope(condition='FOO AND bar', test1='parent')
+ scopes = [scope, _new_scope(parent_scope=scope, condition='ON', test2='child')]
+
+ recursive_evaluate_scope(scope)
+
+ result = merge_scopes(scopes)
+
+ assert len(result) == 1
+ r0 = result[0]
+ assert r0.parent == None
+ assert r0.total_condition == 'FOO AND bar'
+ assert r0.get_string('test1') == 'parent'
+ assert r0.get_string('test2') == 'child'
+
+
+# Real world examples:
+
+# qstandardpaths selection:
+
+def test_qstandardpaths_scopes():
+ # top level:
+ scope1 = _new_scope(condition='ON', scope_id=1)
+
+ # win32 {
+ scope2 = _new_scope(parent_scope=scope1, condition='WIN32')
+ # !winrt {
+ # SOURCES += io/qstandardpaths_win.cpp
+ scope3 = _new_scope(parent_scope=scope2, condition='NOT WINRT',
+ SOURCES='qsp_win.cpp')
+ # } else {
+ # SOURCES += io/qstandardpaths_winrt.cpp
+ scope4 = _new_scope(parent_scope=scope2, condition='else',
+ SOURCES='qsp_winrt.cpp')
+ # }
+ # else: unix {
+ scope5 = _new_scope(parent_scope=scope1, condition='else')
+ scope6 = _new_scope(parent_scope=scope5, condition='UNIX')
+ # mac {
+ # OBJECTIVE_SOURCES += io/qstandardpaths_mac.mm
+ scope7 = _new_scope(parent_scope=scope6, condition='APPLE_OSX', SOURCES='qsp_mac.mm')
+ # } else:android:!android-embedded {
+ # SOURCES += io/qstandardpaths_android.cpp
+ scope8 = _new_scope(parent_scope=scope6, condition='else')
+ scope9 = _new_scope(parent_scope=scope8,
+ condition='ANDROID AND NOT ANDROID_EMBEDDED',
+ SOURCES='qsp_android.cpp')
+ # } else:haiku {
+ # SOURCES += io/qstandardpaths_haiku.cpp
+ scope10 = _new_scope(parent_scope=scope8, condition='else')
+ scope11 = _new_scope(parent_scope=scope10, condition='HAIKU', SOURCES='qsp_haiku.cpp')
+ # } else {
+ # SOURCES +=io/qstandardpaths_unix.cpp
+ scope12 = _new_scope(parent_scope=scope10, condition='else', SOURCES='qsp_unix.cpp')
+ # }
+ # }
+
+ recursive_evaluate_scope(scope1)
+
+ assert scope1.total_condition == 'ON'
+ assert scope2.total_condition == 'WIN32'
+ assert scope3.total_condition == 'WIN32 AND NOT WINRT'
+ assert scope4.total_condition == 'WINRT'
+ assert scope5.total_condition == 'UNIX'
+ assert scope6.total_condition == 'UNIX'
+ assert scope7.total_condition == 'APPLE_OSX'
+ assert scope8.total_condition == 'UNIX AND NOT APPLE_OSX'
+ assert scope9.total_condition == 'ANDROID AND NOT ANDROID_EMBEDDED'
+ assert scope10.total_condition == 'UNIX AND NOT APPLE_OSX AND (ANDROID_EMBEDDED OR NOT ANDROID)'
+ assert scope11.total_condition == 'HAIKU AND (ANDROID_EMBEDDED OR NOT ANDROID)'
+ assert scope12.total_condition == 'UNIX AND NOT APPLE_OSX AND NOT HAIKU AND (ANDROID_EMBEDDED OR NOT ANDROID)'