Skip to content

Commit

Permalink
Implement processing of conditions (#276)
Browse files Browse the repository at this point in the history
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ó
  • Loading branch information
softwarefactory-project-zuul[bot] authored Aug 22, 2023
2 parents 901ca42 + 6c6bdc0 commit 5781f2f
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 14 deletions.
137 changes: 137 additions & 0 deletions specfile/conditions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Copyright Contributors to the Packit project.
# SPDX-License-Identifier: MIT

import re
from typing import TYPE_CHECKING, List, Optional, Tuple

from specfile.exceptions import RPMException
from specfile.macros import Macros

if TYPE_CHECKING:
from specfile.macro_definitions import MacroDefinitions
from specfile.specfile import Specfile


def resolve_expression(
keyword: str, expression: str, context: Optional["Specfile"] = None
) -> bool:
"""
Resolves a RPM expression.
Args:
keyword: Condition keyword, e.g. `%if` or `%ifarch`.
expression: Expression string or a whitespace-delimited list
of arches/OSes in case keyword is a variant of `%ifarch`/`%ifos`.
context: `Specfile` instance that defines the context for macro expansions.
Returns:
Resolved expression as a boolean value.
"""

def expand(s):
if not context:
return Macros.expand(s)
result = context.expand(s, skip_parsing=getattr(expand, "skip_parsing", False))
# parse only once
expand.skip_parsing = True
return result

if keyword in ("%if", "%elif"):
try:
result = expand(f"%{{expr:{expression}}}")
except RPMException:
return False
try:
return int(result) != 0
except ValueError:
return True
elif keyword.endswith("arch"):
target_cpu = expand("%{_target_cpu}")
match = any(t for t in expression.split() if t == target_cpu)
return not match if keyword == "%ifnarch" else match
elif keyword.endswith("os"):
target_os = expand("%{_target_os}")
match = any(t for t in expression.split() if t == target_os)
return not match if keyword == "%ifnos" else match
return False


def process_conditions(
lines: List[str],
macro_definitions: Optional["MacroDefinitions"] = None,
context: Optional["Specfile"] = None,
) -> List[Tuple[str, bool]]:
"""
Processes conditions in a spec file. Takes a list of lines and returns the same
list of lines extended with information about their validity. A line is considered
valid if it doesn't appear in a false branch of any condition.
Args:
lines: List of lines in a spec file.
macro_definitions: Parsed macro definitions to be used to prevent parsing conditions
inside their bodies (and most likely failing).
context: `Specfile` instance that defines the context for macro expansions.
Returns:
List of tuples in the form of (line, validity).
"""
excluded_lines = []
for md in macro_definitions or []:
position = md.get_position(macro_definitions)
excluded_lines.append(range(position, position + len(md.body.splitlines())))
condition_regex = re.compile(
r"""
^
\s* # optional preceding whitespace
(?P<kwd>%((el)?if(n?(arch|os))?|endif|else)) # keyword
\s*
(
\s+
(?P<expr>.*?) # expression
(?P<end>\s*|\\) # optional following whitespace
# or a backslash indicating
# that the expression continues
# on the next line
)?
$
""",
re.VERBOSE,
)
result = []
branches = [True]
indexed_lines = list(enumerate(lines))
while indexed_lines:
index, line = indexed_lines.pop(0)
# ignore conditions inside macro definition body
if any(index in r for r in excluded_lines):
result.append((line, branches[-1]))
continue
m = condition_regex.match(line)
if not m:
result.append((line, branches[-1]))
continue
keyword = m.group("kwd")
if keyword == "%endif":
result.append((line, branches[-2]))
branches.pop()
elif keyword.startswith("%el"):
result.append((line, branches[-2]))
branches[-1] = not branches[-1]
else:
result.append((line, branches[-1]))
expression = m.group("expr")
if expression:
if m.group("end") == "\\":
expression += "\\"
while expression.endswith("\\") and indexed_lines:
_, line = indexed_lines.pop(0)
result.append((line, branches[-1]))
expression = expression[:-1] + line
branch = (
False if not branches[-1] else resolve_expression(keyword, expression)
)
if keyword.startswith("%el"):
branches[-1] = branch
else:
branches.append(branch)
return result
56 changes: 50 additions & 6 deletions specfile/macro_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
import collections
import copy
import re
from typing import List, Optional, Tuple, Union, overload
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, overload

from specfile.conditions import process_conditions
from specfile.formatter import formatted
from specfile.types import SupportsIndex

if TYPE_CHECKING:
from specfile.specfile import Specfile


class MacroDefinition:
def __init__(
Expand All @@ -17,12 +21,14 @@ def __init__(
body: str,
is_global: bool,
whitespace: Tuple[str, str, str, str],
valid: bool = True,
preceding_lines: Optional[List[str]] = None,
) -> None:
self.name = name
self.body = body
self.is_global = is_global
self._whitespace = whitespace
self.valid = valid
self._preceding_lines = (
preceding_lines.copy() if preceding_lines is not None else []
)
Expand All @@ -42,7 +48,7 @@ def __eq__(self, other: object) -> bool:
def __repr__(self) -> str:
return (
f"MacroDefinition({self.name!r}, {self.body!r}, {self.is_global!r}, "
f"{self._whitespace!r}, {self._preceding_lines!r})"
f"{self._whitespace!r}, {self.valid!r}, {self._preceding_lines!r})"
)

def __str__(self) -> str:
Expand Down Expand Up @@ -189,7 +195,9 @@ def find(self, name: str) -> int:
raise ValueError

@classmethod
def parse(cls, lines: List[str]) -> "MacroDefinitions":
def _parse(
cls, lines: Union[List[str], List[Tuple[str, bool]]]
) -> "MacroDefinitions":
"""
Parses given lines into macro defintions.
Expand All @@ -200,6 +208,13 @@ def parse(cls, lines: List[str]) -> "MacroDefinitions":
Constructed instance of `MacroDefinitions` class.
"""

def pop(lines):
line = lines.pop(0)
if isinstance(line, str):
return line, True
else:
return line

def count_brackets(s):
bc = pc = 0
chars = list(s)
Expand Down Expand Up @@ -248,7 +263,7 @@ def count_brackets(s):
buffer: List[str] = []
lines = lines.copy()
while lines:
line = lines.pop(0)
line, valid = pop(lines)
m = md_regex.match(line)
if m:
ws0, macro, ws1, name, ws2, body, ws3 = m.groups()
Expand All @@ -257,7 +272,7 @@ def count_brackets(s):
ws3 = ""
bc, pc = count_brackets(body)
while (bc > 0 or pc > 0 or body.endswith("\\")) and lines:
line = lines.pop(0)
line, _ = pop(lines)
body += "\n" + line
bc, pc = count_brackets(body)
tokens = re.split(r"(\s+)$", body, maxsplit=1)
Expand All @@ -268,14 +283,43 @@ def count_brackets(s):
ws3 = ws + ws3
data.append(
MacroDefinition(
name, body, macro == "%global", (ws0, ws1, ws2, ws3), buffer
name,
body,
macro == "%global",
(ws0, ws1, ws2, ws3),
valid,
buffer,
)
)
buffer = []
else:
buffer.append(line)
return cls(data, buffer)

@classmethod
def parse(
cls,
lines: List[str],
with_conditions: bool = False,
context: Optional["Specfile"] = None,
) -> "MacroDefinitions":
"""
Parses given lines into macro defintions.
Args:
lines: Lines to parse.
with_conditions: Whether to process conditions before parsing and populate
the `valid` attribute.
context: `Specfile` instance that defines the context for macro expansions.
Returns:
Constructed instance of `MacroDefinitions` class.
"""
result = cls._parse(lines)
if not with_conditions:
return result
return cls._parse(process_conditions(lines, result, context))

def get_raw_data(self) -> List[str]:
result = []
for macro_definition in self.data:
Expand Down
35 changes: 32 additions & 3 deletions specfile/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,21 @@ def _get_initial_tag_setup(self, number: int = 0) -> Tuple[int, str, str]:
suffix = f"{number:0{self._default_source_number_digits}}"
return len(self._tags) if self._tags else 0, f"{self.prefix}{suffix}", ": "

def _get_tag_validity(self, reference: Optional[TagSource] = None) -> bool:
"""
Determines validity of a new source tag based on a reference tag, if specified,
or the last tag in the spec file. Defaults to True.
Args:
reference: Optional reference tag source.
Returns:
Whether the new source tag is valid or not.
"""
if reference is not None:
return reference._tag.valid
return self._tags[-1].valid if self._tags else True

def _deduplicate_tag_names(self, start: int = 0) -> None:
"""
Eliminates duplicate numbers in source tag names.
Expand Down Expand Up @@ -505,9 +520,17 @@ def insert(self, i: int, location: str) -> None:
number = source.number
if isinstance(source, self.tag_class):
name, separator = self._get_tag_format(cast(TagSource, source), number)
valid = self._get_tag_validity(cast(TagSource, source))
container.insert(
index,
Tag(name, location, separator, Comments(), context=self._context),
Tag(
name,
location,
separator,
Comments(),
valid,
context=self._context,
),
)
self._deduplicate_tag_names(i)
else:
Expand All @@ -523,9 +546,12 @@ def insert(self, i: int, location: str) -> None:
)
else:
index, name, separator = self._get_initial_tag_setup()
valid = self._get_tag_validity()
self._tags.insert(
index,
Tag(name, location, separator, Comments(), context=self._context),
Tag(
name, location, separator, Comments(), valid, context=self._context
),
)

def insert_numbered(self, number: int, location: str) -> int:
Expand Down Expand Up @@ -555,11 +581,14 @@ def insert_numbered(self, number: int, location: str) -> int:
i += 1
index += 1
name, separator = self._get_tag_format(source, number)
valid = self._get_tag_validity(source)
else:
i = 0
index, name, separator = self._get_initial_tag_setup(number)
valid = self._get_tag_validity()
self._tags.insert(
index, Tag(name, location, separator, Comments(), context=self._context)
index,
Tag(name, location, separator, Comments(), valid, context=self._context),
)
self._deduplicate_tag_names(i)
return i
Expand Down
4 changes: 3 additions & 1 deletion specfile/specfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ def macro_definitions(self) -> Generator[MacroDefinitions, None, None]:
Macro definitions in the spec file as `MacroDefinitions` object.
"""
with self.lines() as lines:
macro_definitions = MacroDefinitions.parse(lines)
macro_definitions = MacroDefinitions.parse(
lines, with_conditions=True, context=self
)
try:
yield macro_definitions
finally:
Expand Down
Loading

0 comments on commit 5781f2f

Please sign in to comment.