Skip to content

Commit 5781f2f

Browse files
Implement processing of conditions (#276)
Implement processing of conditions This is not a full support for conditions, i.e. it doesn't re-solve #231 where an entire section can be conditionalized (however, the new process_conditions() function is generic enough to be used as a base for the resolution of that issue), nevertheless I think it's quite an improvement. Note that macro definitions have to be parsed twice, because a macro definition can contain a condition (see https://src.fedoraproject.org/rpms/kernel/blob/rawhide/f/kernel.spec for examples), and vice versa, a condition can encapsulate a macro definition. Macro definitions and tags gained a valid attribute that can be used to determine if that particular macro definition/tag is valid and can affect other entities in the spec file or if it would be ignored when parsing the spec file with RPM. This will be used to fix Specfile.update_value() and Specfile.update_tag(), but it could be beneficial for other use cases as well. Related to packit/packit#2033. RELEASE NOTES BEGIN Macro definitions and tags gained a new valid attribute. A macro definition/tag is considered valid if it doesn't appear in a false branch of any condition appearing in the spec file. RELEASE NOTES END Reviewed-by: František Lachman <[email protected]> Reviewed-by: Nikola Forró
2 parents 901ca42 + 6c6bdc0 commit 5781f2f

8 files changed

+320
-14
lines changed

specfile/conditions.py

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright Contributors to the Packit project.
2+
# SPDX-License-Identifier: MIT
3+
4+
import re
5+
from typing import TYPE_CHECKING, List, Optional, Tuple
6+
7+
from specfile.exceptions import RPMException
8+
from specfile.macros import Macros
9+
10+
if TYPE_CHECKING:
11+
from specfile.macro_definitions import MacroDefinitions
12+
from specfile.specfile import Specfile
13+
14+
15+
def resolve_expression(
16+
keyword: str, expression: str, context: Optional["Specfile"] = None
17+
) -> bool:
18+
"""
19+
Resolves a RPM expression.
20+
21+
Args:
22+
keyword: Condition keyword, e.g. `%if` or `%ifarch`.
23+
expression: Expression string or a whitespace-delimited list
24+
of arches/OSes in case keyword is a variant of `%ifarch`/`%ifos`.
25+
context: `Specfile` instance that defines the context for macro expansions.
26+
27+
Returns:
28+
Resolved expression as a boolean value.
29+
"""
30+
31+
def expand(s):
32+
if not context:
33+
return Macros.expand(s)
34+
result = context.expand(s, skip_parsing=getattr(expand, "skip_parsing", False))
35+
# parse only once
36+
expand.skip_parsing = True
37+
return result
38+
39+
if keyword in ("%if", "%elif"):
40+
try:
41+
result = expand(f"%{{expr:{expression}}}")
42+
except RPMException:
43+
return False
44+
try:
45+
return int(result) != 0
46+
except ValueError:
47+
return True
48+
elif keyword.endswith("arch"):
49+
target_cpu = expand("%{_target_cpu}")
50+
match = any(t for t in expression.split() if t == target_cpu)
51+
return not match if keyword == "%ifnarch" else match
52+
elif keyword.endswith("os"):
53+
target_os = expand("%{_target_os}")
54+
match = any(t for t in expression.split() if t == target_os)
55+
return not match if keyword == "%ifnos" else match
56+
return False
57+
58+
59+
def process_conditions(
60+
lines: List[str],
61+
macro_definitions: Optional["MacroDefinitions"] = None,
62+
context: Optional["Specfile"] = None,
63+
) -> List[Tuple[str, bool]]:
64+
"""
65+
Processes conditions in a spec file. Takes a list of lines and returns the same
66+
list of lines extended with information about their validity. A line is considered
67+
valid if it doesn't appear in a false branch of any condition.
68+
69+
Args:
70+
lines: List of lines in a spec file.
71+
macro_definitions: Parsed macro definitions to be used to prevent parsing conditions
72+
inside their bodies (and most likely failing).
73+
context: `Specfile` instance that defines the context for macro expansions.
74+
75+
Returns:
76+
List of tuples in the form of (line, validity).
77+
"""
78+
excluded_lines = []
79+
for md in macro_definitions or []:
80+
position = md.get_position(macro_definitions)
81+
excluded_lines.append(range(position, position + len(md.body.splitlines())))
82+
condition_regex = re.compile(
83+
r"""
84+
^
85+
\s* # optional preceding whitespace
86+
(?P<kwd>%((el)?if(n?(arch|os))?|endif|else)) # keyword
87+
\s*
88+
(
89+
\s+
90+
(?P<expr>.*?) # expression
91+
(?P<end>\s*|\\) # optional following whitespace
92+
# or a backslash indicating
93+
# that the expression continues
94+
# on the next line
95+
)?
96+
$
97+
""",
98+
re.VERBOSE,
99+
)
100+
result = []
101+
branches = [True]
102+
indexed_lines = list(enumerate(lines))
103+
while indexed_lines:
104+
index, line = indexed_lines.pop(0)
105+
# ignore conditions inside macro definition body
106+
if any(index in r for r in excluded_lines):
107+
result.append((line, branches[-1]))
108+
continue
109+
m = condition_regex.match(line)
110+
if not m:
111+
result.append((line, branches[-1]))
112+
continue
113+
keyword = m.group("kwd")
114+
if keyword == "%endif":
115+
result.append((line, branches[-2]))
116+
branches.pop()
117+
elif keyword.startswith("%el"):
118+
result.append((line, branches[-2]))
119+
branches[-1] = not branches[-1]
120+
else:
121+
result.append((line, branches[-1]))
122+
expression = m.group("expr")
123+
if expression:
124+
if m.group("end") == "\\":
125+
expression += "\\"
126+
while expression.endswith("\\") and indexed_lines:
127+
_, line = indexed_lines.pop(0)
128+
result.append((line, branches[-1]))
129+
expression = expression[:-1] + line
130+
branch = (
131+
False if not branches[-1] else resolve_expression(keyword, expression)
132+
)
133+
if keyword.startswith("%el"):
134+
branches[-1] = branch
135+
else:
136+
branches.append(branch)
137+
return result

specfile/macro_definitions.py

+50-6
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
import collections
55
import copy
66
import re
7-
from typing import List, Optional, Tuple, Union, overload
7+
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, overload
88

9+
from specfile.conditions import process_conditions
910
from specfile.formatter import formatted
1011
from specfile.types import SupportsIndex
1112

13+
if TYPE_CHECKING:
14+
from specfile.specfile import Specfile
15+
1216

1317
class MacroDefinition:
1418
def __init__(
@@ -17,12 +21,14 @@ def __init__(
1721
body: str,
1822
is_global: bool,
1923
whitespace: Tuple[str, str, str, str],
24+
valid: bool = True,
2025
preceding_lines: Optional[List[str]] = None,
2126
) -> None:
2227
self.name = name
2328
self.body = body
2429
self.is_global = is_global
2530
self._whitespace = whitespace
31+
self.valid = valid
2632
self._preceding_lines = (
2733
preceding_lines.copy() if preceding_lines is not None else []
2834
)
@@ -42,7 +48,7 @@ def __eq__(self, other: object) -> bool:
4248
def __repr__(self) -> str:
4349
return (
4450
f"MacroDefinition({self.name!r}, {self.body!r}, {self.is_global!r}, "
45-
f"{self._whitespace!r}, {self._preceding_lines!r})"
51+
f"{self._whitespace!r}, {self.valid!r}, {self._preceding_lines!r})"
4652
)
4753

4854
def __str__(self) -> str:
@@ -189,7 +195,9 @@ def find(self, name: str) -> int:
189195
raise ValueError
190196

191197
@classmethod
192-
def parse(cls, lines: List[str]) -> "MacroDefinitions":
198+
def _parse(
199+
cls, lines: Union[List[str], List[Tuple[str, bool]]]
200+
) -> "MacroDefinitions":
193201
"""
194202
Parses given lines into macro defintions.
195203
@@ -200,6 +208,13 @@ def parse(cls, lines: List[str]) -> "MacroDefinitions":
200208
Constructed instance of `MacroDefinitions` class.
201209
"""
202210

211+
def pop(lines):
212+
line = lines.pop(0)
213+
if isinstance(line, str):
214+
return line, True
215+
else:
216+
return line
217+
203218
def count_brackets(s):
204219
bc = pc = 0
205220
chars = list(s)
@@ -248,7 +263,7 @@ def count_brackets(s):
248263
buffer: List[str] = []
249264
lines = lines.copy()
250265
while lines:
251-
line = lines.pop(0)
266+
line, valid = pop(lines)
252267
m = md_regex.match(line)
253268
if m:
254269
ws0, macro, ws1, name, ws2, body, ws3 = m.groups()
@@ -257,7 +272,7 @@ def count_brackets(s):
257272
ws3 = ""
258273
bc, pc = count_brackets(body)
259274
while (bc > 0 or pc > 0 or body.endswith("\\")) and lines:
260-
line = lines.pop(0)
275+
line, _ = pop(lines)
261276
body += "\n" + line
262277
bc, pc = count_brackets(body)
263278
tokens = re.split(r"(\s+)$", body, maxsplit=1)
@@ -268,14 +283,43 @@ def count_brackets(s):
268283
ws3 = ws + ws3
269284
data.append(
270285
MacroDefinition(
271-
name, body, macro == "%global", (ws0, ws1, ws2, ws3), buffer
286+
name,
287+
body,
288+
macro == "%global",
289+
(ws0, ws1, ws2, ws3),
290+
valid,
291+
buffer,
272292
)
273293
)
274294
buffer = []
275295
else:
276296
buffer.append(line)
277297
return cls(data, buffer)
278298

299+
@classmethod
300+
def parse(
301+
cls,
302+
lines: List[str],
303+
with_conditions: bool = False,
304+
context: Optional["Specfile"] = None,
305+
) -> "MacroDefinitions":
306+
"""
307+
Parses given lines into macro defintions.
308+
309+
Args:
310+
lines: Lines to parse.
311+
with_conditions: Whether to process conditions before parsing and populate
312+
the `valid` attribute.
313+
context: `Specfile` instance that defines the context for macro expansions.
314+
315+
Returns:
316+
Constructed instance of `MacroDefinitions` class.
317+
"""
318+
result = cls._parse(lines)
319+
if not with_conditions:
320+
return result
321+
return cls._parse(process_conditions(lines, result, context))
322+
279323
def get_raw_data(self) -> List[str]:
280324
result = []
281325
for macro_definition in self.data:

specfile/sources.py

+32-3
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,21 @@ def _get_initial_tag_setup(self, number: int = 0) -> Tuple[int, str, str]:
458458
suffix = f"{number:0{self._default_source_number_digits}}"
459459
return len(self._tags) if self._tags else 0, f"{self.prefix}{suffix}", ": "
460460

461+
def _get_tag_validity(self, reference: Optional[TagSource] = None) -> bool:
462+
"""
463+
Determines validity of a new source tag based on a reference tag, if specified,
464+
or the last tag in the spec file. Defaults to True.
465+
466+
Args:
467+
reference: Optional reference tag source.
468+
469+
Returns:
470+
Whether the new source tag is valid or not.
471+
"""
472+
if reference is not None:
473+
return reference._tag.valid
474+
return self._tags[-1].valid if self._tags else True
475+
461476
def _deduplicate_tag_names(self, start: int = 0) -> None:
462477
"""
463478
Eliminates duplicate numbers in source tag names.
@@ -505,9 +520,17 @@ def insert(self, i: int, location: str) -> None:
505520
number = source.number
506521
if isinstance(source, self.tag_class):
507522
name, separator = self._get_tag_format(cast(TagSource, source), number)
523+
valid = self._get_tag_validity(cast(TagSource, source))
508524
container.insert(
509525
index,
510-
Tag(name, location, separator, Comments(), context=self._context),
526+
Tag(
527+
name,
528+
location,
529+
separator,
530+
Comments(),
531+
valid,
532+
context=self._context,
533+
),
511534
)
512535
self._deduplicate_tag_names(i)
513536
else:
@@ -523,9 +546,12 @@ def insert(self, i: int, location: str) -> None:
523546
)
524547
else:
525548
index, name, separator = self._get_initial_tag_setup()
549+
valid = self._get_tag_validity()
526550
self._tags.insert(
527551
index,
528-
Tag(name, location, separator, Comments(), context=self._context),
552+
Tag(
553+
name, location, separator, Comments(), valid, context=self._context
554+
),
529555
)
530556

531557
def insert_numbered(self, number: int, location: str) -> int:
@@ -555,11 +581,14 @@ def insert_numbered(self, number: int, location: str) -> int:
555581
i += 1
556582
index += 1
557583
name, separator = self._get_tag_format(source, number)
584+
valid = self._get_tag_validity(source)
558585
else:
559586
i = 0
560587
index, name, separator = self._get_initial_tag_setup(number)
588+
valid = self._get_tag_validity()
561589
self._tags.insert(
562-
index, Tag(name, location, separator, Comments(), context=self._context)
590+
index,
591+
Tag(name, location, separator, Comments(), valid, context=self._context),
563592
)
564593
self._deduplicate_tag_names(i)
565594
return i

specfile/specfile.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,9 @@ def macro_definitions(self) -> Generator[MacroDefinitions, None, None]:
223223
Macro definitions in the spec file as `MacroDefinitions` object.
224224
"""
225225
with self.lines() as lines:
226-
macro_definitions = MacroDefinitions.parse(lines)
226+
macro_definitions = MacroDefinitions.parse(
227+
lines, with_conditions=True, context=self
228+
)
227229
try:
228230
yield macro_definitions
229231
finally:

0 commit comments

Comments
 (0)