2

Inside of a makefile, I'm trying to check if fileA was modified more recently than fileB. Using a few prior posts (this and this) as references, I've come up with this as an attempt to store the time since last file modification as a variable:

(I'd rather this be in a function outside of a make recipe, but one step at a time.)

    .PHONY: all clean

    all      : (stuff happens here)

    radio    :
              BASE_MOD_TIME="$( expr $(date +%s) - $(date +%s -r src/radio_interface/profile_init.c) )"
              @echo "$(BASE_MOD_TIME)"

I thought that I would be assigning the output of the expr command to a variable, BASE_MOD_TIME, but the output is:

    bash-4.1$
    BASE_MOD_TIME=""
    echo ""

What am I doing wrong here? Simple attempts to save the output of ls -l also didn't work like this.

Community
  • 1
  • 1
Allen
  • 162
  • 2
  • 3
  • 15

3 Answers3

8

Make variables are normally global, and you don't normally set make variables in a recipe. A recipe is simply a list of commands to be executed by a shell, and so what looks like a variable assignment in a recipe is a shell variable assignment.

However, each line in a make recipe is run in its own shell subprocess. So a variable set in one line won't be visible in another line; they are not persistent. That makes setting shell variables in recipes less useful. [Note 1]

But you can combine multiple lines of a recipe into a single shell command using the backslash escape at the end of the line, and remembering to terminate the individual commands with semicolons (or, better, link them with &&), because the backslash-escaped newline will not be passed to the shell. Also, don't forget to escape the $ characters so they will be passed to the shell, rather than being interpreted by make.

So you could do the following:

radio:
    @BASE_MOD_TIME="$$( expr $$(date +%s) - $$(date +%s -r src/radio_interface/profile_init.c) )"; \
    echo "$$BASE_MOD_TIME"; \
    # More commands in the same subprocess

But that gets quite awkward if there are more than a couple of commands, and a better solution is usually to write a shell script and invoke it from the recipe (although that means that the Makefile is no longer self-contained.)

Gnu make provides two ways to set make variables in a recipe:

1. Target-specific variables.

You can create a target-specific variable (which is not exactly local to the target) by adding a line like:

target: var := value

To set the variable from a shell command, use the shell function:

target: var := $(shell ....)

This variable will be available in the target recipe and all dependencies triggered by the target. Note that a dependency is only evaluated once, so if it could be triggered by a different target, the target-specific variable might or might not be available in the dependency, depending on the order in which make resolves dependencies.

2. Using the eval function

Since the expansion of recipes is always deferred, you can use the eval function inside a recipe to defer the assignment of a make variable. The eval function can be placed pretty well anywhere in a recipe because its value is the empty string. However, evaling a variable assignment makes the variable assignment global; it will be visible throughout the makefile, but its value in other recipes will depend, again, on the order in which make evaluates recipes, which is not necessarily predictable.

For example:

radio:
    $(eval var = $(shell ...))

Notes:

  1. You can change this behaviour using the .ONESHELL: pseudo-target, but that will apply to the entire Makefile; there is no way to mark a single recipe as being executed in a single subprocess. Since changing the behaviour can break recipes in unexpected ways, I don't usually recommend this feature.
Community
  • 1
  • 1
rici
  • 234,347
  • 28
  • 237
  • 341
  • "You can't set a make variable in a recipe": actually you can with something like `$(eval FOO := bar)`. It has to begin with a tab though, otherwise it will be set at parsing time. Also there's `.ONESHELL`. – Andrea Biondo Sep 14 '16 at 00:03
  • @rici, I tried your solution here but was still unable to capture the output of the `expr` command for whatever reason. After trying a few things to fix it, I settled on a separate `bash` script that I invoke in the `make` recipe. I posted more details in an answer. Thanks for your help! – Allen Sep 14 '16 at 00:34
  • 1
    @Allen: yes, the bash script is probably the best solution. I should have suggested it. You can certainly capture the output in a shell variable but as I said the variable is local to the command, not the recipe. So the entire recipe would need to be a single command-line, which requires backslash-escaping all tje line endings and semicolon-terminating all the commands. I probably should have been clearer. – rici Sep 14 '16 at 02:38
  • @andrea: I'm always reluctant to suggest `.ONESHELL` because its effect is global, which csn wreak havoc. – rici Sep 14 '16 at 02:41
1

What's wrong with this?

fileB: fileA
    @echo $< was modified more recently than $@
Beta
  • 96,650
  • 16
  • 149
  • 150
  • This seems to just be echoing that fileB was modified more recently. I was looking to test if that was the case, then do stuff if so. – Allen Sep 14 '16 at 00:37
  • Make already does that; it's what it does. A dependency on the file whose modification time you are interested in is all it takes. – tripleee Sep 14 '16 at 17:39
  • @tripleee: the files of interest were python files, so they do not get compiled. – Allen Sep 21 '16 at 22:18
  • I don't see how that's pertinent. If you have a recipe with a dependency `A: B` then it will execute if B is newer than A, or if A is missing, when you `make A`, and otherwise, Make will conclude that nothing needs to be done. If this is not what you want, your question is unclear. – tripleee Sep 22 '16 at 03:10
  • Just to be painfully clear, you'd replace the `echo` with the commands you actually want to execute. – tripleee Sep 22 '16 at 06:35
0

Instead of forcing the makefile to do all of the heavy lifting via some bash commands, I just called a separate bash script. This provided a lot more clarity for a newbie to bash scripting like myself since I didn't have to worry about escaping the current shell being used by make.

Final solution:

    .PHONY: all clean

    all      : (stuff happens here)

    radio    :
            ./radio_init_check.sh
            $(MKDIR_P) $(OBJDIR)
            make $(radio_10)

with radio_init_check.sh being my sew script.

Allen
  • 162
  • 2
  • 3
  • 15