When should a call to *eval* be evaluated in a mak

2019-07-16 16:45发布

问题:

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?

回答1:

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).



回答2:

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)


回答3:

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.