makefilegnu-make

How do I benchmark Makefile targets?


Consider the following Makefile:

test:
    @echo "$(shell date) Before test"
    sleep 10
    @echo "$(shell date) After test"

When I run this, the output is the following:

Wed Nov 24 17:00:22 PST 2021 Before test
sleep 10
Wed Nov 24 17:00:22 PST 2021 After test

It looks like make is executing the shell commands before executing the target.

How can I force make to execute the shell commands when they appear in the recipe?


Solution

  • Make recipes are expanded by make prior passing them to the shell. So when make expands your first recipe the recipe becomes:

        @echo "Wed Nov 24 17:00:22 PST 2021 Before test"
        sleep 10
        @echo "Wed Nov 24 17:00:22 PST 2021 After test"
    

    because date is called at almost the same time. It is only after the recipe has been expanded that it is passed to the shell. In this case each line is executed by a different shell. The last one is executed 10 seconds after the others but as the string to echo is already set...

    Make expands the recipes before passing them to the shell for very good reasons. For instance to replace the automatic make variables like $@ (rule's target) or $^ (rule's prerequisites) by their actual values. The shell could not do this.

    To obtain the desired effect, as you realized yourself, you need to let the recipe itself call date, for instance with $(date) or with the old-fashioned backticks. Note that you must escape the $ sign to protect it from the make expansion:

    @echo foobar."$$(date)"
    

    which, after make expansion, becomes:

    @echo foobar."$(date)"
    

    This is why people frequently still use the backticks in make recipes, even if the $(...) syntax is now recommended for command substitution; there is no need to escape them:

    @echo foobar."`date`"
    

    Using the $(shell ...) make function in a recipe is useless. Recipes are already shell scripts. Same with most make functions. The only reason to use them in a recipe is when they are more efficient or simpler than their shell equivalent. But we must remember that they are expanded by make itself, not by the shell, and that make expands them before the recipe is passed to the shell. Example: if you want to print the basename of all prerequisites the notdir make function is convenient:

    @echo $(notdir $^)
    

    instead of:

    @for f in $^; do basename "$$f"; done
    

    But you cannot print the basenames of all C source files in ./project/src with:

    @$(notdir find ./project/src -name '*.c')
    

    because make applies its notdir function to each of its (expanded) word arguments and what it passes to the shell is:

    @find src -name '*.c'
    

    The find command is then executed with a wrong starting directory (src instead of ./project/src) and, even if this wrong starting directory exists, the directory part of the results is not stripped off by notdir which has already been expanded.