summaryrefslogtreecommitdiffstats
path: root/util
diff options
context:
space:
mode:
Diffstat (limited to 'util')
-rw-r--r--util/cmake/Makefile20
-rw-r--r--util/cmake/Pipfile18
-rw-r--r--util/cmake/README.md54
-rwxr-xr-xutil/cmake/cmakeconversionrate.py141
-rw-r--r--util/cmake/condition_simplifier.py237
-rw-r--r--util/cmake/condition_simplifier_cache.py183
-rwxr-xr-xutil/cmake/configurejson2cmake.py1105
-rwxr-xr-xutil/cmake/generate_module_map.sh38
-rw-r--r--util/cmake/helper.py740
-rw-r--r--util/cmake/json_parser.py101
-rwxr-xr-xutil/cmake/pro2cmake.py3993
-rwxr-xr-xutil/cmake/pro_conversion_rate.py235
-rw-r--r--util/cmake/qmake_parser.py388
-rw-r--r--util/cmake/requirements.txt8
-rwxr-xr-xutil/cmake/run_pro2cmake.py246
-rw-r--r--util/cmake/special_case_helper.py415
-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
-rw-r--r--util/cmake/tests/data/value_function.pro2
-rwxr-xr-xutil/cmake/tests/test_lc_fixup.py44
-rwxr-xr-xutil/cmake/tests/test_logic_mapping.py186
-rwxr-xr-xutil/cmake/tests/test_operations.py57
-rwxr-xr-xutil/cmake/tests/test_parsing.py354
-rwxr-xr-xutil/cmake/tests/test_scope_handling.py346
53 files changed, 9103 insertions, 0 deletions
diff --git a/util/cmake/Makefile b/util/cmake/Makefile
new file mode 100644
index 0000000000..2243ad111e
--- /dev/null
+++ b/util/cmake/Makefile
@@ -0,0 +1,20 @@
+
+test: flake8 mypy pytest black_format_check
+
+coverage:
+ pytest --cov .
+
+format:
+ black *.py --line-length 100
+
+black_format_check:
+ black *.py --line-length 100 --check
+
+flake8:
+ flake8 *.py --ignore=E501,E266,E203,W503
+
+pytest:
+ pytest
+
+mypy:
+ mypy --pretty *.py
diff --git a/util/cmake/Pipfile b/util/cmake/Pipfile
new file mode 100644
index 0000000000..21c18f4743
--- /dev/null
+++ b/util/cmake/Pipfile
@@ -0,0 +1,18 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+pyparsing = "*"
+sympy = "*"
+mypy = "*"
+pytest = "*"
+pytest-cov = "*"
+flake8 = "*"
+portalocker = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.7"
diff --git a/util/cmake/README.md b/util/cmake/README.md
new file mode 100644
index 0000000000..e1699d5283
--- /dev/null
+++ b/util/cmake/README.md
@@ -0,0 +1,54 @@
+# CMake Utils
+
+This directory holds scripts to help the porting process from `qmake` to `cmake` for Qt6.
+
+# Requirements
+
+* [Python 3.7](https://www.python.org/downloads/),
+* `pipenv` or `pip` to manage the modules.
+
+## Python modules
+
+Since Python has many ways of handling projects, you have a couple of options to
+install the dependencies of the scripts:
+
+### Using `pipenv`
+
+The dependencies are specified on the `Pipfile`, so you just need to run
+`pipenv install` and that will automatically create a virtual environment
+that you can activate with a `pipenv shell`.
+
+### Using `pip`
+
+It's highly recommended to use a [virtualenvironment](https://virtualenv.pypa.io/en/latest/)
+to avoid conflict with other packages that are already installed: `pip install virtualenv`.
+
+* Create an environment: `virtualenv env`,
+* Activate the environment: `source env/bin/activate`
+ (on Windows: `source env\Scripts\activate.bat`)
+* Install the requirements: `pip install -r requirements.txt`
+
+# Contributing to the scripts
+
+You can verify if the styling of a script complaint with PEP8, with a couple of exceptions:
+
+Install [flake8](http://flake8.pycqa.org/en/latest/) (`pip install flake8`) and run it
+on the script you want to test:
+
+```
+flake8 <file>.py --ignore=E501,E266,W503
+```
+
+* `E501`: Line too long (82>79 characters),
+* `E266`: Too many leading '#' for block comment,
+* `W503`: Line break occurred before a binary operator)
+
+You can also modify the file with an automatic formatter,
+like [black](https://black.readthedocs.io/en/stable/) (`pip install black`),
+and execute it:
+
+```
+black -l 100 <file>.py
+```
+
+Using Qt's maximum line length, 100.
diff --git a/util/cmake/cmakeconversionrate.py b/util/cmake/cmakeconversionrate.py
new file mode 100755
index 0000000000..b87957df6c
--- /dev/null
+++ b/util/cmake/cmakeconversionrate.py
@@ -0,0 +1,141 @@
+#!/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 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(f'Scanning "{source_directory}" for qmake-based tests.')
+ 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(f'Running cmake in "{binary_directory}"')
+ result = subprocess.run(["/usr/bin/cmake", "-GNinja", abs_source], cwd=binary_directory)
+ if debug:
+ print(f"CMake return code: {result.returncode}.")
+
+ assert result.returncode == 0
+
+ if debug:
+ print(f'Running ninja in "{binary_directory}".')
+ result = subprocess.run("/usr/bin/ninja", cwd=binary_directory)
+ if debug:
+ print(f"Ninja return code: {result.returncode}.")
+
+ assert result.returncode == 0
+
+
+def test(binary_directory: str, *, debug=False) -> typing.Tuple[int, int]:
+ if debug:
+ print(f'Running ctest in "{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(f"Test summary: {summary} ({result.returncode}).")
+
+ matches = re.fullmatch(r"\d+% tests passed, (\d+) tests failed out of (\d+)", summary)
+ if matches:
+ if debug:
+ print(f"Matches: failed {matches.group(1)}, total {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(f"Could not find the qmake baseline in {args.source_directory}.")
+ return 1
+
+ if args.debug:
+ print(f"qmake baseline: {base_line} test binaries.")
+
+ 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(f"\n\n\nCMake test conversion rate: {cmake_total/base_line:.2f}.")
+ print(f"CMake test success rate : {cmake_success/base_line:.2f}.")
+ return 0
+
+
+if __name__ == "__main__":
+ main()
diff --git a/util/cmake/condition_simplifier.py b/util/cmake/condition_simplifier.py
new file mode 100644
index 0000000000..d02e70e489
--- /dev/null
+++ b/util/cmake/condition_simplifier.py
@@ -0,0 +1,237 @@
+#!/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 re
+from sympy import simplify_logic, And, Or, Not, SympifyError # type: ignore
+from condition_simplifier_cache import simplify_condition_memoize
+
+
+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 knowledge 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
+
+
+@simplify_condition_memoize
+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 ")
+ # Replace dashes with a token
+ condition = condition.replace("-", "_dash_")
+
+ # SymPy chokes on expressions that contain two tokens one next to
+ # the other delimited by a space, which are not an operation.
+ # So a CMake condition like "TARGET Foo::Bar" fails the whole
+ # expression simplifying process.
+ # Turn these conditions into a single token so that SymPy can parse
+ # the expression, and thus simplify it.
+ # Do this by replacing and keeping a map of conditions to single
+ # token symbols.
+ # Support both target names without double colons, and with double
+ # colons.
+ pattern = re.compile(r"(TARGET [a-zA-Z]+(?:::[a-zA-Z]+)?)")
+ target_symbol_mapping = {}
+ all_target_conditions = re.findall(pattern, condition)
+ for target_condition in all_target_conditions:
+ # Replace spaces and colons with underscores.
+ target_condition_symbol_name = re.sub("[ :]", "_", target_condition)
+ target_symbol_mapping[target_condition_symbol_name] = target_condition
+ condition = re.sub(target_condition, target_condition_symbol_name, condition)
+
+ # Do similar token mapping for comparison operators.
+ pattern = re.compile(r"([a-zA-Z_0-9]+ (?:STRLESS|STREQUAL|STRGREATER) [a-zA-Z_0-9]+)")
+ comparison_symbol_mapping = {}
+ all_comparisons = re.findall(pattern, condition)
+ for comparison in all_comparisons:
+ # Replace spaces and colons with underscores.
+ comparison_symbol_name = re.sub("[ ]", "_", comparison)
+ comparison_symbol_mapping[comparison_symbol_name] = comparison
+ condition = re.sub(comparison, comparison_symbol_name, condition)
+
+ try:
+ # Generate and simplify condition using sympy:
+ condition_expr = simplify_logic(condition)
+ condition = str(_recursive_simplify(condition_expr))
+
+ # Restore the target conditions.
+ for symbol_name in target_symbol_mapping:
+ condition = re.sub(symbol_name, target_symbol_mapping[symbol_name], condition)
+
+ # Restore comparisons.
+ for comparison in comparison_symbol_mapping:
+ condition = re.sub(comparison, comparison_symbol_mapping[comparison], condition)
+
+ # Map back to CMake syntax:
+ condition = condition.replace("~", "NOT ")
+ condition = condition.replace("&", "AND")
+ condition = condition.replace("|", "OR")
+ condition = condition.replace("True", "ON")
+ condition = condition.replace("False", "OFF")
+ condition = condition.replace("_dash_", "-")
+ except (SympifyError, TypeError, AttributeError):
+ # sympy did not like our input, so leave this condition alone:
+ condition = input_condition
+
+ return condition or "ON"
diff --git a/util/cmake/condition_simplifier_cache.py b/util/cmake/condition_simplifier_cache.py
new file mode 100644
index 0000000000..58cd5b88c5
--- /dev/null
+++ b/util/cmake/condition_simplifier_cache.py
@@ -0,0 +1,183 @@
+#!/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 atexit
+import hashlib
+import json
+import os
+import sys
+import time
+
+from typing import Any, Callable, Dict
+
+condition_simplifier_cache_enabled = True
+
+
+def set_condition_simplified_cache_enabled(value: bool):
+ global condition_simplifier_cache_enabled
+ condition_simplifier_cache_enabled = value
+
+
+def get_current_file_path() -> str:
+ try:
+ this_file = __file__
+ except NameError:
+ this_file = sys.argv[0]
+ this_file = os.path.abspath(this_file)
+ return this_file
+
+
+def get_cache_location() -> str:
+ this_file = get_current_file_path()
+ dir_path = os.path.dirname(this_file)
+ cache_path = os.path.join(dir_path, ".pro2cmake_cache", "cache.json")
+ return cache_path
+
+
+def get_file_checksum(file_path: str) -> str:
+ try:
+ with open(file_path, "r") as content_file:
+ content = content_file.read()
+ except IOError:
+ content = str(time.time())
+ checksum = hashlib.md5(content.encode("utf-8")).hexdigest()
+ return checksum
+
+
+def get_condition_simplifier_checksum() -> str:
+ current_file_path = get_current_file_path()
+ dir_name = os.path.dirname(current_file_path)
+ condition_simplifier_path = os.path.join(dir_name, "condition_simplifier.py")
+ return get_file_checksum(condition_simplifier_path)
+
+
+def init_cache_dict():
+ return {
+ "checksum": get_condition_simplifier_checksum(),
+ "schema_version": "1",
+ "cache": {"conditions": {}},
+ }
+
+
+def merge_dicts_recursive(a: Dict[str, Any], other: Dict[str, Any]) -> Dict[str, Any]:
+ """Merges values of "other" into "a", mutates a."""
+ for key in other:
+ if key in a:
+ if isinstance(a[key], dict) and isinstance(other[key], dict):
+ merge_dicts_recursive(a[key], other[key])
+ elif a[key] == other[key]:
+ pass
+ else:
+ a[key] = other[key]
+ return a
+
+
+def open_file_safe(file_path: str, mode: str = "r+"):
+ # Use portalocker package for file locking if available,
+ # otherwise print a message to install the package.
+ try:
+ import portalocker # type: ignore
+
+ file_open_func = portalocker.Lock
+ file_open_args = [file_path]
+ file_open_kwargs = {"mode": mode, "flags": portalocker.LOCK_EX}
+ file_handle = file_open_func(*file_open_args, **file_open_kwargs)
+ return file_handle
+ except ImportError:
+ print(
+ "The conversion script is missing a required package: portalocker. Please run "
+ "python -m pip install -r requirements.txt to install the missing dependency."
+ )
+ exit(1)
+
+
+def simplify_condition_memoize(f: Callable[[str], str]):
+ cache_path = get_cache_location()
+ cache_file_content: Dict[str, Any] = {}
+
+ if os.path.exists(cache_path):
+ try:
+ with open_file_safe(cache_path, mode="r") as cache_file:
+ cache_file_content = json.load(cache_file)
+ except (IOError, ValueError):
+ print(f"Invalid pro2cmake cache file found at: {cache_path}. Removing it.")
+ os.remove(cache_path)
+
+ if not cache_file_content:
+ cache_file_content = init_cache_dict()
+
+ current_checksum = get_condition_simplifier_checksum()
+ if cache_file_content["checksum"] != current_checksum:
+ cache_file_content = init_cache_dict()
+
+ def update_cache_file():
+ if not os.path.exists(cache_path):
+ os.makedirs(os.path.dirname(cache_path), exist_ok=True)
+ # Create the file if it doesn't exist, but don't override
+ # it.
+ with open(cache_path, "a"):
+ pass
+
+ updated_cache = cache_file_content
+
+ with open_file_safe(cache_path, "r+") as cache_file_write_handle:
+ # Read any existing cache content, and truncate the file.
+ cache_file_existing_content = cache_file_write_handle.read()
+ cache_file_write_handle.seek(0)
+ cache_file_write_handle.truncate()
+
+ # Merge the new cache into the old cache if it exists.
+ if cache_file_existing_content:
+ possible_cache = json.loads(cache_file_existing_content)
+ if (
+ "checksum" in possible_cache
+ and "schema_version" in possible_cache
+ and possible_cache["checksum"] == cache_file_content["checksum"]
+ and possible_cache["schema_version"] == cache_file_content["schema_version"]
+ ):
+ updated_cache = merge_dicts_recursive(dict(possible_cache), updated_cache)
+
+ json.dump(updated_cache, cache_file_write_handle, indent=4)
+
+ # Flush any buffered writes.
+ cache_file_write_handle.flush()
+ os.fsync(cache_file_write_handle.fileno())
+
+ atexit.register(update_cache_file)
+
+ def helper(condition: str) -> str:
+ if (
+ condition not in cache_file_content["cache"]["conditions"]
+ or not condition_simplifier_cache_enabled
+ ):
+ cache_file_content["cache"]["conditions"][condition] = f(condition)
+ return cache_file_content["cache"]["conditions"][condition]
+
+ return helper
diff --git a/util/cmake/configurejson2cmake.py b/util/cmake/configurejson2cmake.py
new file mode 100755
index 0000000000..9f93ecafa6
--- /dev/null
+++ b/util/cmake/configurejson2cmake.py
@@ -0,0 +1,1105 @@
+#!/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 posixpath
+import re
+import sys
+from typing import Optional, Set
+from textwrap import dedent
+
+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) -> Optional[str]:
+ testmap = {
+ "c99": "c_std_99 IN_LIST CMAKE_C_COMPILE_FEATURES",
+ "c11": "c_std_11 IN_LIST CMAKE_C_COMPILE_FEATURES",
+ "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",
+ "libclang": "TEST_libclang",
+ "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",
+ "wayland-scanner": "WaylandScanner_FOUND",
+ }
+ if test in testmap:
+ return testmap.get(test, None)
+ if test in knownTests:
+ return f"TEST_{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(path: str) -> str:
+ path = posixpath.join(path, "configure.json")
+
+ print(f"Reading {path}...")
+ assert posixpath.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(f' XXXX Unknown library "{lib}".')
+ return
+
+ if newlib.packageName is None:
+ print(f' **** Skipping library "{lib}" -- was masked.')
+ return
+
+ print(f" mapped library {lib} to {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 f"libs.{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:
+ escaped_value = value.replace('"', '\\"')
+ return f' {label} "{escaped_value}"\n'
+ return f" {label} {value}\n"
+ 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 = f"QT_FEATURE_{featureName(match.group(2))}"
+
+ elif match.group(1) == "subarch":
+ substitution = f"TEST_arch_{'${TEST_architecture_arch}'}_subarch_{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 = f"INPUT_{featureName(match.group(2))}"
+
+ elif match.group(1) == "config":
+ substitution = map_platform(match.group(2))
+ elif match.group(1) == "module":
+ substitution = f"TARGET {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(f' XXXX Unknown condition "{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()
+
+ # Special case for WrapLibClang in qttools
+ condition = condition.replace("TEST_libclang.has_clangcpp", "TEST_libclang")
+
+ if has_failed:
+ condition += " OR FIXME"
+
+ return condition
+
+
+def parseInput(ctx, sinput, 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 sinput in skip_inputs:
+ print(f" **** Skipping input {sinput}: masked.")
+ return
+
+ dtype = data
+ if isinstance(data, dict):
+ dtype = data["type"]
+
+ if dtype == "boolean":
+ print(f" **** Skipping boolean input {sinput}: masked.")
+ return
+
+ if dtype == "enum":
+ values_line = " ".join(data["values"])
+ cm_fh.write(f"# input {sinput}\n")
+ cm_fh.write(f'set(INPUT_{featureName(sinput)} "undefined" CACHE STRING "")\n')
+ cm_fh.write(
+ f"set_property(CACHE INPUT_{featureName(sinput)} PROPERTY STRINGS undefined {values_line})\n\n"
+ )
+ return
+
+ print(f" XXXX UNHANDLED INPUT TYPE {dtype} in input description")
+ 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 = {
+ "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",
+ "wayland-scanner",
+ "xlib",
+ }
+
+ if test in skip_tests:
+ print(f" **** Skipping features {test}: masked.")
+ return
+
+ if data["type"] == "compile":
+ knownTests.add(test)
+
+ details = data["test"]
+
+ if isinstance(details, str):
+ if not ctx["test_dir"]:
+ print(f" XXXX UNHANDLED TEST SUB-TYPE {details} in test description")
+ return
+
+ cm_fh.write(
+ f"""
+if(EXISTS "${{CMAKE_CURRENT_SOURCE_DIR}}/{ctx['test_dir']}/{data['test']}/CMakeLists.txt")
+ qt_config_compile_test("{data['test']}"
+ LABEL "{data['label']}"
+ PROJECT_PATH "${{CMAKE_CURRENT_SOURCE_DIR}}/{ctx['test_dir']}/{data['test']}")
+endif()
+"""
+ )
+ 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 = f"#include <{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 = ""
+ languageStandard = ""
+ qmakeFixme = ""
+
+ cm_fh.write(f"# {test}\n")
+ 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"] == "!winrt: LIBS += runtimeobject.lib":
+ librariesCmakeName = format(featureName(test)) + "_TEST_LIBRARIES"
+ cm_fh.write("if (NOT WINRT)\n")
+ cm_fh.write(" set(" + librariesCmakeName + " runtimeobject)\n")
+ cm_fh.write("endif()\n")
+ elif details["qmake"] == "CONFIG += c++11":
+ # do nothing we're always in c++11 mode
+ pass
+ elif details["qmake"] == "CONFIG += c++11 c++14":
+ languageStandard = "CXX_STANDARD 14"
+ elif details["qmake"] == "CONFIG += c++11 c++14 c++17":
+ languageStandard = "CXX_STANDARD 17"
+ elif details["qmake"] == "CONFIG += c++11 c++14 c++17 c++2a":
+ languageStandard = "CXX_STANDARD 20"
+ else:
+ qmakeFixme = f"# FIXME: qmake: {details['qmake']}\n"
+
+ library_list = []
+ if "use" in data:
+ for library in data["use"].split(" "):
+ if len(library) == 0:
+ continue
+
+ mapped_library = find_3rd_party_library_mapping(library)
+ if not mapped_library:
+ qmakeFixme += f"# FIXME: use: unmapped library: {library}\n"
+ continue
+ library_list.append(mapped_library.targetName)
+
+ cm_fh.write(f"qt_config_compile_test({featureName(test)}\n")
+ cm_fh.write(lineify("LABEL", data.get("label", "")))
+ if librariesCmakeName != "" or len(library_list) != 0:
+ cm_fh.write(" LIBRARIES\n")
+ if librariesCmakeName != "":
+ cm_fh.write(lineify("", "${" + librariesCmakeName + "}"))
+ if len(library_list) != 0:
+ cm_fh.write(" ")
+ cm_fh.write("\n ".join(library_list))
+ cm_fh.write("\n")
+ cm_fh.write(" CODE\n")
+ cm_fh.write('"' + sourceCode + '"')
+ if qmakeFixme != "":
+ cm_fh.write(qmakeFixme)
+ if languageStandard != "":
+ cm_fh.write(f"\n {languageStandard}\n")
+ cm_fh.write(")\n\n")
+
+ elif data["type"] == "libclang":
+ knownTests.add(test)
+
+ cm_fh.write(f"# {test}\n")
+ lib_clang_lib = find_3rd_party_library_mapping("libclang")
+ cm_fh.write(generate_find_package_info(lib_clang_lib))
+ cm_fh.write(
+ dedent(
+ """
+ if(TARGET WrapLibClang::WrapLibClang)
+ set(TEST_libclang "ON" CACHE BOOL "Required libclang version found." FORCE)
+ endif()
+ """
+ )
+ )
+ cm_fh.write("\n")
+
+ elif data["type"] == "x86Simd":
+ knownTests.add(test)
+
+ label = data["label"]
+
+ cm_fh.write(f"# {test}\n")
+ cm_fh.write(f'qt_config_compile_test_x86simd({test} "{label}")\n')
+ 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(f" XXXX UNHANDLED TEST TYPE {data['type']} in test description")
+
+
+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 )"
+ },
+ "simulator_and_device": {"condition": "APPLE_UIKIT AND NOT QT_UIKIT_SDK"},
+ "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(f" **** Skipping features {feature}: masked.")
+ 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(f" XXXX UNHANDLED KEY {k} in feature description")
+
+ 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
+ publicConfig = False # add to CONFIG in public pri file
+ privateConfig = False # add to CONFIG in private pri file
+ publicQtConfig = False # add to QT_CONFIG in public pri file
+
+ for o in output:
+ outputType = o
+ if isinstance(o, dict):
+ outputType = o["type"]
+
+ if outputType in ["varAssign", "varAppend", "varRemove"]:
+ 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
+ elif outputType == "publicConfig":
+ publicConfig = True
+ elif outputType == "privateConfig":
+ privateConfig = True
+ elif outputType == "publicQtConfig":
+ publicQtConfig = True
+ else:
+ print(f" XXXX UNHANDLED OUTPUT TYPE {outputType} in feature {feature}.")
+ continue
+
+ if not any(
+ [
+ publicFeature,
+ privateFeature,
+ internalFeature,
+ publicDefine,
+ negativeFeature,
+ publicConfig,
+ privateConfig,
+ publicQtConfig,
+ ]
+ ):
+ print(f" **** Skipping feature {feature}: Not relevant for C++.")
+ return
+
+ normalized_feature_name = featureName(feature)
+
+ def writeFeature(
+ name,
+ publicFeature=False,
+ privateFeature=False,
+ labelAppend="",
+ superFeature=None,
+ autoDetect="",
+ ):
+ if comment:
+ cm_fh.write(f"# {comment}\n")
+
+ cm_fh.write(f'qt_feature("{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))
+ if superFeature:
+ feature_condition = f"QT_FEATURE_{superFeature}"
+ else:
+ feature_condition = condition
+ cm_fh.write(lineify("CONDITION", feature_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[feature] = {"name": feature, "labelAppend": "", "autoDetect": autoDetect}
+
+ # Go over all outputs to compute the number of features that have to be declared
+ for o in output:
+ outputType = o
+ name = feature
+
+ # 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 = f": {o['name']}"
+
+ if outputType not in ["feature", "publicFeature", "privateFeature"]:
+ continue
+ if name not in featureCalls:
+ featureCalls[name] = {"name": name, "labelAppend": labelAppend}
+
+ if name != feature:
+ featureCalls[name]["superFeature"] = normalized_feature_name
+
+ if outputType in ["feature", "publicFeature"]:
+ featureCalls[name]["publicFeature"] = True
+ elif outputType == "privateFeature":
+ featureCalls[name]["privateFeature"] = True
+ elif outputType == "publicConfig":
+ featureCalls[name]["publicConfig"] = True
+ elif outputType == "privateConfig":
+ featureCalls[name]["privateConfig"] = True
+ elif outputType == "publicQtConfig":
+ featureCalls[name]["publicQtConfig"] = 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": f"QT_NO_{normalized_feature_name.upper()}",
+ "negative": True,
+ "value": 1,
+ "type": "define",
+ }
+
+ if outputType != "define":
+ continue
+
+ if outputArgs.get("name") is None:
+ print(f" XXXX DEFINE output without name in feature {feature}.")
+ continue
+
+ out_name = outputArgs.get("name")
+ cm_fh.write(f'qt_feature_definition("{feature}" "{out_name}"')
+ if outputArgs.get("negative", False):
+ cm_fh.write(" NEGATE")
+ if outputArgs.get("value") is not None:
+ cm_fh.write(f' VALUE "{outputArgs.get("value")}"')
+ cm_fh.write(")\n")
+
+ # Write qt_feature_config() calls
+ for o in output:
+ outputType = o
+ name = feature
+ modified_name = name
+
+ outputArgs = {}
+ if isinstance(o, dict):
+ outputType = o["type"]
+ outputArgs = o
+ if "name" in o:
+ modified_name = o["name"]
+
+ if outputType not in ["publicConfig", "privateConfig", "publicQtConfig"]:
+ continue
+
+ config_type = ""
+ if outputType == "publicConfig":
+ config_type = "QMAKE_PUBLIC_CONFIG"
+ elif outputType == "privateConfig":
+ config_type = "QMAKE_PRIVATE_CONFIG"
+ elif outputType == "publicQtConfig":
+ config_type = "QMAKE_PUBLIC_QT_CONFIG"
+
+ if not config_type:
+ print(" XXXX config output without type in feature {}.".format(feature))
+ continue
+
+ cm_fh.write('qt_feature_config("{}" {}'.format(name, config_type))
+ if outputArgs.get("negative", False):
+ cm_fh.write("\n NEGATE")
+ if modified_name != name:
+ cm_fh.write("\n")
+ cm_fh.write(lineify("NAME", modified_name, quote=True))
+
+ 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_option in commandLine["options"]:
+ parseInput(ctx, input_option, commandLine["options"][input_option], 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(path, ctx, data):
+ assert ctx is not None
+ if "subconfigs" in data:
+ for subconf in data["subconfigs"]:
+ subconfDir = posixpath.join(path, subconf)
+ subconfData = readJsonFromDir(subconfDir)
+ subconfCtx = ctx
+ processJson(subconfDir, subconfCtx, subconfData)
+
+
+def processJson(path, ctx, data):
+ ctx["module"] = data.get("module", "global")
+ ctx["test_dir"] = data.get("testDir", "")
+
+ ctx = processFiles(ctx, data)
+
+ with open(posixpath.join(path, "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(path, ctx, data)
+
+
+def main():
+ if len(sys.argv) != 2:
+ print("This scripts needs one directory to process!")
+ quit(1)
+
+ directory = sys.argv[1]
+
+ print(f"Processing: {directory}.")
+
+ data = readJsonFromDir(directory)
+ processJson(directory, {}, 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..091b619201
--- /dev/null
+++ b/util/cmake/helper.py
@@ -0,0 +1,740 @@
+#############################################################################
+##
+## 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("coap", "Qt6", "Qt::Coap", extra=["COMPONENTS", "Coap"]),
+ 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("designer", "Qt6", "Qt::Designer", extra=["COMPONENTS", "Designer"]),
+ LibraryMapping(
+ "designercomponents",
+ "Qt6",
+ "Qt::DesignerComponents",
+ extra=["COMPONENTS", "DesignerComponents"],
+ ),
+ 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("knx", "Qt6", "Qt::Knx", extra=["COMPONENTS", "Knx"]),
+ 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("mqtt", "Qt6", "Qt::Mqtt", extra=["COMPONENTS", "Mqtt"]),
+ 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(
+ "quick3dassetimport",
+ "Qt6",
+ "Qt::Quick3DAssetImport",
+ extra=["COMPONENTS", "Quick3DAssetImport"],
+ ),
+ LibraryMapping("quick3d", "Qt6", "Qt::Quick3D", extra=["COMPONENTS", "Quick3D"]),
+ LibraryMapping(
+ "quick3drender", "Qt6", "Qt::Quick3DRender", extra=["COMPONENTS", "Quick3DRender"]
+ ),
+ LibraryMapping(
+ "quick3druntimerender",
+ "Qt6",
+ "Qt::Quick3DRuntimeRender",
+ extra=["COMPONENTS", "Quick3DRuntimeRender"],
+ ),
+ LibraryMapping("quick3dutils", "Qt6", "Qt::Quick3DUtils", extra=["COMPONENTS", "Quick3DUtils"]),
+ 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(
+ "remoteobjects", "Qt6", "Qt::RemoteObjects", extra=["COMPONENTS", "RemoteObjects"]
+ ),
+ LibraryMapping("script", "Qt6", "Qt::Script", extra=["COMPONENTS", "Script"]),
+ LibraryMapping("scripttools", "Qt6", "Qt::ScriptTools", extra=["COMPONENTS", "ScriptTools"]),
+ LibraryMapping("scxml", "Qt6", "Qt::Scxml", extra=["COMPONENTS", "Scxml"]),
+ LibraryMapping("sensors", "Qt6", "Qt::Sensors", extra=["COMPONENTS", "Sensors"]),
+ LibraryMapping("serialport", "Qt6", "Qt::SerialPort", extra=["COMPONENTS", "SerialPort"]),
+ LibraryMapping("serialbus", "Qt6", "Qt::SerialBus", extra=["COMPONENTS", "SerialBus"]),
+ 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("texttospeech", "Qt6", "Qt::TextToSpeech", extra=["COMPONENTS", "TextToSpeech"]),
+ 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(
+ "waylandclient", "Qt6", "Qt::WaylandClient", extra=["COMPONENTS", "WaylandClient"]
+ ),
+ LibraryMapping(
+ "waylandcompositor",
+ "Qt6",
+ "Qt::WaylandCompositor",
+ extra=["COMPONENTS", "WaylandCompositor"],
+ ),
+ 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"]),
+ LibraryMapping(
+ "qmlworkerscript", "Qt6", "Qt::QmlWorkerScript", extra=["COMPONENTS", "QmlWorkerScript"]
+ ),
+ LibraryMapping(
+ "quickparticles", "Qt6", "Qt::QuickParticles", extra=["COMPONENTS", "QuickParticles"]
+ ),
+ LibraryMapping(
+ "linuxofono_support",
+ "Qt6",
+ "Qt::LinuxOfonoSupport",
+ extra=["COMPONENTS", "LinuxOfonoSupport"],
+ ),
+ LibraryMapping(
+ "linuxofono_support_private",
+ "Qt6",
+ "Qt::LinuxOfonoSupportPrivate",
+ extra=["COMPONENTS", "LinuxOfonoSupportPrivate"],
+ ),
+ LibraryMapping("tools", "Qt6", "Qt::Tools", extra=["COMPONENTS", "Tools"]),
+ LibraryMapping("axcontainer", "Qt6", "Qt::AxContainer", extra=["COMPONENTS", "AxContainer"]),
+ LibraryMapping(
+ "webkitwidgets", "Qt6", "Qt::WebKitWidgets", extra=["COMPONENTS", "WebKitWidgets"]
+ )
+ # qtzlib: No longer supported.
+]
+
+# Note that the library map is adjusted dynamically further down.
+_library_map = [
+ # 3rd party:
+ LibraryMapping("atspi", "ATSPI2", "PkgConfig::ATSPI2"),
+ LibraryMapping("bluez", "BlueZ", "PkgConfig::BlueZ"),
+ LibraryMapping("corewlan", None, None),
+ LibraryMapping("cups", "Cups", "Cups::Cups"),
+ LibraryMapping("directfb", "DirectFB", "PkgConfig::DirectFB"),
+ LibraryMapping("db2", "DB2", "DB2::DB2"),
+ LibraryMapping("dbus", "WrapDBus1", "dbus-1", resultVariable="DBus1"),
+ LibraryMapping("doubleconversion", None, None),
+ LibraryMapping("drm", "Libdrm", "Libdrm::Libdrm"),
+ LibraryMapping("egl", "EGL", "EGL::EGL"),
+ LibraryMapping("flite", "Flite", "Flite::Flite"),
+ LibraryMapping("flite_alsa", "ALSA", "ALSA::ALSA"),
+ 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("gssapi", "GSSAPI", "GSSAPI::GSSAPI"),
+ LibraryMapping("harfbuzz", "WrapHarfbuzz", "WrapHarfbuzz::WrapHarfbuzz"),
+ 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("libclang", "WrapLibClang", "WrapLibClang::WrapLibClang"),
+ 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("mysql", "MySQL", "MySQL::MySQL"),
+ 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("oci", "Oracle", "Oracle::OCI"),
+ 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("speechd", "SpeechDispatcher", "SpeechDispatcher::SpeechDispatcher"),
+ 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"), # used in qtbase/src/gui
+ LibraryMapping("wayland-server", "Wayland", "Wayland::Server"), # used in qtwayland
+ LibraryMapping("wayland-client", "Wayland", "Wayland::Client"),
+ LibraryMapping("wayland-cursor", "Wayland", "Wayland::Cursor"),
+ LibraryMapping("wayland-egl", "Wayland", "Wayland::Egl"),
+ LibraryMapping(
+ "wayland-kms", "Waylandkms", "PkgConfig::Waylandkms"
+ ), # TODO: check if this actually works
+ LibraryMapping("x11", "X11", "X11::X11"),
+ LibraryMapping("x11sm", "X11", "${X11_SM_LIB} ${X11_ICE_LIB}", resultVariable="X11_SM"),
+ LibraryMapping(
+ "xcb",
+ "XCB",
+ "XCB::XCB",
+ extra=["1.9"],
+ resultVariable="TARGET XCB::XCB",
+ appendFoundSuffix=False,
+ ),
+ 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("xcomposite", "XComposite", "PkgConfig::XComposite"),
+ 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"),
+ LibraryMapping("sdl2", "WrapSDL2", "WrapSDL2::WrapSDL2"),
+]
+
+
+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(name: str) -> str:
+ replacement_char = "_"
+ if name.startswith("c++"):
+ replacement_char = "x"
+ return re.sub(r"[^a-zA-Z0-9_]", replacement_char, name)
+
+
+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",
+ "emscripten": "EMSCRIPTEN",
+ "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",
+ "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 = f"{ind}qt_find_package({lib.packageName} {' '.join(extra)})\n"
+ else:
+ result = f"{ind}qt_find_package({lib.packageName})\n"
+
+ if isRequired:
+ result += f"{ind}set_package_properties({lib.packageName} PROPERTIES TYPE REQUIRED)\n"
+ else:
+ if extra:
+ result = f"{ind}find_package({lib.packageName} {' '.join(extra)})\n"
+ else:
+ result = f"{ind}find_package({lib.packageName})\n"
+
+ # If a package should be found only in certain conditions, wrap
+ # the find_package call within that condition.
+ if emit_if:
+ result = f"if(({emit_if}) OR QT_FIND_ALL_PACKAGES_ALWAYS)\n{one_ind}{result}endif()\n"
+
+ 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..a0aaecab9d
--- /dev/null
+++ b/util/cmake/json_parser.py
@@ -0,0 +1,101 @@
+#!/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 # type: ignore
+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(f'Pre processing "{file}" using py parsing to remove incorrect newlines.')
+ 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(f'Parsing "{file}" using json.loads().')
+ 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..04443b00b7
--- /dev/null
+++ b/util/cmake/pro2cmake.py
@@ -0,0 +1,3993 @@
+#!/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
+
+import copy
+import os.path
+import posixpath
+import sys
+import re
+import io
+import glob
+
+from condition_simplifier import simplify_condition
+from condition_simplifier_cache import set_condition_simplified_cache_enabled
+
+import pyparsing as pp # type: ignore
+import xml.etree.ElementTree as ET
+
+from argparse import ArgumentParser
+from textwrap import dedent
+from textwrap import indent as textwrap_indent
+from functools import lru_cache
+from shutil import copyfile
+from collections import defaultdict
+from typing import (
+ List,
+ Optional,
+ Dict,
+ Set,
+ IO,
+ Union,
+ Any,
+ Callable,
+ FrozenSet,
+ Tuple,
+ Match,
+ Type,
+)
+
+from qmake_parser import parseProFile
+from special_case_helper import SpecialCaseHandler
+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,
+)
+
+
+cmake_version_string = "3.15.0"
+cmake_api_version = 2
+
+
+def _parse_commandline():
+ parser = ArgumentParser(
+ description="Generate CMakeLists.txt files from ." "pro files.",
+ epilog="Requirements: pip install sympy pyparsing",
+ )
+ 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(
+ "-e",
+ "--skip-condition-cache",
+ dest="skip_condition_cache",
+ action="store_true",
+ help="Don't use condition simplifier cache (conversion speed may decrease).",
+ )
+
+ parser.add_argument(
+ "--skip-subdirs-project",
+ dest="skip_subdirs_project",
+ action="store_true",
+ help="Skip converting project if it ends up being a TEMPLATE=subdirs project.",
+ )
+
+ parser.add_argument(
+ "-i",
+ "--ignore-skip-marker",
+ dest="ignore_skip_marker",
+ action="store_true",
+ help="If set, pro file will be converted even if skip marker is found in CMakeLists.txt.",
+ )
+
+ parser.add_argument(
+ "--api-version",
+ dest="api_version",
+ type=int,
+ help="Specify which cmake api version should be generated. 1 or 2, 2 is latest.",
+ )
+
+ parser.add_argument(
+ "files",
+ metavar="<.pro/.pri file>",
+ type=str,
+ nargs="+",
+ help="The .pro/.pri file to process",
+ )
+ return parser.parse_args()
+
+
+def is_top_level_repo_project(project_file_path: str = "") -> bool:
+ qmake_conf_path = find_qmake_conf(project_file_path)
+ qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
+ project_dir_path = os.path.dirname(project_file_path)
+ return qmake_conf_dir_path == project_dir_path
+
+
+def is_top_level_repo_tests_project(project_file_path: str = "") -> bool:
+ qmake_conf_path = find_qmake_conf(project_file_path)
+ qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
+ project_dir_path = os.path.dirname(project_file_path)
+ project_dir_name = os.path.basename(project_dir_path)
+ maybe_same_level_dir_path = os.path.join(project_dir_path, "..")
+ normalized_maybe_same_level_dir_path = os.path.normpath(maybe_same_level_dir_path)
+ return (
+ qmake_conf_dir_path == normalized_maybe_same_level_dir_path and project_dir_name == "tests"
+ )
+
+
+def is_top_level_repo_examples_project(project_file_path: str = "") -> bool:
+ qmake_conf_path = find_qmake_conf(project_file_path)
+ qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
+ project_dir_path = os.path.dirname(project_file_path)
+ project_dir_name = os.path.basename(project_dir_path)
+ maybe_same_level_dir_path = os.path.join(project_dir_path, "..")
+ normalized_maybe_same_level_dir_path = os.path.normpath(maybe_same_level_dir_path)
+ return (
+ qmake_conf_dir_path == normalized_maybe_same_level_dir_path
+ and project_dir_name == "examples"
+ )
+
+
+def is_example_project(project_file_path: str = "") -> bool:
+ qmake_conf_path = find_qmake_conf(project_file_path)
+ qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
+
+ project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
+ # If the project file is found in a subdir called 'examples'
+ # relative to the repo source dir, then it must be an example, but
+ # some examples contain 3rdparty libraries that do not need to be
+ # built as examples.
+ return project_relative_path.startswith("examples") and "3rdparty" not in project_relative_path
+
+
+def is_config_test_project(project_file_path: str = "") -> bool:
+ qmake_conf_path = find_qmake_conf(project_file_path)
+ qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
+ dir_name_with_qmake_confg = os.path.basename(qmake_conf_dir_path)
+
+ project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
+ # If the project file is found in a subdir called 'config.tests'
+ # relative to the repo source dir, then it's probably a config test.
+ # Also if the .qmake.conf is found within config.tests dir (like in qtbase)
+ # then the project is probably a config .test
+ return (
+ project_relative_path.startswith("config.tests")
+ or dir_name_with_qmake_confg == "config.tests"
+ )
+
+
+def is_benchmark_project(project_file_path: str = "") -> bool:
+ qmake_conf_path = find_qmake_conf(project_file_path)
+ qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
+
+ project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
+ # If the project file is found in a subdir called 'tests/benchmarks'
+ # relative to the repo source dir, then it must be a benchmark
+ return project_relative_path.startswith("tests/benchmarks")
+
+
+def is_manual_test_project(project_file_path: str = "") -> bool:
+ qmake_conf_path = find_qmake_conf(project_file_path)
+ qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
+
+ project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
+ # If the project file is found in a subdir called 'tests/manual'
+ # relative to the repo source dir, then it must be a manual test
+ return project_relative_path.startswith("tests/manual")
+
+
+@lru_cache(maxsize=None)
+def find_qmake_conf(project_file_path: str = "") -> str:
+ if not os.path.isabs(project_file_path):
+ print(
+ f"Warning: could not find .qmake.conf file, given path is not an "
+ f"absolute path: {project_file_path}"
+ )
+ return ""
+
+ cwd = os.path.dirname(project_file_path)
+ file_name = ".qmake.conf"
+
+ while os.path.isdir(cwd):
+ maybe_file = posixpath.join(cwd, file_name)
+ if os.path.isfile(maybe_file):
+ return maybe_file
+ else:
+ cwd = os.path.dirname(cwd)
+
+ print(f"Warning: could not find .qmake.conf file")
+ return ""
+
+
+def set_up_cmake_api_calls():
+ def nested_dict():
+ return defaultdict(nested_dict)
+
+ api = nested_dict()
+
+ api[1]["qt_extend_target"] = "extend_target"
+ api[1]["qt_add_module"] = "add_qt_module"
+ api[1]["qt_add_plugin"] = "add_qt_plugin"
+ api[1]["qt_add_tool"] = "add_qt_tool"
+ api[1]["qt_add_test"] = "add_qt_test"
+ api[1]["qt_add_test_helper"] = "add_qt_test_helper"
+ api[1]["qt_add_manual_test"] = "add_qt_manual_test"
+ api[1]["qt_add_benchmark"] = "add_qt_benchmark"
+ api[1]["qt_add_executable"] = "add_qt_executable"
+ api[1]["qt_add_simd_part"] = "add_qt_simd_part"
+ api[1]["qt_add_docs"] = "add_qt_docs"
+ api[1]["qt_add_resource"] = "add_qt_resource"
+ api[1]["qt_add_qml_module"] = "add_qml_module"
+ api[1]["qt_add_cmake_library"] = "add_cmake_library"
+
+ api[2]["qt_extend_target"] = "qt_extend_target"
+ api[2]["qt_add_module"] = "qt_add_module"
+ api[2]["qt_add_plugin"] = "qt_add_plugin"
+ api[2]["qt_add_tool"] = "qt_add_tool"
+ api[2]["qt_add_test"] = "qt_add_test"
+ api[2]["qt_add_test_helper"] = "qt_add_test_helper"
+ api[2]["qt_add_manual_test"] = "qt_add_manual_test"
+ api[2]["qt_add_benchmark"] = "qt_add_benchmark"
+ api[2]["qt_add_executable"] = "qt_add_executable"
+ api[2]["qt_add_simd_part"] = "qt_add_simd_part"
+ api[2]["qt_add_docs"] = "qt_add_docs"
+ api[2]["qt_add_resource"] = "qt_add_resource"
+ api[2]["qt_add_qml_module"] = "qt_add_qml_module"
+ api[2]["qt_add_cmake_library"] = "qt_add_cmake_library"
+
+ return api
+
+
+cmake_api_calls = set_up_cmake_api_calls()
+
+
+def detect_cmake_api_version_used_in_file_content(project_file_path: str) -> Optional[int]:
+ dir_path = os.path.dirname(project_file_path)
+ cmake_project_path = os.path.join(dir_path, "CMakeLists.txt")
+
+ # If file doesn't exist, None implies default version selected by
+ # script.
+ if not os.path.exists(cmake_project_path):
+ return None
+
+ with open(cmake_project_path, "r") as file_fd:
+ contents = file_fd.read()
+
+ new_api_calls = [api_call for api_call in cmake_api_calls[2]]
+ new_api_calls_alternatives = "|".join(new_api_calls)
+ match = re.search(new_api_calls_alternatives, contents)
+
+ # If new style found, return latest api version. Otherwise
+ # the old version.
+ if match:
+ return 2
+ else:
+ return 1
+
+
+def get_cmake_api_call(api_name: str, api_version: Optional[int] = None) -> str:
+ if not api_version:
+ global cmake_api_version
+ api_version = cmake_api_version
+ if not cmake_api_calls[api_version][api_name]:
+ raise RuntimeError(f"No CMake API call {api_name} of version {api_version} found.")
+
+ return cmake_api_calls[api_version][api_name]
+
+
+def process_qrc_file(
+ target: str,
+ filepath: str,
+ base_dir: str = "",
+ project_file_path: str = "",
+ skip_qtquick_compiler: bool = False,
+ retain_qtquick_compiler: bool = False,
+ is_example: bool = False,
+) -> str:
+ assert target
+
+ # Hack to handle QT_SOURCE_TREE. Assume currently that it's the same
+ # as the qtbase source path.
+ qt_source_tree_literal = "${QT_SOURCE_TREE}"
+ if qt_source_tree_literal in filepath:
+ qmake_conf = find_qmake_conf(project_file_path)
+
+ if qmake_conf:
+ qt_source_tree = os.path.dirname(qmake_conf)
+ filepath = filepath.replace(qt_source_tree_literal, qt_source_tree)
+ else:
+ print(
+ f"Warning, could not determine QT_SOURCE_TREE location while trying "
+ f"to find: {filepath}"
+ )
+
+ resource_name = os.path.splitext(os.path.basename(filepath))[0]
+ dir_name = os.path.dirname(filepath)
+ base_dir = posixpath.join("" if base_dir == "." else base_dir, dir_name)
+
+ # Small not very thorough check to see if this a shared qrc resource
+ # pattern is mostly used by the tests.
+ is_parent_path = dir_name.startswith("..")
+ if not os.path.isfile(filepath):
+ raise RuntimeError(f"Invalid file path given to process_qrc_file: {filepath}")
+
+ tree = ET.parse(filepath)
+ root = tree.getroot()
+ assert root.tag == "RCC"
+
+ output = ""
+
+ resource_count = 0
+ for resource in root:
+ assert resource.tag == "qresource"
+ lang = resource.get("lang", "")
+ prefix = resource.get("prefix", "/")
+ if not prefix.startswith("/"):
+ prefix = f"/{prefix}"
+
+ full_resource_name = resource_name + (str(resource_count) if resource_count > 0 else "")
+
+ files: Dict[str, str] = {}
+ for file in resource:
+ path = file.text
+ assert path
+
+ # Get alias:
+ alias = file.get("alias", "")
+ # In cases where examples use shared resources, we set the alias
+ # too the same name of the file, or the applications won't be
+ # be able to locate the resource
+ if not alias and is_parent_path:
+ alias = path
+ files[path] = alias
+
+ output += write_add_qt_resource_call(
+ target,
+ full_resource_name,
+ prefix,
+ base_dir,
+ lang,
+ files,
+ skip_qtquick_compiler,
+ retain_qtquick_compiler,
+ is_example,
+ )
+ resource_count += 1
+
+ return output
+
+
+def write_add_qt_resource_call(
+ target: str,
+ resource_name: str,
+ prefix: Optional[str],
+ base_dir: str,
+ lang: Optional[str],
+ files: Dict[str, str],
+ skip_qtquick_compiler: bool,
+ retain_qtquick_compiler: bool,
+ is_example: bool,
+) -> str:
+ output = ""
+
+ sorted_files = sorted(files.keys())
+
+ assert sorted_files
+
+ for source in sorted_files:
+ alias = files[source]
+ if alias:
+ full_source = posixpath.join(base_dir, source)
+ output += dedent(
+ f"""\
+ set_source_files_properties("{full_source}"
+ PROPERTIES QT_RESOURCE_ALIAS "{alias}"
+ )
+ """
+ )
+
+ # Quote file paths in case there are spaces.
+ sorted_files_backup = sorted_files
+ sorted_files = []
+ for source in sorted_files_backup:
+ if source.startswith("${"):
+ sorted_files.append(source)
+ else:
+ sorted_files.append(f'"{source}"')
+
+ file_list = "\n ".join(sorted_files)
+ output += dedent(
+ f"""\
+ set({resource_name}_resource_files
+ {file_list}
+ )\n
+ """
+ )
+ file_list = f"${{{resource_name}_resource_files}}"
+ if skip_qtquick_compiler:
+ output += (
+ f"set_source_files_properties(${{{resource_name}_resource_files}}"
+ " PROPERTIES QT_SKIP_QUICKCOMPILER 1)\n\n"
+ )
+
+ if retain_qtquick_compiler:
+ output += (
+ f"set_source_files_properties(${{{resource_name}_resource_files}}"
+ "PROPERTIES QT_RETAIN_QUICKCOMPILER 1)\n\n"
+ )
+
+ params = ""
+ if lang:
+ params += f'{spaces(1)}LANG\n{spaces(2)}"{lang}"\n'
+ params += f'{spaces(1)}PREFIX\n{spaces(2)}"{prefix}"\n'
+ if base_dir:
+ params += f'{spaces(1)}BASE\n{spaces(2)}"{base_dir}"\n'
+ add_resource_command = ""
+ if is_example:
+ add_resource_command = "qt6_add_resources"
+ else:
+ add_resource_command = get_cmake_api_call("qt_add_resource")
+ output += (
+ f'{add_resource_command}({target} "{resource_name}"\n{params}{spaces(1)}FILES\n'
+ f"{spaces(2)}{file_list}\n)\n"
+ )
+
+ return output
+
+
+class QmlDirFileInfo:
+ def __init__(self, file_path: str, type_name: str) -> None:
+ self.file_path = file_path
+ self.version = ""
+ self.type_name = type_name
+ self.internal = False
+ self.singleton = False
+ self.path = ""
+
+
+class QmlDir:
+ def __init__(self) -> None:
+ self.module = ""
+ self.plugin_name = ""
+ self.plugin_path = ""
+ self.classname = ""
+ self.imports: List[str] = []
+ self.type_names: Dict[str, QmlDirFileInfo] = {}
+ self.type_infos: List[str] = []
+ self.depends: List[Tuple[str, str]] = []
+ self.designer_supported = False
+
+ def __str__(self) -> str:
+ type_infos_line = " \n".join(self.type_infos)
+ imports_line = " \n".join(self.imports)
+ string = f"""\
+ module: {self.module}
+ plugin: {self.plugin_name} {self.plugin_path}
+ classname: {self.classname}
+ type_infos:{type_infos_line}
+ imports:{imports_line}
+ dependends:
+ """
+ for dep in self.depends:
+ string += f" {dep[0]} {dep[1]}\n"
+ string += f"designer supported: {self.designer_supported}\n"
+ string += "type_names:\n"
+ for key in self.type_names:
+ file_info = self.type_names[key]
+ string += (
+ f" type:{file_info.type_name} "
+ f"version:{file_info.version} "
+ f"path:{file_info.file_path} "
+ f"internal:{file_info.internal} "
+ f"singleton:{file_info.singleton}\n"
+ )
+ return string
+
+ def get_or_create_file_info(self, path: str, type_name: str) -> QmlDirFileInfo:
+ if path not in self.type_names:
+ self.type_names[path] = QmlDirFileInfo(path, type_name)
+ qmldir_file = self.type_names[path]
+ if qmldir_file.type_name != type_name:
+ raise RuntimeError("Registered qmldir file type_name does not match.")
+ return qmldir_file
+
+ def handle_file_internal(self, type_name: str, path: str):
+ qmldir_file = self.get_or_create_file_info(path, type_name)
+ qmldir_file.internal = True
+
+ def handle_file_singleton(self, type_name: str, version: str, path: str):
+ qmldir_file = self.handle_file(type_name, version, path)
+ qmldir_file.singleton = True
+
+ def handle_file(self, type_name: str, version: str, path: str) -> QmlDirFileInfo:
+ qmldir_file = self.get_or_create_file_info(path, type_name)
+ qmldir_file.version = version
+ qmldir_file.type_name = type_name
+ qmldir_file.path = path
+ return qmldir_file
+
+ def from_lines(self, lines: List[str]):
+ for line in lines:
+ self.handle_line(line)
+
+ def from_file(self, path: str):
+ f = open(path, "r")
+ if not f:
+ raise RuntimeError(f"Failed to open qmldir file at: {path}")
+ for line in f:
+ self.handle_line(line)
+
+ def handle_line(self, line: str):
+ if line.startswith("#"):
+ return
+ line = line.strip().replace("\n", "")
+ if len(line) == 0:
+ return
+
+ entries = line.split(" ")
+ if len(entries) == 0:
+ raise RuntimeError("Unexpected QmlDir file line entry")
+ if entries[0] == "module":
+ self.module = entries[1]
+ elif entries[0] == "[singleton]":
+ self.handle_file_singleton(entries[1], entries[2], entries[3])
+ elif entries[0] == "internal":
+ self.handle_file_internal(entries[1], entries[2])
+ elif entries[0] == "plugin":
+ self.plugin_name = entries[1]
+ if len(entries) > 2:
+ self.plugin_path = entries[2]
+ elif entries[0] == "classname":
+ self.classname = entries[1]
+ elif entries[0] == "typeinfo":
+ self.type_infos.append(entries[1])
+ elif entries[0] == "depends":
+ self.depends.append((entries[1], entries[2]))
+ elif entries[0] == "designersupported":
+ self.designer_supported = True
+ elif entries[0] == "import":
+ self.imports.append(entries[1])
+ elif len(entries) == 3:
+ self.handle_file(entries[0], entries[1], entries[2])
+ else:
+ raise RuntimeError(f"Uhandled qmldir entry {line}")
+
+
+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 = posixpath.join(base_dir, f)
+
+ return trim_leading_dot(f)
+
+
+def handle_vpath(source: str, base_dir: str, vpath: 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 = posixpath.join(v, source)
+ if os.path.exists(fullpath):
+ return trim_leading_dot(posixpath.relpath(fullpath, base_dir))
+
+ print(f" XXXX: Source {source}: Not found.")
+ return f"{source}-NOTFOUND"
+
+
+class Operation:
+ def __init__(self, value: Union[List[str], str], line_no: int = -1) -> None:
+ if isinstance(value, list):
+ self._value = value
+ else:
+ self._value = [str(value)]
+ self._line_no = line_no
+
+ def process(
+ self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
+ ) -> 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, sinput: List[str], transformer: Callable[[List[str]], List[str]]
+ ) -> List[str]:
+ return sinput + transformer(self._value)
+
+ def __repr__(self):
+ return f"+({self._dump()})"
+
+
+class UniqueAddOperation(Operation):
+ def process(
+ self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
+ ) -> List[str]:
+ result = sinput
+ for v in transformer(self._value):
+ if v not in result:
+ result.append(v)
+ return result
+
+ def __repr__(self):
+ return f"*({self._dump()})"
+
+
+class ReplaceOperation(Operation):
+ def process(
+ self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
+ ) -> List[str]:
+ result = []
+ for s in sinput:
+ for v in transformer(self._value):
+ pattern, replacement = self.split_rex(v)
+ result.append(re.sub(pattern, replacement, s))
+ return result
+
+ def split_rex(self, s):
+ pattern = ""
+ replacement = ""
+ if len(s) < 4:
+ return pattern, replacement
+ sep = s[1]
+ s = s[2:]
+ rex = re.compile(f"[^\\\\]{sep}")
+ m = rex.search(s)
+ if not m:
+ return pattern, replacement
+ pattern = s[: m.start() + 1]
+ replacement = s[m.end() :]
+ m = rex.search(replacement)
+ if m:
+ replacement = replacement[: m.start() + 1]
+ return pattern, replacement
+
+ def __repr__(self):
+ return f"*({self._dump()})"
+
+
+class SetOperation(Operation):
+ def process(
+ self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
+ ) -> List[str]:
+ values = [] # List[str]
+ for v in self._value:
+ if v != f"$${key}":
+ values.append(v)
+ else:
+ values += sinput
+
+ if transformer:
+ return list(transformer(values))
+ else:
+ return values
+
+ def __repr__(self):
+ return f"=({self._dump()})"
+
+
+class RemoveOperation(Operation):
+ def process(
+ self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
+ ) -> List[str]:
+ sinput_set = set(sinput)
+ value_set = set(self._value)
+ result: List[str] = []
+
+ # Add everything that is not going to get removed:
+ for v in sinput:
+ if v not in value_set:
+ result += [v]
+
+ # Add everything else with removal marker:
+ for v in transformer(self._value):
+ if v not in sinput_set:
+ result += [f"-{v}"]
+
+ return result
+
+ def __repr__(self):
+ return f"-({self._dump()})"
+
+
+# Helper class that stores a list of tuples, representing a scope id and
+# a line number within that scope's project file. The whole list
+# represents the full path location for a certain operation while
+# traversing include()'d scopes. Used for sorting when determining
+# operation order when evaluating operations.
+class OperationLocation(object):
+ def __init__(self):
+ self.list_of_scope_ids_and_line_numbers = []
+
+ def clone_and_append(self, scope_id: int, line_number: int) -> OperationLocation:
+ new_location = OperationLocation()
+ new_location.list_of_scope_ids_and_line_numbers = list(
+ self.list_of_scope_ids_and_line_numbers
+ )
+ new_location.list_of_scope_ids_and_line_numbers.append((scope_id, line_number))
+ return new_location
+
+ def __lt__(self, other: OperationLocation) -> Any:
+ return self.list_of_scope_ids_and_line_numbers < other.list_of_scope_ids_and_line_numbers
+
+ def __repr__(self) -> str:
+ s = ""
+ for t in self.list_of_scope_ids_and_line_numbers:
+ s += f"s{t[0]}:{t[1]} "
+ s = s.strip(" ")
+ return s
+
+
+class Scope(object):
+
+ SCOPE_ID: int = 1
+
+ def __init__(
+ self,
+ *,
+ parent_scope: Optional[Scope],
+ qmake_file: str,
+ condition: str = "",
+ base_dir: str = "",
+ operations: Union[Dict[str, List[Operation]], None] = None,
+ parent_include_line_no: int = -1,
+ ) -> None:
+ if not operations:
+ operations = {
+ "QT_SOURCE_TREE": [SetOperation(["${QT_SOURCE_TREE}"])],
+ "QT_BUILD_TREE": [SetOperation(["${PROJECT_BINARY_DIR}"])],
+ "QTRO_SOURCE_TREE": [SetOperation(["${CMAKE_SOURCE_DIR}"])],
+ }
+
+ self._operations: Dict[str, List[Operation]] = copy.deepcopy(operations)
+ if parent_scope:
+ parent_scope._add_child(self)
+ else:
+ self._parent = None # type: Optional[Scope]
+ # Only add the "QT = core gui" Set operation once, on the
+ # very top-level .pro scope, aka it's basedir is empty.
+ if not base_dir:
+ self._operations["QT"] = [SetOperation(["core", "gui"])]
+
+ self._basedir = base_dir
+ if qmake_file:
+ self._currentdir = os.path.dirname(qmake_file) or "."
+ if not self._basedir:
+ self._basedir = self._currentdir
+
+ self._scope_id = Scope.SCOPE_ID
+ Scope.SCOPE_ID += 1
+ self._file = qmake_file
+ self._file_absolute_path = os.path.abspath(qmake_file)
+ self._condition = map_condition(condition)
+ self._children = [] # type: List[Scope]
+ self._included_children = [] # type: List[Scope]
+ self._visited_keys = set() # type: Set[str]
+ self._total_condition = None # type: Optional[str]
+ self._parent_include_line_no = parent_include_line_no
+
+ def __repr__(self):
+ return (
+ f"{self._scope_id}:{self._basedir}:{self._currentdir}:{self._file}:"
+ f"{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) -> 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: List[Scope] = []
+ for c in self._children:
+ c.settle_condition()
+
+ if c.can_merge_condition():
+ child = c._children[0]
+ child._condition = "({c._condition}) AND ({child._condition})"
+ new_children += c._children
+ else:
+ new_children.append(c)
+ self._children = new_children
+
+ @staticmethod
+ def FromDict(
+ parent_scope: Optional["Scope"],
+ file: str,
+ statements,
+ cond: str = "",
+ base_dir: str = "",
+ project_file_content: str = "",
+ parent_include_line_no: int = -1,
+ ) -> Scope:
+ scope = Scope(
+ parent_scope=parent_scope,
+ qmake_file=file,
+ condition=cond,
+ base_dir=base_dir,
+ parent_include_line_no=parent_include_line_no,
+ )
+ 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 != ""
+
+ op_location_start = operation["locn_start"]
+ operation = operation["value"]
+ op_line_no = pp.lineno(op_location_start, project_file_content)
+
+ if operation == "=":
+ scope._append_operation(key, SetOperation(value, line_no=op_line_no))
+ elif operation == "-=":
+ scope._append_operation(key, RemoveOperation(value, line_no=op_line_no))
+ elif operation == "+=":
+ scope._append_operation(key, AddOperation(value, line_no=op_line_no))
+ elif operation == "*=":
+ scope._append_operation(key, UniqueAddOperation(value, line_no=op_line_no))
+ elif operation == "~=":
+ scope._append_operation(key, ReplaceOperation(value, line_no=op_line_no))
+ else:
+ print(f'Unexpected operation "{operation}" in scope "{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:
+ included_location_start = included["locn_start"]
+ included = included["value"]
+ included_line_no = pp.lineno(included_location_start, project_file_content)
+ scope._append_operation(
+ "_INCLUDED", UniqueAddOperation(included, line_no=included_line_no)
+ )
+ continue
+
+ project_required_condition = statement.get("project_required_condition")
+ if project_required_condition:
+ scope._append_operation("_REQUIREMENTS", AddOperation(project_required_condition))
+
+ scope.settle_condition()
+
+ if scope.scope_debug:
+ print(f"..... [SCOPE_DEBUG]: Created scope {scope}:")
+ scope.dump(indent=1)
+ print("..... [SCOPE_DEBUG]: <<END OF SCOPE>>")
+ return scope
+
+ def _append_operation(self, key: str, op: Operation) -> None:
+ if key in self._operations:
+ self._operations[key].append(op)
+ else:
+ self._operations[key] = [op]
+
+ @property
+ def file(self) -> str:
+ return self._file or ""
+
+ @property
+ def file_absolute_path(self) -> str:
+ return self._file_absolute_path or ""
+
+ @property
+ def generated_cmake_lists_path(self) -> str:
+ assert self.basedir
+ return os.path.join(self.basedir, "CMakeLists.gen.txt")
+
+ @property
+ def original_cmake_lists_path(self) -> str:
+ assert self.basedir
+ return os.path.join(self.basedir, "CMakeLists.txt")
+
+ @property
+ def condition(self) -> str:
+ return self._condition
+
+ @property
+ def total_condition(self) -> 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) -> 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 = spaces(indent)
+ print(f'{ind}Scope "{self}":')
+ if self.total_condition:
+ print(f"{ind} Total condition = {self.total_condition}")
+ print(f"{ind} Keys:")
+ keys = self._operations.keys()
+ if not keys:
+ print(f"{ind} -- NONE --")
+ else:
+ for k in sorted(keys):
+ print(f'{ind} {k} = "{self._operations.get(k, [])}"')
+ print(f"{ind} Children:")
+ if not self._children:
+ print(f"{ind} -- NONE --")
+ else:
+ for c in self._children:
+ c.dump(indent=indent + 1)
+ print(f"{ind} Includes:")
+ if not self._included_children:
+ print(f"{ind} -- NONE --")
+ else:
+ for c in self._included_children:
+ c.dump(indent=indent + 1)
+
+ def dump_structure(self, *, structure_type: str = "ROOT", indent: int = 0) -> None:
+ print(f"{spaces(indent)}{structure_type}: {self}")
+ for i in self._included_children:
+ i.dump_structure(structure_type="INCL", indent=indent + 1)
+ for i in self._children:
+ i.dump_structure(structure_type="CHLD", indent=indent + 1)
+
+ @property
+ def keys(self):
+ return self._operations.keys()
+
+ @property
+ def visited_keys(self):
+ return self._visited_keys
+
+ # Traverses a scope and its children, and collects operations
+ # that need to be processed for a certain key.
+ def _gather_operations_from_scope(
+ self,
+ operations_result: List[Dict[str, Any]],
+ current_scope: Scope,
+ op_key: str,
+ current_location: OperationLocation,
+ ):
+ for op in current_scope._operations.get(op_key, []):
+ new_op_location = current_location.clone_and_append(
+ current_scope._scope_id, op._line_no
+ )
+ op_info: Dict[str, Any] = {}
+ op_info["op"] = op
+ op_info["scope"] = current_scope
+ op_info["location"] = new_op_location
+ operations_result.append(op_info)
+
+ for included_child in current_scope._included_children:
+ new_scope_location = current_location.clone_and_append(
+ current_scope._scope_id, included_child._parent_include_line_no
+ )
+ self._gather_operations_from_scope(
+ operations_result, included_child, op_key, new_scope_location
+ )
+
+ # Partially applies a scope argument to a given transformer.
+ @staticmethod
+ def _create_transformer_for_operation(
+ given_transformer: Optional[Callable[[Scope, List[str]], List[str]]],
+ transformer_scope: Scope,
+ ) -> Callable[[List[str]], List[str]]:
+ if given_transformer:
+
+ def wrapped_transformer(values):
+ return given_transformer(transformer_scope, values)
+
+ else:
+
+ def wrapped_transformer(values):
+ return values
+
+ return wrapped_transformer
+
+ def _evalOps(
+ self,
+ key: str,
+ transformer: Optional[Callable[[Scope, List[str]], List[str]]],
+ result: List[str],
+ *,
+ inherit: bool = False,
+ ) -> List[str]:
+ self._visited_keys.add(key)
+
+ # Inherit values from parent scope.
+ # This is a strange edge case which is wrong in principle, because
+ # .pro files are imperative and not declarative. Nevertheless
+ # this fixes certain mappings (e.g. for handling
+ # VERSIONTAGGING_SOURCES in src/corelib/global/global.pri).
+ if self._parent and inherit:
+ result = self._parent._evalOps(key, transformer, result)
+
+ operations_to_run: List[Dict[str, Any]] = []
+ starting_location = OperationLocation()
+ starting_scope = self
+ self._gather_operations_from_scope(
+ operations_to_run, starting_scope, key, starting_location
+ )
+
+ # Sorts the operations based on the location of each operation. Technically compares two
+ # lists of tuples.
+ operations_to_run = sorted(operations_to_run, key=lambda o: o["location"])
+
+ # Process the operations.
+ for op_info in operations_to_run:
+ op_transformer = self._create_transformer_for_operation(transformer, op_info["scope"])
+ result = op_info["op"].process(key, result, op_transformer)
+ return result
+
+ def get(self, key: str, *, ignore_includes: bool = False, inherit: bool = False) -> List[str]:
+ is_same_path = self.currentdir == self.basedir
+ if not is_same_path:
+ relative_path = os.path.relpath(self.currentdir, self.basedir)
+
+ if key == "QQC2_SOURCE_TREE":
+ qmake_conf_path = find_qmake_conf(os.path.abspath(self.currentdir))
+ qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
+ project_relative_path = os.path.relpath(qmake_conf_dir_path, self.currentdir)
+ return ["${CMAKE_CURRENT_SOURCE_DIR}/" + project_relative_path]
+
+ if key == "QT_ARCH":
+ return ["${CMAKE_SYSTEM_PROCESSOR}"]
+
+ if key == "_PRO_FILE_PWD_":
+ return ["${CMAKE_CURRENT_SOURCE_DIR}"]
+ if key == "PWD":
+ if is_same_path:
+ return ["${CMAKE_CURRENT_SOURCE_DIR}"]
+ else:
+ return [f"${{CMAKE_CURRENT_SOURCE_DIR}}/{relative_path}"]
+ if key == "OUT_PWD":
+ if is_same_path:
+ return ["${CMAKE_CURRENT_BINARY_DIR}"]
+ else:
+ return [f"${{CMAKE_CURRENT_BINARY_DIR}}/{relative_path}"]
+
+ return self._evalOps(key, None, [], inherit=inherit)
+
+ def get_string(self, key: str, default: str = "", inherit: bool = False) -> str:
+ v = self.get(key, inherit=inherit)
+ if len(v) == 0:
+ return default
+ assert len(v) == 1
+ return v[0]
+
+ def _map_files(
+ self, files: List[str], *, use_vpath: bool = True, is_include: bool = False
+ ) -> List[str]:
+
+ expanded_files = [] # type: List[str]
+ for f in files:
+ r = self._expand_value(f)
+ expanded_files += r
+
+ mapped_files = list(
+ map(lambda f: map_to_file(f, self, is_include=is_include), expanded_files)
+ )
+
+ if use_vpath:
+ result = list(
+ map(
+ lambda f: handle_vpath(f, self.basedir, self.get("VPATH", inherit=True)),
+ mapped_files,
+ )
+ )
+ else:
+ result = mapped_files
+
+ # strip ${CMAKE_CURRENT_SOURCE_DIR}:
+ result = list(
+ map(lambda f: f[28:] if f.startswith("${CMAKE_CURRENT_SOURCE_DIR}/") else f, result)
+ )
+
+ # strip leading ./:
+ result = list(map(lambda f: trim_leading_dot(f), result))
+
+ return result
+
+ def get_files(
+ self, key: str, *, use_vpath: bool = False, is_include: bool = False
+ ) -> List[str]:
+ def transformer(scope, files):
+ return scope._map_files(files, use_vpath=use_vpath, is_include=is_include)
+
+ return list(self._evalOps(key, transformer, []))
+
+ @staticmethod
+ def _replace_env_var_value(value: Any) -> Any:
+ if not isinstance(value, str):
+ return value
+
+ pattern = re.compile(r"\$\$\(([A-Za-z_][A-Za-z0-9_]*)\)")
+ match = re.search(pattern, value)
+ if match:
+ value = re.sub(pattern, r"$ENV{\1}", value)
+
+ return value
+
+ def _expand_value(self, value: str) -> 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
+ match_group_0 = match.group(0)
+ if match_group_0 == value:
+ get_result = self.get(match.group(1), inherit=True)
+ if len(get_result) == 1:
+ result = get_result[0]
+ result = self._replace_env_var_value(result)
+ else:
+ # Recursively expand each value from the result list
+ # returned from self.get().
+ result_list: List[str] = []
+ for entry_value in get_result:
+ result_list += self._expand_value(self._replace_env_var_value(entry_value))
+ return result_list
+ else:
+ replacement = self.get(match.group(1), inherit=True)
+ replacement_str = replacement[0] if replacement else ""
+ if replacement_str == value:
+ # we have recursed
+ replacement_str = ""
+ result = result[: match.start()] + replacement_str + result[match.end() :]
+ result = self._replace_env_var_value(result)
+
+ if result == old_result:
+ return [result] # Do not go into infinite loop
+
+ match = re.search(pattern, result)
+
+ result = self._replace_env_var_value(result)
+ return [result]
+
+ def expand(self, key: str) -> List[str]:
+ value = self.get(key)
+ result: 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]
+
+ def _get_operation_at_index(self, key, index):
+ return self._operations[key][index]
+
+ @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:
+ target = self.expandString("TARGET") or os.path.splitext(os.path.basename(self.file))[0]
+ return re.sub(r"\.\./", "", target)
+
+ @property
+ def TARGET_ORIGINAL(self) -> str:
+ return self.expandString("TARGET") or os.path.splitext(os.path.basename(self.file))[0]
+
+ @property
+ def _INCLUDED(self) -> List[str]:
+ return self.get("_INCLUDED")
+
+
+# Given "if(a|b):c" returns "(a|b):c". Uses pyparsing to keep the parentheses
+# balanced.
+def unwrap_if(input_string):
+ # Compute the grammar only once.
+ if not hasattr(unwrap_if, "if_grammar"):
+
+ def handle_expr_with_parentheses(s, l, t):
+ # The following expression unwraps the condition via the
+ # additional info set by originalTextFor, thus returning the
+ # condition without parentheses.
+ condition_without_parentheses = s[t._original_start + 1 : t._original_end - 1]
+
+ # Re-add the parentheses, but with spaces in-between. This
+ # fixes map_condition -> map_platform to apply properly.
+ condition_with_parentheses = "( " + condition_without_parentheses + " )"
+ return condition_with_parentheses
+
+ expr_with_parentheses = pp.originalTextFor(pp.nestedExpr())
+ expr_with_parentheses.setParseAction(handle_expr_with_parentheses)
+
+ if_keyword = pp.Suppress(pp.Keyword("if"))
+ unwrap_if.if_grammar = if_keyword + expr_with_parentheses
+
+ output_string = unwrap_if.if_grammar.transformString(input_string)
+ return output_string
+
+
+def map_condition(condition: str) -> str:
+ # Some hardcoded cases that are too bothersome to generalize.
+ condition = re.sub(
+ r"qtConfig\(opengles\.\)",
+ r"(QT_FEATURE_opengles2 OR QT_FEATURE_opengles3 OR QT_FEATURE_opengles31 OR QT_FEATURE_opengles32)",
+ condition,
+ )
+ 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)
+ condition = re.sub(r"^no-png$", r"NOT QT_FEATURE_png", condition)
+ condition = re.sub(r"contains\(CONFIG, static\)", r"NOT QT_BUILD_SHARED_LIBS", condition)
+ condition = re.sub(r"contains\(QT_CONFIG,\w*shared\)", r"QT_BUILD_SHARED_LIBS", condition)
+ condition = re.sub(r"CONFIG\(osx\)", r"APPLE_OSX", condition)
+
+ def gcc_version_handler(match_obj: 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 f"(QT_COMPILER_VERSION_{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)
+
+ def windows_sdk_version_handler(match_obj: Match):
+ operator = match_obj.group(1)
+ if operator == "equals":
+ operator = "STREQUAL"
+ elif operator == "greaterThan":
+ operator = "STRGREATER"
+ elif operator == "lessThan":
+ operator = "STRLESS"
+
+ version = match_obj.group(2)
+ return f"(QT_WINDOWS_SDK_VERSION {operator} {version})"
+
+ pattern = r"(equals|greaterThan|lessThan)\(WINDOWS_SDK_VERSION,[ ]*([0-9]+)\)"
+ condition = re.sub(pattern, windows_sdk_version_handler, condition)
+
+ # Generic lessThan|equals|lessThan()
+
+ def generic_version_handler(match_obj: Match):
+ operator = match_obj.group(1)
+ if operator == "equals":
+ operator = "EQUAL"
+ elif operator == "greaterThan":
+ operator = "GREATER"
+ elif operator == "lessThan":
+ operator = "LESS"
+
+ variable = match_obj.group(2)
+ version = match_obj.group(3)
+ return f"({variable} {operator} {version})"
+
+ pattern = r"(equals|greaterThan|lessThan)\(([^,]+?),[ ]*([0-9]+)\)"
+ condition = re.sub(pattern, generic_version_handler, condition)
+
+ # Handle if(...) conditions.
+ condition = unwrap_if(condition)
+
+ condition = re.sub(r"\bisEmpty\s*\((.*?)\)", r"\1_ISEMPTY", condition)
+ condition = re.sub(
+ r"\bcontains\s*\(\s*(?:QT_)?CONFIG\s*,\s*c\+\+(\d+)\)",
+ r"cxx_std_\1 IN_LIST CMAKE_CXX_COMPILE_FEATURES",
+ 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)
+
+ # checking mkspec, predating gcc scope in qmake, will then be replaced by platform_mapping in helper.py
+ condition = condition.replace("*-g++*", "GCC")
+ condition = condition.replace("*g++*", "GCC")
+ condition = condition.replace("aix-g++*", "AIX")
+ condition = condition.replace("*-icc*", "ICC")
+ condition = condition.replace("*-clang*", "CLANG")
+ condition = condition.replace("*-llvm", "CLANG")
+ condition = condition.replace("win32-*", "WIN32")
+
+ 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, f"(CMAKE_BUILD_TYPE STREQUAL {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 ")
+
+ # new conditions added by the android multi arch qmake build
+ condition = re.sub(r"(^| )x86((?=[^\w])|$)", "TEST_architecture_arch STREQUAL i386", condition)
+ condition = re.sub(r"(^| )x86_64", " TEST_architecture_arch STREQUAL x86_64", condition)
+ condition = re.sub(r"(^| )arm64-v8a", "TEST_architecture_arch STREQUAL arm64", condition)
+ condition = re.sub(r"(^| )armeabi-v7a", "TEST_architecture_arch STREQUAL arm", condition)
+
+ # some defines replacements
+ condition = re.sub(r"DEFINES___contains___QT_NO_CURSOR", r"(NOT QT_FEATURE_cursor)", condition)
+ condition = re.sub(
+ r"DEFINES___contains___QT_NO_TRANSLATION", r"(NOT QT_FEATURE_translation)", condition
+ )
+ condition = re.sub(r"styles___contains___fusion", r"QT_FEATURE_style_fusion", condition)
+
+ condition = condition.replace("cross_compile", "CMAKE_CROSSCOMPILING")
+
+ 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 = f"TARGET {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()
+
+
+_path_replacements = {
+ "$$[QT_INSTALL_PREFIX]": "${INSTALL_DIRECTORY}",
+ "$$[QT_INSTALL_EXAMPLES]": "${INSTALL_EXAMPLESDIR}",
+ "$$[QT_INSTALL_TESTS]": "${INSTALL_TESTSDIR}",
+ "$$OUT_PWD": "${CMAKE_CURRENT_BINARY_DIR}",
+}
+
+
+def replace_path_constants(path: str, scope: Scope) -> str:
+ """ Clean up DESTDIR and target.path """
+ if path.startswith("./"):
+ path = f"${{CMAKE_CURRENT_BINARY_DIR}}/{path[2:]}"
+ elif path.startswith("../"):
+ path = f"${{CMAKE_CURRENT_BINARY_DIR}}/{path}"
+ for original, replacement in _path_replacements.items():
+ path = path.replace(original, replacement)
+ path = path.replace("$$TARGET", scope.TARGET)
+ return path
+
+
+def handle_subdir(
+ scope: Scope, cm_fh: IO[str], *, indent: int = 0, is_example: bool = False
+) -> None:
+
+ # Global nested dictionary that will contain sub_dir assignments and their conditions.
+ # Declared as a global in order not to pollute the nested function signatures with giant
+ # type hints.
+ sub_dirs: Dict[str, Dict[str, Set[FrozenSet[str]]]] = {}
+
+ # Collects assignment conditions into global sub_dirs dict.
+ def collect_subdir_info(sub_dir_assignment: str, *, current_conditions: FrozenSet[str] = None):
+ subtraction = sub_dir_assignment.startswith("-")
+ if subtraction:
+ subdir_name = sub_dir_assignment[1:]
+ else:
+ subdir_name = sub_dir_assignment
+ if subdir_name not in sub_dirs:
+ sub_dirs[subdir_name] = {}
+ additions = sub_dirs[subdir_name].get("additions", set())
+ subtractions = sub_dirs[subdir_name].get("subtractions", set())
+ if current_conditions:
+ if subtraction:
+ subtractions.add(current_conditions)
+ else:
+ additions.add(current_conditions)
+ if additions:
+ sub_dirs[subdir_name]["additions"] = additions
+ if subtractions:
+ sub_dirs[subdir_name]["subtractions"] = subtractions
+
+ # Recursive helper that collects subdir info for given scope,
+ # and the children of the given scope.
+ def handle_subdir_helper(
+ scope: Scope,
+ cm_fh: IO[str],
+ *,
+ indent: int = 0,
+ current_conditions: FrozenSet[str] = frozenset(),
+ is_example: bool = False,
+ ):
+ for sd in scope.get_files("SUBDIRS"):
+ # Collect info about conditions and SUBDIR assignments in the
+ # current scope.
+ if os.path.isdir(sd) or sd.startswith("-"):
+ collect_subdir_info(sd, current_conditions=current_conditions)
+ # For the file case, directly write into the file handle.
+ elif os.path.isfile(sd):
+ # Handle cases with SUBDIRS += Foo/bar/z.pro. We want to be able
+ # to generate add_subdirectory(Foo/bar) instead of parsing the full
+ # .pro file in the current CMakeLists.txt. This causes issues
+ # with relative paths in certain projects otherwise.
+ dirname = os.path.dirname(sd)
+ if dirname:
+ collect_subdir_info(dirname, current_conditions=current_conditions)
+ else:
+ subdir_result, project_file_content = parseProFile(sd, debug=False)
+ subdir_scope = Scope.FromDict(
+ scope,
+ sd,
+ subdir_result.asDict().get("statements"),
+ "",
+ scope.basedir,
+ project_file_content=project_file_content,
+ )
+
+ do_include(subdir_scope)
+ cmakeify_scope(subdir_scope, cm_fh, indent=indent, is_example=is_example)
+ else:
+ print(f" XXXX: SUBDIR {sd} in {scope}: Not found.")
+
+ # Collect info about conditions and SUBDIR assignments in child
+ # scopes, aka recursively call the same function, but with an
+ # updated current_conditions frozen set.
+ for c in scope.children:
+ # Use total_condition for 'else' conditions, otherwise just use the regular value to
+ # simplify the logic.
+ child_conditions = current_conditions
+ child_condition = c.total_condition if c.condition == "else" else c.condition
+ if child_condition:
+ child_conditions = frozenset((*child_conditions, child_condition))
+
+ handle_subdir_helper(
+ c,
+ cm_fh,
+ indent=indent + 1,
+ current_conditions=child_conditions,
+ is_example=is_example,
+ )
+
+ def group_and_print_sub_dirs(scope: Scope, indent: int = 0) -> None:
+ # Simplify conditions, and group
+ # subdirectories with the same conditions.
+ grouped_sub_dirs: Dict[str, List[str]] = {}
+
+ # Wraps each element in the given interable with parentheses,
+ # to make sure boolean simplification happens correctly.
+ def wrap_in_parenthesis(iterable):
+ return [f"({c})" for c in iterable]
+
+ def join_all_conditions(set_of_alternatives):
+ # Elements within one frozen set represent one single
+ # alternative whose pieces are ANDed together.
+ # This is repeated for each alternative that would
+ # enable a subdir, and are thus ORed together.
+ final_str = ""
+ if set_of_alternatives:
+ wrapped_set_of_alternatives = [
+ wrap_in_parenthesis(alternative) for alternative in set_of_alternatives
+ ]
+ alternatives = [
+ f'({" AND ".join(alternative)})' for alternative in wrapped_set_of_alternatives
+ ]
+ final_str = " OR ".join(sorted(alternatives))
+ return final_str
+
+ for subdir_name in sub_dirs:
+ additions = sub_dirs[subdir_name].get("additions", set())
+ subtractions = sub_dirs[subdir_name].get("subtractions", set())
+
+ # An empty condition key represents the group of sub dirs
+ # that should be added unconditionally.
+ condition_key = ""
+ if additions or subtractions:
+ addition_str = join_all_conditions(additions)
+ if addition_str:
+ addition_str = f"({addition_str})"
+ subtraction_str = join_all_conditions(subtractions)
+ if subtraction_str:
+ subtraction_str = f"NOT ({subtraction_str})"
+
+ condition_str = addition_str
+ if condition_str and subtraction_str:
+ condition_str += " AND "
+ condition_str += subtraction_str
+ if not condition_str.rstrip("()").strip():
+ continue
+ condition_simplified = simplify_condition(condition_str)
+ condition_key = condition_simplified
+
+ sub_dir_list_by_key: List[str] = grouped_sub_dirs.get(condition_key, [])
+ sub_dir_list_by_key.append(subdir_name)
+ grouped_sub_dirs[condition_key] = sub_dir_list_by_key
+
+ # Print any requires() blocks.
+ cm_fh.write(expand_project_requirements(scope, skip_message=True))
+
+ # Print the groups.
+ ind = spaces(indent)
+ for condition_key in grouped_sub_dirs:
+ cond_ind = ind
+ if condition_key:
+ cm_fh.write(f"{ind}if({condition_key})\n")
+ cond_ind += " "
+
+ sub_dir_list_by_key = grouped_sub_dirs.get(condition_key, [])
+ for subdir_name in sub_dir_list_by_key:
+ cm_fh.write(f"{cond_ind}add_subdirectory({subdir_name})\n")
+ if condition_key:
+ cm_fh.write(f"{ind}endif()\n")
+
+ # A set of conditions which will be ANDed together. The set is recreated with more conditions
+ # as the scope deepens.
+ current_conditions: FrozenSet[str] = frozenset()
+
+ # Compute the total condition for scopes. Needed for scopes that
+ # have 'else' as a condition.
+ recursive_evaluate_scope(scope)
+
+ # Do the work.
+ handle_subdir_helper(
+ scope, cm_fh, indent=indent, current_conditions=current_conditions, is_example=is_example
+ )
+ group_and_print_sub_dirs(scope, indent=indent)
+
+
+def sort_sources(sources: List[str]) -> List[str]:
+ to_sort = {} # type: Dict[str, List[str]]
+ for s in sources:
+ if s is None:
+ continue
+
+ path = os.path.dirname(s)
+ base = os.path.splitext(os.path.basename(s))[0]
+ if base.endswith("_p"):
+ base = base[:-2]
+ sort_name = posixpath.join(path, 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: List[str], known_libraries: Set[str], is_example: bool = False
+) -> List[str]:
+ result = [] # type: List[str]
+ is_framework = False
+
+ for lib in libraries:
+ if lib == "-framework":
+ is_framework = True
+ continue
+ if is_framework:
+ if is_example:
+ lib = f'"-framework {lib}"'
+ else:
+ lib = f"${{FW{lib}}}"
+ if lib.startswith("-l"):
+ lib = lib[2:]
+
+ if lib.startswith("-"):
+ lib = f"# Remove: {lib[1:]}"
+ else:
+ lib = map_3rd_party_library(lib)
+
+ if not lib or lib in result or lib in known_libraries:
+ continue
+
+ result.append(lib)
+ is_framework = False
+
+ return result
+
+
+def extract_cmake_libraries(
+ scope: Scope, *, known_libraries: Optional[Set[str]] = None, is_example: bool = False
+) -> Tuple[List[str], List[str]]:
+ if known_libraries is None:
+ known_libraries = set()
+ public_dependencies = [] # type: List[str]
+ private_dependencies = [] # type: List[str]
+
+ for key in ["QMAKE_USE", "LIBS"]:
+ public_dependencies += scope.expand(key)
+ for key in ["QMAKE_USE_PRIVATE", "QMAKE_USE_FOR_PRIVATE", "LIBS_PRIVATE"]:
+ private_dependencies += scope.expand(key)
+
+ for key in ["QT_FOR_PRIVATE", "QT_PRIVATE"]:
+ private_dependencies += [map_qt_library(q) for q in scope.expand(key)]
+
+ for key in ["QT"]:
+ # Qt public libs: These may include FooPrivate in which case we get
+ # a private dependency on FooPrivate as well as a public dependency on Foo
+ for lib in scope.expand(key):
+ mapped_lib = map_qt_library(lib)
+
+ if mapped_lib.endswith("Private"):
+ private_dependencies.append(mapped_lib)
+ public_dependencies.append(mapped_lib[:-7])
+ else:
+ public_dependencies.append(mapped_lib)
+
+ return (
+ _map_libraries_to_cmake(public_dependencies, known_libraries, is_example=is_example),
+ _map_libraries_to_cmake(private_dependencies, known_libraries, is_example=is_example),
+ )
+
+
+def write_header(cm_fh: IO[str], name: str, typename: str, *, indent: int = 0):
+ ind = spaces(indent)
+ comment_line = "#" * 69
+ cm_fh.write(f"{ind}{comment_line}\n")
+ cm_fh.write(f"{ind}## {name} {typename}:\n")
+ cm_fh.write(f"{ind}{comment_line}\n\n")
+
+
+def write_scope_header(cm_fh: IO[str], *, indent: int = 0):
+ ind = spaces(indent)
+ comment_line = "#" * 69
+ cm_fh.write(f"\n{ind}## Scopes:\n")
+ cm_fh.write(f"{ind}{comment_line}\n")
+
+
+def write_list(
+ cm_fh: IO[str],
+ entries: List[str],
+ cmake_parameter: str,
+ indent: int = 0,
+ *,
+ header: str = "",
+ footer: str = "",
+ prefix: str = "",
+):
+ if not entries:
+ return
+
+ ind = spaces(indent)
+ extra_indent = ""
+
+ if header:
+ cm_fh.write(f"{ind}{header}")
+ extra_indent += " "
+ if cmake_parameter:
+ cm_fh.write(f"{ind}{extra_indent}{cmake_parameter}\n")
+ extra_indent += " "
+ for s in sort_sources(entries):
+ cm_fh.write(f"{ind}{extra_indent}{prefix}{s}\n")
+ if footer:
+ cm_fh.write(f"{ind}{footer}\n")
+
+
+def write_source_file_list(
+ cm_fh: IO[str],
+ scope,
+ cmake_parameter: str,
+ keys: List[str],
+ indent: int = 0,
+ *,
+ header: str = "",
+ footer: str = "",
+):
+ # collect sources
+ sources: List[str] = []
+ for key in keys:
+ sources += scope.get_files(key, use_vpath=True)
+
+ # Remove duplicates, like in the case when NO_PCH_SOURCES ends up
+ # adding the file to SOURCES, but SOURCES might have already
+ # contained it before. Preserves order in Python 3.7+ because
+ # dict keys are ordered.
+ sources = list(dict.fromkeys(sources))
+
+ write_list(cm_fh, sources, cmake_parameter, indent, header=header, footer=footer)
+
+
+def write_all_source_file_lists(
+ cm_fh: IO[str],
+ scope: Scope,
+ header: str,
+ *,
+ indent: int = 0,
+ footer: str = "",
+ extra_keys: Optional[List[str]] = None,
+):
+ if extra_keys is None:
+ extra_keys = []
+ write_source_file_list(
+ cm_fh,
+ scope,
+ header,
+ ["SOURCES", "HEADERS", "OBJECTIVE_SOURCES", "OBJECTIVE_HEADERS", "NO_PCH_SOURCES", "FORMS"]
+ + extra_keys,
+ indent,
+ footer=footer,
+ )
+
+
+def write_defines(
+ cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = ""
+):
+ defines = scope.expand("DEFINES")
+ defines += [d[2:] for d in scope.expand("QMAKE_CXXFLAGS") if d.startswith("-D")]
+ defines = [
+ d.replace('=\\\\\\"$$PWD/\\\\\\"', '="${CMAKE_CURRENT_SOURCE_DIR}/"') for d in defines
+ ]
+
+ if "qml_debug" in scope.get("CONFIG"):
+ defines.append("QT_QML_DEBUG")
+
+ write_list(cm_fh, defines, cmake_parameter, indent, footer=footer)
+
+
+def write_include_paths(
+ cm_fh: 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: 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: IO[str], scope: Scope, *, indent: int = 0, known_libraries: Optional[Set[str]] = None
+):
+ if known_libraries is None:
+ known_libraries = 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: 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: IO[str], scope: Scope, *, indent: int = 0, known_libraries: Optional[Set[str]] = None
+):
+ if known_libraries is None:
+ 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:
+ dbus_adaptor_flags_line = '" "'.join(dbus_adaptor_flags)
+ cm_fh.write(f"{ind} DBUS_ADAPTOR_FLAGS\n")
+ cm_fh.write(f'{ind} "{dbus_adaptor_flags_line}"\n')
+
+ 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:
+ dbus_interface_flags_line = '" "'.join(dbus_interface_flags)
+ cm_fh.write(f"{ind} DBUS_INTERFACE_FLAGS\n")
+ cm_fh.write(f'{ind} "{dbus_interface_flags_line}"\n')
+
+ 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(f"{ind} LINK_OPTIONS\n")
+ for lo in link_options:
+ cm_fh.write(f'{ind} "{lo}"\n')
+
+ moc_options = scope.get("QMAKE_MOC_OPTIONS")
+ if moc_options:
+ cm_fh.write(f"{ind} MOC_OPTIONS\n")
+ for mo in moc_options:
+ cm_fh.write(f'{ind} "{mo}"\n')
+
+ precompiled_header = scope.get("PRECOMPILED_HEADER")
+ if precompiled_header:
+ cm_fh.write(f"{ind} PRECOMPILED_HEADER\n")
+ for header in precompiled_header:
+ cm_fh.write(f'{ind} "{header}"\n')
+
+ no_pch_sources = scope.get("NO_PCH_SOURCES")
+ if no_pch_sources:
+ cm_fh.write(f"{ind} NO_PCH_SOURCES\n")
+ for source in no_pch_sources:
+ cm_fh.write(f'{ind} "{source}"\n')
+
+
+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 in {
+ "_INCLUDED",
+ "_LOADED",
+ "TARGET",
+ "QMAKE_DOCS",
+ "QT_SOURCE_TREE",
+ "QT_BUILD_TREE",
+ "QTRO_SOURCE_TREE",
+ "TRACEPOINT_PROVIDER",
+ "PLUGIN_TYPE",
+ "PLUGIN_CLASS_NAME",
+ "CLASS_NAME",
+ "MODULE_PLUGIN_TYPES",
+ }:
+ # All these keys are actually reported already
+ continue
+ values = scope.get(k)
+ value_string = "<EMPTY>" if not values else '"' + '" "'.join(scope.get(k)) + '"'
+ result += f"{indent}# {k} = {value_string}\n"
+
+ if result:
+ result = f"\n#### Keys ignored in scope {scope}:\n{result}"
+
+ return result
+
+
+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, f"Else branch without previous condition in: {scope.file}"
+ total_condition = f"NOT ({previous_condition})"
+ if parent_condition:
+ if not total_condition:
+ total_condition = parent_condition
+ else:
+ total_condition = f"({parent_condition}) AND ({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: str = "") -> str:
+ condition = condition.replace("QTDIR_build", "QT_BUILDING_QT")
+ condition = re.sub(
+ r"\bQT_ARCH___equals___([a-zA-Z_0-9]*)",
+ r'(TEST_architecture_arch STREQUAL "\1")',
+ condition or "",
+ )
+ condition = re.sub(
+ r"\bQT_ARCH___contains___([a-zA-Z_0-9]*)",
+ r'(TEST_architecture_arch STREQUAL "\1")',
+ condition or "",
+ )
+ condition = condition.replace("QT___contains___opengl", "QT_FEATURE_opengl")
+ condition = condition.replace("QT___contains___widgets", "QT_FEATURE_widgets")
+ condition = condition.replace(
+ "DEFINES___contains___QT_NO_PRINTER", "(QT_FEATURE_printer EQUAL FALSE)"
+ )
+ return condition
+
+
+resource_file_expansion_counter = 0
+
+
+def expand_resource_glob(cm_fh: IO[str], expression: str) -> str:
+ global resource_file_expansion_counter
+ r = expression.replace('"', "")
+
+ cm_fh.write(
+ dedent(
+ f"""
+ file(GLOB resource_glob_{resource_file_expansion_counter} RELATIVE "${{CMAKE_CURRENT_SOURCE_DIR}}" "{r}")
+ foreach(file IN LISTS resource_glob_{resource_file_expansion_counter})
+ set_source_files_properties("${{CMAKE_CURRENT_SOURCE_DIR}}/${{file}}" PROPERTIES QT_RESOURCE_ALIAS "${{file}}")
+ endforeach()
+ """
+ )
+ )
+
+ expanded_var = f"${{resource_glob_{resource_file_expansion_counter}}}"
+ resource_file_expansion_counter += 1
+ return expanded_var
+
+
+def write_resources(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0, is_example=False):
+ # vpath = scope.expand('VPATH')
+
+ # Handle QRC files by turning them into qt_add_resource:
+ resources = scope.get_files("RESOURCES")
+ qtquickcompiler_skipped = scope.get_files("QTQUICK_COMPILER_SKIPPED_RESOURCES")
+ qtquickcompiler_retained = scope.get_files("QTQUICK_COMPILER_RETAINED_RESOURCES")
+ qrc_output = ""
+ if resources:
+ standalone_files: List[str] = []
+ for r in resources:
+ skip_qtquick_compiler = r in qtquickcompiler_skipped
+ retain_qtquick_compiler = r in qtquickcompiler_retained
+ if r.endswith(".qrc"):
+ if "${CMAKE_CURRENT_BINARY_DIR}" in r:
+ cm_fh.write(f"#### Ignored generated resource: {r}")
+ continue
+ qrc_output += process_qrc_file(
+ target,
+ r,
+ scope.basedir,
+ scope.file_absolute_path,
+ skip_qtquick_compiler,
+ retain_qtquick_compiler,
+ is_example,
+ )
+ else:
+ immediate_files = {f: "" for f in scope.get_files(f"{r}.files")}
+ if immediate_files:
+ immediate_files_filtered = []
+ for f in immediate_files:
+ if "*" in f:
+ immediate_files_filtered.append(expand_resource_glob(cm_fh, f))
+ else:
+ immediate_files_filtered.append(f)
+ immediate_files = {f: "" for f in immediate_files_filtered}
+ scope_prefix = scope.get(f"{r}.prefix")
+ if scope_prefix:
+ immediate_prefix = scope_prefix[0]
+ else:
+ immediate_prefix = "/"
+ immediate_base_list = scope.get(f"{r}.base")
+ assert (
+ len(immediate_base_list) < 2
+ ), f"immediate base directory must be at most one entry"
+ immediate_base = replace_path_constants("".join(immediate_base_list), scope)
+ immediate_lang = None
+ immediate_name = f"qmake_{r}"
+ qrc_output += write_add_qt_resource_call(
+ target=target,
+ resource_name=immediate_name,
+ prefix=immediate_prefix,
+ base_dir=immediate_base,
+ lang=immediate_lang,
+ files=immediate_files,
+ skip_qtquick_compiler=skip_qtquick_compiler,
+ retain_qtquick_compiler=retain_qtquick_compiler,
+ is_example=is_example,
+ )
+ else:
+ if "*" in r:
+ standalone_files.append(expand_resource_glob(cm_fh, r))
+ else:
+ # stadalone source file properties need to be set as they
+ # are parsed.
+ if skip_qtquick_compiler:
+ qrc_output += (
+ f'set_source_files_properties("{r}" PROPERTIES '
+ f"QT_SKIP_QUICKCOMPILER 1)\n\n"
+ )
+
+ if retain_qtquick_compiler:
+ qrc_output += (
+ f'set_source_files_properties("{r}" PROPERTIES '
+ f"QT_RETAIN_QUICKCOMPILER 1)\n\n"
+ )
+ standalone_files.append(r)
+
+ if standalone_files:
+ name = "qmake_immediate"
+ prefix = "/"
+ base = ""
+ lang = None
+ files = {f: "" for f in standalone_files}
+ skip_qtquick_compiler = False
+ qrc_output += write_add_qt_resource_call(
+ target=target,
+ resource_name=name,
+ prefix=prefix,
+ base_dir=base,
+ lang=lang,
+ files=files,
+ skip_qtquick_compiler=False,
+ retain_qtquick_compiler=False,
+ is_example=is_example,
+ )
+
+ if qrc_output:
+ str_indent = spaces(indent)
+ cm_fh.write(f"\n{str_indent}# Resources:\n")
+ for line in qrc_output.split("\n"):
+ if line:
+ cm_fh.write(f"{str_indent}{line}\n")
+ else:
+ # do not add spaces to empty lines
+ cm_fh.write("\n")
+
+
+def write_statecharts(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0, is_example=False):
+ sources = scope.get_files("STATECHARTS", use_vpath=True)
+ if not sources:
+ return
+ cm_fh.write("\n# Statecharts:\n")
+ if is_example:
+ cm_fh.write(f"qt6_add_statecharts({target}\n")
+ else:
+ cm_fh.write(f"add_qt_statecharts({target} FILES\n")
+ indent += 1
+ for f in sources:
+ cm_fh.write(f"{spaces(indent)}{f}\n")
+ cm_fh.write(")\n")
+
+
+def write_qlalrsources(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
+ sources = scope.get_files("QLALRSOURCES", use_vpath=True)
+ if not sources:
+ return
+ cm_fh.write("\n# QLALR Grammars:\n")
+ cm_fh.write(f"qt_process_qlalr(\n")
+ indent += 1
+ cm_fh.write(f"{spaces(indent)}{target}\n")
+ cm_fh.write(f"{spaces(indent)}{';'.join(sources)}\n")
+ cm_fh.write(f'{spaces(indent)}""\n')
+ cm_fh.write(")\n")
+
+
+def write_repc_files(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
+ for t in ["SOURCE", "REPLICA", "MERGED"]:
+ sources = scope.get_files("REPC_" + t, use_vpath=True)
+ if not sources:
+ continue
+ cm_fh.write(f"qt6_add_repc_{t.lower()}({target}\n")
+ indent += 1
+ for f in sources:
+ cm_fh.write(f"{spaces(indent)}{f}\n")
+ cm_fh.write(")\n")
+
+
+def expand_project_requirements(scope: Scope, skip_message: bool = False) -> str:
+ requirements = ""
+ for requirement in scope.get("_REQUIREMENTS"):
+ original_condition = simplify_condition(map_condition(requirement))
+ inverted_requirement = simplify_condition(f"NOT ({map_condition(requirement)})")
+ if not skip_message:
+ message = f"""
+{spaces(7)}message(NOTICE "Skipping the build as the condition \\"{original_condition}\\" is not met.")"""
+ else:
+ message = ""
+ requirements += dedent(
+ f"""\
+ if({inverted_requirement}){message}
+ return()
+ endif()
+"""
+ )
+ return requirements
+
+
+def write_extend_target(cm_fh: 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()
+
+ assert scope.total_condition, "Cannot write CONDITION when scope.condition is None"
+
+ condition = map_to_cmake_condition(scope.total_condition)
+
+ cmake_api_call = get_cmake_api_call("qt_extend_target")
+ extend_scope = (
+ f"\n{ind}{cmake_api_call}({target} CONDITION"
+ f" {condition}\n"
+ f"{extend_qt_string}{ind})\n"
+ )
+
+ if not extend_qt_string:
+ extend_scope = "" # Nothing to report, so don't!
+
+ cm_fh.write(extend_scope)
+
+ io_string = io.StringIO()
+ write_resources(io_string, target, scope, indent + 1)
+ resource_string = io_string.getvalue()
+ if len(resource_string) != 0:
+ resource_string = resource_string.strip("\n").rstrip(f"\n{spaces(indent + 1)}")
+ cm_fh.write(f"\n{spaces(indent)}if({condition})\n{resource_string}")
+ cm_fh.write(f"\n{spaces(indent)}endif()\n")
+
+
+def flatten_scopes(scope: Scope) -> List[Scope]:
+ result = [scope] # type: List[Scope]
+ for c in scope.children:
+ result += flatten_scopes(c)
+ return result
+
+
+def merge_scopes(scopes: List[Scope]) -> List[Scope]:
+ result = [] # type: List[Scope]
+
+ # Merge scopes with their parents:
+ known_scopes = {} # type: Dict[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: 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",
+ ]
+
+ simd_io_string = io.StringIO()
+
+ condition = "ON"
+ if scope.total_condition:
+ condition = map_to_cmake_condition(scope.total_condition)
+
+ if condition != "ON":
+ indent += 1
+
+ for simd in simd_options:
+ SIMD = simd.upper()
+ write_source_file_list(
+ simd_io_string,
+ scope,
+ "SOURCES",
+ [f"{SIMD}_HEADERS", f"{SIMD}_SOURCES", f"{SIMD}_C_SOURCES", f"{SIMD}_ASM"],
+ indent=indent,
+ header=f"{get_cmake_api_call('qt_add_simd_part')}({target} SIMD {simd}\n",
+ footer=")\n",
+ )
+
+ simd_string = simd_io_string.getvalue()
+ if simd_string:
+ simd_string = simd_string.rstrip("\n")
+ cond_start = ""
+ cond_end = ""
+ if condition != "ON":
+ cond_start = f"{spaces(indent - 1)}if({condition})"
+ cond_end = f"{spaces(indent - 1)}endif()"
+
+ extend_scope = f"\n{cond_start}\n" f"{simd_string}" f"\n{cond_end}\n"
+ cm_fh.write(extend_scope)
+
+
+def write_android_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
+ keys = [
+ "ANDROID_BUNDLED_JAR_DEPENDENCIES",
+ "ANDROID_LIB_DEPENDENCIES",
+ "ANDROID_JAR_DEPENDENCIES",
+ "ANDROID_LIB_DEPENDENCY_REPLACEMENTS",
+ "ANDROID_BUNDLED_FILES",
+ "ANDROID_PERMISSIONS",
+ "ANDROID_PACKAGE_SOURCE_DIR",
+ ]
+
+ has_no_values = True
+ for key in keys:
+ value = scope.expand(key)
+ if len(value) != 0:
+ if has_no_values:
+ if scope.condition:
+ cm_fh.write(f"\n{spaces(indent)}if(ANDROID AND ({scope.condition}))\n")
+ else:
+ cm_fh.write(f"\n{spaces(indent)}if(ANDROID)\n")
+ indent += 1
+ has_no_values = False
+ cm_fh.write(f"{spaces(indent)}set_property(TARGET {target} APPEND PROPERTY QT_{key}\n")
+ write_list(cm_fh, value, "", indent + 1)
+ cm_fh.write(f"{spaces(indent)})\n")
+ indent -= 1
+
+ if not has_no_values:
+ cm_fh.write(f"{spaces(indent)}endif()\n")
+
+
+def write_wayland_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
+ client_sources = scope.get_files("WAYLANDCLIENTSOURCES", use_vpath=True)
+ server_sources = scope.get_files("WAYLANDSERVERSOURCES", use_vpath=True)
+ if len(client_sources) == 0 and len(server_sources) == 0:
+ return
+
+ condition = "ON"
+ if scope.total_condition:
+ condition = map_to_cmake_condition(scope.total_condition)
+
+ if condition != "ON":
+ cm_fh.write(f"\n{spaces(indent)}if({condition})\n")
+ indent += 1
+
+ if len(client_sources) != 0:
+ cm_fh.write(f"\n{spaces(indent)}qt6_generate_wayland_protocol_client_sources({target}\n")
+ write_list(
+ cm_fh, client_sources, "FILES", indent + 1, prefix="${CMAKE_CURRENT_SOURCE_DIR}/"
+ )
+ cm_fh.write(f"{spaces(indent)})\n")
+
+ if len(server_sources) != 0:
+ cm_fh.write(f"\n{spaces(indent)}qt6_generate_wayland_protocol_server_sources({target}\n")
+ write_list(
+ cm_fh, server_sources, "FILES", indent + 1, prefix="${CMAKE_CURRENT_SOURCE_DIR}/"
+ )
+ cm_fh.write(f"{spaces(indent)})\n")
+
+ if condition != "ON":
+ indent -= 1
+ cm_fh.write(f"\n{spaces(indent)}endif()\n")
+
+
+def handle_source_subtractions(scopes: List[Scope]):
+ """
+ Handles source subtractions like SOURCES -= painting/qdrawhelper.cpp
+ by creating a new scope with a new condition containing all addition
+ and subtraction conditions.
+
+ Algorithm is as follows:
+ - Go through each scope and find files in SOURCES starting with "-"
+ - Save that file and the scope condition in modified_sources dict.
+ - Remove the file from the found scope (optionally remove the
+ NO_PCH_SOURCES entry for that file as well).
+ - Go through each file in modified_sources dict.
+ - Find scopes where the file is added, remove the file from that
+ scope and save the condition.
+ - Create a new scope just for that file with a new simplified
+ condition that takes all the other conditions into account.
+ """
+
+ def remove_file_from_operation(
+ scope: Scope, ops_key: str, file: str, op_type: Type[Operation]
+ ) -> bool:
+ """
+ Remove a source file from an operation in a scope.
+ Example: remove foo.cpp from any operations that have
+ ops_key="SOURCES" in "scope", where the operation is of
+ type "op_type".
+
+ The implementation is very rudimentary and might not work in
+ all cases.
+
+ Returns True if a file was found and removed in any operation.
+ """
+ file_removed = False
+ ops = scope._operations.get(ops_key, list())
+ for op in ops:
+ if not isinstance(op, op_type):
+ continue
+ if file in op._value:
+ op._value.remove(file)
+ file_removed = True
+ for include_child_scope in scope._included_children:
+ file_removed = file_removed or remove_file_from_operation(
+ include_child_scope, ops_key, file, op_type
+ )
+ return file_removed
+
+ def join_all_conditions(set_of_alternatives: Set[str]):
+ final_str = ""
+ if set_of_alternatives:
+ alternatives = [f"({alternative})" for alternative in set_of_alternatives]
+ final_str = " OR ".join(sorted(alternatives))
+ return final_str
+
+ modified_sources: Dict[str, Dict[str, Union[Set[str], bool]]] = {}
+
+ new_scopes = []
+ top_most_scope = scopes[0]
+
+ for scope in scopes:
+ sources = scope.get_files("SOURCES")
+ for file in sources:
+ # Find subtractions.
+ if file.startswith("-"):
+ file_without_minus = file[1:]
+
+ if file_without_minus not in modified_sources:
+ modified_sources[file_without_minus] = {}
+
+ subtractions = modified_sources[file_without_minus].get("subtractions", set())
+ assert isinstance(subtractions, set)
+
+ # Add the condition to the set of conditions and remove
+ # the file subtraction from the processed scope, which
+ # will be later re-added in a new scope.
+ if scope.condition:
+ assert scope.total_condition
+ subtractions.add(scope.total_condition)
+ remove_file_from_operation(scope, "SOURCES", file_without_minus, RemoveOperation)
+ if subtractions:
+ modified_sources[file_without_minus]["subtractions"] = subtractions
+
+ # In case if the source is also listed in a
+ # NO_PCH_SOURCES operation, remove it from there as
+ # well, and add it back later.
+ no_pch_source_removed = remove_file_from_operation(
+ scope, "NO_PCH_SOURCES", file_without_minus, AddOperation
+ )
+ if no_pch_source_removed:
+ modified_sources[file_without_minus]["add_to_no_pch_sources"] = True
+
+ for modified_source in modified_sources:
+ additions = modified_sources[modified_source].get("additions", set())
+ assert isinstance(additions, set), f"Additions must be a set, got {additions} instead."
+ subtractions = modified_sources[modified_source].get("subtractions", set())
+ assert isinstance(
+ subtractions, set
+ ), f"Subtractions must be a set, got {additions} instead."
+ add_to_no_pch_sources = modified_sources[modified_source].get(
+ "add_to_no_pch_sources", False
+ )
+
+ for scope in scopes:
+ sources = scope.get_files("SOURCES")
+ if modified_source in sources:
+ # Remove the source file from any addition operations
+ # that mention it.
+ remove_file_from_operation(scope, "SOURCES", modified_source, AddOperation)
+ if scope.total_condition:
+ additions.add(scope.total_condition)
+
+ # Construct a condition that takes into account all addition
+ # and subtraction conditions.
+ addition_str = join_all_conditions(additions)
+ if addition_str:
+ addition_str = f"({addition_str})"
+ subtraction_str = join_all_conditions(subtractions)
+ if subtraction_str:
+ subtraction_str = f"NOT ({subtraction_str})"
+
+ condition_str = addition_str
+ if condition_str and subtraction_str:
+ condition_str += " AND "
+ condition_str += subtraction_str
+ condition_simplified = simplify_condition(condition_str)
+
+ # Create a new scope with that condition and add the source
+ # operations.
+ new_scope = Scope(
+ parent_scope=top_most_scope,
+ qmake_file=top_most_scope.file,
+ condition=condition_simplified,
+ base_dir=top_most_scope.basedir,
+ )
+ new_scope.total_condition = condition_simplified
+ new_scope._append_operation("SOURCES", AddOperation([modified_source]))
+ if add_to_no_pch_sources:
+ new_scope._append_operation("NO_PCH_SOURCES", AddOperation([modified_source]))
+
+ new_scopes.append(new_scope)
+
+ # Add all the newly created scopes.
+ scopes += new_scopes
+
+
+def write_main_part(
+ cm_fh: IO[str],
+ name: str,
+ typename: str,
+ cmake_function: str,
+ scope: Scope,
+ *,
+ extra_lines: Optional[List[str]] = None,
+ indent: int = 0,
+ extra_keys: List[str],
+ **kwargs: Any,
+):
+ # Evaluate total condition of all scopes:
+ if extra_lines is None:
+ extra_lines = []
+ 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)
+
+ # Handle SOURCES -= foo calls, and merge scopes one more time
+ # because there might have been several files removed with the same
+ # scope condition.
+ handle_source_subtractions(scopes)
+ scopes = merge_scopes(scopes)
+
+ assert len(scopes)
+ assert scopes[0].total_condition == "ON"
+
+ scopes[0].reset_visited_keys()
+ for k in extra_keys:
+ scopes[0].get(k)
+
+ # Now write out the scopes:
+ write_header(cm_fh, name, typename, indent=indent)
+
+ # collect all testdata and insert globbing commands
+ has_test_data = False
+ if typename == "Test":
+ test_data = scope.expand("TESTDATA")
+ if test_data:
+ has_test_data = True
+ cm_fh.write("# Collect test data\n")
+ for data in test_data:
+ if "*" in data:
+ cm_fh.write(
+ dedent(
+ f"""\
+ {spaces(indent)}file(GLOB_RECURSE test_data_glob
+ {spaces(indent+1)}RELATIVE ${{CMAKE_CURRENT_SOURCE_DIR}}
+ {spaces(indent+1)}{data})
+ """
+ )
+ )
+ cm_fh.write(f"{spaces(indent)}list(APPEND test_data ${{test_data_glob}})\n")
+ else:
+ cm_fh.write(f'{spaces(indent)}list(APPEND test_data "{data}")\n')
+ cm_fh.write("\n")
+
+ # Check for DESTDIR override
+ destdir = scope.get_string("DESTDIR")
+ if destdir:
+ already_added = False
+ for line in extra_lines:
+ if line.startswith("OUTPUT_DIRECTORY"):
+ already_added = True
+ break
+ if not already_added:
+ destdir = replace_path_constants(destdir, scope)
+ extra_lines.append(f'OUTPUT_DIRECTORY "{destdir}"')
+
+ cm_fh.write(f"{spaces(indent)}{cmake_function}({name}\n")
+ for extra_line in extra_lines:
+ cm_fh.write(f"{spaces(indent)} {extra_line}\n")
+
+ write_sources_section(cm_fh, scopes[0], indent=indent, **kwargs)
+
+ if has_test_data:
+ cm_fh.write(f"{spaces(indent)} TESTDATA ${{test_data}}\n")
+ # Footer:
+ cm_fh.write(f"{spaces(indent)})\n")
+
+ write_resources(cm_fh, name, scope, indent)
+
+ write_statecharts(cm_fh, name, scope, indent)
+
+ write_qlalrsources(cm_fh, name, scope, indent)
+
+ write_repc_files(cm_fh, name, scope, indent)
+
+ write_simd_part(cm_fh, name, scope, indent)
+
+ write_android_part(cm_fh, name, scopes[0], indent)
+
+ write_wayland_part(cm_fh, name, scopes[0], indent)
+
+ ignored_keys_report = write_ignored_keys(scopes[0], spaces(indent))
+ if ignored_keys_report:
+ cm_fh.write(ignored_keys_report)
+
+ # Scopes:
+ if len(scopes) == 1:
+ return
+
+ write_scope_header(cm_fh, indent=indent)
+
+ for c in scopes[1:]:
+ c.reset_visited_keys()
+ write_android_part(cm_fh, name, c, indent=indent)
+ write_wayland_part(cm_fh, name, c, indent=indent)
+ write_extend_target(cm_fh, name, c, indent=indent)
+ write_simd_part(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_generic_library(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
+
+ target_name = scope.TARGET
+
+ library_type = ""
+
+ if "dll" in scope.get("CONFIG"):
+ library_type = "SHARED"
+
+ is_plugin = False
+ if "plugin" in scope.get("CONFIG"):
+ library_type = "MODULE"
+ is_plugin = True
+
+ # static after plugin in order to handle static library plugins
+ if "static" in scope.get("CONFIG"):
+ library_type = "STATIC"
+
+ extra_lines = []
+
+ if library_type:
+ extra_lines.append(library_type)
+
+ target_path = scope.expandString("target.path")
+ target_path = replace_path_constants(target_path, scope)
+ if target_path:
+ extra_lines.append(f'INSTALL_DIRECTORY "{target_path}"')
+
+ write_main_part(
+ cm_fh,
+ target_name,
+ "Generic Library",
+ get_cmake_api_call("qt_add_cmake_library"),
+ scope,
+ extra_lines=extra_lines,
+ indent=indent,
+ known_libraries={},
+ extra_keys=[],
+ )
+
+ if is_plugin:
+ # Plugins need to be able to run auto moc
+ cm_fh.write(f"\nqt_autogen_tools_initial_setup({target_name})\n")
+
+ if library_type == "STATIC":
+ cm_fh.write(f"\ntarget_compile_definitions({target_name} PRIVATE QT_STATICPLUGIN)\n")
+
+ return target_name
+
+
+def write_module(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
+ module_name = scope.TARGET
+ if not module_name.startswith("Qt"):
+ print(f"XXXXXX Module name {module_name} does not start with Qt!")
+
+ 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")
+ if "no_private_module" in scope.get("CONFIG"):
+ extra.append("NO_PRIVATE_MODULE")
+ if "header_module" in scope.get("CONFIG"):
+ extra.append("HEADER_MODULE")
+ if "metatypes" in scope.get("CONFIG") or "qmltypes" in scope.get("CONFIG"):
+ extra.append("GENERATE_METATYPES")
+
+ module_config = scope.get("MODULE_CONFIG")
+ if len(module_config):
+ extra.append(f'QMAKE_MODULE_CONFIG {" ".join(module_config)}')
+
+ module_plugin_types = scope.get_files("MODULE_PLUGIN_TYPES")
+ if module_plugin_types:
+ extra.append(f"PLUGIN_TYPES {' '.join(module_plugin_types)}")
+
+ target_name = module_name[2:]
+ write_main_part(
+ cm_fh,
+ target_name,
+ "Module",
+ f"{get_cmake_api_call('qt_add_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(
+ f"\n\n{spaces(indent)}qt_create_tracepoints({module_name[2:]} {' '.join(tracepoints)})\n"
+ )
+
+ return target_name
+
+
+def write_tool(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
+ tool_name = scope.TARGET
+
+ if "force_bootstrap" in scope.get("CONFIG"):
+ extra = ["BOOTSTRAP"]
+
+ # Remove default QT libs.
+ scope._append_operation("QT", RemoveOperation(["core", "gui"]))
+ else:
+ extra = []
+
+ write_main_part(
+ cm_fh,
+ tool_name,
+ "Tool",
+ get_cmake_api_call("qt_add_tool"),
+ scope,
+ indent=indent,
+ known_libraries={"Qt::Core"},
+ extra_lines=extra,
+ extra_keys=["CONFIG"],
+ )
+
+ return tool_name
+
+
+def write_test(cm_fh: IO[str], scope: Scope, gui: bool = False, *, indent: int = 0) -> str:
+ test_name = scope.TARGET
+ assert test_name
+
+ extra = ["GUI"] if gui else []
+ libraries = {"Qt::Core", "Qt::Test"}
+
+ if "qmltestcase" in scope.get("CONFIG"):
+ libraries.add("Qt::QmlTest")
+ extra.append("QMLTEST")
+ importpath = scope.expand("IMPORTPATH")
+ if importpath:
+ extra.append("QML_IMPORTPATH")
+ for path in importpath:
+ extra.append(f' "{path}"')
+
+ target_original = scope.TARGET_ORIGINAL
+ if target_original and target_original.startswith("../"):
+ extra.append('OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/../"')
+
+ requires_content = expand_project_requirements(scope, skip_message=True)
+ if requires_content:
+ requires_content += "\n"
+ cm_fh.write(requires_content)
+
+ write_main_part(
+ cm_fh,
+ test_name,
+ "Test",
+ get_cmake_api_call("qt_add_test"),
+ scope,
+ indent=indent,
+ known_libraries=libraries,
+ extra_lines=extra,
+ extra_keys=[],
+ )
+
+ return test_name
+
+
+def write_binary(cm_fh: IO[str], scope: Scope, gui: bool = False, *, indent: int = 0) -> str:
+ binary_name = scope.TARGET
+ assert binary_name
+
+ is_benchmark = is_benchmark_project(scope.file_absolute_path)
+ is_manual_test = is_manual_test_project(scope.file_absolute_path)
+
+ is_qt_test_helper = "qt_test_helper" in scope.get("_LOADED")
+
+ extra = ["GUI"] if gui and not is_qt_test_helper else []
+ cmake_function_call = get_cmake_api_call("qt_add_executable")
+ extra_keys: List[str] = []
+
+ if is_qt_test_helper:
+ binary_name += "_helper"
+ cmake_function_call = get_cmake_api_call("qt_add_test_helper")
+
+ if is_benchmark:
+ cmake_function_call = get_cmake_api_call("qt_add_benchmark")
+ elif is_manual_test:
+ cmake_function_call = get_cmake_api_call("qt_add_manual_test")
+ else:
+ extra_keys = ["target.path", "INSTALLS"]
+ target_path = scope.get_string("target.path")
+ if target_path:
+ target_path = replace_path_constants(target_path, scope)
+ if not scope.get("DESTDIR"):
+ extra.append(f'OUTPUT_DIRECTORY "{target_path}"')
+ if "target" in scope.get("INSTALLS"):
+ extra.append(f'INSTALL_DIRECTORY "{target_path}"')
+
+ write_main_part(
+ cm_fh,
+ binary_name,
+ "Binary",
+ cmake_function_call,
+ scope,
+ extra_lines=extra,
+ indent=indent,
+ known_libraries={"Qt::Core"},
+ extra_keys=extra_keys,
+ )
+
+ return binary_name
+
+
+def write_find_package_section(
+ cm_fh: IO[str], public_libs: List[str], private_libs: List[str], *, indent: int = 0
+):
+ packages = [] # type: 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_jar(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
+
+ target = scope.TARGET
+
+ install_dir = scope.expandString("target.path")
+ if not install_dir:
+ raise RuntimeError("Could not locate jar install path")
+ install_dir = install_dir.replace("$$[QT_INSTALL_PREFIX]/", "")
+
+ android_sdk_jar = "${QT_ANDROID_JAR}"
+ android_api_level = scope.get_string("API_VERSION")
+ if android_api_level:
+ cm_fh.write(
+ f'{spaces(indent)}qt_get_android_sdk_jar_for_api("{android_api_level}" android_sdk)\n\n'
+ )
+ android_sdk_jar = "${android_sdk}"
+
+ write_source_file_list(
+ cm_fh, scope, "", ["JAVASOURCES"], indent=indent, header=f"set(java_sources\n", footer=")\n"
+ )
+
+ cm_fh.write(f"{spaces(indent)}add_jar({target}\n")
+ cm_fh.write(f"{spaces(indent+1)}INCLUDE_JARS {android_sdk_jar}\n")
+ cm_fh.write(f"{spaces(indent+1)}SOURCES ${{java_sources}}\n")
+ cm_fh.write(f"{spaces(indent)})\n\n")
+
+ cm_fh.write(f"{spaces(indent)}install_jar({target}\n")
+ cm_fh.write(f"{spaces(indent+1)}DESTINATION {install_dir}\n")
+ cm_fh.write(f"{spaces(indent+1)}COMPONENT Devel\n")
+ cm_fh.write(f"{spaces(indent)})\n\n")
+
+ return target
+
+
+def write_example(
+ cm_fh: IO[str], scope: Scope, gui: bool = False, *, indent: int = 0, is_plugin: bool = False
+) -> str:
+ binary_name = scope.TARGET
+ assert binary_name
+
+ example_install_dir = scope.expandString("target.path")
+ if not example_install_dir:
+ example_install_dir = "examples"
+ example_install_dir = example_install_dir.replace("$$[QT_INSTALL_EXAMPLES]", "examples")
+
+ cm_fh.write(
+ "cmake_minimum_required(VERSION 3.14)\n"
+ f"project({binary_name} LANGUAGES CXX)\n\n"
+ "set(CMAKE_INCLUDE_CURRENT_DIR ON)\n\n"
+ "set(CMAKE_AUTOMOC ON)\n"
+ "set(CMAKE_AUTORCC ON)\n"
+ "set(CMAKE_AUTOUIC ON)\n\n"
+ f'set(INSTALL_EXAMPLEDIR "{example_install_dir}")\n\n'
+ )
+
+ recursive_evaluate_scope(scope)
+
+ # Get a flat list of all scopes but the main one:
+ scopes = flatten_scopes(scope)
+ # Merge scopes based on their conditions:
+ scopes = merge_scopes(scopes)
+ # Handle SOURCES -= foo calls, and merge scopes one more time
+ # because there might have been several files removed with the same
+ # scope condition.
+ handle_source_subtractions(scopes)
+ scopes = merge_scopes(scopes)
+
+ (public_libs, private_libs) = extract_cmake_libraries(scope, is_example=True)
+ write_find_package_section(cm_fh, public_libs, private_libs, indent=indent)
+
+ add_target = ""
+
+ qmldir = None
+ if is_plugin:
+ if "qml" in scope.get("QT"):
+ # Get the uri from the destination directory
+ dest_dir = scope.expandString("DESTDIR")
+ if not dest_dir:
+ dest_dir = "${CMAKE_CURRENT_BINARY_DIR}"
+ else:
+ uri = os.path.basename(dest_dir)
+ dest_dir = f"${{CMAKE_CURRENT_BINARY_DIR}}/{dest_dir}"
+
+ add_target = ""
+
+ qml_dir = None
+ qml_dir_dynamic_imports = False
+
+ qmldir_file_path_list = scope.get_files("qmldir.files")
+ assert len(qmldir_file_path_list) < 2, "File path must only contain one path"
+ qmldir_file_path = qmldir_file_path_list[0] if qmldir_file_path_list else "qmldir"
+ qmldir_file_path = os.path.join(os.getcwd(), qmldir_file_path[0])
+
+ dynamic_qmldir = scope.get("DYNAMIC_QMLDIR")
+ if os.path.exists(qmldir_file_path):
+ qml_dir = QmlDir()
+ qml_dir.from_file(qmldir_file_path)
+ elif dynamic_qmldir:
+ qml_dir = QmlDir()
+ qml_dir.from_lines(dynamic_qmldir)
+ qml_dir_dynamic_imports = True
+
+ add_target += "set(module_dynamic_qml_imports\n "
+ if len(qml_dir.imports) != 0:
+ add_target += "\n ".join(qml_dir.imports)
+ add_target += "\n)\n\n"
+
+ for sc in scopes[1:]:
+ import_list = []
+ qml_imports = sc.get("DYNAMIC_QMLDIR")
+ for qml_import in qml_imports:
+ if not qml_import.startswith("import "):
+ raise RuntimeError(
+ "Only qmldir import statements expected in conditional scope!"
+ )
+ import_list.append(qml_import[len("import ") :])
+ if len(import_list) == 0:
+ continue
+
+ assert sc.condition
+
+ add_target += f"if ({sc.condition})\n"
+ add_target += f" list(APPEND module_dynamic_qml_imports\n "
+ add_target += "\n ".join(import_list)
+ add_target += f"\n )\nendif()\n\n"
+
+ add_target += dedent(
+ f"""\
+ qt6_add_qml_module({binary_name}
+ OUTPUT_DIRECTORY "{dest_dir}"
+ VERSION 1.0
+ URI "{uri}"
+ """
+ )
+
+ if qml_dir is not None:
+ if qml_dir.designer_supported:
+ add_target += " DESIGNER_SUPPORTED\n"
+ if len(qml_dir.classname) != 0:
+ add_target += f" CLASSNAME {qml_dir.classname}\n"
+ if len(qml_dir.depends) != 0:
+ add_target += " DEPENDENCIES\n"
+ for dep in qml_dir.depends:
+ add_target += f" {dep[0]}/{dep[1]}\n"
+ if len(qml_dir.type_names) == 0:
+ add_target += " SKIP_TYPE_REGISTRATION\n"
+ if len(qml_dir.imports) != 0 and not qml_dir_dynamic_imports:
+ qml_dir_imports_line = " \n".join(qml_dir.imports)
+ add_target += f" IMPORTS\n{qml_dir_imports_line}"
+ if qml_dir_dynamic_imports:
+ add_target += " IMPORTS ${module_dynamic_qml_imports}\n"
+
+ add_target += " INSTALL_LOCATION ${INSTALL_EXAMPLEDIR}\n)\n\n"
+ add_target += f"target_sources({binary_name} PRIVATE"
+ else:
+ add_target = f"add_library({binary_name} MODULE"
+
+ else:
+ add_target = f'add_{"qt_gui_" if gui else ""}executable({binary_name}'
+
+ write_all_source_file_lists(cm_fh, scope, add_target, indent=0)
+ cm_fh.write(")\n")
+
+ handling_first_scope = True
+
+ for scope in scopes:
+ # write wayland already has condition scope handling
+ write_wayland_part(cm_fh, binary_name, scope, indent=0)
+
+ # The following options do not
+ io_string = io.StringIO()
+ condition_str = ""
+ condition = "ON"
+ if scope.total_condition:
+ condition = map_to_cmake_condition(scope.total_condition)
+
+ if condition != "ON":
+ condition_str = f"\n{spaces(indent)}if({condition})\n"
+ indent += 1
+
+ if not handling_first_scope:
+ target_sources = f"target_sources({binary_name} PUBLIC"
+ write_all_source_file_lists(
+ io_string, scope, target_sources, indent=indent, footer=")\n"
+ )
+
+ write_include_paths(
+ io_string,
+ scope,
+ f"target_include_directories({binary_name} PUBLIC",
+ indent=indent,
+ footer=")\n",
+ )
+ write_defines(
+ io_string,
+ scope,
+ f"target_compile_definitions({binary_name} PUBLIC",
+ indent=indent,
+ footer=")\n",
+ )
+
+ (scope_public_libs, scope_private_libs) = extract_cmake_libraries(scope, is_example=True)
+
+ write_list(
+ io_string,
+ scope_private_libs,
+ "",
+ indent=indent,
+ header=f"target_link_libraries({binary_name} PRIVATE\n",
+ footer=")\n",
+ )
+ write_list(
+ io_string,
+ scope_public_libs,
+ "",
+ indent=indent,
+ header=f"target_link_libraries({binary_name} PUBLIC\n",
+ footer=")\n",
+ )
+ write_compile_options(
+ io_string, scope, f"target_compile_options({binary_name}", indent=indent, footer=")\n"
+ )
+
+ write_resources(io_string, binary_name, scope, indent=indent, is_example=True)
+ write_statecharts(io_string, binary_name, scope, indent=indent, is_example=True)
+ write_repc_files(io_string, binary_name, scope, indent=indent)
+
+ if condition != "ON":
+ indent -= 1
+ string = io_string.getvalue()
+ if len(string) != 0:
+ string = string.rstrip("\n")
+ cm_fh.write(f"{condition_str}{string}\n")
+ if condition != "ON":
+ cm_fh.write(f"{spaces(indent)}endif()\n")
+
+ handling_first_scope = False
+
+ if qmldir:
+ write_qml_plugin_epilogue(cm_fh, binary_name, scope, qmldir, indent)
+
+ cm_fh.write(
+ f"\ninstall(TARGETS {binary_name}\n"
+ f' RUNTIME DESTINATION "${{INSTALL_EXAMPLEDIR}}"\n'
+ f' BUNDLE DESTINATION "${{INSTALL_EXAMPLEDIR}}"\n'
+ f' LIBRARY DESTINATION "${{INSTALL_EXAMPLEDIR}}"\n'
+ f")\n"
+ )
+
+ return binary_name
+
+
+def write_plugin(cm_fh, scope, *, indent: int = 0) -> str:
+ extra = []
+ is_qml_plugin = any("qml_plugin" == s for s in scope.get("_LOADED"))
+ qmake_target_name = scope.TARGET
+
+ # Forward the original Qt5 plugin target name, to correctly name the
+ # final library file name, and also for .prl generation.
+ if qmake_target_name and not is_qml_plugin:
+ extra.append(f"OUTPUT_NAME {qmake_target_name}")
+
+ # In Qt 6 CMake, the CMake target name for a plugin should be the
+ # same as it is in Qt5. qmake in Qt 5 derived the CMake target name
+ # from the "plugin class name", so use that.
+ # If the class name isn't empty, use that as the target name.
+ # Otherwise use the of value qmake TARGET
+ plugin_class_name = scope.get_string("PLUGIN_CLASS_NAME")
+ if plugin_class_name:
+ plugin_name = plugin_class_name
+ else:
+ plugin_name = qmake_target_name
+ assert plugin_name
+
+ # If the target name is derived from the class name, no need to
+ # forward the class name.
+ if plugin_class_name and plugin_class_name != plugin_name:
+ extra.append(f"CLASS_NAME {plugin_class_name}")
+
+ qmldir = None
+ plugin_type = scope.get_string("PLUGIN_TYPE")
+ plugin_function_name = get_cmake_api_call("qt_add_plugin")
+ if plugin_type:
+ extra.append(f"TYPE {plugin_type}")
+ elif is_qml_plugin:
+ plugin_function_name = get_cmake_api_call("qt_add_qml_module")
+ qmldir = write_qml_plugin(cm_fh, plugin_name, scope, indent=indent, extra_lines=extra)
+ else:
+ target_path = scope.expandString("target.path")
+ target_path = replace_path_constants(target_path, scope)
+ if target_path:
+ extra.append(f'INSTALL_DIRECTORY "{target_path}"')
+ else:
+ extra.append("SKIP_INSTALL")
+ if "qmltypes" in scope.get("CONFIG"):
+ extra.append("GENERATE_QMLTYPES")
+
+ if "static" in scope.get("CONFIG"):
+ extra.append("STATIC")
+
+ write_main_part(
+ cm_fh,
+ plugin_name,
+ "Plugin",
+ plugin_function_name,
+ scope,
+ indent=indent,
+ extra_lines=extra,
+ known_libraries={},
+ extra_keys=[],
+ )
+
+ if qmldir:
+ write_qml_plugin_epilogue(cm_fh, plugin_name, scope, qmldir, indent)
+
+ return plugin_name
+
+
+def write_qml_plugin(
+ cm_fh: IO[str],
+ target: str,
+ scope: Scope,
+ *,
+ extra_lines: Optional[List[str]] = None,
+ indent: int = 0,
+ **kwargs: Any,
+) -> Optional[QmlDir]:
+ # Collect other args if available
+ if extra_lines is None:
+ extra_lines = []
+ indent += 2
+
+ target_path = scope.get_string("TARGETPATH")
+ if target_path:
+ uri = target_path.replace("/", ".")
+ import_name = scope.get_string("IMPORT_NAME")
+ # Catch special cases such as foo.QtQuick.2.bar, which when converted
+ # into a target path via cmake will result in foo/QtQuick/2/bar, which is
+ # not what we want. So we supply the target path override.
+ target_path_from_uri = uri.replace(".", "/")
+ if target_path != target_path_from_uri:
+ extra_lines.append(f'TARGET_PATH "{target_path}"')
+ if import_name:
+ extra_lines.append(f'URI "{import_name}"')
+ else:
+ uri = re.sub("\\.\\d+", "", uri)
+ extra_lines.append(f'URI "{uri}"')
+
+ import_version = scope.get_string("IMPORT_VERSION")
+ if import_version:
+ import_version = import_version.replace(
+ "$$QT_MINOR_VERSION", "${CMAKE_PROJECT_VERSION_MINOR}"
+ )
+ extra_lines.append(f'VERSION "{import_version}"')
+
+ plugindump_dep = scope.get_string("QML_PLUGINDUMP_DEPENDENCIES")
+
+ if plugindump_dep:
+ extra_lines.append(f'QML_PLUGINDUMP_DEPENDENCIES "{plugindump_dep}"')
+
+ qml_dir = None
+ qmldir_file_path = os.path.join(os.getcwd(), "qmldir")
+ qml_dir_dynamic_imports = False
+ if os.path.exists(qmldir_file_path):
+ qml_dir = QmlDir()
+ qml_dir.from_file(qmldir_file_path)
+ else:
+ dynamic_qmldir = scope.get("DYNAMIC_QMLDIR")
+ if not dynamic_qmldir:
+ return None
+ qml_dir = QmlDir()
+ qml_dir.from_lines(dynamic_qmldir)
+ qml_dir_dynamic_imports = True
+
+ # Check scopes for conditional entries
+ scopes = flatten_scopes(scope)
+ cm_fh.write("set(module_dynamic_qml_imports\n ")
+ if len(qml_dir.imports) != 0:
+ cm_fh.write("\n ".join(qml_dir.imports))
+ cm_fh.write("\n)\n\n")
+
+ for sc in scopes[1:]:
+ import_list = []
+ qml_imports = sc.get("DYNAMIC_QMLDIR")
+ for qml_import in qml_imports:
+ if not qml_import.startswith("import "):
+ raise RuntimeError(
+ "Only qmldir import statements expected in conditional scope!"
+ )
+ import_list.append(qml_import[len("import ") :])
+ if len(import_list) == 0:
+ continue
+
+ assert sc.condition
+
+ cm_fh.write(f"if ({sc.condition})\n")
+ cm_fh.write(f" list(APPEND module_dynamic_qml_imports\n ")
+ cm_fh.write("\n ".join(import_list))
+ cm_fh.write(f"\n )\nendif()\n\n")
+
+ if qml_dir is not None:
+ if qml_dir.designer_supported:
+ extra_lines.append("DESIGNER_SUPPORTED")
+ if len(qml_dir.classname) != 0:
+ extra_lines.append(f"CLASSNAME {qml_dir.classname}")
+ if len(qml_dir.depends) != 0:
+ extra_lines.append("DEPENDENCIES")
+ for dep in qml_dir.depends:
+ extra_lines.append(f" {dep[0]}/{dep[1]}")
+ if len(qml_dir.type_names) == 0:
+ extra_lines.append("SKIP_TYPE_REGISTRATION")
+ if len(qml_dir.imports) != 0 and not qml_dir_dynamic_imports:
+ qml_dir_imports_line = "\n ".join(qml_dir.imports)
+ extra_lines.append("IMPORTS\n " f"{qml_dir_imports_line}")
+ if qml_dir_dynamic_imports:
+ extra_lines.append("IMPORTS ${module_dynamic_qml_imports}")
+
+ return qml_dir
+
+
+def write_qml_plugin_epilogue(
+ cm_fh: IO[str], target: str, scope: Scope, qmldir: QmlDir, indent: int = 0
+):
+
+ qml_files = scope.get_files("QML_FILES", use_vpath=True)
+ if qml_files:
+
+ indent_0 = spaces(indent)
+ indent_1 = spaces(indent + 1)
+ # Quote file paths in case there are spaces.
+ qml_files_quoted = [f'"{qf}"' for qf in qml_files]
+
+ indented_qml_files = f"\n{indent_1}".join(qml_files_quoted)
+ cm_fh.write(f"\n{indent_0}set(qml_files\n{indent_1}" f"{indented_qml_files}\n)\n")
+
+ for qml_file in qml_files:
+ if qml_file in qmldir.type_names:
+ qmldir_file_info = qmldir.type_names[qml_file]
+ cm_fh.write(f"{indent_0}set_source_files_properties({qml_file} PROPERTIES\n")
+ cm_fh.write(f'{indent_1}QT_QML_SOURCE_VERSION "{qmldir_file_info.version}"\n')
+ # Only write typename if they are different, CMake will infer
+ # the name by default
+ if (
+ os.path.splitext(os.path.basename(qmldir_file_info.path))[0]
+ != qmldir_file_info.type_name
+ ):
+ cm_fh.write(f"{indent_1}QT_QML_SOURCE_TYPENAME {qmldir_file_info.type_name}\n")
+ if qmldir_file_info.singleton:
+ cm_fh.write(f"{indent_1}QT_QML_SINGLETON_TYPE TRUE\n")
+ if qmldir_file_info.internal:
+ cm_fh.write(f"{indent_1}QT_QML_INTERNAL_TYPE TRUE\n")
+ cm_fh.write(f"{indent_0})\n")
+
+ cm_fh.write(
+ f"\n{indent_0}qt6_target_qml_files({target}\n{indent_1}FILES\n"
+ f"{spaces(indent+2)}${{qml_files}}\n)\n"
+ )
+
+
+def handle_app_or_lib(
+ scope: Scope, cm_fh: IO[str], *, indent: int = 0, is_example: bool = False
+) -> None:
+ assert scope.TEMPLATE in ("app", "lib")
+
+ config = scope.get("CONFIG")
+ is_jar = "java" in config
+ is_lib = scope.TEMPLATE == "lib"
+ is_qml_plugin = any("qml_plugin" == s for s in scope.get("_LOADED"))
+ is_plugin = "plugin" in config
+ is_qt_plugin = any("qt_plugin" == s for s in scope.get("_LOADED")) or is_qml_plugin
+ target = ""
+ gui = all(
+ val not in config for val in ["console", "cmdline", "-app_bundle"]
+ ) and "testlib" not in scope.expand("QT")
+
+ if is_jar:
+ write_jar(cm_fh, scope, indent=indent)
+ elif is_example:
+ target = write_example(cm_fh, scope, gui, indent=indent, is_plugin=is_plugin)
+ elif is_qt_plugin:
+ assert not is_example
+ target = write_plugin(cm_fh, scope, indent=indent)
+ elif (is_lib and "qt_module" not in scope.get("_LOADED")) or is_plugin:
+ assert not is_example
+ target = write_generic_library(cm_fh, scope, indent=indent)
+ elif is_lib or "qt_module" in scope.get("_LOADED"):
+ assert not is_example
+ target = write_module(cm_fh, scope, indent=indent)
+ elif "qt_tool" in scope.get("_LOADED"):
+ assert not is_example
+ target = write_tool(cm_fh, scope, indent=indent)
+ else:
+ if "testcase" in config or "testlib" in config or "qmltestcase" in config:
+ assert not is_example
+ target = write_test(cm_fh, scope, gui, indent=indent)
+ else:
+ target = write_binary(cm_fh, scope, gui, indent=indent)
+
+ # ind = spaces(indent)
+ cmake_api_call = get_cmake_api_call("qt_add_docs")
+ write_source_file_list(
+ cm_fh,
+ scope,
+ "",
+ ["QMAKE_DOCS"],
+ indent,
+ header=f"{cmake_api_call}({target}\n",
+ footer=")\n",
+ )
+
+ # Generate qmltypes instruction for anything that may have CONFIG += qmltypes
+ # that is not a qml plugin
+ if "qmltypes" in scope.get("CONFIG") and "qml_plugin" not in scope.get("_LOADED"):
+ cm_fh.write(f"\n{spaces(indent)}set_target_properties({target} PROPERTIES\n")
+ cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_INSTALL_QMLTYPES TRUE\n")
+
+ import_version = scope.get_string("IMPORT_VERSION")
+ if not import_version:
+ import_version = scope.get_string("QML_IMPORT_VERSION")
+ if not import_version:
+ import_major_version = scope.get_string("QML_IMPORT_MAJOR_VERSION")
+ import_minor_version = scope.get_string("QML_IMPORT_MINOR_VERSION")
+
+ if not import_major_version and not import_minor_version:
+ raise RuntimeError(f"No QML_IMPORT_VERSION info found for target {target}.")
+
+ if not import_minor_version:
+ import_minor_version = str(0)
+ import_version = f"{import_major_version}.{import_minor_version}"
+
+ if import_version:
+ import_version = import_version.replace(
+ "$$QT_MINOR_VERSION", "${CMAKE_PROJECT_VERSION_MINOR}"
+ )
+ cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_VERSION {import_version}\n")
+
+ import_name = scope.expandString("QML_IMPORT_NAME")
+ if import_name:
+ cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_URI {import_name}\n")
+
+ target_path = scope.get("TARGETPATH")
+ if target_path:
+ cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_TARGET_PATH {target_path}\n")
+
+ install_dir = scope.expandString("QMLTYPES_INSTALL_DIR")
+ if install_dir:
+ install_dir = install_dir.replace("$$[QT_INSTALL_QML]", "${Qt6_DIR}/../../../qml")
+ cm_fh.write(f'{spaces(indent+1)}QT_QML_MODULE_INSTALL_DIR "{install_dir}"\n')
+
+ cm_fh.write(f"{spaces(indent)})\n\n")
+ cm_fh.write(f"qt6_qml_type_registration({target})\n")
+
+
+def handle_top_level_repo_project(scope: Scope, cm_fh: IO[str]):
+ # qtdeclarative
+ project_file_name = os.path.splitext(os.path.basename(scope.file_absolute_path))[0]
+
+ # declarative
+ file_name_without_qt_prefix = project_file_name[2:]
+
+ # Qt::Declarative
+ qt_lib = map_qt_library(file_name_without_qt_prefix)
+
+ # Found a mapping, adjust name.
+ if qt_lib != file_name_without_qt_prefix:
+ # QtDeclarative
+ qt_lib = re.sub(r":", r"", qt_lib)
+
+ # Declarative
+ qt_lib_no_prefix = qt_lib[2:]
+ else:
+ qt_lib += "_FIXME"
+ qt_lib_no_prefix = qt_lib
+
+ header = dedent(
+ f"""\
+ cmake_minimum_required(VERSION {cmake_version_string})
+
+ project({qt_lib}
+ VERSION 6.0.0
+ DESCRIPTION "Qt {qt_lib_no_prefix} Libraries"
+ HOMEPAGE_URL "https://qt.io/"
+ LANGUAGES CXX C
+ )
+
+ find_package(Qt6 ${{PROJECT_VERSION}} CONFIG REQUIRED COMPONENTS BuildInternals Core SET_ME_TO_SOMETHING_USEFUL)
+ find_package(Qt6 ${{PROJECT_VERSION}} CONFIG OPTIONAL_COMPONENTS SET_ME_TO_SOMETHING_USEFUL)
+
+ """
+ )
+
+ build_repo = dedent(
+ f"""\
+ qt_build_repo()
+ """
+ )
+
+ cm_fh.write(f"{header}{expand_project_requirements(scope)}{build_repo}")
+
+
+def find_top_level_repo_project_file(project_file_path: str = "") -> Optional[str]:
+ qmake_conf_path = find_qmake_conf(project_file_path)
+ qmake_dir = os.path.dirname(qmake_conf_path)
+
+ # Hope to a programming god that there's only one .pro file at the
+ # top level directory of repository.
+ glob_result = glob.glob(os.path.join(qmake_dir, "*.pro"))
+ if len(glob_result) > 0:
+ return glob_result[0]
+ return None
+
+
+def handle_top_level_repo_tests_project(scope: Scope, cm_fh: IO[str]):
+ top_level_project_path = find_top_level_repo_project_file(scope.file_absolute_path)
+ if top_level_project_path:
+ # qtdeclarative
+ file_name = os.path.splitext(os.path.basename(top_level_project_path))[0]
+
+ # declarative
+ file_name_without_qt = file_name[2:]
+
+ # Qt::Declarative
+ qt_lib = map_qt_library(file_name_without_qt)
+
+ # Found a mapping, adjust name.
+ if qt_lib != file_name_without_qt:
+ # QtDeclarative
+ qt_lib = f'{re.sub(r":", r"", qt_lib)}{"Tests"}'
+ else:
+ qt_lib += "Tests_FIXME"
+ else:
+ qt_lib = "Tests_FIXME"
+
+ requires_content = expand_project_requirements(scope, skip_message=True)
+ if requires_content:
+ requires_content = f"\n\n{textwrap_indent(requires_content, spaces(3))}"
+
+ content = dedent(
+ f"""\
+ if(QT_BUILD_STANDALONE_TESTS)
+ # Add qt_find_package calls for extra dependencies that need to be found when building
+ # the standalone tests here.
+ endif()
+ qt_build_tests()
+"""
+ )
+
+ cm_fh.write(f"{content}")
+
+
+def write_regular_cmake_target_scope_section(
+ scope: Scope, cm_fh: IO[str], indent: int = 0, skip_sources: bool = False
+):
+ if not skip_sources:
+ target_sources = "target_sources(${PROJECT_NAME} PUBLIC"
+ write_all_source_file_lists(cm_fh, scope, target_sources, indent=indent, footer=")")
+
+ write_include_paths(
+ cm_fh,
+ scope,
+ f"target_include_directories(${{PROJECT_NAME}} PUBLIC",
+ indent=indent,
+ footer=")",
+ )
+ write_defines(
+ cm_fh,
+ scope,
+ f"target_compile_definitions(${{PROJECT_NAME}} PUBLIC",
+ indent=indent,
+ footer=")",
+ )
+ (public_libs, private_libs) = extract_cmake_libraries(scope)
+ write_list(
+ cm_fh,
+ private_libs,
+ "",
+ indent=indent,
+ header=f"target_link_libraries(${{PROJECT_NAME}} PRIVATE\n",
+ footer=")",
+ )
+ write_list(
+ cm_fh,
+ public_libs,
+ "",
+ indent=indent,
+ header=f"target_link_libraries(${{PROJECT_NAME}} PUBLIC\n",
+ footer=")",
+ )
+ write_compile_options(
+ cm_fh, scope, f"target_compile_options(${{PROJECT_NAME}}", indent=indent, footer=")"
+ )
+
+
+def handle_config_test_project(scope: Scope, cm_fh: IO[str]):
+ project_name = os.path.splitext(os.path.basename(scope.file_absolute_path))[0]
+ content = (
+ f"cmake_minimum_required(VERSION 3.14.0)\n"
+ f"project(config_test_{project_name} LANGUAGES CXX)\n"
+ )
+ cm_fh.write(f"{content}\n")
+
+ # Remove default QT libs.
+ scope._append_operation("QT", RemoveOperation(["core", "gui"]))
+
+ add_target = f"add_executable(${{PROJECT_NAME}}"
+
+ temp_buffer = io.StringIO()
+ write_all_source_file_lists(temp_buffer, scope, add_target, indent=0)
+ buffer_value = temp_buffer.getvalue()
+
+ if buffer_value:
+ cm_fh.write(buffer_value)
+ else:
+ cm_fh.write(add_target)
+ cm_fh.write(")\n")
+
+ indent = 0
+ write_regular_cmake_target_scope_section(scope, cm_fh, indent, skip_sources=True)
+
+ recursive_evaluate_scope(scope)
+ scopes = flatten_scopes(scope)
+ scopes = merge_scopes(scopes)
+
+ assert len(scopes)
+ assert scopes[0].total_condition == "ON"
+
+ for c in scopes[1:]:
+ extend_scope_io_string = io.StringIO()
+ write_regular_cmake_target_scope_section(c, extend_scope_io_string, indent=indent + 1)
+ extend_string = extend_scope_io_string.getvalue()
+
+ if extend_string:
+ assert c.total_condition, "Cannot write if with empty condition"
+ extend_scope = (
+ f"\nif({map_to_cmake_condition(c.total_condition)})\n"
+ f"{extend_string}"
+ f"endif()\n"
+ )
+ cm_fh.write(extend_scope)
+
+
+def cmakeify_scope(
+ scope: Scope, cm_fh: IO[str], *, indent: int = 0, is_example: bool = False
+) -> None:
+ template = scope.TEMPLATE
+
+ temp_buffer = io.StringIO()
+
+ # Handle top level repo project in a special way.
+ if is_top_level_repo_project(scope.file_absolute_path):
+ handle_top_level_repo_project(scope, temp_buffer)
+ # Same for top-level tests.
+ elif is_top_level_repo_tests_project(scope.file_absolute_path):
+ handle_top_level_repo_tests_project(scope, temp_buffer)
+ elif is_config_test_project(scope.file_absolute_path):
+ handle_config_test_project(scope, temp_buffer)
+ elif template == "subdirs":
+ handle_subdir(scope, temp_buffer, indent=indent, is_example=is_example)
+ elif template in ("app", "lib"):
+ handle_app_or_lib(scope, temp_buffer, indent=indent, is_example=is_example)
+ else:
+ print(f" XXXX: {scope.file}: Template type {template} not yet supported.")
+
+ buffer_value = temp_buffer.getvalue()
+
+ if is_top_level_repo_examples_project(scope.file_absolute_path):
+ # Wrap top level examples project with some commands which
+ # are necessary to build examples as part of the overall
+ # build.
+ buffer_value = f"qt_examples_build_begin()\n\n{buffer_value}\nqt_examples_build_end()\n"
+
+ cm_fh.write(buffer_value)
+
+
+def generate_new_cmakelists(scope: Scope, *, is_example: bool = False, debug: bool = False) -> None:
+ if debug:
+ print("Generating CMakeLists.gen.txt")
+ with open(scope.generated_cmake_lists_path, "w") as cm_fh:
+ assert scope.file
+ cm_fh.write(f"# Generated from {os.path.basename(scope.file)}.\n\n")
+
+ is_example_heuristic = is_example_project(scope.file_absolute_path)
+ final_is_example_decision = is_example or is_example_heuristic
+ cmakeify_scope(scope, cm_fh, is_example=final_is_example_decision)
+
+
+def do_include(scope: Scope, *, debug: bool = False) -> None:
+ for c in scope.children:
+ do_include(c)
+
+ for include_index, include_file in enumerate(scope.get_files("_INCLUDED", is_include=True)):
+ if not include_file:
+ continue
+ if not os.path.isfile(include_file):
+ generated_config_pri_pattern = re.compile(r"qt.+?-config\.pri$")
+ match_result = re.search(generated_config_pri_pattern, include_file)
+ if not match_result:
+ print(f" XXXX: Failed to include {include_file}.")
+ continue
+
+ include_op = scope._get_operation_at_index("_INCLUDED", include_index)
+ include_line_no = include_op._line_no
+
+ include_result, project_file_content = parseProFile(include_file, debug=debug)
+ include_scope = Scope.FromDict(
+ None,
+ include_file,
+ include_result.asDict().get("statements"),
+ "",
+ scope.basedir,
+ project_file_content=project_file_content,
+ parent_include_line_no=include_line_no,
+ ) # 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, debug: bool = False
+) -> None:
+ if debug:
+ print(f"Copying {scope.generated_cmake_lists_path} to {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 cmake_project_has_skip_marker(project_file_path: str = "") -> bool:
+ dir_path = os.path.dirname(project_file_path)
+ cmake_project_path = os.path.join(dir_path, "CMakeLists.txt")
+ if not os.path.exists(cmake_project_path):
+ return False
+
+ with open(cmake_project_path, "r") as file_fd:
+ contents = file_fd.read()
+
+ if "# special case skip regeneration" in contents:
+ return True
+
+ return False
+
+
+def should_convert_project(project_file_path: str = "", ignore_skip_marker: bool = False) -> bool:
+ qmake_conf_path = find_qmake_conf(project_file_path)
+ qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
+
+ project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
+
+ # Skip cmake auto tests, they should not be converted.
+ if project_relative_path.startswith("tests/auto/cmake"):
+ return False
+ if project_relative_path.startswith("tests/auto/installed_cmake"):
+ return False
+
+ # Skip qmake testdata projects.
+ if project_relative_path.startswith("tests/auto/tools/qmake/testdata"):
+ return False
+
+ # Skip certain config tests.
+ config_tests = [
+ # Relative to qtbase/config.tests
+ "arch/arch.pro",
+ "avx512/avx512.pro",
+ "stl/stl.pro",
+ "verifyspec/verifyspec.pro",
+ "x86_simd/x86_simd.pro",
+ # Relative to repo src dir
+ "config.tests/hostcompiler/hostcompiler.pro",
+ ]
+ skip_certain_tests = any(project_relative_path.startswith(c) for c in config_tests)
+ if skip_certain_tests:
+ return False
+
+ # Skip if CMakeLists.txt in the same path as project_file_path has a
+ # special skip marker.
+ if not ignore_skip_marker and cmake_project_has_skip_marker(project_file_path):
+ return False
+
+ return True
+
+
+def should_convert_project_after_parsing(
+ file_scope: Scope, skip_subdirs_project: bool = False
+) -> bool:
+ template = file_scope.TEMPLATE
+ if template == "subdirs" and skip_subdirs_project:
+ return False
+ return True
+
+
+def main() -> None:
+ # Be sure of proper Python version
+ assert sys.version_info >= (3, 7)
+
+ args = _parse_commandline()
+
+ debug_parsing = args.debug_parser or args.debug
+ if args.skip_condition_cache:
+ set_condition_simplified_cache_enabled(False)
+
+ 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)
+
+ project_file_absolute_path = os.path.abspath(file_relative_path)
+ if not should_convert_project(project_file_absolute_path, args.ignore_skip_marker):
+ print(f'Skipping conversion of project: "{project_file_absolute_path}"')
+ continue
+
+ parseresult, project_file_content = parseProFile(file_relative_path, debug=debug_parsing)
+
+ # If CMake api version is given on command line, that means the
+ # user wants to force use that api version.
+ global cmake_api_version
+ if args.api_version:
+ cmake_api_version = args.api_version
+ else:
+ # Otherwise detect the api version in the old CMakeLists.txt
+ # if it exsists.
+ detected_cmake_api_version = detect_cmake_api_version_used_in_file_content(
+ file_relative_path
+ )
+ if detected_cmake_api_version:
+ cmake_api_version = detected_cmake_api_version
+
+ 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"),
+ project_file_content=project_file_content,
+ )
+
+ 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")
+
+ if not should_convert_project_after_parsing(file_scope, args.skip_subdirs_project):
+ print(f'Skipping conversion of project: "{project_file_absolute_path}"')
+ continue
+
+ generate_new_cmakelists(file_scope, is_example=args.is_example, debug=args.debug)
+
+ 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..3c0c7e3070
--- /dev/null
+++ b/util/cmake/pro_conversion_rate.py
@@ -0,0 +1,235 @@
+#!/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 typing import Dict, Union
+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 # type: ignore
+
+ 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 # type: ignore
+
+
+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):
+ 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: Dict[str, Dict[str, Union[str, int, float]]] = {}
+ 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"):
+ assert isinstance(stats["missing examples"]["value"], int)
+ stats["missing examples"]["value"] += 1
+ elif rel_path.startswith("tests"):
+ assert isinstance(stats["missing tests"]["value"], int)
+ stats["missing tests"]["value"] += 1
+ elif rel_path.startswith(os.path.join("src", "plugins")):
+ assert isinstance(stats["missing plugins"]["value"], int)
+ stats["missing plugins"]["value"] += 1
+ elif rel_path.startswith("src"):
+ assert isinstance(stats["missing src"]["value"], int)
+ stats["missing src"]["value"] += 1
+
+ for stat in stats:
+ if int(stats[stat]["value"]) > 0:
+ stats[stat]["percentage"] = round(float(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(
+ f"{stats[stat]['label']:<40}: {stats[stat]['value']} ({stats[stat]['percentage']}%)"
+ )
+
+ print(f"\n{'Scan time':<40}: {scan_time:.10f} seconds")
+ print(f"{'Total script time':<40}: {script_time:.10f} seconds")
+
+
+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/qmake_parser.py b/util/cmake/qmake_parser.py
new file mode 100644
index 0000000000..5cb629a495
--- /dev/null
+++ b/util/cmake/qmake_parser.py
@@ -0,0 +1,388 @@
+#!/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 collections
+import os
+import re
+from itertools import chain
+from typing import Tuple
+
+import pyparsing as pp # type: ignore
+
+from helper import _set_up_py_parsing_nicer_debug_output
+
+_set_up_py_parsing_nicer_debug_output(pp)
+
+
+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 flatten_list(l):
+ """ Flattens an irregular nested list into a simple list."""
+ for el in l:
+ if isinstance(el, collections.abc.Iterable) and not isinstance(el, (str, bytes)):
+ yield from flatten_list(el)
+ else:
+ yield el
+
+
+def handle_function_value(group: pp.ParseResults):
+ function_name = group[0]
+ function_args = group[1]
+ if function_name == "qtLibraryTarget":
+ if len(function_args) > 1:
+ raise RuntimeError(
+ "Don't know what to with more than one function argument "
+ "for $$qtLibraryTarget()."
+ )
+ return str(function_args[0])
+
+ if function_name == "quote":
+ # Do nothing, just return a string result
+ return str(group)
+
+ if function_name == "files":
+ return str(function_args[0])
+
+ if function_name == "basename":
+ if len(function_args) != 1:
+ print(f"XXXX basename with more than one argument")
+ if function_args[0] == "_PRO_FILE_PWD_":
+ return os.path.basename(os.getcwd())
+ print(f"XXXX basename with value other than _PRO_FILE_PWD_")
+ return os.path.basename(str(function_args[0]))
+
+ if isinstance(function_args, pp.ParseResults):
+ function_args = list(flatten_list(function_args.asList()))
+
+ # For other functions, return the whole expression as a string.
+ return f"$${function_name}({' '.join(function_args)})"
+
+
+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(f"{pp.alphas}_", bodyChars=pp.alphanums + "_-./")
+ )
+ BracedValue = add_element(
+ "BracedValue",
+ pp.nestedExpr(
+ ignoreExpr=pp.quotedString
+ | pp.QuotedString(
+ quoteChar="$(", endQuoteChar=")", escQuote="\\", unquoteResults=False
+ )
+ ).setParseAction(lambda s, l, t: ["(", *t[0], ")"]),
+ )
+
+ Substitution = add_element(
+ "Substitution",
+ pp.Combine(
+ pp.Literal("$")
+ + (
+ (
+ (pp.Literal("$") + Identifier + pp.Optional(pp.nestedExpr()))
+ | (pp.Literal("(") + Identifier + pp.Literal(")"))
+ | (pp.Literal("{") + Identifier + pp.Literal("}"))
+ | (
+ pp.Literal("$")
+ + pp.Literal("{")
+ + Identifier
+ + pp.Optional(pp.nestedExpr())
+ + pp.Literal("}")
+ )
+ | (pp.Literal("$") + pp.Literal("[") + Identifier + pp.Literal("]"))
+ )
+ )
+ ),
+ )
+ LiteralValuePart = add_element(
+ "LiteralValuePart", pp.Word(pp.printables, excludeChars="$#{}()")
+ )
+ SubstitutionValue = add_element(
+ "SubstitutionValue",
+ pp.Combine(pp.OneOrMore(Substitution | LiteralValuePart | pp.Literal("$"))),
+ )
+ FunctionValue = add_element(
+ "FunctionValue",
+ pp.Group(
+ pp.Suppress(pp.Literal("$") + pp.Literal("$"))
+ + Identifier
+ + pp.nestedExpr() # .setParseAction(lambda s, l, t: ['(', *t[0], ')'])
+ ).setParseAction(lambda s, l, t: handle_function_value(*t)),
+ )
+ Value = add_element(
+ "Value",
+ pp.NotAny(Else | pp.Literal("}") | EOL)
+ + (
+ pp.QuotedString(quoteChar='"', escChar="\\")
+ | FunctionValue
+ | SubstitutionValue
+ | BracedValue
+ ),
+ )
+
+ Values = add_element("Values", pp.ZeroOrMore(Value)("value"))
+
+ Op = add_element(
+ "OP",
+ pp.Literal("=")
+ | pp.Literal("-=")
+ | pp.Literal("+=")
+ | pp.Literal("*=")
+ | pp.Literal("~="),
+ )
+
+ Key = add_element("Key", Identifier)
+
+ Operation = add_element(
+ "Operation", Key("key") + pp.locatedExpr(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") + pp.locatedExpr(CallArgs)("included")
+ )
+ Option = add_element("Option", pp.Keyword("option") + CallArgs("option"))
+ RequiresCondition = add_element("RequiresCondition", pp.originalTextFor(pp.nestedExpr()))
+
+ def parse_requires_condition(s, l, t):
+ # The following expression unwraps the condition via the additional info
+ # set by originalTextFor.
+ condition_without_parentheses = s[t._original_start + 1 : t._original_end - 1]
+
+ # And this replaces the colons with '&&' similar how it's done for 'Condition'.
+ condition_without_parentheses = (
+ condition_without_parentheses.strip().replace(":", " && ").strip(" && ")
+ )
+ return condition_without_parentheses
+
+ RequiresCondition.setParseAction(parse_requires_condition)
+ Requires = add_element(
+ "Requires", pp.Keyword("requires") + RequiresCondition("project_required_condition")
+ )
+
+ # 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
+ | Requires
+ | 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) -> Tuple[pp.ParseResults, str]:
+ print(f'Parsing "{file}"...')
+ try:
+ with open(file, "r") as file_fd:
+ contents = file_fd.read()
+
+ # old_contents = contents
+ contents = fixup_comments(contents)
+ contents = fixup_linecontinuation(contents)
+ result = self._Grammar.parseString(contents, parseAll=True)
+ except pp.ParseException as pe:
+ print(pe.line)
+ print(f"{' ' * (pe.col-1)}^")
+ print(pe)
+ raise pe
+ return result, contents
+
+
+def parseProFile(file: str, *, debug=False) -> Tuple[pp.ParseResults, str]:
+ parser = QmakeParser(debug=debug)
+ return parser.parseFile(file)
diff --git a/util/cmake/requirements.txt b/util/cmake/requirements.txt
new file mode 100644
index 0000000000..16fb99a08c
--- /dev/null
+++ b/util/cmake/requirements.txt
@@ -0,0 +1,8 @@
+pytest; python_version >= '3.7'
+pytest-cov; python_version >= '3.7'
+mypy; python_version >= '3.7'
+pyparsing; python_version >= '3.7'
+sympy; python_version >= '3.7'
+portalocker; python_version >= '3.7'
+black; python_version >= '3.7'
+
diff --git a/util/cmake/run_pro2cmake.py b/util/cmake/run_pro2cmake.py
new file mode 100755
index 0000000000..4a12c57b83
--- /dev/null
+++ b/util/cmake/run_pro2cmake.py
@@ -0,0 +1,246 @@
+#!/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 sys
+import typing
+import argparse
+from argparse import ArgumentParser
+
+
+def parse_command_line() -> argparse.Namespace:
+ parser = ArgumentParser(
+ description="Run pro2cmake on all .pro files recursively in given path. "
+ "You can pass additional arguments to the pro2cmake calls by appending "
+ "-- --foo --bar"
+ )
+ 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-missing",
+ dest="only_missing",
+ action="store_true",
+ help="Run pro2cmake only on .pro files that do not 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(
+ "--skip-subdirs-projects",
+ dest="skip_subdirs_projects",
+ action="store_true",
+ help="Don't run pro2cmake on TEMPLATE=subdirs projects.",
+ )
+ parser.add_argument(
+ "--is-example",
+ dest="is_example",
+ action="store_true",
+ help="Run pro2cmake with --is-example flag.",
+ )
+ parser.add_argument(
+ "--count", dest="count", help="How many projects should be converted.", type=int
+ )
+ parser.add_argument(
+ "--offset",
+ dest="offset",
+ help="From the list of found projects, from which project should conversion begin.",
+ type=int,
+ )
+ parser.add_argument(
+ "path", metavar="<path>", type=str, help="The path where to look for .pro files."
+ )
+
+ args, unknown = parser.parse_known_args()
+
+ # Error out when the unknown arguments do not start with a "--",
+ # which implies passing through arguments to pro2cmake.
+ if len(unknown) > 0 and unknown[0] != "--":
+ parser.error("unrecognized arguments: {}".format(" ".join(unknown)))
+ else:
+ args.pro2cmake_args = unknown[1:]
+
+ return 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 == ".":
+ dir_name = os.path.basename(os.getcwd())
+ if dir_name.endswith(pro_file_without_suffix):
+ return dir_name
+ return dir_name + "/__" + pro_file
+
+ all_files = []
+ previous_dir_name: typing.Optional[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 cmake_lists_missing_filter(path):
+ return not cmake_lists_exists_filter(path)
+
+ def qtbase_main_modules_filter(path):
+ main_modules = [
+ "corelib",
+ "network",
+ "gui",
+ "widgets",
+ "testlib",
+ "printsupport",
+ "opengl",
+ "sql",
+ "dbus",
+ "concurrent",
+ "xml",
+ ]
+ path_suffixes = [f"src/{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_missing:
+ filter_func = cmake_lists_missing_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, 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
+ pro2cmake_args = []
+ if sys.platform == "win32":
+ pro2cmake_args.append(sys.executable)
+ pro2cmake_args.append(pro2cmake)
+ if args.is_example:
+ pro2cmake_args.append("--is-example")
+ if args.skip_subdirs_projects:
+ pro2cmake_args.append("--skip-subdirs-project")
+ pro2cmake_args.append(os.path.basename(filename))
+
+ if args.pro2cmake_args:
+ pro2cmake_args += args.pro2cmake_args
+
+ result = subprocess.run(
+ pro2cmake_args,
+ cwd=os.path.dirname(filename),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+ stdout = f"Converted[{index}/{total}]: {filename}\n"
+ 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)
+ if args.offset:
+ all_files = all_files[args.offset :]
+ if args.count:
+ all_files = all_files[: args.count]
+ 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(
+ f"The following files were not successfully "
+ f"converted ({len(failed_files)} of {files_count}):"
+ )
+ for f in failed_files:
+ print(f' "{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..48c7181a04
--- /dev/null
+++ b/util/cmake/special_case_helper.py
@@ -0,0 +1,415 @@
+#!/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
+from textwrap import dedent
+
+
+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(f"Copying {src} to {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(f'Running command: "{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:
+ if debug:
+ print(
+ dedent(
+ f"""\
+ Error while running: "{args_string}"
+ {e.stdout}"""
+ )
+ )
+ return False
+ return True
+
+
+def does_file_have_conflict_markers(file_path: str, debug=False) -> bool:
+ if debug:
+ print(f"Checking if {file_path} has no leftover conflict markers.")
+ content_actual = read_content_from_file(file_path)
+ if "<<<<<<< HEAD" in content_actual:
+ print(f"Conflict markers found in {file_path}. " "Please remove or solve them first.")
+ 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(f"Removing special case blocks from {original_file_path}.")
+ content_no_special_cases = remove_special_cases(content_actual)
+
+ if debug:
+ print(
+ f"Saving original contents of {original_file_path} "
+ f"with removed special case blocks to {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(f"Error while trying to remove path: {path}. Exception: {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(f"Failed to create temporary directory for temporary git repo. Exception: {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(f"git add {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(f"git add {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(f"git add {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(f"Git merge conflict resolution process failed. Exception: {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(f"Error removing temporary repo. Exception: {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(f"git add {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:
+ if self.debug:
+ print("Retrying git add, the index.lock was probably acquired.")
+ if failed_once and success:
+ if self.debug:
+ print("git add succeeded.")
+ elif failed_once and not success:
+ print(f"git add failed. Make sure to git add {self.prev_file_path} yourself.")
+
+ 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(
+ f"Using git to reapply special case modifications to newly "
+ f"generated {self.generated_file_path} file"
+ )
+
+ 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)
+ if self.debug:
+ print(
+ "Special case reapplication using git is complete. "
+ "Make sure to fix remaining conflict markers."
+ )
+
+ except Exception as e:
+ print(f"Error occurred while trying to reapply special case modifications: {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/data/value_function.pro b/util/cmake/tests/data/value_function.pro
new file mode 100644
index 0000000000..598e4fadbd
--- /dev/null
+++ b/util/cmake/tests/data/value_function.pro
@@ -0,0 +1,2 @@
+TARGET = Dummy
+TARGET = $$qtLibraryTarget($$TARGET)
diff --git a/util/cmake/tests/test_lc_fixup.py b/util/cmake/tests/test_lc_fixup.py
new file mode 100755
index 0000000000..42094a5288
--- /dev/null
+++ b/util/cmake/tests/test_lc_fixup.py
@@ -0,0 +1,44 @@
+#!/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 qmake_parser 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..c18c3ddc65
--- /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 condition_simplifier 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..9acee46007
--- /dev/null
+++ b/util/cmake/tests/test_parsing.py
@@ -0,0 +1,354 @@
+#!/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 qmake_parser 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']['value']
+ 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 'included' in include
+ assert include['included'].get('value', '') == '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 'included' in result[1]
+ assert result[1]['included'].get('value', '') == '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
+
+def test_value_function():
+ result = parse_file(_tests_path + '/data/value_function.pro')
+ target = result[0]['value'][0]
+ assert target == 'Dummy'
+ value = result[1]['value']
+ assert value[0] == '$$TARGET'
diff --git a/util/cmake/tests/test_scope_handling.py b/util/cmake/tests/test_scope_handling.py
new file mode 100755
index 0000000000..1db8b2a079
--- /dev/null
+++ b/util/cmake/tests/test_scope_handling.py
@@ -0,0 +1,346 @@
+#!/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,
+ qmake_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)'
+
+def test_recursive_expansion():
+ scope = _new_scope(A='Foo',B='$$A/Bar')
+ assert scope.get_string('A') == 'Foo'
+ assert scope.get_string('B') == '$$A/Bar'
+ assert scope._expand_value('$$B/Source.cpp') == ['Foo/Bar/Source.cpp']
+ assert scope._expand_value('$$B') == ['Foo/Bar']
+