#!/usr/bin/env python3 # Copyright (C) 2018 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 # Requires Python 3.7. The import statement needs to be the first line of code # so it's not possible to conditionally check the version and raise an # exception. from __future__ import annotations import copy import os.path import posixpath import sys import re import io import itertools import glob import fnmatch 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 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.16" cmake_api_version = 3 def _parse_commandline(): parser = ArgumentParser( description="Generate CMakeLists.txt files from ." "pro files.", epilog="Requirements: pip install -r requirements.txt", ) 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 a Qt example.", ) parser.add_argument( "--is-user-project", action="store_true", dest="is_user_project", help="Treat the input .pro file as a user project.", ) 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, 2 or 3, 3 is latest.", ) parser.add_argument( "-o", "--output-file", dest="output_file", type=str, help="Specify a file path where the generated content should be written to. " "Default is to write to CMakeLists.txt in the same directory as the .pro file.", ) parser.add_argument( "files", metavar="<.pro/.pri file>", type=str, nargs="+", help="The .pro/.pri file to process", ) return parser.parse_args() def get_top_level_repo_project_path(project_file_path: str = "") -> str: qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path) return qmake_or_cmake_conf_dir_path def is_top_level_repo_project(project_file_path: str = "") -> bool: qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path) project_dir_path = os.path.dirname(project_file_path) return qmake_or_cmake_conf_dir_path == project_dir_path def is_top_level_repo_tests_project(project_file_path: str = "") -> bool: qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_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_or_cmake_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_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_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_or_cmake_conf_dir_path == normalized_maybe_same_level_dir_path and project_dir_name == "examples" ) def is_example_project(project_file_path: str = "") -> bool: # If there's a .qmake.conf or .cmake.conf file in the parent # directories of the given project path, it is likely that the # project is an internal Qt project that uses private Qt CMake # API. found_qt_repo_version = False qmake_conf = find_qmake_conf(project_file_path) if qmake_conf: repo_version = parse_qt_repo_module_version_from_qmake_conf(qmake_conf) if repo_version: found_qt_repo_version = True cmake_conf = find_cmake_conf(project_file_path) if cmake_conf: repo_version = parse_qt_repo_module_version_from_cmake_conf(cmake_conf) if repo_version: found_qt_repo_version = True # If we haven't found a conf file, we assume this is an example # project and not a project under a qt source repository. if not found_qt_repo_version: return True # 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. qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path) project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_conf_dir_path) is_example_under_repo_sources = ( project_relative_path.startswith("examples") and "3rdparty" not in project_relative_path ) return is_example_under_repo_sources def is_config_test_project(project_file_path: str = "") -> bool: qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path) dir_name_with_qmake_or_cmake_conf = os.path.basename(qmake_or_cmake_conf_dir_path) project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_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_or_cmake_conf == "config.tests" ) def is_benchmark_project(project_file_path: str = "") -> bool: qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path) project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_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_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path) project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_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_file_walking_parent_dirs(file_name: str, project_file_path: str = "") -> str: assert file_name if not os.path.isabs(project_file_path): print( f"Warning: could not find {file_name} file, given path is not an " f"absolute path: {project_file_path}" ) return "" cwd = os.path.dirname(project_file_path) while os.path.isdir(cwd): maybe_file = posixpath.join(cwd, file_name) if os.path.isfile(maybe_file): return maybe_file else: last_cwd = cwd cwd = os.path.dirname(cwd) if last_cwd == cwd: # reached the top level directory, stop looking break return "" def find_qmake_conf(project_file_path: str = "") -> str: return find_file_walking_parent_dirs(".qmake.conf", project_file_path) def find_cmake_conf(project_file_path: str = "") -> str: return find_file_walking_parent_dirs(".cmake.conf", project_file_path) def find_qmake_or_cmake_conf(project_file_path: str = "") -> str: qmake_conf = find_qmake_conf(project_file_path) if qmake_conf: return qmake_conf cmake_conf = find_cmake_conf(project_file_path) return cmake_conf def parse_qt_repo_module_version_from_qmake_conf(qmake_conf_path: str = "") -> str: with open(qmake_conf_path) as f: file_contents = f.read() m = re.search(r"MODULE_VERSION\s*=\s*([0-9.]+)", file_contents) return m.group(1) if m else "" def parse_qt_repo_module_version_from_cmake_conf(cmake_conf_path: str = "") -> str: with open(cmake_conf_path) as f: file_contents = f.read() m = re.search(r'set\(QT_REPO_MODULE_VERSION\s*"([0-9.]+)"\)', file_contents) return m.group(1) if m else "" 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[2]["qt_internal_add_app"] = "qt_internal_add_app" 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[1]["qt_add_3rdparty_library"] = "qt_add_3rdparty_library" api[1]["qt_create_tracepoints"] = "qt_create_tracepoints" api[2]["qt_extend_target"] = "qt_extend_target" api[2]["qt_add_module"] = "qt_add_module" api[2]["qt_add_plugin"] = "qt_internal_add_plugin" api[2]["qt_add_tool"] = "qt_add_tool" api[2]["qt_internal_add_app"] = "qt_internal_add_app" 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" api[2]["qt_add_3rdparty_library"] = "qt_add_3rdparty_library" api[2]["qt_create_tracepoints"] = "qt_create_tracepoints" api[3]["qt_extend_target"] = "qt_internal_extend_target" api[3]["qt_add_module"] = "qt_internal_add_module" api[3]["qt_add_plugin"] = "qt_internal_add_plugin" api[3]["qt_add_tool"] = "qt_internal_add_tool" api[3]["qt_internal_add_app"] = "qt_internal_add_app" api[3]["qt_add_test"] = "qt_internal_add_test" api[3]["qt_add_test_helper"] = "qt_internal_add_test_helper" api[3]["qt_add_manual_test"] = "qt_internal_add_manual_test" api[3]["qt_add_benchmark"] = "qt_internal_add_benchmark" api[3]["qt_add_executable"] = "qt_internal_add_executable" api[3]["qt_add_simd_part"] = "qt_internal_add_simd_part" api[3]["qt_add_docs"] = "qt_internal_add_docs" api[3]["qt_add_resource"] = "qt_internal_add_resource" api[3]["qt_add_qml_module"] = "qt_internal_add_qml_module" api[3]["qt_add_cmake_library"] = "qt_internal_add_cmake_library" api[3]["qt_add_3rdparty_library"] = "qt_internal_add_3rdparty_library" api[3]["qt_create_tracepoints"] = "qt_internal_create_tracepoints" 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() api_call_versions = [version for version in cmake_api_calls] api_call_versions = sorted(api_call_versions, reverse=True) api_call_version_matches = {} for version in api_call_versions: versioned_api_calls = [ cmake_api_calls[version][api_call] for api_call in cmake_api_calls[version] ] versioned_api_calls_alternatives = "|".join(versioned_api_calls) api_call_version_matches[version] = re.search( versioned_api_calls_alternatives, contents ) # If new style found, return latest api version. Otherwise # return the current version. for version in api_call_version_matches: if api_call_version_matches[version]: return version 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] class QtResource: def __init__( self, name: str = "", prefix: str = "", base_dir: str = "", files: Dict[str, str] = {}, lang: str = None, generated: bool = False, skip_qtquick_compiler: bool = False, ) -> None: self.name = name self.prefix = prefix self.base_dir = base_dir self.files = files self.lang = lang self.generated = generated self.skip_qtquick_compiler = skip_qtquick_compiler def read_qrc_file( filepath: str, base_dir: str = "", project_file_path: str = "", skip_qtquick_compiler: bool = False, ) -> List[QtResource]: # 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_or_cmake_conf = find_qmake_or_cmake_conf(project_file_path) if qmake_or_cmake_conf: qt_source_tree = os.path.dirname(qmake_or_cmake_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. 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" result: List[QtResource] = [] for resource in root: assert resource.tag == "qresource" r = QtResource( name=resource_name, prefix=resource.get("prefix", "/"), base_dir=base_dir, lang=resource.get("lang", ""), skip_qtquick_compiler=skip_qtquick_compiler, ) if len(result) > 0: r.name += str(len(result)) if not r.prefix.startswith("/"): r.prefix = f"/{r.prefix}" for file in resource: path = file.text assert path # Get alias: alias = file.get("alias", "") r.files[path] = alias result.append(r) return result def write_resource_source_file_properties( sorted_files: List[str], files: Dict[str, str], base_dir: str, skip_qtquick_compiler: bool ) -> str: output = "" source_file_properties = defaultdict(list) for source in sorted_files: alias = files[source] if alias: source_file_properties[source].append(f'QT_RESOURCE_ALIAS "{alias}"') # If a base dir is given, we have to write the source file property # assignments that disable the quick compiler per file. if base_dir and skip_qtquick_compiler: source_file_properties[source].append("QT_SKIP_QUICKCOMPILER 1") for full_source in source_file_properties: per_file_props = source_file_properties[full_source] if per_file_props: prop_spaces = " " per_file_props_joined = f"\n{prop_spaces}".join(per_file_props) output += dedent( f"""\ set_source_files_properties("{full_source}" PROPERTIES {per_file_props_joined} ) """ ) return output def write_add_qt_resource_call( target: str, scope: Scope, resource_name: str, prefix: Optional[str], base_dir: str, lang: Optional[str], files: Dict[str, str], skip_qtquick_compiler: bool, is_example: bool, ) -> str: output = "" if base_dir: base_dir_expanded = scope.expandString(base_dir) if base_dir_expanded: base_dir = base_dir_expanded new_files = {} for file_path, alias in files.items(): full_file_path = posixpath.join(base_dir, file_path) new_files[full_file_path] = alias files = new_files sorted_files = sorted(files.keys()) assert sorted_files output += write_resource_source_file_properties( sorted_files, files, base_dir, skip_qtquick_compiler ) # 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 and not base_dir: output += ( f"set_source_files_properties(${{{resource_name}_resource_files}}" " PROPERTIES QT_SKIP_QUICKCOMPILER 1)\n\n" ) prefix_expanded = scope.expandString(str(prefix)) if prefix_expanded: prefix = prefix_expanded 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' 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.versions = "" 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_optional = False self.plugin_path = "" self.classname = "" self.imports: List[str] = [] self.optional_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) optional_imports_line = " \n".join(self.optional_imports) string = f"""\ module: {self.module} plugin: {self.plugin_optional} {self.plugin_name} {self.plugin_path} classname: {self.classname} type_infos:{type_infos_line} imports:{imports_line} optional_imports:{optional_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"versions:{file_info.versions} " 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) # If this is not the first version we've found, # append ';' to delineate the next version; e.g.: "2.0;2.6" if qmldir_file.versions: qmldir_file.versions += ";" qmldir_file.versions += 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] == "optional": if entries[1] == "plugin": self.plugin_name = entries[2] self.plugin_optional = True if len(entries) > 3: self.plugin_path = entries[3] elif entries[1] == "import": if len(entries) == 4: self.optional_imports.append(entries[2] + "/" + entries[3]) else: self.optional_imports.append(entries[2]) else: raise RuntimeError("Only plugins and imports can be optional in qmldir files") 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": if len(entries) == 3: self.imports.append(entries[1] + "/" + entries[2]) else: 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 "" if not isinstance(self._value, list): return "" result = [] for i in self._value: if not i: result.append("") 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._including_scope = None # type: Optional[Scope] self._visited_keys = set() # type: Set[str] self._total_condition = None # type: Optional[str] self._parent_include_line_no = parent_include_line_no self._is_public_module = False self._has_private_module = False self._is_internal_qt_app = False def __repr__(self): return ( f"{self._scope_id}:{self._basedir}:{self._currentdir}:{self._file}:" f"{self._condition or ''}" ) def reset_visited_keys(self): self._visited_keys = set() def merge(self, other: "Scope") -> None: assert self != other other._including_scope = self 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 including_scope(self) -> Optional[Scope]: return self._including_scope @property def basedir(self) -> str: return self._basedir @property def currentdir(self) -> str: return self._currentdir @property def is_public_module(self) -> bool: return self._is_public_module @property def has_private_module(self) -> bool: return self._has_private_module @property def is_internal_qt_app(self) -> bool: is_app = self._is_internal_qt_app current_scope = self while not is_app and current_scope.parent: current_scope = current_scope.parent is_app = current_scope.is_internal_qt_app return is_app 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)) qt_no_make_tools = statement.get("qt_no_make_tools_arguments") if qt_no_make_tools: qt_no_make_tools = qt_no_make_tools.strip("()").strip() qt_no_make_tools = qt_no_make_tools.split() for entry in qt_no_make_tools: scope._append_operation("_QT_NO_MAKE_TOOLS", AddOperation(entry)) scope.settle_condition() if scope.scope_debug: print(f"..... [SCOPE_DEBUG]: Created scope {scope}:") scope.dump(indent=1) print("..... [SCOPE_DEBUG]: <>") 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 = posixpath.relpath(self.currentdir, self.basedir) if key == "QQC2_SOURCE_TREE": qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(os.path.abspath(self.currentdir)) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path) project_relative_path = os.path.relpath(qmake_or_cmake_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}"] # Horrible hack. If we're returning the values for some key # that looks like source or header files, make sure to use a # map_files transformer, so that $$PWD values are evaluated # in the transformer scope, otherwise relative paths will be # broken. # Looking at you qmltyperegistrar.pro. eval_ops_transformer = None if key.endswith("SOURCES") or key.endswith("HEADERS"): def file_transformer(scope, files): return scope._map_files(files) eval_ops_transformer = file_transformer return self._evalOps(key, eval_ops_transformer, [], 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 if len(v) > 1: return " ".join(v) 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_unused, 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"MACOS", 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) def qt_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" operator_prefix = "VERSION_" version_variable = "QT_VERSION" version_flavor = match_obj.group(2) if version_flavor: version_variable += "_" + version_flavor[:-1] operator_prefix = "" version = match_obj.group(3) return f"({version_variable} {operator_prefix}{operator} {version})" pattern = r"(equals|greaterThan|lessThan)\(QT_(MAJOR_|MINOR_|PATCH_)?VERSION,[ ]*([0-9.]+)\)" condition = re.sub(pattern, qt_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 = re.sub(r"CONFIG___contains___largefile", r"QT_FEATURE_largefile", 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:]) and not feature_name.startswith("system_jpeg") and not feature_name.startswith("system_zlib") and not feature_name.startswith("system_tiff") and not feature_name.startswith("system_assimp") and not feature_name.startswith("system_doubleconversion") and not feature_name.startswith("system_sqlite") and not feature_name.startswith("system_hunspell") and not feature_name.startswith("system_libb2") and not feature_name.startswith("system_webp") ): 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}", "$$MODULE_BASE_OUTDIR": "${QT_BUILD_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, is_user_project: 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, is_user_project=is_user_project, ) 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 ) # Make sure to exclude targets within subdirectories first. qt_no_make_tools = scope.get("_QT_NO_MAKE_TOOLS") if qt_no_make_tools: ind = spaces(indent + 1) directories_string = "" for directory in qt_no_make_tools: directories_string += f"{ind}{directory}\n" cm_fh.write( f"\nqt_exclude_tool_directories_from_default_target(\n{directories_string})\n\n" ) # Then write the subdirectories. 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"]: for lib in scope.expand(key): mapped_lib = map_qt_library(lib) 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 ] # Handle LIBS_SUFFIX='\\"_$${QT_ARCH}.so\\"'. # The escaping of backslashes is still needed even if it's a raw # string, because backslashes have a special meaning for regular # expressions (escape next char). So we actually expect to match # 2 backslashes in the input string. pattern = r"""([^ ]+)='\\\\"([^ ]*)\\\\"'""" # Replace with regular quotes, CMake will escape the quotes when # passing the define to the compiler. replacement = r'\1="\2"' defines = [re.sub(pattern, replacement, d) 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_3rd_party_defines( cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = "" ): defines = scope.expand("MODULE_DEFINES") write_list(cm_fh, defines, cmake_parameter, indent, footer=footer) def get_include_paths_helper(scope: Scope, include_var_name: str) -> List[str]: includes = [i.rstrip("/") or ("/") for i in scope.get_files(include_var_name)] return includes def write_include_paths( cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = "" ): includes = get_include_paths_helper(scope, "INCLUDEPATH") write_list(cm_fh, includes, cmake_parameter, indent, footer=footer) def write_3rd_party_include_paths( cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = "" ): # Used in qt_helper_lib.prf. includes = get_include_paths_helper(scope, "MODULE_INCLUDEPATH") # Wrap the includes in BUILD_INTERFACE generator expression, because # the include paths point to a source dir, and CMake will error out # when trying to create consumable exported targets. processed_includes = [] for i in includes: # CMake generator expressions don't seem to like relative paths. # Make them absolute relative to the source dir. if is_path_relative_ish(i): i = f"${{CMAKE_CURRENT_SOURCE_DIR}}/{i}" i = f"$" processed_includes.append(i) write_list(cm_fh, processed_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) # Return True if given scope belongs to a public module. # First, traverse the parent/child hierarchy. Then, traverse the include hierarchy. def recursive_is_public_module(scope: Scope): if scope.is_public_module: return True if scope.parent: return recursive_is_public_module(scope.parent) if scope.including_scope: return recursive_is_public_module(scope.including_scope) return False 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 ) is_public_module = recursive_is_public_module(scope) # When handling module dependencies, handle QT += foo-private magic. # This implies: # target_link_libraries(Module PUBLIC Qt::Foo) # target_link_libraries(Module PRIVATE Qt::FooPrivate) # target_link_libraries(ModulePrivate INTERFACE Qt::FooPrivate) if is_public_module: private_module_dep_pattern = re.compile(r"^(Qt::(.+))Private$") public_module_public_deps = [] public_module_private_deps = private_dependencies private_module_interface_deps = [] for dep in public_dependencies: match = re.match(private_module_dep_pattern, dep) if match: if match[1] not in public_module_public_deps: public_module_public_deps.append(match[1]) private_module_interface_deps.append(dep) if dep not in public_module_private_deps: public_module_private_deps.append(dep) else: if dep not in public_module_public_deps: public_module_public_deps.append(dep) private_module_interface_deps.extend( [map_qt_library(q) for q in scope.expand("QT_FOR_PRIVATE")] ) private_module_interface_deps.extend( _map_libraries_to_cmake(scope.expand("QMAKE_USE_FOR_PRIVATE"), known_libraries) ) write_list(cm_fh, public_module_private_deps, "LIBRARIES", indent + 1) write_list(cm_fh, public_module_public_deps, "PUBLIC_LIBRARIES", indent + 1) write_list(cm_fh, private_module_interface_deps, "PRIVATE_MODULE_INTERFACE", indent + 1) else: 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_3rd_party_defines(cm_fh, scope, "PUBLIC_DEFINES", indent=indent + 1) write_include_paths(cm_fh, scope, "INCLUDE_DIRECTORIES", indent=indent + 1) write_3rd_party_include_paths(cm_fh, scope, "PUBLIC_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 = "" 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 extract_resources( target: str, scope: Scope, ) -> Tuple[List[QtResource], List[str]]: """Read the resources of the given scope. Return a tuple: - list of QtResource objects - list of standalone sources files that are marked as QTQUICK_COMPILER_SKIPPED_RESOURCES""" resource_infos: List[QtResource] = [] skipped_standalone_files: List[str] = [] resources = scope.get_files("RESOURCES") qtquickcompiler_skipped = scope.get_files("QTQUICK_COMPILER_SKIPPED_RESOURCES") if resources: standalone_files: List[str] = [] for r in resources: skip_qtquick_compiler = r in qtquickcompiler_skipped if r.endswith(".qrc"): if "${CMAKE_CURRENT_BINARY_DIR}" in r: resource_infos.append( QtResource( name=r, generated=True, skip_qtquick_compiler=skip_qtquick_compiler ) ) continue resource_infos += read_qrc_file( r, scope.basedir, scope.file_absolute_path, skip_qtquick_compiler=skip_qtquick_compiler, ) else: immediate_files = {f: "" for f in scope.get_files(f"{r}.files")} if immediate_files: immediate_files_filtered = [] for f in immediate_files: 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 ), "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}" resource_infos.append( QtResource( name=immediate_name, prefix=immediate_prefix, base_dir=immediate_base, lang=immediate_lang, files=immediate_files, skip_qtquick_compiler=skip_qtquick_compiler, ) ) else: standalone_files.append(r) if not ("*" in r) and skip_qtquick_compiler: skipped_standalone_files.append(r) if standalone_files: resource_infos.append( QtResource( name="qmake_immediate", prefix="/", base_dir="", files={f: "" for f in standalone_files}, ) ) return (resource_infos, skipped_standalone_files) def write_resources( cm_fh: IO[str], target: str, scope: Scope, indent: int = 0, is_example=False, target_ref: str = None, resources: List[QtResource] = None, skipped_standalone_files: List[str] = None, ): if resources is None: (resources, skipped_standalone_files) = extract_resources(target, scope) if target_ref is None: target_ref = target qrc_output = "" for r in resources: name = r.name if "*" in name: name = expand_resource_glob(cm_fh, name) qrc_output += write_add_qt_resource_call( target=target_ref, scope=scope, resource_name=name, prefix=r.prefix, base_dir=r.base_dir, lang=r.lang, files=r.files, skip_qtquick_compiler=r.skip_qtquick_compiler, is_example=is_example, ) if skipped_standalone_files: for f in skipped_standalone_files: qrc_output += ( f'set_source_files_properties("{f}" PROPERTIES ' f"QT_SKIP_QUICKCOMPILER 1)\n\n" ) 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("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 write_generic_cmake_command( cm_fh: IO[str], command_name: str, arguments: List[str], indent: int = 0 ): ind = spaces(indent) arguments_str = " ".join(arguments) cm_fh.write(f"{ind}{command_name}({arguments_str})\n") def write_set_target_properties( cm_fh: IO[str], targets: List[str], properties: List[str], indent: int = 0 ): ind = spaces(indent) command_name = "set_target_properties" arguments_ind = spaces(indent + 1) prop_pairs = [(properties[i] + " " + properties[i + 1]) for i in range(0, len(properties), 2)] properties_str = f"\n{arguments_ind}" + f"\n{arguments_ind}".join(prop_pairs) if len(targets) == 1: targets_str = targets[0] + " " else: targets_str = ( f"\n{arguments_ind}" + f"\n{arguments_ind}".join(targets) + f"\n{arguments_ind}" ) cm_fh.write(f"{ind}{command_name}({targets_str}PROPERTIES{properties_str}\n{ind})\n") def write_set_source_files_properties( cm_fh: IO[str], files: List[str], properties: List[str], indent: int = 0 ): ind = spaces(indent) command_name = "set_source_files_properties" arguments_ind = spaces(indent + 1) prop_pairs = [(properties[i] + " " + properties[i + 1]) for i in range(0, len(properties), 2)] properties_str = f"\n{arguments_ind}" + f"\n{arguments_ind}".join(prop_pairs) if len(files) == 1: targets_str = files[0] + " " else: targets_str = f"\n{arguments_ind}" + f"\n{arguments_ind}".join(files) + f"\n{arguments_ind}" cm_fh.write(f"{ind}{command_name}({targets_str}PROPERTIES{properties_str}\n{ind})\n") def write_target_sources( cm_fh: IO[str], target: str, sources: List[str], visibility: str = "PRIVATE", indent: int = 0 ): command_name = "target_sources" header = f"{command_name}({target} {visibility}\n" write_list(cm_fh, sources, "", indent, footer=")", header=header) 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, target_ref: str = None ): if target_ref is None: target_ref = target 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_ref} 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, target_ref=target_ref) 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_reduce_relocations_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0): ind = spaces(indent) dynlist_file = scope.get_files("QMAKE_DYNAMIC_LIST_FILE") if dynlist_file: dynlist_path = "${CMAKE_CURRENT_LIST_DIR}/" + dynlist_file[0] cm_fh.write(f"{ind}if(QT_FEATURE_reduce_relocations AND UNIX AND GCC)\n") ind = spaces(indent + 1) cm_fh.write(f"{ind}target_link_options({target} PRIVATE\n") cm_fh.write(f'{ind} "LINKER:--dynamic-list={dynlist_path}")\n') ind = spaces(indent) cm_fh.write(f"{ind}endif()\n") 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, indent = write_scope_condition_begin(cm_fh, scope, indent=indent) 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") write_scope_condition_end(cm_fh, condition, indent=indent) def write_scope_condition_begin(cm_fh: IO[str], scope: Scope, indent: int = 0) -> Tuple[str, int]: 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 return condition, indent def write_scope_condition_end(cm_fh: IO[str], condition: str, indent: int = 0) -> int: if condition != "ON": indent -= 1 cm_fh.write(f"{spaces(indent)}endif()\n") return indent def is_path_relative_ish(path: str) -> bool: if not os.path.isabs(path) and not path.startswith("$"): return True return False def absolutify_path(path: str, base_dir: str = "${CMAKE_CURRENT_SOURCE_DIR}") -> str: if not path: return path if is_path_relative_ish(path): path = posixpath.join(base_dir, path) return path def write_version_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0): if scope.is_internal_qt_app: version_value = scope.get_string("VERSION") if version_value: version_value = re.sub(r"\$\${QT_VERSION\}", "${PROJECT_VERSION}", version_value) target_description = scope.expandString("QMAKE_TARGET_DESCRIPTION") if version_value or target_description: condition, indent = write_scope_condition_begin(cm_fh, scope, indent=indent) properties = [] if version_value: properties.extend(["QT_TARGET_VERSION", f'"{version_value}"']) if target_description: properties.extend(["QT_TARGET_DESCRIPTION", f'"{target_description}"']) if properties: write_set_target_properties(cm_fh, [target], properties, indent=indent) write_scope_condition_end(cm_fh, condition, indent=indent) def write_darwin_part( cm_fh: IO[str], target: str, scope: Scope, main_scope_target_name: str = "", indent: int = 0 ): if scope.is_internal_qt_app: # Embed custom provided Info.plist file. info_plist = scope.expandString("QMAKE_INFO_PLIST") info_plist = absolutify_path(info_plist) icon_path = scope.expandString("ICON") icon_basename = "" new_output_name = None current_scope_output_name = scope.TARGET if current_scope_output_name != main_scope_target_name: new_output_name = current_scope_output_name if icon_path: icon_basename = os.path.basename(icon_path) if info_plist or icon_path or new_output_name: condition, indent = write_scope_condition_begin(cm_fh, scope, indent=indent) properties = [] if info_plist: properties.extend(["MACOSX_BUNDLE_INFO_PLIST", f'"{info_plist}"']) properties.extend(["MACOSX_BUNDLE", "TRUE"]) if icon_path: properties.extend(["MACOSX_BUNDLE_ICON_FILE", f'"{icon_basename}"']) if new_output_name: properties.extend(["OUTPUT_NAME", f'"{new_output_name}"']) if properties: write_set_target_properties(cm_fh, [target], properties, indent=indent) if icon_path: source_properties = ["MACOSX_PACKAGE_LOCATION", "Resources"] write_set_source_files_properties( cm_fh, [icon_path], source_properties, indent=indent ) write_target_sources(cm_fh, target, [icon_path], indent=indent) write_scope_condition_end(cm_fh, condition, indent=indent) def write_windows_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0): if scope.is_internal_qt_app: # Handle CONFIG += console assignments. is_console = "console" in scope.get("CONFIG") rc_file = scope.expandString("RC_FILE") rc_file = absolutify_path(rc_file) rc_icons = scope.expandString("RC_ICONS") rc_icons = absolutify_path(rc_icons) if is_console or rc_file or rc_icons: condition, indent = write_scope_condition_begin(cm_fh, scope, indent=indent) properties = [] if is_console: properties.extend(["WIN32_EXECUTABLE", "FALSE"]) if rc_file: properties.extend(["QT_TARGET_WINDOWS_RC_FILE", f'"{rc_file}"']) if rc_icons: properties.extend(["QT_TARGET_RC_ICONS", f'"{rc_icons}"']) if properties: write_set_target_properties(cm_fh, [target], properties, indent=indent) write_scope_condition_end(cm_fh, condition, indent=indent) def write_aux_qml_file_install_call(cm_fh: IO[str], file_list: List[str], indent: int = 0): cm_fh.write(f"\n{spaces(indent)}qt_copy_or_install(\n") write_list(cm_fh, file_list, "FILES", indent + 1) destination_option = 'DESTINATION "${__aux_qml_files_install_dir}"' cm_fh.write(f"{spaces(indent + 1)}{destination_option})\n") def write_aux_qml_path_setup(cm_fh: IO[str], base_dir: str, indent: int = 0): path_join_args = f'__aux_qml_files_install_dir "${{__aux_qml_files_install_base}}" "{base_dir}"' cm_fh.write(f"\n{spaces(indent)}qt_path_join({path_join_args})\n") def write_aux_qml_files_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0): aux_files = scope.get_files("AUX_QML_FILES") if aux_files and isinstance(aux_files, list): aux_files_per_dir = defaultdict(list) aux_files_globs = [] # Handle globs differently from regular paths. # For regular paths, group by base dir. Each base dir will get # its own install call. for path in aux_files: if "*" in path: aux_files_globs.append(path) else: base_dir = os.path.dirname(path) aux_files_per_dir[base_dir].append(path) condition, indent = write_scope_condition_begin(cm_fh, scope, indent=indent) # Extract the location of $prefix/qml, where we want to install # files. get_prop_args = f"__aux_qml_files_install_base {target} QT_QML_MODULE_INSTALL_DIR" cm_fh.write(f"{spaces(indent)}get_target_property({get_prop_args})\n") # Handle glob installs. for path in aux_files_globs: cm_fh.write( f""" {spaces(indent)}file(GLOB_RECURSE __aux_qml_glob_files {spaces(indent + 1)}RELATIVE "${{CMAKE_CURRENT_SOURCE_DIR}}" {spaces(indent + 1)}"{path}")""" ) file_list = ["${__aux_qml_glob_files}"] # Extract base dir. Hopes that the globs only appear in the # file name part. base_dir = os.path.dirname(path) write_aux_qml_path_setup(cm_fh, base_dir, indent=indent) write_aux_qml_file_install_call(cm_fh, file_list, indent=indent) # Handle regular per base-dir installs. for base_dir in aux_files_per_dir: file_list = aux_files_per_dir[base_dir] write_aux_qml_path_setup(cm_fh, base_dir, indent=indent) write_aux_qml_file_install_call(cm_fh, file_list, indent=indent) write_scope_condition_end(cm_fh, condition, indent=indent) 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": cm_fh.write(f"{spaces(indent)}if (NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)\n") cm_fh.write(f"{spaces(indent+1)}cmake_minimum_required(VERSION 3.16)\n") cm_fh.write(f"{spaces(indent+1)}project({name} LANGUAGES C CXX ASM)\n") cm_fh.write( f"{spaces(indent+1)}find_package(Qt6BuildInternals COMPONENTS STANDALONE_TEST)\n" ) cm_fh.write(f"{spaces(indent)}endif()\n\n") 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") target_ref = name if typename == "Tool": target_ref = "${target_name}" cm_fh.write(f"{spaces(indent)}qt_get_tool_target_name(target_name {name})\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}({target_ref}\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") if typename == "Tool": cm_fh.write(f"{spaces(indent)}qt_internal_return_unless_building_tools()\n") write_resources(cm_fh, name, scope, indent, target_ref=target_ref) 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_reduce_relocations_part(cm_fh, name, scope, indent) write_android_part(cm_fh, name, scopes[0], indent) write_wayland_part(cm_fh, name, scopes[0], indent) write_windows_part(cm_fh, name, scopes[0], indent) write_darwin_part(cm_fh, name, scopes[0], main_scope_target_name=name, indent=indent) write_version_part(cm_fh, name, scopes[0], indent) write_aux_qml_files_part(cm_fh, name, scopes[0], indent) if "warn_off" in scope.get("CONFIG"): write_generic_cmake_command(cm_fh, "qt_disable_warnings", [name]) if "hide_symbols" in scope.get("CONFIG"): write_generic_cmake_command(cm_fh, "qt_set_symbol_visibility_hidden", [name]) 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_windows_part(cm_fh, name, c, indent=indent) write_darwin_part(cm_fh, name, c, main_scope_target_name=name, indent=indent) write_version_part(cm_fh, name, c, indent=indent) write_aux_qml_files_part(cm_fh, name, c, indent=indent) write_extend_target(cm_fh, name, c, target_ref=target_ref, 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_3rdparty_library(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str: # Remove default QT libs. scope._append_operation("QT", RemoveOperation(["core", "gui"])) target_name = re.sub(r"^qt", "", scope.TARGET) target_name = target_name.replace("-", "_") qmake_lib_name = target_name # Capitalize the first letter for a nicer name. target_name = target_name.title() # Prefix with Bundled, to avoid possible duplicate target names # e.g. "BundledFreetype" instead of "freetype". target_name = f"Bundled{target_name}" if "dll" in scope.get("CONFIG"): library_type = "SHARED" else: library_type = "STATIC" extra_lines = [f"QMAKE_LIB_NAME {qmake_lib_name}"] if library_type: extra_lines.append(library_type) if "installed" in scope.get("CONFIG"): extra_lines.append("INSTALL") write_main_part( cm_fh, target_name, "Generic Library", get_cmake_api_call("qt_add_3rdparty_library"), scope, extra_lines=extra_lines, indent=indent, known_libraries={}, extra_keys=[], ) return target_name 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 forward_target_info(scope: Scope, extra: List[str], skip: Optional[Dict[str, bool]] = None): s = scope.get_string("QMAKE_TARGET_PRODUCT") if s: extra.append(f'TARGET_PRODUCT "{s}"') s = scope.get_string("QMAKE_TARGET_DESCRIPTION") if s and (not skip or "QMAKE_TARGET_DESCRIPTION" not in skip): extra.append(f'TARGET_DESCRIPTION "{s}"') s = scope.get_string("QMAKE_TARGET_COMPANY") if s: extra.append(f'TARGET_COMPANY "{s}"') s = scope.get_string("QMAKE_TARGET_COPYRIGHT") if s: extra.append(f'TARGET_COPYRIGHT "{s}"') def write_module(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str: # e.g. QtCore qt_module_name = scope.TARGET if not qt_module_name.startswith("Qt"): print(f"XXXXXX Module name {qt_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") is_public_module = True # CMake target name as passed to qt_internal_add_module() # e.g. Core cmake_target_name = qt_module_name[2:] # MODULE is used for the name of the generated .pri file. # If MODULE is not explicitly set, qmake computes its value in # mkspecs/features/qt_build_config.prf module_name_for_pri = scope.expandString("MODULE") if not module_name_for_pri: module_name_for_pri_as_qmake_computes_it = scope.file[:-4] module_name_for_pri = module_name_for_pri_as_qmake_computes_it # Given 'qt_internal_add_module(Core)', computes 'core'. module_name_for_pri_as_cmake_computes_it = cmake_target_name.lower() if module_name_for_pri != module_name_for_pri_as_cmake_computes_it: extra.append(f"CONFIG_MODULE_NAME {module_name_for_pri}") if is_static: extra.append("STATIC") if "internal_module" in scope.get("CONFIG"): is_public_module = False cmake_target_name += "Private" # Assume all internal modules have the 'Private' suffix 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") else: scope._has_private_module = True if "header_module" in scope.get("CONFIG"): extra.append("HEADER_MODULE") if not("metatypes" in scope.get("CONFIG") or "qmltypes" in scope.get("CONFIG")): extra.append("NO_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)}") scope._is_public_module = is_public_module forward_target_info(scope, extra) write_main_part( cm_fh, cmake_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") create_trace_points = get_cmake_api_call("qt_create_tracepoints") cm_fh.write( f"\n\n{spaces(indent)}{create_trace_points}({cmake_target_name} {' '.join(tracepoints)})\n" ) return cmake_target_name def write_tool(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> Tuple[str, 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 = [] forward_target_info(scope, 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, "${target_name}" def write_qt_app(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str: app_name = scope.TARGET extra: List[str] = [] target_info_skip = {} target_info_skip["QMAKE_TARGET_DESCRIPTION"] = True forward_target_info(scope, extra, target_info_skip) write_main_part( cm_fh, app_name, "App", get_cmake_api_call("qt_internal_add_app"), scope, indent=indent, known_libraries={"Qt::Core"}, extra_lines=extra, extra_keys=["CONFIG"], ) return app_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, is_required: bool = True, end_with_extra_newline: bool = True, qt_package_name: str = "Qt6", ): packages = [] # type: List[LibraryMapping] all_libs = public_libs + private_libs for one_lib in all_libs: info = find_library_info_for_target(one_lib) if info and info not in packages: packages.append(info) qt_components: List[str] = [] for p in filter(LibraryMapping.is_qt, packages): if p.components is not None: qt_components += p.components if qt_components: if "Core" in qt_components: qt_components.remove("Core") qt_components = sorted(qt_components) qt_package = LibraryMapping("unknown", qt_package_name, "unknown", components=qt_components) if is_required: qt_package.extra = ["REQUIRED"] cm_fh.write( generate_find_package_info( qt_package, use_qt_find_package=False, remove_REQUIRED_from_extra=False, components_required=is_required, indent=indent, ) ) for p in itertools.filterfalse(LibraryMapping.is_qt, packages): cm_fh.write(generate_find_package_info(p, use_qt_find_package=False, indent=indent)) if packages and end_with_extra_newline: 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="set(java_sources\n", footer=")\n" ) cm_fh.write(f"{spaces(indent)}qt_internal_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+1)}OUTPUT_DIR "${{QT_BUILD_DIR}}/{install_dir}"\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 get_win32_and_mac_bundle_properties(scope: Scope) -> tuple: config = scope.get("CONFIG") win32 = all(val not in config for val in ["cmdline", "console"]) mac_bundle = all(val not in config for val in ["cmdline", "-app_bundle"]) return win32, mac_bundle def write_win32_and_mac_bundle_properties( cm_fh: IO[str], scope: Scope, target: str, *, handling_first_scope=False, indent: int = 0 ): win32, mac_bundle = get_win32_and_mac_bundle_properties(scope) true_value = "TRUE" false_value = "FALSE" properties_mapping = { "WIN32_EXECUTABLE": true_value if win32 else false_value, "MACOSX_BUNDLE": true_value if mac_bundle else false_value, } properties = [] # Always write the properties for the first scope. # For conditional scopes, only write them if the value is different # from the default value (aka different from TRUE). # This is a heurestic that should cover 90% of the example projects # without creating excess noise of setting the properties in every # single scope. for name, value in properties_mapping.items(): if not handling_first_scope and value != true_value: properties.extend([name, value]) if properties: write_set_target_properties(cm_fh, [target], properties, indent=indent) def is_qtquick_source_file(filename: str): return filename.endswith(".qml") or filename.endswith(".js") or filename.endswith(".mjs") def looks_like_qml_resource(resource: QtResource): if resource.generated or "*" in resource.name: return False for f in resource.files: if is_qtquick_source_file(f): return True return False def find_qml_resource(resources: List[QtResource]): """Return the resource object that's most likely the one that should be used for qt_add_qml_module. Return None if there's no such resource.""" return next(filter(looks_like_qml_resource, resources), None) def write_example( cm_fh: IO[str], scope: Scope, gui: bool = False, *, indent: int = 0, is_plugin: bool = False, is_user_project: bool = False, ) -> str: binary_name = scope.TARGET assert binary_name config = scope.get("CONFIG") is_qml_plugin = ("qml" in scope.get("QT")) or "qmltypes" in config if not is_user_project: example_install_dir = scope.expandString("target.path") if not example_install_dir: example_install_dir = "${INSTALL_EXAMPLESDIR}" example_install_dir = example_install_dir.replace( "$$[QT_INSTALL_EXAMPLES]", "${INSTALL_EXAMPLESDIR}" ) project_version = scope.get_string("VERSION", "1.0") cm_fh.write( f"cmake_minimum_required(VERSION {cmake_version_string})\n" f"project({binary_name} VERSION {project_version} LANGUAGES CXX)\n\n" "set(CMAKE_INCLUDE_CURRENT_DIR ON)\n\n" "set(CMAKE_AUTOMOC ON)\n" ) if scope.get_files("FORMS"): cm_fh.write("set(CMAKE_AUTOUIC ON)\n") cm_fh.write("\n") if not is_user_project: cm_fh.write( "if(NOT DEFINED INSTALL_EXAMPLESDIR)\n" ' set(INSTALL_EXAMPLESDIR "examples")\n' "endif()\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) # Write find_package call for Qt5/Qt6 and make it available as package QT. cm_fh.write("find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core)\n") # Write find_package calls for required packages. # We consider packages as required if they appear at the top-level scope. (public_libs, private_libs) = extract_cmake_libraries(scope, is_example=True) write_find_package_section( cm_fh, public_libs, private_libs, indent=indent, end_with_extra_newline=False, qt_package_name="Qt${QT_VERSION_MAJOR}", ) # Write find_package calls for optional packages. # We consider packages inside scopes other than the top-level one as optional. optional_public_libs: List[str] = [] optional_private_libs: List[str] = [] handling_first_scope = True for inner_scope in scopes: if handling_first_scope: handling_first_scope = False continue (public_libs, private_libs) = extract_cmake_libraries(inner_scope, is_example=True) optional_public_libs += public_libs optional_private_libs += private_libs write_find_package_section( cm_fh, optional_public_libs, optional_private_libs, indent=indent, is_required=False, end_with_extra_newline=False, qt_package_name="Qt${QT_VERSION_MAJOR}", ) cm_fh.write("\n") (resources, standalone_qtquick_compiler_skipped_files) = extract_resources(binary_name, scope) qml_resource = find_qml_resource(resources) if is_qml_plugin else None add_target = "" if is_plugin: if is_qml_plugin: extra_args = [f"PLUGIN_TARGET {binary_name}"] io_string = io.StringIO() write_qml_module( io_string, binary_name, scope, scopes, indent=indent, resource=qml_resource, extra_add_qml_module_args=extra_args, ) add_target += io_string.getvalue() else: add_target = f"qt_add_plugin({binary_name}" if "static" in scope.get("CONFIG"): add_target += " STATIC" add_target += ")\n" add_target += f"target_sources({binary_name} PRIVATE" else: add_target = f"qt_add_executable({binary_name}" property_win32, property_mac_bundle = get_win32_and_mac_bundle_properties(scope) if property_win32: add_target += " " + "WIN32" if property_mac_bundle: add_target += " " + "MACOSX_BUNDLE" write_all_source_file_lists(cm_fh, scope, add_target, indent=0) cm_fh.write(")\n") if is_qml_plugin and not is_plugin: write_qml_module(cm_fh, binary_name, scope, scopes, indent=indent, resource=qml_resource) 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_win32_and_mac_bundle_properties( io_string, scope, binary_name, handling_first_scope=handling_first_scope, indent=indent ) 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" ) (resources, standalone_qtquick_compiler_skipped_files) = extract_resources( binary_name, scope ) # Remove the QML resource, because we've handled it in write_qml_module. if qml_resource is not None: resources = list(filter(lambda r: r.name != qml_resource.name, resources)) write_resources( io_string, binary_name, scope, indent=indent, is_example=True, resources=resources, skipped_standalone_files=standalone_qtquick_compiler_skipped_files, ) 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 not is_user_project: 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") past_major_versions = scope.expandString("QML_PAST_MAJOR_VERSIONS") if past_major_versions: extra.append(f"PAST_MAJOR_VERSIONS {past_major_versions}") if "qmltypes" in scope.get("CONFIG"): extra.append("GENERATE_QMLTYPES") if "install_qmltypes" in scope.get("CONFIG"): extra.append("INSTALL_QMLTYPES") if "static" in scope.get("CONFIG"): extra.append("STATIC") plugin_extends = scope.get_string("PLUGIN_EXTENDS") if plugin_type != "platform" and plugin_extends == "-": extra.append("DEFAULT_IF FALSE") forward_target_info(scope, extra) 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 get_qml_import_version(scope: Scope, target: str) -> str: 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: replacements = [ ("$$QT_MINOR_VERSION", "${PROJECT_VERSION_MINOR}"), ("$$QT_VERSION", "${PROJECT_VERSION}"), ] for needle, replacement in replacements: import_version = import_version.replace(needle, replacement) return import_version def write_qml_module( cm_fh: IO[str], target: str, scope: Scope, scopes: List[Scope], resource: QtResource, extra_add_qml_module_args: List[str] = [], indent: int = 0, ): uri = scope.get_string("QML_IMPORT_NAME") if not uri: uri = target try: version = get_qml_import_version(scope, target) except RuntimeError: version = "${PROJECT_VERSION}" dest_dir = scope.expandString("DESTDIR") if dest_dir: dest_dir = f"${{CMAKE_CURRENT_BINARY_DIR}}/{dest_dir}" content = "" 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 content += "set(module_dynamic_qml_imports\n " if len(qml_dir.imports) != 0: content += "\n ".join(qml_dir.imports) content += "\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 ") :].replace(" ", "/")) if len(import_list) == 0: continue assert sc.condition content += f"if ({sc.condition})\n" content += " list(APPEND module_dynamic_qml_imports\n " content += "\n ".join(import_list) content += "\n )\nendif()\n\n" content += dedent( f"""\ qt_add_qml_module({target} URI {uri} VERSION {version} """ ) if resource is not None: qml_files = list(filter(is_qtquick_source_file, resource.files.keys())) if qml_files: content += " QML_FILES\n" for file in qml_files: content += f" {file}\n" other_files = list(itertools.filterfalse(is_qtquick_source_file, resource.files.keys())) if other_files: content += " RESOURCES\n" for file in other_files: content += f" {file}\n" if resource.prefix != "/": content += f" RESOURCE_PREFIX {resource.prefix}\n" if scope.TEMPLATE == "app": content += " NO_RESOURCE_TARGET_PATH\n" if dest_dir: content += f" OUTPUT_DIRECTORY {dest_dir}\n" if qml_dir is not None: if qml_dir.designer_supported: content += " DESIGNER_SUPPORTED\n" if len(qml_dir.classname) != 0: content += f" CLASSNAME {qml_dir.classname}\n" if len(qml_dir.depends) != 0: content += " DEPENDENCIES\n" for dep in qml_dir.depends: content += f" {dep[0]}/{dep[1]}\n" if len(qml_dir.type_names) == 0: content += " 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) content += f" IMPORTS\n{qml_dir_imports_line}" if qml_dir_dynamic_imports: content += " IMPORTS ${module_dynamic_qml_imports}\n" if len(qml_dir.optional_imports) != 0: qml_dir_optional_imports_line = " \n".join(qml_dir.optional_imports) content += f" OPTIONAL_IMPORTS\n{qml_dir_optional_imports_line}" if qml_dir.plugin_optional: content += " PLUGIN_OPTIONAL\n" for arg in extra_add_qml_module_args: content += " " content += arg content += "\n" content += ")\n" if resource: content += write_resource_source_file_properties( sorted(resource.files.keys()), resource.files, resource.base_dir, resource.skip_qtquick_compiler, ) content += "\n" cm_fh.write(content) 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 = get_qml_import_version(scope, target) if import_version: 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 ") :].replace(" ", "/")) if len(import_list) == 0: continue assert sc.condition cm_fh.write(f"if ({sc.condition})\n") cm_fh.write(" list(APPEND module_dynamic_qml_imports\n ") cm_fh.write("\n ".join(import_list)) cm_fh.write("\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}") if len(qml_dir.optional_imports): qml_dir_optional_imports_line = "\n ".join(qml_dir.optional_imports) extra_lines.append("OPTIONAL_IMPORTS\n " f"{qml_dir_optional_imports_line}") if qml_dir.plugin_optional: extra_lines.append("PLUGIN_OPTIONAL") 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.versions}"\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") else: cm_fh.write( f"{indent_0}set_source_files_properties({qml_file} PROPERTIES\n" f"{indent_1}QT_QML_SKIP_QMLDIR_ENTRY TRUE\n" 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, is_user_project=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 = "" target_ref = None gui = all(val not in config for val in ["console", "cmdline", "-app_bundle"]) and all( val not in scope.expand("QT") for val in ["testlib", "testlib-private"] ) if is_jar: write_jar(cm_fh, scope, indent=indent) elif "qt_helper_lib" in scope.get("_LOADED"): assert not is_example target = write_3rdparty_library(cm_fh, scope, indent=indent) elif is_example: target = write_example( cm_fh, scope, gui, indent=indent, is_plugin=is_plugin, is_user_project=is_user_project ) 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, target_ref = write_tool(cm_fh, scope, indent=indent) elif "qt_app" in scope.get("_LOADED"): assert not is_example scope._is_internal_qt_app = True target = write_qt_app(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) if target_ref is None: target_ref = target # 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_ref}\n", footer=")\n", ) # Generate qmltypes instruction for anything that may have CONFIG += qmltypes # that is not a qml plugin if ( not is_example and "qmltypes" in scope.get("CONFIG") and "qml_plugin" not in scope.get("_LOADED") ): cm_fh.write(f"\n{spaces(indent)}set_target_properties({target_ref} PROPERTIES\n") install_dir = scope.expandString("QMLTYPES_INSTALL_DIR") if install_dir: cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_INSTALL_QMLTYPES TRUE\n") import_version = get_qml_import_version(scope, target) if import_version: cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_VERSION {import_version}\n") past_major_versions = scope.expandString("QML_PAST_MAJOR_VERSIONS") if past_major_versions: cm_fh.write(f"{spaces(indent+1)}QT_QML_PAST_MAJOR_VERSIONS {past_major_versions}\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") json_output_filename = scope.expandString("QMLTYPES_FILENAME") if json_output_filename: cm_fh.write(f"{spaces(indent+1)}QT_QMLTYPES_FILENAME {json_output_filename}\n") target_path = scope.get("TARGETPATH") if target_path: cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_TARGET_PATH {target_path}\n") if install_dir: install_dir = install_dir.replace("$$[QT_INSTALL_QML]", "${INSTALL_QMLDIR}") 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_ref})\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}) include(.cmake.conf) project({qt_lib} VERSION "${{QT_REPO_MODULE_VERSION}}" 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( """\ qt_build_repo() """ ) cm_fh.write(f"{header}{expand_project_requirements(scope)}{build_repo}") def create_top_level_cmake_conf(): conf_file_name = ".cmake.conf" try: with open(conf_file_name, "x") as file: file.write('set(QT_REPO_MODULE_VERSION "6.5.3")\n') except FileExistsError: pass def find_top_level_repo_project_file(project_file_path: str = "") -> Optional[str]: qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_dir = os.path.dirname(qmake_or_cmake_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_or_cmake_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]): content = dedent( """\ 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, "target_include_directories(${{PROJECT_NAME}} PUBLIC", indent=indent, footer=")", ) write_defines( cm_fh, scope, "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="target_link_libraries(${{PROJECT_NAME}} PRIVATE\n", footer=")", ) write_list( cm_fh, public_libs, "", indent=indent, header="target_link_libraries(${{PROJECT_NAME}} PUBLIC\n", footer=")", ) write_compile_options( cm_fh, scope, "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.16)\n" f"project(config_test_{project_name} LANGUAGES C CXX)\n" """ if(DEFINED QT_CONFIG_COMPILE_TEST_CMAKE_SYSTEM_PREFIX_PATH) set(CMAKE_SYSTEM_PREFIX_PATH "${QT_CONFIG_COMPILE_TEST_CMAKE_SYSTEM_PREFIX_PATH}") endif() if(DEFINED QT_CONFIG_COMPILE_TEST_CMAKE_SYSTEM_FRAMEWORK_PATH) set(CMAKE_SYSTEM_FRAMEWORK_PATH "${QT_CONFIG_COMPILE_TEST_CMAKE_SYSTEM_FRAMEWORK_PATH}") endif() foreach(p ${QT_CONFIG_COMPILE_TEST_PACKAGES}) find_package(${p}) endforeach() if(QT_CONFIG_COMPILE_TEST_LIBRARIES) link_libraries(${QT_CONFIG_COMPILE_TEST_LIBRARIES}) endif() if(QT_CONFIG_COMPILE_TEST_LIBRARY_TARGETS) foreach(lib ${QT_CONFIG_COMPILE_TEST_LIBRARY_TARGETS}) if(TARGET ${lib}) link_libraries(${lib}) endif() endforeach() endif() """ ) cm_fh.write(f"{content}\n") # Remove default QT libs. scope._append_operation("QT", RemoveOperation(["core", "gui"])) add_target = "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, is_user_project: bool = False, ) -> None: template = scope.TEMPLATE if is_user_project: if template == "subdirs": handle_subdir(scope, cm_fh, indent=indent, is_example=True, is_user_project=True) elif template in ("app", "lib"): handle_app_or_lib(scope, cm_fh, indent=indent, is_example=True, is_user_project=True) else: temp_buffer = io.StringIO() # Handle top level repo project in a special way. if is_top_level_repo_project(scope.file_absolute_path): create_top_level_cmake_conf() 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, is_user_project: bool = True, 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, is_user_project=is_user_project ) 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 # Ignore selfcover.pri as this generates too many incompatible flags # need to be removed with special cases if include_file.endswith("selfcover.pri"): continue if include_file.startswith("${QT_SOURCE_TREE}"): root_source_dir = get_top_level_repo_project_path(scope.file_absolute_path) include_file = include_file.replace("${QT_SOURCE_TREE}", root_source_dir) 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, output_file: str, keep_temporary_files=False, debug: bool = False ) -> None: if debug: print(f"Copying {scope.generated_cmake_lists_path} to {output_file}") base_dir = os.path.dirname(output_file) base_dir_abs = os.path.realpath(base_dir) os.makedirs(base_dir_abs, exist_ok=True) copyfile(scope.generated_cmake_lists_path, output_file) 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_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path) qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path) project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_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 doc snippets. if fnmatch.fnmatch(project_relative_path, "src/*/doc/snippets/*"): 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 exists. 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, is_user_project=args.is_user_project, debug=args.debug, ) copy_generated_file = True output_file = file_scope.original_cmake_lists_path if args.output_file: output_file = args.output_file if not args.skip_special_case_preservation: debug_special_case = args.debug_special_case_preservation or args.debug handler = SpecialCaseHandler( output_file, 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, output_file, keep_temporary_files=args.keep_temporary_files ) os.chdir(backup_current_dir) if __name__ == "__main__": main()