CMake: pro2cmake.py: Better parsing of scopes with else

Parse conditions more exactly as before, enabling proper handling
of else scopes.

Change-Id: Icb5dcc73010be4833b2d1cbc1396191992df1ee4
Reviewed-by: Albert Astals Cid <albert.astals.cid@kdab.com>
This commit is contained in:
Tobias Hunger 2019-02-11 18:02:22 +01:00
parent b1fa25e7b8
commit 35f23a3dad
7 changed files with 233 additions and 57 deletions

View File

@ -32,6 +32,7 @@ from __future__ import annotations
from argparse import ArgumentParser
import copy
from itertools import chain
import os.path
import re
import io
@ -509,10 +510,12 @@ class QmakeParser:
# Define grammar:
pp.ParserElement.setDefaultWhitespaceChars(' \t')
LC = pp.Suppress(pp.Literal('\\') + pp.LineEnd())
EOL = pp.Suppress(pp.Optional(pp.pythonStyleComment()) + pp.LineEnd())
LC = pp.Suppress(pp.Literal('\\\n'))
EOL = pp.Suppress(pp.Literal('\n'))
Else = pp.Keyword('else')
DefineTest = pp.Keyword('defineTest')
Identifier = pp.Word(pp.alphas + '_', bodyChars=pp.alphanums+'_-./')
Substitution \
= pp.Combine(pp.Literal('$')
+ (((pp.Literal('$') + Identifier
@ -525,32 +528,31 @@ class QmakeParser:
| (pp.Literal('$') + pp.Literal('[') + Identifier
+ pp.Literal(']'))
)))
# Do not match word ending in '\' since that breaks line
# continuation:-/
LiteralValuePart = pp.Word(pp.printables, excludeChars='$#{}()')
SubstitutionValue \
= pp.Combine(pp.OneOrMore(Substitution | LiteralValuePart
| pp.Literal('$')))
Value = (pp.QuotedString(quoteChar='"', escChar='\\')
| SubstitutionValue)
Value = pp.NotAny(Else | pp.Literal('}') | EOL | pp.Literal('\\')) \
+ (pp.QuotedString(quoteChar='"', escChar='\\')
| SubstitutionValue)
Values = pp.ZeroOrMore(Value)('value')
Values = pp.ZeroOrMore(Value + pp.Optional(LC))('value')
Op = pp.Literal('=') | pp.Literal('-=') | pp.Literal('+=') \
| pp.Literal('*=')
Operation = Identifier('key') + Op('operation') + Values('value')
Load = pp.Keyword('load') + pp.Suppress('(') \
+ Identifier('loaded') + pp.Suppress(')')
Include = pp.Keyword('include') + pp.Suppress('(') \
+ pp.CharsNotIn(':{=}#)\n')('included') + pp.Suppress(')')
Option = pp.Keyword('option') + pp.Suppress('(') \
+ Identifier('option') + pp.Suppress(')')
DefineTest = pp.Suppress(pp.Keyword('defineTest')
+ pp.Suppress('(') + Identifier
+ pp.Suppress(')')
+ pp.nestedExpr(opener='{', closer='}')
+ pp.LineEnd()) # ignore the whole thing...
Key = Identifier
Operation = Key('key') + pp.Optional(LC) \
+ Op('operation') + pp.Optional(LC) \
+ Values('value')
CallArgs = pp.nestedExpr()
CallArgs.setParseAction(lambda x: ' '.join(chain(*x)))
Load = pp.Keyword('load') + CallArgs('loaded')
Include = pp.Keyword('include') + CallArgs('included')
Option = pp.Keyword('option') + CallArgs('option')
DefineTestDefinition = pp.Suppress(DefineTest + CallArgs \
+ pp.nestedExpr(opener='{', closer='}')) # ignore the whole thing...
ForLoop = pp.Suppress(pp.Keyword('for') + pp.nestedExpr()
+ pp.nestedExpr(opener='{', closer='}',
ignoreExpr=None)
@ -559,45 +561,54 @@ class QmakeParser:
Scope = pp.Forward()
Statement = pp.Group(Load | Include | Option | DefineTest
| ForLoop | FunctionCall | Operation)
StatementLine = Statement + EOL
StatementGroup = pp.ZeroOrMore(StatementLine | Scope | EOL)
Statement = pp.Group(Load | Include | Option | ForLoop \
| DefineTestDefinition | FunctionCall | Operation)
StatementLine = Statement + (EOL | pp.FollowedBy('}'))
StatementGroup = pp.ZeroOrMore(StatementLine | Scope | pp.Suppress(EOL))
Block = pp.Suppress('{') + pp.Optional(EOL) \
+ pp.ZeroOrMore(EOL | Statement + EOL | Scope) \
+ pp.Optional(Statement) + pp.Optional(EOL) \
+ pp.Suppress('}') + pp.Optional(EOL)
Block = pp.Suppress('{') + pp.Optional(LC | EOL) \
+ StatementGroup + pp.Optional(LC | EOL) \
+ pp.Suppress('}') + pp.Optional(LC | EOL)
Condition = pp.Optional(pp.White()) + pp.CharsNotIn(':{=}#\\\n')
Condition.setParseAction(lambda x: ' '.join(x).strip())
ConditionEnd = pp.FollowedBy((pp.Optional(LC) + (pp.Literal(':') \
| pp.Literal('{') \
| pp.Literal('|'))))
ConditionPart = pp.CharsNotIn('#{}|:=\\\n') + pp.Optional(LC) + ConditionEnd
Condition = pp.Combine(ConditionPart \
+ pp.ZeroOrMore((pp.Literal('|') ^ pp.Literal(':')) \
+ ConditionPart))
Condition.setParseAction(lambda x: ' '.join(x).strip().replace(':', ' && ').strip(' && '))
SingleLineScope = pp.Suppress(pp.Literal(':')) \
+ pp.Group(Scope | Block | StatementLine)('statements')
MultiLineScope = Block('statements')
SingleLineScope = pp.Suppress(pp.Literal(':')) + pp.Optional(LC) \
+ pp.Group(Block | (Statement + EOL))('statements')
MultiLineScope = pp.Optional(LC) + Block('statements')
SingleLineElse = pp.Suppress(pp.Literal(':')) \
+ pp.Group(Scope | StatementLine)('else_statements')
MultiLineElse = pp.Group(Block)('else_statements')
Else = pp.Suppress(pp.Keyword('else')) \
+ (SingleLineElse | MultiLineElse)
Scope <<= pp.Group(Condition('condition')
+ (SingleLineScope | MultiLineScope)
+ pp.Optional(Else))
SingleLineElse = pp.Suppress(pp.Literal(':')) + pp.Optional(LC) \
+ (Scope | Block | (Statement + pp.Optional(EOL)))
MultiLineElse = Block
ElseBranch = pp.Suppress(Else) + (SingleLineElse | MultiLineElse)
Scope <<= pp.Optional(LC) \
+ pp.Group(Condition('condition') \
+ (SingleLineScope | MultiLineScope) \
+ pp.Optional(ElseBranch)('else_statements'))
if debug:
for ename in 'EOL Identifier Substitution SubstitutionValue ' \
'LiteralValuePart Value Values SingleLineScope ' \
'MultiLineScope Scope SingleLineElse ' \
'MultiLineElse Else Condition Block ' \
'StatementGroup Statement Load Include Option ' \
'DefineTest ForLoop FunctionCall Operation'.split():
for ename in 'LC EOL ' \
'Condition ConditionPart ConditionEnd ' \
'Else ElseBranch SingleLineElse MultiLineElse ' \
'SingleLineScope MultiLineScope ' \
'Identifier ' \
'Key Op Values Value ' \
'Scope Block ' \
'StatementGroup StatementLine Statement '\
'Load Include Option DefineTest ForLoop ' \
'FunctionCall CallArgs Operation'.split():
expr = locals()[ename]
expr.setName(ename)
expr.setDebug()
Grammar = StatementGroup('statements')
Grammar.ignore(LC)
Grammar.ignore(pp.pythonStyleComment())
return Grammar
@ -971,8 +982,8 @@ def simplify_condition(condition: str) -> str:
condition = condition.replace(' NOT ', ' ~ ')
condition = condition.replace(' AND ', ' & ')
condition = condition.replace(' OR ', ' | ')
condition = condition.replace(' ON ', 'true')
condition = condition.replace(' OFF ', 'false')
condition = condition.replace(' ON ', ' true ')
condition = condition.replace(' OFF ', ' false ')
try:
# Generate and simplify condition using sympy:
@ -989,9 +1000,7 @@ def simplify_condition(condition: str) -> str:
# sympy did not like our input, so leave this condition alone:
condition = input_condition
if condition == '':
condition = 'ON'
return condition
return condition or 'ON'
def recursive_evaluate_scope(scope: Scope, parent_condition: str = '',

View File

@ -0,0 +1,6 @@
# QtCore can't be compiled with -Wl,-no-undefined because it uses the "environ"
# variable and on FreeBSD and OpenBSD, this variable is in the final executable itself.
# OpenBSD 6.0 will include environ in libc.
freebsd|openbsd: QMAKE_LFLAGS_NOUNDEF =
include(animation/animation.pri)

View File

@ -0,0 +1,4 @@
contains(DEFINES,QT_EVAL):include(eval.pri)
HOST_BINS = $$[QT_HOST_BINS]

View File

@ -0,0 +1,4 @@
A = 42 \
43 \
44
B=23

View File

@ -0,0 +1,17 @@
win32 {
!winrt {
SOURCES +=io/qstandardpaths_win.cpp
} else {
SOURCES +=io/qstandardpaths_winrt.cpp
}
} else:unix {
mac {
OBJECTIVE_SOURCES += io/qstandardpaths_mac.mm
} else:android:!android-embedded {
SOURCES += io/qstandardpaths_android.cpp
} else:haiku {
SOURCES += io/qstandardpaths_haiku.cpp
} else {
SOURCES += io/qstandardpaths_unix.cpp
}
}

View File

@ -37,7 +37,7 @@ _tests_path = os.path.dirname(os.path.abspath(__file__))
def validate_op(key, op, value, to_validate):
assert key == to_validate['key']
assert op == to_validate['operation']
assert value == to_validate['value']
assert value == to_validate.get('value', None)
def validate_single_op(key, op, value, to_validate):
@ -71,10 +71,21 @@ def validate_default_else_test(file_name):
def parse_file(file):
p = QmakeParser(debug=True)
result = p.parseFile(file).asDict()
assert len(result) == 1
result = p.parseFile(file)
return result['statements']
print('\n\n#### Parser result:')
print(result)
print('\n#### End of parser result.\n')
print('\n\n####Parser result dictionary:')
print(result.asDict())
print('\n#### End of parser result dictionary.\n')
result_dictionary = result.asDict()
assert len(result_dictionary) == 1
return result_dictionary['statements']
def test_else():
@ -129,6 +140,13 @@ def test_else8():
validate_default_else_test(_tests_path + '/data/else8.pro')
def test_multiline_assign():
result = parse_file(_tests_path + '/data/multiline_assign.pro')
assert len(result) == 2
validate_op('A', '=', ['42', '43', '44'], result[0])
validate_op('B', '=', ['23'], result[1])
def test_include():
result = parse_file(_tests_path + '/data/include.pro')
assert len(result) == 3
@ -174,3 +192,65 @@ def test_complex_values():
def test_function_if():
result = parse_file(_tests_path + '/data/function_if.pro')
assert len(result) == 1
def test_realworld_standardpaths():
result = parse_file(_tests_path + '/data/standardpaths.pro')
(cond, if_branch, else_branch) = evaluate_condition(result[0])
assert cond == 'win32'
assert len(if_branch) == 1
assert len(else_branch) == 1
# win32:
(cond1, if_branch1, else_branch1) = evaluate_condition(if_branch[0])
assert cond1 == '!winrt'
assert len(if_branch1) == 1
validate_op('SOURCES', '+=', ['io/qstandardpaths_win.cpp'], if_branch1[0])
assert len(else_branch1) == 1
validate_op('SOURCES', '+=', ['io/qstandardpaths_winrt.cpp'], else_branch1[0])
# unix:
(cond2, if_branch2, else_branch2) = evaluate_condition(else_branch[0])
assert cond2 == 'unix'
assert len(if_branch2) == 1
assert len(else_branch2) == 0
# mac / else:
(cond3, if_branch3, else_branch3) = evaluate_condition(if_branch2[0])
assert cond3 == 'mac'
assert len(if_branch3) == 1
validate_op('OBJECTIVE_SOURCES', '+=', ['io/qstandardpaths_mac.mm'], if_branch3[0])
assert len(else_branch3) == 1
# android / else:
(cond4, if_branch4, else_branch4) = evaluate_condition(else_branch3[0])
assert cond4 == 'android && !android-embedded'
assert len(if_branch4) == 1
validate_op('SOURCES', '+=', ['io/qstandardpaths_android.cpp'], if_branch4[0])
assert len(else_branch4) == 1
# haiku / else:
(cond5, if_branch5, else_branch5) = evaluate_condition(else_branch4[0])
assert cond5 == 'haiku'
assert len(if_branch5) == 1
validate_op('SOURCES', '+=', ['io/qstandardpaths_haiku.cpp'], if_branch5[0])
assert len(else_branch5) == 1
validate_op('SOURCES', '+=', ['io/qstandardpaths_unix.cpp'], else_branch5[0])
def test_realworld_comment_scope():
result = parse_file(_tests_path + '/data/comment_scope.pro')
assert len(result) == 2
(cond, if_branch, else_branch) = evaluate_condition(result[0])
assert cond == 'freebsd|openbsd'
assert len(if_branch) == 1
validate_op('QMAKE_LFLAGS_NOUNDEF', '=', None, if_branch[0])
assert result[1].get('included', '') == 'animation/animation.pri'
def test_realworld_contains_scope():
result = parse_file(_tests_path + '/data/contains_scope.pro')
assert len(result) == 2

View File

@ -280,3 +280,59 @@ def test_merge_parent_child_scopes_with_on_child_condition():
assert r0.getString('test1') == 'parent'
assert r0.getString('test2') == 'child'
# Real world examples:
# qstandardpaths selection:
def test_qstandardpaths_scopes():
# top level:
scope1 = _new_scope(condition='ON', scope_id=1)
# win32 {
scope2 = _new_scope(parent_scope=scope1, condition='WIN32')
# !winrt {
# SOURCES += io/qstandardpaths_win.cpp
scope3 = _new_scope(parent_scope=scope2, condition='NOT WINRT',
SOURCES='qsp_win.cpp')
# } else {
# SOURCES += io/qstandardpaths_winrt.cpp
scope4 = _new_scope(parent_scope=scope2, condition='else',
SOURCES='qsp_winrt.cpp')
# }
# else: unix {
scope5 = _new_scope(parent_scope=scope1, condition='else')
scope6 = _new_scope(parent_scope=scope5, condition='UNIX')
# mac {
# OBJECTIVE_SOURCES += io/qstandardpaths_mac.mm
scope7 = _new_scope(parent_scope=scope6, condition='APPLE_OSX', SOURCES='qsp_mac.mm')
# } else:android:!android-embedded {
# SOURCES += io/qstandardpaths_android.cpp
scope8 = _new_scope(parent_scope=scope6, condition='else')
scope9 = _new_scope(parent_scope=scope8,
condition='ANDROID AND NOT ANDROID_EMBEDDED',
SOURCES='qsp_android.cpp')
# } else:haiku {
# SOURCES += io/qstandardpaths_haiku.cpp
scope10 = _new_scope(parent_scope=scope8, condition='else')
scope11 = _new_scope(parent_scope=scope10, condition='HAIKU', SOURCES='qsp_haiku.cpp')
# } else {
# SOURCES +=io/qstandardpaths_unix.cpp
scope12 = _new_scope(parent_scope=scope10, condition='else', SOURCES='qsp_unix.cpp')
# }
# }
recursive_evaluate_scope(scope1)
assert scope1.total_condition == 'ON'
assert scope2.total_condition == 'WIN32'
assert scope3.total_condition == 'WIN32 AND NOT WINRT'
assert scope4.total_condition == 'WINRT'
assert scope5.total_condition == 'UNIX'
assert scope6.total_condition == 'UNIX'
assert scope7.total_condition == 'APPLE_OSX'
assert scope8.total_condition == 'UNIX AND NOT APPLE_OSX'
assert scope9.total_condition == 'ANDROID AND NOT ANDROID_EMBEDDED AND NOT APPLE_OSX'
assert scope10.total_condition == 'UNIX AND NOT APPLE_OSX AND (ANDROID_EMBEDDED OR NOT ANDROID)'
assert scope11.total_condition == 'HAIKU AND UNIX AND NOT APPLE_OSX AND (ANDROID_EMBEDDED OR NOT ANDROID)'
assert scope12.total_condition == 'UNIX AND NOT APPLE_OSX AND NOT HAIKU AND (ANDROID_EMBEDDED OR NOT ANDROID)'