qt5base-lts/util/cmake/pro2cmake.py
Alexandru Croitor 34437b9e14 CMake: pro2cmake: Handle AUX_QML_FILES installation
Generate copy and installation rules for AUX_QML_FILES entries.
These are usually handled by mkspecs/features/qml_module.prf.

For the purposes of unblocking the Designer team, instead of creating
AUX_QML file specic functions that would handle installation, generate
qt_copy_or_install() rules.

To ensure installation destination is correct, we need to group file
paths by base directory and specify those in the DESTINATION
parameter. Otherwise the file hierarchy would be flattened.
This is usually handled by file_copies.prf and the equivalent qmake
install mechanism. In CMake we need to handle it manually.

Also detect usage of wildcards to ensure we use globs for
installation.

Task-number: QTBUG-87818
Change-Id: I8a5db445274fb670d90cf90b38598a6b3326bc44
Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
2020-11-12 15:25:06 +01:00

4743 lines
166 KiB
Python
Executable File

#!/usr/bin/env python3
#############################################################################
##
## Copyright (C) 2018 The Qt Company Ltd.
## Contact: https://www.qt.io/licensing/
##
## This file is part of the plugins of the Qt Toolkit.
##
## $QT_BEGIN_LICENSE:GPL-EXCEPT$
## Commercial License Usage
## Licensees holding valid commercial Qt licenses may use this file in
## accordance with the commercial license agreement provided with the
## Software or, alternatively, in accordance with the terms contained in
## a written agreement between you and The Qt Company. For licensing terms
## and conditions see https://www.qt.io/terms-conditions. For further
## information use the contact form at https://www.qt.io/contact-us.
##
## GNU General Public License Usage
## Alternatively, this file may be used under the terms of the GNU
## General Public License version 3 as published by the Free Software
## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
## included in the packaging of this file. Please review the following
## information to ensure the GNU General Public License requirements will
## be met: https://www.gnu.org/licenses/gpl-3.0.html.
##
## $QT_END_LICENSE$
##
#############################################################################
# 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 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.15.0"
cmake_api_version = 2
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 an example.",
)
parser.add_argument(
"-s",
"--skip-special-case-preservation",
dest="skip_special_case_preservation",
action="store_true",
help="Skips behavior to reapply " "special case modifications (requires git in PATH)",
)
parser.add_argument(
"-k",
"--keep-temporary-files",
dest="keep_temporary_files",
action="store_true",
help="Don't automatically remove CMakeLists.gen.txt and other " "intermediate files.",
)
parser.add_argument(
"-e",
"--skip-condition-cache",
dest="skip_condition_cache",
action="store_true",
help="Don't use condition simplifier cache (conversion speed may decrease).",
)
parser.add_argument(
"--skip-subdirs-project",
dest="skip_subdirs_project",
action="store_true",
help="Skip converting project if it ends up being a TEMPLATE=subdirs project.",
)
parser.add_argument(
"-i",
"--ignore-skip-marker",
dest="ignore_skip_marker",
action="store_true",
help="If set, pro file will be converted even if skip marker is found in CMakeLists.txt.",
)
parser.add_argument(
"--api-version",
dest="api_version",
type=int,
help="Specify which cmake api version should be generated. 1 or 2, 2 is latest.",
)
parser.add_argument(
"-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_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
return qmake_conf_dir_path
def is_top_level_repo_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_dir_path = os.path.dirname(project_file_path)
return qmake_conf_dir_path == project_dir_path
def is_top_level_repo_tests_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_dir_path = os.path.dirname(project_file_path)
project_dir_name = os.path.basename(project_dir_path)
maybe_same_level_dir_path = os.path.join(project_dir_path, "..")
normalized_maybe_same_level_dir_path = os.path.normpath(maybe_same_level_dir_path)
return (
qmake_conf_dir_path == normalized_maybe_same_level_dir_path and project_dir_name == "tests"
)
def is_top_level_repo_examples_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_dir_path = os.path.dirname(project_file_path)
project_dir_name = os.path.basename(project_dir_path)
maybe_same_level_dir_path = os.path.join(project_dir_path, "..")
normalized_maybe_same_level_dir_path = os.path.normpath(maybe_same_level_dir_path)
return (
qmake_conf_dir_path == normalized_maybe_same_level_dir_path
and project_dir_name == "examples"
)
def is_example_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
# If the project file is found in a subdir called 'examples'
# relative to the repo source dir, then it must be an example, but
# some examples contain 3rdparty libraries that do not need to be
# built as examples.
return project_relative_path.startswith("examples") and "3rdparty" not in project_relative_path
def is_config_test_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
dir_name_with_qmake_confg = os.path.basename(qmake_conf_dir_path)
project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
# If the project file is found in a subdir called 'config.tests'
# relative to the repo source dir, then it's probably a config test.
# Also if the .qmake.conf is found within config.tests dir (like in qtbase)
# then the project is probably a config .test
return (
project_relative_path.startswith("config.tests")
or dir_name_with_qmake_confg == "config.tests"
)
def is_benchmark_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
# If the project file is found in a subdir called 'tests/benchmarks'
# relative to the repo source dir, then it must be a benchmark
return project_relative_path.startswith("tests/benchmarks")
def is_manual_test_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
# If the project file is found in a subdir called 'tests/manual'
# relative to the repo source dir, then it must be a manual test
return project_relative_path.startswith("tests/manual")
@lru_cache(maxsize=None)
def find_qmake_conf(project_file_path: str = "") -> str:
if not os.path.isabs(project_file_path):
print(
f"Warning: could not find .qmake.conf file, given path is not an "
f"absolute path: {project_file_path}"
)
return ""
cwd = os.path.dirname(project_file_path)
file_name = ".qmake.conf"
while os.path.isdir(cwd):
maybe_file = posixpath.join(cwd, file_name)
if os.path.isfile(maybe_file):
return maybe_file
else:
last_cwd = cwd
cwd = os.path.dirname(cwd)
if last_cwd == cwd:
# reached the top level directory, stop looking
break
print(f"Warning: could not find .qmake.conf file")
return ""
def set_up_cmake_api_calls():
def nested_dict():
return defaultdict(nested_dict)
api = nested_dict()
api[1]["qt_extend_target"] = "extend_target"
api[1]["qt_add_module"] = "add_qt_module"
api[1]["qt_add_plugin"] = "add_qt_plugin"
api[1]["qt_add_tool"] = "add_qt_tool"
api[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]
def process_qrc_file(
target: str,
scope: Scope,
filepath: str,
base_dir: str = "",
project_file_path: str = "",
skip_qtquick_compiler: bool = False,
is_example: bool = False,
) -> str:
assert target
# Hack to handle QT_SOURCE_TREE. Assume currently that it's the same
# as the qtbase source path.
qt_source_tree_literal = "${QT_SOURCE_TREE}"
if qt_source_tree_literal in filepath:
qmake_conf = find_qmake_conf(project_file_path)
if qmake_conf:
qt_source_tree = os.path.dirname(qmake_conf)
filepath = filepath.replace(qt_source_tree_literal, qt_source_tree)
else:
print(
f"Warning, could not determine QT_SOURCE_TREE location while trying "
f"to find: {filepath}"
)
resource_name = os.path.splitext(os.path.basename(filepath))[0]
dir_name = os.path.dirname(filepath)
base_dir = posixpath.join("" if base_dir == "." else base_dir, dir_name)
# Small not very thorough check to see if this a shared qrc resource
# pattern is mostly used by the tests.
is_parent_path = dir_name.startswith("..")
if not os.path.isfile(filepath):
raise RuntimeError(f"Invalid file path given to process_qrc_file: {filepath}")
tree = ET.parse(filepath)
root = tree.getroot()
assert root.tag == "RCC"
output = ""
resource_count = 0
for resource in root:
assert resource.tag == "qresource"
lang = resource.get("lang", "")
prefix = resource.get("prefix", "/")
if not prefix.startswith("/"):
prefix = f"/{prefix}"
full_resource_name = resource_name + (str(resource_count) if resource_count > 0 else "")
files: Dict[str, str] = {}
for file in resource:
path = file.text
assert path
# Get alias:
alias = file.get("alias", "")
# In cases where examples use shared resources, we set the alias
# too the same name of the file, or the applications won't be
# be able to locate the resource
if not alias and is_parent_path:
alias = path
files[path] = alias
output += write_add_qt_resource_call(
target,
scope,
full_resource_name,
prefix,
base_dir,
lang,
files,
skip_qtquick_compiler,
is_example,
)
resource_count += 1
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 = ""
sorted_files = sorted(files.keys())
assert sorted_files
if base_dir:
base_dir_expanded = scope.expandString(base_dir)
if base_dir_expanded:
base_dir = base_dir_expanded
source_file_properties = defaultdict(list)
for source in sorted_files:
full_source = posixpath.join(base_dir, source)
alias = files[source]
if alias:
source_file_properties[full_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[full_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}
)
"""
)
# 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 "<NOTHING>"
if not isinstance(self._value, list):
return "<NOT A LIST>"
result = []
for i in self._value:
if not i:
result.append("<NONE>")
else:
result.append(str(i))
return '"' + '", "'.join(result) + '"'
class AddOperation(Operation):
def process(
self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
) -> List[str]:
return sinput + transformer(self._value)
def __repr__(self):
return f"+({self._dump()})"
class UniqueAddOperation(Operation):
def process(
self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
) -> List[str]:
result = sinput
for v in transformer(self._value):
if v not in result:
result.append(v)
return result
def __repr__(self):
return f"*({self._dump()})"
class ReplaceOperation(Operation):
def process(
self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
) -> List[str]:
result = []
for s in sinput:
for v in transformer(self._value):
pattern, replacement = self.split_rex(v)
result.append(re.sub(pattern, replacement, s))
return result
def split_rex(self, s):
pattern = ""
replacement = ""
if len(s) < 4:
return pattern, replacement
sep = s[1]
s = s[2:]
rex = re.compile(f"[^\\\\]{sep}")
m = rex.search(s)
if not m:
return pattern, replacement
pattern = s[: m.start() + 1]
replacement = s[m.end() :]
m = rex.search(replacement)
if m:
replacement = replacement[: m.start() + 1]
return pattern, replacement
def __repr__(self):
return f"*({self._dump()})"
class SetOperation(Operation):
def process(
self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
) -> List[str]:
values = [] # List[str]
for v in self._value:
if v != f"$${key}":
values.append(v)
else:
values += sinput
if transformer:
return list(transformer(values))
else:
return values
def __repr__(self):
return f"=({self._dump()})"
class RemoveOperation(Operation):
def process(
self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
) -> List[str]:
sinput_set = set(sinput)
value_set = set(self._value)
result: List[str] = []
# Add everything that is not going to get removed:
for v in sinput:
if v not in value_set:
result += [v]
# Add everything else with removal marker:
for v in transformer(self._value):
if v not in sinput_set:
result += [f"-{v}"]
return result
def __repr__(self):
return f"-({self._dump()})"
# Helper class that stores a list of tuples, representing a scope id and
# a line number within that scope's project file. The whole list
# represents the full path location for a certain operation while
# traversing include()'d scopes. Used for sorting when determining
# operation order when evaluating operations.
class OperationLocation(object):
def __init__(self):
self.list_of_scope_ids_and_line_numbers = []
def clone_and_append(self, scope_id: int, line_number: int) -> OperationLocation:
new_location = OperationLocation()
new_location.list_of_scope_ids_and_line_numbers = list(
self.list_of_scope_ids_and_line_numbers
)
new_location.list_of_scope_ids_and_line_numbers.append((scope_id, line_number))
return new_location
def __lt__(self, other: OperationLocation) -> Any:
return self.list_of_scope_ids_and_line_numbers < other.list_of_scope_ids_and_line_numbers
def __repr__(self) -> str:
s = ""
for t in self.list_of_scope_ids_and_line_numbers:
s += f"s{t[0]}:{t[1]} "
s = s.strip(" ")
return s
class Scope(object):
SCOPE_ID: int = 1
def __init__(
self,
*,
parent_scope: Optional[Scope],
qmake_file: str,
condition: str = "",
base_dir: str = "",
operations: Union[Dict[str, List[Operation]], None] = None,
parent_include_line_no: int = -1,
) -> None:
if not operations:
operations = {
"QT_SOURCE_TREE": [SetOperation(["${QT_SOURCE_TREE}"])],
"QT_BUILD_TREE": [SetOperation(["${PROJECT_BINARY_DIR}"])],
"QTRO_SOURCE_TREE": [SetOperation(["${CMAKE_SOURCE_DIR}"])],
}
self._operations: Dict[str, List[Operation]] = copy.deepcopy(operations)
if parent_scope:
parent_scope._add_child(self)
else:
self._parent = None # type: Optional[Scope]
# Only add the "QT = core gui" Set operation once, on the
# very top-level .pro scope, aka it's basedir is empty.
if not base_dir:
self._operations["QT"] = [SetOperation(["core", "gui"])]
self._basedir = base_dir
if qmake_file:
self._currentdir = os.path.dirname(qmake_file) or "."
if not self._basedir:
self._basedir = self._currentdir
self._scope_id = Scope.SCOPE_ID
Scope.SCOPE_ID += 1
self._file = qmake_file
self._file_absolute_path = os.path.abspath(qmake_file)
self._condition = map_condition(condition)
self._children = [] # type: List[Scope]
self._included_children = [] # type: List[Scope]
self._visited_keys = set() # type: Set[str]
self._total_condition = None # type: Optional[str]
self._parent_include_line_no = parent_include_line_no
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 '<TRUE>'}"
)
def reset_visited_keys(self):
self._visited_keys = set()
def merge(self, other: "Scope") -> None:
assert self != other
self._included_children.append(other)
@property
def scope_debug(self) -> bool:
merge = self.get_string("PRO2CMAKE_SCOPE_DEBUG").lower()
return merge == "1" or merge == "on" or merge == "yes" or merge == "true"
@property
def parent(self) -> Optional[Scope]:
return self._parent
@property
def basedir(self) -> str:
return self._basedir
@property
def currentdir(self) -> str:
return self._currentdir
@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]: <<END OF SCOPE>>")
return scope
def _append_operation(self, key: str, op: Operation) -> None:
if key in self._operations:
self._operations[key].append(op)
else:
self._operations[key] = [op]
@property
def file(self) -> str:
return self._file or ""
@property
def file_absolute_path(self) -> str:
return self._file_absolute_path or ""
@property
def generated_cmake_lists_path(self) -> str:
assert self.basedir
return os.path.join(self.basedir, "CMakeLists.gen.txt")
@property
def original_cmake_lists_path(self) -> str:
assert self.basedir
return os.path.join(self.basedir, "CMakeLists.txt")
@property
def condition(self) -> str:
return self._condition
@property
def total_condition(self) -> Optional[str]:
return self._total_condition
@total_condition.setter
def total_condition(self, condition: str) -> None:
self._total_condition = condition
def _add_child(self, scope: "Scope") -> None:
scope._parent = self
self._children.append(scope)
@property
def children(self) -> List["Scope"]:
result = list(self._children)
for include_scope in self._included_children:
result += include_scope.children
return result
def dump(self, *, indent: int = 0) -> None:
ind = spaces(indent)
print(f'{ind}Scope "{self}":')
if self.total_condition:
print(f"{ind} Total condition = {self.total_condition}")
print(f"{ind} Keys:")
keys = self._operations.keys()
if not keys:
print(f"{ind} -- NONE --")
else:
for k in sorted(keys):
print(f'{ind} {k} = "{self._operations.get(k, [])}"')
print(f"{ind} Children:")
if not self._children:
print(f"{ind} -- NONE --")
else:
for c in self._children:
c.dump(indent=indent + 1)
print(f"{ind} Includes:")
if not self._included_children:
print(f"{ind} -- NONE --")
else:
for c in self._included_children:
c.dump(indent=indent + 1)
def dump_structure(self, *, structure_type: str = "ROOT", indent: int = 0) -> None:
print(f"{spaces(indent)}{structure_type}: {self}")
for i in self._included_children:
i.dump_structure(structure_type="INCL", indent=indent + 1)
for i in self._children:
i.dump_structure(structure_type="CHLD", indent=indent + 1)
@property
def keys(self):
return self._operations.keys()
@property
def visited_keys(self):
return self._visited_keys
# Traverses a scope and its children, and collects operations
# that need to be processed for a certain key.
def _gather_operations_from_scope(
self,
operations_result: List[Dict[str, Any]],
current_scope: Scope,
op_key: str,
current_location: OperationLocation,
):
for op in current_scope._operations.get(op_key, []):
new_op_location = current_location.clone_and_append(
current_scope._scope_id, op._line_no
)
op_info: Dict[str, Any] = {}
op_info["op"] = op
op_info["scope"] = current_scope
op_info["location"] = new_op_location
operations_result.append(op_info)
for included_child in current_scope._included_children:
new_scope_location = current_location.clone_and_append(
current_scope._scope_id, included_child._parent_include_line_no
)
self._gather_operations_from_scope(
operations_result, included_child, op_key, new_scope_location
)
# Partially applies a scope argument to a given transformer.
@staticmethod
def _create_transformer_for_operation(
given_transformer: Optional[Callable[[Scope, List[str]], List[str]]],
transformer_scope: Scope,
) -> Callable[[List[str]], List[str]]:
if given_transformer:
def wrapped_transformer(values):
return given_transformer(transformer_scope, values)
else:
def wrapped_transformer(values):
return values
return wrapped_transformer
def _evalOps(
self,
key: str,
transformer: Optional[Callable[[Scope, List[str]], List[str]]],
result: List[str],
*,
inherit: bool = False,
) -> List[str]:
self._visited_keys.add(key)
# Inherit values from parent scope.
# This is a strange edge case which is wrong in principle, because
# .pro files are imperative and not declarative. Nevertheless
# this fixes certain mappings (e.g. for handling
# VERSIONTAGGING_SOURCES in src/corelib/global/global.pri).
if self._parent and inherit:
result = self._parent._evalOps(key, transformer, result)
operations_to_run: List[Dict[str, Any]] = []
starting_location = OperationLocation()
starting_scope = self
self._gather_operations_from_scope(
operations_to_run, starting_scope, key, starting_location
)
# Sorts the operations based on the location of each operation. Technically compares two
# lists of tuples.
operations_to_run = sorted(operations_to_run, key=lambda o: o["location"])
# Process the operations.
for op_info in operations_to_run:
op_transformer = self._create_transformer_for_operation(transformer, op_info["scope"])
result = op_info["op"].process(key, result, op_transformer)
return result
def get(self, key: str, *, ignore_includes: bool = False, inherit: bool = False) -> List[str]:
is_same_path = self.currentdir == self.basedir
if not is_same_path:
relative_path = posixpath.relpath(self.currentdir, self.basedir)
if key == "QQC2_SOURCE_TREE":
qmake_conf_path = find_qmake_conf(os.path.abspath(self.currentdir))
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_relative_path = os.path.relpath(qmake_conf_dir_path, self.currentdir)
return ["${CMAKE_CURRENT_SOURCE_DIR}/" + project_relative_path]
if key == "QT_ARCH":
return ["${CMAKE_SYSTEM_PROCESSOR}"]
if key == "_PRO_FILE_PWD_":
return ["${CMAKE_CURRENT_SOURCE_DIR}"]
if key == "PWD":
if is_same_path:
return ["${CMAKE_CURRENT_SOURCE_DIR}"]
else:
return [f"${{CMAKE_CURRENT_SOURCE_DIR}}/{relative_path}"]
if key == "OUT_PWD":
if is_same_path:
return ["${CMAKE_CURRENT_BINARY_DIR}"]
else:
return [f"${{CMAKE_CURRENT_BINARY_DIR}}/{relative_path}"]
# 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)
# 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")
):
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
) -> None:
# Global nested dictionary that will contain sub_dir assignments and their conditions.
# Declared as a global in order not to pollute the nested function signatures with giant
# type hints.
sub_dirs: Dict[str, Dict[str, Set[FrozenSet[str]]]] = {}
# Collects assignment conditions into global sub_dirs dict.
def collect_subdir_info(sub_dir_assignment: str, *, current_conditions: FrozenSet[str] = None):
subtraction = sub_dir_assignment.startswith("-")
if subtraction:
subdir_name = sub_dir_assignment[1:]
else:
subdir_name = sub_dir_assignment
if subdir_name not in sub_dirs:
sub_dirs[subdir_name] = {}
additions = sub_dirs[subdir_name].get("additions", set())
subtractions = sub_dirs[subdir_name].get("subtractions", set())
if current_conditions:
if subtraction:
subtractions.add(current_conditions)
else:
additions.add(current_conditions)
if additions:
sub_dirs[subdir_name]["additions"] = additions
if subtractions:
sub_dirs[subdir_name]["subtractions"] = subtractions
# Recursive helper that collects subdir info for given scope,
# and the children of the given scope.
def handle_subdir_helper(
scope: Scope,
cm_fh: IO[str],
*,
indent: int = 0,
current_conditions: FrozenSet[str] = frozenset(),
is_example: bool = False,
):
for sd in scope.get_files("SUBDIRS"):
# Collect info about conditions and SUBDIR assignments in the
# current scope.
if os.path.isdir(sd) or sd.startswith("-"):
collect_subdir_info(sd, current_conditions=current_conditions)
# For the file case, directly write into the file handle.
elif os.path.isfile(sd):
# Handle cases with SUBDIRS += Foo/bar/z.pro. We want to be able
# to generate add_subdirectory(Foo/bar) instead of parsing the full
# .pro file in the current CMakeLists.txt. This causes issues
# with relative paths in certain projects otherwise.
dirname = os.path.dirname(sd)
if dirname:
collect_subdir_info(dirname, current_conditions=current_conditions)
else:
subdir_result, project_file_content = parseProFile(sd, debug=False)
subdir_scope = Scope.FromDict(
scope,
sd,
subdir_result.asDict().get("statements"),
"",
scope.basedir,
project_file_content=project_file_content,
)
do_include(subdir_scope)
cmakeify_scope(subdir_scope, cm_fh, indent=indent, is_example=is_example)
else:
print(f" XXXX: SUBDIR {sd} in {scope}: Not found.")
# Collect info about conditions and SUBDIR assignments in child
# scopes, aka recursively call the same function, but with an
# updated current_conditions frozen set.
for c in scope.children:
# Use total_condition for 'else' conditions, otherwise just use the regular value to
# simplify the logic.
child_conditions = current_conditions
child_condition = c.total_condition if c.condition == "else" else c.condition
if child_condition:
child_conditions = frozenset((*child_conditions, child_condition))
handle_subdir_helper(
c,
cm_fh,
indent=indent + 1,
current_conditions=child_conditions,
is_example=is_example,
)
def group_and_print_sub_dirs(scope: Scope, indent: int = 0) -> None:
# Simplify conditions, and group
# subdirectories with the same conditions.
grouped_sub_dirs: Dict[str, List[str]] = {}
# Wraps each element in the given interable with parentheses,
# to make sure boolean simplification happens correctly.
def wrap_in_parenthesis(iterable):
return [f"({c})" for c in iterable]
def join_all_conditions(set_of_alternatives):
# Elements within one frozen set represent one single
# alternative whose pieces are ANDed together.
# This is repeated for each alternative that would
# enable a subdir, and are thus ORed together.
final_str = ""
if set_of_alternatives:
wrapped_set_of_alternatives = [
wrap_in_parenthesis(alternative) for alternative in set_of_alternatives
]
alternatives = [
f'({" AND ".join(alternative)})' for alternative in wrapped_set_of_alternatives
]
final_str = " OR ".join(sorted(alternatives))
return final_str
for subdir_name in sub_dirs:
additions = sub_dirs[subdir_name].get("additions", set())
subtractions = sub_dirs[subdir_name].get("subtractions", set())
# An empty condition key represents the group of sub dirs
# that should be added unconditionally.
condition_key = ""
if additions or subtractions:
addition_str = join_all_conditions(additions)
if addition_str:
addition_str = f"({addition_str})"
subtraction_str = join_all_conditions(subtractions)
if subtraction_str:
subtraction_str = f"NOT ({subtraction_str})"
condition_str = addition_str
if condition_str and subtraction_str:
condition_str += " AND "
condition_str += subtraction_str
if not condition_str.rstrip("()").strip():
continue
condition_simplified = simplify_condition(condition_str)
condition_key = condition_simplified
sub_dir_list_by_key: List[str] = grouped_sub_dirs.get(condition_key, [])
sub_dir_list_by_key.append(subdir_name)
grouped_sub_dirs[condition_key] = sub_dir_list_by_key
# Print any requires() blocks.
cm_fh.write(expand_project_requirements(scope, skip_message=True))
# Print the groups.
ind = spaces(indent)
for condition_key in grouped_sub_dirs:
cond_ind = ind
if condition_key:
cm_fh.write(f"{ind}if({condition_key})\n")
cond_ind += " "
sub_dir_list_by_key = grouped_sub_dirs.get(condition_key, [])
for subdir_name in sub_dir_list_by_key:
cm_fh.write(f"{cond_ind}add_subdirectory({subdir_name})\n")
if condition_key:
cm_fh.write(f"{ind}endif()\n")
# A set of conditions which will be ANDed together. The set is recreated with more conditions
# as the scope deepens.
current_conditions: FrozenSet[str] = frozenset()
# Compute the total condition for scopes. Needed for scopes that
# have 'else' as a condition.
recursive_evaluate_scope(scope)
# Do the work.
handle_subdir_helper(
scope, cm_fh, indent=indent, current_conditions=current_conditions, is_example=is_example
)
# 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"$<BUILD_INTERFACE:{i}>"
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)
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 = scope.is_public_module
current_scope = scope
while not is_public_module and current_scope.parent:
current_scope = current_scope.parent
is_public_module = current_scope.is_public_module
# 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 = "<EMPTY>" if not values else '"' + '" "'.join(scope.get(k)) + '"'
result += f"{indent}# {k} = {value_string}\n"
if result:
result = f"\n#### Keys ignored in scope {scope}:\n{result}"
return result
def recursive_evaluate_scope(
scope: Scope, parent_condition: str = "", previous_condition: str = ""
) -> str:
current_condition = scope.condition
total_condition = current_condition
if total_condition == "else":
assert previous_condition, f"Else branch without previous condition in: {scope.file}"
total_condition = f"NOT ({previous_condition})"
if parent_condition:
if not total_condition:
total_condition = parent_condition
else:
total_condition = f"({parent_condition}) AND ({total_condition})"
scope.total_condition = simplify_condition(total_condition)
prev_condition = ""
for c in scope.children:
prev_condition = recursive_evaluate_scope(c, total_condition, prev_condition)
return current_condition
def map_to_cmake_condition(condition: str = "") -> str:
condition = condition.replace("QTDIR_build", "QT_BUILDING_QT")
condition = re.sub(
r"\bQT_ARCH___equals___([a-zA-Z_0-9]*)",
r'(TEST_architecture_arch STREQUAL "\1")',
condition or "",
)
condition = re.sub(
r"\bQT_ARCH___contains___([a-zA-Z_0-9]*)",
r'(TEST_architecture_arch STREQUAL "\1")',
condition or "",
)
condition = condition.replace("QT___contains___opengl", "QT_FEATURE_opengl")
condition = condition.replace("QT___contains___widgets", "QT_FEATURE_widgets")
condition = condition.replace(
"DEFINES___contains___QT_NO_PRINTER", "(QT_FEATURE_printer EQUAL FALSE)"
)
return condition
resource_file_expansion_counter = 0
def expand_resource_glob(cm_fh: IO[str], expression: str) -> str:
global resource_file_expansion_counter
r = expression.replace('"', "")
cm_fh.write(
dedent(
f"""
file(GLOB resource_glob_{resource_file_expansion_counter} RELATIVE "${{CMAKE_CURRENT_SOURCE_DIR}}" "{r}")
foreach(file IN LISTS resource_glob_{resource_file_expansion_counter})
set_source_files_properties("${{CMAKE_CURRENT_SOURCE_DIR}}/${{file}}" PROPERTIES QT_RESOURCE_ALIAS "${{file}}")
endforeach()
"""
)
)
expanded_var = f"${{resource_glob_{resource_file_expansion_counter}}}"
resource_file_expansion_counter += 1
return expanded_var
def write_resources(
cm_fh: IO[str],
target: str,
scope: Scope,
indent: int = 0,
is_example=False,
target_ref: str = None,
):
if target_ref is None:
target_ref = target
# vpath = scope.expand('VPATH')
# Handle QRC files by turning them into qt_add_resource:
resources = scope.get_files("RESOURCES")
qtquickcompiler_skipped = scope.get_files("QTQUICK_COMPILER_SKIPPED_RESOURCES")
qrc_output = ""
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:
cm_fh.write(f"#### Ignored generated resource: {r}")
continue
qrc_output += process_qrc_file(
target_ref,
scope,
r,
scope.basedir,
scope.file_absolute_path,
skip_qtquick_compiler,
is_example,
)
else:
immediate_files = {f: "" for f in scope.get_files(f"{r}.files")}
if immediate_files:
immediate_files_filtered = []
for f in immediate_files:
if "*" in f:
immediate_files_filtered.append(expand_resource_glob(cm_fh, f))
else:
immediate_files_filtered.append(f)
immediate_files = {f: "" for f in immediate_files_filtered}
scope_prefix = scope.get(f"{r}.prefix")
if scope_prefix:
immediate_prefix = scope_prefix[0]
else:
immediate_prefix = "/"
immediate_base_list = scope.get(f"{r}.base")
assert (
len(immediate_base_list) < 2
), f"immediate base directory must be at most one entry"
immediate_base = replace_path_constants("".join(immediate_base_list), scope)
immediate_lang = None
immediate_name = f"qmake_{r}"
qrc_output += write_add_qt_resource_call(
target=target_ref,
scope=scope,
resource_name=immediate_name,
prefix=immediate_prefix,
base_dir=immediate_base,
lang=immediate_lang,
files=immediate_files,
skip_qtquick_compiler=skip_qtquick_compiler,
is_example=is_example,
)
else:
if "*" in r:
standalone_files.append(expand_resource_glob(cm_fh, r))
else:
# stadalone source file properties need to be set as they
# are parsed.
if skip_qtquick_compiler:
qrc_output += (
f'set_source_files_properties("{r}" PROPERTIES '
f"QT_SKIP_QUICKCOMPILER 1)\n\n"
)
standalone_files.append(r)
if standalone_files:
name = "qmake_immediate"
prefix = "/"
base = ""
lang = None
files = {f: "" for f in standalone_files}
qrc_output += write_add_qt_resource_call(
target=target_ref,
scope=scope,
resource_name=name,
prefix=prefix,
base_dir=base,
lang=lang,
files=files,
skip_qtquick_compiler=False,
is_example=is_example,
)
if qrc_output:
str_indent = spaces(indent)
cm_fh.write(f"\n{str_indent}# Resources:\n")
for line in qrc_output.split("\n"):
if line:
cm_fh.write(f"{str_indent}{line}\n")
else:
# do not add spaces to empty lines
cm_fh.write("\n")
def write_statecharts(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0, is_example=False):
sources = scope.get_files("STATECHARTS", use_vpath=True)
if not sources:
return
cm_fh.write("\n# Statecharts:\n")
if is_example:
cm_fh.write(f"qt6_add_statecharts({target}\n")
else:
cm_fh.write(f"add_qt_statecharts({target} FILES\n")
indent += 1
for f in sources:
cm_fh.write(f"{spaces(indent)}{f}\n")
cm_fh.write(")\n")
def write_qlalrsources(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
sources = scope.get_files("QLALRSOURCES", use_vpath=True)
if not sources:
return
cm_fh.write("\n# QLALR Grammars:\n")
cm_fh.write(f"qt_process_qlalr(\n")
indent += 1
cm_fh.write(f"{spaces(indent)}{target}\n")
cm_fh.write(f"{spaces(indent)}{';'.join(sources)}\n")
cm_fh.write(f'{spaces(indent)}""\n')
cm_fh.write(")\n")
def write_repc_files(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
for t in ["SOURCE", "REPLICA", "MERGED"]:
sources = scope.get_files("REPC_" + t, use_vpath=True)
if not sources:
continue
cm_fh.write(f"qt6_add_repc_{t.lower()}({target}\n")
indent += 1
for f in sources:
cm_fh.write(f"{spaces(indent)}{f}\n")
cm_fh.write(")\n")
def 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=f")", 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":
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")
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
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 "metatypes" in scope.get("CONFIG") or "qmltypes" in scope.get("CONFIG"):
extra.append("GENERATE_METATYPES")
module_config = scope.get("MODULE_CONFIG")
if len(module_config):
extra.append(f'QMAKE_MODULE_CONFIG {" ".join(module_config)}')
module_plugin_types = scope.get_files("MODULE_PLUGIN_TYPES")
if module_plugin_types:
extra.append(f"PLUGIN_TYPES {' '.join(module_plugin_types)}")
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
):
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)
# ind = spaces(indent)
for p in packages:
cm_fh.write(generate_find_package_info(p, use_qt_find_package=False, indent=indent))
if packages:
cm_fh.write("\n")
def write_jar(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
target = scope.TARGET
install_dir = scope.expandString("target.path")
if not install_dir:
raise RuntimeError("Could not locate jar install path")
install_dir = install_dir.replace("$$[QT_INSTALL_PREFIX]/", "")
android_sdk_jar = "${QT_ANDROID_JAR}"
android_api_level = scope.get_string("API_VERSION")
if android_api_level:
cm_fh.write(
f'{spaces(indent)}qt_get_android_sdk_jar_for_api("{android_api_level}" android_sdk)\n\n'
)
android_sdk_jar = "${android_sdk}"
write_source_file_list(
cm_fh, scope, "", ["JAVASOURCES"], indent=indent, header=f"set(java_sources\n", footer=")\n"
)
cm_fh.write(f"{spaces(indent)}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 write_win32_and_mac_bundle_properties(cm_fh: IO[str], scope: Scope, target: str, *,
handling_first_scope=False, indent: int = 0):
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"])
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 handling_first_scope or (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 write_example(
cm_fh: IO[str], scope: Scope, gui: bool = False, *, indent: int = 0, is_plugin: bool = False
) -> str:
binary_name = scope.TARGET
assert binary_name
example_install_dir = scope.expandString("target.path")
if not example_install_dir:
example_install_dir = "${INSTALL_EXAMPLESDIR}"
example_install_dir = example_install_dir.replace(
"$$[QT_INSTALL_EXAMPLES]", "${INSTALL_EXAMPLESDIR}"
)
cm_fh.write(
"cmake_minimum_required(VERSION 3.14)\n"
f"project({binary_name} LANGUAGES CXX)\n\n"
"set(CMAKE_INCLUDE_CURRENT_DIR ON)\n\n"
"set(CMAKE_AUTOMOC ON)\n"
"set(CMAKE_AUTORCC ON)\n"
"set(CMAKE_AUTOUIC ON)\n\n"
"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)
(public_libs, private_libs) = extract_cmake_libraries(scope, is_example=True)
write_find_package_section(cm_fh, public_libs, private_libs, indent=indent)
add_target = ""
if is_plugin:
if "qml" in scope.get("QT"):
# Get the uri from the destination directory
dest_dir = scope.expandString("DESTDIR")
if not dest_dir:
dest_dir = "${CMAKE_CURRENT_BINARY_DIR}"
else:
uri = os.path.basename(dest_dir)
dest_dir = f"${{CMAKE_CURRENT_BINARY_DIR}}/{dest_dir}"
add_target = ""
qml_dir = None
qml_dir_dynamic_imports = False
qmldir_file_path_list = scope.get_files("qmldir.files")
assert len(qmldir_file_path_list) < 2, "File path must only contain one path"
qmldir_file_path = qmldir_file_path_list[0] if qmldir_file_path_list else "qmldir"
qmldir_file_path = os.path.join(os.getcwd(), qmldir_file_path[0])
dynamic_qmldir = scope.get("DYNAMIC_QMLDIR")
if os.path.exists(qmldir_file_path):
qml_dir = QmlDir()
qml_dir.from_file(qmldir_file_path)
elif dynamic_qmldir:
qml_dir = QmlDir()
qml_dir.from_lines(dynamic_qmldir)
qml_dir_dynamic_imports = True
add_target += "set(module_dynamic_qml_imports\n "
if len(qml_dir.imports) != 0:
add_target += "\n ".join(qml_dir.imports)
add_target += "\n)\n\n"
for sc in scopes[1:]:
import_list = []
qml_imports = sc.get("DYNAMIC_QMLDIR")
for qml_import in qml_imports:
if not qml_import.startswith("import "):
raise RuntimeError(
"Only qmldir import statements expected in conditional scope!"
)
import_list.append(qml_import[len("import ") :].replace(" ", "/"))
if len(import_list) == 0:
continue
assert sc.condition
add_target += f"if ({sc.condition})\n"
add_target += f" list(APPEND module_dynamic_qml_imports\n "
add_target += "\n ".join(import_list)
add_target += f"\n )\nendif()\n\n"
add_target += dedent(
f"""\
qt6_add_qml_module({binary_name}
OUTPUT_DIRECTORY "{dest_dir}"
VERSION 1.0
URI "{uri}"
"""
)
if qml_dir is not None:
if qml_dir.designer_supported:
add_target += " DESIGNER_SUPPORTED\n"
if len(qml_dir.classname) != 0:
add_target += f" CLASSNAME {qml_dir.classname}\n"
if len(qml_dir.depends) != 0:
add_target += " DEPENDENCIES\n"
for dep in qml_dir.depends:
add_target += f" {dep[0]}/{dep[1]}\n"
if len(qml_dir.type_names) == 0:
add_target += " SKIP_TYPE_REGISTRATION\n"
if len(qml_dir.imports) != 0 and not qml_dir_dynamic_imports:
qml_dir_imports_line = " \n".join(qml_dir.imports)
add_target += f" IMPORTS\n{qml_dir_imports_line}"
if qml_dir_dynamic_imports:
add_target += " IMPORTS ${module_dynamic_qml_imports}\n"
if len(qml_dir.optional_imports) != 0:
qml_dir_optional_imports_line = " \n".join(qml_dir.optional_imports)
add_target += f" OPTIONAL_IMPORTS\n{qml_dir_optional_imports_line}"
if qml_dir.plugin_optional:
add_target += " PLUGIN_OPTIONAL\n"
add_target += " INSTALL_LOCATION ${INSTALL_EXAMPLEDIR}\n)\n\n"
add_target += f"target_sources({binary_name} PRIVATE"
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}'
write_all_source_file_lists(cm_fh, scope, add_target, indent=0)
cm_fh.write(")\n")
handling_first_scope = True
for scope in scopes:
# write wayland already has condition scope handling
write_wayland_part(cm_fh, binary_name, scope, indent=0)
# The following options do not
io_string = io.StringIO()
condition_str = ""
condition = "ON"
if scope.total_condition:
condition = map_to_cmake_condition(scope.total_condition)
if condition != "ON":
condition_str = f"\n{spaces(indent)}if({condition})\n"
indent += 1
if not handling_first_scope:
target_sources = f"target_sources({binary_name} PUBLIC"
write_all_source_file_lists(
io_string, scope, target_sources, indent=indent, footer=")\n"
)
write_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"
)
write_resources(io_string, binary_name, scope, indent=indent, is_example=True)
write_statecharts(io_string, binary_name, scope, indent=indent, is_example=True)
write_repc_files(io_string, binary_name, scope, indent=indent)
if condition != "ON":
indent -= 1
string = io_string.getvalue()
if len(string) != 0:
string = string.rstrip("\n")
cm_fh.write(f"{condition_str}{string}\n")
if condition != "ON":
cm_fh.write(f"{spaces(indent)}endif()\n")
handling_first_scope = False
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", "${CMAKE_PROJECT_VERSION_MINOR}"),
("$$QT_VERSION", "${CMAKE_PROJECT_VERSION}"),
]
for needle, replacement in replacements:
import_version = import_version.replace(needle, replacement)
return import_version
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(f" list(APPEND module_dynamic_qml_imports\n ")
cm_fh.write("\n ".join(import_list))
cm_fh.write(f"\n )\nendif()\n\n")
if qml_dir is not None:
if qml_dir.designer_supported:
extra_lines.append("DESIGNER_SUPPORTED")
if len(qml_dir.classname) != 0:
extra_lines.append(f"CLASSNAME {qml_dir.classname}")
if len(qml_dir.depends) != 0:
extra_lines.append("DEPENDENCIES")
for dep in qml_dir.depends:
extra_lines.append(f" {dep[0]}/{dep[1]}")
if len(qml_dir.type_names) == 0:
extra_lines.append("SKIP_TYPE_REGISTRATION")
if len(qml_dir.imports) != 0 and not qml_dir_dynamic_imports:
qml_dir_imports_line = "\n ".join(qml_dir.imports)
extra_lines.append("IMPORTS\n " f"{qml_dir_imports_line}")
if qml_dir_dynamic_imports:
extra_lines.append("IMPORTS ${module_dynamic_qml_imports}")
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
) -> 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)
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 "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(
f"""\
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.0.0")\n')
except FileExistsError:
pass
def find_top_level_repo_project_file(project_file_path: str = "") -> Optional[str]:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_dir = os.path.dirname(qmake_conf_path)
# Hope to a programming god that there's only one .pro file at the
# top level directory of repository.
glob_result = glob.glob(os.path.join(qmake_dir, "*.pro"))
if len(glob_result) > 0:
return glob_result[0]
return None
def handle_top_level_repo_tests_project(scope: Scope, cm_fh: IO[str]):
content = dedent(
f"""\
if(QT_BUILD_STANDALONE_TESTS)
# Add qt_find_package calls for extra dependencies that need to be found when building
# the standalone tests here.
endif()
qt_build_tests()
"""
)
cm_fh.write(f"{content}")
def write_regular_cmake_target_scope_section(
scope: Scope, cm_fh: IO[str], indent: int = 0, skip_sources: bool = False
):
if not skip_sources:
target_sources = "target_sources(${PROJECT_NAME} PUBLIC"
write_all_source_file_lists(cm_fh, scope, target_sources, indent=indent, footer=")")
write_include_paths(
cm_fh,
scope,
f"target_include_directories(${{PROJECT_NAME}} PUBLIC",
indent=indent,
footer=")",
)
write_defines(
cm_fh,
scope,
f"target_compile_definitions(${{PROJECT_NAME}} PUBLIC",
indent=indent,
footer=")",
)
(public_libs, private_libs) = extract_cmake_libraries(scope)
write_list(
cm_fh,
private_libs,
"",
indent=indent,
header=f"target_link_libraries(${{PROJECT_NAME}} PRIVATE\n",
footer=")",
)
write_list(
cm_fh,
public_libs,
"",
indent=indent,
header=f"target_link_libraries(${{PROJECT_NAME}} PUBLIC\n",
footer=")",
)
write_compile_options(
cm_fh, scope, f"target_compile_options(${{PROJECT_NAME}}", indent=indent, footer=")"
)
def handle_config_test_project(scope: Scope, cm_fh: IO[str]):
project_name = os.path.splitext(os.path.basename(scope.file_absolute_path))[0]
content = (
f"cmake_minimum_required(VERSION 3.14.0)\n"
f"project(config_test_{project_name} LANGUAGES C CXX)\n"
"""
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 = f"add_executable(${{PROJECT_NAME}}"
temp_buffer = io.StringIO()
write_all_source_file_lists(temp_buffer, scope, add_target, indent=0)
buffer_value = temp_buffer.getvalue()
if buffer_value:
cm_fh.write(buffer_value)
else:
cm_fh.write(add_target)
cm_fh.write(")\n")
indent = 0
write_regular_cmake_target_scope_section(scope, cm_fh, indent, skip_sources=True)
recursive_evaluate_scope(scope)
scopes = flatten_scopes(scope)
scopes = merge_scopes(scopes)
assert len(scopes)
assert scopes[0].total_condition == "ON"
for c in scopes[1:]:
extend_scope_io_string = io.StringIO()
write_regular_cmake_target_scope_section(c, extend_scope_io_string, indent=indent + 1)
extend_string = extend_scope_io_string.getvalue()
if extend_string:
assert c.total_condition, "Cannot write if with empty condition"
extend_scope = (
f"\nif({map_to_cmake_condition(c.total_condition)})\n"
f"{extend_string}"
f"endif()\n"
)
cm_fh.write(extend_scope)
def cmakeify_scope(
scope: Scope, cm_fh: IO[str], *, indent: int = 0, is_example: bool = False
) -> None:
template = scope.TEMPLATE
temp_buffer = io.StringIO()
# Handle top level repo project in a special way.
if is_top_level_repo_project(scope.file_absolute_path):
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, debug: bool = False) -> None:
if debug:
print("Generating CMakeLists.gen.txt")
with open(scope.generated_cmake_lists_path, "w") as cm_fh:
assert scope.file
cm_fh.write(f"# Generated from {os.path.basename(scope.file)}.\n\n")
is_example_heuristic = is_example_project(scope.file_absolute_path)
final_is_example_decision = is_example or is_example_heuristic
cmakeify_scope(scope, cm_fh, is_example=final_is_example_decision)
def do_include(scope: Scope, *, debug: bool = False) -> None:
for c in scope.children:
do_include(c)
for include_index, include_file in enumerate(scope.get_files("_INCLUDED", is_include=True)):
if not include_file:
continue
# 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_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
# Skip cmake auto tests, they should not be converted.
if project_relative_path.startswith("tests/auto/cmake"):
return False
if project_relative_path.startswith("tests/auto/installed_cmake"):
return False
# Skip qmake testdata projects.
if project_relative_path.startswith("tests/auto/tools/qmake/testdata"):
return False
# Skip 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 exsists.
detected_cmake_api_version = detect_cmake_api_version_used_in_file_content(
file_relative_path
)
if detected_cmake_api_version:
cmake_api_version = detected_cmake_api_version
if args.debug_parse_result or args.debug:
print("\n\n#### Parser result:")
print(parseresult)
print("\n#### End of parser result.\n")
if args.debug_parse_dictionary or args.debug:
print("\n\n####Parser result dictionary:")
print(parseresult.asDict())
print("\n#### End of parser result dictionary.\n")
file_scope = Scope.FromDict(
None,
file_relative_path,
parseresult.asDict().get("statements"),
project_file_content=project_file_content,
)
if args.debug_pro_structure or args.debug:
print("\n\n#### .pro/.pri file structure:")
file_scope.dump()
print("\n#### End of .pro/.pri file structure.\n")
do_include(file_scope, debug=debug_parsing)
if args.debug_full_pro_structure or args.debug:
print("\n\n#### Full .pro/.pri file structure:")
file_scope.dump()
print("\n#### End of full .pro/.pri file structure.\n")
if not should_convert_project_after_parsing(file_scope, args.skip_subdirs_project):
print(f'Skipping conversion of project: "{project_file_absolute_path}"')
continue
generate_new_cmakelists(file_scope, is_example=args.is_example, debug=args.debug)
copy_generated_file = True
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()