diff options
author | Alexandru Croitor <alexandru.croitor@qt.io> | 2020-01-31 11:43:22 +0100 |
---|---|---|
committer | Simon Hausmann <simon.hausmann@qt.io> | 2020-02-07 18:19:51 +0000 |
commit | 1c571e5fe7a31e7accb04b37a1d3bd7c8855d05c (patch) | |
tree | 65241f1258136d0c45bcaa3b51e1393a2f595e6a /util | |
parent | 2145cdc54d5812793310f7e3b3709bfa2648bd50 (diff) | |
parent | 4e7af2061e8c323b2a21f0549643a2cfab191664 (diff) |
Merge "Merge remote-tracking branch 'origin/wip/cmake' into dev"
Diffstat (limited to 'util')
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'] + |