diff options
Diffstat (limited to 'util')
24 files changed, 2283 insertions, 0 deletions
diff --git a/util/cmake/Pipfile b/util/cmake/Pipfile new file mode 100644 index 0000000000..d7e1905378 --- /dev/null +++ b/util/cmake/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pytest = "*" +mypy = "*" +pyparsing = "*" + +[dev-packages] + +[requires] +python_version = "3.7" diff --git a/util/cmake/cmakeconversionrate.py b/util/cmake/cmakeconversionrate.py new file mode 100755 index 0000000000..3496ed1b91 --- /dev/null +++ b/util/cmake/cmakeconversionrate.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +############################################################################# +## +## Copyright (C) 2018 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the plugins of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:GPL-EXCEPT$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 3 as published by the Free Software +## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +from argparse import ArgumentParser + +import os +import re +import subprocess +import sys +import typing + + +def _parse_commandline(): + parser = ArgumentParser(description='Calculate the conversion rate to cmake.') + parser.add_argument('--debug', dest='debug', action='store_true', + help='Turn on debug output') + parser.add_argument('source_directory', metavar='<Source Directory>', type=str, + help='The Qt module source directory') + parser.add_argument('binary_directory', metavar='<CMake build direcotry>', type=str, + help='The CMake build directory (might be empty)') + + return parser.parse_args() + + +def calculate_baseline(source_directory: str, *, debug: bool=False) -> int: + if debug: + print('Scanning "{}" for qmake-based tests.'.format(source_directory)) + result = subprocess.run('/usr/bin/git grep -E "^\\s*CONFIG\\s*\\+?=.*\\btestcase\\b" | sort -u | wc -l', + shell=True, capture_output=True, cwd=source_directory) + return int(result.stdout) + + +def build(source_directory: str, binary_directory: str, *, debug=False) -> None: + abs_source = os.path.abspath(source_directory) + if not os.path.isdir(binary_directory): + os.makedirs(binary_directory) + if not os.path.exists(os.path.join(binary_directory, 'CMakeCache.txt')): + + if debug: + print('Running cmake in "{}".'.format(binary_directory)) + result = subprocess.run(['/usr/bin/cmake', '-GNinja', abs_source], cwd=binary_directory) + if debug: + print('CMake return code: {}.'.format(result.returncode)) + + assert result.returncode == 0 + + if debug: + print('Running ninja in "{}".'.format(binary_directory)) + result = subprocess.run('/usr/bin/ninja', cwd=binary_directory) + if debug: + print('Ninja return code: {}.'.format(result.returncode)) + + assert result.returncode == 0 + + +def test(binary_directory: str, *, debug=False) -> typing.Tuple[int, int]: + if debug: + print('Running ctest in "{}".'.format(binary_directory)) + result = subprocess.run('/usr/bin/ctest -j 250 | grep "tests passed, "', + shell=True, capture_output=True, cwd=binary_directory) + summary = result.stdout.decode('utf-8').replace('\n', '') + if debug: + print('Test summary: {} ({}).'.format(summary, result.returncode)) + + matches = re.fullmatch(r'\d+% tests passed, (\d+) tests failed out of (\d+)', summary) + if matches: + if debug: + print('Matches: failed {}, total {}.'.format(matches.group(1), matches.group(2))) + return (int(matches.group(2)), int(matches.group(2)) - int(matches.group(1)), ) + + return (0, 0,) + + +def main() -> int: + args = _parse_commandline() + + base_line = calculate_baseline(args.source_directory, debug=args.debug) + if base_line <= 0: + print('Could not find the qmake baseline in {}.'.format(args.source_directory)) + return 1 + + if args.debug: + print('qmake baseline: {} test binaries.'.format(base_line)) + + cmake_total = 0 + cmake_success = 0 + try: + build(args.source_directory, args.binary_directory, debug=args.debug) + (cmake_total, cmake_success, ) = test(args.binary_directory, debug=args.debug) + finally: + if cmake_total == 0: + print('\n\n\nCould not calculate the cmake state.') + return 2 + else: + print('\n\n\nCMake test conversion rate: {:.2%}.'.format(cmake_total / base_line)) + print('CMake test success rate : {:.2%}.'.format(cmake_success / base_line)) + return 0 + + +if __name__ == '__main__': + main() diff --git a/util/cmake/configurejson2cmake.py b/util/cmake/configurejson2cmake.py new file mode 100755 index 0000000000..4623e8d874 --- /dev/null +++ b/util/cmake/configurejson2cmake.py @@ -0,0 +1,860 @@ +#!/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 +import os.path +import re +import sys +from typing import Set, Union, List, Dict + +from helper import map_qt_library, featureName, substitute_platform + +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_library(lib: str) -> Union[str, LibraryMapping, List[str]]: + libmap = { + 'zlib': 'ZLIB', + 'gbm': 'gbm', + 'host_dbus': None, + 'libdl': None, # handled by CMAKE_DL_LIBS + 'libatomic': 'Atomic', + 'double-conversion': 'WrapDoubleConversion', + 'gnu_iconv': None, + 'sun_iconv': None, + 'posix_iconv': None, + 'icu': ['ICU', 'COMPONENTS', 'i18n', 'uc', 'data'], + 'pcre2': ['PCRE2', 'REQUIRED'], + 'libpng': 'PNG', + 'libudev': 'Libudev', + 'udev': 'Libudev', + 'journald': 'Libsystemd', + 'vulkan': 'Vulkan', + 'glib': 'GLib', + 'harfbuzz': 'harfbuzz', + 'opengl': LibraryMapping(package="OpenGL", resultVariable="OpenGL_OpenGL"), + 'egl': LibraryMapping(package="OpenGL", resultVariable="OpenGL_EGL"), + 'openssl_headers': LibraryMapping(package="OpenSSL", resultVariable="OPENSSL_INCLUDE_DIR", appendFoundSuffix=False), + 'libpng': 'PNG', + 'libjpeg': 'JPEG', + 'freetype': 'Freetype', + 'fontconfig': LibraryMapping(package='Fontconfig', resultVariable="FONTCONFIG"), + 'libinput': 'Libinput', + 'xcb': ['XCB', '1.9'], + 'libproxy': 'libproxy', + 'drm': 'Libdrm', + 'xkbcommon': ['XKB', '0.4.1'], + 'xlib': 'X11', + 'xcb_xlib': 'X11_XCB', + 'xrender': LibraryMapping(package="XCB", resultVariable="XCB_RENDER"), + 'xcb_render': LibraryMapping(package="XCB", resultVariable="XCB_RENDER"), + 'xcb_glx': LibraryMapping(package="XCB", resultVariable="XCB_GLX"), + 'xcb_xkb': LibraryMapping(package="XCB", resultVariable="XCB_XKB"), + 'xcb_xinput': LibraryMapping(package="XCB", resultVariable="XCB_XINPUT"), + 'x11sm': LibraryMapping(package="X11", resultVariable="X11_SM"), + 'wayland_server': 'Wayland', + } # type: Dict[str, Union[str, List[str], LibraryMapping]] + if lib not in libmap: + raise Exception(' XXXX Unknown library "{}".'.format(lib)) + + return libmap[lib] + + +def map_tests(test: str) -> str: + testmap = { + 'c++11': '$<COMPILE_FEATURES:cxx_std_11>', + 'c++14': '$<COMPILE_FEATURES:cxx_std_14>', + 'c++1z': '$<COMPILE_FEATURES:cxx_std_17>', + 'c99': '$<COMPILE_FEATURES:c_std_99>', + 'c11': '$<COMPILE_FEATURES:c_std_11>', + + 'x86SimdAlways': 'ON', # FIXME: Is this the right thing? + + '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_c16c', + 'fma': 'TEST_subarch_fma', + 'fma4': 'TEST_subarch_fma4', + 'fsgsbase': 'TEST_subarch_fsgsbase', + 'gfni': 'TEST_subarch_gfni', + 'ibt': 'TEST_subarch_ibt', + 'lwp': 'TEST_subarch_lwp', + 'lzcnt': 'TEST_subarch_lzcnt', + 'mmx': 'TEST_subarch_mmx', + 'movbe': 'TEST_subarch_movbe', + 'mpx': 'TEST_subarch_mpx', + 'no-sahf': 'TEST_subarch_no_shaf', + 'pclmul': 'TEST_subarch_pclmul', + 'popcnt': 'TEST_subarch_popcnt', + 'prefetchwt1': 'TEST_subarch_prefetchwt1', + 'prfchw': 'TEST_subarch_prfchw', + 'pdpid': 'TEST_subarch_rdpid', + 'rdpid': 'TEST_subarch_rdpid', + 'rdseed': 'TEST_subarch_rdseed', + 'rdrnd': 'TEST_subarch_rdseed', # FIXME: Is this the right thing? + 'rtm': 'TEST_subarch_rtm', + 'shani': 'TEST_subarch_sha', + 'shstk': 'TEST_subarch_shstk', + 'sse2': 'TEST_subarch_sse2', + 'sse3': 'TEST_subarch_sse3', + 'ssse3': 'TEST_subarch_ssse3', + 'sse4a': 'TEST_subarch_sse4a', + 'sse4_1': 'TEST_subarch_sse4_1', + 'sse4_2': 'TEST_subarch_sse4_2', + 'tbm': 'TEST_subarch_tbm', + 'xop': 'TEST_subarch_xop', + + 'neon': 'TEST_subarch_neon', + 'iwmmxt': 'TEST_subarch_iwmmxt', + 'crc32': 'TEST_subarch_crc32', + + 'vis': 'TEST_subarch_vis', + 'vis2': 'TEST_subarch_vis2', + 'vis3': 'TEST_subarch_vis3', + + 'dsp': 'TEST_subarch_dsp', + 'dspr2': 'TEST_subarch_dspr2', + + 'altivec': 'TEST_subarch_altivec', + 'spe': 'TEST_subarch_spe', + 'vsx': 'TEST_subarch_vsx', + + 'posix-iconv': 'TEST_posix_iconv', + 'sun-iconv': 'TEST_sun_iconv', + + 'openssl11': '(OPENSSL_VERSION VERSION_GREATER_EQUAL "1.1.0")', + + 'reduce_exports': 'CMAKE_CXX_COMPILE_OPTIONS_VISIBILITY', + } + if test in testmap: + return testmap.get(test, None) + if test in knownTests: + return 'TEST_{}'.format(featureName(test)) + return None + + +def cm(ctx, *output): + txt = ctx['output'] + if txt != '' and not txt.endswith('\n'): + txt += '\n' + txt += '\n'.join(output) + + ctx['output'] = txt + return ctx + + +def readJsonFromDir(dir): + path = os.path.join(dir, 'configure.json') + + print('Reading {}...'.format(path)) + assert os.path.exists(path) + + with open(path, 'r') as fh: + return json.load(fh) + + +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): + extra = [] + try: + newlib = map_library(lib) + if isinstance(newlib, list): + extra = newlib[1:] + newlib = newlib[0] + elif isinstance(newlib, LibraryMapping): + newlib = newlib.package + except Exception: + return ctx + + if newlib is None: + print(' **** Skipping library "{}" -- was masked.'.format(lib)) + return + + print(' mapped library {} to {}.'.format(lib, newlib)) + + # Avoid duplicate find_package calls. + if newlib in cmake_find_packages_set: + return + + cmake_find_packages_set.add(newlib) + + isRequired = False + + if extra: + if "REQUIRED" in extra: + isRequired = True + extra.remove("REQUIRED") + + if extra: + cm_fh.write('find_package({} {})\n'.format(newlib, ' '.join(extra))) + else: + cm_fh.write('find_package({})\n'.format(newlib)) + + cm_fh.write('set_package_properties({} PROPERTIES TYPE {})\n' + .format(newlib, 'REQUIRED' if isRequired else 'OPTIONAL') + ) + +def lineify(label, value, quote=True): + if value: + if quote: + return ' {} "{}"\n'.format(label, value.replace('"', '\\"')) + return ' {} {}\n'.format(label, value) + return '' + +def map_condition(condition): + # Handle NOT: + if isinstance(condition, list): + condition = '(' + ') AND ('.join(condition) + ')' + if isinstance(condition, bool): + if condition: + return 'ON' + else: + return 'OFF' + assert isinstance(condition, str) + + mapped_features = { + "dlopen": "UNIX", + 'gbm': 'gbm_FOUND', + "sun-libiconv": "TEST_sun_iconv", + "system-xcb": "ON", + "system-freetype": "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': + try: + substitution = map_library(match.group(2)) + if isinstance(substitution, list): + substitution = substitution[0] + elif isinstance(substitution, LibraryMapping): + appendFoundSuffix = substitution.appendFoundSuffix + substitution = substitution.resultVariable + except Exception: + substitution = None + + if substitution is not None and appendFoundSuffix: + substitution += '_FOUND' + + elif match.group(1) == 'features': + feature = match.group(2) + if feature in mapped_features: + substitution = mapped_features.get(feature) + else: + substitution = 'QT_FEATURE_{}'.format(featureName(match.group(2))) + + elif match.group(1) == 'subarch': + substitution = 'TEST_subarch_{}'.format(match.group(2)) + + elif match.group(1) == 'call': + if match.group(2) == 'crossCompile': + substitution = 'CMAKE_CROSSCOMPILING' + + elif match.group(1) == 'tests': + substitution = map_tests(match.group(2)) + + elif match.group(1) == 'input': + substitution = 'INPUT_{}'.format(featureName(match.group(2))) + + elif match.group(1) == 'config': + substitution = substitute_platform(match.group(2)) + + elif match.group(1) == 'arch': + if match.group(2) == 'i386': + # FIXME: Does this make sense? + substitution = '(TEST_architecture_arch STREQUAL i386)' + elif match.group(2) == 'x86_64': + substitution = '(TEST_architecture_arch STREQUAL x86_64)' + elif match.group(2) == 'arm': + # FIXME: Does this make sense? + substitution = '(TEST_architecture_arch STREQUAL arm)' + elif match.group(2) == 'arm64': + # FIXME: Does this make sense? + substitution = '(TEST_architecture_arch STREQUAL arm64)' + elif match.group(2) == 'mips': + # FIXME: Does this make sense? + substitution = '(TEST_architecture_arch STREQUAL mips)' + + if substitution is None: + print(' XXXX Unknown condition "{}".'.format(match.group(0))) + has_failed = True + else: + mapped_condition += condition[last_pos:match.start(1)] + substitution + last_pos = match.end(2) + + mapped_condition += condition[last_pos:] + + # Space out '(' and ')': + mapped_condition = mapped_condition.replace('(', ' ( ') + mapped_condition = mapped_condition.replace(')', ' ) ') + + # Prettify: + condition = re.sub('\\s+', ' ', mapped_condition) + condition = condition.strip() + + if has_failed: + condition += ' OR FIXME' + + return condition + + +def parseInput(ctx, input, data, cm_fh): + skip_inputs = { + "prefix", "hostprefix", "extprefix", + + "archdatadir", "bindir", "datadir", "docdir", + "examplesdir", "external-hostbindir", "headerdir", + "hostbindir", "hostdatadir", "hostlibdir", + "importdir", "libdir", "libexecdir", + "plugindir", "qmldir", "settingsdir", + "sysconfdir", "testsdir", "translationdir", + + "android-arch", "android-ndk", "android-ndk-host", + "android-ndk-platform", "android-sdk", + "android-toolchain-version", "android-style-assets", + + "appstore-compliant", + + "avx", "avx2", "avx512", "c++std", "ccache", "commercial", + "compile-examples", "confirm-license", + "dbus", + "dbus-runtime", + + "debug", "debug-and-release", + + "developer-build", + + "device", "device-option", + + "f16c", + + "force-asserts", "force-debug-info", "force-pkg-config", + "framework", + + "gc-binaries", + + "gdb-index", + + "gcc-sysroot", + + "gcov", + + "gnumake", + + "gui", + + "headersclean", + + "incredibuild-xge", + + "libudev", + "ltcg", + "make", + "make-tool", + + "mips_dsp", + "mips_dspr2", + "mp", + + "nomake", + + "opensource", + + "optimize-debug", "optimize-size", "optimized-qmake", "optimized-tools", + + "pch", + + "pkg-config", + + "platform", + + "plugin-manifests", + "profile", + "qreal", + + "reduce-exports", "reduce-relocations", + + "release", + + "rpath", + + "sanitize", + + "sdk", + + "separate-debug-info", + + "shared", + + "silent", + + "qdbus", + + "sse2", + "sse3", + "sse4.1", + "sse4.2", + "ssse3", + "static", + "static-runtime", + "strip", + "syncqt", + "sysroot", + "testcocoon", + "use-gold-linker", + "warnings-are-errors", + "Werror", + "widgets", + "xplatform", + "zlib", + + "doubleconversion", + + "eventfd", + "glib", + "icu", + "inotify", + "journald", + "pcre", + "posix-ipc", + "pps", + "slog2", + "syslog", + + "sqlite", + } + + if input in skip_inputs: + print(' **** Skipping input {}: masked.'.format(input)) + return + + type = data + if isinstance(data, dict): + type = data["type"] + + if type == "boolean": + print(' **** Skipping boolean input {}: masked.'.format(input)) + return + + if type == "enum": + cm_fh.write("# input {}\n".format(input)) + cm_fh.write('set(INPUT_{} "undefined" CACHE STRING "")\n'.format(featureName(input))) + cm_fh.write('set_property(CACHE INPUT_{} PROPERTY STRINGS undefined {})\n\n'.format(featureName(input), " ".join(data["values"]))) + return + + print(' XXXX UNHANDLED INPUT TYPE {} in input description'.format(type)) + return + + +# "tests": { +# "cxx11_future": { +# "label": "C++11 <future>", +# "type": "compile", +# "test": { +# "include": "future", +# "main": [ +# "std::future<int> f = std::async([]() { return 42; });", +# "(void)f.get();" +# ], +# "qmake": "unix:LIBS += -lpthread" +# } +# }, +def parseTest(ctx, test, data, cm_fh): + skip_tests = { + 'c11', 'c99', + 'c++11', 'c++14', 'c++1y', 'c++1z', + 'reduce_exports', + 'posix-iconv', "sun-iconv", + 'separate_debug_info', # FIXME: see if cmake can do this + 'gc_binaries', + } + + if test in skip_tests: + print(' **** Skipping features {}: masked.'.format(test)) + return + + if data["type"] == "compile": + knownTests.add(test) + + details = data["test"] + + if isinstance(details, str): + print(' XXXX UNHANDLED TEST SUB-TYPE {} in test description'.format(details)) + return + + head = details.get("head", "") + if isinstance(head, list): + head = "\n".join(head) + + sourceCode = head + '\n' + + include = details.get("include", "") + if isinstance(include, list): + include = '#include <' + '>\n#include <'.join(include) + '>' + elif include: + include = '#include <{}>'.format(include) + + sourceCode += include + '\n' + + tail = details.get("tail", "") + if isinstance(tail, list): + tail = "\n".join(tail) + + sourceCode += tail + '\n' + + sourceCode += "int main(int argc, char **argv)\n" + sourceCode += "{\n" + sourceCode += " (void)argc; (void)argv;\n" + sourceCode += " /* BEGIN TEST: */\n" + + main = details.get("main", "") + if isinstance(main, list): + main = "\n".join(main) + + sourceCode += main + '\n' + + sourceCode += " /* END TEST: */\n" + sourceCode += " return 0;\n" + sourceCode += "}\n" + + sourceCode = sourceCode.replace('"', '\\"') + + cm_fh.write("# {}\n".format(test)) + cm_fh.write("qt_config_compile_test({}\n".format(featureName(test))) + cm_fh.write(lineify("LABEL", data.get("label", ""))) + cm_fh.write('"' + sourceCode + '"') + if "qmake" in details: + cm_fh.write("# FIXME: qmake: {}\n".format(details["qmake"])) + cm_fh.write(")\n\n") + + elif data["type"] == "x86Simd": + knownTests.add(test) + + label = data["label"] + + cm_fh.write("# {}\n".format(test)) + cm_fh.write("qt_config_compile_test_x86simd({} \"{}\")\n".format(test, label)) + cm_fh.write("\n") + +# "features": { +# "android-style-assets": { +# "label": "Android Style Assets", +# "condition": "config.android", +# "output": [ "privateFeature" ], +# "comment": "This belongs into gui, but the license check needs it here already." +# }, + else: + print(' XXXX UNHANDLED TEST TYPE {} in test description'.format(data["type"])) + + +def parseFeature(ctx, feature, data, cm_fh): + skip_features = { + 'c++11', 'c++14', 'c++1y', 'c++1z', # C++ versions + 'c89', 'c99', 'c11', # C versions + 'stl', # Do we really need to test for this in 2018?! + 'rpath', 'rpath_dir', # rpath related + 'static', 'shared', # static/shared libs + 'debug', 'release', 'debug_and_release', 'build_all', 'optimize_debug', 'optimize_size', # build types + 'release_tools', 'gcov', 'silent', 'profile', + 'msvc_mp', 'static_runtime', 'incredibuild_xge', 'ccache', # compiler specific stuff + 'sanitize_address', 'sanitize_thread', 'sanitize_memory', # sanitizer + 'sanitize_undefined', 'sanitizer', + 'force_debug_info', 'separate_debug_info', 'warnings_are_errors', # FIXME: Do we need these? + 'strip', 'precompile_header', 'ltcg', 'enable_new_dtags', + 'enable_gdb_index', 'reduce_relocations', + 'stack-protector-strong', + 'host-dbus', # dbus related + 'cross_compile', 'gcc-sysroot', # cross compile related + 'gc_binaries', 'qmakeargs', 'use_gold_linker', 'pkg-config', 'verifyspec', # qmake stuff... + 'GNUmake', 'compiler-flags', + 'system-doubleconversion', 'system-pcre2', 'system-zlib', 'system-png', 'system-jpeg', 'system-freetype', 'system-xcb', 'xkbcommon-system', # system libraries + 'doubleconversion', + 'dlopen', # handled by CMAKE_DL_LIBS + 'alloc_stdlib_h', 'alloc_h', 'alloc_malloc_h', # handled by alloc target + 'posix_fallocate', # Only needed for sqlite, which we do not want to build + 'qpa_default_platform', # Not a bool! + 'sun-libiconv', # internal feature but not referenced in our system + } + if feature in skip_features: + print(' **** Skipping features {}: masked.'.format(feature)) + return + + disabled_features = set() + + override_condition = {} + + handled = { 'autoDetect', 'comment', 'condition', 'description', 'disable', 'emitIf', 'enable', 'label', 'output', 'purpose', 'section' } + label = data.get('label', '') + purpose = data.get('purpose', data.get('description', label)) + autoDetect = map_condition(data.get('autoDetect', '')) + condition = override_condition.get(feature, map_condition(data.get('condition', ''))) + output = data.get('output', []) + comment = data.get('comment', '') + section = data.get('section', '') + enable = map_condition(data.get('enable', '')) + disable = map_condition(data.get('disable', '')) + emitIf = map_condition(data.get('emitIf', '')) + + if feature in disabled_features: + condition = "FALSE" + + for k in [k for k in data.keys() if k not in handled]: + print(' XXXX UNHANDLED KEY {} in feature description'.format(k)) + + if not output: + # feature that is only used in the conditions of other features + output = ["internalFeature"] + + publicInfo = False + privateInfo = False + internalFeature = False + + for o in output: + outputType = o + outputArgs = {} + if isinstance(o, dict): + outputType = o['type'] + outputArgs = o + + if outputType in ['varAssign', 'varAppend', 'varRemove', 'publicQtConfig', 'privateConfig', 'publicConfig']: + continue + + elif outputType in ['feature', 'publicFeature', 'define']: + publicInfo = True + elif outputType == 'privateFeature': + privateInfo = True + elif outputType == 'internalFeature': + internalFeature = True + else: + print(' XXXX UNHANDLED OUTPUT TYPE {} in feature {}.'.format(outputType, feature)) + continue + + if not publicInfo and not privateInfo and not internalFeature: + print(' **** Skipping feature {}: Not relevant for C++.'.format(feature)) + return + + # write feature: + cxxFeature = featureName(feature) + if comment: + cm_fh.write('# {}\n'.format(comment)) + + cm_fh.write('qt_feature("{}"'.format(cxxFeature)) + if publicInfo: + cm_fh.write(' PUBLIC') + if privateInfo: + cm_fh.write(' PRIVATE') + cm_fh.write('\n') + + cm_fh.write(lineify('SECTION', section)) + cm_fh.write(lineify('LABEL', label)) + if purpose != label: + cm_fh.write(lineify('PURPOSE', purpose)) + cm_fh.write(lineify('AUTODETECT', autoDetect, quote=False)) + cm_fh.write(lineify('CONDITION', condition, quote=False)) + cm_fh.write(lineify('ENABLE', enable, quote=False)) + cm_fh.write(lineify('DISABLE', disable, quote=False)) + cm_fh.write(lineify('EMIT_IF', emitIf, quote=False)) + cm_fh.write(')\n') + + for o in output: + outputType = o + outputArgs = {} + if isinstance(o, dict): + outputType = o['type'] + outputArgs = o + + # Map feature to define: + if outputType == 'feature': + outputType = 'define' + outputArgs = {'name': 'QT_NO_{}'.format(cxxFeature.upper()), + 'negative': True, + 'value': 1, + 'type': 'define'} + + if outputType != 'define': + continue + + if outputArgs.get('name') is None: + print(' XXXX DEFINE output without name in feature {}.'.format(feature)) + continue + + cm_fh.write('qt_feature_definition("{}" "{}"'.format(cxxFeature, outputArgs.get('name'))) + if outputArgs.get('negative', False): + cm_fh.write(' NEGATE') + if outputArgs.get('value') is not None: + cm_fh.write(' VALUE "{}"'.format(outputArgs.get('value'))) + cm_fh.write(')\n') + + +def processInputs(ctx, data, cm_fh): + print(' inputs:') + if 'commandline' not in data: + return + + commandLine = data['commandline'] + if "options" not in commandLine: + return + + for input in commandLine['options']: + parseInput(ctx, input, commandLine['options'][input], cm_fh) + + +def processTests(ctx, data, cm_fh): + print(' tests:') + if 'tests' not in data: + return + + for test in data['tests']: + parseTest(ctx, test, data['tests'][test], cm_fh) + + +def processFeatures(ctx, data, cm_fh): + print(' features:') + if 'features' not in data: + return + + for feature in data['features']: + parseFeature(ctx, feature, data['features'][feature], cm_fh) + + +def processLibraries(ctx, data, cm_fh): + cmake_find_packages_set = set() + print(' libraries:') + if 'libraries' not in data: + return + + for lib in data['libraries']: + parseLib(ctx, lib, data['libraries'][lib], cm_fh, cmake_find_packages_set) + + +def processSubconfigs(dir, ctx, data): + assert ctx is not None + if 'subconfigs' in data: + for subconf in data['subconfigs']: + subconfDir = os.path.join(dir, subconf) + subconfData = readJsonFromDir(subconfDir) + subconfCtx = ctx + processJson(subconfDir, subconfCtx, subconfData) + + +def processJson(dir, ctx, data): + ctx['module'] = data.get('module', 'global') + + ctx = processFiles(ctx, data) + + with open(os.path.join(dir, "configure.cmake"), 'w') as cm_fh: + cm_fh.write("\n\n#### Inputs\n\n") + + processInputs(ctx, data, cm_fh) + + cm_fh.write("\n\n#### Libraries\n\n") + + processLibraries(ctx, data, cm_fh) + + cm_fh.write("\n\n#### Tests\n\n") + + processTests(ctx, data, cm_fh) + + cm_fh.write("\n\n#### Features\n\n") + + processFeatures(ctx, data, cm_fh) + + if ctx.get('module') == 'global': + cm_fh.write('\nqt_extra_definition("QT_VERSION_STR" "\\\"${PROJECT_VERSION}\\\"" PUBLIC)\n') + cm_fh.write('qt_extra_definition("QT_VERSION_MAJOR" ${PROJECT_VERSION_MAJOR} PUBLIC)\n') + cm_fh.write('qt_extra_definition("QT_VERSION_MINOR" ${PROJECT_VERSION_MINOR} PUBLIC)\n') + cm_fh.write('qt_extra_definition("QT_VERSION_PATCH" ${PROJECT_VERSION_PATCH} PUBLIC)\n') + + if ctx.get('module') == 'gui': + cm_fh.write('\nqt_extra_definition("QT_QPA_DEFAULT_PLATFORM" "${QT_QPA_DEFAULT_PLATFORM}" PUBLIC)\n') + + # do this late: + processSubconfigs(dir, ctx, data) + + +def main(): + if len(sys.argv) != 2: + print("This scripts needs one directory to process!") + quit(1) + + dir = sys.argv[1] + + print("Processing: {}.".format(dir)) + + data = readJsonFromDir(dir) + processJson(dir, {}, data) + + +if __name__ == '__main__': + main() diff --git a/util/cmake/generate_module_map.sh b/util/cmake/generate_module_map.sh new file mode 100755 index 0000000000..1ca0bfc43c --- /dev/null +++ b/util/cmake/generate_module_map.sh @@ -0,0 +1,38 @@ +#!/usr/bin/bash +############################################################################# +## +## Copyright (C) 2018 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the plugins of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:GPL-EXCEPT$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 3 as published by the Free Software +## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +pro_files=$(find . -name \*.pro) + +for f in ${pro_files}; do + if grep "^load(qt_module)" "${f}" > /dev/null ; then + target=$(grep "TARGET" "${f}" | cut -d'=' -f2 | sed -e "s/\s*//g") + module=$(basename ${f}) + echo "'${module%.pro}': '${target}'," + fi +done diff --git a/util/cmake/helper.py b/util/cmake/helper.py new file mode 100644 index 0000000000..2ddeee98c1 --- /dev/null +++ b/util/cmake/helper.py @@ -0,0 +1,206 @@ +############################################################################# +## +## 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 + + +def featureName(input: str) -> str: + return re.sub(r'[^a-zA-Z0-9_]', '_', input) + + +def map_qt_base_library(lib: str) -> str: + library_map = { + 'global': 'Qt::Core', # manually added special case + 'accessibility_support': 'Qt::AccessibilitySupport', + 'androidextras': 'Qt::AndroidExtras', + 'animation': 'Qt::3DAnimation', + 'application-lib': 'Qt::AppManApplication', + 'bluetooth': 'Qt::Bluetooth', + 'bootstrap-dbus': 'Qt::BootstrapDBus', + 'bootstrap': 'Qt::Bootstrap', + 'client': 'Qt::WaylandClient', + 'clipboard_support': 'Qt::ClipboardSupport', + 'common-lib': 'Qt::AppManCommon', + 'compositor': 'Qt::WaylandCompositor', + 'concurrent': 'Qt::Concurrent', + 'container': 'Qt::AxContainer', + 'control': 'Qt::AxServer', + 'core_headers': 'Qt::WebEngineCore', + 'core': 'Qt::Core', + 'coretest': 'Qt::3DCoreTest', + 'crypto-lib': 'Qt::AppManCrypto', + 'dbus': 'Qt::DBus', + 'devicediscovery': 'Qt::DeviceDiscoverySupport', + 'edid': 'Qt::EdidSupport', + 'eglconvenience': 'Qt::EglSupport', + 'eglfsdeviceintegration': 'Qt::EglFSDeviceIntegration', + 'eglfs_kms_support': 'Qt::EglFsKmsSupport', + 'enginio_client': 'Enginio', + 'eventdispatchers': 'Qt::EventDispatcherSupport', + 'extras': 'Qt::3DExtras', + 'fbconvenience': 'Qt::FbSupport', + 'fontdatabase_support': 'Qt::FontDatabaseSupport', + 'gamepad': 'Qt::Gamepad', + 'glxconvenience': 'Qt::GlxSupport', + 'graphics_support': 'Qt::GraphicsSupport', + 'gsttools': 'Qt::MultimediaGstTools', + 'gui': 'Qt::Gui', + 'help': 'Qt::Help', + 'hunspellinputmethod': 'Qt::HunspellInputMethod', + 'input': 'Qt::InputSupport', + 'installer-lib': 'Qt::AppManInstaller', + 'kmsconvenience': 'Qt::KmsSupport', + 'launcher-lib': 'Qt::AppManLauncher', + 'lib': 'Qt::Designer', + 'linuxaccessibility': 'Qt::LinuxAccessibilitySupport', + 'location': 'Qt::Location', + 'logic': 'Qt::3DLogic', + 'macextras': 'Qt::MacExtras', + 'main-lib': 'Qt::AppManMain', + 'manager-lib': 'Qt::AppManManager', + 'monitor-lib': 'Qt::AppManMonitor', + 'multimedia': 'Qt::Multimedia', + 'multimediawidgets': 'Qt::MultimediaWidgets', + 'network': 'Qt::Network', + 'nfc': 'Qt::Nfc', + 'oauth': 'Qt::NetworkAuth', + 'openglextensions': 'Qt::OpenGLExtensions', + 'opengl': 'Qt::OpenGL', + 'package-lib': 'Qt::AppManPackage', + 'packetprotocol': 'Qt::PacketProtocol', + 'particles': 'Qt::QuickParticles', + 'platformcompositor': 'Qt::PlatformCompositorSupport', + 'plugin-interfaces': 'Qt::AppManPluginInterfaces', + 'positioning': 'Qt::Positioning', + 'positioningquick': 'Qt::PositioningQuick', + 'printsupport': 'Qt::PrintSupport', + 'purchasing': 'Qt::Purchasing', + 'qmldebug': 'Qt::QmlDebug', + 'qmldevtools': 'Qt::QmlDevTools', + 'qml': 'Qt::Qml', + 'qmltest': 'Qt::QuickTest', + 'qtmultimediaquicktools': 'Qt::MultimediaQuick', + 'qtzlib': 'Qt::Zlib', + 'quick3danimation': 'Qt::3DQuickAnimation', + 'quick3dextras': 'Qt::3DQuickExtras', + 'quick3dinput': 'Qt::3DQuickInput', + 'quick3d': 'Qt::3DQuick', + 'quick3drender': 'Qt::3DQuickRender', + 'quick3dscene2d': 'Qt::3DQuickScene2D', + 'quickcontrols2': 'Qt::QuickControls2', + 'quick': 'Qt::Quick', + 'quickshapes': 'Qt::QuickShapes', + 'quicktemplates2': 'Qt::QuickTemplates2', + 'quickwidgets': 'Qt::QuickWidgets', + 'render': 'Qt::3DRender', + 'script': 'Qt::Script', + 'scripttools': 'Qt::ScriptTools', + 'sensors': 'Qt::Sensors', + 'serialport': 'Qt::SerialPort', + 'services': 'Qt::ServiceSupport', + 'sql': 'Qt::Sql', + 'svg': 'Qt::Svg', + 'testlib': 'Qt::Test', + 'theme_support': 'Qt::ThemeSupport', + 'service_support': 'Qt::ServiceSupport', + 'eventdispatcher_support': 'Qt::EventDispatcherSupport', + 'edid_support': 'Qt::EdidSupport', + 'tts': 'Qt::TextToSpeech', + 'uiplugin': 'Qt::UiPlugin', + 'uitools': 'Qt::UiTools', + 'virtualkeyboard': 'Qt::VirtualKeyboard', + 'vkconvenience': 'Qt::VulkanSupport', + 'webchannel': 'Qt::WebChannel', + 'webengine': 'Qt::WebEngine', + 'webenginewidgets': 'Qt::WebEngineWidgets', + 'websockets': 'Qt::WebSockets', + 'webview': 'Qt::WebView', + 'widgets': 'Qt::Widgets', + 'window-lib': 'Qt::AppManWindow', + 'windowsuiautomation': 'Qt::WindowsUIAutomationSupport', + 'winextras': 'Qt::WinExtras', + 'x11extras': 'Qt::X11Extras', + 'xcb_qpa_lib': 'Qt::XcbQpa', + 'xmlpatterns': 'Qt::XmlPatterns', + 'xml': 'Qt::Xml', + } + return library_map.get(lib, lib) + + +def map_qt_library(lib: str) -> str: + private = False + if lib.endswith('-private'): + private = True + lib = lib[:-8] + mapped = map_qt_base_library(lib) + if private: + mapped += 'Private' + return mapped + + +platform_mapping = { + 'win32': 'WIN32', + 'unix': 'UNIX', + 'darwin': 'APPLE', + 'linux': 'LINUX', + 'integrity': 'INTEGRITY', + 'qnx': 'QNX', + 'vxworks': 'VXWORKS', + 'hpux': 'HPUX', + 'nacl': 'NACL', + 'android': 'ANDROID', + 'android-embedded': 'ANDROID_EMBEDDED', + 'uikit': 'APPLE_UIKIT', + 'tvos': 'APPLE_TVOS', + 'watchos': 'APPLE_WATCHOS', + 'winrt': 'WINRT', + 'wasm': 'WASM', + 'msvc': 'MSVC', + 'clang': 'CLANG', + 'gcc': 'GCC', + 'osx': 'APPLE_OSX', + 'freebsd': 'FREEBSD', + 'haiku': 'HAIKU', + 'netbsd': 'NETBSD', + 'mac': 'APPLE_OSX', +} + + +def substitute_platform(platform: str) -> str: + """ Return the qmake platform as cmake platform or the unchanged string. """ + return platform_mapping.get(platform, platform) + + +libray_mapping = { + 'zlib': 'ZLIB::ZLIB', + 'glib': 'PkgConfig::GLib', +} + + +def substitute_libs(lib: str) -> str: + return libray_mapping.get(lib, lib) diff --git a/util/cmake/pro2cmake.py b/util/cmake/pro2cmake.py new file mode 100755 index 0000000000..37364d63ad --- /dev/null +++ b/util/cmake/pro2cmake.py @@ -0,0 +1,736 @@ +#!/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.path +import re +import sys +import io +from typing import IO, List, Dict, Union +import typing + +import pyparsing as pp + +from helper import map_qt_library, featureName, substitute_platform, substitute_libs + + +def _parse_commandline(): + parser = ArgumentParser(description='Generate CMakeLists.txt files from .pro files.') + parser.add_argument('--debug', dest='debug', action='store_true', + help='Turn on all debug output') + parser.add_argument('--debug-parser', dest='debug_parser', action='store_true', + help='Print debug output from qmake parser.') + parser.add_argument('--debug-parse-result', dest='debug_parse_result', action='store_true', + help='Dump the qmake parser result.') + parser.add_argument('--debug-parse-dictionary', dest='debug_parse_dictionary', action='store_true', + help='Dump the qmake parser result as dictionary.') + parser.add_argument('--debug-pro-structure', dest='debug_pro_structure', action='store_true', + help='Dump the structure of the qmake .pro-file.') + parser.add_argument('--debug-full-pro-structure', dest='debug_full_pro_structure', action='store_true', + help='Dump the full structure of the qmake .pro-file (with includes).') + parser.add_argument('files', metavar='<.pro/.pri file>', type=str, nargs='+', + help='The .pro/.pri file to process') + + return parser.parse_args() + + +def spaces(indent: int) -> str: + return ' ' * indent + + +def map_to_file(f: str, top_dir: str, current_dir: str, + want_absolute_path: bool = False) -> typing.Optional[str]: + if f == '$$NO_PCH_SOURCES': + return None + if f.startswith('$$PWD/') or f == '$$PWD': # INCLUDEPATH += $$PWD + return os.path.join(os.path.relpath(current_dir, top_dir), f[6:]) + if f.startswith('$$OUT_PWD/'): + return "${CMAKE_CURRENT_BUILD_DIR}/" + f[10:] + if f.startswith('$$QT_SOURCE_TREE'): + return "${PROJECT_SOURCE_DIR}/" + f[17:] + if f.startswith("./"): + return os.path.join(current_dir, f) + if want_absolute_path and not os.path.isabs(f): + return os.path.join(current_dir, f) + return f + + +def map_source_to_cmake(source: str) -> typing.Optional[str]: + if not source or source == '$$NO_PCH_SOURCES': + return None + if source.startswith('$$PWD/'): + return source[6:] + if source == '.': + return "${CMAKE_CURRENT_SOURCE_DIR}" + if source.startswith('$$QT_SOURCE_TREE/'): + return "${PROJECT_SOURCE_DIR}/" + source[17:] + return source + + +def map_source_to_fs(base_dir: str, file: str, source: str) -> typing.Optional[str]: + if source is None or source == '$$NO_PCH_SOURCES': + return None + if source.startswith('$$PWD/'): + return os.path.join(os.path.dirname(file), source[6:]) + if source.startswith('$$QT_SOURCE_TREE/'): + return os.path.join('.', source[17:]) + if source.startswith('${PROJECT_SOURCE_DIR}/'): + return os.path.join('.', source[22:]) + if source.startswith('${CMAKE_CURRENT_SOURCE_DIR}/'): + return os.path.join(base_dir, source[28:]) + return os.path.join(base_dir, source) + + +class Scope: + def __init__(self, file: typing.Optional[str]=None, condition: str='', base_dir: str='') -> None: + self._parent = None # type: Scope + self._basedir = base_dir + if file: + self._currentdir = os.path.dirname(file) + if not self._currentdir: + self._currentdir = '.' + if not self._basedir: + self._basedir = self._currentdir + + self._file = file + self._condition = map_condition(condition) + self._children = [] # type: List[Scope] + self._values = {} # type: Dict[str, List[str]] + + def merge(self, other: 'Scope') -> None: + for c in other._children: + self.add_child(c) + other.set_basedir(self._basedir) + + for k in self._values.keys(): + self.append_value(k, other.get(k, [])) + + for k in other._values.keys(): + if k not in self._values: + self.set_value(k, other.get(k)) + + def set_basedir(self, dir: str) -> None: + self._basedir = dir + for c in self._children: + c.set_basedir(dir) + + def basedir(self) -> str: + return self._basedir + + def currentdir(self) -> str: + return self._currentdir + + @staticmethod + def FromDict(file: str, statements, cond: str = '', base_dir: str = ''): + scope = Scope(file, cond, base_dir) + for statement in statements: + if isinstance(statement, list): # Handle skipped parts... + assert not statement + continue + + operation = statement.get('operation', None) + if operation: + key = statement.get('key', '') + value = statement.get('value', []) + assert key != '' + + if key in ('HEADERS', 'SOURCES', 'INCLUDEPATH') or key.endswith('_HEADERS') or key.endswith('_SOURCES'): + value = [map_to_file(v, scope.basedir(), scope.currentdir()) for v in value] + + if operation == '=': + scope.set_value(key, value) + elif operation == '-=': + scope.substract_value(key, value) + elif operation == '+=' or operation == '*=': + scope.append_value(key, value) + else: + print('Unexpected operation "{}" in scope with condition {}.'.format(operation, cond)) + assert(False) + + continue + + condition = statement.get('condition', None) + if condition: + child = Scope.FromDict(file, statement.get('statements'), condition, scope.basedir()) + scope.add_child(child) + + else_statements = statement.get('else_statements') + if else_statements: + child = Scope.FromDict(file, else_statements, 'NOT ' + condition, scope.basedir()) + scope.add_child(child) + continue + + loaded = statement.get('loaded', None) + if loaded: + scope.append_value('_LOADED', loaded) + continue + + option = statement.get('option', None) + if option: + scope.append_value('_OPTION', option) + continue + + included = statement.get('included', None) + if included: + scope.append_value('_INCLUDED', + map_to_file(included, scope.basedir(), scope.currentdir())) + continue + + return scope + + def file(self) -> str: + return self._file or '' + + def cMakeListsFile(self) -> str: + return os.path.join(self.basedir(), 'CMakeLists.txt') + + def condition(self) -> str: + return self._condition + + def _push_down_TEMPLATE(self, template: str) -> None: + if not self._rawTemplate(): + self.set_value('TEMPLATE', [template, ]) + for c in self._children: + c._push_down_TEMPLATE(template) + + def add_child(self, scope: 'Scope') -> None: + scope._parent = self + if not scope._rawTemplate(): + scope._push_down_TEMPLATE(self.getTemplate()) + self._children.append(scope) + + def set_value(self, key: str, value: List[str]) -> None: + self._values[key] = value + + def append_value(self, key: str, value: Union[str, List[str]]) -> None: + array = self._values.get(key, []) + if isinstance(value, str): + array.append(value) + elif isinstance(value, list): + array += value + else: + assert False + self._values[key] = array + + def substract_value(self, key: str, value: Union[str, List[str]]) -> None: + if isinstance(value, str): + to_remove = [value, ] + if isinstance(value, list): + to_remove = value + + self.append_value(key, ['-{}'.format(v) for v in to_remove]) + + def children(self) -> List['Scope']: + return self._children + + def dump(self, *, indent: int = 0) -> None: + ind = ' ' * indent + if self._condition == '': + print('{}Scope {} in {}.'.format(ind, self._file, self._basedir)) + else: + print('{}Scope {} in {} with condition: {}.'.format(ind, self._file, self._basedir, self._condition)) + print('{}Keys:'.format(ind)) + for k in sorted(self._values.keys()): + print('{} {} = "{}"'.format(ind, k, self._values[k])) + print('{}Children:'.format(ind)) + for c in self._children: + c.dump(indent=indent + 1) + + def get(self, key: str, default=None) -> List[str]: + default = default or [] + return self._values.get(key, default) + + def getString(self, key: str, default: str = '') -> str: + v = self.get(key) + if isinstance(v, list): + if len(v) == 0: + return default + assert len(v) == 1 + return v[0] + elif isinstance(v, str): + return v + else: + assert False + return default + + def getTemplate(self) -> str: + return self.getString('TEMPLATE', 'app') + + def _rawTemplate(self) -> str: + return self.getString('TEMPLATE') + + def getTarget(self) -> str: + return self.getString('TARGET') or os.path.splitext(os.path.basename(self.file()))[0] + + +class QmakeParser: + def __init__(self, *, debug: bool = False) -> None: + self._Grammar = self._generate_grammar(debug) + + def _generate_grammar(self, debug: bool): + # Define grammar: + pp.ParserElement.setDefaultWhitespaceChars(' \t') + + LC = pp.Suppress(pp.Literal('\\') + pp.LineEnd()) + EOL = pp.Suppress(pp.Optional(pp.pythonStyleComment()) + pp.LineEnd()) + + Identifier = pp.Word(pp.alphas + '_', bodyChars=pp.alphanums+'_./') + 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(']')) + ))) + # Do not match word ending in '\' since that breaks line continuation:-/ + LiteralValuePart = pp.Word(pp.printables, excludeChars='$#{}()') + SubstitutionValue = pp.Combine(pp.OneOrMore(Substitution | LiteralValuePart | pp.Literal('$'))) + Value = (pp.QuotedString(quoteChar='"', escChar='\\') | SubstitutionValue) + + Values = pp.ZeroOrMore(Value)('value') + + Op = pp.Literal('=') | pp.Literal('-=') | pp.Literal('+=') | pp.Literal('*=') + + Operation = Identifier('key') + Op('operation') + Values('value') + Load = pp.Keyword('load') + pp.Suppress('(') + Identifier('loaded') + pp.Suppress(')') + Include = pp.Keyword('include') + pp.Suppress('(') + pp.CharsNotIn(':{=}#)\n')('included') + pp.Suppress(')') + Option = pp.Keyword('option') + pp.Suppress('(') + Identifier('option') + pp.Suppress(')') + DefineTest = pp.Suppress(pp.Keyword('defineTest') + pp.Suppress('(') + Identifier + pp.Suppress(')') + + pp.nestedExpr(opener='{', closer='}') + pp.LineEnd()) # ignore the whole thing... + FunctionCall = pp.Suppress(Identifier + pp.nestedExpr()) + + Scope = pp.Forward() + + Statement = pp.Group(Load | Include | Option | DefineTest | FunctionCall | Operation) + StatementLine = Statement + EOL + StatementGroup = pp.ZeroOrMore(Scope | EOL | StatementLine) + + Block = pp.Suppress('{') + pp.Optional(EOL) \ + + pp.ZeroOrMore(EOL | Statement + EOL | Scope) \ + + pp.Optional(Statement) + pp.Optional(EOL) \ + + pp.Suppress('}') + pp.Optional(EOL) + + Condition = pp.Optional(pp.White()) + pp.CharsNotIn(':{=}#\\\n') + Condition.setParseAction(lambda x: ' '.join(x).strip()) + + SingleLineScope = pp.Suppress(pp.Literal(':')) + pp.Group(Scope | Block | StatementLine)('statements') + MultiLineScope = Block('statements') + + SingleLineElse = pp.Suppress(pp.Literal(':')) + pp.Group(Scope | StatementLine)('else_statements') + MultiLineElse = pp.Group(Block)('else_statements') + Else = pp.Suppress(pp.Keyword('else')) + (SingleLineElse | MultiLineElse) + Scope <<= pp.Group(Condition('condition') + (SingleLineScope | MultiLineScope) + pp.Optional(Else)) + + if debug: + for ename in "EOL Identifier Substitution SubstitutionValue LiteralValuePart Value Values SingleLineScope MultiLineScope Scope SingleLineElse MultiLineElse Else Condition Block StatementGroup Statement Load Include Option DefineTest FunctionCall Operation".split(): + expr = locals()[ename] + expr.setName(ename) + expr.setDebug() + + Grammar = StatementGroup('statements') + Grammar.ignore(LC) + + return Grammar + + def parseFile(self, file: str): + print('Parsing \"{}\"...'.format(file)) + try: + result = self._Grammar.parseFile(file, parseAll=True) + except pp.ParseException as pe: + print(pe.line) + print(' '*(pe.col-1) + '^') + print(pe) + raise pe + return result + + +def parseProFile(file: str, *, debug=False): + parser = QmakeParser(debug=debug) + return parser.parseFile(file) + + +def map_condition(condition: str) -> str: + condition = condition.replace('!', 'NOT ') + condition = condition.replace('&&', ' AND ') + condition = condition.replace('|', ' OR ') + condition = condition.replace('==', ' STREQUAL ') + + 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: + part = 'QT_FEATURE_' + featureName(feature.group(2)) + else: + part = substitute_platform(part) + + part = part.replace('true', 'ON') + part = part.replace('false', 'OFF') + cmake_condition += ' ' + part + + return cmake_condition.strip() + + +def handle_subdir(scope: Scope, cm_fh: IO[str], *, indent: int = 0) -> None: + assert scope.getTemplate() == 'subdirs' + ind = ' ' * indent + for sd in scope.get('SUBDIRS', []): + full_sd = os.path.join(scope.basedir(), sd) + if os.path.isdir(full_sd): + cm_fh.write('{}add_subdirectory({})\n'.format(ind, sd)) + elif os.path.isfile(full_sd): + subdir_result = parseProFile(full_sd, debug=False) + subdir_scope = Scope.FromDict(full_sd, subdir_result.asDict().get('statements'), + '', scope.basedir()) + + cmakeify_scope(subdir_scope, cm_fh, indent=indent + 1) + elif sd.startswith('-'): + cm_fh.write('{}### remove_subdirectory("{}")\n'.format(ind, sd[1:])) + else: + print(' XXXX: SUBDIR {} in {}: Not found.'.format(sd, scope.file())) + + for c in scope.children(): + cond = c.condition() + if cond == 'else': + cm_fh.write('\n{}else()\n'.format(ind)) + elif cond: + cm_fh.write('\n{}if({})\n'.format(ind, cond)) + + handle_subdir(c, cm_fh, indent=indent + 1) + + if cond: + cm_fh.write('{}endif()\n'.format(ind)) + + +def sort_sources(sources) -> List[str]: + to_sort = {} # type: Dict[str, List[str]] + for s in sources: + if s is None: + continue + + dir = os.path.dirname(s) + base = os.path.splitext(os.path.basename(s))[0] + if base.endswith('_p'): + base = base[:-2] + sort_name = os.path.join(dir, base) + + array = to_sort.get(sort_name, []) + array.append(s) + + to_sort[sort_name] = array + + lines = [] + for k in sorted(to_sort.keys()): + lines.append(' '.join(sorted(to_sort[k]))) + + return lines + + +def write_header(cm_fh: IO[str], name: str, typename: str, *, indent: int=0): + cm_fh.write('{}#####################################################################\n'.format(spaces(indent))) + cm_fh.write('{}## {} {}:\n'.format(spaces(indent), name, typename)) + cm_fh.write('{}#####################################################################\n\n'.format(spaces(indent))) + + +def write_scope_header(cm_fh: IO[str], *, indent: int=0): + cm_fh.write('\n{}## Scopes:\n'.format(spaces(indent))) + cm_fh.write('{}#####################################################################\n'.format(spaces(indent))) + + +def write_sources_section(cm_fh: IO[str], scope: Scope, *, indent: int=0, + known_libraries=set()) -> None: + ind = spaces(indent) + + plugin_type = scope.get('PLUGIN_TYPE') + if plugin_type: + cm_fh.write('{} TYPE {}\n'.format(ind, plugin_type[0])) + + sources = scope.get('SOURCES') + scope.get('HEADERS') + scope.get('OBJECTIVE_SOURCES') + scope.get('NO_PCH_SOURCES') + scope.get('FORMS') + resources = scope.get('RESOURCES') + if resources: + qrc_only = True + for r in resources: + if not r.endswith('.qrc'): + qrc_only = False + break + + if not qrc_only: + print(' XXXX Ignoring non-QRC file resources.') + else: + sources += resources + + sources = [map_source_to_cmake(s) for s in sources] + if sources: + cm_fh.write('{} SOURCES\n'.format(ind)) + for l in sort_sources(sources): + cm_fh.write('{} {}\n'.format(ind, l)) + + if scope.get('DEFINES'): + cm_fh.write('{} DEFINES\n'.format(ind)) + for d in scope.get('DEFINES'): + d = d.replace('=\\\\\\"$$PWD/\\\\\\"', '="${CMAKE_CURRENT_SOURCE_DIR}/"') + cm_fh.write('{} {}\n'.format(ind, d)) + if scope.get('INCLUDEPATH'): + cm_fh.write('{} INCLUDE_DIRECTORIES\n'.format(ind)) + for i in scope.get('INCLUDEPATH'): + cm_fh.write('{} {}\n'.format(ind, i)) + + dependencies = [map_qt_library(q) for q in scope.get('QT') if map_qt_library(q) not in known_libraries] + dependencies += [map_qt_library(q) for q in scope.get('QT_FOR_PRIVATE') if map_qt_library(q) not in known_libraries] + dependencies += scope.get('QMAKE_USE_PRIVATE') + scope.get('LIBS_PRIVATE') + scope.get('LIBS') + if dependencies: + cm_fh.write('{} LIBRARIES\n'.format(ind)) + is_framework = False + for d in dependencies: + if d == '-framework': + is_framework = True + continue + if is_framework: + d = '${FW%s}' % d + if d.startswith('-l'): + d = d[2:] + d = substitute_libs(d) + cm_fh.write('{} {}\n'.format(ind, d)) + is_framework = False + + +def write_extend_target(cm_fh: IO[str], target: str, scope: Scope, parent_condition: str='', + previous_conditon: str='', *, indent: int=0) -> str: + total_condition = scope.condition() + if total_condition == 'else': + assert previous_conditon, "Else branch without previous condition in: %s" % scope.file() + total_condition = 'NOT ({})'.format(previous_conditon) + if parent_condition: + total_condition = '({}) AND ({})'.format(parent_condition, total_condition) + + extend_qt_io_string = io.StringIO() + write_sources_section(extend_qt_io_string, scope) + extend_qt_string = extend_qt_io_string.getvalue() + + extend_scope = '\n{}extend_target({} CONDITION {}\n{})\n'.format(spaces(indent), target, total_condition, extend_qt_string) + + if not extend_qt_string: + # Comment out the generated extend_target call because there no sources were found, but keep it commented + # for informational purposes. + extend_scope = ''.join(['#' + line for line in extend_scope.splitlines(keepends=True)]) + cm_fh.write(extend_scope) + + children = scope.children() + if children: + prev_condition = '' + for c in children: + prev_condition = write_extend_target(cm_fh, target, c, total_condition, prev_condition) + + return total_condition + + +def write_main_part(cm_fh: IO[str], name: str, typename: str, + cmake_function: str, scope: Scope, *, + extra_lines: typing.List[str] = [], + indent: int=0, + **kwargs: typing.Any): + write_header(cm_fh, name, typename, indent=indent) + + cm_fh.write('{}{}({}\n'.format(spaces(indent), cmake_function, name)) + for extra_line in extra_lines: + cm_fh.write('{} {}\n'.format(spaces(indent), extra_line)) + + write_sources_section(cm_fh, scope, indent=indent, **kwargs) + + # Footer: + cm_fh.write('{})\n'.format(spaces(indent))) + + # Scopes: + if not scope.children(): + return + + write_scope_header(cm_fh, indent=indent) + + for c in scope.children(): + write_extend_target(cm_fh, name, c, '', indent=indent) + + + +def write_module(cm_fh: IO[str], scope: Scope, *, indent: int=0) -> None: + module_name = scope.getTarget() + assert module_name.startswith('Qt') + + extra = [] + if 'static' in scope.get('CONFIG'): + extra.append('STATIC') + if 'no_module_headers' in scope.get('CONFIG'): + extra.append('NO_MODULE_HEADERS') + + write_main_part(cm_fh, module_name[2:], 'Module', 'add_qt_module', scope, + extra_lines=extra, indent=indent, known_libraries={'Qt::Core', }) + + + if 'qt_tracepoints' in scope.get('CONFIG'): + tracepoints = map_to_file(scope.getString('TRACEPOINT_PROVIDER'), scope.basedir(), scope.currentdir()) + cm_fh.write('\n\n{}qt_create_tracepoints({} {})\n'.format(spaces(indent), module_name[2:], tracepoints)) + + +def write_tool(cm_fh: IO[str], scope: Scope, *, indent: int=0) -> None: + tool_name = scope.getTarget() + + write_main_part(cm_fh, tool_name, 'Tool', 'add_qt_tool', scope, + indent=indent, known_libraries={'Qt::Core', }) + + +def write_test(cm_fh: IO[str], scope: Scope, *, indent: int=0) -> None: + test_name = scope.getTarget() + assert test_name + + write_main_part(cm_fh, test_name, 'Test', 'add_qt_test', scope, + indent=indent, known_libraries={'Qt::Core', 'Qt::Test', }) + + +def write_binary(cm_fh: IO[str], scope: Scope, gui: bool=False, *, indent: int=0) -> None: + binary_name = scope.getTarget() + assert binary_name + + extra = ['GUI',] if gui else [] + write_main_part(cm_fh, binary_name, 'Binary', 'add_qt_executable', scope, + extra_lines=extra, indent=indent, known_libraries={'Qt::Core', }) + + +def write_plugin(cm_fh, scope, *, indent: int=0): + plugin_name = scope.getTarget() + assert plugin_name + + write_main_part(cm_fh, plugin_name, 'Plugin', 'add_qt_plugin', scope, + indent=indent, known_libraries={'QtCore', }) + + +def handle_app_or_lib(scope: Scope, cm_fh: IO[str], *, indent=0) -> None: + assert scope.getTemplate() in ('app', 'lib', None) + + is_lib = scope.getTemplate() == 'lib' + is_plugin = any('qt_plugin' == s for s in scope.get('_LOADED', [])) + + if is_lib or 'qt_module' in scope.get('_LOADED', []): + write_module(cm_fh, scope, indent=indent) + elif is_plugin: + write_plugin(cm_fh, scope, indent=indent) + elif 'qt_tool' in scope.get('_LOADED', []): + write_tool(cm_fh, scope, indent=indent) + else: + if 'testcase' in scope.get('CONFIG') or 'testlib' in scope.get('CONFIG'): + write_test(cm_fh, scope, indent=indent) + else: + gui = 'console' not in scope.get('CONFIG') + write_binary(cm_fh, scope, gui, indent=indent) + + docs = scope.getString("QMAKE_DOCS") + if docs: + cm_fh.write("\n{}add_qt_docs({})\n".format(spaces(indent), map_to_file(docs, scope.basedir(), scope.currentdir()))) + + +def handle_qt_for_config(scope: Scope, cm_fh: IO[str], *, indent: int=0) -> None: + for config in scope.get("QT_FOR_CONFIG") or []: + lib = map_qt_library(config) + if lib.endswith("Private"): + cm_fh.write('{}qt_pull_features_into_current_scope(PRIVATE_FEATURES {})\n'.format(spaces(indent), lib[:-len("Private")])) + else: + cm_fh.write('{}qt_pull_features_into_current_scope(PUBLIC_FEATURES {})\n'.format(spaces(indent), lib)) + + +def cmakeify_scope(scope: Scope, cm_fh: IO[str], *, indent: int=0) -> None: + template = scope.getTemplate() + handle_qt_for_config(scope, cm_fh) + if template == 'subdirs': + handle_subdir(scope, cm_fh, indent=indent) + elif template in ('app', 'lib', None): + handle_app_or_lib(scope, cm_fh, indent=indent) + else: + print(' XXXX: {}: Template type {} not yet supported.' + .format(scope.file(), template)) + + +def generate_cmakelists(scope: Scope) -> None: + with open(scope.cMakeListsFile(), 'w') as cm_fh: + assert scope.file() + cm_fh.write('# Generated from {}.\n\n'.format(os.path.basename(scope.file()))) + cmakeify_scope(scope, cm_fh) + + +def do_include(scope: Scope, *, debug: bool=False) -> None: + for i in scope.get('_INCLUDED', []): + dir = scope.basedir() + include_file = map_to_file(i, dir, scope.currentdir(), want_absolute_path=True) + if not os.path.isfile(include_file): + print(' XXXX: Failed to include {}.'.format(include_file)) + continue + + include_result = parseProFile(include_file, debug=debug) + include_scope = Scope.FromDict(include_file, include_result.asDict().get('statements'), + '', dir) + + do_include(include_scope) + + scope.merge(include_scope) + + for c in scope.children(): + do_include(c) + + +def main() -> None: + args = _parse_commandline() + + for file in args.files: + parseresult = parseProFile(file, debug=args.debug_parser or args.debug) + + 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(file, parseresult.asDict().get('statements')) + + if args.debug_pro_structure or args.debug: + print('\n\n#### .pro/.pri file structure:') + print(file_scope.dump()) + print('\n#### End of .pro/.pri file structure.\n') + + do_include(file_scope) + + if args.debug_full_pro_structure or args.debug: + print('\n\n#### Full .pro/.pri file structure:') + print(file_scope.dump()) + print('\n#### End of full .pro/.pri file structure.\n') + + generate_cmakelists(file_scope) + + +if __name__ == '__main__': + main() diff --git a/util/cmake/run_pro2cmake.py b/util/cmake/run_pro2cmake.py new file mode 100755 index 0000000000..4340eab094 --- /dev/null +++ b/util/cmake/run_pro2cmake.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$ +## +############################################################################# + +import glob +import os +import subprocess +import sys + +script_path = os.path.dirname(os.path.abspath(__file__)) +base_path = os.path.dirname(script_path) +pro2cmake = script_path + '/pro2cmake.py' + +if len(sys.argv) > 1: + base_path = os.path.abspath(sys.argv[1]) + +for filename in glob.iglob(base_path + '/**/*.pro', recursive=True): + print('Converting:', filename) + subprocess.run([pro2cmake, filename]) 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/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/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/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/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/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/unset.pro b/util/cmake/tests/data/unset.pro new file mode 100644 index 0000000000..7ffb0582f1 --- /dev/null +++ b/util/cmake/tests/data/unset.pro @@ -0,0 +1,2 @@ +unset(f16c_cxx) + diff --git a/util/cmake/tests/test_parsing.py b/util/cmake/tests/test_parsing.py new file mode 100755 index 0000000000..78ac7fed26 --- /dev/null +++ b/util/cmake/tests/test_parsing.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +############################################################################# +## +## Copyright (C) 2018 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the plugins of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:GPL-EXCEPT$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 3 as published by the Free Software +## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +import os +from pro2cmake import QmakeParser + + +_tests_path = os.path.dirname(os.path.abspath(__file__)) + + +def validate_op(key, op, value, to_validate): + assert key == to_validate['key'] + assert op == to_validate['operation'] + assert value == to_validate['value'] + + +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).asDict() + assert len(result) == 1 + + return result['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_include(): + result = parse_file(_tests_path + '/data/include.pro') + assert len(result) == 3 + validate_op('A', '=', ['42'], result[0]) + include = result[1] + assert len(include) == 1 + assert include.get('included', '') == 'foo' + validate_op('B', '=', ['23'], result[2]) + +def test_load(): + result = parse_file(_tests_path + '/data/load.pro') + assert len(result) == 3 + validate_op('A', '=', ['42'], result[0]) + load = result[1] + assert len(load) == 1 + assert load.get('loaded', '') == 'foo' + validate_op('B', '=', ['23'], result[2]) + +def test_definetest(): + result = parse_file(_tests_path + '/data/definetest.pro') + assert len(result) == 1 + assert result[0] == [] + +def test_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 |