Search code examples
makefilesubst

makefile subst doesn't use make variable as expected


in a makefile I use subst to assign a value to a make variable, and it uses another make variable in the subst parameters, but it's not really working as expected :

USER_HOME := $(shell echo ~$$SUDO_USER)
EXP_VAR := $(subst $$HOME_PATH,$(USER_HOME),"$$HOME_PATH/project/dir")

all:
    @echo $(USER_HOME)
    @echo $(EXP_VAR)

and the output of make is :

/home/me
~/project/dir

instead of the expected :

/home/me
/home/me/project/dir

so it seems that the variable $(USER_HOME) is correctly assigned the value /home/me, but incorrectly assigned ~ when used in the subst function

I don't see anything explaining a special treatment of make variables here on the presentation of the subst function : https://www.gnu.org/software/make/manual/html_node/Text-Functions.html

I can imagine that the subst function does not use the value of the $(USER_HOME), and instead runs the $(shell echo ~$$SUDO_USER) command again, but certainly there is something that prevents the expansion of the shell command, so it only output the literal string ~$$SUDO_USER and fails to expend $SUDO_USER so it ends up being ~

I can solve the problem with a little more complicated command that i found here https://unix.stackexchange.com/a/247582, but I don't understand why i should need it in the first place :

USER_HOME := $(shell eval echo "~$$SUDO_USER")

in this case, if i understand correctly :

  • makefile expand $$ : USER_HOME := $(shell eval echo "~$SUDO_USER")
  • then echo output a string : ~$SUDO_USER
  • then eval expand the string : /home/me
  • and this output is assigned to the variable $(USER_HOME) and in the subst function

But why the subst function doesn't use the value of the make variable $(USER_HOME) instead of, apparently, expanding the $(shell echo ~$$SUDO_USER) command again ?

And also, but not as important, why the expansion works differently in the subst function ?


Solution

  • You are doing many problematic things which are leading to your confusion.

    First, you should never use @ to hide make's output of your recipe when you are trying to debug makefiles. That's like sending the output of your compiler to /dev/null. This is always Rule #1 for Debugging Makefiles.

    Second, you should always quote make variables when you show them via echo. Otherwise the shell will get at them and manipulate them for you so you can't see what the contents really are. E.g., you should use echo '$(USER_HOME)' not echo $(USER_HOME).

    Third, you should just use built-in make functions like $(info ...) to show the values of variables, not shell commands like echo because between the invocation of the shell and the output generated, you can't tell what the real value was. E.g., you should say $(info USER_HOME is $(USER_HOME)) or similar.

    If you do all the above things, your understanding of what is happening will be very different (note, I'm ignoring the syntax error of the extra close-paren on the second line of the example: I'm assuming that's some kind of cut and paste artifact).

    In fact, this line:

    USER_HOME := $(shell echo ~$$USER)
    

    (assuming the environment variable $USER is set to me) sets the value of USER_HOME to ~me, not /home/me. And this line:

    EXP_VAR := $(subst $$HOME_PATH,$(USER_HOME),"$$HOME_PATH/project/dir")
    

    sets the value of EXP_VAR to "~me/project/dir" (note the double quotes here!! make doesn't know or care about quotes in virtually any situation; they're just another character like a or b as far as make is concerned).

    Now if you look at what make runs in the recipe (be removing the @ before the echo commands), you'll see that it runs:

    echo ~me
    echo "~me/project/dir"
    

    The output of the first line is /home/me, and the output of the second line is ~me/project/dir because the string is quoted the shell doesn't do tilde-expansion on the text.