qt5base-lts/util/cmake/pro2cmake.py
Simon Hausmann ceb2937498 Fix add_qt_docs calls
Brown paper bag for me, don't use a comma to separate parameters when
calling a function.

Change-Id: I21e16142fab4fd1c2714df13cd6a892b036e899a
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
2019-09-19 07:43:13 +00:00

2991 lines
99 KiB
Python
Executable File

#!/usr/bin/env python3
#############################################################################
##
## Copyright (C) 2018 The Qt Company Ltd.
## Contact: https://www.qt.io/licensing/
##
## This file is part of the plugins of the Qt Toolkit.
##
## $QT_BEGIN_LICENSE:GPL-EXCEPT$
## Commercial License Usage
## Licensees holding valid commercial Qt licenses may use this file in
## accordance with the commercial license agreement provided with the
## Software or, alternatively, in accordance with the terms contained in
## a written agreement between you and The Qt Company. For licensing terms
## and conditions see https://www.qt.io/terms-conditions. For further
## information use the contact form at https://www.qt.io/contact-us.
##
## GNU General Public License Usage
## Alternatively, this file may be used under the terms of the GNU
## General Public License version 3 as published by the Free Software
## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
## included in the packaging of this file. Please review the following
## information to ensure the GNU General Public License requirements will
## be met: https://www.gnu.org/licenses/gpl-3.0.html.
##
## $QT_END_LICENSE$
##
#############################################################################
from __future__ import annotations
import copy
import os.path
import posixpath
import sys
import re
import io
import glob
import collections
try:
collectionsAbc = collections.abc
except AttributeError:
collectionsAbc = collections
import pyparsing as pp
import xml.etree.ElementTree as ET
from argparse import ArgumentParser
from textwrap import dedent
from itertools import chain
from shutil import copyfile
from sympy.logic import simplify_logic, And, Or, Not
from sympy.core.sympify import SympifyError
from typing import List, Optional, Dict, Set, IO, Union, Mapping, Any, Callable, FrozenSet, Tuple
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,
)
from helper import _set_up_py_parsing_nicer_debug_output
_set_up_py_parsing_nicer_debug_output(pp)
cmake_version_string = "3.15.0"
def _parse_commandline():
parser = ArgumentParser(
description="Generate CMakeLists.txt files from ." "pro files.",
epilog="Requirements: pip install sympy pyparsing",
)
parser.add_argument(
"--debug", dest="debug", action="store_true", help="Turn on all debug output"
)
parser.add_argument(
"--debug-parser",
dest="debug_parser",
action="store_true",
help="Print debug output from qmake parser.",
)
parser.add_argument(
"--debug-parse-result",
dest="debug_parse_result",
action="store_true",
help="Dump the qmake parser result.",
)
parser.add_argument(
"--debug-parse-dictionary",
dest="debug_parse_dictionary",
action="store_true",
help="Dump the qmake parser result as dictionary.",
)
parser.add_argument(
"--debug-pro-structure",
dest="debug_pro_structure",
action="store_true",
help="Dump the structure of the qmake .pro-file.",
)
parser.add_argument(
"--debug-full-pro-structure",
dest="debug_full_pro_structure",
action="store_true",
help="Dump the full structure of the qmake .pro-file " "(with includes).",
)
parser.add_argument(
"--debug-special-case-preservation",
dest="debug_special_case_preservation",
action="store_true",
help="Show all git commands and file copies.",
)
parser.add_argument(
"--is-example",
action="store_true",
dest="is_example",
help="Treat the input .pro file as an example.",
)
parser.add_argument(
"-s",
"--skip-special-case-preservation",
dest="skip_special_case_preservation",
action="store_true",
help="Skips behavior to reapply " "special case modifications (requires git in PATH)",
)
parser.add_argument(
"-k",
"--keep-temporary-files",
dest="keep_temporary_files",
action="store_true",
help="Don't automatically remove CMakeLists.gen.txt and other " "intermediate files.",
)
parser.add_argument(
"files",
metavar="<.pro/.pri file>",
type=str,
nargs="+",
help="The .pro/.pri file to process",
)
return parser.parse_args()
def is_top_level_repo_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_dir_path = os.path.dirname(project_file_path)
return qmake_conf_dir_path == project_dir_path
def is_top_level_repo_tests_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_dir_path = os.path.dirname(project_file_path)
project_dir_name = os.path.basename(project_dir_path)
maybe_same_level_dir_path = os.path.join(project_dir_path, "..")
normalized_maybe_same_level_dir_path = os.path.normpath(maybe_same_level_dir_path)
return (
qmake_conf_dir_path == normalized_maybe_same_level_dir_path and project_dir_name == "tests"
)
def is_top_level_repo_examples_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_dir_path = os.path.dirname(project_file_path)
project_dir_name = os.path.basename(project_dir_path)
maybe_same_level_dir_path = os.path.join(project_dir_path, "..")
normalized_maybe_same_level_dir_path = os.path.normpath(maybe_same_level_dir_path)
return (
qmake_conf_dir_path == normalized_maybe_same_level_dir_path
and project_dir_name == "examples"
)
def is_example_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
# If the project file is found in a subdir called 'examples'
# relative to the repo source dir, then it must be an example.
return project_relative_path.startswith("examples")
def find_qmake_conf(project_file_path: str = "") -> Optional[str]:
if not os.path.isabs(project_file_path):
print(
f"Warning: could not find .qmake.conf file, given path is not an "
f"absolute path: {project_file_path}"
)
return None
cwd = os.path.dirname(project_file_path)
file_name = ".qmake.conf"
while os.path.isdir(cwd):
maybe_file = posixpath.join(cwd, file_name)
if os.path.isfile(maybe_file):
return maybe_file
else:
cwd = os.path.dirname(cwd)
return None
def process_qrc_file(
target: str,
filepath: str,
base_dir: str = "",
project_file_path: str = "",
skip_qtquick_compiler: bool = False,
retain_qtquick_compiler: bool = False,
is_example: bool = False,
) -> str:
assert target
# Hack to handle QT_SOURCE_TREE. Assume currently that it's the same
# as the qtbase source path.
qt_source_tree_literal = "${QT_SOURCE_TREE}"
if qt_source_tree_literal in filepath:
qmake_conf = find_qmake_conf(project_file_path)
if qmake_conf:
qt_source_tree = os.path.dirname(qmake_conf)
filepath = filepath.replace(qt_source_tree_literal, qt_source_tree)
else:
print(
f"Warning, could not determine QT_SOURCE_TREE location while trying "
f"to find: {filepath}"
)
resource_name = os.path.splitext(os.path.basename(filepath))[0]
dir_name = os.path.dirname(filepath)
base_dir = posixpath.join("" if base_dir == "." else base_dir, dir_name)
# Small not very thorough check to see if this a shared qrc resource
# pattern is mostly used by the tests.
is_parent_path = dir_name.startswith("..")
if not os.path.isfile(filepath):
raise RuntimeError(f"Invalid file path given to process_qrc_file: {filepath}")
tree = ET.parse(filepath)
root = tree.getroot()
assert root.tag == "RCC"
output = ""
resource_count = 0
for resource in root:
assert resource.tag == "qresource"
lang = resource.get("lang", "")
prefix = resource.get("prefix", "/")
if not prefix.startswith("/"):
prefix = "/" + prefix
full_resource_name = resource_name + (str(resource_count) if resource_count > 0 else "")
files: Dict[str, str] = {}
for file in resource:
path = file.text
assert path
# Get alias:
alias = file.get("alias", "")
# In cases where examples use shared resources, we set the alias
# too the same name of the file, or the applications won't be
# be able to locate the resource
if not alias and is_parent_path:
alias = path
files[path] = alias
output += write_add_qt_resource_call(
target,
full_resource_name,
prefix,
base_dir,
lang,
files,
skip_qtquick_compiler,
retain_qtquick_compiler,
is_example,
)
resource_count += 1
return output
def write_add_qt_resource_call(
target: str,
resource_name: str,
prefix: Optional[str],
base_dir: Optional[str],
lang: Optional[str],
files: Dict[str, str],
skip_qtquick_compiler: bool,
retain_qtquick_compiler: bool,
is_example: bool,
) -> str:
output = ""
sorted_files = sorted(files.keys())
assert sorted_files
for source in sorted_files:
alias = files[source]
if alias:
full_source = posixpath.join(base_dir, source)
output += (
f'set_source_files_properties("{full_source}"\n'
f' PROPERTIES QT_RESOURCE_ALIAS "{alias}"\n)\n'
)
# 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 += f"set({resource_name}_resource_files\n {file_list}\n)\n\n"
file_list = f"${{{resource_name}_resource_files}}"
if skip_qtquick_compiler:
output += (
f"set_source_files_properties(${{{resource_name}_resource_files}}"
" PROPERTIES QT_SKIP_QUICKCOMPILER 1)\n\n"
)
if retain_qtquick_compiler:
output += (
f"set_source_files_properties(${{{resource_name}_resource_files}}"
"PROPERTIES QT_RETAIN_QUICKCOMPILER 1)\n\n"
)
params = ""
if lang:
params += f'{spaces(1)}LANG\n{spaces(2)}"{lang}"\n'
params += f'{spaces(1)}PREFIX\n{spaces(2)}"{prefix}"\n'
if base_dir:
params += f'{spaces(1)}BASE\n{spaces(2)}"{base_dir}"\n'
add_resource_command = ""
if is_example:
add_resource_command = "QT6_ADD_RESOURCES"
else:
add_resource_command = "add_qt_resource"
output += (
f'{add_resource_command}({target} "{resource_name}"\n{params}{spaces(1)}FILES\n'
f"{spaces(2)}{file_list}\n)\n"
)
return output
def fixup_linecontinuation(contents: str) -> str:
# Remove all line continuations, aka a backslash followed by
# a newline character with an arbitrary amount of whitespace
# between the backslash and the newline.
# This greatly simplifies the qmake parsing grammar.
contents = re.sub(r"([^\t ])\\[ \t]*\n", "\\1 ", contents)
contents = re.sub(r"\\[ \t]*\n", "", contents)
return contents
def fixup_comments(contents: str) -> str:
# Get rid of completely commented out lines.
# So any line which starts with a '#' char and ends with a new line
# will be replaced by a single new line.
#
# This is needed because qmake syntax is weird. In a multi line
# assignment (separated by backslashes and newlines aka
# # \\\n ), if any of the lines are completely commented out, in
# principle the assignment should fail.
#
# It should fail because you would have a new line separating
# the previous value from the next value, and the next value would
# not be interpreted as a value, but as a new token / operation.
# qmake is lenient though, and accepts that, so we need to take
# care of it as well, as if the commented line didn't exist in the
# first place.
contents = re.sub(r"\n#[^\n]*?\n", "\n", contents, re.DOTALL)
return contents
def 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"
def flatten_list(l):
""" Flattens an irregular nested list into a simple list."""
for el in l:
if isinstance(el, collectionsAbc.Iterable) and not isinstance(el, (str, bytes)):
yield from flatten_list(el)
else:
yield el
def handle_function_value(group: pp.ParseResults):
function_name = group[0]
function_args = group[1]
if function_name == "qtLibraryTarget":
if len(function_args) > 1:
raise RuntimeError(
"Don't know what to with more than one function argument "
"for $$qtLibraryTarget()."
)
return str(function_args[0])
if function_name == "quote":
# Do nothing, just return a string result
return str(group)
if function_name == "files":
if len(function_args) > 1:
raise RuntimeError(
"Don't know what to with more than one function argument for $$files()."
)
return str(function_args[0])
if isinstance(function_args, pp.ParseResults):
function_args = list(flatten_list(function_args.asList()))
# Return the whole expression as a string.
if function_name in [
"join",
"files",
"cmakeRelativePath",
"shell_quote",
"shadowed",
"cmakeTargetPath",
"shell_path",
"cmakeProcessLibs",
"cmakeTargetPaths",
"cmakePortablePaths",
"escape_expand",
"member",
]:
return f"join({''.join(function_args)})"
class Operation:
def __init__(self, value: Union[List[str], str]):
if isinstance(value, list):
self._value = value
else:
self._value = [str(value)]
def process(
self, key: str, input: 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, input: List[str], transformer: Callable[[List[str]], List[str]]
) -> List[str]:
return input + transformer(self._value)
def __repr__(self):
return f"+({self._dump()})"
class UniqueAddOperation(Operation):
def process(
self, key: str, input: List[str], transformer: Callable[[List[str]], List[str]]
) -> List[str]:
result = input
for v in transformer(self._value):
if v not in result:
result.append(v)
return result
def __repr__(self):
return f"*({self._dump()})"
class SetOperation(Operation):
def process(
self, key: str, input: 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 += input
if transformer:
return list(transformer(values))
else:
return values
def __repr__(self):
return f"=({self._dump()})"
class RemoveOperation(Operation):
def __init__(self, value):
super().__init__(value)
def process(
self, key: str, input: List[str], transformer: Callable[[List[str]], List[str]]
) -> List[str]:
input_set = set(input)
value_set = set(self._value)
result: List[str] = []
# Add everything that is not going to get removed:
for v in input:
if v not in value_set:
result += [v]
# Add everything else with removal marker:
for v in transformer(self._value):
if v not in input_set:
result += [f"-{v}"]
return result
def __repr__(self):
return f"-({self._dump()})"
class Scope(object):
SCOPE_ID: int = 1
def __init__(
self,
*,
parent_scope: Optional[Scope],
file: Optional[str] = None,
condition: str = "",
base_dir: str = "",
operations: Union[Mapping[str, List[Operation]], None] = None,
) -> None:
if operations is None:
operations = {
"QT_SOURCE_TREE": [SetOperation(["${QT_SOURCE_TREE}"])],
"QT_BUILD_TREE": [SetOperation(["${PROJECT_BUILD_DIR}"])],
}
self._operations = 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 file:
self._currentdir = os.path.dirname(file)
if not self._currentdir:
self._currentdir = "."
if not self._basedir:
self._basedir = self._currentdir
self._scope_id = Scope.SCOPE_ID
Scope.SCOPE_ID += 1
self._file = file
self._file_absolute_path = os.path.abspath(file)
self._condition = map_condition(condition)
self._children = [] # type: List[Scope]
self._included_children = [] # type: List[Scope]
self._visited_keys = set() # type: Set[str]
self._total_condition = None # type: Optional[str]
def __repr__(self):
return (
f"{self._scope_id}:{self._basedir}:{self._currentdir}:{self._file}:"
f"{self._condition or '<TRUE>'}"
)
def reset_visited_keys(self):
self._visited_keys = set()
def merge(self, other: "Scope") -> None:
assert self != other
self._included_children.append(other)
@property
def scope_debug(self) -> bool:
merge = self.get_string("PRO2CMAKE_SCOPE_DEBUG").lower()
return merge == "1" or merge == "on" or merge == "yes" or merge == "true"
@property
def parent(self) -> Optional[Scope]:
return self._parent
@property
def basedir(self) -> str:
return self._basedir
@property
def currentdir(self) -> str:
return self._currentdir
def can_merge_condition(self):
if self._condition == "else":
return False
if self._operations:
return False
child_count = len(self._children)
if child_count == 0 or child_count > 2:
return False
assert child_count != 1 or self._children[0]._condition != "else"
return child_count == 1 or self._children[1]._condition == "else"
def settle_condition(self):
new_children: List[Scope] = []
for c in self._children:
c.settle_condition()
if c.can_merge_condition():
child = c._children[0]
child._condition = "({c._condition}) AND ({child._condition})"
new_children += c._children
else:
new_children.append(c)
self._children = new_children
@staticmethod
def FromDict(
parent_scope: Optional["Scope"], file: str, statements, cond: str = "", base_dir: str = ""
) -> Scope:
scope = Scope(parent_scope=parent_scope, file=file, condition=cond, base_dir=base_dir)
for statement in statements:
if isinstance(statement, list): # Handle skipped parts...
assert not statement
continue
operation = statement.get("operation", None)
if operation:
key = statement.get("key", "")
value = statement.get("value", [])
assert key != ""
if operation == "=":
scope._append_operation(key, SetOperation(value))
elif operation == "-=":
scope._append_operation(key, RemoveOperation(value))
elif operation == "+=":
scope._append_operation(key, AddOperation(value))
elif operation == "*=":
scope._append_operation(key, UniqueAddOperation(value))
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:
scope._append_operation("_INCLUDED", UniqueAddOperation(included))
continue
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, *, type: str = "ROOT", indent: int = 0) -> None:
print(f"{spaces(indent)}{type}: {self}")
for i in self._included_children:
i.dump_structure(type="INCL", indent=indent + 1)
for i in self._children:
i.dump_structure(type="CHLD", indent=indent + 1)
@property
def keys(self):
return self._operations.keys()
@property
def visited_keys(self):
return self._visited_keys
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)
# Inherrit values from above:
if self._parent and inherit:
result = self._parent._evalOps(key, transformer, result)
if transformer:
def op_transformer(files):
return transformer(self, files)
else:
def op_transformer(files):
return files
for op in self._operations.get(key, []):
result = op.process(key, result, op_transformer)
for ic in self._included_children:
result = list(ic._evalOps(key, transformer, result))
return result
def get(self, key: str, *, ignore_includes: bool = False, inherit: bool = False) -> List[str]:
is_same_path = self.currentdir == self.basedir
if not is_same_path:
relative_path = os.path.relpath(self.currentdir, self.basedir)
if key == "_PRO_FILE_PWD_":
return ["${CMAKE_CURRENT_SOURCE_DIR}"]
if key == "PWD":
if is_same_path:
return ["${CMAKE_CURRENT_SOURCE_DIR}"]
else:
return [f"${{CMAKE_CURRENT_SOURCE_DIR}}/{relative_path}"]
if key == "OUT_PWD":
if is_same_path:
return ["${CMAKE_CURRENT_BINARY_DIR}"]
else:
return [f"${{CMAKE_CURRENT_BINARY_DIR}}/{relative_path}"]
return self._evalOps(key, None, [], inherit=inherit)
def get_string(self, key: str, default: str = "", inherit: bool = False) -> str:
v = self.get(key, inherit=inherit)
if len(v) == 0:
return default
assert len(v) == 1
return v[0]
def _map_files(
self, files: List[str], *, use_vpath: bool = True, is_include: bool = False
) -> List[str]:
expanded_files = [] # type: List[str]
for f in files:
r = self._expand_value(f)
expanded_files += r
mapped_files = list(
map(lambda f: map_to_file(f, self, is_include=is_include), expanded_files)
)
if use_vpath:
result = list(
map(
lambda f: handle_vpath(f, self.basedir, self.get("VPATH", inherit=True)),
mapped_files,
)
)
else:
result = mapped_files
# strip ${CMAKE_CURRENT_SOURCE_DIR}:
result = list(
map(lambda f: f[28:] if f.startswith("${CMAKE_CURRENT_SOURCE_DIR}/") else f, result)
)
# strip leading ./:
result = list(map(lambda f: trim_leading_dot(f), result))
return result
def get_files(
self, key: str, *, use_vpath: bool = False, is_include: bool = False
) -> List[str]:
def transformer(scope, files):
return scope._map_files(files, use_vpath=use_vpath, is_include=is_include)
return list(self._evalOps(key, transformer, []))
@staticmethod
def _replace_env_var_value(value: Any) -> str:
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 = []
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 ""
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]
@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 _INCLUDED(self) -> List[str]:
return self.get("_INCLUDED")
class QmakeParser:
def __init__(self, *, debug: bool = False) -> None:
self.debug = debug
self._Grammar = self._generate_grammar()
def _generate_grammar(self):
# Define grammar:
pp.ParserElement.setDefaultWhitespaceChars(" \t")
def add_element(name: str, value: pp.ParserElement):
nonlocal self
if self.debug:
value.setName(name)
value.setDebug()
return value
EOL = add_element("EOL", pp.Suppress(pp.LineEnd()))
Else = add_element("Else", pp.Keyword("else"))
Identifier = add_element(
"Identifier", pp.Word(f"{pp.alphas}_", bodyChars=pp.alphanums + "_-./")
)
BracedValue = add_element(
"BracedValue",
pp.nestedExpr(
ignoreExpr=pp.quotedString
| pp.QuotedString(
quoteChar="$(", endQuoteChar=")", escQuote="\\", unquoteResults=False
)
).setParseAction(lambda s, l, t: ["(", *t[0], ")"]),
)
Substitution = add_element(
"Substitution",
pp.Combine(
pp.Literal("$")
+ (
(
(pp.Literal("$") + Identifier + pp.Optional(pp.nestedExpr()))
| (pp.Literal("(") + Identifier + pp.Literal(")"))
| (pp.Literal("{") + Identifier + pp.Literal("}"))
| (
pp.Literal("$")
+ pp.Literal("{")
+ Identifier
+ pp.Optional(pp.nestedExpr())
+ pp.Literal("}")
)
| (pp.Literal("$") + pp.Literal("[") + Identifier + pp.Literal("]"))
)
)
),
)
LiteralValuePart = add_element(
"LiteralValuePart", pp.Word(pp.printables, excludeChars="$#{}()")
)
SubstitutionValue = add_element(
"SubstitutionValue",
pp.Combine(pp.OneOrMore(Substitution | LiteralValuePart | pp.Literal("$"))),
)
FunctionValue = add_element(
"FunctionValue",
pp.Group(
pp.Suppress(pp.Literal("$") + pp.Literal("$"))
+ Identifier
+ pp.nestedExpr() # .setParseAction(lambda s, l, t: ['(', *t[0], ')'])
).setParseAction(lambda s, l, t: handle_function_value(*t)),
)
Value = add_element(
"Value",
pp.NotAny(Else | pp.Literal("}") | EOL)
+ (
pp.QuotedString(quoteChar='"', escChar="\\")
| FunctionValue
| SubstitutionValue
| BracedValue
),
)
Values = add_element("Values", pp.ZeroOrMore(Value)("value"))
Op = add_element(
"OP", pp.Literal("=") | pp.Literal("-=") | pp.Literal("+=") | pp.Literal("*=")
)
Key = add_element("Key", Identifier)
Operation = add_element("Operation", Key("key") + Op("operation") + Values("value"))
CallArgs = add_element("CallArgs", pp.nestedExpr())
def parse_call_args(results):
out = ""
for item in chain(*results):
if isinstance(item, str):
out += item
else:
out += "(" + parse_call_args(item) + ")"
return out
CallArgs.setParseAction(parse_call_args)
Load = add_element("Load", pp.Keyword("load") + CallArgs("loaded"))
Include = add_element("Include", pp.Keyword("include") + CallArgs("included"))
Option = add_element("Option", pp.Keyword("option") + CallArgs("option"))
# ignore the whole thing...
DefineTestDefinition = add_element(
"DefineTestDefinition",
pp.Suppress(
pp.Keyword("defineTest")
+ CallArgs
+ pp.nestedExpr(opener="{", closer="}", ignoreExpr=pp.LineEnd())
),
)
# ignore the whole thing...
ForLoop = add_element(
"ForLoop",
pp.Suppress(
pp.Keyword("for")
+ CallArgs
+ pp.nestedExpr(opener="{", closer="}", ignoreExpr=pp.LineEnd())
),
)
# ignore the whole thing...
ForLoopSingleLine = add_element(
"ForLoopSingleLine",
pp.Suppress(pp.Keyword("for") + CallArgs + pp.Literal(":") + pp.SkipTo(EOL)),
)
# ignore the whole thing...
FunctionCall = add_element("FunctionCall", pp.Suppress(Identifier + pp.nestedExpr()))
Scope = add_element("Scope", pp.Forward())
Statement = add_element(
"Statement",
pp.Group(
Load
| Include
| Option
| ForLoop
| ForLoopSingleLine
| DefineTestDefinition
| FunctionCall
| Operation
),
)
StatementLine = add_element("StatementLine", Statement + (EOL | pp.FollowedBy("}")))
StatementGroup = add_element(
"StatementGroup", pp.ZeroOrMore(StatementLine | Scope | pp.Suppress(EOL))
)
Block = add_element(
"Block",
pp.Suppress("{")
+ pp.Optional(EOL)
+ StatementGroup
+ pp.Optional(EOL)
+ pp.Suppress("}")
+ pp.Optional(EOL),
)
ConditionEnd = add_element(
"ConditionEnd",
pp.FollowedBy(
(pp.Optional(pp.White()) + (pp.Literal(":") | pp.Literal("{") | pp.Literal("|")))
),
)
ConditionPart1 = add_element(
"ConditionPart1", (pp.Optional("!") + Identifier + pp.Optional(BracedValue))
)
ConditionPart2 = add_element("ConditionPart2", pp.CharsNotIn("#{}|:=\\\n"))
ConditionPart = add_element(
"ConditionPart", (ConditionPart1 ^ ConditionPart2) + ConditionEnd
)
ConditionOp = add_element("ConditionOp", pp.Literal("|") ^ pp.Literal(":"))
ConditionWhiteSpace = add_element(
"ConditionWhiteSpace", pp.Suppress(pp.Optional(pp.White(" ")))
)
ConditionRepeated = add_element(
"ConditionRepeated", pp.ZeroOrMore(ConditionOp + ConditionWhiteSpace + ConditionPart)
)
Condition = add_element("Condition", pp.Combine(ConditionPart + ConditionRepeated))
Condition.setParseAction(lambda x: " ".join(x).strip().replace(":", " && ").strip(" && "))
# Weird thing like write_file(a)|error() where error() is the alternative condition
# which happens to be a function call. In this case there is no scope, but our code expects
# a scope with a list of statements, so create a fake empty statement.
ConditionEndingInFunctionCall = add_element(
"ConditionEndingInFunctionCall",
pp.Suppress(ConditionOp)
+ FunctionCall
+ pp.Empty().setParseAction(lambda x: [[]]).setResultsName("statements"),
)
SingleLineScope = add_element(
"SingleLineScope",
pp.Suppress(pp.Literal(":")) + pp.Group(Block | (Statement + EOL))("statements"),
)
MultiLineScope = add_element("MultiLineScope", Block("statements"))
SingleLineElse = add_element(
"SingleLineElse",
pp.Suppress(pp.Literal(":")) + (Scope | Block | (Statement + pp.Optional(EOL))),
)
MultiLineElse = add_element("MultiLineElse", Block)
ElseBranch = add_element("ElseBranch", pp.Suppress(Else) + (SingleLineElse | MultiLineElse))
# Scope is already add_element'ed in the forward declaration above.
Scope <<= pp.Group(
Condition("condition")
+ (SingleLineScope | MultiLineScope | ConditionEndingInFunctionCall)
+ pp.Optional(ElseBranch)("else_statements")
)
Grammar = StatementGroup("statements")
Grammar.ignore(pp.pythonStyleComment())
return Grammar
def parseFile(self, file: str):
print(f'Parsing "{file}"...')
try:
with open(file, "r") as file_fd:
contents = file_fd.read()
# old_contents = contents
contents = fixup_comments(contents)
contents = fixup_linecontinuation(contents)
result = self._Grammar.parseString(contents, parseAll=True)
except pp.ParseException as pe:
print(pe.line)
print(f"{' ' * (pe.col-1)}^")
print(pe)
raise pe
return result
def parseProFile(file: str, *, debug=False):
parser = QmakeParser(debug=debug)
return parser.parseFile(file)
def map_condition(condition: str) -> str:
# Some hardcoded cases that are too bothersome to generalize.
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)
def gcc_version_handler(match_obj: re.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)
# TODO: the current if(...) replacement makes the parentheses
# unbalanced when there are nested expressions.
# Need to fix this either with pypi regex recursive regexps,
# using pyparsing, or some other proper means of handling
# balanced parentheses.
condition = re.sub(r"\bif\s*\((.*?)\)", r"\1", condition)
condition = re.sub(r"\bisEmpty\s*\((.*?)\)", r"\1_ISEMPTY", 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)
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 ")
cmake_condition = ""
for part in condition.split():
# some features contain e.g. linux, that should not be
# turned upper case
feature = re.match(r"(qtConfig|qtHaveModule)\(([a-zA-Z0-9_-]+)\)", part)
if feature:
if feature.group(1) == "qtHaveModule":
part = f"TARGET {map_qt_library(feature.group(2))}"
else:
feature_name = featureName(feature.group(2))
if feature_name.startswith("system_") and is_known_3rd_party_library(
feature_name[7:]
):
part = "ON"
elif feature == "dlopen":
part = "ON"
else:
part = "QT_FEATURE_" + feature_name
else:
part = map_platform(part)
part = part.replace("true", "ON")
part = part.replace("false", "OFF")
cmake_condition += " " + part
return cmake_condition.strip()
def handle_subdir(
scope: Scope, cm_fh: IO[str], *, indent: int = 0, is_example: bool = False
) -> None:
# Global nested dictionary that will contain sub_dir assignments and their conditions.
# Declared as a global in order not to pollute the nested function signatures with giant
# type hints.
sub_dirs: Dict[str, Dict[str, Set[FrozenSet[str]]]] = {}
# Collects assignment conditions into global sub_dirs dict.
def collect_subdir_info(sub_dir_assignment: str, *, current_conditions: FrozenSet[str] = None):
subtraction = sub_dir_assignment.startswith("-")
if subtraction:
subdir_name = sub_dir_assignment[1:]
else:
subdir_name = sub_dir_assignment
if subdir_name not in sub_dirs:
sub_dirs[subdir_name] = {}
additions = sub_dirs[subdir_name].get("additions", set())
subtractions = sub_dirs[subdir_name].get("subtractions", set())
if current_conditions:
if subtraction:
subtractions.add(current_conditions)
else:
additions.add(current_conditions)
if additions:
sub_dirs[subdir_name]["additions"] = additions
if subtractions:
sub_dirs[subdir_name]["subtractions"] = subtractions
# Recursive helper that collects subdir info for given scope,
# and the children of the given scope.
def handle_subdir_helper(
scope: Scope,
cm_fh: IO[str],
*,
indent: int = 0,
current_conditions: FrozenSet[str] = None,
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 = parseProFile(sd, debug=False)
subdir_scope = Scope.FromDict(
scope, sd, subdir_result.asDict().get("statements"), "", scope.basedir
)
do_include(subdir_scope)
cmakeify_scope(subdir_scope, cm_fh, indent=indent, is_example=is_example)
else:
print(f" XXXX: SUBDIR {sd} in {scope}: Not found.")
# Collect info about conditions and SUBDIR assignments in child
# scopes, aka recursively call the same function, but with an
# updated current_conditions frozen set.
for c in scope.children:
# Use total_condition for 'else' conditions, otherwise just use the regular value to
# simplify the logic.
child_condition = c.total_condition if c.condition == "else" else c.condition
handle_subdir_helper(
c,
cm_fh,
indent=indent + 1,
is_example=is_example,
current_conditions=frozenset((*current_conditions, child_condition)),
)
def group_and_print_sub_dirs(indent: int = 0):
# Simplify conditions, and group
# subdirectories with the same conditions.
grouped_sub_dirs = {}
# 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
condition_simplified = simplify_condition(condition_str)
condition_key = condition_simplified
sub_dir_list_by_key = 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 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()
# Compute the total condition for scopes. Needed for scopes that
# have 'else' as a condition.
recursive_evaluate_scope(scope)
# Do the work.
handle_subdir_helper(
scope, cm_fh, indent=indent, current_conditions=current_conditions, is_example=is_example
)
group_and_print_sub_dirs(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
dir = os.path.dirname(s)
base = os.path.splitext(os.path.basename(s))[0]
if base.endswith("_p"):
base = base[:-2]
sort_name = posixpath.join(dir, base)
array = to_sort.get(sort_name, [])
array.append(s)
to_sort[sort_name] = array
lines = []
for k in sorted(to_sort.keys()):
lines.append(" ".join(sorted(to_sort[k])))
return lines
def _map_libraries_to_cmake(libraries: List[str], known_libraries: Set[str]) -> List[str]:
result = [] # type: List[str]
is_framework = False
for lib in libraries:
if lib == "-framework":
is_framework = True
continue
if is_framework:
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: Set[str] = set()
) -> Tuple[List[str], List[str]]:
public_dependencies = [] # type: List[str]
private_dependencies = [] # type: List[str]
for key in ["QMAKE_USE", "LIBS"]:
public_dependencies += scope.expand(key)
for key in ["QMAKE_USE_PRIVATE", "QMAKE_USE_FOR_PRIVATE", "LIBS_PRIVATE"]:
private_dependencies += scope.expand(key)
for key in ["QT_FOR_PRIVATE", "QT_PRIVATE"]:
private_dependencies += [map_qt_library(q) for q in scope.expand(key)]
for key in ["QT"]:
# Qt public libs: These may include FooPrivate in which case we get
# a private dependency on FooPrivate as well as a public dependency on Foo
for lib in scope.expand(key):
mapped_lib = map_qt_library(lib)
if mapped_lib.endswith("Private"):
private_dependencies.append(mapped_lib)
public_dependencies.append(mapped_lib[:-7])
else:
public_dependencies.append(mapped_lib)
return (
_map_libraries_to_cmake(public_dependencies, known_libraries),
_map_libraries_to_cmake(private_dependencies, known_libraries),
)
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 = "",
):
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}{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)
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", "NO_PCH_SOURCES", "FORMS"] + extra_keys,
indent,
footer=footer,
)
def write_defines(
cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = ""
):
defines = scope.expand("DEFINES")
defines += [d[2:] for d in scope.expand("QMAKE_CXXFLAGS") if d.startswith("-D")]
defines = [
d.replace('=\\\\\\"$$PWD/\\\\\\"', '="${CMAKE_CURRENT_SOURCE_DIR}/"') for d in defines
]
if "qml_debug" in scope.get("CONFIG"):
defines.append("QT_QML_DEBUG")
write_list(cm_fh, defines, cmake_parameter, indent, footer=footer)
def write_include_paths(
cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = ""
):
includes = [i.rstrip("/") or ("/") for i in scope.get_files("INCLUDEPATH")]
write_list(cm_fh, includes, cmake_parameter, indent, footer=footer)
def write_compile_options(
cm_fh: IO[str], scope: Scope, cmake_parameter: str, *, indent: int = 0, footer: str = ""
):
compile_options = [d for d in scope.expand("QMAKE_CXXFLAGS") if not d.startswith("-D")]
write_list(cm_fh, compile_options, cmake_parameter, indent, footer=footer)
def write_library_section(
cm_fh: IO[str], scope: Scope, *, indent: int = 0, known_libraries: Set[str] = set()
):
public_dependencies, private_dependencies = extract_cmake_libraries(
scope, known_libraries=known_libraries
)
write_list(cm_fh, private_dependencies, "LIBRARIES", indent + 1)
write_list(cm_fh, public_dependencies, "PUBLIC_LIBRARIES", indent + 1)
def write_autogen_section(cm_fh: IO[str], scope: Scope, *, indent: int = 0):
forms = scope.get_files("FORMS")
if forms:
write_list(cm_fh, ["uic"], "ENABLE_AUTOGEN_TOOLS", indent)
def write_sources_section(cm_fh: IO[str], scope: Scope, *, indent: int = 0, known_libraries=set()):
ind = spaces(indent)
# mark RESOURCES as visited:
scope.get("RESOURCES")
write_all_source_file_lists(cm_fh, scope, "SOURCES", indent=indent + 1)
write_source_file_list(cm_fh, scope, "DBUS_ADAPTOR_SOURCES", ["DBUS_ADAPTORS"], indent + 1)
dbus_adaptor_flags = scope.expand("QDBUSXML2CPP_ADAPTOR_HEADER_FLAGS")
if dbus_adaptor_flags:
dbus_adaptor_flags_line = '" "'.join(dbus_adaptor_flags)
cm_fh.write(f"{ind} DBUS_ADAPTOR_FLAGS\n")
cm_fh.write(f'{ind} "{dbus_adaptor_flags_line}"\n')
write_source_file_list(cm_fh, scope, "DBUS_INTERFACE_SOURCES", ["DBUS_INTERFACES"], indent + 1)
dbus_interface_flags = scope.expand("QDBUSXML2CPP_INTERFACE_HEADER_FLAGS")
if dbus_interface_flags:
dbus_interface_flags_line = '" "'.join(dbus_interface_flags)
cm_fh.write(f"{ind} DBUS_INTERFACE_FLAGS\n")
cm_fh.write(f'{ind} "{dbus_interface_flags_line}"\n')
write_defines(cm_fh, scope, "DEFINES", indent=indent + 1)
write_include_paths(cm_fh, scope, "INCLUDE_DIRECTORIES", indent=indent + 1)
write_library_section(cm_fh, scope, indent=indent, known_libraries=known_libraries)
write_compile_options(cm_fh, scope, "COMPILE_OPTIONS", indent=indent + 1)
write_autogen_section(cm_fh, scope, indent=indent + 1)
link_options = scope.get("QMAKE_LFLAGS")
if link_options:
cm_fh.write(f"{ind} LINK_OPTIONS\n")
for lo in link_options:
cm_fh.write(f'{ind} "{lo}"\n')
moc_options = scope.get("QMAKE_MOC_OPTIONS")
if moc_options:
cm_fh.write(f"{ind} MOC_OPTIONS\n")
for mo in moc_options:
cm_fh.write(f'{ind} "{mo}"\n')
precompiled_header = scope.get("PRECOMPILED_HEADER")
if precompiled_header:
cm_fh.write(f"{ind} PRECOMPILED_HEADER\n")
for header in precompiled_header:
cm_fh.write(f'{ind} "{header}"\n')
no_pch_sources = scope.get("NO_PCH_SOURCES")
if no_pch_sources:
cm_fh.write(f"{ind} NO_PCH_SOURCES\n")
for source in no_pch_sources:
cm_fh.write(f'{ind} "{source}"\n')
def is_simple_condition(condition: str) -> bool:
return " " not in condition or (condition.startswith("NOT ") and " " not in condition[4:])
def write_ignored_keys(scope: Scope, indent: str) -> str:
result = ""
ignored_keys = scope.keys - scope.visited_keys
for k in sorted(ignored_keys):
if k in {
"_INCLUDED",
"TARGET",
"QMAKE_DOCS",
"QT_SOURCE_TREE",
"QT_BUILD_TREE",
"TRACEPOINT_PROVIDER",
}:
# 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 _iterate_expr_tree(expr, op, matches):
assert expr.func == op
keepers = ()
for arg in expr.args:
if arg in matches:
matches = tuple(x for x in matches if x != arg)
elif arg == op:
(matches, extra_keepers) = _iterate_expr_tree(arg, op, matches)
keepers = (*keepers, *extra_keepers)
else:
keepers = (*keepers, arg)
return matches, keepers
def _simplify_expressions(expr, op, matches, replacement):
for arg in expr.args:
expr = expr.subs(arg, _simplify_expressions(arg, op, matches, replacement))
if expr.func == op:
(to_match, keepers) = tuple(_iterate_expr_tree(expr, op, matches))
if len(to_match) == 0:
# build expression with keepers and replacement:
if keepers:
start = replacement
current_expr = None
last_expr = keepers[-1]
for repl_arg in keepers[:-1]:
current_expr = op(start, repl_arg)
start = current_expr
top_expr = op(start, last_expr)
else:
top_expr = replacement
expr = expr.subs(expr, top_expr)
return expr
def _simplify_flavors_in_condition(base: str, flavors, expr):
""" Simplify conditions based on the knownledge of which flavors
belong to which OS. """
base_expr = simplify_logic(base)
false_expr = simplify_logic("false")
for flavor in flavors:
flavor_expr = simplify_logic(flavor)
expr = _simplify_expressions(expr, And, (base_expr, flavor_expr), flavor_expr)
expr = _simplify_expressions(expr, Or, (base_expr, flavor_expr), base_expr)
expr = _simplify_expressions(expr, And, (Not(base_expr), flavor_expr), false_expr)
return expr
def _simplify_os_families(expr, family_members, other_family_members):
for family in family_members:
for other in other_family_members:
if other in family_members:
continue # skip those in the sub-family
f_expr = simplify_logic(family)
o_expr = simplify_logic(other)
expr = _simplify_expressions(expr, And, (f_expr, Not(o_expr)), f_expr)
expr = _simplify_expressions(expr, And, (Not(f_expr), o_expr), o_expr)
expr = _simplify_expressions(expr, And, (f_expr, o_expr), simplify_logic("false"))
return expr
def _recursive_simplify(expr):
""" Simplify the expression as much as possible based on
domain knowledge. """
input_expr = expr
# Simplify even further, based on domain knowledge:
# windowses = ('WIN32', 'WINRT')
apples = ("APPLE_OSX", "APPLE_UIKIT", "APPLE_IOS", "APPLE_TVOS", "APPLE_WATCHOS")
bsds = ("FREEBSD", "OPENBSD", "NETBSD")
androids = ("ANDROID", "ANDROID_EMBEDDED")
unixes = (
"APPLE",
*apples,
"BSD",
*bsds,
"LINUX",
*androids,
"HAIKU",
"INTEGRITY",
"VXWORKS",
"QNX",
"WASM",
)
unix_expr = simplify_logic("UNIX")
win_expr = simplify_logic("WIN32")
false_expr = simplify_logic("false")
true_expr = simplify_logic("true")
expr = expr.subs(Not(unix_expr), win_expr) # NOT UNIX -> WIN32
expr = expr.subs(Not(win_expr), unix_expr) # NOT WIN32 -> UNIX
# UNIX [OR foo ]OR WIN32 -> ON [OR foo]
expr = _simplify_expressions(expr, Or, (unix_expr, win_expr), true_expr)
# UNIX [AND foo ]AND WIN32 -> OFF [AND foo]
expr = _simplify_expressions(expr, And, (unix_expr, win_expr), false_expr)
expr = _simplify_flavors_in_condition("WIN32", ("WINRT",), expr)
expr = _simplify_flavors_in_condition("APPLE", apples, expr)
expr = _simplify_flavors_in_condition("BSD", bsds, expr)
expr = _simplify_flavors_in_condition("UNIX", unixes, expr)
expr = _simplify_flavors_in_condition("ANDROID", ("ANDROID_EMBEDDED",), expr)
# Simplify families of OSes against other families:
expr = _simplify_os_families(expr, ("WIN32", "WINRT"), unixes)
expr = _simplify_os_families(expr, androids, unixes)
expr = _simplify_os_families(expr, ("BSD", *bsds), unixes)
for family in ("HAIKU", "QNX", "INTEGRITY", "LINUX", "VXWORKS"):
expr = _simplify_os_families(expr, (family,), unixes)
# Now simplify further:
expr = simplify_logic(expr)
while expr != input_expr:
input_expr = expr
expr = _recursive_simplify(expr)
return expr
def simplify_condition(condition: str) -> str:
input_condition = condition.strip()
# Map to sympy syntax:
condition = " " + input_condition + " "
condition = condition.replace("(", " ( ")
condition = condition.replace(")", " ) ")
tmp = ""
while tmp != condition:
tmp = condition
condition = condition.replace(" NOT ", " ~ ")
condition = condition.replace(" AND ", " & ")
condition = condition.replace(" OR ", " | ")
condition = condition.replace(" ON ", " true ")
condition = condition.replace(" OFF ", " false ")
# Replace dashes with a token
condition = condition.replace("-", "_dash_")
# SymPy chokes on expressions that contain two tokens one next to
# the other delimited by a space, which are not an operation.
# So a CMake condition like "TARGET Foo::Bar" fails the whole
# expression simplifying process.
# Turn these conditions into a single token so that SymPy can parse
# the expression, and thus simplify it.
# Do this by replacing and keeping a map of conditions to single
# token symbols.
# Support both target names without double colons, and with double
# colons.
pattern = re.compile(r"(TARGET [a-zA-Z]+(?:::[a-zA-Z]+)?)")
target_symbol_mapping = {}
all_target_conditions = re.findall(pattern, condition)
for target_condition in all_target_conditions:
# Replace spaces and colons with underscores.
target_condition_symbol_name = re.sub("[ :]", "_", target_condition)
target_symbol_mapping[target_condition_symbol_name] = target_condition
condition = re.sub(target_condition, target_condition_symbol_name, condition)
try:
# Generate and simplify condition using sympy:
condition_expr = simplify_logic(condition)
condition = str(_recursive_simplify(condition_expr))
# Restore the target conditions.
for symbol_name in target_symbol_mapping:
condition = re.sub(symbol_name, target_symbol_mapping[symbol_name], condition)
# Map back to CMake syntax:
condition = condition.replace("~", "NOT ")
condition = condition.replace("&", "AND")
condition = condition.replace("|", "OR")
condition = condition.replace("True", "ON")
condition = condition.replace("False", "OFF")
condition = condition.replace("_dash_", "-")
except (SympifyError, TypeError):
# sympy did not like our input, so leave this condition alone:
condition = input_condition
return condition or "ON"
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 "",
)
return condition
resource_file_expansion_counter = 0
def write_resources(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0, is_example=False):
# vpath = scope.expand('VPATH')
# Handle QRC files by turning them into add_qt_resource:
resources = scope.get_files("RESOURCES")
qtquickcompiler_skipped = scope.get_files("QTQUICK_COMPILER_SKIPPED_RESOURCES")
qtquickcompiler_retained = scope.get_files("QTQUICK_COMPILER_RETAINED_RESOURCES")
qrc_output = ""
if resources:
standalone_files: List[str] = []
for r in resources:
skip_qtquick_compiler = r in qtquickcompiler_skipped
retain_qtquick_compiler = r in qtquickcompiler_retained
if r.endswith(".qrc"):
qrc_output += process_qrc_file(
target,
r,
scope.basedir,
scope.file_absolute_path,
skip_qtquick_compiler,
retain_qtquick_compiler,
is_example,
)
else:
immediate_files = {f: "" for f in scope.get_files(f"{r}.files")}
if immediate_files:
immediate_prefix = scope.get(r + ".prefix")
if immediate_prefix:
immediate_prefix = immediate_prefix[0]
else:
immediate_prefix = "/"
immediate_base = scope.get(f"{r}.base")
immediate_lang = None
immediate_name = f"qmake_{r}"
qrc_output += write_add_qt_resource_call(
target,
immediate_name,
immediate_prefix,
immediate_base,
immediate_lang,
immediate_files,
skip_qtquick_compiler,
retain_qtquick_compiler,
is_example,
)
else:
if "*" in r:
global resource_file_expansion_counter
r = r.replace('"', "")
qrc_output += 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()
"""
)
standalone_files.append(
f"${{resource_glob_{resource_file_expansion_counter}}}"
)
resource_file_expansion_counter += 1
else:
# stadalone source file properties need to be set as they
# are parsed.
if skip_qtquick_compiler:
qrc_output += 'set_source_files_properties(f"{r}" PROPERTIES QT_SKIP_QUICKCOMPILER 1)\n\n'
if retain_qtquick_compiler:
qrc_output += 'set_source_files_properties(f"{r}" PROPERTIES QT_RETAIN_QUICKCOMPILER 1)\n\n'
standalone_files.append(r)
if standalone_files:
name = "qmake_immediate"
prefix = "/"
base = None
lang = None
files = {f: "" for f in standalone_files}
skip_qtquick_compiler = False
qrc_output += write_add_qt_resource_call(
target,
name,
prefix,
base,
lang,
files,
skip_qtquick_compiler=False,
retain_qtquick_compiler=False,
is_example=is_example,
)
if qrc_output:
cm_fh.write("\n# Resources:\n")
for line in qrc_output.split("\n"):
cm_fh.write(f"{' ' * indent}{line}\n")
def write_extend_target(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
ind = spaces(indent)
extend_qt_io_string = io.StringIO()
write_sources_section(extend_qt_io_string, scope)
extend_qt_string = extend_qt_io_string.getvalue()
extend_scope = (
f"\n{ind}extend_target({target} CONDITION"
f" {map_to_cmake_condition(scope.total_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)
write_resources(cm_fh, target, scope, indent)
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: Mapping[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",
]
for simd in simd_options:
SIMD = simd.upper()
write_source_file_list(
cm_fh,
scope,
"SOURCES",
[f"{SIMD}_HEADERS", f"{SIMD}_SOURCES", f"{SIMD}_C_SOURCES", f"{SIMD}_ASM"],
indent,
header=f"add_qt_simd_part({target} SIMD {simd}\n",
footer=")\n\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",
]
has_no_values = True
for key in keys:
value = scope.get(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_main_part(
cm_fh: IO[str],
name: str,
typename: str,
cmake_function: str,
scope: Scope,
*,
extra_lines: List[str] = [],
indent: int = 0,
extra_keys: List[str],
**kwargs: Any,
):
# Evaluate total condition of all scopes:
recursive_evaluate_scope(scope)
is_qml_plugin = any("qml_plugin" == s for s in scope.get("_LOADED"))
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)
assert len(scopes)
assert scopes[0].total_condition == "ON"
scopes[0].reset_visited_keys()
for k in extra_keys:
scopes[0].get(k)
# Now write out the scopes:
write_header(cm_fh, name, typename, indent=indent)
# collect all testdata and insert globbing commands
has_test_data = False
if typename == "Test":
test_data = scope.expand("TESTDATA")
if test_data:
has_test_data = True
cm_fh.write("# Collect test data\n")
for data in test_data:
if "*" in data:
cm_fh.write(
dedent(
"""
{spaces(indent)}file(GLOB_RECURSE test_data_glob
{spaces(indent+1)}RELATIVE ${{CMAKE_CURRENT_SOURCE_DIR}}
{spaces(indent+1)}"{}")
"""
)
)
cm_fh.write(f"{spaces(indent)}list(APPEND test_data ${{test_data_glob}})\n")
else:
cm_fh.write(f'{spaces(indent)}list(APPEND test_data "{data}")\n')
cm_fh.write("\n")
# Check for DESTDIR override
destdir = scope.get_string("DESTDIR")
if destdir:
if destdir.startswith("./") or destdir.startswith("../"):
destdir = "${CMAKE_CURRENT_BINARY_DIR}/" + destdir
extra_lines.append(f'OUTPUT_DIRECTORY "{destdir}"')
cm_fh.write(f"{spaces(indent)}{cmake_function}({name}\n")
for extra_line in extra_lines:
cm_fh.write(f"{spaces(indent)} {extra_line}\n")
write_sources_section(cm_fh, scopes[0], indent=indent, **kwargs)
if has_test_data:
cm_fh.write(f"{spaces(indent)} TESTDATA ${{test_data}}\n")
# Footer:
cm_fh.write(f"{spaces(indent)})\n")
write_resources(cm_fh, name, scope, indent)
write_simd_part(cm_fh, name, scope, indent)
write_android_part(cm_fh, name, scopes[0], indent)
if is_qml_plugin:
write_qml_plugin_qml_files(cm_fh, name, scopes[0], indent)
ignored_keys_report = write_ignored_keys(scopes[0], spaces(indent))
if ignored_keys_report:
cm_fh.write(ignored_keys_report)
# Scopes:
if len(scopes) == 1:
return
write_scope_header(cm_fh, indent=indent)
for c in scopes[1:]:
c.reset_visited_keys()
write_android_part(cm_fh, name, c, indent=indent)
write_extend_target(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_module(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
module_name = scope.TARGET
if not module_name.startswith("Qt"):
print(f"XXXXXX Module name {module_name} does not start with Qt!")
extra = []
# A module should be static when 'static' is in CONFIG
# or when option(host_build) is used, as described in qt_module.prf.
is_static = "static" in scope.get("CONFIG") or "host_build" in scope.get("_OPTION")
if is_static:
extra.append("STATIC")
if "internal_module" in scope.get("CONFIG"):
extra.append("INTERNAL_MODULE")
if "no_module_headers" in scope.get("CONFIG"):
extra.append("NO_MODULE_HEADERS")
if "minimal_syncqt" in scope.get("CONFIG"):
extra.append("NO_SYNC_QT")
if "no_private_module" in scope.get("CONFIG"):
extra.append("NO_PRIVATE_MODULE")
if "header_module" in scope.get("CONFIG"):
extra.append("HEADER_MODULE")
module_config = scope.get("MODULE_CONFIG")
if len(module_config):
extra.append(f'QMAKE_MODULE_CONFIG {" ".join(module_config)}')
module_plugin_types = scope.get_files("MODULE_PLUGIN_TYPES")
if module_plugin_types:
extra.append(f"PLUGIN_TYPES {' '.join(module_plugin_types)}")
target_name = module_name[2:]
write_main_part(
cm_fh,
target_name,
"Module",
"add_qt_module",
scope,
extra_lines=extra,
indent=indent,
known_libraries={},
extra_keys=[],
)
if "qt_tracepoints" in scope.get("CONFIG"):
tracepoints = scope.get_files("TRACEPOINT_PROVIDER")
cm_fh.write(
f"\n\n{spaces(indent)}qt_create_tracepoints({module_name[2:]} {' '.join(tracepoints)})\n"
)
return target_name
def write_tool(cm_fh: IO[str], scope: Scope, *, indent: int = 0) -> str:
tool_name = scope.TARGET
extra = ["BOOTSTRAP"] if "force_bootstrap" in scope.get("CONFIG") else []
write_main_part(
cm_fh,
tool_name,
"Tool",
"add_qt_tool",
scope,
indent=indent,
known_libraries={"Qt::Core"},
extra_lines=extra,
extra_keys=["CONFIG"],
)
return tool_name
def write_test(cm_fh: IO[str], scope: Scope, gui: bool = False, *, indent: int = 0) -> str:
test_name = scope.TARGET
assert test_name
extra = ["GUI"] if gui else []
libraries = {"Qt::Core", "Qt::Test"}
if "qmltestcase" in scope.get("CONFIG"):
libraries.add("Qt::QmlTest")
extra.append("QMLTEST")
importpath = scope.expand("IMPORTPATH")
if importpath:
extra.append("QML_IMPORTPATH")
for path in importpath:
extra.append(f' "{path}"')
write_main_part(
cm_fh,
test_name,
"Test",
"add_qt_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) -> None:
binary_name = scope.TARGET
assert binary_name
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 = "add_qt_executable"
if is_qt_test_helper:
binary_name += "_helper"
cmake_function_call = "add_qt_test_helper"
target_path = scope.get_string("target.path")
if target_path:
target_path = target_path.replace("$$[QT_INSTALL_EXAMPLES]", "${INSTALL_EXAMPLESDIR}")
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=["target.path", "INSTALLS"],
)
return binary_name
def write_find_package_section(
cm_fh: IO[str], public_libs: List[str], private_libs: List[str], *, indent: int = 0
):
packages = [] # type: List[LibraryMapping]
all_libs = public_libs + private_libs
for l in all_libs:
info = find_library_info_for_target(l)
if info and info not in packages:
packages.append(info)
# ind = spaces(indent)
for p in packages:
cm_fh.write(generate_find_package_info(p, use_qt_find_package=False, indent=indent))
if packages:
cm_fh.write("\n")
def write_example(cm_fh: IO[str], scope: Scope, gui: bool = False, *, indent: int = 0) -> str:
binary_name = scope.TARGET
assert binary_name
cm_fh.write(
"cmake_minimum_required(VERSION 3.14)\n"
f"project({binary_name} LANGUAGES CXX)\n\n"
"set(CMAKE_INCLUDE_CURRENT_DIR ON)\n\n"
"set(CMAKE_AUTOMOC ON)\n"
"set(CMAKE_AUTORCC ON)\n"
"set(CMAKE_AUTOUIC ON)\n\n"
'set(INSTALL_EXAMPLEDIR "examples")\n\n'
)
(public_libs, private_libs) = extract_cmake_libraries(scope)
write_find_package_section(cm_fh, public_libs, private_libs, indent=indent)
add_executable = f'add_{"qt_gui_" if gui else ""}executable({binary_name}'
write_all_source_file_lists(cm_fh, scope, add_executable, indent=0)
cm_fh.write(")\n")
write_include_paths(
cm_fh, scope, f"target_include_directories({binary_name} PUBLIC", indent=0, footer=")"
)
write_defines(
cm_fh, scope, f"target_compile_definitions({binary_name} PUBLIC", indent=0, footer=")"
)
write_list(
cm_fh,
private_libs,
"",
indent=indent,
header=f"target_link_libraries({binary_name} PRIVATE\n",
footer=")",
)
write_list(
cm_fh,
public_libs,
"",
indent=indent,
header=f"target_link_libraries({binary_name} PUBLIC\n",
footer=")",
)
write_compile_options(
cm_fh, scope, f"target_compile_options({binary_name}", indent=0, footer=")"
)
write_resources(cm_fh, binary_name, scope, indent=indent, is_example=True)
cm_fh.write(
f"\ninstall(TARGETS {binary_name}\n"
' RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"\n'
' BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"\n'
' LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"\n'
")\n"
)
return binary_name
def write_plugin(cm_fh, scope, *, indent: int = 0) -> str:
plugin_name = scope.TARGET
assert plugin_name
extra = []
plugin_type = scope.get_string("PLUGIN_TYPE")
is_qml_plugin = any("qml_plugin" == s for s in scope.get("_LOADED"))
plugin_function_name = "add_qt_plugin"
if plugin_type:
extra.append(f"TYPE {plugin_type}")
elif is_qml_plugin:
plugin_function_name = "add_qml_module"
write_qml_plugin(cm_fh, plugin_name, scope, indent=indent, extra_lines=extra)
plugin_class_name = scope.get_string("PLUGIN_CLASS_NAME")
if plugin_class_name:
extra.append(f"CLASS_NAME {plugin_class_name}")
write_main_part(
cm_fh,
plugin_name,
"Plugin",
plugin_function_name,
scope,
indent=indent,
extra_lines=extra,
known_libraries={},
extra_keys=[],
)
return plugin_name
def write_qml_plugin(
cm_fh: IO[str],
target: str,
scope: Scope,
*,
extra_lines: List[str] = [],
indent: int = 0,
**kwargs: Any,
):
# Collect other args if available
indent += 2
# scope_config = scope.get('CONFIG')
# is_embedding_qml_files = False
sources = scope.get_files("SOURCES")
if len(sources) != 0:
extra_lines.append("CPP_PLUGIN")
target_path = scope.get_string("TARGETPATH")
if target_path:
uri = target_path.replace("/", ".")
import_name = scope.get_string("IMPORT_NAME")
# Catch special cases such as foo.QtQuick.2.bar, which when converted
# into a target path via cmake will result in foo/QtQuick/2/bar, which is
# not what we want. So we supply the target path override.
target_path_from_uri = uri.replace(".", "/")
if target_path != target_path_from_uri:
extra_lines.append(f'TARGET_PATH "{target_path}"')
if import_name:
extra_lines.append(f'URI "{import_name}"')
else:
uri = re.sub("\\.\\d+", "", uri)
extra_lines.append(f'URI "{uri}"')
import_version = scope.get_string("IMPORT_VERSION")
if import_version:
import_version = import_version.replace(
"$$QT_MINOR_VERSION", "${CMAKE_PROJECT_VERSION_MINOR}"
)
extra_lines.append(f'VERSION "{import_version}"')
plugindump_dep = scope.get_string("QML_PLUGINDUMP_DEPENDENCIES")
if plugindump_dep:
extra_lines.append(f'QML_PLUGINDUMP_DEPENDENCIES "{plugindump_dep}"')
def write_qml_plugin_qml_files(cm_fh: IO[str], target: str, scope: Scope, indent: int = 0):
qml_files = scope.get_files("QML_FILES", use_vpath=True)
if qml_files:
# Quote file paths in case there are spaces.
qml_files = [f'"{f}"' for f in qml_files]
qml_files_line = "\n{spaces(indent+1)}".join(qml_files)
cm_fh.write(f"\n{spaces(indent)}set(qml_files\n{spaces(indent+1)}{qml_files_line}\n)\n")
target_path = scope.get_string("TARGETPATH", inherit=True)
target_path_mangled = target_path.replace("/", "_")
target_path_mangled = target_path_mangled.replace(".", "_")
resource_name = "qmake_" + target_path_mangled
cm_fh.write(
f"\n{spaces(indent)}add_qt_resource({target} {resource_name}\n"
f"{spaces(indent+1)}FILES\n{spaces(indent+2)}${{qml_files}}\n)\n"
)
cm_fh.write(f"\nqt_install_qml_files({target}\n FILES ${{qml_files}}\n)\n\n")
def handle_app_or_lib(
scope: Scope, cm_fh: IO[str], *, indent: int = 0, is_example: bool = False
) -> None:
assert scope.TEMPLATE in ("app", "lib")
config = scope.get("CONFIG")
is_lib = scope.TEMPLATE == "lib"
is_qml_plugin = any("qml_plugin" == s for s in scope.get("_LOADED"))
is_plugin = (
any("qt_plugin" == s for s in scope.get("_LOADED")) or is_qml_plugin or "plugin" in config
)
target = ""
if is_plugin:
assert not is_example
target = write_plugin(cm_fh, scope, indent=indent)
elif is_lib or "qt_module" in scope.get("_LOADED"):
assert not is_example
target = write_module(cm_fh, scope, indent=indent)
elif "qt_tool" in scope.get("_LOADED"):
assert not is_example
target = write_tool(cm_fh, scope, indent=indent)
else:
gui = all(
val not in config for val in ["console", "cmdline"]
) and "testlib" not in scope.expand("QT")
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:
if is_example:
target = write_example(cm_fh, scope, gui, indent=indent)
else:
target = write_binary(cm_fh, scope, gui, indent=indent)
# ind = spaces(indent)
write_source_file_list(
cm_fh, scope, "", ["QMAKE_DOCS"], indent, header=f"add_qt_docs({target}\n", footer=")\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
content = dedent(
f"""\
cmake_minimum_required(VERSION {cmake_version_string})
project({qt_lib}
VERSION 6.0.0
DESCRIPTION "Qt {qt_lib_no_prefix} Libraries"
HOMEPAGE_URL "https://qt.io/"
LANGUAGES CXX C
)
find_package(Qt6 ${{PROJECT_VERSION}} CONFIG REQUIRED COMPONENTS BuildInternals Core SET_ME_TO_SOMETHING_USEFUL)
find_package(Qt6 ${{PROJECT_VERSION}} CONFIG OPTIONAL_COMPONENTS SET_ME_TO_SOMETHING_USEFUL)
qt_build_repo()
"""
)
cm_fh.write(f"{content}")
def find_top_level_repo_project_file(project_file_path: str = "") -> Optional[str]:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_dir = os.path.dirname(qmake_conf_path)
# Hope to a programming god that there's only one .pro file at the
# top level directory of repository.
glob_result = glob.glob(os.path.join(qmake_dir, "*.pro"))
if len(glob_result) > 0:
return glob_result[0]
return None
def handle_top_level_repo_tests_project(scope: Scope, cm_fh: IO[str]):
top_level_project_path = find_top_level_repo_project_file(scope.file_absolute_path)
if top_level_project_path:
# qtdeclarative
file_name = os.path.splitext(os.path.basename(top_level_project_path))[0]
# declarative
file_name_without_qt = file_name[2:]
# Qt::Declarative
qt_lib = map_qt_library(file_name_without_qt)
# Found a mapping, adjust name.
if qt_lib != file_name_without_qt:
# QtDeclarative
qt_lib = re.sub(r":", r"", qt_lib) + "Tests"
else:
qt_lib += "Tests_FIXME"
else:
qt_lib = "Tests_FIXME"
content = dedent(
f"""\
if(NOT TARGET Qt::Test)
cmake_minimum_required(VERSION {cmake_version_string})
project({qt_lib} VERSION 6.0.0 LANGUAGES C CXX)
find_package(Qt6 ${{PROJECT_VERSION}} REQUIRED COMPONENTS BuildInternals Core SET_ME_TO_SOMETHING_USEFUL)
find_package(Qt6 ${{PROJECT_VERSION}} OPTIONAL_COMPONENTS SET_ME_TO_SOMETHING_USEFUL)
qt_set_up_standalone_tests_build()
endif()
qt_build_tests()"""
)
cm_fh.write(f"{content}")
def cmakeify_scope(
scope: Scope, cm_fh: IO[str], *, indent: int = 0, is_example: bool = False
) -> None:
template = scope.TEMPLATE
temp_buffer = io.StringIO()
# Handle top level repo project in a special way.
if is_top_level_repo_project(scope.file_absolute_path):
handle_top_level_repo_project(scope, temp_buffer)
# Same for top-level tests.
elif is_top_level_repo_tests_project(scope.file_absolute_path):
handle_top_level_repo_tests_project(scope, temp_buffer)
elif 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"\nqt_examples_build_begin()\n\n{buffer_value}\nqt_examples_build_end()"
cm_fh.write(buffer_value)
def generate_new_cmakelists(scope: Scope, *, is_example: bool = False) -> None:
print("Generating CMakeLists.gen.txt")
with open(scope.generated_cmake_lists_path, "w") as cm_fh:
assert scope.file
cm_fh.write(f"# Generated from {os.path.basename(scope.file)}.\n\n")
is_example_heuristic = is_example_project(scope.file_absolute_path)
final_is_example_decision = is_example or is_example_heuristic
cmakeify_scope(scope, cm_fh, is_example=final_is_example_decision)
def do_include(scope: Scope, *, debug: bool = False) -> None:
for c in scope.children:
do_include(c)
for include_file in scope.get_files("_INCLUDED", is_include=True):
if not include_file:
continue
if not os.path.isfile(include_file):
print(f" XXXX: Failed to include {include_file}.")
continue
include_result = parseProFile(include_file, debug=debug)
include_scope = Scope.FromDict(
None, include_file, include_result.asDict().get("statements"), "", scope.basedir
) # This scope will be merged into scope!
do_include(include_scope)
scope.merge(include_scope)
def copy_generated_file_to_final_location(scope: Scope, keep_temporary_files=False) -> None:
print(f"Copying {scope.generated_cmake_lists_path} to {scope.original_cmake_lists_path}")
copyfile(scope.generated_cmake_lists_path, scope.original_cmake_lists_path)
if not keep_temporary_files:
os.remove(scope.generated_cmake_lists_path)
def should_convert_project(project_file_path: str = "") -> bool:
qmake_conf_path = find_qmake_conf(project_file_path)
qmake_conf_dir_path = os.path.dirname(qmake_conf_path)
project_relative_path = os.path.relpath(project_file_path, qmake_conf_dir_path)
# Skip cmake auto tests, they should not be converted.
if project_relative_path.startswith("tests/auto/cmake"):
return False
# Skip qmake testdata projects.
if project_relative_path.startswith("tests/auto/tools/qmake/testdata"):
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
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):
print(f'Skipping conversion of project: "{project_file_absolute_path}"')
continue
parseresult = parseProFile(file_relative_path, debug=debug_parsing)
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")
)
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")
generate_new_cmakelists(file_scope, is_example=args.is_example)
copy_generated_file = True
if not args.skip_special_case_preservation:
debug_special_case = args.debug_special_case_preservation or args.debug
handler = SpecialCaseHandler(
file_scope.original_cmake_lists_path,
file_scope.generated_cmake_lists_path,
file_scope.basedir,
keep_temporary_files=args.keep_temporary_files,
debug=debug_special_case,
)
copy_generated_file = handler.handle_special_cases()
if copy_generated_file:
copy_generated_file_to_final_location(
file_scope, keep_temporary_files=args.keep_temporary_files
)
os.chdir(backup_current_dir)
if __name__ == "__main__":
main()