808a63e761
Change-Id: I69954ccc5cfb44e7bf02b8fcab18e9320e7e8748 Reviewed-by: Jani Heikkinen <jani.heikkinen@qt.io>
5103 lines
177 KiB
Python
Executable File
5103 lines
177 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (C) 2018 The Qt Company Ltd.
|
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
|
|
# Requires Python 3.7. The import statement needs to be the first line of code
|
|
# so it's not possible to conditionally check the version and raise an
|
|
# exception.
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import os.path
|
|
import posixpath
|
|
import sys
|
|
import re
|
|
import io
|
|
import itertools
|
|
import glob
|
|
import fnmatch
|
|
|
|
from condition_simplifier import simplify_condition
|
|
from condition_simplifier_cache import set_condition_simplified_cache_enabled
|
|
|
|
import pyparsing as pp # type: ignore
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from argparse import ArgumentParser
|
|
from textwrap import dedent
|
|
from functools import lru_cache
|
|
from shutil import copyfile
|
|
from collections import defaultdict
|
|
from typing import (
|
|
List,
|
|
Optional,
|
|
Dict,
|
|
Set,
|
|
IO,
|
|
Union,
|
|
Any,
|
|
Callable,
|
|
FrozenSet,
|
|
Tuple,
|
|
Match,
|
|
Type,
|
|
)
|
|
|
|
from qmake_parser import parseProFile
|
|
from special_case_helper import SpecialCaseHandler
|
|
from helper import (
|
|
map_qt_library,
|
|
map_3rd_party_library,
|
|
is_known_3rd_party_library,
|
|
featureName,
|
|
map_platform,
|
|
find_library_info_for_target,
|
|
generate_find_package_info,
|
|
LibraryMapping,
|
|
)
|
|
|
|
cmake_version_string = "3.16"
|
|
cmake_api_version = 3
|
|
|
|
|
|
def _parse_commandline():
|
|
parser = ArgumentParser(
|
|
description="Generate CMakeLists.txt files from ." "pro files.",
|
|
epilog="Requirements: pip install -r requirements.txt",
|
|
)
|
|
parser.add_argument(
|
|
"--debug", dest="debug", action="store_true", help="Turn on all debug output"
|
|
)
|
|
parser.add_argument(
|
|
"--debug-parser",
|
|
dest="debug_parser",
|
|
action="store_true",
|
|
help="Print debug output from qmake parser.",
|
|
)
|
|
parser.add_argument(
|
|
"--debug-parse-result",
|
|
dest="debug_parse_result",
|
|
action="store_true",
|
|
help="Dump the qmake parser result.",
|
|
)
|
|
parser.add_argument(
|
|
"--debug-parse-dictionary",
|
|
dest="debug_parse_dictionary",
|
|
action="store_true",
|
|
help="Dump the qmake parser result as dictionary.",
|
|
)
|
|
parser.add_argument(
|
|
"--debug-pro-structure",
|
|
dest="debug_pro_structure",
|
|
action="store_true",
|
|
help="Dump the structure of the qmake .pro-file.",
|
|
)
|
|
parser.add_argument(
|
|
"--debug-full-pro-structure",
|
|
dest="debug_full_pro_structure",
|
|
action="store_true",
|
|
help="Dump the full structure of the qmake .pro-file " "(with includes).",
|
|
)
|
|
parser.add_argument(
|
|
"--debug-special-case-preservation",
|
|
dest="debug_special_case_preservation",
|
|
action="store_true",
|
|
help="Show all git commands and file copies.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--is-example",
|
|
action="store_true",
|
|
dest="is_example",
|
|
help="Treat the input .pro file as a Qt example.",
|
|
)
|
|
parser.add_argument(
|
|
"--is-user-project",
|
|
action="store_true",
|
|
dest="is_user_project",
|
|
help="Treat the input .pro file as a user project.",
|
|
)
|
|
parser.add_argument(
|
|
"-s",
|
|
"--skip-special-case-preservation",
|
|
dest="skip_special_case_preservation",
|
|
action="store_true",
|
|
help="Skips behavior to reapply " "special case modifications (requires git in PATH)",
|
|
)
|
|
parser.add_argument(
|
|
"-k",
|
|
"--keep-temporary-files",
|
|
dest="keep_temporary_files",
|
|
action="store_true",
|
|
help="Don't automatically remove CMakeLists.gen.txt and other " "intermediate files.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-e",
|
|
"--skip-condition-cache",
|
|
dest="skip_condition_cache",
|
|
action="store_true",
|
|
help="Don't use condition simplifier cache (conversion speed may decrease).",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--skip-subdirs-project",
|
|
dest="skip_subdirs_project",
|
|
action="store_true",
|
|
help="Skip converting project if it ends up being a TEMPLATE=subdirs project.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-i",
|
|
"--ignore-skip-marker",
|
|
dest="ignore_skip_marker",
|
|
action="store_true",
|
|
help="If set, pro file will be converted even if skip marker is found in CMakeLists.txt.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--api-version",
|
|
dest="api_version",
|
|
type=int,
|
|
help="Specify which cmake api version should be generated. 1, 2 or 3, 3 is latest.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output-file",
|
|
dest="output_file",
|
|
type=str,
|
|
help="Specify a file path where the generated content should be written to. "
|
|
"Default is to write to CMakeLists.txt in the same directory as the .pro file.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"files",
|
|
metavar="<.pro/.pri file>",
|
|
type=str,
|
|
nargs="+",
|
|
help="The .pro/.pri file to process",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def get_top_level_repo_project_path(project_file_path: str = "") -> str:
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
return qmake_or_cmake_conf_dir_path
|
|
|
|
|
|
def is_top_level_repo_project(project_file_path: str = "") -> bool:
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
project_dir_path = os.path.dirname(project_file_path)
|
|
return qmake_or_cmake_conf_dir_path == project_dir_path
|
|
|
|
|
|
def is_top_level_repo_tests_project(project_file_path: str = "") -> bool:
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
project_dir_path = os.path.dirname(project_file_path)
|
|
project_dir_name = os.path.basename(project_dir_path)
|
|
maybe_same_level_dir_path = os.path.join(project_dir_path, "..")
|
|
normalized_maybe_same_level_dir_path = os.path.normpath(maybe_same_level_dir_path)
|
|
return (
|
|
qmake_or_cmake_conf_dir_path == normalized_maybe_same_level_dir_path
|
|
and project_dir_name == "tests"
|
|
)
|
|
|
|
|
|
def is_top_level_repo_examples_project(project_file_path: str = "") -> bool:
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
project_dir_path = os.path.dirname(project_file_path)
|
|
project_dir_name = os.path.basename(project_dir_path)
|
|
maybe_same_level_dir_path = os.path.join(project_dir_path, "..")
|
|
normalized_maybe_same_level_dir_path = os.path.normpath(maybe_same_level_dir_path)
|
|
return (
|
|
qmake_or_cmake_conf_dir_path == normalized_maybe_same_level_dir_path
|
|
and project_dir_name == "examples"
|
|
)
|
|
|
|
|
|
def is_example_project(project_file_path: str = "") -> bool:
|
|
# If there's a .qmake.conf or .cmake.conf file in the parent
|
|
# directories of the given project path, it is likely that the
|
|
# project is an internal Qt project that uses private Qt CMake
|
|
# API.
|
|
found_qt_repo_version = False
|
|
qmake_conf = find_qmake_conf(project_file_path)
|
|
if qmake_conf:
|
|
repo_version = parse_qt_repo_module_version_from_qmake_conf(qmake_conf)
|
|
if repo_version:
|
|
found_qt_repo_version = True
|
|
|
|
cmake_conf = find_cmake_conf(project_file_path)
|
|
if cmake_conf:
|
|
repo_version = parse_qt_repo_module_version_from_cmake_conf(cmake_conf)
|
|
if repo_version:
|
|
found_qt_repo_version = True
|
|
|
|
# If we haven't found a conf file, we assume this is an example
|
|
# project and not a project under a qt source repository.
|
|
if not found_qt_repo_version:
|
|
return True
|
|
|
|
# If the project file is found in a subdir called 'examples'
|
|
# relative to the repo source dir, then it must be an example, but
|
|
# some examples contain 3rdparty libraries that do not need to be
|
|
# built as examples.
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_conf_dir_path)
|
|
|
|
is_example_under_repo_sources = (
|
|
project_relative_path.startswith("examples") and "3rdparty" not in project_relative_path
|
|
)
|
|
return is_example_under_repo_sources
|
|
|
|
|
|
def is_config_test_project(project_file_path: str = "") -> bool:
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
dir_name_with_qmake_or_cmake_conf = os.path.basename(qmake_or_cmake_conf_dir_path)
|
|
|
|
project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_conf_dir_path)
|
|
# If the project file is found in a subdir called 'config.tests'
|
|
# relative to the repo source dir, then it's probably a config test.
|
|
# Also if the .qmake.conf is found within config.tests dir (like in qtbase)
|
|
# then the project is probably a config .test
|
|
return (
|
|
project_relative_path.startswith("config.tests")
|
|
or dir_name_with_qmake_or_cmake_conf == "config.tests"
|
|
)
|
|
|
|
|
|
def is_benchmark_project(project_file_path: str = "") -> bool:
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
|
|
project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_conf_dir_path)
|
|
# If the project file is found in a subdir called 'tests/benchmarks'
|
|
# relative to the repo source dir, then it must be a benchmark
|
|
return project_relative_path.startswith("tests/benchmarks")
|
|
|
|
|
|
def is_manual_test_project(project_file_path: str = "") -> bool:
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
|
|
project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_conf_dir_path)
|
|
# If the project file is found in a subdir called 'tests/manual'
|
|
# relative to the repo source dir, then it must be a manual test
|
|
return project_relative_path.startswith("tests/manual")
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
def find_file_walking_parent_dirs(file_name: str, project_file_path: str = "") -> str:
|
|
assert file_name
|
|
if not os.path.isabs(project_file_path):
|
|
print(
|
|
f"Warning: could not find {file_name} file, given path is not an "
|
|
f"absolute path: {project_file_path}"
|
|
)
|
|
return ""
|
|
|
|
cwd = os.path.dirname(project_file_path)
|
|
|
|
while os.path.isdir(cwd):
|
|
maybe_file = posixpath.join(cwd, file_name)
|
|
if os.path.isfile(maybe_file):
|
|
return maybe_file
|
|
else:
|
|
last_cwd = cwd
|
|
cwd = os.path.dirname(cwd)
|
|
if last_cwd == cwd:
|
|
# reached the top level directory, stop looking
|
|
break
|
|
|
|
return ""
|
|
|
|
|
|
def find_qmake_conf(project_file_path: str = "") -> str:
|
|
return find_file_walking_parent_dirs(".qmake.conf", project_file_path)
|
|
|
|
|
|
def find_cmake_conf(project_file_path: str = "") -> str:
|
|
return find_file_walking_parent_dirs(".cmake.conf", project_file_path)
|
|
|
|
|
|
def find_qmake_or_cmake_conf(project_file_path: str = "") -> str:
|
|
qmake_conf = find_qmake_conf(project_file_path)
|
|
if qmake_conf:
|
|
return qmake_conf
|
|
cmake_conf = find_cmake_conf(project_file_path)
|
|
return cmake_conf
|
|
|
|
|
|
def parse_qt_repo_module_version_from_qmake_conf(qmake_conf_path: str = "") -> str:
|
|
with open(qmake_conf_path) as f:
|
|
file_contents = f.read()
|
|
m = re.search(r"MODULE_VERSION\s*=\s*([0-9.]+)", file_contents)
|
|
return m.group(1) if m else ""
|
|
|
|
|
|
def parse_qt_repo_module_version_from_cmake_conf(cmake_conf_path: str = "") -> str:
|
|
with open(cmake_conf_path) as f:
|
|
file_contents = f.read()
|
|
m = re.search(r'set\(QT_REPO_MODULE_VERSION\s*"([0-9.]+)"\)', file_contents)
|
|
return m.group(1) if m else ""
|
|
|
|
|
|
def set_up_cmake_api_calls():
|
|
def nested_dict():
|
|
return defaultdict(nested_dict)
|
|
|
|
api = nested_dict()
|
|
|
|
api[1]["qt_extend_target"] = "extend_target"
|
|
api[1]["qt_add_module"] = "add_qt_module"
|
|
api[1]["qt_add_plugin"] = "add_qt_plugin"
|
|
api[1]["qt_add_tool"] = "add_qt_tool"
|
|
api[2]["qt_internal_add_app"] = "qt_internal_add_app"
|
|
api[1]["qt_add_test"] = "add_qt_test"
|
|
api[1]["qt_add_test_helper"] = "add_qt_test_helper"
|
|
api[1]["qt_add_manual_test"] = "add_qt_manual_test"
|
|
api[1]["qt_add_benchmark"] = "add_qt_benchmark"
|
|
api[1]["qt_add_executable"] = "add_qt_executable"
|
|
api[1]["qt_add_simd_part"] = "add_qt_simd_part"
|
|
api[1]["qt_add_docs"] = "add_qt_docs"
|
|
api[1]["qt_add_resource"] = "add_qt_resource"
|
|
api[1]["qt_add_qml_module"] = "add_qml_module"
|
|
api[1]["qt_add_cmake_library"] = "add_cmake_library"
|
|
api[1]["qt_add_3rdparty_library"] = "qt_add_3rdparty_library"
|
|
api[1]["qt_create_tracepoints"] = "qt_create_tracepoints"
|
|
|
|
api[2]["qt_extend_target"] = "qt_extend_target"
|
|
api[2]["qt_add_module"] = "qt_add_module"
|
|
api[2]["qt_add_plugin"] = "qt_internal_add_plugin"
|
|
api[2]["qt_add_tool"] = "qt_add_tool"
|
|
api[2]["qt_internal_add_app"] = "qt_internal_add_app"
|
|
api[2]["qt_add_test"] = "qt_add_test"
|
|
api[2]["qt_add_test_helper"] = "qt_add_test_helper"
|
|
api[2]["qt_add_manual_test"] = "qt_add_manual_test"
|
|
api[2]["qt_add_benchmark"] = "qt_add_benchmark"
|
|
api[2]["qt_add_executable"] = "qt_add_executable"
|
|
api[2]["qt_add_simd_part"] = "qt_add_simd_part"
|
|
api[2]["qt_add_docs"] = "qt_add_docs"
|
|
api[2]["qt_add_resource"] = "qt_add_resource"
|
|
api[2]["qt_add_qml_module"] = "qt_add_qml_module"
|
|
api[2]["qt_add_cmake_library"] = "qt_add_cmake_library"
|
|
api[2]["qt_add_3rdparty_library"] = "qt_add_3rdparty_library"
|
|
api[2]["qt_create_tracepoints"] = "qt_create_tracepoints"
|
|
|
|
api[3]["qt_extend_target"] = "qt_internal_extend_target"
|
|
api[3]["qt_add_module"] = "qt_internal_add_module"
|
|
api[3]["qt_add_plugin"] = "qt_internal_add_plugin"
|
|
api[3]["qt_add_tool"] = "qt_internal_add_tool"
|
|
api[3]["qt_internal_add_app"] = "qt_internal_add_app"
|
|
api[3]["qt_add_test"] = "qt_internal_add_test"
|
|
api[3]["qt_add_test_helper"] = "qt_internal_add_test_helper"
|
|
api[3]["qt_add_manual_test"] = "qt_internal_add_manual_test"
|
|
api[3]["qt_add_benchmark"] = "qt_internal_add_benchmark"
|
|
api[3]["qt_add_executable"] = "qt_internal_add_executable"
|
|
api[3]["qt_add_simd_part"] = "qt_internal_add_simd_part"
|
|
api[3]["qt_add_docs"] = "qt_internal_add_docs"
|
|
api[3]["qt_add_resource"] = "qt_internal_add_resource"
|
|
api[3]["qt_add_qml_module"] = "qt_internal_add_qml_module"
|
|
api[3]["qt_add_cmake_library"] = "qt_internal_add_cmake_library"
|
|
api[3]["qt_add_3rdparty_library"] = "qt_internal_add_3rdparty_library"
|
|
api[3]["qt_create_tracepoints"] = "qt_internal_create_tracepoints"
|
|
|
|
return api
|
|
|
|
|
|
cmake_api_calls = set_up_cmake_api_calls()
|
|
|
|
|
|
def detect_cmake_api_version_used_in_file_content(project_file_path: str) -> Optional[int]:
|
|
dir_path = os.path.dirname(project_file_path)
|
|
cmake_project_path = os.path.join(dir_path, "CMakeLists.txt")
|
|
|
|
# If file doesn't exist, None implies default version selected by
|
|
# script.
|
|
if not os.path.exists(cmake_project_path):
|
|
return None
|
|
|
|
with open(cmake_project_path, "r") as file_fd:
|
|
contents = file_fd.read()
|
|
|
|
api_call_versions = [version for version in cmake_api_calls]
|
|
api_call_versions = sorted(api_call_versions, reverse=True)
|
|
api_call_version_matches = {}
|
|
for version in api_call_versions:
|
|
versioned_api_calls = [
|
|
cmake_api_calls[version][api_call] for api_call in cmake_api_calls[version]
|
|
]
|
|
versioned_api_calls_alternatives = "|".join(versioned_api_calls)
|
|
api_call_version_matches[version] = re.search(
|
|
versioned_api_calls_alternatives, contents
|
|
)
|
|
|
|
# If new style found, return latest api version. Otherwise
|
|
# return the current version.
|
|
for version in api_call_version_matches:
|
|
if api_call_version_matches[version]:
|
|
return version
|
|
|
|
return 1
|
|
|
|
|
|
def get_cmake_api_call(api_name: str, api_version: Optional[int] = None) -> str:
|
|
if not api_version:
|
|
global cmake_api_version
|
|
api_version = cmake_api_version
|
|
if not cmake_api_calls[api_version][api_name]:
|
|
raise RuntimeError(f"No CMake API call {api_name} of version {api_version} found.")
|
|
|
|
return cmake_api_calls[api_version][api_name]
|
|
|
|
|
|
class QtResource:
|
|
def __init__(
|
|
self,
|
|
name: str = "",
|
|
prefix: str = "",
|
|
base_dir: str = "",
|
|
files: Dict[str, str] = {},
|
|
lang: str = None,
|
|
generated: bool = False,
|
|
skip_qtquick_compiler: bool = False,
|
|
) -> None:
|
|
self.name = name
|
|
self.prefix = prefix
|
|
self.base_dir = base_dir
|
|
self.files = files
|
|
self.lang = lang
|
|
self.generated = generated
|
|
self.skip_qtquick_compiler = skip_qtquick_compiler
|
|
|
|
|
|
def read_qrc_file(
|
|
filepath: str,
|
|
base_dir: str = "",
|
|
project_file_path: str = "",
|
|
skip_qtquick_compiler: bool = False,
|
|
) -> List[QtResource]:
|
|
# Hack to handle QT_SOURCE_TREE. Assume currently that it's the same
|
|
# as the qtbase source path.
|
|
qt_source_tree_literal = "${QT_SOURCE_TREE}"
|
|
if qt_source_tree_literal in filepath:
|
|
qmake_or_cmake_conf = find_qmake_or_cmake_conf(project_file_path)
|
|
|
|
if qmake_or_cmake_conf:
|
|
qt_source_tree = os.path.dirname(qmake_or_cmake_conf)
|
|
filepath = filepath.replace(qt_source_tree_literal, qt_source_tree)
|
|
else:
|
|
print(
|
|
f"Warning, could not determine QT_SOURCE_TREE location while trying "
|
|
f"to find: {filepath}"
|
|
)
|
|
|
|
resource_name = os.path.splitext(os.path.basename(filepath))[0]
|
|
dir_name = os.path.dirname(filepath)
|
|
base_dir = posixpath.join("" if base_dir == "." else base_dir, dir_name)
|
|
|
|
# Small not very thorough check to see if this a shared qrc resource
|
|
# pattern is mostly used by the tests.
|
|
if not os.path.isfile(filepath):
|
|
raise RuntimeError(f"Invalid file path given to process_qrc_file: {filepath}")
|
|
|
|
tree = ET.parse(filepath)
|
|
root = tree.getroot()
|
|
assert root.tag == "RCC"
|
|
|
|
result: List[QtResource] = []
|
|
for resource in root:
|
|
assert resource.tag == "qresource"
|
|
r = QtResource(
|
|
name=resource_name,
|
|
prefix=resource.get("prefix", "/"),
|
|
base_dir=base_dir,
|
|
lang=resource.get("lang", ""),
|
|
skip_qtquick_compiler=skip_qtquick_compiler,
|
|
)
|
|
|
|
if len(result) > 0:
|
|
r.name += str(len(result))
|
|
|
|
if not r.prefix.startswith("/"):
|
|
r.prefix = f"/{r.prefix}"
|
|
|
|
for file in resource:
|
|
path = file.text
|
|
assert path
|
|
|
|
# Get alias:
|
|
alias = file.get("alias", "")
|
|
r.files[path] = alias
|
|
|
|
result.append(r)
|
|
|
|
return result
|
|
|
|
|
|
def write_resource_source_file_properties(
|
|
sorted_files: List[str], files: Dict[str, str], base_dir: str, skip_qtquick_compiler: bool
|
|
) -> str:
|
|
output = ""
|
|
source_file_properties = defaultdict(list)
|
|
|
|
for source in sorted_files:
|
|
alias = files[source]
|
|
if alias:
|
|
source_file_properties[source].append(f'QT_RESOURCE_ALIAS "{alias}"')
|
|
# If a base dir is given, we have to write the source file property
|
|
# assignments that disable the quick compiler per file.
|
|
if base_dir and skip_qtquick_compiler:
|
|
source_file_properties[source].append("QT_SKIP_QUICKCOMPILER 1")
|
|
|
|
for full_source in source_file_properties:
|
|
per_file_props = source_file_properties[full_source]
|
|
if per_file_props:
|
|
prop_spaces = " "
|
|
per_file_props_joined = f"\n{prop_spaces}".join(per_file_props)
|
|
output += dedent(
|
|
f"""\
|
|
set_source_files_properties("{full_source}"
|
|
PROPERTIES {per_file_props_joined}
|
|
)
|
|
"""
|
|
)
|
|
|
|
return output
|
|
|
|
|
|
def write_add_qt_resource_call(
|
|
target: str,
|
|
scope: Scope,
|
|
resource_name: str,
|
|
prefix: Optional[str],
|
|
base_dir: str,
|
|
lang: Optional[str],
|
|
files: Dict[str, str],
|
|
skip_qtquick_compiler: bool,
|
|
is_example: bool,
|
|
) -> str:
|
|
output = ""
|
|
|
|
if base_dir:
|
|
base_dir_expanded = scope.expandString(base_dir)
|
|
if base_dir_expanded:
|
|
base_dir = base_dir_expanded
|
|
new_files = {}
|
|
for file_path, alias in files.items():
|
|
full_file_path = posixpath.join(base_dir, file_path)
|
|
new_files[full_file_path] = alias
|
|
files = new_files
|
|
|
|
sorted_files = sorted(files.keys())
|
|
assert sorted_files
|
|
|
|
output += write_resource_source_file_properties(
|
|
sorted_files, files, base_dir, skip_qtquick_compiler
|
|
)
|
|
|
|
# Quote file paths in case there are spaces.
|
|
sorted_files_backup = sorted_files
|
|
sorted_files = []
|
|
for source in sorted_files_backup:
|
|
if source.startswith("${"):
|
|
sorted_files.append(source)
|
|
else:
|
|
sorted_files.append(f'"{source}"')
|
|
|
|
file_list = "\n ".join(sorted_files)
|
|
output += dedent(
|
|
f"""\
|
|
set({resource_name}_resource_files
|
|
{file_list}
|
|
)\n
|
|
"""
|
|
)
|
|
file_list = f"${{{resource_name}_resource_files}}"
|
|
if skip_qtquick_compiler and not base_dir:
|
|
output += (
|
|
f"set_source_files_properties(${{{resource_name}_resource_files}}"
|
|
" PROPERTIES QT_SKIP_QUICKCOMPILER 1)\n\n"
|
|
)
|
|
|
|
prefix_expanded = scope.expandString(str(prefix))
|
|
if prefix_expanded:
|
|
prefix = prefix_expanded
|
|
params = ""
|
|
if lang:
|
|
params += f'{spaces(1)}LANG\n{spaces(2)}"{lang}"\n'
|
|
params += f'{spaces(1)}PREFIX\n{spaces(2)}"{prefix}"\n'
|
|
if base_dir:
|
|
params += f'{spaces(1)}BASE\n{spaces(2)}"{base_dir}"\n'
|
|
if is_example:
|
|
add_resource_command = "qt6_add_resources"
|
|
else:
|
|
add_resource_command = get_cmake_api_call("qt_add_resource")
|
|
output += (
|
|
f'{add_resource_command}({target} "{resource_name}"\n{params}{spaces(1)}FILES\n'
|
|
f"{spaces(2)}{file_list}\n)\n"
|
|
)
|
|
|
|
return output
|
|
|
|
|
|
class QmlDirFileInfo:
|
|
def __init__(self, file_path: str, type_name: str) -> None:
|
|
self.file_path = file_path
|
|
self.versions = ""
|
|
self.type_name = type_name
|
|
self.internal = False
|
|
self.singleton = False
|
|
self.path = ""
|
|
|
|
|
|
class QmlDir:
|
|
def __init__(self) -> None:
|
|
self.module = ""
|
|
self.plugin_name = ""
|
|
self.plugin_optional = False
|
|
self.plugin_path = ""
|
|
self.classname = ""
|
|
self.imports: List[str] = []
|
|
self.optional_imports: List[str] = []
|
|
self.type_names: Dict[str, QmlDirFileInfo] = {}
|
|
self.type_infos: List[str] = []
|
|
self.depends: List[Tuple[str, str]] = []
|
|
self.designer_supported = False
|
|
|
|
def __str__(self) -> str:
|
|
type_infos_line = " \n".join(self.type_infos)
|
|
imports_line = " \n".join(self.imports)
|
|
optional_imports_line = " \n".join(self.optional_imports)
|
|
string = f"""\
|
|
module: {self.module}
|
|
plugin: {self.plugin_optional} {self.plugin_name} {self.plugin_path}
|
|
classname: {self.classname}
|
|
type_infos:{type_infos_line}
|
|
imports:{imports_line}
|
|
optional_imports:{optional_imports_line}
|
|
dependends:
|
|
"""
|
|
for dep in self.depends:
|
|
string += f" {dep[0]} {dep[1]}\n"
|
|
string += f"designer supported: {self.designer_supported}\n"
|
|
string += "type_names:\n"
|
|
for key in self.type_names:
|
|
file_info = self.type_names[key]
|
|
string += (
|
|
f" type:{file_info.type_name} "
|
|
f"versions:{file_info.versions} "
|
|
f"path:{file_info.file_path} "
|
|
f"internal:{file_info.internal} "
|
|
f"singleton:{file_info.singleton}\n"
|
|
)
|
|
return string
|
|
|
|
def get_or_create_file_info(self, path: str, type_name: str) -> QmlDirFileInfo:
|
|
if path not in self.type_names:
|
|
self.type_names[path] = QmlDirFileInfo(path, type_name)
|
|
qmldir_file = self.type_names[path]
|
|
if qmldir_file.type_name != type_name:
|
|
raise RuntimeError("Registered qmldir file type_name does not match.")
|
|
return qmldir_file
|
|
|
|
def handle_file_internal(self, type_name: str, path: str):
|
|
qmldir_file = self.get_or_create_file_info(path, type_name)
|
|
qmldir_file.internal = True
|
|
|
|
def handle_file_singleton(self, type_name: str, version: str, path: str):
|
|
qmldir_file = self.handle_file(type_name, version, path)
|
|
qmldir_file.singleton = True
|
|
|
|
def handle_file(self, type_name: str, version: str, path: str) -> QmlDirFileInfo:
|
|
qmldir_file = self.get_or_create_file_info(path, type_name)
|
|
# If this is not the first version we've found,
|
|
# append ';' to delineate the next version; e.g.: "2.0;2.6"
|
|
if qmldir_file.versions:
|
|
qmldir_file.versions += ";"
|
|
qmldir_file.versions += version
|
|
qmldir_file.type_name = type_name
|
|
qmldir_file.path = path
|
|
return qmldir_file
|
|
|
|
def from_lines(self, lines: List[str]):
|
|
for line in lines:
|
|
self.handle_line(line)
|
|
|
|
def from_file(self, path: str):
|
|
f = open(path, "r")
|
|
if not f:
|
|
raise RuntimeError(f"Failed to open qmldir file at: {path}")
|
|
for line in f:
|
|
self.handle_line(line)
|
|
|
|
def handle_line(self, line: str):
|
|
if line.startswith("#"):
|
|
return
|
|
line = line.strip().replace("\n", "")
|
|
if len(line) == 0:
|
|
return
|
|
|
|
entries = line.split(" ")
|
|
if len(entries) == 0:
|
|
raise RuntimeError("Unexpected QmlDir file line entry")
|
|
if entries[0] == "module":
|
|
self.module = entries[1]
|
|
elif entries[0] == "singleton":
|
|
self.handle_file_singleton(entries[1], entries[2], entries[3])
|
|
elif entries[0] == "internal":
|
|
self.handle_file_internal(entries[1], entries[2])
|
|
elif entries[0] == "plugin":
|
|
self.plugin_name = entries[1]
|
|
if len(entries) > 2:
|
|
self.plugin_path = entries[2]
|
|
elif entries[0] == "optional":
|
|
if entries[1] == "plugin":
|
|
self.plugin_name = entries[2]
|
|
self.plugin_optional = True
|
|
if len(entries) > 3:
|
|
self.plugin_path = entries[3]
|
|
elif entries[1] == "import":
|
|
if len(entries) == 4:
|
|
self.optional_imports.append(entries[2] + "/" + entries[3])
|
|
else:
|
|
self.optional_imports.append(entries[2])
|
|
else:
|
|
raise RuntimeError("Only plugins and imports can be optional in qmldir files")
|
|
elif entries[0] == "classname":
|
|
self.classname = entries[1]
|
|
elif entries[0] == "typeinfo":
|
|
self.type_infos.append(entries[1])
|
|
elif entries[0] == "depends":
|
|
self.depends.append((entries[1], entries[2]))
|
|
elif entries[0] == "designersupported":
|
|
self.designer_supported = True
|
|
elif entries[0] == "import":
|
|
if len(entries) == 3:
|
|
self.imports.append(entries[1] + "/" + entries[2])
|
|
else:
|
|
self.imports.append(entries[1])
|
|
elif len(entries) == 3:
|
|
self.handle_file(entries[0], entries[1], entries[2])
|
|
else:
|
|
raise RuntimeError(f"Uhandled qmldir entry {line}")
|
|
|
|
|
|
def spaces(indent: int) -> str:
|
|
return " " * indent
|
|
|
|
|
|
def trim_leading_dot(file: str) -> str:
|
|
while file.startswith("./"):
|
|
file = file[2:]
|
|
return file
|
|
|
|
|
|
def map_to_file(f: str, scope: Scope, *, is_include: bool = False) -> str:
|
|
assert "$$" not in f
|
|
|
|
if f.startswith("${"): # Some cmake variable is prepended
|
|
return f
|
|
|
|
base_dir = scope.currentdir if is_include else scope.basedir
|
|
f = posixpath.join(base_dir, f)
|
|
|
|
return trim_leading_dot(f)
|
|
|
|
|
|
def handle_vpath(source: str, base_dir: str, vpath: List[str]) -> str:
|
|
assert "$$" not in source
|
|
|
|
if not source:
|
|
return ""
|
|
|
|
if not vpath:
|
|
return source
|
|
|
|
if os.path.exists(os.path.join(base_dir, source)):
|
|
return source
|
|
|
|
variable_pattern = re.compile(r"\$\{[A-Za-z0-9_]+\}")
|
|
match = re.match(variable_pattern, source)
|
|
if match:
|
|
# a complex, variable based path, skipping validation
|
|
# or resolving
|
|
return source
|
|
|
|
for v in vpath:
|
|
fullpath = posixpath.join(v, source)
|
|
if os.path.exists(fullpath):
|
|
return trim_leading_dot(posixpath.relpath(fullpath, base_dir))
|
|
|
|
print(f" XXXX: Source {source}: Not found.")
|
|
return f"{source}-NOTFOUND"
|
|
|
|
|
|
class Operation:
|
|
def __init__(self, value: Union[List[str], str], line_no: int = -1) -> None:
|
|
if isinstance(value, list):
|
|
self._value = value
|
|
else:
|
|
self._value = [str(value)]
|
|
self._line_no = line_no
|
|
|
|
def process(
|
|
self, key: str, sinput: List[str], transformer: Callable[[List[str]], List[str]]
|
|
) -> List[str]:
|
|
assert False
|
|
|
|
def __repr__(self):
|
|
assert False
|
|
|
|
def _dump(self):
|
|
if not self._value:
|
|
return "<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._including_scope = None # type: Optional[Scope]
|
|
self._visited_keys = set() # type: Set[str]
|
|
self._total_condition = None # type: Optional[str]
|
|
self._parent_include_line_no = parent_include_line_no
|
|
self._is_public_module = False
|
|
self._has_private_module = False
|
|
self._is_internal_qt_app = False
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"{self._scope_id}:{self._basedir}:{self._currentdir}:{self._file}:"
|
|
f"{self._condition or '<TRUE>'}"
|
|
)
|
|
|
|
def reset_visited_keys(self):
|
|
self._visited_keys = set()
|
|
|
|
def merge(self, other: "Scope") -> None:
|
|
assert self != other
|
|
other._including_scope = self
|
|
self._included_children.append(other)
|
|
|
|
@property
|
|
def scope_debug(self) -> bool:
|
|
merge = self.get_string("PRO2CMAKE_SCOPE_DEBUG").lower()
|
|
return merge == "1" or merge == "on" or merge == "yes" or merge == "true"
|
|
|
|
@property
|
|
def parent(self) -> Optional[Scope]:
|
|
return self._parent
|
|
|
|
@property
|
|
def including_scope(self) -> Optional[Scope]:
|
|
return self._including_scope
|
|
|
|
@property
|
|
def basedir(self) -> str:
|
|
return self._basedir
|
|
|
|
@property
|
|
def currentdir(self) -> str:
|
|
return self._currentdir
|
|
|
|
@property
|
|
def is_public_module(self) -> bool:
|
|
return self._is_public_module
|
|
|
|
@property
|
|
def has_private_module(self) -> bool:
|
|
return self._has_private_module
|
|
|
|
@property
|
|
def is_internal_qt_app(self) -> bool:
|
|
is_app = self._is_internal_qt_app
|
|
current_scope = self
|
|
while not is_app and current_scope.parent:
|
|
current_scope = current_scope.parent
|
|
is_app = current_scope.is_internal_qt_app
|
|
|
|
return is_app
|
|
|
|
def can_merge_condition(self):
|
|
if self._condition == "else":
|
|
return False
|
|
if self._operations:
|
|
return False
|
|
|
|
child_count = len(self._children)
|
|
if child_count == 0 or child_count > 2:
|
|
return False
|
|
assert child_count != 1 or self._children[0]._condition != "else"
|
|
return child_count == 1 or self._children[1]._condition == "else"
|
|
|
|
def settle_condition(self):
|
|
new_children: List[Scope] = []
|
|
for c in self._children:
|
|
c.settle_condition()
|
|
|
|
if c.can_merge_condition():
|
|
child = c._children[0]
|
|
child._condition = "({c._condition}) AND ({child._condition})"
|
|
new_children += c._children
|
|
else:
|
|
new_children.append(c)
|
|
self._children = new_children
|
|
|
|
@staticmethod
|
|
def FromDict(
|
|
parent_scope: Optional["Scope"],
|
|
file: str,
|
|
statements,
|
|
cond: str = "",
|
|
base_dir: str = "",
|
|
project_file_content: str = "",
|
|
parent_include_line_no: int = -1,
|
|
) -> Scope:
|
|
scope = Scope(
|
|
parent_scope=parent_scope,
|
|
qmake_file=file,
|
|
condition=cond,
|
|
base_dir=base_dir,
|
|
parent_include_line_no=parent_include_line_no,
|
|
)
|
|
for statement in statements:
|
|
if isinstance(statement, list): # Handle skipped parts...
|
|
assert not statement
|
|
continue
|
|
|
|
operation = statement.get("operation", None)
|
|
if operation:
|
|
key = statement.get("key", "")
|
|
value = statement.get("value", [])
|
|
assert key != ""
|
|
|
|
op_location_start = operation["locn_start"]
|
|
operation = operation["value"]
|
|
op_line_no = pp.lineno(op_location_start, project_file_content)
|
|
|
|
if operation == "=":
|
|
scope._append_operation(key, SetOperation(value, line_no=op_line_no))
|
|
elif operation == "-=":
|
|
scope._append_operation(key, RemoveOperation(value, line_no=op_line_no))
|
|
elif operation == "+=":
|
|
scope._append_operation(key, AddOperation(value, line_no=op_line_no))
|
|
elif operation == "*=":
|
|
scope._append_operation(key, UniqueAddOperation(value, line_no=op_line_no))
|
|
elif operation == "~=":
|
|
scope._append_operation(key, ReplaceOperation(value, line_no=op_line_no))
|
|
else:
|
|
print(f'Unexpected operation "{operation}" in scope "{scope}".')
|
|
assert False
|
|
|
|
continue
|
|
|
|
condition = statement.get("condition", None)
|
|
if condition:
|
|
Scope.FromDict(scope, file, statement.get("statements"), condition, scope.basedir)
|
|
|
|
else_statements = statement.get("else_statements")
|
|
if else_statements:
|
|
Scope.FromDict(scope, file, else_statements, "else", scope.basedir)
|
|
continue
|
|
|
|
loaded = statement.get("loaded")
|
|
if loaded:
|
|
scope._append_operation("_LOADED", UniqueAddOperation(loaded))
|
|
continue
|
|
|
|
option = statement.get("option", None)
|
|
if option:
|
|
scope._append_operation("_OPTION", UniqueAddOperation(option))
|
|
continue
|
|
|
|
included = statement.get("included", None)
|
|
if included:
|
|
included_location_start = included["locn_start"]
|
|
included = included["value"]
|
|
included_line_no = pp.lineno(included_location_start, project_file_content)
|
|
scope._append_operation(
|
|
"_INCLUDED", UniqueAddOperation(included, line_no=included_line_no)
|
|
)
|
|
continue
|
|
|
|
project_required_condition = statement.get("project_required_condition")
|
|
if project_required_condition:
|
|
scope._append_operation("_REQUIREMENTS", AddOperation(project_required_condition))
|
|
|
|
qt_no_make_tools = statement.get("qt_no_make_tools_arguments")
|
|
if qt_no_make_tools:
|
|
qt_no_make_tools = qt_no_make_tools.strip("()").strip()
|
|
qt_no_make_tools = qt_no_make_tools.split()
|
|
for entry in qt_no_make_tools:
|
|
scope._append_operation("_QT_NO_MAKE_TOOLS", AddOperation(entry))
|
|
|
|
scope.settle_condition()
|
|
|
|
if scope.scope_debug:
|
|
print(f"..... [SCOPE_DEBUG]: Created scope {scope}:")
|
|
scope.dump(indent=1)
|
|
print("..... [SCOPE_DEBUG]: <<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_or_cmake_conf_path = find_qmake_or_cmake_conf(os.path.abspath(self.currentdir))
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
project_relative_path = os.path.relpath(qmake_or_cmake_conf_dir_path, self.currentdir)
|
|
return ["${CMAKE_CURRENT_SOURCE_DIR}/" + project_relative_path]
|
|
|
|
if key == "QT_ARCH":
|
|
return ["${CMAKE_SYSTEM_PROCESSOR}"]
|
|
|
|
if key == "_PRO_FILE_PWD_":
|
|
return ["${CMAKE_CURRENT_SOURCE_DIR}"]
|
|
if key == "PWD":
|
|
if is_same_path:
|
|
return ["${CMAKE_CURRENT_SOURCE_DIR}"]
|
|
else:
|
|
return [f"${{CMAKE_CURRENT_SOURCE_DIR}}/{relative_path}"]
|
|
if key == "OUT_PWD":
|
|
if is_same_path:
|
|
return ["${CMAKE_CURRENT_BINARY_DIR}"]
|
|
else:
|
|
return [f"${{CMAKE_CURRENT_BINARY_DIR}}/{relative_path}"]
|
|
|
|
# Horrible hack. If we're returning the values for some key
|
|
# that looks like source or header files, make sure to use a
|
|
# map_files transformer, so that $$PWD values are evaluated
|
|
# in the transformer scope, otherwise relative paths will be
|
|
# broken.
|
|
# Looking at you qmltyperegistrar.pro.
|
|
eval_ops_transformer = None
|
|
if key.endswith("SOURCES") or key.endswith("HEADERS"):
|
|
|
|
def file_transformer(scope, files):
|
|
return scope._map_files(files)
|
|
|
|
eval_ops_transformer = file_transformer
|
|
return self._evalOps(key, eval_ops_transformer, [], inherit=inherit)
|
|
|
|
def get_string(self, key: str, default: str = "", inherit: bool = False) -> str:
|
|
v = self.get(key, inherit=inherit)
|
|
if len(v) == 0:
|
|
return default
|
|
if len(v) > 1:
|
|
return " ".join(v)
|
|
return v[0]
|
|
|
|
def _map_files(
|
|
self, files: List[str], *, use_vpath: bool = True, is_include: bool = False
|
|
) -> List[str]:
|
|
|
|
expanded_files = [] # type: List[str]
|
|
for f in files:
|
|
r = self._expand_value(f)
|
|
expanded_files += r
|
|
|
|
mapped_files = list(
|
|
map(lambda f: map_to_file(f, self, is_include=is_include), expanded_files)
|
|
)
|
|
|
|
if use_vpath:
|
|
result = list(
|
|
map(
|
|
lambda f: handle_vpath(f, self.basedir, self.get("VPATH", inherit=True)),
|
|
mapped_files,
|
|
)
|
|
)
|
|
else:
|
|
result = mapped_files
|
|
|
|
# strip ${CMAKE_CURRENT_SOURCE_DIR}:
|
|
result = list(
|
|
map(lambda f: f[28:] if f.startswith("${CMAKE_CURRENT_SOURCE_DIR}/") else f, result)
|
|
)
|
|
|
|
# strip leading ./:
|
|
result = list(map(lambda f: trim_leading_dot(f), result))
|
|
|
|
return result
|
|
|
|
def get_files(
|
|
self, key: str, *, use_vpath: bool = False, is_include: bool = False
|
|
) -> List[str]:
|
|
def transformer(scope, files):
|
|
return scope._map_files(files, use_vpath=use_vpath, is_include=is_include)
|
|
|
|
return list(self._evalOps(key, transformer, []))
|
|
|
|
@staticmethod
|
|
def _replace_env_var_value(value: Any) -> Any:
|
|
if not isinstance(value, str):
|
|
return value
|
|
|
|
pattern = re.compile(r"\$\$\(([A-Za-z_][A-Za-z0-9_]*)\)")
|
|
match = re.search(pattern, value)
|
|
if match:
|
|
value = re.sub(pattern, r"$ENV{\1}", value)
|
|
|
|
return value
|
|
|
|
def _expand_value(self, value: str) -> List[str]:
|
|
result = value
|
|
pattern = re.compile(r"\$\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?")
|
|
match = re.search(pattern, result)
|
|
while match:
|
|
old_result = result
|
|
match_group_0 = match.group(0)
|
|
if match_group_0 == value:
|
|
get_result = self.get(match.group(1), inherit=True)
|
|
if len(get_result) == 1:
|
|
result = get_result[0]
|
|
result = self._replace_env_var_value(result)
|
|
else:
|
|
# Recursively expand each value from the result list
|
|
# returned from self.get().
|
|
result_list: List[str] = []
|
|
for entry_value in get_result:
|
|
result_list += self._expand_value(self._replace_env_var_value(entry_value))
|
|
return result_list
|
|
else:
|
|
replacement = self.get(match.group(1), inherit=True)
|
|
replacement_str = replacement[0] if replacement else ""
|
|
if replacement_str == value:
|
|
# we have recursed
|
|
replacement_str = ""
|
|
result = result[: match.start()] + replacement_str + result[match.end() :]
|
|
result = self._replace_env_var_value(result)
|
|
|
|
if result == old_result:
|
|
return [result] # Do not go into infinite loop
|
|
|
|
match = re.search(pattern, result)
|
|
|
|
result = self._replace_env_var_value(result)
|
|
return [result]
|
|
|
|
def expand(self, key: str) -> List[str]:
|
|
value = self.get(key)
|
|
result: List[str] = []
|
|
assert isinstance(value, list)
|
|
for v in value:
|
|
result += self._expand_value(v)
|
|
return result
|
|
|
|
def expandString(self, key: str) -> str:
|
|
result = self._expand_value(self.get_string(key))
|
|
assert len(result) == 1
|
|
return result[0]
|
|
|
|
def _get_operation_at_index(self, key, index):
|
|
return self._operations[key][index]
|
|
|
|
@property
|
|
def TEMPLATE(self) -> str:
|
|
return self.get_string("TEMPLATE", "app")
|
|
|
|
def _rawTemplate(self) -> str:
|
|
return self.get_string("TEMPLATE")
|
|
|
|
@property
|
|
def TARGET(self) -> str:
|
|
target = self.expandString("TARGET") or os.path.splitext(os.path.basename(self.file))[0]
|
|
return re.sub(r"\.\./", "", target)
|
|
|
|
@property
|
|
def TARGET_ORIGINAL(self) -> str:
|
|
return self.expandString("TARGET") or os.path.splitext(os.path.basename(self.file))[0]
|
|
|
|
@property
|
|
def _INCLUDED(self) -> List[str]:
|
|
return self.get("_INCLUDED")
|
|
|
|
|
|
# Given "if(a|b):c" returns "(a|b):c". Uses pyparsing to keep the parentheses
|
|
# balanced.
|
|
def unwrap_if(input_string):
|
|
# Compute the grammar only once.
|
|
if not hasattr(unwrap_if, "if_grammar"):
|
|
|
|
def handle_expr_with_parentheses(s, l_unused, t):
|
|
# The following expression unwraps the condition via the
|
|
# additional info set by originalTextFor, thus returning the
|
|
# condition without parentheses.
|
|
condition_without_parentheses = s[t._original_start + 1 : t._original_end - 1]
|
|
|
|
# Re-add the parentheses, but with spaces in-between. This
|
|
# fixes map_condition -> map_platform to apply properly.
|
|
condition_with_parentheses = "( " + condition_without_parentheses + " )"
|
|
return condition_with_parentheses
|
|
|
|
expr_with_parentheses = pp.originalTextFor(pp.nestedExpr())
|
|
expr_with_parentheses.setParseAction(handle_expr_with_parentheses)
|
|
|
|
if_keyword = pp.Suppress(pp.Keyword("if"))
|
|
unwrap_if.if_grammar = if_keyword + expr_with_parentheses
|
|
|
|
output_string = unwrap_if.if_grammar.transformString(input_string)
|
|
return output_string
|
|
|
|
|
|
def map_condition(condition: str) -> str:
|
|
# Some hardcoded cases that are too bothersome to generalize.
|
|
condition = re.sub(
|
|
r"qtConfig\(opengles\.\)",
|
|
r"(QT_FEATURE_opengles2 OR QT_FEATURE_opengles3 OR QT_FEATURE_opengles31 OR QT_FEATURE_opengles32)",
|
|
condition,
|
|
)
|
|
condition = re.sub(
|
|
r"qtConfig\(opengl\(es1\|es2\)\?\)",
|
|
r"(QT_FEATURE_opengl OR QT_FEATURE_opengles2 OR QT_FEATURE_opengles3)",
|
|
condition,
|
|
)
|
|
condition = re.sub(r"qtConfig\(opengl\.\*\)", r"QT_FEATURE_opengl", condition)
|
|
condition = re.sub(r"^win\*$", r"win", condition)
|
|
condition = re.sub(r"^no-png$", r"NOT QT_FEATURE_png", condition)
|
|
condition = re.sub(r"contains\(CONFIG, static\)", r"NOT QT_BUILD_SHARED_LIBS", condition)
|
|
condition = re.sub(r"contains\(QT_CONFIG,\w*shared\)", r"QT_BUILD_SHARED_LIBS", condition)
|
|
condition = re.sub(r"CONFIG\(osx\)", r"MACOS", condition)
|
|
|
|
def gcc_version_handler(match_obj: Match):
|
|
operator = match_obj.group(1)
|
|
version_type = match_obj.group(2)
|
|
if operator == "equals":
|
|
operator = "STREQUAL"
|
|
elif operator == "greaterThan":
|
|
operator = "STRGREATER"
|
|
elif operator == "lessThan":
|
|
operator = "STRLESS"
|
|
|
|
version = match_obj.group(3)
|
|
return f"(QT_COMPILER_VERSION_{version_type} {operator} {version})"
|
|
|
|
# TODO: Possibly fix for other compilers.
|
|
pattern = r"(equals|greaterThan|lessThan)\(QT_GCC_([A-Z]+)_VERSION,[ ]*([0-9]+)\)"
|
|
condition = re.sub(pattern, gcc_version_handler, condition)
|
|
|
|
def windows_sdk_version_handler(match_obj: Match):
|
|
operator = match_obj.group(1)
|
|
if operator == "equals":
|
|
operator = "STREQUAL"
|
|
elif operator == "greaterThan":
|
|
operator = "STRGREATER"
|
|
elif operator == "lessThan":
|
|
operator = "STRLESS"
|
|
|
|
version = match_obj.group(2)
|
|
return f"(QT_WINDOWS_SDK_VERSION {operator} {version})"
|
|
|
|
pattern = r"(equals|greaterThan|lessThan)\(WINDOWS_SDK_VERSION,[ ]*([0-9]+)\)"
|
|
condition = re.sub(pattern, windows_sdk_version_handler, condition)
|
|
|
|
def qt_version_handler(match_obj: Match):
|
|
operator = match_obj.group(1)
|
|
if operator == "equals":
|
|
operator = "EQUAL"
|
|
elif operator == "greaterThan":
|
|
operator = "GREATER"
|
|
elif operator == "lessThan":
|
|
operator = "LESS"
|
|
|
|
operator_prefix = "VERSION_"
|
|
version_variable = "QT_VERSION"
|
|
version_flavor = match_obj.group(2)
|
|
if version_flavor:
|
|
version_variable += "_" + version_flavor[:-1]
|
|
operator_prefix = ""
|
|
|
|
version = match_obj.group(3)
|
|
return f"({version_variable} {operator_prefix}{operator} {version})"
|
|
|
|
pattern = r"(equals|greaterThan|lessThan)\(QT_(MAJOR_|MINOR_|PATCH_)?VERSION,[ ]*([0-9.]+)\)"
|
|
condition = re.sub(pattern, qt_version_handler, condition)
|
|
|
|
# Generic lessThan|equals|lessThan()
|
|
|
|
def generic_version_handler(match_obj: Match):
|
|
operator = match_obj.group(1)
|
|
if operator == "equals":
|
|
operator = "EQUAL"
|
|
elif operator == "greaterThan":
|
|
operator = "GREATER"
|
|
elif operator == "lessThan":
|
|
operator = "LESS"
|
|
|
|
variable = match_obj.group(2)
|
|
version = match_obj.group(3)
|
|
return f"({variable} {operator} {version})"
|
|
|
|
pattern = r"(equals|greaterThan|lessThan)\(([^,]+?),[ ]*([0-9]+)\)"
|
|
condition = re.sub(pattern, generic_version_handler, condition)
|
|
|
|
# Handle if(...) conditions.
|
|
condition = unwrap_if(condition)
|
|
|
|
condition = re.sub(r"\bisEmpty\s*\((.*?)\)", r"\1_ISEMPTY", condition)
|
|
condition = re.sub(
|
|
r"\bcontains\s*\(\s*(?:QT_)?CONFIG\s*,\s*c\+\+(\d+)\)",
|
|
r"cxx_std_\1 IN_LIST CMAKE_CXX_COMPILE_FEATURES",
|
|
condition,
|
|
)
|
|
condition = re.sub(r'\bcontains\s*\((.*?),\s*"?(.*?)"?\)', r"\1___contains___\2", condition)
|
|
condition = re.sub(r'\bequals\s*\((.*?),\s*"?(.*?)"?\)', r"\1___equals___\2", condition)
|
|
condition = re.sub(r'\bisEqual\s*\((.*?),\s*"?(.*?)"?\)', r"\1___equals___\2", condition)
|
|
condition = re.sub(r"\s*==\s*", "___STREQUAL___", condition)
|
|
condition = re.sub(r"\bexists\s*\((.*?)\)", r"EXISTS \1", condition)
|
|
|
|
# checking mkspec, predating gcc scope in qmake, will then be replaced by platform_mapping in helper.py
|
|
condition = condition.replace("*-g++*", "GCC")
|
|
condition = condition.replace("*g++*", "GCC")
|
|
condition = condition.replace("aix-g++*", "AIX")
|
|
condition = condition.replace("*-icc*", "ICC")
|
|
condition = condition.replace("*-clang*", "CLANG")
|
|
condition = condition.replace("*-llvm", "CLANG")
|
|
condition = condition.replace("win32-*", "WIN32")
|
|
|
|
pattern = r"CONFIG\((debug|release),debug\|release\)"
|
|
match_result = re.match(pattern, condition)
|
|
if match_result:
|
|
build_type = match_result.group(1)
|
|
if build_type == "debug":
|
|
build_type = "Debug"
|
|
elif build_type == "release":
|
|
build_type = "Release"
|
|
condition = re.sub(pattern, f"(CMAKE_BUILD_TYPE STREQUAL {build_type})", condition)
|
|
|
|
condition = condition.replace("*", "_x_")
|
|
condition = condition.replace(".$$", "__ss_")
|
|
condition = condition.replace("$$", "_ss_")
|
|
|
|
condition = condition.replace("!", "NOT ")
|
|
condition = condition.replace("&&", " AND ")
|
|
condition = condition.replace("|", " OR ")
|
|
|
|
# new conditions added by the android multi arch qmake build
|
|
condition = re.sub(r"(^| )x86((?=[^\w])|$)", "TEST_architecture_arch STREQUAL i386", condition)
|
|
condition = re.sub(r"(^| )x86_64", " TEST_architecture_arch STREQUAL x86_64", condition)
|
|
condition = re.sub(r"(^| )arm64-v8a", "TEST_architecture_arch STREQUAL arm64", condition)
|
|
condition = re.sub(r"(^| )armeabi-v7a", "TEST_architecture_arch STREQUAL arm", condition)
|
|
|
|
# some defines replacements
|
|
condition = re.sub(r"DEFINES___contains___QT_NO_CURSOR", r"(NOT QT_FEATURE_cursor)", condition)
|
|
condition = re.sub(
|
|
r"DEFINES___contains___QT_NO_TRANSLATION", r"(NOT QT_FEATURE_translation)", condition
|
|
)
|
|
condition = re.sub(r"styles___contains___fusion", r"QT_FEATURE_style_fusion", condition)
|
|
condition = re.sub(r"CONFIG___contains___largefile", r"QT_FEATURE_largefile", condition)
|
|
|
|
condition = condition.replace("cross_compile", "CMAKE_CROSSCOMPILING")
|
|
|
|
cmake_condition = ""
|
|
for part in condition.split():
|
|
# some features contain e.g. linux, that should not be
|
|
# turned upper case
|
|
feature = re.match(r"(qtConfig|qtHaveModule)\(([a-zA-Z0-9_-]+)\)", part)
|
|
if feature:
|
|
if feature.group(1) == "qtHaveModule":
|
|
part = f"TARGET {map_qt_library(feature.group(2))}"
|
|
else:
|
|
feature_name = featureName(feature.group(2))
|
|
if (
|
|
feature_name.startswith("system_")
|
|
and is_known_3rd_party_library(feature_name[7:])
|
|
and not feature_name.startswith("system_jpeg")
|
|
and not feature_name.startswith("system_zlib")
|
|
and not feature_name.startswith("system_tiff")
|
|
and not feature_name.startswith("system_assimp")
|
|
and not feature_name.startswith("system_doubleconversion")
|
|
and not feature_name.startswith("system_sqlite")
|
|
and not feature_name.startswith("system_hunspell")
|
|
and not feature_name.startswith("system_libb2")
|
|
and not feature_name.startswith("system_webp")
|
|
):
|
|
part = "ON"
|
|
elif feature == "dlopen":
|
|
part = "ON"
|
|
else:
|
|
part = "QT_FEATURE_" + feature_name
|
|
else:
|
|
part = map_platform(part)
|
|
|
|
part = part.replace("true", "ON")
|
|
part = part.replace("false", "OFF")
|
|
cmake_condition += " " + part
|
|
return cmake_condition.strip()
|
|
|
|
|
|
_path_replacements = {
|
|
"$$[QT_INSTALL_PREFIX]": "${INSTALL_DIRECTORY}",
|
|
"$$[QT_INSTALL_EXAMPLES]": "${INSTALL_EXAMPLESDIR}",
|
|
"$$[QT_INSTALL_TESTS]": "${INSTALL_TESTSDIR}",
|
|
"$$OUT_PWD": "${CMAKE_CURRENT_BINARY_DIR}",
|
|
"$$MODULE_BASE_OUTDIR": "${QT_BUILD_DIR}",
|
|
}
|
|
|
|
|
|
def replace_path_constants(path: str, scope: Scope) -> str:
|
|
"""Clean up DESTDIR and target.path"""
|
|
if path.startswith("./"):
|
|
path = f"${{CMAKE_CURRENT_BINARY_DIR}}/{path[2:]}"
|
|
elif path.startswith("../"):
|
|
path = f"${{CMAKE_CURRENT_BINARY_DIR}}/{path}"
|
|
for original, replacement in _path_replacements.items():
|
|
path = path.replace(original, replacement)
|
|
path = path.replace("$$TARGET", scope.TARGET)
|
|
return path
|
|
|
|
|
|
def handle_subdir(
|
|
scope: Scope,
|
|
cm_fh: IO[str],
|
|
*,
|
|
indent: int = 0,
|
|
is_example: bool = False,
|
|
is_user_project: bool = False,
|
|
) -> None:
|
|
|
|
# Global nested dictionary that will contain sub_dir assignments and their conditions.
|
|
# Declared as a global in order not to pollute the nested function signatures with giant
|
|
# type hints.
|
|
sub_dirs: Dict[str, Dict[str, Set[FrozenSet[str]]]] = {}
|
|
|
|
# Collects assignment conditions into global sub_dirs dict.
|
|
def collect_subdir_info(sub_dir_assignment: str, *, current_conditions: FrozenSet[str] = None):
|
|
subtraction = sub_dir_assignment.startswith("-")
|
|
if subtraction:
|
|
subdir_name = sub_dir_assignment[1:]
|
|
else:
|
|
subdir_name = sub_dir_assignment
|
|
if subdir_name not in sub_dirs:
|
|
sub_dirs[subdir_name] = {}
|
|
additions = sub_dirs[subdir_name].get("additions", set())
|
|
subtractions = sub_dirs[subdir_name].get("subtractions", set())
|
|
if current_conditions:
|
|
if subtraction:
|
|
subtractions.add(current_conditions)
|
|
else:
|
|
additions.add(current_conditions)
|
|
if additions:
|
|
sub_dirs[subdir_name]["additions"] = additions
|
|
if subtractions:
|
|
sub_dirs[subdir_name]["subtractions"] = subtractions
|
|
|
|
# Recursive helper that collects subdir info for given scope,
|
|
# and the children of the given scope.
|
|
def handle_subdir_helper(
|
|
scope: Scope,
|
|
cm_fh: IO[str],
|
|
*,
|
|
indent: int = 0,
|
|
current_conditions: FrozenSet[str] = frozenset(),
|
|
is_example: bool = False,
|
|
):
|
|
for sd in scope.get_files("SUBDIRS"):
|
|
# Collect info about conditions and SUBDIR assignments in the
|
|
# current scope.
|
|
if os.path.isdir(sd) or sd.startswith("-"):
|
|
collect_subdir_info(sd, current_conditions=current_conditions)
|
|
# For the file case, directly write into the file handle.
|
|
elif os.path.isfile(sd):
|
|
# Handle cases with SUBDIRS += Foo/bar/z.pro. We want to be able
|
|
# to generate add_subdirectory(Foo/bar) instead of parsing the full
|
|
# .pro file in the current CMakeLists.txt. This causes issues
|
|
# with relative paths in certain projects otherwise.
|
|
dirname = os.path.dirname(sd)
|
|
if dirname:
|
|
collect_subdir_info(dirname, current_conditions=current_conditions)
|
|
else:
|
|
subdir_result, project_file_content = parseProFile(sd, debug=False)
|
|
subdir_scope = Scope.FromDict(
|
|
scope,
|
|
sd,
|
|
subdir_result.asDict().get("statements"),
|
|
"",
|
|
scope.basedir,
|
|
project_file_content=project_file_content,
|
|
)
|
|
|
|
do_include(subdir_scope)
|
|
cmakeify_scope(
|
|
subdir_scope,
|
|
cm_fh,
|
|
indent=indent,
|
|
is_example=is_example,
|
|
is_user_project=is_user_project,
|
|
)
|
|
else:
|
|
print(f" XXXX: SUBDIR {sd} in {scope}: Not found.")
|
|
|
|
# Collect info about conditions and SUBDIR assignments in child
|
|
# scopes, aka recursively call the same function, but with an
|
|
# updated current_conditions frozen set.
|
|
for c in scope.children:
|
|
# Use total_condition for 'else' conditions, otherwise just use the regular value to
|
|
# simplify the logic.
|
|
child_conditions = current_conditions
|
|
child_condition = c.total_condition if c.condition == "else" else c.condition
|
|
if child_condition:
|
|
child_conditions = frozenset((*child_conditions, child_condition))
|
|
|
|
handle_subdir_helper(
|
|
c,
|
|
cm_fh,
|
|
indent=indent + 1,
|
|
current_conditions=child_conditions,
|
|
is_example=is_example,
|
|
)
|
|
|
|
def group_and_print_sub_dirs(scope: Scope, indent: int = 0) -> None:
|
|
# Simplify conditions, and group
|
|
# subdirectories with the same conditions.
|
|
grouped_sub_dirs: Dict[str, List[str]] = {}
|
|
|
|
# Wraps each element in the given interable with parentheses,
|
|
# to make sure boolean simplification happens correctly.
|
|
def wrap_in_parenthesis(iterable):
|
|
return [f"({c})" for c in iterable]
|
|
|
|
def join_all_conditions(set_of_alternatives):
|
|
# Elements within one frozen set represent one single
|
|
# alternative whose pieces are ANDed together.
|
|
# This is repeated for each alternative that would
|
|
# enable a subdir, and are thus ORed together.
|
|
final_str = ""
|
|
if set_of_alternatives:
|
|
wrapped_set_of_alternatives = [
|
|
wrap_in_parenthesis(alternative) for alternative in set_of_alternatives
|
|
]
|
|
alternatives = [
|
|
f'({" AND ".join(alternative)})' for alternative in wrapped_set_of_alternatives
|
|
]
|
|
final_str = " OR ".join(sorted(alternatives))
|
|
return final_str
|
|
|
|
for subdir_name in sub_dirs:
|
|
additions = sub_dirs[subdir_name].get("additions", set())
|
|
subtractions = sub_dirs[subdir_name].get("subtractions", set())
|
|
|
|
# An empty condition key represents the group of sub dirs
|
|
# that should be added unconditionally.
|
|
condition_key = ""
|
|
if additions or subtractions:
|
|
addition_str = join_all_conditions(additions)
|
|
if addition_str:
|
|
addition_str = f"({addition_str})"
|
|
subtraction_str = join_all_conditions(subtractions)
|
|
if subtraction_str:
|
|
subtraction_str = f"NOT ({subtraction_str})"
|
|
|
|
condition_str = addition_str
|
|
if condition_str and subtraction_str:
|
|
condition_str += " AND "
|
|
condition_str += subtraction_str
|
|
if not condition_str.rstrip("()").strip():
|
|
continue
|
|
condition_simplified = simplify_condition(condition_str)
|
|
condition_key = condition_simplified
|
|
|
|
sub_dir_list_by_key: List[str] = grouped_sub_dirs.get(condition_key, [])
|
|
sub_dir_list_by_key.append(subdir_name)
|
|
grouped_sub_dirs[condition_key] = sub_dir_list_by_key
|
|
|
|
# Print any requires() blocks.
|
|
cm_fh.write(expand_project_requirements(scope, skip_message=True))
|
|
|
|
# Print the groups.
|
|
ind = spaces(indent)
|
|
for condition_key in grouped_sub_dirs:
|
|
cond_ind = ind
|
|
if condition_key:
|
|
cm_fh.write(f"{ind}if({condition_key})\n")
|
|
cond_ind += " "
|
|
|
|
sub_dir_list_by_key = grouped_sub_dirs.get(condition_key, [])
|
|
for subdir_name in sub_dir_list_by_key:
|
|
cm_fh.write(f"{cond_ind}add_subdirectory({subdir_name})\n")
|
|
if condition_key:
|
|
cm_fh.write(f"{ind}endif()\n")
|
|
|
|
# A set of conditions which will be ANDed together. The set is recreated with more conditions
|
|
# as the scope deepens.
|
|
current_conditions: FrozenSet[str] = frozenset()
|
|
|
|
# Compute the total condition for scopes. Needed for scopes that
|
|
# have 'else' as a condition.
|
|
recursive_evaluate_scope(scope)
|
|
|
|
# Do the work.
|
|
handle_subdir_helper(
|
|
scope, cm_fh, indent=indent, current_conditions=current_conditions, is_example=is_example
|
|
)
|
|
|
|
# Make sure to exclude targets within subdirectories first.
|
|
qt_no_make_tools = scope.get("_QT_NO_MAKE_TOOLS")
|
|
if qt_no_make_tools:
|
|
ind = spaces(indent + 1)
|
|
directories_string = ""
|
|
for directory in qt_no_make_tools:
|
|
directories_string += f"{ind}{directory}\n"
|
|
cm_fh.write(
|
|
f"\nqt_exclude_tool_directories_from_default_target(\n{directories_string})\n\n"
|
|
)
|
|
|
|
# Then write the subdirectories.
|
|
group_and_print_sub_dirs(scope, indent=indent)
|
|
|
|
|
|
def sort_sources(sources: List[str]) -> List[str]:
|
|
to_sort = {} # type: Dict[str, List[str]]
|
|
for s in sources:
|
|
if s is None:
|
|
continue
|
|
|
|
path = os.path.dirname(s)
|
|
base = os.path.splitext(os.path.basename(s))[0]
|
|
if base.endswith("_p"):
|
|
base = base[:-2]
|
|
sort_name = posixpath.join(path, base)
|
|
|
|
array = to_sort.get(sort_name, [])
|
|
array.append(s)
|
|
|
|
to_sort[sort_name] = array
|
|
|
|
lines = []
|
|
for k in sorted(to_sort.keys()):
|
|
lines.append(" ".join(sorted(to_sort[k])))
|
|
|
|
return lines
|
|
|
|
|
|
def _map_libraries_to_cmake(
|
|
libraries: List[str], known_libraries: Set[str], is_example: bool = False
|
|
) -> List[str]:
|
|
result = [] # type: List[str]
|
|
is_framework = False
|
|
|
|
for lib in libraries:
|
|
if lib == "-framework":
|
|
is_framework = True
|
|
continue
|
|
if is_framework:
|
|
if is_example:
|
|
lib = f'"-framework {lib}"'
|
|
else:
|
|
lib = f"${{FW{lib}}}"
|
|
if lib.startswith("-l"):
|
|
lib = lib[2:]
|
|
|
|
if lib.startswith("-"):
|
|
lib = f"# Remove: {lib[1:]}"
|
|
else:
|
|
lib = map_3rd_party_library(lib)
|
|
|
|
if not lib or lib in result or lib in known_libraries:
|
|
continue
|
|
|
|
result.append(lib)
|
|
is_framework = False
|
|
|
|
return result
|
|
|
|
|
|
def extract_cmake_libraries(
|
|
scope: Scope, *, known_libraries: Optional[Set[str]] = None, is_example: bool = False
|
|
) -> Tuple[List[str], List[str]]:
|
|
if known_libraries is None:
|
|
known_libraries = set()
|
|
public_dependencies = [] # type: List[str]
|
|
private_dependencies = [] # type: List[str]
|
|
|
|
for key in ["QMAKE_USE", "LIBS"]:
|
|
public_dependencies += scope.expand(key)
|
|
for key in ["QMAKE_USE_PRIVATE", "QMAKE_USE_FOR_PRIVATE", "LIBS_PRIVATE"]:
|
|
private_dependencies += scope.expand(key)
|
|
|
|
for key in ["QT_FOR_PRIVATE", "QT_PRIVATE"]:
|
|
private_dependencies += [map_qt_library(q) for q in scope.expand(key)]
|
|
|
|
for key in ["QT"]:
|
|
for lib in scope.expand(key):
|
|
mapped_lib = map_qt_library(lib)
|
|
public_dependencies.append(mapped_lib)
|
|
|
|
return (
|
|
_map_libraries_to_cmake(public_dependencies, known_libraries, is_example=is_example),
|
|
_map_libraries_to_cmake(private_dependencies, known_libraries, is_example=is_example),
|
|
)
|
|
|
|
|
|
def write_header(cm_fh: IO[str], name: str, typename: str, *, indent: int = 0):
|
|
ind = spaces(indent)
|
|
comment_line = "#" * 69
|
|
cm_fh.write(f"{ind}{comment_line}\n")
|
|
cm_fh.write(f"{ind}## {name} {typename}:\n")
|
|
cm_fh.write(f"{ind}{comment_line}\n\n")
|
|
|
|
|
|
def write_scope_header(cm_fh: IO[str], *, indent: int = 0):
|
|
ind = spaces(indent)
|
|
comment_line = "#" * 69
|
|
cm_fh.write(f"\n{ind}## Scopes:\n")
|
|
cm_fh.write(f"{ind}{comment_line}\n")
|
|
|
|
|
|
def write_list(
|
|
cm_fh: IO[str],
|
|
entries: List[str],
|
|
cmake_parameter: str,
|
|
indent: int = 0,
|
|
*,
|
|
header: str = "",
|
|
footer: str = "",
|
|
prefix: str = "",
|
|
):
|
|
if not entries:
|
|
return
|
|
|
|
ind = spaces(indent)
|
|
extra_indent = ""
|
|
|
|
if header:
|
|
cm_fh.write(f"{ind}{header}")
|
|
extra_indent += " "
|
|
if cmake_parameter:
|
|
cm_fh.write(f"{ind}{extra_indent}{cmake_parameter}\n")
|
|
extra_indent += " "
|
|
for s in sort_sources(entries):
|
|
cm_fh.write(f"{ind}{extra_indent}{prefix}{s}\n")
|
|
if footer:
|
|
cm_fh.write(f"{ind}{footer}\n")
|
|
|
|
|
|
def write_source_file_list(
|
|
cm_fh: IO[str],
|
|
scope,
|
|
cmake_parameter: str,
|
|
keys: List[str],
|
|
indent: int = 0,
|
|
*,
|
|
header: str = "",
|
|
footer: str = "",
|
|
):
|
|
# collect sources
|
|
sources: List[str] = []
|
|
for key in keys:
|
|
sources += scope.get_files(key, use_vpath=True)
|
|
|
|
# Remove duplicates, like in the case when NO_PCH_SOURCES ends up
|
|
# adding the file to SOURCES, but SOURCES might have already
|
|
# contained it before. Preserves order in Python 3.7+ because
|
|
# dict keys are ordered.
|
|
sources = list(dict.fromkeys(sources))
|
|
|
|
write_list(cm_fh, sources, cmake_parameter, indent, header=header, footer=footer)
|
|
|
|
|
|
def write_all_source_file_lists(
|
|
cm_fh: IO[str],
|
|
scope: Scope,
|
|
header: str,
|
|
*,
|
|
indent: int = 0,
|
|
footer: str = "",
|
|
extra_keys: Optional[List[str]] = None,
|
|
):
|
|
if extra_keys is None:
|
|
extra_keys = []
|
|
write_source_file_list(
|
|
cm_fh,
|
|
scope,
|
|
header,
|
|
["SOURCES", "HEADERS", "OBJECTIVE_SOURCES", "OBJECTIVE_HEADERS", "NO_PCH_SOURCES", "FORMS"]
|
|
+ extra_keys,
|
|
indent,
|
|
footer=footer,
|
|
)
|
|
|
|
|
|
def write_defines(
|
|
cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = ""
|
|
):
|
|
defines = scope.expand("DEFINES")
|
|
defines += [d[2:] for d in scope.expand("QMAKE_CXXFLAGS") if d.startswith("-D")]
|
|
defines = [
|
|
d.replace('=\\\\\\"$$PWD/\\\\\\"', '="${CMAKE_CURRENT_SOURCE_DIR}/"') for d in defines
|
|
]
|
|
|
|
# Handle LIBS_SUFFIX='\\"_$${QT_ARCH}.so\\"'.
|
|
# The escaping of backslashes is still needed even if it's a raw
|
|
# string, because backslashes have a special meaning for regular
|
|
# expressions (escape next char). So we actually expect to match
|
|
# 2 backslashes in the input string.
|
|
pattern = r"""([^ ]+)='\\\\"([^ ]*)\\\\"'"""
|
|
|
|
# Replace with regular quotes, CMake will escape the quotes when
|
|
# passing the define to the compiler.
|
|
replacement = r'\1="\2"'
|
|
defines = [re.sub(pattern, replacement, d) for d in defines]
|
|
|
|
if "qml_debug" in scope.get("CONFIG"):
|
|
defines.append("QT_QML_DEBUG")
|
|
|
|
write_list(cm_fh, defines, cmake_parameter, indent, footer=footer)
|
|
|
|
|
|
def write_3rd_party_defines(
|
|
cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = ""
|
|
):
|
|
defines = scope.expand("MODULE_DEFINES")
|
|
write_list(cm_fh, defines, cmake_parameter, indent, footer=footer)
|
|
|
|
|
|
def get_include_paths_helper(scope: Scope, include_var_name: str) -> List[str]:
|
|
includes = [i.rstrip("/") or ("/") for i in scope.get_files(include_var_name)]
|
|
return includes
|
|
|
|
|
|
def write_include_paths(
|
|
cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = ""
|
|
):
|
|
includes = get_include_paths_helper(scope, "INCLUDEPATH")
|
|
write_list(cm_fh, includes, cmake_parameter, indent, footer=footer)
|
|
|
|
|
|
def write_3rd_party_include_paths(
|
|
cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = ""
|
|
):
|
|
# Used in qt_helper_lib.prf.
|
|
includes = get_include_paths_helper(scope, "MODULE_INCLUDEPATH")
|
|
|
|
# Wrap the includes in BUILD_INTERFACE generator expression, because
|
|
# the include paths point to a source dir, and CMake will error out
|
|
# when trying to create consumable exported targets.
|
|
processed_includes = []
|
|
for i in includes:
|
|
# CMake generator expressions don't seem to like relative paths.
|
|
# Make them absolute relative to the source dir.
|
|
if is_path_relative_ish(i):
|
|
i = f"${{CMAKE_CURRENT_SOURCE_DIR}}/{i}"
|
|
i = f"$<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)
|
|
|
|
|
|
# Return True if given scope belongs to a public module.
|
|
# First, traverse the parent/child hierarchy. Then, traverse the include hierarchy.
|
|
def recursive_is_public_module(scope: Scope):
|
|
if scope.is_public_module:
|
|
return True
|
|
if scope.parent:
|
|
return recursive_is_public_module(scope.parent)
|
|
if scope.including_scope:
|
|
return recursive_is_public_module(scope.including_scope)
|
|
return False
|
|
|
|
|
|
def write_library_section(
|
|
cm_fh: IO[str], scope: Scope, *, indent: int = 0, known_libraries: Optional[Set[str]] = None
|
|
):
|
|
if known_libraries is None:
|
|
known_libraries = set()
|
|
public_dependencies, private_dependencies = extract_cmake_libraries(
|
|
scope, known_libraries=known_libraries
|
|
)
|
|
|
|
is_public_module = recursive_is_public_module(scope)
|
|
|
|
# When handling module dependencies, handle QT += foo-private magic.
|
|
# This implies:
|
|
# target_link_libraries(Module PUBLIC Qt::Foo)
|
|
# target_link_libraries(Module PRIVATE Qt::FooPrivate)
|
|
# target_link_libraries(ModulePrivate INTERFACE Qt::FooPrivate)
|
|
if is_public_module:
|
|
private_module_dep_pattern = re.compile(r"^(Qt::(.+))Private$")
|
|
|
|
public_module_public_deps = []
|
|
public_module_private_deps = private_dependencies
|
|
private_module_interface_deps = []
|
|
|
|
for dep in public_dependencies:
|
|
match = re.match(private_module_dep_pattern, dep)
|
|
if match:
|
|
if match[1] not in public_module_public_deps:
|
|
public_module_public_deps.append(match[1])
|
|
private_module_interface_deps.append(dep)
|
|
if dep not in public_module_private_deps:
|
|
public_module_private_deps.append(dep)
|
|
else:
|
|
if dep not in public_module_public_deps:
|
|
public_module_public_deps.append(dep)
|
|
|
|
private_module_interface_deps.extend(
|
|
[map_qt_library(q) for q in scope.expand("QT_FOR_PRIVATE")]
|
|
)
|
|
private_module_interface_deps.extend(
|
|
_map_libraries_to_cmake(scope.expand("QMAKE_USE_FOR_PRIVATE"), known_libraries)
|
|
)
|
|
|
|
write_list(cm_fh, public_module_private_deps, "LIBRARIES", indent + 1)
|
|
write_list(cm_fh, public_module_public_deps, "PUBLIC_LIBRARIES", indent + 1)
|
|
write_list(cm_fh, private_module_interface_deps, "PRIVATE_MODULE_INTERFACE", indent + 1)
|
|
else:
|
|
write_list(cm_fh, private_dependencies, "LIBRARIES", indent + 1)
|
|
write_list(cm_fh, public_dependencies, "PUBLIC_LIBRARIES", indent + 1)
|
|
|
|
|
|
def write_autogen_section(cm_fh: IO[str], scope: Scope, *, indent: int = 0):
|
|
forms = scope.get_files("FORMS")
|
|
if forms:
|
|
write_list(cm_fh, ["uic"], "ENABLE_AUTOGEN_TOOLS", indent)
|
|
|
|
|
|
def write_sources_section(
|
|
cm_fh: IO[str], scope: Scope, *, indent: int = 0, known_libraries: Optional[Set[str]] = None
|
|
):
|
|
if known_libraries is None:
|
|
known_libraries = set()
|
|
ind = spaces(indent)
|
|
|
|
# mark RESOURCES as visited:
|
|
scope.get("RESOURCES")
|
|
|
|
write_all_source_file_lists(cm_fh, scope, "SOURCES", indent=indent + 1)
|
|
|
|
write_source_file_list(cm_fh, scope, "DBUS_ADAPTOR_SOURCES", ["DBUS_ADAPTORS"], indent + 1)
|
|
dbus_adaptor_flags = scope.expand("QDBUSXML2CPP_ADAPTOR_HEADER_FLAGS")
|
|
if dbus_adaptor_flags:
|
|
dbus_adaptor_flags_line = '" "'.join(dbus_adaptor_flags)
|
|
cm_fh.write(f"{ind} DBUS_ADAPTOR_FLAGS\n")
|
|
cm_fh.write(f'{ind} "{dbus_adaptor_flags_line}"\n')
|
|
|
|
write_source_file_list(cm_fh, scope, "DBUS_INTERFACE_SOURCES", ["DBUS_INTERFACES"], indent + 1)
|
|
dbus_interface_flags = scope.expand("QDBUSXML2CPP_INTERFACE_HEADER_FLAGS")
|
|
if dbus_interface_flags:
|
|
dbus_interface_flags_line = '" "'.join(dbus_interface_flags)
|
|
cm_fh.write(f"{ind} DBUS_INTERFACE_FLAGS\n")
|
|
cm_fh.write(f'{ind} "{dbus_interface_flags_line}"\n')
|
|
|
|
write_defines(cm_fh, scope, "DEFINES", indent=indent + 1)
|
|
|
|
write_3rd_party_defines(cm_fh, scope, "PUBLIC_DEFINES", indent=indent + 1)
|
|
|
|
write_include_paths(cm_fh, scope, "INCLUDE_DIRECTORIES", indent=indent + 1)
|
|
|
|
write_3rd_party_include_paths(cm_fh, scope, "PUBLIC_INCLUDE_DIRECTORIES", indent=indent + 1)
|
|
|
|
write_library_section(cm_fh, scope, indent=indent, known_libraries=known_libraries)
|
|
|
|
write_compile_options(cm_fh, scope, "COMPILE_OPTIONS", indent=indent + 1)
|
|
|
|
write_autogen_section(cm_fh, scope, indent=indent + 1)
|
|
|
|
link_options = scope.get("QMAKE_LFLAGS")
|
|
if link_options:
|
|
cm_fh.write(f"{ind} LINK_OPTIONS\n")
|
|
for lo in link_options:
|
|
cm_fh.write(f'{ind} "{lo}"\n')
|
|
|
|
moc_options = scope.get("QMAKE_MOC_OPTIONS")
|
|
if moc_options:
|
|
cm_fh.write(f"{ind} MOC_OPTIONS\n")
|
|
for mo in moc_options:
|
|
cm_fh.write(f'{ind} "{mo}"\n')
|
|
|
|
precompiled_header = scope.get("PRECOMPILED_HEADER")
|
|
if precompiled_header:
|
|
cm_fh.write(f"{ind} PRECOMPILED_HEADER\n")
|
|
for header in precompiled_header:
|
|
cm_fh.write(f'{ind} "{header}"\n')
|
|
|
|
no_pch_sources = scope.get("NO_PCH_SOURCES")
|
|
if no_pch_sources:
|
|
cm_fh.write(f"{ind} NO_PCH_SOURCES\n")
|
|
for source in no_pch_sources:
|
|
cm_fh.write(f'{ind} "{source}"\n')
|
|
|
|
|
|
def is_simple_condition(condition: str) -> bool:
|
|
return " " not in condition or (condition.startswith("NOT ") and " " not in condition[4:])
|
|
|
|
|
|
def write_ignored_keys(scope: Scope, indent: str) -> str:
|
|
result = ""
|
|
ignored_keys = scope.keys - scope.visited_keys
|
|
for k in sorted(ignored_keys):
|
|
if k in {
|
|
"_INCLUDED",
|
|
"_LOADED",
|
|
"TARGET",
|
|
"QMAKE_DOCS",
|
|
"QT_SOURCE_TREE",
|
|
"QT_BUILD_TREE",
|
|
"QTRO_SOURCE_TREE",
|
|
"TRACEPOINT_PROVIDER",
|
|
"PLUGIN_TYPE",
|
|
"PLUGIN_CLASS_NAME",
|
|
"CLASS_NAME",
|
|
"MODULE_PLUGIN_TYPES",
|
|
}:
|
|
# All these keys are actually reported already
|
|
continue
|
|
values = scope.get(k)
|
|
value_string = "<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 extract_resources(
|
|
target: str,
|
|
scope: Scope,
|
|
) -> Tuple[List[QtResource], List[str]]:
|
|
"""Read the resources of the given scope.
|
|
Return a tuple:
|
|
- list of QtResource objects
|
|
- list of standalone sources files that are marked as QTQUICK_COMPILER_SKIPPED_RESOURCES"""
|
|
|
|
resource_infos: List[QtResource] = []
|
|
skipped_standalone_files: List[str] = []
|
|
|
|
resources = scope.get_files("RESOURCES")
|
|
qtquickcompiler_skipped = scope.get_files("QTQUICK_COMPILER_SKIPPED_RESOURCES")
|
|
if resources:
|
|
standalone_files: List[str] = []
|
|
for r in resources:
|
|
skip_qtquick_compiler = r in qtquickcompiler_skipped
|
|
if r.endswith(".qrc"):
|
|
if "${CMAKE_CURRENT_BINARY_DIR}" in r:
|
|
resource_infos.append(
|
|
QtResource(
|
|
name=r, generated=True, skip_qtquick_compiler=skip_qtquick_compiler
|
|
)
|
|
)
|
|
continue
|
|
resource_infos += read_qrc_file(
|
|
r,
|
|
scope.basedir,
|
|
scope.file_absolute_path,
|
|
skip_qtquick_compiler=skip_qtquick_compiler,
|
|
)
|
|
else:
|
|
immediate_files = {f: "" for f in scope.get_files(f"{r}.files")}
|
|
if immediate_files:
|
|
immediate_files_filtered = []
|
|
for f in immediate_files:
|
|
immediate_files_filtered.append(f)
|
|
immediate_files = {f: "" for f in immediate_files_filtered}
|
|
scope_prefix = scope.get(f"{r}.prefix")
|
|
if scope_prefix:
|
|
immediate_prefix = scope_prefix[0]
|
|
else:
|
|
immediate_prefix = "/"
|
|
immediate_base_list = scope.get(f"{r}.base")
|
|
assert (
|
|
len(immediate_base_list) < 2
|
|
), "immediate base directory must be at most one entry"
|
|
immediate_base = replace_path_constants("".join(immediate_base_list), scope)
|
|
immediate_lang = None
|
|
immediate_name = f"qmake_{r}"
|
|
resource_infos.append(
|
|
QtResource(
|
|
name=immediate_name,
|
|
prefix=immediate_prefix,
|
|
base_dir=immediate_base,
|
|
lang=immediate_lang,
|
|
files=immediate_files,
|
|
skip_qtquick_compiler=skip_qtquick_compiler,
|
|
)
|
|
)
|
|
else:
|
|
standalone_files.append(r)
|
|
if not ("*" in r) and skip_qtquick_compiler:
|
|
skipped_standalone_files.append(r)
|
|
|
|
if standalone_files:
|
|
resource_infos.append(
|
|
QtResource(
|
|
name="qmake_immediate",
|
|
prefix="/",
|
|
base_dir="",
|
|
files={f: "" for f in standalone_files},
|
|
)
|
|
)
|
|
|
|
return (resource_infos, skipped_standalone_files)
|
|
|
|
|
|
def write_resources(
|
|
cm_fh: IO[str],
|
|
target: str,
|
|
scope: Scope,
|
|
indent: int = 0,
|
|
is_example=False,
|
|
target_ref: str = None,
|
|
resources: List[QtResource] = None,
|
|
skipped_standalone_files: List[str] = None,
|
|
):
|
|
if resources is None:
|
|
(resources, skipped_standalone_files) = extract_resources(target, scope)
|
|
if target_ref is None:
|
|
target_ref = target
|
|
|
|
qrc_output = ""
|
|
for r in resources:
|
|
name = r.name
|
|
if "*" in name:
|
|
name = expand_resource_glob(cm_fh, name)
|
|
qrc_output += write_add_qt_resource_call(
|
|
target=target_ref,
|
|
scope=scope,
|
|
resource_name=name,
|
|
prefix=r.prefix,
|
|
base_dir=r.base_dir,
|
|
lang=r.lang,
|
|
files=r.files,
|
|
skip_qtquick_compiler=r.skip_qtquick_compiler,
|
|
is_example=is_example,
|
|
)
|
|
|
|
if skipped_standalone_files:
|
|
for f in skipped_standalone_files:
|
|
qrc_output += (
|
|
f'set_source_files_properties("{f}" PROPERTIES ' f"QT_SKIP_QUICKCOMPILER 1)\n\n"
|
|
)
|
|
|
|
if qrc_output:
|
|
str_indent = spaces(indent)
|
|
cm_fh.write(f"\n{str_indent}# Resources:\n")
|
|
for line in qrc_output.split("\n"):
|
|
if line:
|
|
cm_fh.write(f"{str_indent}{line}\n")
|
|
else:
|
|
# do not add spaces to empty lines
|
|
cm_fh.write("\n")
|
|
|
|
|
|
def write_statecharts(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0, is_example=False):
|
|
sources = scope.get_files("STATECHARTS", use_vpath=True)
|
|
if not sources:
|
|
return
|
|
cm_fh.write("\n# Statecharts:\n")
|
|
if is_example:
|
|
cm_fh.write(f"qt6_add_statecharts({target}\n")
|
|
else:
|
|
cm_fh.write(f"add_qt_statecharts({target} FILES\n")
|
|
indent += 1
|
|
for f in sources:
|
|
cm_fh.write(f"{spaces(indent)}{f}\n")
|
|
cm_fh.write(")\n")
|
|
|
|
|
|
def write_qlalrsources(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
|
|
sources = scope.get_files("QLALRSOURCES", use_vpath=True)
|
|
if not sources:
|
|
return
|
|
cm_fh.write("\n# QLALR Grammars:\n")
|
|
cm_fh.write("qt_process_qlalr(\n")
|
|
indent += 1
|
|
cm_fh.write(f"{spaces(indent)}{target}\n")
|
|
cm_fh.write(f"{spaces(indent)}{';'.join(sources)}\n")
|
|
cm_fh.write(f'{spaces(indent)}""\n')
|
|
cm_fh.write(")\n")
|
|
|
|
|
|
def write_repc_files(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
|
|
for t in ["SOURCE", "REPLICA", "MERGED"]:
|
|
sources = scope.get_files("REPC_" + t, use_vpath=True)
|
|
if not sources:
|
|
continue
|
|
cm_fh.write(f"qt6_add_repc_{t.lower()}({target}\n")
|
|
indent += 1
|
|
for f in sources:
|
|
cm_fh.write(f"{spaces(indent)}{f}\n")
|
|
cm_fh.write(")\n")
|
|
|
|
|
|
def write_generic_cmake_command(
|
|
cm_fh: IO[str], command_name: str, arguments: List[str], indent: int = 0
|
|
):
|
|
ind = spaces(indent)
|
|
arguments_str = " ".join(arguments)
|
|
cm_fh.write(f"{ind}{command_name}({arguments_str})\n")
|
|
|
|
|
|
def write_set_target_properties(
|
|
cm_fh: IO[str], targets: List[str], properties: List[str], indent: int = 0
|
|
):
|
|
ind = spaces(indent)
|
|
command_name = "set_target_properties"
|
|
arguments_ind = spaces(indent + 1)
|
|
|
|
prop_pairs = [(properties[i] + " " + properties[i + 1]) for i in range(0, len(properties), 2)]
|
|
properties_str = f"\n{arguments_ind}" + f"\n{arguments_ind}".join(prop_pairs)
|
|
|
|
if len(targets) == 1:
|
|
targets_str = targets[0] + " "
|
|
else:
|
|
targets_str = (
|
|
f"\n{arguments_ind}" + f"\n{arguments_ind}".join(targets) + f"\n{arguments_ind}"
|
|
)
|
|
|
|
cm_fh.write(f"{ind}{command_name}({targets_str}PROPERTIES{properties_str}\n{ind})\n")
|
|
|
|
|
|
def write_set_source_files_properties(
|
|
cm_fh: IO[str], files: List[str], properties: List[str], indent: int = 0
|
|
):
|
|
ind = spaces(indent)
|
|
command_name = "set_source_files_properties"
|
|
arguments_ind = spaces(indent + 1)
|
|
|
|
prop_pairs = [(properties[i] + " " + properties[i + 1]) for i in range(0, len(properties), 2)]
|
|
properties_str = f"\n{arguments_ind}" + f"\n{arguments_ind}".join(prop_pairs)
|
|
|
|
if len(files) == 1:
|
|
targets_str = files[0] + " "
|
|
else:
|
|
targets_str = f"\n{arguments_ind}" + f"\n{arguments_ind}".join(files) + f"\n{arguments_ind}"
|
|
|
|
cm_fh.write(f"{ind}{command_name}({targets_str}PROPERTIES{properties_str}\n{ind})\n")
|
|
|
|
|
|
def write_target_sources(
|
|
cm_fh: IO[str], target: str, sources: List[str], visibility: str = "PRIVATE", indent: int = 0
|
|
):
|
|
command_name = "target_sources"
|
|
header = f"{command_name}({target} {visibility}\n"
|
|
write_list(cm_fh, sources, "", indent, footer=")", header=header)
|
|
|
|
|
|
def expand_project_requirements(scope: Scope, skip_message: bool = False) -> str:
|
|
requirements = ""
|
|
for requirement in scope.get("_REQUIREMENTS"):
|
|
original_condition = simplify_condition(map_condition(requirement))
|
|
inverted_requirement = simplify_condition(f"NOT ({map_condition(requirement)})")
|
|
if not skip_message:
|
|
message = f"""
|
|
{spaces(7)}message(NOTICE "Skipping the build as the condition \\"{original_condition}\\" is not met.")"""
|
|
else:
|
|
message = ""
|
|
requirements += dedent(
|
|
f"""\
|
|
if({inverted_requirement}){message}
|
|
return()
|
|
endif()
|
|
"""
|
|
)
|
|
return requirements
|
|
|
|
|
|
def write_extend_target(
|
|
cm_fh: IO[str], target: str, scope: Scope, indent: int = 0, target_ref: str = None
|
|
):
|
|
if target_ref is None:
|
|
target_ref = target
|
|
ind = spaces(indent)
|
|
extend_qt_io_string = io.StringIO()
|
|
write_sources_section(extend_qt_io_string, scope)
|
|
extend_qt_string = extend_qt_io_string.getvalue()
|
|
|
|
assert scope.total_condition, "Cannot write CONDITION when scope.condition is None"
|
|
|
|
condition = map_to_cmake_condition(scope.total_condition)
|
|
|
|
cmake_api_call = get_cmake_api_call("qt_extend_target")
|
|
extend_scope = (
|
|
f"\n{ind}{cmake_api_call}({target_ref} CONDITION"
|
|
f" {condition}\n"
|
|
f"{extend_qt_string}{ind})\n"
|
|
)
|
|
|
|
if not extend_qt_string:
|
|
extend_scope = "" # Nothing to report, so don't!
|
|
|
|
cm_fh.write(extend_scope)
|
|
|
|
io_string = io.StringIO()
|
|
write_resources(io_string, target, scope, indent + 1, target_ref=target_ref)
|
|
resource_string = io_string.getvalue()
|
|
if len(resource_string) != 0:
|
|
resource_string = resource_string.strip("\n").rstrip(f"\n{spaces(indent + 1)}")
|
|
cm_fh.write(f"\n{spaces(indent)}if({condition})\n{resource_string}")
|
|
cm_fh.write(f"\n{spaces(indent)}endif()\n")
|
|
|
|
|
|
def flatten_scopes(scope: Scope) -> List[Scope]:
|
|
result = [scope] # type: List[Scope]
|
|
for c in scope.children:
|
|
result += flatten_scopes(c)
|
|
return result
|
|
|
|
|
|
def merge_scopes(scopes: List[Scope]) -> List[Scope]:
|
|
result = [] # type: List[Scope]
|
|
|
|
# Merge scopes with their parents:
|
|
known_scopes = {} # type: Dict[str, Scope]
|
|
for scope in scopes:
|
|
total_condition = scope.total_condition
|
|
assert total_condition
|
|
if total_condition == "OFF":
|
|
# ignore this scope entirely!
|
|
pass
|
|
elif total_condition in known_scopes:
|
|
known_scopes[total_condition].merge(scope)
|
|
else:
|
|
# Keep everything else:
|
|
result.append(scope)
|
|
known_scopes[total_condition] = scope
|
|
|
|
return result
|
|
|
|
|
|
def write_simd_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
|
|
simd_options = [
|
|
"sse2",
|
|
"sse3",
|
|
"ssse3",
|
|
"sse4_1",
|
|
"sse4_2",
|
|
"aesni",
|
|
"shani",
|
|
"avx",
|
|
"avx2",
|
|
"avx512f",
|
|
"avx512cd",
|
|
"avx512er",
|
|
"avx512pf",
|
|
"avx512dq",
|
|
"avx512bw",
|
|
"avx512vl",
|
|
"avx512ifma",
|
|
"avx512vbmi",
|
|
"f16c",
|
|
"rdrnd",
|
|
"neon",
|
|
"mips_dsp",
|
|
"mips_dspr2",
|
|
"arch_haswell",
|
|
"avx512common",
|
|
"avx512core",
|
|
]
|
|
|
|
simd_io_string = io.StringIO()
|
|
|
|
condition = "ON"
|
|
if scope.total_condition:
|
|
condition = map_to_cmake_condition(scope.total_condition)
|
|
|
|
if condition != "ON":
|
|
indent += 1
|
|
|
|
for simd in simd_options:
|
|
SIMD = simd.upper()
|
|
write_source_file_list(
|
|
simd_io_string,
|
|
scope,
|
|
"SOURCES",
|
|
[f"{SIMD}_HEADERS", f"{SIMD}_SOURCES", f"{SIMD}_C_SOURCES", f"{SIMD}_ASM"],
|
|
indent=indent,
|
|
header=f"{get_cmake_api_call('qt_add_simd_part')}({target} SIMD {simd}\n",
|
|
footer=")\n",
|
|
)
|
|
|
|
simd_string = simd_io_string.getvalue()
|
|
if simd_string:
|
|
simd_string = simd_string.rstrip("\n")
|
|
cond_start = ""
|
|
cond_end = ""
|
|
if condition != "ON":
|
|
cond_start = f"{spaces(indent - 1)}if({condition})"
|
|
cond_end = f"{spaces(indent - 1)}endif()"
|
|
|
|
extend_scope = f"\n{cond_start}\n" f"{simd_string}" f"\n{cond_end}\n"
|
|
cm_fh.write(extend_scope)
|
|
|
|
|
|
def write_reduce_relocations_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
|
|
ind = spaces(indent)
|
|
dynlist_file = scope.get_files("QMAKE_DYNAMIC_LIST_FILE")
|
|
if dynlist_file:
|
|
dynlist_path = "${CMAKE_CURRENT_LIST_DIR}/" + dynlist_file[0]
|
|
cm_fh.write(f"{ind}if(QT_FEATURE_reduce_relocations AND UNIX AND GCC)\n")
|
|
ind = spaces(indent + 1)
|
|
cm_fh.write(f"{ind}target_link_options({target} PRIVATE\n")
|
|
cm_fh.write(f'{ind} "LINKER:--dynamic-list={dynlist_path}")\n')
|
|
ind = spaces(indent)
|
|
cm_fh.write(f"{ind}endif()\n")
|
|
|
|
|
|
def write_android_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
|
|
keys = [
|
|
"ANDROID_BUNDLED_JAR_DEPENDENCIES",
|
|
"ANDROID_LIB_DEPENDENCIES",
|
|
"ANDROID_JAR_DEPENDENCIES",
|
|
"ANDROID_LIB_DEPENDENCY_REPLACEMENTS",
|
|
"ANDROID_BUNDLED_FILES",
|
|
"ANDROID_PERMISSIONS",
|
|
"ANDROID_PACKAGE_SOURCE_DIR",
|
|
]
|
|
|
|
has_no_values = True
|
|
for key in keys:
|
|
value = scope.expand(key)
|
|
if len(value) != 0:
|
|
if has_no_values:
|
|
if scope.condition:
|
|
cm_fh.write(f"\n{spaces(indent)}if(ANDROID AND ({scope.condition}))\n")
|
|
else:
|
|
cm_fh.write(f"\n{spaces(indent)}if(ANDROID)\n")
|
|
indent += 1
|
|
has_no_values = False
|
|
cm_fh.write(f"{spaces(indent)}set_property(TARGET {target} APPEND PROPERTY QT_{key}\n")
|
|
write_list(cm_fh, value, "", indent + 1)
|
|
cm_fh.write(f"{spaces(indent)})\n")
|
|
indent -= 1
|
|
|
|
if not has_no_values:
|
|
cm_fh.write(f"{spaces(indent)}endif()\n")
|
|
|
|
|
|
def write_wayland_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
|
|
client_sources = scope.get_files("WAYLANDCLIENTSOURCES", use_vpath=True)
|
|
server_sources = scope.get_files("WAYLANDSERVERSOURCES", use_vpath=True)
|
|
if len(client_sources) == 0 and len(server_sources) == 0:
|
|
return
|
|
|
|
condition, indent = write_scope_condition_begin(cm_fh, scope, indent=indent)
|
|
|
|
if len(client_sources) != 0:
|
|
cm_fh.write(f"\n{spaces(indent)}qt6_generate_wayland_protocol_client_sources({target}\n")
|
|
write_list(
|
|
cm_fh, client_sources, "FILES", indent + 1, prefix="${CMAKE_CURRENT_SOURCE_DIR}/"
|
|
)
|
|
cm_fh.write(f"{spaces(indent)})\n")
|
|
|
|
if len(server_sources) != 0:
|
|
cm_fh.write(f"\n{spaces(indent)}qt6_generate_wayland_protocol_server_sources({target}\n")
|
|
write_list(
|
|
cm_fh, server_sources, "FILES", indent + 1, prefix="${CMAKE_CURRENT_SOURCE_DIR}/"
|
|
)
|
|
cm_fh.write(f"{spaces(indent)})\n")
|
|
|
|
write_scope_condition_end(cm_fh, condition, indent=indent)
|
|
|
|
|
|
def write_scope_condition_begin(cm_fh: IO[str], scope: Scope, indent: int = 0) -> Tuple[str, int]:
|
|
condition = "ON"
|
|
if scope.total_condition:
|
|
condition = map_to_cmake_condition(scope.total_condition)
|
|
|
|
if condition != "ON":
|
|
cm_fh.write(f"\n{spaces(indent)}if({condition})\n")
|
|
indent += 1
|
|
|
|
return condition, indent
|
|
|
|
|
|
def write_scope_condition_end(cm_fh: IO[str], condition: str, indent: int = 0) -> int:
|
|
if condition != "ON":
|
|
indent -= 1
|
|
cm_fh.write(f"{spaces(indent)}endif()\n")
|
|
return indent
|
|
|
|
|
|
def is_path_relative_ish(path: str) -> bool:
|
|
if not os.path.isabs(path) and not path.startswith("$"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def absolutify_path(path: str, base_dir: str = "${CMAKE_CURRENT_SOURCE_DIR}") -> str:
|
|
if not path:
|
|
return path
|
|
if is_path_relative_ish(path):
|
|
path = posixpath.join(base_dir, path)
|
|
return path
|
|
|
|
|
|
def write_version_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
|
|
if scope.is_internal_qt_app:
|
|
version_value = scope.get_string("VERSION")
|
|
if version_value:
|
|
version_value = re.sub(r"\$\${QT_VERSION\}", "${PROJECT_VERSION}", version_value)
|
|
target_description = scope.expandString("QMAKE_TARGET_DESCRIPTION")
|
|
|
|
if version_value or target_description:
|
|
condition, indent = write_scope_condition_begin(cm_fh, scope, indent=indent)
|
|
|
|
properties = []
|
|
if version_value:
|
|
properties.extend(["QT_TARGET_VERSION", f'"{version_value}"'])
|
|
if target_description:
|
|
properties.extend(["QT_TARGET_DESCRIPTION", f'"{target_description}"'])
|
|
|
|
if properties:
|
|
write_set_target_properties(cm_fh, [target], properties, indent=indent)
|
|
|
|
write_scope_condition_end(cm_fh, condition, indent=indent)
|
|
|
|
|
|
def write_darwin_part(
|
|
cm_fh: IO[str], target: str, scope: Scope, main_scope_target_name: str = "", indent: int = 0
|
|
):
|
|
if scope.is_internal_qt_app:
|
|
# Embed custom provided Info.plist file.
|
|
info_plist = scope.expandString("QMAKE_INFO_PLIST")
|
|
info_plist = absolutify_path(info_plist)
|
|
|
|
icon_path = scope.expandString("ICON")
|
|
icon_basename = ""
|
|
|
|
new_output_name = None
|
|
current_scope_output_name = scope.TARGET
|
|
if current_scope_output_name != main_scope_target_name:
|
|
new_output_name = current_scope_output_name
|
|
|
|
if icon_path:
|
|
icon_basename = os.path.basename(icon_path)
|
|
|
|
if info_plist or icon_path or new_output_name:
|
|
condition, indent = write_scope_condition_begin(cm_fh, scope, indent=indent)
|
|
|
|
properties = []
|
|
if info_plist:
|
|
properties.extend(["MACOSX_BUNDLE_INFO_PLIST", f'"{info_plist}"'])
|
|
properties.extend(["MACOSX_BUNDLE", "TRUE"])
|
|
if icon_path:
|
|
properties.extend(["MACOSX_BUNDLE_ICON_FILE", f'"{icon_basename}"'])
|
|
|
|
if new_output_name:
|
|
properties.extend(["OUTPUT_NAME", f'"{new_output_name}"'])
|
|
|
|
if properties:
|
|
write_set_target_properties(cm_fh, [target], properties, indent=indent)
|
|
if icon_path:
|
|
source_properties = ["MACOSX_PACKAGE_LOCATION", "Resources"]
|
|
write_set_source_files_properties(
|
|
cm_fh, [icon_path], source_properties, indent=indent
|
|
)
|
|
write_target_sources(cm_fh, target, [icon_path], indent=indent)
|
|
|
|
write_scope_condition_end(cm_fh, condition, indent=indent)
|
|
|
|
|
|
def write_windows_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
|
|
if scope.is_internal_qt_app:
|
|
# Handle CONFIG += console assignments.
|
|
is_console = "console" in scope.get("CONFIG")
|
|
rc_file = scope.expandString("RC_FILE")
|
|
rc_file = absolutify_path(rc_file)
|
|
rc_icons = scope.expandString("RC_ICONS")
|
|
rc_icons = absolutify_path(rc_icons)
|
|
|
|
if is_console or rc_file or rc_icons:
|
|
condition, indent = write_scope_condition_begin(cm_fh, scope, indent=indent)
|
|
|
|
properties = []
|
|
if is_console:
|
|
properties.extend(["WIN32_EXECUTABLE", "FALSE"])
|
|
|
|
if rc_file:
|
|
properties.extend(["QT_TARGET_WINDOWS_RC_FILE", f'"{rc_file}"'])
|
|
|
|
if rc_icons:
|
|
properties.extend(["QT_TARGET_RC_ICONS", f'"{rc_icons}"'])
|
|
|
|
if properties:
|
|
write_set_target_properties(cm_fh, [target], properties, indent=indent)
|
|
|
|
write_scope_condition_end(cm_fh, condition, indent=indent)
|
|
|
|
|
|
def write_aux_qml_file_install_call(cm_fh: IO[str], file_list: List[str], indent: int = 0):
|
|
cm_fh.write(f"\n{spaces(indent)}qt_copy_or_install(\n")
|
|
write_list(cm_fh, file_list, "FILES", indent + 1)
|
|
|
|
destination_option = 'DESTINATION "${__aux_qml_files_install_dir}"'
|
|
cm_fh.write(f"{spaces(indent + 1)}{destination_option})\n")
|
|
|
|
|
|
def write_aux_qml_path_setup(cm_fh: IO[str], base_dir: str, indent: int = 0):
|
|
path_join_args = f'__aux_qml_files_install_dir "${{__aux_qml_files_install_base}}" "{base_dir}"'
|
|
cm_fh.write(f"\n{spaces(indent)}qt_path_join({path_join_args})\n")
|
|
|
|
|
|
def write_aux_qml_files_part(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
|
|
aux_files = scope.get_files("AUX_QML_FILES")
|
|
if aux_files and isinstance(aux_files, list):
|
|
aux_files_per_dir = defaultdict(list)
|
|
aux_files_globs = []
|
|
|
|
# Handle globs differently from regular paths.
|
|
# For regular paths, group by base dir. Each base dir will get
|
|
# its own install call.
|
|
for path in aux_files:
|
|
if "*" in path:
|
|
aux_files_globs.append(path)
|
|
else:
|
|
base_dir = os.path.dirname(path)
|
|
aux_files_per_dir[base_dir].append(path)
|
|
|
|
condition, indent = write_scope_condition_begin(cm_fh, scope, indent=indent)
|
|
|
|
# Extract the location of $prefix/qml, where we want to install
|
|
# files.
|
|
get_prop_args = f"__aux_qml_files_install_base {target} QT_QML_MODULE_INSTALL_DIR"
|
|
cm_fh.write(f"{spaces(indent)}get_target_property({get_prop_args})\n")
|
|
|
|
# Handle glob installs.
|
|
for path in aux_files_globs:
|
|
cm_fh.write(
|
|
f"""
|
|
{spaces(indent)}file(GLOB_RECURSE __aux_qml_glob_files
|
|
{spaces(indent + 1)}RELATIVE "${{CMAKE_CURRENT_SOURCE_DIR}}"
|
|
{spaces(indent + 1)}"{path}")"""
|
|
)
|
|
file_list = ["${__aux_qml_glob_files}"]
|
|
|
|
# Extract base dir. Hopes that the globs only appear in the
|
|
# file name part.
|
|
base_dir = os.path.dirname(path)
|
|
write_aux_qml_path_setup(cm_fh, base_dir, indent=indent)
|
|
write_aux_qml_file_install_call(cm_fh, file_list, indent=indent)
|
|
|
|
# Handle regular per base-dir installs.
|
|
for base_dir in aux_files_per_dir:
|
|
file_list = aux_files_per_dir[base_dir]
|
|
write_aux_qml_path_setup(cm_fh, base_dir, indent=indent)
|
|
write_aux_qml_file_install_call(cm_fh, file_list, indent=indent)
|
|
write_scope_condition_end(cm_fh, condition, indent=indent)
|
|
|
|
|
|
def handle_source_subtractions(scopes: List[Scope]):
|
|
"""
|
|
Handles source subtractions like SOURCES -= painting/qdrawhelper.cpp
|
|
by creating a new scope with a new condition containing all addition
|
|
and subtraction conditions.
|
|
|
|
Algorithm is as follows:
|
|
- Go through each scope and find files in SOURCES starting with "-"
|
|
- Save that file and the scope condition in modified_sources dict.
|
|
- Remove the file from the found scope (optionally remove the
|
|
NO_PCH_SOURCES entry for that file as well).
|
|
- Go through each file in modified_sources dict.
|
|
- Find scopes where the file is added, remove the file from that
|
|
scope and save the condition.
|
|
- Create a new scope just for that file with a new simplified
|
|
condition that takes all the other conditions into account.
|
|
"""
|
|
|
|
def remove_file_from_operation(
|
|
scope: Scope, ops_key: str, file: str, op_type: Type[Operation]
|
|
) -> bool:
|
|
"""
|
|
Remove a source file from an operation in a scope.
|
|
Example: remove foo.cpp from any operations that have
|
|
ops_key="SOURCES" in "scope", where the operation is of
|
|
type "op_type".
|
|
|
|
The implementation is very rudimentary and might not work in
|
|
all cases.
|
|
|
|
Returns True if a file was found and removed in any operation.
|
|
"""
|
|
file_removed = False
|
|
ops = scope._operations.get(ops_key, list())
|
|
for op in ops:
|
|
if not isinstance(op, op_type):
|
|
continue
|
|
if file in op._value:
|
|
op._value.remove(file)
|
|
file_removed = True
|
|
for include_child_scope in scope._included_children:
|
|
file_removed = file_removed or remove_file_from_operation(
|
|
include_child_scope, ops_key, file, op_type
|
|
)
|
|
return file_removed
|
|
|
|
def join_all_conditions(set_of_alternatives: Set[str]):
|
|
final_str = ""
|
|
if set_of_alternatives:
|
|
alternatives = [f"({alternative})" for alternative in set_of_alternatives]
|
|
final_str = " OR ".join(sorted(alternatives))
|
|
return final_str
|
|
|
|
modified_sources: Dict[str, Dict[str, Union[Set[str], bool]]] = {}
|
|
|
|
new_scopes = []
|
|
top_most_scope = scopes[0]
|
|
|
|
for scope in scopes:
|
|
sources = scope.get_files("SOURCES")
|
|
for file in sources:
|
|
# Find subtractions.
|
|
if file.startswith("-"):
|
|
file_without_minus = file[1:]
|
|
|
|
if file_without_minus not in modified_sources:
|
|
modified_sources[file_without_minus] = {}
|
|
|
|
subtractions = modified_sources[file_without_minus].get("subtractions", set())
|
|
assert isinstance(subtractions, set)
|
|
|
|
# Add the condition to the set of conditions and remove
|
|
# the file subtraction from the processed scope, which
|
|
# will be later re-added in a new scope.
|
|
if scope.condition:
|
|
assert scope.total_condition
|
|
subtractions.add(scope.total_condition)
|
|
remove_file_from_operation(scope, "SOURCES", file_without_minus, RemoveOperation)
|
|
if subtractions:
|
|
modified_sources[file_without_minus]["subtractions"] = subtractions
|
|
|
|
# In case if the source is also listed in a
|
|
# NO_PCH_SOURCES operation, remove it from there as
|
|
# well, and add it back later.
|
|
no_pch_source_removed = remove_file_from_operation(
|
|
scope, "NO_PCH_SOURCES", file_without_minus, AddOperation
|
|
)
|
|
if no_pch_source_removed:
|
|
modified_sources[file_without_minus]["add_to_no_pch_sources"] = True
|
|
|
|
for modified_source in modified_sources:
|
|
additions = modified_sources[modified_source].get("additions", set())
|
|
assert isinstance(additions, set), f"Additions must be a set, got {additions} instead."
|
|
subtractions = modified_sources[modified_source].get("subtractions", set())
|
|
assert isinstance(
|
|
subtractions, set
|
|
), f"Subtractions must be a set, got {additions} instead."
|
|
add_to_no_pch_sources = modified_sources[modified_source].get(
|
|
"add_to_no_pch_sources", False
|
|
)
|
|
|
|
for scope in scopes:
|
|
sources = scope.get_files("SOURCES")
|
|
if modified_source in sources:
|
|
# Remove the source file from any addition operations
|
|
# that mention it.
|
|
remove_file_from_operation(scope, "SOURCES", modified_source, AddOperation)
|
|
if scope.total_condition:
|
|
additions.add(scope.total_condition)
|
|
|
|
# Construct a condition that takes into account all addition
|
|
# and subtraction conditions.
|
|
addition_str = join_all_conditions(additions)
|
|
if addition_str:
|
|
addition_str = f"({addition_str})"
|
|
subtraction_str = join_all_conditions(subtractions)
|
|
if subtraction_str:
|
|
subtraction_str = f"NOT ({subtraction_str})"
|
|
|
|
condition_str = addition_str
|
|
if condition_str and subtraction_str:
|
|
condition_str += " AND "
|
|
condition_str += subtraction_str
|
|
condition_simplified = simplify_condition(condition_str)
|
|
|
|
# Create a new scope with that condition and add the source
|
|
# operations.
|
|
new_scope = Scope(
|
|
parent_scope=top_most_scope,
|
|
qmake_file=top_most_scope.file,
|
|
condition=condition_simplified,
|
|
base_dir=top_most_scope.basedir,
|
|
)
|
|
new_scope.total_condition = condition_simplified
|
|
new_scope._append_operation("SOURCES", AddOperation([modified_source]))
|
|
if add_to_no_pch_sources:
|
|
new_scope._append_operation("NO_PCH_SOURCES", AddOperation([modified_source]))
|
|
|
|
new_scopes.append(new_scope)
|
|
|
|
# Add all the newly created scopes.
|
|
scopes += new_scopes
|
|
|
|
|
|
def write_main_part(
|
|
cm_fh: IO[str],
|
|
name: str,
|
|
typename: str,
|
|
cmake_function: str,
|
|
scope: Scope,
|
|
*,
|
|
extra_lines: Optional[List[str]] = None,
|
|
indent: int = 0,
|
|
extra_keys: List[str],
|
|
**kwargs: Any,
|
|
):
|
|
# Evaluate total condition of all scopes:
|
|
if extra_lines is None:
|
|
extra_lines = []
|
|
recursive_evaluate_scope(scope)
|
|
|
|
if "exceptions" in scope.get("CONFIG"):
|
|
extra_lines.append("EXCEPTIONS")
|
|
|
|
# Get a flat list of all scopes but the main one:
|
|
scopes = flatten_scopes(scope)
|
|
# total_scopes = len(scopes)
|
|
# Merge scopes based on their conditions:
|
|
scopes = merge_scopes(scopes)
|
|
|
|
# Handle SOURCES -= foo calls, and merge scopes one more time
|
|
# because there might have been several files removed with the same
|
|
# scope condition.
|
|
handle_source_subtractions(scopes)
|
|
scopes = merge_scopes(scopes)
|
|
|
|
assert len(scopes)
|
|
assert scopes[0].total_condition == "ON"
|
|
|
|
scopes[0].reset_visited_keys()
|
|
for k in extra_keys:
|
|
scopes[0].get(k)
|
|
|
|
# Now write out the scopes:
|
|
write_header(cm_fh, name, typename, indent=indent)
|
|
|
|
# collect all testdata and insert globbing commands
|
|
has_test_data = False
|
|
if typename == "Test":
|
|
cm_fh.write(f"{spaces(indent)}if (NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)\n")
|
|
cm_fh.write(f"{spaces(indent+1)}cmake_minimum_required(VERSION 3.16)\n")
|
|
cm_fh.write(f"{spaces(indent+1)}project({name} LANGUAGES C CXX ASM)\n")
|
|
cm_fh.write(
|
|
f"{spaces(indent+1)}find_package(Qt6BuildInternals COMPONENTS STANDALONE_TEST)\n"
|
|
)
|
|
cm_fh.write(f"{spaces(indent)}endif()\n\n")
|
|
|
|
test_data = scope.expand("TESTDATA")
|
|
if test_data:
|
|
has_test_data = True
|
|
cm_fh.write("# Collect test data\n")
|
|
for data in test_data:
|
|
if "*" in data:
|
|
cm_fh.write(
|
|
dedent(
|
|
f"""\
|
|
{spaces(indent)}file(GLOB_RECURSE test_data_glob
|
|
{spaces(indent+1)}RELATIVE ${{CMAKE_CURRENT_SOURCE_DIR}}
|
|
{spaces(indent+1)}{data})
|
|
"""
|
|
)
|
|
)
|
|
cm_fh.write(f"{spaces(indent)}list(APPEND test_data ${{test_data_glob}})\n")
|
|
else:
|
|
cm_fh.write(f'{spaces(indent)}list(APPEND test_data "{data}")\n')
|
|
cm_fh.write("\n")
|
|
|
|
target_ref = name
|
|
if typename == "Tool":
|
|
target_ref = "${target_name}"
|
|
cm_fh.write(f"{spaces(indent)}qt_get_tool_target_name(target_name {name})\n")
|
|
|
|
# Check for DESTDIR override
|
|
destdir = scope.get_string("DESTDIR")
|
|
if destdir:
|
|
already_added = False
|
|
for line in extra_lines:
|
|
if line.startswith("OUTPUT_DIRECTORY"):
|
|
already_added = True
|
|
break
|
|
if not already_added:
|
|
destdir = replace_path_constants(destdir, scope)
|
|
extra_lines.append(f'OUTPUT_DIRECTORY "{destdir}"')
|
|
|
|
cm_fh.write(f"{spaces(indent)}{cmake_function}({target_ref}\n")
|
|
for extra_line in extra_lines:
|
|
cm_fh.write(f"{spaces(indent)} {extra_line}\n")
|
|
|
|
write_sources_section(cm_fh, scopes[0], indent=indent, **kwargs)
|
|
|
|
if has_test_data:
|
|
cm_fh.write(f"{spaces(indent)} TESTDATA ${{test_data}}\n")
|
|
# Footer:
|
|
cm_fh.write(f"{spaces(indent)})\n")
|
|
|
|
if typename == "Tool":
|
|
cm_fh.write(f"{spaces(indent)}qt_internal_return_unless_building_tools()\n")
|
|
|
|
write_resources(cm_fh, name, scope, indent, target_ref=target_ref)
|
|
|
|
write_statecharts(cm_fh, name, scope, indent)
|
|
|
|
write_qlalrsources(cm_fh, name, scope, indent)
|
|
|
|
write_repc_files(cm_fh, name, scope, indent)
|
|
|
|
write_simd_part(cm_fh, name, scope, indent)
|
|
|
|
write_reduce_relocations_part(cm_fh, name, scope, indent)
|
|
|
|
write_android_part(cm_fh, name, scopes[0], indent)
|
|
|
|
write_wayland_part(cm_fh, name, scopes[0], indent)
|
|
|
|
write_windows_part(cm_fh, name, scopes[0], indent)
|
|
|
|
write_darwin_part(cm_fh, name, scopes[0], main_scope_target_name=name, indent=indent)
|
|
|
|
write_version_part(cm_fh, name, scopes[0], indent)
|
|
|
|
write_aux_qml_files_part(cm_fh, name, scopes[0], indent)
|
|
|
|
if "warn_off" in scope.get("CONFIG"):
|
|
write_generic_cmake_command(cm_fh, "qt_disable_warnings", [name])
|
|
|
|
if "hide_symbols" in scope.get("CONFIG"):
|
|
write_generic_cmake_command(cm_fh, "qt_set_symbol_visibility_hidden", [name])
|
|
|
|
ignored_keys_report = write_ignored_keys(scopes[0], spaces(indent))
|
|
if ignored_keys_report:
|
|
cm_fh.write(ignored_keys_report)
|
|
|
|
# Scopes:
|
|
if len(scopes) == 1:
|
|
return
|
|
|
|
write_scope_header(cm_fh, indent=indent)
|
|
|
|
for c in scopes[1:]:
|
|
c.reset_visited_keys()
|
|
write_android_part(cm_fh, name, c, indent=indent)
|
|
write_wayland_part(cm_fh, name, c, indent=indent)
|
|
write_windows_part(cm_fh, name, c, indent=indent)
|
|
write_darwin_part(cm_fh, name, c, main_scope_target_name=name, indent=indent)
|
|
write_version_part(cm_fh, name, c, indent=indent)
|
|
write_aux_qml_files_part(cm_fh, name, c, indent=indent)
|
|
write_extend_target(cm_fh, name, c, target_ref=target_ref, indent=indent)
|
|
write_simd_part(cm_fh, name, c, indent=indent)
|
|
|
|
ignored_keys_report = write_ignored_keys(c, spaces(indent))
|
|
if ignored_keys_report:
|
|
cm_fh.write(ignored_keys_report)
|
|
|
|
|
|
def write_3rdparty_library(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
|
|
# Remove default QT libs.
|
|
scope._append_operation("QT", RemoveOperation(["core", "gui"]))
|
|
|
|
target_name = re.sub(r"^qt", "", scope.TARGET)
|
|
target_name = target_name.replace("-", "_")
|
|
qmake_lib_name = target_name
|
|
|
|
# Capitalize the first letter for a nicer name.
|
|
target_name = target_name.title()
|
|
|
|
# Prefix with Bundled, to avoid possible duplicate target names
|
|
# e.g. "BundledFreetype" instead of "freetype".
|
|
target_name = f"Bundled{target_name}"
|
|
|
|
if "dll" in scope.get("CONFIG"):
|
|
library_type = "SHARED"
|
|
else:
|
|
library_type = "STATIC"
|
|
|
|
extra_lines = [f"QMAKE_LIB_NAME {qmake_lib_name}"]
|
|
|
|
if library_type:
|
|
extra_lines.append(library_type)
|
|
|
|
if "installed" in scope.get("CONFIG"):
|
|
extra_lines.append("INSTALL")
|
|
|
|
write_main_part(
|
|
cm_fh,
|
|
target_name,
|
|
"Generic Library",
|
|
get_cmake_api_call("qt_add_3rdparty_library"),
|
|
scope,
|
|
extra_lines=extra_lines,
|
|
indent=indent,
|
|
known_libraries={},
|
|
extra_keys=[],
|
|
)
|
|
|
|
return target_name
|
|
|
|
|
|
def write_generic_library(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
|
|
|
|
target_name = scope.TARGET
|
|
|
|
library_type = ""
|
|
|
|
if "dll" in scope.get("CONFIG"):
|
|
library_type = "SHARED"
|
|
|
|
is_plugin = False
|
|
if "plugin" in scope.get("CONFIG"):
|
|
library_type = "MODULE"
|
|
is_plugin = True
|
|
|
|
# static after plugin in order to handle static library plugins
|
|
if "static" in scope.get("CONFIG"):
|
|
library_type = "STATIC"
|
|
|
|
extra_lines = []
|
|
|
|
if library_type:
|
|
extra_lines.append(library_type)
|
|
|
|
target_path = scope.expandString("target.path")
|
|
target_path = replace_path_constants(target_path, scope)
|
|
if target_path:
|
|
extra_lines.append(f'INSTALL_DIRECTORY "{target_path}"')
|
|
|
|
write_main_part(
|
|
cm_fh,
|
|
target_name,
|
|
"Generic Library",
|
|
get_cmake_api_call("qt_add_cmake_library"),
|
|
scope,
|
|
extra_lines=extra_lines,
|
|
indent=indent,
|
|
known_libraries={},
|
|
extra_keys=[],
|
|
)
|
|
|
|
if is_plugin:
|
|
# Plugins need to be able to run auto moc
|
|
cm_fh.write(f"\nqt_autogen_tools_initial_setup({target_name})\n")
|
|
|
|
if library_type == "STATIC":
|
|
cm_fh.write(f"\ntarget_compile_definitions({target_name} PRIVATE QT_STATICPLUGIN)\n")
|
|
|
|
return target_name
|
|
|
|
|
|
def forward_target_info(scope: Scope, extra: List[str], skip: Optional[Dict[str, bool]] = None):
|
|
s = scope.get_string("QMAKE_TARGET_PRODUCT")
|
|
if s:
|
|
extra.append(f'TARGET_PRODUCT "{s}"')
|
|
s = scope.get_string("QMAKE_TARGET_DESCRIPTION")
|
|
if s and (not skip or "QMAKE_TARGET_DESCRIPTION" not in skip):
|
|
extra.append(f'TARGET_DESCRIPTION "{s}"')
|
|
s = scope.get_string("QMAKE_TARGET_COMPANY")
|
|
if s:
|
|
extra.append(f'TARGET_COMPANY "{s}"')
|
|
s = scope.get_string("QMAKE_TARGET_COPYRIGHT")
|
|
if s:
|
|
extra.append(f'TARGET_COPYRIGHT "{s}"')
|
|
|
|
|
|
def write_module(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
|
|
# e.g. QtCore
|
|
qt_module_name = scope.TARGET
|
|
if not qt_module_name.startswith("Qt"):
|
|
print(f"XXXXXX Module name {qt_module_name} does not start with Qt!")
|
|
|
|
extra = []
|
|
|
|
# A module should be static when 'static' is in CONFIG
|
|
# or when option(host_build) is used, as described in qt_module.prf.
|
|
is_static = "static" in scope.get("CONFIG") or "host_build" in scope.get("_OPTION")
|
|
|
|
is_public_module = True
|
|
|
|
# CMake target name as passed to qt_internal_add_module()
|
|
# e.g. Core
|
|
cmake_target_name = qt_module_name[2:]
|
|
|
|
# MODULE is used for the name of the generated .pri file.
|
|
# If MODULE is not explicitly set, qmake computes its value in
|
|
# mkspecs/features/qt_build_config.prf
|
|
module_name_for_pri = scope.expandString("MODULE")
|
|
if not module_name_for_pri:
|
|
module_name_for_pri_as_qmake_computes_it = scope.file[:-4]
|
|
module_name_for_pri = module_name_for_pri_as_qmake_computes_it
|
|
|
|
# Given 'qt_internal_add_module(Core)', computes 'core'.
|
|
module_name_for_pri_as_cmake_computes_it = cmake_target_name.lower()
|
|
|
|
if module_name_for_pri != module_name_for_pri_as_cmake_computes_it:
|
|
extra.append(f"CONFIG_MODULE_NAME {module_name_for_pri}")
|
|
|
|
if is_static:
|
|
extra.append("STATIC")
|
|
if "internal_module" in scope.get("CONFIG"):
|
|
is_public_module = False
|
|
cmake_target_name += "Private" # Assume all internal modules have the 'Private' suffix
|
|
extra.append("INTERNAL_MODULE")
|
|
if "no_module_headers" in scope.get("CONFIG"):
|
|
extra.append("NO_MODULE_HEADERS")
|
|
if "minimal_syncqt" in scope.get("CONFIG"):
|
|
extra.append("NO_SYNC_QT")
|
|
if "no_private_module" in scope.get("CONFIG"):
|
|
extra.append("NO_PRIVATE_MODULE")
|
|
else:
|
|
scope._has_private_module = True
|
|
if "header_module" in scope.get("CONFIG"):
|
|
extra.append("HEADER_MODULE")
|
|
if not("metatypes" in scope.get("CONFIG") or "qmltypes" in scope.get("CONFIG")):
|
|
extra.append("NO_GENERATE_METATYPES")
|
|
|
|
module_config = scope.get("MODULE_CONFIG")
|
|
if len(module_config):
|
|
extra.append(f'QMAKE_MODULE_CONFIG {" ".join(module_config)}')
|
|
|
|
module_plugin_types = scope.get_files("MODULE_PLUGIN_TYPES")
|
|
if module_plugin_types:
|
|
extra.append(f"PLUGIN_TYPES {' '.join(module_plugin_types)}")
|
|
|
|
scope._is_public_module = is_public_module
|
|
|
|
forward_target_info(scope, extra)
|
|
write_main_part(
|
|
cm_fh,
|
|
cmake_target_name,
|
|
"Module",
|
|
f"{get_cmake_api_call('qt_add_module')}",
|
|
scope,
|
|
extra_lines=extra,
|
|
indent=indent,
|
|
known_libraries={},
|
|
extra_keys=[],
|
|
)
|
|
|
|
if "qt_tracepoints" in scope.get("CONFIG"):
|
|
tracepoints = scope.get_files("TRACEPOINT_PROVIDER")
|
|
create_trace_points = get_cmake_api_call("qt_create_tracepoints")
|
|
cm_fh.write(
|
|
f"\n\n{spaces(indent)}{create_trace_points}({cmake_target_name} {' '.join(tracepoints)})\n"
|
|
)
|
|
|
|
return cmake_target_name
|
|
|
|
|
|
def write_tool(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> Tuple[str, str]:
|
|
tool_name = scope.TARGET
|
|
|
|
if "force_bootstrap" in scope.get("CONFIG"):
|
|
extra = ["BOOTSTRAP"]
|
|
|
|
# Remove default QT libs.
|
|
scope._append_operation("QT", RemoveOperation(["core", "gui"]))
|
|
else:
|
|
extra = []
|
|
|
|
forward_target_info(scope, extra)
|
|
|
|
write_main_part(
|
|
cm_fh,
|
|
tool_name,
|
|
"Tool",
|
|
get_cmake_api_call("qt_add_tool"),
|
|
scope,
|
|
indent=indent,
|
|
known_libraries={"Qt::Core"},
|
|
extra_lines=extra,
|
|
extra_keys=["CONFIG"],
|
|
)
|
|
|
|
return tool_name, "${target_name}"
|
|
|
|
|
|
def write_qt_app(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
|
|
app_name = scope.TARGET
|
|
|
|
extra: List[str] = []
|
|
|
|
target_info_skip = {}
|
|
target_info_skip["QMAKE_TARGET_DESCRIPTION"] = True
|
|
forward_target_info(scope, extra, target_info_skip)
|
|
|
|
write_main_part(
|
|
cm_fh,
|
|
app_name,
|
|
"App",
|
|
get_cmake_api_call("qt_internal_add_app"),
|
|
scope,
|
|
indent=indent,
|
|
known_libraries={"Qt::Core"},
|
|
extra_lines=extra,
|
|
extra_keys=["CONFIG"],
|
|
)
|
|
|
|
return app_name
|
|
|
|
|
|
def write_test(cm_fh: IO[str], scope: Scope, gui: bool = False, *, indent: int = 0) -> str:
|
|
test_name = scope.TARGET
|
|
assert test_name
|
|
|
|
extra = ["GUI"] if gui else []
|
|
libraries = {"Qt::Core", "Qt::Test"}
|
|
|
|
if "qmltestcase" in scope.get("CONFIG"):
|
|
libraries.add("Qt::QmlTest")
|
|
extra.append("QMLTEST")
|
|
importpath = scope.expand("IMPORTPATH")
|
|
if importpath:
|
|
extra.append("QML_IMPORTPATH")
|
|
for path in importpath:
|
|
extra.append(f' "{path}"')
|
|
|
|
target_original = scope.TARGET_ORIGINAL
|
|
if target_original and target_original.startswith("../"):
|
|
extra.append('OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/../"')
|
|
|
|
requires_content = expand_project_requirements(scope, skip_message=True)
|
|
if requires_content:
|
|
requires_content += "\n"
|
|
cm_fh.write(requires_content)
|
|
|
|
write_main_part(
|
|
cm_fh,
|
|
test_name,
|
|
"Test",
|
|
get_cmake_api_call("qt_add_test"),
|
|
scope,
|
|
indent=indent,
|
|
known_libraries=libraries,
|
|
extra_lines=extra,
|
|
extra_keys=[],
|
|
)
|
|
|
|
return test_name
|
|
|
|
|
|
def write_binary(cm_fh: IO[str], scope: Scope, gui: bool = False, *, indent: int = 0) -> str:
|
|
binary_name = scope.TARGET
|
|
assert binary_name
|
|
|
|
is_benchmark = is_benchmark_project(scope.file_absolute_path)
|
|
is_manual_test = is_manual_test_project(scope.file_absolute_path)
|
|
|
|
is_qt_test_helper = "qt_test_helper" in scope.get("_LOADED")
|
|
|
|
extra = ["GUI"] if gui and not is_qt_test_helper else []
|
|
cmake_function_call = get_cmake_api_call("qt_add_executable")
|
|
extra_keys: List[str] = []
|
|
|
|
if is_qt_test_helper:
|
|
binary_name += "_helper"
|
|
cmake_function_call = get_cmake_api_call("qt_add_test_helper")
|
|
|
|
if is_benchmark:
|
|
cmake_function_call = get_cmake_api_call("qt_add_benchmark")
|
|
elif is_manual_test:
|
|
cmake_function_call = get_cmake_api_call("qt_add_manual_test")
|
|
else:
|
|
extra_keys = ["target.path", "INSTALLS"]
|
|
target_path = scope.get_string("target.path")
|
|
if target_path:
|
|
target_path = replace_path_constants(target_path, scope)
|
|
if not scope.get("DESTDIR"):
|
|
extra.append(f'OUTPUT_DIRECTORY "{target_path}"')
|
|
if "target" in scope.get("INSTALLS"):
|
|
extra.append(f'INSTALL_DIRECTORY "{target_path}"')
|
|
|
|
write_main_part(
|
|
cm_fh,
|
|
binary_name,
|
|
"Binary",
|
|
cmake_function_call,
|
|
scope,
|
|
extra_lines=extra,
|
|
indent=indent,
|
|
known_libraries={"Qt::Core"},
|
|
extra_keys=extra_keys,
|
|
)
|
|
|
|
return binary_name
|
|
|
|
|
|
def write_find_package_section(
|
|
cm_fh: IO[str],
|
|
public_libs: List[str],
|
|
private_libs: List[str],
|
|
*,
|
|
indent: int = 0,
|
|
is_required: bool = True,
|
|
end_with_extra_newline: bool = True,
|
|
qt_package_name: str = "Qt6",
|
|
):
|
|
packages = [] # type: List[LibraryMapping]
|
|
all_libs = public_libs + private_libs
|
|
|
|
for one_lib in all_libs:
|
|
info = find_library_info_for_target(one_lib)
|
|
if info and info not in packages:
|
|
packages.append(info)
|
|
|
|
qt_components: List[str] = []
|
|
for p in filter(LibraryMapping.is_qt, packages):
|
|
if p.components is not None:
|
|
qt_components += p.components
|
|
if qt_components:
|
|
if "Core" in qt_components:
|
|
qt_components.remove("Core")
|
|
qt_components = sorted(qt_components)
|
|
qt_package = LibraryMapping("unknown", qt_package_name, "unknown", components=qt_components)
|
|
if is_required:
|
|
qt_package.extra = ["REQUIRED"]
|
|
cm_fh.write(
|
|
generate_find_package_info(
|
|
qt_package,
|
|
use_qt_find_package=False,
|
|
remove_REQUIRED_from_extra=False,
|
|
components_required=is_required,
|
|
indent=indent,
|
|
)
|
|
)
|
|
|
|
for p in itertools.filterfalse(LibraryMapping.is_qt, packages):
|
|
cm_fh.write(generate_find_package_info(p, use_qt_find_package=False, indent=indent))
|
|
|
|
if packages and end_with_extra_newline:
|
|
cm_fh.write("\n")
|
|
|
|
|
|
def write_jar(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
|
|
|
|
target = scope.TARGET
|
|
|
|
install_dir = scope.expandString("target.path")
|
|
if not install_dir:
|
|
raise RuntimeError("Could not locate jar install path")
|
|
install_dir = install_dir.replace("$$[QT_INSTALL_PREFIX]/", "")
|
|
|
|
android_sdk_jar = "${QT_ANDROID_JAR}"
|
|
android_api_level = scope.get_string("API_VERSION")
|
|
if android_api_level:
|
|
cm_fh.write(
|
|
f'{spaces(indent)}qt_get_android_sdk_jar_for_api("{android_api_level}" android_sdk)\n\n'
|
|
)
|
|
android_sdk_jar = "${android_sdk}"
|
|
|
|
write_source_file_list(
|
|
cm_fh, scope, "", ["JAVASOURCES"], indent=indent, header="set(java_sources\n", footer=")\n"
|
|
)
|
|
|
|
cm_fh.write(f"{spaces(indent)}qt_internal_add_jar({target}\n")
|
|
cm_fh.write(f"{spaces(indent+1)}INCLUDE_JARS {android_sdk_jar}\n")
|
|
cm_fh.write(f"{spaces(indent+1)}SOURCES ${{java_sources}}\n")
|
|
cm_fh.write(f'{spaces(indent+1)}OUTPUT_DIR "${{QT_BUILD_DIR}}/{install_dir}"\n')
|
|
cm_fh.write(f"{spaces(indent)})\n\n")
|
|
|
|
cm_fh.write(f"{spaces(indent)}install_jar({target}\n")
|
|
cm_fh.write(f"{spaces(indent+1)}DESTINATION {install_dir}\n")
|
|
cm_fh.write(f"{spaces(indent+1)}COMPONENT Devel\n")
|
|
cm_fh.write(f"{spaces(indent)})\n\n")
|
|
|
|
return target
|
|
|
|
|
|
def get_win32_and_mac_bundle_properties(scope: Scope) -> tuple:
|
|
config = scope.get("CONFIG")
|
|
win32 = all(val not in config for val in ["cmdline", "console"])
|
|
mac_bundle = all(val not in config for val in ["cmdline", "-app_bundle"])
|
|
|
|
return win32, mac_bundle
|
|
|
|
|
|
def write_win32_and_mac_bundle_properties(
|
|
cm_fh: IO[str], scope: Scope, target: str, *, handling_first_scope=False, indent: int = 0
|
|
):
|
|
win32, mac_bundle = get_win32_and_mac_bundle_properties(scope)
|
|
|
|
true_value = "TRUE"
|
|
false_value = "FALSE"
|
|
|
|
properties_mapping = {
|
|
"WIN32_EXECUTABLE": true_value if win32 else false_value,
|
|
"MACOSX_BUNDLE": true_value if mac_bundle else false_value,
|
|
}
|
|
|
|
properties = []
|
|
|
|
# Always write the properties for the first scope.
|
|
# For conditional scopes, only write them if the value is different
|
|
# from the default value (aka different from TRUE).
|
|
# This is a heurestic that should cover 90% of the example projects
|
|
# without creating excess noise of setting the properties in every
|
|
# single scope.
|
|
for name, value in properties_mapping.items():
|
|
if not handling_first_scope and value != true_value:
|
|
properties.extend([name, value])
|
|
|
|
if properties:
|
|
write_set_target_properties(cm_fh, [target], properties, indent=indent)
|
|
|
|
|
|
def is_qtquick_source_file(filename: str):
|
|
return filename.endswith(".qml") or filename.endswith(".js") or filename.endswith(".mjs")
|
|
|
|
|
|
def looks_like_qml_resource(resource: QtResource):
|
|
if resource.generated or "*" in resource.name:
|
|
return False
|
|
for f in resource.files:
|
|
if is_qtquick_source_file(f):
|
|
return True
|
|
return False
|
|
|
|
|
|
def find_qml_resource(resources: List[QtResource]):
|
|
"""Return the resource object that's most likely the one that should be used for
|
|
qt_add_qml_module. Return None if there's no such resource."""
|
|
return next(filter(looks_like_qml_resource, resources), None)
|
|
|
|
|
|
def write_example(
|
|
cm_fh: IO[str],
|
|
scope: Scope,
|
|
gui: bool = False,
|
|
*,
|
|
indent: int = 0,
|
|
is_plugin: bool = False,
|
|
is_user_project: bool = False,
|
|
) -> str:
|
|
binary_name = scope.TARGET
|
|
assert binary_name
|
|
config = scope.get("CONFIG")
|
|
is_qml_plugin = ("qml" in scope.get("QT")) or "qmltypes" in config
|
|
|
|
if not is_user_project:
|
|
example_install_dir = scope.expandString("target.path")
|
|
if not example_install_dir:
|
|
example_install_dir = "${INSTALL_EXAMPLESDIR}"
|
|
example_install_dir = example_install_dir.replace(
|
|
"$$[QT_INSTALL_EXAMPLES]", "${INSTALL_EXAMPLESDIR}"
|
|
)
|
|
|
|
project_version = scope.get_string("VERSION", "1.0")
|
|
cm_fh.write(
|
|
f"cmake_minimum_required(VERSION {cmake_version_string})\n"
|
|
f"project({binary_name} VERSION {project_version} LANGUAGES CXX)\n\n"
|
|
"set(CMAKE_INCLUDE_CURRENT_DIR ON)\n\n"
|
|
"set(CMAKE_AUTOMOC ON)\n"
|
|
)
|
|
if scope.get_files("FORMS"):
|
|
cm_fh.write("set(CMAKE_AUTOUIC ON)\n")
|
|
cm_fh.write("\n")
|
|
if not is_user_project:
|
|
cm_fh.write(
|
|
"if(NOT DEFINED INSTALL_EXAMPLESDIR)\n"
|
|
' set(INSTALL_EXAMPLESDIR "examples")\n'
|
|
"endif()\n\n"
|
|
f'set(INSTALL_EXAMPLEDIR "{example_install_dir}")\n\n'
|
|
)
|
|
|
|
recursive_evaluate_scope(scope)
|
|
|
|
# Get a flat list of all scopes but the main one:
|
|
scopes = flatten_scopes(scope)
|
|
# Merge scopes based on their conditions:
|
|
scopes = merge_scopes(scopes)
|
|
# Handle SOURCES -= foo calls, and merge scopes one more time
|
|
# because there might have been several files removed with the same
|
|
# scope condition.
|
|
handle_source_subtractions(scopes)
|
|
scopes = merge_scopes(scopes)
|
|
|
|
# Write find_package call for Qt5/Qt6 and make it available as package QT.
|
|
cm_fh.write("find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core)\n")
|
|
|
|
# Write find_package calls for required packages.
|
|
# We consider packages as required if they appear at the top-level scope.
|
|
(public_libs, private_libs) = extract_cmake_libraries(scope, is_example=True)
|
|
write_find_package_section(
|
|
cm_fh,
|
|
public_libs,
|
|
private_libs,
|
|
indent=indent,
|
|
end_with_extra_newline=False,
|
|
qt_package_name="Qt${QT_VERSION_MAJOR}",
|
|
)
|
|
|
|
# Write find_package calls for optional packages.
|
|
# We consider packages inside scopes other than the top-level one as optional.
|
|
optional_public_libs: List[str] = []
|
|
optional_private_libs: List[str] = []
|
|
handling_first_scope = True
|
|
for inner_scope in scopes:
|
|
if handling_first_scope:
|
|
handling_first_scope = False
|
|
continue
|
|
(public_libs, private_libs) = extract_cmake_libraries(inner_scope, is_example=True)
|
|
optional_public_libs += public_libs
|
|
optional_private_libs += private_libs
|
|
write_find_package_section(
|
|
cm_fh,
|
|
optional_public_libs,
|
|
optional_private_libs,
|
|
indent=indent,
|
|
is_required=False,
|
|
end_with_extra_newline=False,
|
|
qt_package_name="Qt${QT_VERSION_MAJOR}",
|
|
)
|
|
|
|
cm_fh.write("\n")
|
|
|
|
(resources, standalone_qtquick_compiler_skipped_files) = extract_resources(binary_name, scope)
|
|
qml_resource = find_qml_resource(resources) if is_qml_plugin else None
|
|
|
|
add_target = ""
|
|
|
|
if is_plugin:
|
|
if is_qml_plugin:
|
|
extra_args = [f"PLUGIN_TARGET {binary_name}"]
|
|
io_string = io.StringIO()
|
|
write_qml_module(
|
|
io_string,
|
|
binary_name,
|
|
scope,
|
|
scopes,
|
|
indent=indent,
|
|
resource=qml_resource,
|
|
extra_add_qml_module_args=extra_args,
|
|
)
|
|
add_target += io_string.getvalue()
|
|
else:
|
|
add_target = f"qt_add_plugin({binary_name}"
|
|
if "static" in scope.get("CONFIG"):
|
|
add_target += " STATIC"
|
|
add_target += ")\n"
|
|
add_target += f"target_sources({binary_name} PRIVATE"
|
|
else:
|
|
add_target = f"qt_add_executable({binary_name}"
|
|
|
|
property_win32, property_mac_bundle = get_win32_and_mac_bundle_properties(scope)
|
|
|
|
if property_win32:
|
|
add_target += " " + "WIN32"
|
|
if property_mac_bundle:
|
|
add_target += " " + "MACOSX_BUNDLE"
|
|
|
|
write_all_source_file_lists(cm_fh, scope, add_target, indent=0)
|
|
cm_fh.write(")\n")
|
|
|
|
if is_qml_plugin and not is_plugin:
|
|
write_qml_module(cm_fh, binary_name, scope, scopes, indent=indent, resource=qml_resource)
|
|
|
|
handling_first_scope = True
|
|
|
|
for scope in scopes:
|
|
# write wayland already has condition scope handling
|
|
write_wayland_part(cm_fh, binary_name, scope, indent=0)
|
|
|
|
# The following options do not
|
|
io_string = io.StringIO()
|
|
condition_str = ""
|
|
condition = "ON"
|
|
if scope.total_condition:
|
|
condition = map_to_cmake_condition(scope.total_condition)
|
|
|
|
if condition != "ON":
|
|
condition_str = f"\n{spaces(indent)}if({condition})\n"
|
|
indent += 1
|
|
|
|
if not handling_first_scope:
|
|
target_sources = f"target_sources({binary_name} PUBLIC"
|
|
write_all_source_file_lists(
|
|
io_string, scope, target_sources, indent=indent, footer=")\n"
|
|
)
|
|
|
|
write_win32_and_mac_bundle_properties(
|
|
io_string, scope, binary_name, handling_first_scope=handling_first_scope, indent=indent
|
|
)
|
|
|
|
write_include_paths(
|
|
io_string,
|
|
scope,
|
|
f"target_include_directories({binary_name} PUBLIC",
|
|
indent=indent,
|
|
footer=")\n",
|
|
)
|
|
write_defines(
|
|
io_string,
|
|
scope,
|
|
f"target_compile_definitions({binary_name} PUBLIC",
|
|
indent=indent,
|
|
footer=")\n",
|
|
)
|
|
|
|
(scope_public_libs, scope_private_libs) = extract_cmake_libraries(scope, is_example=True)
|
|
|
|
write_list(
|
|
io_string,
|
|
scope_private_libs,
|
|
"",
|
|
indent=indent,
|
|
header=f"target_link_libraries({binary_name} PRIVATE\n",
|
|
footer=")\n",
|
|
)
|
|
write_list(
|
|
io_string,
|
|
scope_public_libs,
|
|
"",
|
|
indent=indent,
|
|
header=f"target_link_libraries({binary_name} PUBLIC\n",
|
|
footer=")\n",
|
|
)
|
|
write_compile_options(
|
|
io_string, scope, f"target_compile_options({binary_name}", indent=indent, footer=")\n"
|
|
)
|
|
|
|
(resources, standalone_qtquick_compiler_skipped_files) = extract_resources(
|
|
binary_name, scope
|
|
)
|
|
|
|
# Remove the QML resource, because we've handled it in write_qml_module.
|
|
if qml_resource is not None:
|
|
resources = list(filter(lambda r: r.name != qml_resource.name, resources))
|
|
|
|
write_resources(
|
|
io_string,
|
|
binary_name,
|
|
scope,
|
|
indent=indent,
|
|
is_example=True,
|
|
resources=resources,
|
|
skipped_standalone_files=standalone_qtquick_compiler_skipped_files,
|
|
)
|
|
write_statecharts(io_string, binary_name, scope, indent=indent, is_example=True)
|
|
write_repc_files(io_string, binary_name, scope, indent=indent)
|
|
|
|
if condition != "ON":
|
|
indent -= 1
|
|
string = io_string.getvalue()
|
|
if len(string) != 0:
|
|
string = string.rstrip("\n")
|
|
cm_fh.write(f"{condition_str}{string}\n")
|
|
if condition != "ON":
|
|
cm_fh.write(f"{spaces(indent)}endif()\n")
|
|
|
|
handling_first_scope = False
|
|
|
|
if not is_user_project:
|
|
cm_fh.write(
|
|
f"\ninstall(TARGETS {binary_name}\n"
|
|
f' RUNTIME DESTINATION "${{INSTALL_EXAMPLEDIR}}"\n'
|
|
f' BUNDLE DESTINATION "${{INSTALL_EXAMPLEDIR}}"\n'
|
|
f' LIBRARY DESTINATION "${{INSTALL_EXAMPLEDIR}}"\n'
|
|
f")\n"
|
|
)
|
|
|
|
return binary_name
|
|
|
|
|
|
def write_plugin(cm_fh, scope, *, indent: int = 0) -> str:
|
|
extra = []
|
|
is_qml_plugin = any("qml_plugin" == s for s in scope.get("_LOADED"))
|
|
qmake_target_name = scope.TARGET
|
|
|
|
# Forward the original Qt5 plugin target name, to correctly name the
|
|
# final library file name, and also for .prl generation.
|
|
if qmake_target_name and not is_qml_plugin:
|
|
extra.append(f"OUTPUT_NAME {qmake_target_name}")
|
|
|
|
# In Qt 6 CMake, the CMake target name for a plugin should be the
|
|
# same as it is in Qt5. qmake in Qt 5 derived the CMake target name
|
|
# from the "plugin class name", so use that.
|
|
# If the class name isn't empty, use that as the target name.
|
|
# Otherwise use the of value qmake TARGET
|
|
plugin_class_name = scope.get_string("PLUGIN_CLASS_NAME")
|
|
if plugin_class_name:
|
|
plugin_name = plugin_class_name
|
|
else:
|
|
plugin_name = qmake_target_name
|
|
assert plugin_name
|
|
|
|
# If the target name is derived from the class name, no need to
|
|
# forward the class name.
|
|
if plugin_class_name and plugin_class_name != plugin_name:
|
|
extra.append(f"CLASS_NAME {plugin_class_name}")
|
|
|
|
qmldir = None
|
|
plugin_type = scope.get_string("PLUGIN_TYPE")
|
|
plugin_function_name = get_cmake_api_call("qt_add_plugin")
|
|
if plugin_type:
|
|
extra.append(f"TYPE {plugin_type}")
|
|
elif is_qml_plugin:
|
|
plugin_function_name = get_cmake_api_call("qt_add_qml_module")
|
|
qmldir = write_qml_plugin(cm_fh, plugin_name, scope, indent=indent, extra_lines=extra)
|
|
else:
|
|
target_path = scope.expandString("target.path")
|
|
target_path = replace_path_constants(target_path, scope)
|
|
if target_path:
|
|
extra.append(f'INSTALL_DIRECTORY "{target_path}"')
|
|
else:
|
|
extra.append("SKIP_INSTALL")
|
|
|
|
past_major_versions = scope.expandString("QML_PAST_MAJOR_VERSIONS")
|
|
if past_major_versions:
|
|
extra.append(f"PAST_MAJOR_VERSIONS {past_major_versions}")
|
|
|
|
if "qmltypes" in scope.get("CONFIG"):
|
|
extra.append("GENERATE_QMLTYPES")
|
|
|
|
if "install_qmltypes" in scope.get("CONFIG"):
|
|
extra.append("INSTALL_QMLTYPES")
|
|
|
|
if "static" in scope.get("CONFIG"):
|
|
extra.append("STATIC")
|
|
|
|
plugin_extends = scope.get_string("PLUGIN_EXTENDS")
|
|
if plugin_type != "platform" and plugin_extends == "-":
|
|
extra.append("DEFAULT_IF FALSE")
|
|
|
|
forward_target_info(scope, extra)
|
|
|
|
write_main_part(
|
|
cm_fh,
|
|
plugin_name,
|
|
"Plugin",
|
|
plugin_function_name,
|
|
scope,
|
|
indent=indent,
|
|
extra_lines=extra,
|
|
known_libraries={},
|
|
extra_keys=[],
|
|
)
|
|
|
|
if qmldir:
|
|
write_qml_plugin_epilogue(cm_fh, plugin_name, scope, qmldir, indent)
|
|
|
|
return plugin_name
|
|
|
|
|
|
def get_qml_import_version(scope: Scope, target: str) -> str:
|
|
import_version = scope.get_string("IMPORT_VERSION")
|
|
if not import_version:
|
|
import_version = scope.get_string("QML_IMPORT_VERSION")
|
|
if not import_version:
|
|
import_major_version = scope.get_string("QML_IMPORT_MAJOR_VERSION")
|
|
import_minor_version = scope.get_string("QML_IMPORT_MINOR_VERSION")
|
|
|
|
if not import_major_version and not import_minor_version:
|
|
raise RuntimeError(f"No QML_IMPORT_VERSION info found for target {target}.")
|
|
|
|
if not import_minor_version:
|
|
import_minor_version = str(0)
|
|
import_version = f"{import_major_version}.{import_minor_version}"
|
|
|
|
if import_version:
|
|
replacements = [
|
|
("$$QT_MINOR_VERSION", "${PROJECT_VERSION_MINOR}"),
|
|
("$$QT_VERSION", "${PROJECT_VERSION}"),
|
|
]
|
|
for needle, replacement in replacements:
|
|
import_version = import_version.replace(needle, replacement)
|
|
return import_version
|
|
|
|
|
|
def write_qml_module(
|
|
cm_fh: IO[str],
|
|
target: str,
|
|
scope: Scope,
|
|
scopes: List[Scope],
|
|
resource: QtResource,
|
|
extra_add_qml_module_args: List[str] = [],
|
|
indent: int = 0,
|
|
):
|
|
uri = scope.get_string("QML_IMPORT_NAME")
|
|
if not uri:
|
|
uri = target
|
|
|
|
try:
|
|
version = get_qml_import_version(scope, target)
|
|
except RuntimeError:
|
|
version = "${PROJECT_VERSION}"
|
|
|
|
dest_dir = scope.expandString("DESTDIR")
|
|
if dest_dir:
|
|
dest_dir = f"${{CMAKE_CURRENT_BINARY_DIR}}/{dest_dir}"
|
|
|
|
content = ""
|
|
|
|
qml_dir = None
|
|
qml_dir_dynamic_imports = False
|
|
|
|
qmldir_file_path_list = scope.get_files("qmldir.files")
|
|
assert len(qmldir_file_path_list) < 2, "File path must only contain one path"
|
|
qmldir_file_path = qmldir_file_path_list[0] if qmldir_file_path_list else "qmldir"
|
|
qmldir_file_path = os.path.join(os.getcwd(), qmldir_file_path[0])
|
|
|
|
dynamic_qmldir = scope.get("DYNAMIC_QMLDIR")
|
|
if os.path.exists(qmldir_file_path):
|
|
qml_dir = QmlDir()
|
|
qml_dir.from_file(qmldir_file_path)
|
|
elif dynamic_qmldir:
|
|
qml_dir = QmlDir()
|
|
qml_dir.from_lines(dynamic_qmldir)
|
|
qml_dir_dynamic_imports = True
|
|
|
|
content += "set(module_dynamic_qml_imports\n "
|
|
if len(qml_dir.imports) != 0:
|
|
content += "\n ".join(qml_dir.imports)
|
|
content += "\n)\n\n"
|
|
|
|
for sc in scopes[1:]:
|
|
import_list = []
|
|
qml_imports = sc.get("DYNAMIC_QMLDIR")
|
|
for qml_import in qml_imports:
|
|
if not qml_import.startswith("import "):
|
|
raise RuntimeError(
|
|
"Only qmldir import statements expected in conditional scope!"
|
|
)
|
|
import_list.append(qml_import[len("import ") :].replace(" ", "/"))
|
|
if len(import_list) == 0:
|
|
continue
|
|
|
|
assert sc.condition
|
|
|
|
content += f"if ({sc.condition})\n"
|
|
content += " list(APPEND module_dynamic_qml_imports\n "
|
|
content += "\n ".join(import_list)
|
|
content += "\n )\nendif()\n\n"
|
|
|
|
content += dedent(
|
|
f"""\
|
|
qt_add_qml_module({target}
|
|
URI {uri}
|
|
VERSION {version}
|
|
"""
|
|
)
|
|
|
|
if resource is not None:
|
|
qml_files = list(filter(is_qtquick_source_file, resource.files.keys()))
|
|
if qml_files:
|
|
content += " QML_FILES\n"
|
|
for file in qml_files:
|
|
content += f" {file}\n"
|
|
other_files = list(itertools.filterfalse(is_qtquick_source_file, resource.files.keys()))
|
|
if other_files:
|
|
content += " RESOURCES\n"
|
|
for file in other_files:
|
|
content += f" {file}\n"
|
|
if resource.prefix != "/":
|
|
content += f" RESOURCE_PREFIX {resource.prefix}\n"
|
|
if scope.TEMPLATE == "app":
|
|
content += " NO_RESOURCE_TARGET_PATH\n"
|
|
if dest_dir:
|
|
content += f" OUTPUT_DIRECTORY {dest_dir}\n"
|
|
|
|
if qml_dir is not None:
|
|
if qml_dir.designer_supported:
|
|
content += " DESIGNER_SUPPORTED\n"
|
|
if len(qml_dir.classname) != 0:
|
|
content += f" CLASSNAME {qml_dir.classname}\n"
|
|
if len(qml_dir.depends) != 0:
|
|
content += " DEPENDENCIES\n"
|
|
for dep in qml_dir.depends:
|
|
content += f" {dep[0]}/{dep[1]}\n"
|
|
if len(qml_dir.type_names) == 0:
|
|
content += " SKIP_TYPE_REGISTRATION\n"
|
|
if len(qml_dir.imports) != 0 and not qml_dir_dynamic_imports:
|
|
qml_dir_imports_line = " \n".join(qml_dir.imports)
|
|
content += f" IMPORTS\n{qml_dir_imports_line}"
|
|
if qml_dir_dynamic_imports:
|
|
content += " IMPORTS ${module_dynamic_qml_imports}\n"
|
|
if len(qml_dir.optional_imports) != 0:
|
|
qml_dir_optional_imports_line = " \n".join(qml_dir.optional_imports)
|
|
content += f" OPTIONAL_IMPORTS\n{qml_dir_optional_imports_line}"
|
|
if qml_dir.plugin_optional:
|
|
content += " PLUGIN_OPTIONAL\n"
|
|
|
|
for arg in extra_add_qml_module_args:
|
|
content += " "
|
|
content += arg
|
|
content += "\n"
|
|
content += ")\n"
|
|
|
|
if resource:
|
|
content += write_resource_source_file_properties(
|
|
sorted(resource.files.keys()),
|
|
resource.files,
|
|
resource.base_dir,
|
|
resource.skip_qtquick_compiler,
|
|
)
|
|
|
|
content += "\n"
|
|
cm_fh.write(content)
|
|
|
|
|
|
def write_qml_plugin(
|
|
cm_fh: IO[str],
|
|
target: str,
|
|
scope: Scope,
|
|
*,
|
|
extra_lines: Optional[List[str]] = None,
|
|
indent: int = 0,
|
|
**kwargs: Any,
|
|
) -> Optional[QmlDir]:
|
|
# Collect other args if available
|
|
if extra_lines is None:
|
|
extra_lines = []
|
|
indent += 2
|
|
|
|
target_path = scope.get_string("TARGETPATH")
|
|
if target_path:
|
|
uri = target_path.replace("/", ".")
|
|
import_name = scope.get_string("IMPORT_NAME")
|
|
# Catch special cases such as foo.QtQuick.2.bar, which when converted
|
|
# into a target path via cmake will result in foo/QtQuick/2/bar, which is
|
|
# not what we want. So we supply the target path override.
|
|
target_path_from_uri = uri.replace(".", "/")
|
|
if target_path != target_path_from_uri:
|
|
extra_lines.append(f'TARGET_PATH "{target_path}"')
|
|
if import_name:
|
|
extra_lines.append(f'URI "{import_name}"')
|
|
else:
|
|
uri = re.sub("\\.\\d+", "", uri)
|
|
extra_lines.append(f'URI "{uri}"')
|
|
|
|
import_version = get_qml_import_version(scope, target)
|
|
if import_version:
|
|
extra_lines.append(f'VERSION "{import_version}"')
|
|
|
|
plugindump_dep = scope.get_string("QML_PLUGINDUMP_DEPENDENCIES")
|
|
|
|
if plugindump_dep:
|
|
extra_lines.append(f'QML_PLUGINDUMP_DEPENDENCIES "{plugindump_dep}"')
|
|
|
|
qml_dir = None
|
|
qmldir_file_path = os.path.join(os.getcwd(), "qmldir")
|
|
qml_dir_dynamic_imports = False
|
|
if os.path.exists(qmldir_file_path):
|
|
qml_dir = QmlDir()
|
|
qml_dir.from_file(qmldir_file_path)
|
|
else:
|
|
dynamic_qmldir = scope.get("DYNAMIC_QMLDIR")
|
|
if not dynamic_qmldir:
|
|
return None
|
|
qml_dir = QmlDir()
|
|
qml_dir.from_lines(dynamic_qmldir)
|
|
qml_dir_dynamic_imports = True
|
|
|
|
# Check scopes for conditional entries
|
|
scopes = flatten_scopes(scope)
|
|
cm_fh.write("set(module_dynamic_qml_imports\n ")
|
|
if len(qml_dir.imports) != 0:
|
|
cm_fh.write("\n ".join(qml_dir.imports))
|
|
cm_fh.write("\n)\n\n")
|
|
|
|
for sc in scopes[1:]:
|
|
import_list = []
|
|
qml_imports = sc.get("DYNAMIC_QMLDIR")
|
|
for qml_import in qml_imports:
|
|
if not qml_import.startswith("import "):
|
|
raise RuntimeError(
|
|
"Only qmldir import statements expected in conditional scope!"
|
|
)
|
|
import_list.append(qml_import[len("import ") :].replace(" ", "/"))
|
|
if len(import_list) == 0:
|
|
continue
|
|
|
|
assert sc.condition
|
|
|
|
cm_fh.write(f"if ({sc.condition})\n")
|
|
cm_fh.write(" list(APPEND module_dynamic_qml_imports\n ")
|
|
cm_fh.write("\n ".join(import_list))
|
|
cm_fh.write("\n )\nendif()\n\n")
|
|
|
|
if qml_dir is not None:
|
|
if qml_dir.designer_supported:
|
|
extra_lines.append("DESIGNER_SUPPORTED")
|
|
if len(qml_dir.classname) != 0:
|
|
extra_lines.append(f"CLASSNAME {qml_dir.classname}")
|
|
if len(qml_dir.depends) != 0:
|
|
extra_lines.append("DEPENDENCIES")
|
|
for dep in qml_dir.depends:
|
|
extra_lines.append(f" {dep[0]}/{dep[1]}")
|
|
if len(qml_dir.type_names) == 0:
|
|
extra_lines.append("SKIP_TYPE_REGISTRATION")
|
|
if len(qml_dir.imports) != 0 and not qml_dir_dynamic_imports:
|
|
qml_dir_imports_line = "\n ".join(qml_dir.imports)
|
|
extra_lines.append("IMPORTS\n " f"{qml_dir_imports_line}")
|
|
if qml_dir_dynamic_imports:
|
|
extra_lines.append("IMPORTS ${module_dynamic_qml_imports}")
|
|
if len(qml_dir.optional_imports):
|
|
qml_dir_optional_imports_line = "\n ".join(qml_dir.optional_imports)
|
|
extra_lines.append("OPTIONAL_IMPORTS\n " f"{qml_dir_optional_imports_line}")
|
|
if qml_dir.plugin_optional:
|
|
extra_lines.append("PLUGIN_OPTIONAL")
|
|
|
|
return qml_dir
|
|
|
|
|
|
def write_qml_plugin_epilogue(
|
|
cm_fh: IO[str], target: str, scope: Scope, qmldir: QmlDir, indent: int = 0
|
|
):
|
|
|
|
qml_files = scope.get_files("QML_FILES", use_vpath=True)
|
|
if qml_files:
|
|
|
|
indent_0 = spaces(indent)
|
|
indent_1 = spaces(indent + 1)
|
|
# Quote file paths in case there are spaces.
|
|
qml_files_quoted = [f'"{qf}"' for qf in qml_files]
|
|
|
|
indented_qml_files = f"\n{indent_1}".join(qml_files_quoted)
|
|
cm_fh.write(f"\n{indent_0}set(qml_files\n{indent_1}" f"{indented_qml_files}\n)\n")
|
|
|
|
for qml_file in qml_files:
|
|
if qml_file in qmldir.type_names:
|
|
qmldir_file_info = qmldir.type_names[qml_file]
|
|
cm_fh.write(f"{indent_0}set_source_files_properties({qml_file} PROPERTIES\n")
|
|
cm_fh.write(f'{indent_1}QT_QML_SOURCE_VERSION "{qmldir_file_info.versions}"\n')
|
|
# Only write typename if they are different, CMake will infer
|
|
# the name by default
|
|
if (
|
|
os.path.splitext(os.path.basename(qmldir_file_info.path))[0]
|
|
!= qmldir_file_info.type_name
|
|
):
|
|
cm_fh.write(f"{indent_1}QT_QML_SOURCE_TYPENAME {qmldir_file_info.type_name}\n")
|
|
if qmldir_file_info.singleton:
|
|
cm_fh.write(f"{indent_1}QT_QML_SINGLETON_TYPE TRUE\n")
|
|
if qmldir_file_info.internal:
|
|
cm_fh.write(f"{indent_1}QT_QML_INTERNAL_TYPE TRUE\n")
|
|
cm_fh.write(f"{indent_0})\n")
|
|
else:
|
|
cm_fh.write(
|
|
f"{indent_0}set_source_files_properties({qml_file} PROPERTIES\n"
|
|
f"{indent_1}QT_QML_SKIP_QMLDIR_ENTRY TRUE\n"
|
|
f"{indent_0})\n"
|
|
)
|
|
|
|
cm_fh.write(
|
|
f"\n{indent_0}qt6_target_qml_files({target}\n{indent_1}FILES\n"
|
|
f"{spaces(indent+2)}${{qml_files}}\n)\n"
|
|
)
|
|
|
|
|
|
def handle_app_or_lib(
|
|
scope: Scope,
|
|
cm_fh: IO[str],
|
|
*,
|
|
indent: int = 0,
|
|
is_example: bool = False,
|
|
is_user_project=False,
|
|
) -> None:
|
|
assert scope.TEMPLATE in ("app", "lib")
|
|
|
|
config = scope.get("CONFIG")
|
|
is_jar = "java" in config
|
|
is_lib = scope.TEMPLATE == "lib"
|
|
is_qml_plugin = any("qml_plugin" == s for s in scope.get("_LOADED"))
|
|
is_plugin = "plugin" in config
|
|
is_qt_plugin = any("qt_plugin" == s for s in scope.get("_LOADED")) or is_qml_plugin
|
|
target = ""
|
|
target_ref = None
|
|
gui = all(val not in config for val in ["console", "cmdline", "-app_bundle"]) and all(
|
|
val not in scope.expand("QT") for val in ["testlib", "testlib-private"]
|
|
)
|
|
|
|
if is_jar:
|
|
write_jar(cm_fh, scope, indent=indent)
|
|
elif "qt_helper_lib" in scope.get("_LOADED"):
|
|
assert not is_example
|
|
target = write_3rdparty_library(cm_fh, scope, indent=indent)
|
|
elif is_example:
|
|
target = write_example(
|
|
cm_fh, scope, gui, indent=indent, is_plugin=is_plugin, is_user_project=is_user_project
|
|
)
|
|
elif is_qt_plugin:
|
|
assert not is_example
|
|
target = write_plugin(cm_fh, scope, indent=indent)
|
|
elif (is_lib and "qt_module" not in scope.get("_LOADED")) or is_plugin:
|
|
assert not is_example
|
|
target = write_generic_library(cm_fh, scope, indent=indent)
|
|
elif is_lib or "qt_module" in scope.get("_LOADED"):
|
|
assert not is_example
|
|
target = write_module(cm_fh, scope, indent=indent)
|
|
elif "qt_tool" in scope.get("_LOADED"):
|
|
assert not is_example
|
|
target, target_ref = write_tool(cm_fh, scope, indent=indent)
|
|
elif "qt_app" in scope.get("_LOADED"):
|
|
assert not is_example
|
|
scope._is_internal_qt_app = True
|
|
target = write_qt_app(cm_fh, scope, indent=indent)
|
|
else:
|
|
if "testcase" in config or "testlib" in config or "qmltestcase" in config:
|
|
assert not is_example
|
|
target = write_test(cm_fh, scope, gui, indent=indent)
|
|
else:
|
|
target = write_binary(cm_fh, scope, gui, indent=indent)
|
|
|
|
if target_ref is None:
|
|
target_ref = target
|
|
|
|
# ind = spaces(indent)
|
|
cmake_api_call = get_cmake_api_call("qt_add_docs")
|
|
write_source_file_list(
|
|
cm_fh,
|
|
scope,
|
|
"",
|
|
["QMAKE_DOCS"],
|
|
indent,
|
|
header=f"{cmake_api_call}({target_ref}\n",
|
|
footer=")\n",
|
|
)
|
|
|
|
# Generate qmltypes instruction for anything that may have CONFIG += qmltypes
|
|
# that is not a qml plugin
|
|
if (
|
|
not is_example
|
|
and "qmltypes" in scope.get("CONFIG")
|
|
and "qml_plugin" not in scope.get("_LOADED")
|
|
):
|
|
cm_fh.write(f"\n{spaces(indent)}set_target_properties({target_ref} PROPERTIES\n")
|
|
|
|
install_dir = scope.expandString("QMLTYPES_INSTALL_DIR")
|
|
if install_dir:
|
|
cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_INSTALL_QMLTYPES TRUE\n")
|
|
|
|
import_version = get_qml_import_version(scope, target)
|
|
if import_version:
|
|
cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_VERSION {import_version}\n")
|
|
|
|
past_major_versions = scope.expandString("QML_PAST_MAJOR_VERSIONS")
|
|
if past_major_versions:
|
|
cm_fh.write(f"{spaces(indent+1)}QT_QML_PAST_MAJOR_VERSIONS {past_major_versions}\n")
|
|
|
|
import_name = scope.expandString("QML_IMPORT_NAME")
|
|
if import_name:
|
|
cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_URI {import_name}\n")
|
|
|
|
json_output_filename = scope.expandString("QMLTYPES_FILENAME")
|
|
if json_output_filename:
|
|
cm_fh.write(f"{spaces(indent+1)}QT_QMLTYPES_FILENAME {json_output_filename}\n")
|
|
|
|
target_path = scope.get("TARGETPATH")
|
|
if target_path:
|
|
cm_fh.write(f"{spaces(indent+1)}QT_QML_MODULE_TARGET_PATH {target_path}\n")
|
|
|
|
if install_dir:
|
|
install_dir = install_dir.replace("$$[QT_INSTALL_QML]", "${INSTALL_QMLDIR}")
|
|
cm_fh.write(f'{spaces(indent+1)}QT_QML_MODULE_INSTALL_DIR "{install_dir}"\n')
|
|
|
|
cm_fh.write(f"{spaces(indent)})\n\n")
|
|
cm_fh.write(f"qt6_qml_type_registration({target_ref})\n")
|
|
|
|
|
|
def handle_top_level_repo_project(scope: Scope, cm_fh: IO[str]):
|
|
# qtdeclarative
|
|
project_file_name = os.path.splitext(os.path.basename(scope.file_absolute_path))[0]
|
|
|
|
# declarative
|
|
file_name_without_qt_prefix = project_file_name[2:]
|
|
|
|
# Qt::Declarative
|
|
qt_lib = map_qt_library(file_name_without_qt_prefix)
|
|
|
|
# Found a mapping, adjust name.
|
|
if qt_lib != file_name_without_qt_prefix:
|
|
# QtDeclarative
|
|
qt_lib = re.sub(r":", r"", qt_lib)
|
|
|
|
# Declarative
|
|
qt_lib_no_prefix = qt_lib[2:]
|
|
else:
|
|
qt_lib += "_FIXME"
|
|
qt_lib_no_prefix = qt_lib
|
|
|
|
header = dedent(
|
|
f"""\
|
|
cmake_minimum_required(VERSION {cmake_version_string})
|
|
|
|
include(.cmake.conf)
|
|
project({qt_lib}
|
|
VERSION "${{QT_REPO_MODULE_VERSION}}"
|
|
DESCRIPTION "Qt {qt_lib_no_prefix} Libraries"
|
|
HOMEPAGE_URL "https://qt.io/"
|
|
LANGUAGES CXX C
|
|
)
|
|
|
|
find_package(Qt6 ${{PROJECT_VERSION}} CONFIG REQUIRED COMPONENTS BuildInternals Core SET_ME_TO_SOMETHING_USEFUL)
|
|
find_package(Qt6 ${{PROJECT_VERSION}} CONFIG OPTIONAL_COMPONENTS SET_ME_TO_SOMETHING_USEFUL)
|
|
|
|
"""
|
|
)
|
|
|
|
build_repo = dedent(
|
|
"""\
|
|
qt_build_repo()
|
|
"""
|
|
)
|
|
|
|
cm_fh.write(f"{header}{expand_project_requirements(scope)}{build_repo}")
|
|
|
|
|
|
def create_top_level_cmake_conf():
|
|
conf_file_name = ".cmake.conf"
|
|
try:
|
|
with open(conf_file_name, "x") as file:
|
|
file.write('set(QT_REPO_MODULE_VERSION "6.7.0")\n')
|
|
except FileExistsError:
|
|
pass
|
|
|
|
|
|
def find_top_level_repo_project_file(project_file_path: str = "") -> Optional[str]:
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_dir = os.path.dirname(qmake_or_cmake_conf_path)
|
|
|
|
# Hope to a programming god that there's only one .pro file at the
|
|
# top level directory of repository.
|
|
glob_result = glob.glob(os.path.join(qmake_or_cmake_dir, "*.pro"))
|
|
if len(glob_result) > 0:
|
|
return glob_result[0]
|
|
return None
|
|
|
|
|
|
def handle_top_level_repo_tests_project(scope: Scope, cm_fh: IO[str]):
|
|
|
|
content = dedent(
|
|
"""\
|
|
if(QT_BUILD_STANDALONE_TESTS)
|
|
# Add qt_find_package calls for extra dependencies that need to be found when building
|
|
# the standalone tests here.
|
|
endif()
|
|
qt_build_tests()
|
|
"""
|
|
)
|
|
|
|
cm_fh.write(f"{content}")
|
|
|
|
|
|
def write_regular_cmake_target_scope_section(
|
|
scope: Scope, cm_fh: IO[str], indent: int = 0, skip_sources: bool = False
|
|
):
|
|
if not skip_sources:
|
|
target_sources = "target_sources(${PROJECT_NAME} PUBLIC"
|
|
write_all_source_file_lists(cm_fh, scope, target_sources, indent=indent, footer=")")
|
|
|
|
write_include_paths(
|
|
cm_fh,
|
|
scope,
|
|
"target_include_directories(${{PROJECT_NAME}} PUBLIC",
|
|
indent=indent,
|
|
footer=")",
|
|
)
|
|
write_defines(
|
|
cm_fh,
|
|
scope,
|
|
"target_compile_definitions(${{PROJECT_NAME}} PUBLIC",
|
|
indent=indent,
|
|
footer=")",
|
|
)
|
|
(public_libs, private_libs) = extract_cmake_libraries(scope)
|
|
write_list(
|
|
cm_fh,
|
|
private_libs,
|
|
"",
|
|
indent=indent,
|
|
header="target_link_libraries(${{PROJECT_NAME}} PRIVATE\n",
|
|
footer=")",
|
|
)
|
|
write_list(
|
|
cm_fh,
|
|
public_libs,
|
|
"",
|
|
indent=indent,
|
|
header="target_link_libraries(${{PROJECT_NAME}} PUBLIC\n",
|
|
footer=")",
|
|
)
|
|
write_compile_options(
|
|
cm_fh, scope, "target_compile_options(${{PROJECT_NAME}}", indent=indent, footer=")"
|
|
)
|
|
|
|
|
|
def handle_config_test_project(scope: Scope, cm_fh: IO[str]):
|
|
project_name = os.path.splitext(os.path.basename(scope.file_absolute_path))[0]
|
|
content = (
|
|
f"cmake_minimum_required(VERSION 3.16)\n"
|
|
f"project(config_test_{project_name} LANGUAGES C CXX)\n"
|
|
"""
|
|
if(DEFINED QT_CONFIG_COMPILE_TEST_CMAKE_SYSTEM_PREFIX_PATH)
|
|
set(CMAKE_SYSTEM_PREFIX_PATH "${QT_CONFIG_COMPILE_TEST_CMAKE_SYSTEM_PREFIX_PATH}")
|
|
endif()
|
|
if(DEFINED QT_CONFIG_COMPILE_TEST_CMAKE_SYSTEM_FRAMEWORK_PATH)
|
|
set(CMAKE_SYSTEM_FRAMEWORK_PATH "${QT_CONFIG_COMPILE_TEST_CMAKE_SYSTEM_FRAMEWORK_PATH}")
|
|
endif()
|
|
|
|
foreach(p ${QT_CONFIG_COMPILE_TEST_PACKAGES})
|
|
find_package(${p})
|
|
endforeach()
|
|
|
|
if(QT_CONFIG_COMPILE_TEST_LIBRARIES)
|
|
link_libraries(${QT_CONFIG_COMPILE_TEST_LIBRARIES})
|
|
endif()
|
|
if(QT_CONFIG_COMPILE_TEST_LIBRARY_TARGETS)
|
|
foreach(lib ${QT_CONFIG_COMPILE_TEST_LIBRARY_TARGETS})
|
|
if(TARGET ${lib})
|
|
link_libraries(${lib})
|
|
endif()
|
|
endforeach()
|
|
endif()
|
|
"""
|
|
)
|
|
cm_fh.write(f"{content}\n")
|
|
|
|
# Remove default QT libs.
|
|
scope._append_operation("QT", RemoveOperation(["core", "gui"]))
|
|
|
|
add_target = "add_executable(${{PROJECT_NAME}}"
|
|
|
|
temp_buffer = io.StringIO()
|
|
write_all_source_file_lists(temp_buffer, scope, add_target, indent=0)
|
|
buffer_value = temp_buffer.getvalue()
|
|
|
|
if buffer_value:
|
|
cm_fh.write(buffer_value)
|
|
else:
|
|
cm_fh.write(add_target)
|
|
cm_fh.write(")\n")
|
|
|
|
indent = 0
|
|
write_regular_cmake_target_scope_section(scope, cm_fh, indent, skip_sources=True)
|
|
|
|
recursive_evaluate_scope(scope)
|
|
scopes = flatten_scopes(scope)
|
|
scopes = merge_scopes(scopes)
|
|
|
|
assert len(scopes)
|
|
assert scopes[0].total_condition == "ON"
|
|
|
|
for c in scopes[1:]:
|
|
extend_scope_io_string = io.StringIO()
|
|
write_regular_cmake_target_scope_section(c, extend_scope_io_string, indent=indent + 1)
|
|
extend_string = extend_scope_io_string.getvalue()
|
|
|
|
if extend_string:
|
|
assert c.total_condition, "Cannot write if with empty condition"
|
|
extend_scope = (
|
|
f"\nif({map_to_cmake_condition(c.total_condition)})\n"
|
|
f"{extend_string}"
|
|
f"endif()\n"
|
|
)
|
|
cm_fh.write(extend_scope)
|
|
|
|
|
|
def cmakeify_scope(
|
|
scope: Scope,
|
|
cm_fh: IO[str],
|
|
*,
|
|
indent: int = 0,
|
|
is_example: bool = False,
|
|
is_user_project: bool = False,
|
|
) -> None:
|
|
template = scope.TEMPLATE
|
|
|
|
if is_user_project:
|
|
if template == "subdirs":
|
|
handle_subdir(scope, cm_fh, indent=indent, is_example=True, is_user_project=True)
|
|
elif template in ("app", "lib"):
|
|
handle_app_or_lib(scope, cm_fh, indent=indent, is_example=True, is_user_project=True)
|
|
else:
|
|
temp_buffer = io.StringIO()
|
|
|
|
# Handle top level repo project in a special way.
|
|
if is_top_level_repo_project(scope.file_absolute_path):
|
|
create_top_level_cmake_conf()
|
|
handle_top_level_repo_project(scope, temp_buffer)
|
|
# Same for top-level tests.
|
|
elif is_top_level_repo_tests_project(scope.file_absolute_path):
|
|
handle_top_level_repo_tests_project(scope, temp_buffer)
|
|
elif is_config_test_project(scope.file_absolute_path):
|
|
handle_config_test_project(scope, temp_buffer)
|
|
elif template == "subdirs":
|
|
handle_subdir(scope, temp_buffer, indent=indent, is_example=is_example)
|
|
elif template in ("app", "lib"):
|
|
handle_app_or_lib(scope, temp_buffer, indent=indent, is_example=is_example)
|
|
else:
|
|
print(f" XXXX: {scope.file}: Template type {template} not yet supported.")
|
|
|
|
buffer_value = temp_buffer.getvalue()
|
|
|
|
if is_top_level_repo_examples_project(scope.file_absolute_path):
|
|
# Wrap top level examples project with some commands which
|
|
# are necessary to build examples as part of the overall
|
|
# build.
|
|
buffer_value = f"qt_examples_build_begin()\n\n{buffer_value}\nqt_examples_build_end()\n"
|
|
|
|
cm_fh.write(buffer_value)
|
|
|
|
|
|
def generate_new_cmakelists(
|
|
scope: Scope, *, is_example: bool = False, is_user_project: bool = True, debug: bool = False
|
|
) -> None:
|
|
if debug:
|
|
print("Generating CMakeLists.gen.txt")
|
|
with open(scope.generated_cmake_lists_path, "w") as cm_fh:
|
|
assert scope.file
|
|
cm_fh.write(f"# Generated from {os.path.basename(scope.file)}.\n\n")
|
|
|
|
is_example_heuristic = is_example_project(scope.file_absolute_path)
|
|
final_is_example_decision = is_example or is_example_heuristic
|
|
cmakeify_scope(
|
|
scope, cm_fh, is_example=final_is_example_decision, is_user_project=is_user_project
|
|
)
|
|
|
|
|
|
def do_include(scope: Scope, *, debug: bool = False) -> None:
|
|
for c in scope.children:
|
|
do_include(c)
|
|
|
|
for include_index, include_file in enumerate(scope.get_files("_INCLUDED", is_include=True)):
|
|
if not include_file:
|
|
continue
|
|
# Ignore selfcover.pri as this generates too many incompatible flags
|
|
# need to be removed with special cases
|
|
if include_file.endswith("selfcover.pri"):
|
|
continue
|
|
if include_file.startswith("${QT_SOURCE_TREE}"):
|
|
root_source_dir = get_top_level_repo_project_path(scope.file_absolute_path)
|
|
include_file = include_file.replace("${QT_SOURCE_TREE}", root_source_dir)
|
|
if not os.path.isfile(include_file):
|
|
generated_config_pri_pattern = re.compile(r"qt.+?-config\.pri$")
|
|
match_result = re.search(generated_config_pri_pattern, include_file)
|
|
if not match_result:
|
|
print(f" XXXX: Failed to include {include_file}.")
|
|
continue
|
|
|
|
include_op = scope._get_operation_at_index("_INCLUDED", include_index)
|
|
include_line_no = include_op._line_no
|
|
|
|
include_result, project_file_content = parseProFile(include_file, debug=debug)
|
|
include_scope = Scope.FromDict(
|
|
None,
|
|
include_file,
|
|
include_result.asDict().get("statements"),
|
|
"",
|
|
scope.basedir,
|
|
project_file_content=project_file_content,
|
|
parent_include_line_no=include_line_no,
|
|
) # This scope will be merged into scope!
|
|
|
|
do_include(include_scope)
|
|
|
|
scope.merge(include_scope)
|
|
|
|
|
|
def copy_generated_file_to_final_location(
|
|
scope: Scope, output_file: str, keep_temporary_files=False, debug: bool = False
|
|
) -> None:
|
|
if debug:
|
|
print(f"Copying {scope.generated_cmake_lists_path} to {output_file}")
|
|
|
|
base_dir = os.path.dirname(output_file)
|
|
base_dir_abs = os.path.realpath(base_dir)
|
|
os.makedirs(base_dir_abs, exist_ok=True)
|
|
|
|
copyfile(scope.generated_cmake_lists_path, output_file)
|
|
if not keep_temporary_files:
|
|
os.remove(scope.generated_cmake_lists_path)
|
|
|
|
|
|
def cmake_project_has_skip_marker(project_file_path: str = "") -> bool:
|
|
dir_path = os.path.dirname(project_file_path)
|
|
cmake_project_path = os.path.join(dir_path, "CMakeLists.txt")
|
|
if not os.path.exists(cmake_project_path):
|
|
return False
|
|
|
|
with open(cmake_project_path, "r") as file_fd:
|
|
contents = file_fd.read()
|
|
|
|
if "# special case skip regeneration" in contents:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def should_convert_project(project_file_path: str = "", ignore_skip_marker: bool = False) -> bool:
|
|
qmake_or_cmake_conf_path = find_qmake_or_cmake_conf(project_file_path)
|
|
qmake_or_cmake_conf_dir_path = os.path.dirname(qmake_or_cmake_conf_path)
|
|
|
|
project_relative_path = os.path.relpath(project_file_path, qmake_or_cmake_conf_dir_path)
|
|
|
|
# Skip cmake auto tests, they should not be converted.
|
|
if project_relative_path.startswith("tests/auto/cmake"):
|
|
return False
|
|
if project_relative_path.startswith("tests/auto/installed_cmake"):
|
|
return False
|
|
|
|
# Skip qmake testdata projects.
|
|
if project_relative_path.startswith("tests/auto/tools/qmake/testdata"):
|
|
return False
|
|
|
|
# Skip doc snippets.
|
|
if fnmatch.fnmatch(project_relative_path, "src/*/doc/snippets/*"):
|
|
return False
|
|
|
|
# Skip certain config tests.
|
|
config_tests = [
|
|
# Relative to qtbase/config.tests
|
|
"arch/arch.pro",
|
|
"avx512/avx512.pro",
|
|
"stl/stl.pro",
|
|
"verifyspec/verifyspec.pro",
|
|
"x86_simd/x86_simd.pro",
|
|
# Relative to repo src dir
|
|
"config.tests/hostcompiler/hostcompiler.pro",
|
|
]
|
|
skip_certain_tests = any(project_relative_path.startswith(c) for c in config_tests)
|
|
if skip_certain_tests:
|
|
return False
|
|
|
|
# Skip if CMakeLists.txt in the same path as project_file_path has a
|
|
# special skip marker.
|
|
if not ignore_skip_marker and cmake_project_has_skip_marker(project_file_path):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def should_convert_project_after_parsing(
|
|
file_scope: Scope, skip_subdirs_project: bool = False
|
|
) -> bool:
|
|
template = file_scope.TEMPLATE
|
|
if template == "subdirs" and skip_subdirs_project:
|
|
return False
|
|
return True
|
|
|
|
|
|
def main() -> None:
|
|
# Be sure of proper Python version
|
|
assert sys.version_info >= (3, 7)
|
|
|
|
args = _parse_commandline()
|
|
|
|
debug_parsing = args.debug_parser or args.debug
|
|
if args.skip_condition_cache:
|
|
set_condition_simplified_cache_enabled(False)
|
|
|
|
backup_current_dir = os.getcwd()
|
|
|
|
for file in args.files:
|
|
new_current_dir = os.path.dirname(file)
|
|
file_relative_path = os.path.basename(file)
|
|
if new_current_dir:
|
|
os.chdir(new_current_dir)
|
|
|
|
project_file_absolute_path = os.path.abspath(file_relative_path)
|
|
if not should_convert_project(project_file_absolute_path, args.ignore_skip_marker):
|
|
print(f'Skipping conversion of project: "{project_file_absolute_path}"')
|
|
continue
|
|
|
|
parseresult, project_file_content = parseProFile(file_relative_path, debug=debug_parsing)
|
|
|
|
# If CMake api version is given on command line, that means the
|
|
# user wants to force use that api version.
|
|
global cmake_api_version
|
|
if args.api_version:
|
|
cmake_api_version = args.api_version
|
|
else:
|
|
# Otherwise detect the api version in the old CMakeLists.txt
|
|
# if it exists.
|
|
detected_cmake_api_version = detect_cmake_api_version_used_in_file_content(
|
|
file_relative_path
|
|
)
|
|
if detected_cmake_api_version:
|
|
cmake_api_version = detected_cmake_api_version
|
|
|
|
if args.debug_parse_result or args.debug:
|
|
print("\n\n#### Parser result:")
|
|
print(parseresult)
|
|
print("\n#### End of parser result.\n")
|
|
if args.debug_parse_dictionary or args.debug:
|
|
print("\n\n####Parser result dictionary:")
|
|
print(parseresult.asDict())
|
|
print("\n#### End of parser result dictionary.\n")
|
|
|
|
file_scope = Scope.FromDict(
|
|
None,
|
|
file_relative_path,
|
|
parseresult.asDict().get("statements"),
|
|
project_file_content=project_file_content,
|
|
)
|
|
|
|
if args.debug_pro_structure or args.debug:
|
|
print("\n\n#### .pro/.pri file structure:")
|
|
file_scope.dump()
|
|
print("\n#### End of .pro/.pri file structure.\n")
|
|
|
|
do_include(file_scope, debug=debug_parsing)
|
|
|
|
if args.debug_full_pro_structure or args.debug:
|
|
print("\n\n#### Full .pro/.pri file structure:")
|
|
file_scope.dump()
|
|
print("\n#### End of full .pro/.pri file structure.\n")
|
|
|
|
if not should_convert_project_after_parsing(file_scope, args.skip_subdirs_project):
|
|
print(f'Skipping conversion of project: "{project_file_absolute_path}"')
|
|
continue
|
|
|
|
generate_new_cmakelists(
|
|
file_scope,
|
|
is_example=args.is_example,
|
|
is_user_project=args.is_user_project,
|
|
debug=args.debug,
|
|
)
|
|
|
|
copy_generated_file = True
|
|
|
|
output_file = file_scope.original_cmake_lists_path
|
|
if args.output_file:
|
|
output_file = args.output_file
|
|
|
|
if not args.skip_special_case_preservation:
|
|
debug_special_case = args.debug_special_case_preservation or args.debug
|
|
handler = SpecialCaseHandler(
|
|
output_file,
|
|
file_scope.generated_cmake_lists_path,
|
|
file_scope.basedir,
|
|
keep_temporary_files=args.keep_temporary_files,
|
|
debug=debug_special_case,
|
|
)
|
|
|
|
copy_generated_file = handler.handle_special_cases()
|
|
|
|
if copy_generated_file:
|
|
copy_generated_file_to_final_location(
|
|
file_scope, output_file, keep_temporary_files=args.keep_temporary_files
|
|
)
|
|
os.chdir(backup_current_dir)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|