6

I have a few software projects which are distributed as RPMs. They are versioned using semantic versioning to which we affix a release number. Using the regular conventions, this is MAJOR.MINOR.PATCH-REL_NUM. Though beyond the scope of this article, the release numbers are stored in git. The release target in the makefile looks something like this:

release:
    make clean
    $(BLD_ROOT)/tools/incr_rel_num
# Although the third step, this was re-ordered to step 1
    $(eval RELEASE_NUMBER=$(shell cat $(BLD_ROOT)/path/to/rel_num.txt))
    make rpm RPM_RELEASE_NUM=$(RELEASE_NUMBER)

While debugging, I eventually discovered that, although the call to eval was the third step in the recipe, it was actually being evaluated first! This is why the RPM always had a release number one less than the number I was watching get pushed to the remote.

I have done much googling on this and I haven't found any hits that explain the order of evaluation with regard to eval when used in recipes. Perhaps it isn't even with respect to eval but functions in general. Furthermore, I haven't found verbiage on this in the GNU manuals for make either (if it's there, kindly point out what chapter). I've worked around the problem so it's not a bother, I'm just wondering, is this expected and if so, why?

Andrew Falanga
  • 2,274
  • 4
  • 26
  • 51
  • Use double $ if you want the variables to be evaluated during eval – Tim Jul 06 '16 at 15:44
  • One other comment -- eval is typically dangerous, as it opens security holes. Your example (if you had the `cat` in your `$(shell ...` command), opens up a security hole where a hacker could gain access to your system by modifying rel_num.txt. – John Jul 06 '16 at 20:17

3 Answers3

10

The missing bit, that no one above is getting, is simple: when make is going to run a recipe it expands all lines of the recipe first, before it starts the first line. So:

release:
        make clean
        $(BLD_ROOT)/tools/incr_rel_num
# Although the third step, this was re-ordered to step 1
        $(eval RELEASE_NUMBER=$(shell $(BLD_ROOT)/path/to/rel_num.txt))
        make rpm RPM_RELEASE_NUM=$(RELEASE_NUMBER)

when make decides to run the release target it first expands all the lines in the recipe, which means the eval is expanded, then it runs the resulting lines. That's why you're getting the behavior you're seeing.

I don't really see why you need to use eval here at all; why not just use:

release:
        $(MAKE) clean
        $(BLD_ROOT)/tools/incr_rel_num
        $(MAKE) rpm RPM_RELEASE_NUM="$$(cat $(BLD_ROOT)/path/to/rel_num.txt))"

(BTW, you should never use bare make inside your makefiles; you should always use $(MAKE) (or ${MAKE}, same thing).

MadScientist
  • 92,819
  • 9
  • 109
  • 136
  • Accepting this answer as recommended by Mike (who's answer I originally accepted). Thank you for the further explanation. As I commented to Mike, the use of `$(eval ...)` was due to my search for how to define a variable within a recipe rather than "globally." I actually got the idea from another stackoverflow article. Though, apparently, I can't be sure that person's approach was valid. I will be retooling my makefiles because this approach, sans `$(eval ...)` is much more concise. – Andrew Falanga Jul 06 '16 at 21:25
  • The only reason to use make's `eval` function is if you want something to be known by make itself. And the only real reason to use `eval` within a recipe is if you want to set a variable in the recipe for one target, then use that value in the recipe for a completely different target. This is highly unusual. Most of the time, as you have it here, you just need the value available within the same recipe. Since each recipe is run by the shell, it's easy to use the _shell_'s programming features to do this; you don't need make's features. – MadScientist Jul 06 '16 at 23:01
  • Yes, I can see that I was making this much more complicated than necessary. – Andrew Falanga Jul 07 '16 at 14:13
4

The $(eval ...) function generates a fragment of make-sytax which becomes part of the parsed makefile. The makefile is parsed entirely before any recipes are executed and when recipes are executed all make-statements, make-expressions and make-variables have been evaluated away.

So it does not make sense to consider an $(eval ...) call as being one of the lines of a recipe. It might generate values that are used in the make-expansion of the recipe, but if so then this happens when the makefile is parsed, before the recipe is run.

Thus in your example, the line:

$(eval RELEASE_NUMBER=$(shell $(BLD_ROOT)/path/to/rel_num.txt))

which I assume should really be:

$(eval RELEASE_NUMBER=$(shell cat $(BLD_ROOT)/path/to/rel_num.txt))

is evaluated when the makefile is parsed, and let's say it results in the make-variable RELEASE_NUMBER acquiring the value 1.0, because, when the makefile is parsed, the file $(BLD_ROOT)/path/to/rel_num.txt) contains 1.0. In that case your recipe:

release:
    make clean
    $(BLD_ROOT)/tools/incr_rel_num
    $(eval RELEASE_NUMBER=$(shell cat $(BLD_ROOT)/path/to/rel_num.txt))
    make rpm RPM_RELEASE_NUM=$(RELEASE_NUMBER)

will resolve to the like of:

release:
    make clean
    some_build_dir/tools/incr_rel_num
    make rpm RPM_RELEASE_NUM=1.0

You will observe when make runs the recipe that it prints no line that is "the expansion of" $(eval RELEASE_NUMBER=$(shell cat $(BLD_ROOT)/path/to/rel_num.txt)), because there is no such thing in the recipe. It doesn't matter that:

some_build_dir/tools/incr_rel_num

is presumably a command that writes, say, 1.1 or 2.0 in the file some_build_dir/path/to/rel_num.txt. That action simply has no effect on the recipe. Nothing that executed in the recipe can change the recipe.

$(eval ...) has no business in your recipe. What you want to achieve is simply:

release:
    make clean
    $(BLD_ROOT)/tools/incr_rel_num
    RELEASE_NUMBER=$$(cat $(BLD_ROOT)/path/to/rel_num.txt) && \
    make rpm RPM_RELEASE_NUM=$$RELEASE_NUMBER

where $$ is what you do in a makefile to escape $ and, in this case, leave it for the shell when the recipe is executed.

This recipe expands to 3 shell commands executed in sequence:

$ make clean
$ some_build_dir/tools/incr_rel_num
$ RELEASE_NUMBER=$(cat some_build_dir/path/to/rel_num.txt) && \
make rpm RPM_RELEASE_NUM=$RELEASE_NUMBER

and might as well be simplified further to:

release:
    make clean
    $(BLD_ROOT)/tools/incr_rel_num
    make rpm RPM_RELEASE_NUM=$$(cat $(BLD_ROOT)/path/to/rel_num.txt)
Mike Kinghan
  • 55,740
  • 12
  • 153
  • 182
  • I was about to reply along the same lines, but I just tried it, and if you do an `$(eval X=Y)` in a recipe, then all references to `$X` _after the recipe is executed_ expand to `Y` (even if they're inside of a separate target). To boot, it seems to set the makefile variable `X` as opposed to the shell variable. I'm having trouble finding any references to this behavior though... I think I'll open a new question on this. – John Jul 06 '16 at 19:47
  • Thank you for the detailed explanation. I was using `$(eval ...)` because I found a stackoverflow article in which an answer provided for defining variables inside the recipe was `$(eval ...)`. LOL, I didn't understand what was happening. Your solution is certainly easier and more concise. – Andrew Falanga Jul 06 '16 at 19:59
  • 1
    Your statement _this happens when the makefile is parsed, before the recipe is run_ is not true. `eval` is like any other make function or variable and obeys the same rules for when it's expanded as any other variable or function: if it appears in a recipe context it will be expanded if, and only if, that recipe is invoked. – MadScientist Jul 06 '16 at 20:22
  • @AndrewFalanga MadScientist's own answer is right. Suggest you un-accept mine and accept his. – Mike Kinghan Jul 06 '16 at 21:13
  • Warning: the proposed solution doesn't work, as make by default sets up a subshell for each line of the recipe, and the RELEASE_NUMBER variable created immediately evaporates as soon as the line is done. – Daniel Griscom Dec 11 '18 at 18:25
1

You are correct, there are multiple levels of evaluation. The content on what is inside eval is evaluated a first time before that the function is actually called. If you want the content of eval to be evaluated at the time eval is called, you have to escape the $ sign by putting it twice, like this :

$(eval RELEASE_NUMBER=$$(shell $(BLD_ROOT)/path/to/rel_num.txt))

To view what is really inside eval at the time it's called you can use the same syntax with info instead of eval :

$(info RELEASE_NUMBER=$$(shell $(BLD_ROOT)/path/to/rel_num.txt))

Now I'm not sure about the part which is evaluated too soon so the $ symbols that I doubled may not be the good one(s), but using the info function will help you to find the correct command.

Tim
  • 1,853
  • 2
  • 24
  • 36