Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement processing of conditions #276

Merged
merged 3 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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