diff --git a/util/cmake/condition_simplifier.py b/util/cmake/condition_simplifier.py new file mode 100644 index 0000000000..c67b78ffad --- /dev/null +++ b/util/cmake/condition_simplifier.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +############################################################################# +## +## Copyright (C) 2019 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the plugins of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:GPL-EXCEPT$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 3 as published by the Free Software +## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + + +import re + +from sympy import simplify_logic, And, Or, Not, SympifyError + + +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 knowledge 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) + + # Do similar token mapping for comparison operators. + pattern = re.compile(r"([a-zA-Z_0-9]+ (?:STRLESS|STREQUAL|STRGREATER) [a-zA-Z_0-9]+)") + comparison_symbol_mapping = {} + all_comparisons = re.findall(pattern, condition) + for comparison in all_comparisons: + # Replace spaces and colons with underscores. + comparison_symbol_name = re.sub("[ ]", "_", comparison) + comparison_symbol_mapping[comparison_symbol_name] = comparison + condition = re.sub(comparison, comparison_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) + + # Restore comparisons. + for comparison in comparison_symbol_mapping: + condition = re.sub(comparison, comparison_symbol_mapping[comparison], 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, AttributeError): + # sympy did not like our input, so leave this condition alone: + condition = input_condition + + return condition or "ON" diff --git a/util/cmake/pro2cmake.py b/util/cmake/pro2cmake.py index a2ea6ce0bf..96d9712bb2 100755 --- a/util/cmake/pro2cmake.py +++ b/util/cmake/pro2cmake.py @@ -39,6 +39,8 @@ import io import glob import collections +from condition_simplifier import simplify_condition + try: collectionsAbc = collections.abc except AttributeError: @@ -53,8 +55,6 @@ from textwrap import indent as textwrap_indent from itertools import chain from functools import lru_cache from shutil import copyfile -from sympy.logic import simplify_logic, And, Or, Not -from sympy.core.sympify import SympifyError from typing import ( List, Optional, @@ -2054,209 +2054,6 @@ def write_ignored_keys(scope: Scope, indent: str) -> str: 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) - - # Do similar token mapping for comparison operators. - pattern = re.compile(r"([a-zA-Z_0-9]+ (?:STRLESS|STREQUAL|STRGREATER) [a-zA-Z_0-9]+)") - comparison_symbol_mapping = {} - all_comparisons = re.findall(pattern, condition) - for comparison in all_comparisons: - # Replace spaces and colons with underscores. - comparison_symbol_name = re.sub("[ ]", "_", comparison) - comparison_symbol_mapping[comparison_symbol_name] = comparison - condition = re.sub(comparison, comparison_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) - - # Restore comparisons. - for comparison in comparison_symbol_mapping: - condition = re.sub(comparison, comparison_symbol_mapping[comparison], 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, AttributeError): - # 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: diff --git a/util/cmake/tests/test_logic_mapping.py b/util/cmake/tests/test_logic_mapping.py index c477aa8351..c18c3ddc65 100755 --- a/util/cmake/tests/test_logic_mapping.py +++ b/util/cmake/tests/test_logic_mapping.py @@ -27,7 +27,7 @@ ## ############################################################################# -from pro2cmake import simplify_condition +from condition_simplifier import simplify_condition def validate_simplify(input: str, expected: str) -> None: