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

Unclosed %if (when defined inside %define) #1198

Open
ignatenkobrain opened this issue Apr 27, 2020 · 17 comments
Open

Unclosed %if (when defined inside %define) #1198

ignatenkobrain opened this issue Apr 27, 2020 · 17 comments

Comments

@ignatenkobrain
Copy link
Contributor

Name: hello
Version: 1
Release: 1%{?dist}
Summary: Hello

License: Public Domain
URL: https://google.com

%if 0%{?foo} >= 8

%define test() \
%if 1\
BUG\
%endif\
%{nil}

%endif

%description
%{summary}.

%prep
%autosetup -c -T

%files

Make sure that foo is not defined in your environment.

This produces:

error: line 9: Unclosed %if

This is happening with rpm-build-4.15.90-0.git14971.8.fc33.x86_64.

@pmatilai
Copy link
Member

It also happens in 4.15, but not 4.14.

@ignatenkobrain
Copy link
Contributor Author

ignatenkobrain commented Apr 27, 2020

5f4fdce is the first bad commit

commit 5f4fdce3041b80ef26e3a87db07ecdbd2041a9b2
Author: Pavlina Moravcova Varekova <pmoravco@redhat.com>
Date:   Mon Jul 1 13:06:35 2019 +0200

    Parse multiline conditional and %include parameters correctly (#775)
    
    Trailing '\' after multiline conditionals or %include will be trimmed.
    After the patch lines like:
    
        %if 1 && ( 2 || \
        3 )
        %endif
    
    will be parsed correctly. The other lines stay unchanged.
    A test is added.

 build/parseSpec.c                 | 25 +++++++++++++++++++------
 tests/Makefile.am                 |  1 +
 tests/data/SPECS/ifmultiline.spec | 28 ++++++++++++++++++++++++++++
 tests/rpmmacro.at                 | 12 ++++++++++++
 4 files changed, 60 insertions(+), 6 deletions(-)
 create mode 100644 tests/data/SPECS/ifmultiline.spec

@pmatilai
Copy link
Member

Ack, thanks for chasing that down.

@dmnks
Copy link
Contributor

dmnks commented Jul 17, 2020

This issue stems from the fact that the line continuation marker \ has different semantics in the spec-level context and in a macro definition. On the spec level, it is used to break long %if statements into multiple lines. Inside macro definitions, it's the whole body that's broken down. The patch in 5f4fdce interprets these markers equally, though.

This becomes a problem when a %define or %global macro is encountered in a false branch of an %if statement and is therefore left unexpanded (for obvious reasons); the spec parser just continues scanning the macro's body as if it were part of the spec itself: it joins the inner %if statement into a single line, including the inner %endif (since all of the lines end with a \) and finally tries to match it with a corresponding %endif line, which fails as there's none. For example, the above conditional would be turned into a line: %if 1 BUG %endif

The remedy here would be to completely skip the macro's body in case it's unexpanded. In fact, there's a very simple and elegant (yet non-obvious) way to do this: when joining multi-line %if conditionals, treat %define as a such a conditional; that way, the whole macro (when unexpanded) collapses into a single line and its content is not interpreted. I have a working patch here: dmnks@9c1c592

That being said, it turns out that conditionals inside macros are unsupported. The macro expander does not interpret them at all. For example, the following construct would print "hello":

%define test() \
%if 0 \
%{echo:hello} \
%endif

%test

This is also mentioned (though not entirely clearly) in the /doc/manual/spec file:

%if-conditionals are not macros, and are unlikely to yield expected results
if used in them.

@ignatenkobrain, is there a specific use case for such conditionals that I'm missing? While there is a simple fix for this (as described above) which shouldn't introduce any side-effects (but I am not 100% sure yet), it doesn't make much sense to apply it if the use case being fixed is in fact unsupported.

@dmnks dmnks added this to Needs triage in Ticket Review (Outdated) via automation Jul 17, 2020
@dmnks dmnks moved this from Needs triage to In progress in Ticket Review (Outdated) Jul 17, 2020
@dmnks dmnks added the DONT DO NOT merge, for whatever reason label Jul 21, 2020
@dmnks
Copy link
Contributor

dmnks commented Aug 18, 2020

Having revisited this again, I think I have a better grasp of the whole mechanism now. And it's way simpler than I originally thought.

First of all, there's no such thing as "support for conditionals inside macro definitions". Macros are just that - they may contain arbitrary text to be substituted by them, including other macros which are expanded recursively. Now, since %if is not a macro per se, it won't get expanded into anything and just gets pushed into the parser for further processing (which includes the parsing of conditionals). As a result, spec directives (such as %package), or macros that recursively expand into such spec directives, are a valid construct even inside %ifs inside %defines.

In fact, we ourselves have been using this for years in the %debug_package macro shipped with RPM:

%debug_package \
%ifnarch noarch\
%global __debug_package 1\
%_debuginfo_template\
%{?_debugsource_packages:%_debugsource_template}\
%endif\
%{nil}

Here, %_debuginfo_template expands into a %package debuginfo directive ("preamble" in the spec parser's terms).

That being said, there's a catch. Since conditionals are only evaluated after recursive macro expansion, the %__debug_package macro above will always be set to 1, regardless of whether BuildArch is noarch. I think this was (quite understandably) misunderstood when the macro was written. Another example is the one I gave in the previous comment where "hello" is always printed due to the fact that echo gets expanded (and executed) before the conditional is.

In any case, bringing back the original functionality makes sense after all, and I'll post a PR shortly.

@pmatilai
Copy link
Member

So... I was about to ask how come this then works:

%{?__debug_package:%{__debug_install_post}}\
%{__arch_install_post}\
%{__os_install_post}\
%{nil}

...but the answer is that it doesn't. The debug foobar gets appended to noarch package %install too, contrary to obvious intention of the original author. And okay, this can easily be witnessed by the debug*.list files appearing in the build directory of a noarch package, they're just unused as the %debug_package macro expands to nothing on noarch packages.

Wacko 😅

@dmnks dmnks removed the DONT DO NOT merge, for whatever reason label Aug 18, 2020
dmnks added a commit to dmnks/rpm that referenced this issue Aug 24, 2020
Since the body of a newly defined macro may span multiple lines and
contain %if expressions, we need to make sure the line parser does not
try to interpret those when the corresponding %define or %global macro
appears in a false %if branch and is therefore left unexpanded in the
line buffer.

This is usually not a problem since any macros found in the body itself
would not be expanded anyway, but it can break the syntax check for
conditionals, which follows after expansion.  This is because, with the
recently introduced support for line-continuation markers in %if
expressions (commit 5f4fdce), the parser would be tricked into thinking
that the markers belong to the %if expression itself and collapse them
into a single line, including the matching %endif, and then complain
about the missing %endif (which must be on a separate line).

Instead, similarly to %if, we should collapse the %define/%global macro
itself so that the line parser doesn't pass through the body at all.

A side effect of this change (of commit 5f4fdce from a year ago, in
fact) is that %if constructs are no longer syntax-checked within macros
defined in false %if branches, but that's arguably correct behavior,
since they only come into existence when their definition macro (%define
or %global) is actually expanded, and should merely be treated as text
values until that point.

Fixes: rpm-software-management#1198
dmnks added a commit to dmnks/rpm that referenced this issue Aug 24, 2020
Since the body of a newly defined macro may span multiple lines and
contain %if expressions, we need to make sure the line parser does not
try to interpret those when the corresponding %define or %global macro
appears in a false %if branch and is therefore left unexpanded in the
line buffer.

This is usually not a problem since any macros found in the body itself
would not be expanded anyway, but it can break the syntax check for
conditionals, which follows in readLine() after expansion.  This is
because, with the recently introduced support for line-continuation
markers in %if expressions (commit 5f4fdce), the parser would be tricked
into thinking that the markers belong to the %if expression itself and
collapse them into a single line, including the matching %endif, and
then complain about the missing %endif (which must be on a separate
line).

Instead, similarly to %if, we should collapse the %define/%global macro
itself so that the line parser doesn't pass through the body at all.

A side effect of this change (of commit 5f4fdce from a year ago, in
fact) is that %if constructs are no longer syntax-checked within macros
defined in false %if branches, but that's arguably correct behavior,
since they only come into existence when their definition macro (%define
or %global) is actually expanded, and should merely be treated as text
values until that point.

Fixes: rpm-software-management#1198
dmnks added a commit to dmnks/rpm that referenced this issue Aug 24, 2020
Since the body of a newly defined macro may span multiple lines and
contain %if expressions, we need to make sure the line parser does not
try to interpret those when the corresponding %define or %global macro
appears in a false %if branch and is therefore left unexpanded in the
line buffer.

This is usually not a problem since any macros found in the body itself
would not be expanded anyway, but it can break the syntax check for
conditionals, which follows after expansion.  This is because, with the
recently introduced support for line-continuation markers in %if
expressions (commit 5f4fdce), the parser would be tricked into thinking
that the markers belong to the %if expression itself and collapse them
into a single line, including the matching %endif, and then complain
about the missing %endif (which must be on a separate line).

Instead, similarly to %if, we should collapse the %define/%global macro
itself so that the line parser doesn't pass through the body at all.

A side effect of this change (of commit 5f4fdce from a year ago, in
fact) is that %if constructs are no longer syntax-checked within macros
defined in false %if branches, but that's arguably correct behavior,
since they only come into existence when their definition macro (%define
or %global) is actually expanded, and should merely be treated as text
values until that point.

Fixes: rpm-software-management#1198
dmnks added a commit to dmnks/rpm that referenced this issue Aug 24, 2020
Since the body of a newly defined macro may span multiple lines and
contain %if expressions, we need to make sure the line parser does not
try to interpret those when the corresponding %define or %global macro
appears in a false %if branch and is therefore left unexpanded in the
line buffer.

This is usually not a problem since any macros found in the body itself
would not be expanded anyway, but it can break the syntax check for
conditionals, which follows after expansion.  This is because, with the
recently introduced support for line-continuation markers in %if
expressions (commit 5f4fdce), the parser would be tricked into thinking
that the markers belong to the %if expression itself and collapse them
into a single line, including the matching %endif, and then complain
about the missing %endif (which must be on a separate line).

Instead, similarly to %if, we should collapse the %define/%global macro
itself so that the line parser doesn't pass through the body at all.

A side effect of this change (of commit 5f4fdce from a year ago, in
fact) is that %if constructs are no longer syntax-checked within macros
defined in false %if branches, but that's arguably correct behavior,
since they only come into existence when their definition macro (%define
or %global) is actually expanded, and should merely be treated as text
values until that point.

Fixes: rpm-software-management#1198
@pmatilai
Copy link
Member

pmatilai commented Aug 25, 2020

Having looked at the PR, I think the right solution to this would be requiring %if and its relatives escaped for this kind of usage within the spec, and error out/warn otherwise. How easy (or not) that is to achieve, I don't know.

The thing is, this is only ambiguous within a spec (or an %included snippet). Anywhere else it is just arbitrary text which the macro engine will pass through as-is. So it should only be an error/warning when used from a %define or %global from inside a spec.

@pmatilai
Copy link
Member

Thinking some more, it might not be such a bad idea in general to require escapes for ambiguous constructs. There's all manner of weird behavior in this neighbourhood, for a loosely related example:

%global test() \
%description

%description
%test

This produces an empty description in the package, no warnings, and removing the %description above the %test line produces identical output 🤪

@dmnks
Copy link
Contributor

dmnks commented Aug 25, 2020

Yes, this is really ugly :)

It turns out, though, these %define & %if constructs are not that rare after all:
https://src.fedoraproject.org/rpms/kernel/blob/master/f/kernel.spec#_2778

I wonder how much disruption it would be for such packages if we start requiring proper escaping. Also, this could make spec files look a bit more messy than they already are.

@pmatilai
Copy link
Member

pmatilai commented Aug 25, 2020

Eliminating ambiguity (which is always buggy from somebody's perspective) is usually worth a fair amount of disruption in the end, and messy is in the eye of the beholder.

%define test() \
%if 1\
BUG\
%endif\
%{nil}

It's not that obvious whether the %if is meant for the spec parser inline, or whether it's meant to be part of the macro body.

%define test() \
%%if 1\
BUG\
%%endif\
%{nil}

This is certainly not pretty, but it does at least hint at %if being special here. And in fact it does seem to do the right thing as-is (although just briefly tested) so it seems the only thing needed would be adding a warning about ambiguous %if and friends when not escaped, somehow. I could be missing details here, though.

@dmnks
Copy link
Contributor

dmnks commented Aug 25, 2020

Eliminating ambiguity (which is always buggy from somebody's perspective) is usually worth a fair amount of disruption in the end, and messy is in the eye of the beholder.

%define test() \
%if 1\
BUG\
%endif\
%{nil}

It's not that obvious whether the %if is meant for the spec parser inline, or whether it's meant to be part of the macro body.

Is that a question for the parser or for the person reading the code?

If the former, that's not really the case, as it's unambiguously part of the macro body, due to the line-continuation marker(s) preceding it. If you would "collapse" the %define, the whole body would become one line and the parser would continue past it.

If the latter, totally agree. But I think this is the case with any other language as well, whenever you manually line-break statements, consider e.g. a long if expression in C. It's more about the "code style culture" at that point (probably indenting in this case). For specs, it's not usual to indent stuff, but that probably has to do with insufficient support for that (is that still the case, btw?).

@dmnks
Copy link
Contributor

dmnks commented Aug 25, 2020

Basically the closest example from the C language would be #define. You have to escape line breaks the same way, leading to the same readability issues if done extensively :)

dmnks added a commit to dmnks/rpm that referenced this issue Apr 5, 2021
Since the body of a newly defined macro may span multiple lines and
contain %if expressions, we need to make sure the line parser does not
try to interpret those when the corresponding %define or %global macro
appears in a false %if branch and is therefore left unexpanded in the
line buffer.

This is usually not a problem since any macros found in the body itself
would not be expanded anyway, but it can break the syntax check for
conditionals, which follows after expansion.  This is because, with the
recently introduced support for line-continuation markers in %if
expressions (commit 5f4fdce), the parser would be tricked into thinking
that the markers belong to the %if expression itself and collapse them
into a single line, including the matching %endif, and then complain
about the missing %endif (which must be on a separate line).

Instead, similarly to %if, we should collapse the %define/%global macro
itself so that the line parser doesn't pass through the body at all.

A side effect of this change (of commit 5f4fdce from a year ago, in
fact) is that %if constructs are no longer syntax-checked within macros
defined in false %if branches, but that's arguably correct behavior,
since they only come into existence when their definition macro (%define
or %global) is actually expanded, and should merely be treated as text
values until that point.

Fixes: rpm-software-management#1198
@dmnks dmnks moved this from In progress to Maybe in Ticket Review (Outdated) May 16, 2022
@Ionic
Copy link

Ionic commented Jun 1, 2022

I'm actually looking for a way to use conditionals in a macro definition file.

Do I understand correctly that escaping the conditionals will work with both 4.14 and 4.15 (and even after future changes that add a warning if used unescaped) and produce the intended result?

I currently have something like this in mind:

%bar \
  %%if 0%{!?foo:1} \
    val1 \
  %%else \
    val2 \
  %%endif \
  %{nil}

(And, for that matter, would I need to actually escape 0%%{!?foo:1} in the conditional part as well, given that it's not supposed to be evaluated right away?)

@pmatilai
Copy link
Member

pmatilai commented Jun 1, 2022

I'm actually looking for a way to use conditionals in a macro definition file.

Don't. Or rather, use expressions which are a macro native construct and do not depend on crazy interactions with the spec parser.

@mikhailnov
Copy link
Contributor

mikhailnov commented Jun 1, 2022 via email

@Ionic

This comment was marked as off-topic.

@pmatilai

This comment was marked as off-topic.

@dmnks dmnks removed this from Maybe in Ticket Review (Outdated) Jul 14, 2022
@dmnks dmnks removed their assignment Oct 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Todo
Development

Successfully merging a pull request may close this issue.

5 participants