Home > Software engineering >  DRY out conditional logic across tasks in Makefile AKA export global variable from within make-task
DRY out conditional logic across tasks in Makefile AKA export global variable from within make-task

Time:08-17

I've searched and Googled a lot, but can't find an answer. If this is not possible then just let me know, and if you can give me more context about "why" I'd appreciate it.

I inherited a makefile which is making the same check in many different make-tasks, but tries to DRY out the logic by moving the body of the check itself into a single upstream make-task, that the other tasks depend on. Basically this:

DEV_FILE_PRESENT:=false

setup:
    @if [ -f ./dev_file.json ]; then \
        DEV_FILE_PRESENT=true; \
        echo "found dev json file"; \
    fi;

check-overwrite: setup
    @if [ DEV_FILE_PRESENT ]; then \
        echo "Existing dev files- overwrite? [Y/n]" && read ans && [ $${ans:-Y} = Y ]; \
    fi;

preview: setup
    @if [ ! DEV_FILE_PRESENT ]; then \
        echo "Can't preview- missing dev file"; \
    else \
        echo "placeholder for moar bash scripting here"; \
    fi;

This doesn't work: The global variable DEV_FILE_PRESENT seems to revert to "false" as soon as the setup task is complete and has been exited. I think that's because "every sub-command is run in its own shell" (From this StackOverflow answer: https://stackoverflow.com/a/29085684/1358187)

Much googling and searching later I settled on this "fix" - removing any attempt to DRY the body of the if-clause itself, and instead duplicating it across tasks:

check-overwrite:
    @if [ -f ./dev_file.json ]; then \
        echo "Existing dev files- overwrite? [Y/n]" && read ans && [ $${ans:-Y} = Y ]; \
    fi;

preview:
    @if [ ! -f ./dev_file.json ]; then \
        echo "Can't preview- missing dev file"; \
    else \
        echo "placeholder for moar bash scripting here"; \
    fi;

Are there any other solutions here? Is there any way to set a variable in one make-task, and reference it in another make-task?

CodePudding user response:

Issues with the original code

There are several problems here:

  • You are creating a bash variable, which, exactly as you say, lives only in the shell when you define it.
  • When you use the variable, you just use DEV_FILE_PRESENT: that's a constant string, not a reference to that variable.

One possible solution

Your solution is fine, another solution would be to use a make variable instead of a bash variable.

Here's a working example:

DEV_FILE_NAME=./dev_file.json
ifeq ($(wildcard $(DEV_FILE_NAME)), $(DEV_FILE_NAME))
    DEV_FILE_PRESENT=yes
    $(info Found dev json file)
else
    DEV_FILE_PRESENT=no
endif

$(info DEV_FILE_PRESENT $(DEV_FILE_PRESENT))

check-overwrite:
        @if [ "$(DEV_FILE_PRESENT)" = yes ]; then \
                echo "Existing dev files- overwrite? [Y/n]" && read ans && [ $${ans:-Y} = Y ]; \
        fi;

preview:
        @if [ "$(DEV_FILE_PRESENT)" = no ]; then \
                echo "Can't preview- missing dev file"; \
        else \
                echo "placeholder for more bash scripting here"; \
        fi;

Notes:

  • Tested with GNU Make 3.81
  • The builtin $(wildcard <arg>) function in make is a bit different from regular wildcard expansion: here I'm taking advantage of the fact that when a pattern does not match any existing file, the wildcard is "expanded" to the empty string, even if there are no wildcard characters in the expression.
  • Using a make variable requires the syntax $(VAR_NAME)
  • Unless you place SHELL=/bin/bash in your Makefile, that's all sh scripting in there, not Bash scripting. It won't always behave like you expect!
  • The $(info DEV_FILE_PRESENT ...) line is just for debugging.

Now, because of where I'm defining DEV_FILE_PRESENT, the test is always going to run when the Makefile is loaded, whether you are asking for those targets to be run or not.

My personal style preference

Frankly, I actually like your own solution better: it's much simpler, and much easier to read. If you were using an expensive test, factoring it out might be worth the code complexity, but for such a trivial test repeating yourself makes more sense to me.

I'd only change one thing: use a variable to hold the name of the dev file, so that if that name changes in the future, you'll only have to make the change in one place:

DEV_FILE_NAME=./dev_file.json

check-overwrite:
    @if [ -f "$(DEV_FILE_NAME)" ]; then \
        echo "Existing dev files- overwrite? [Y/n]" && read ans && [ $${ans:-Y} = Y ]; \
    fi;

preview:
    @if [ ! -f "$(DEV_FILE_NAME)" ]; then \
        echo "Can't preview- missing dev file"; \
    else \
        echo "placeholder for moar bash scripting here"; \
    fi;
  • Related