Passing a shell variable from a sub-make - makefile

I am trying to pass the shell variable $$s from the sub recipe into the main one but after many attempts (tried exporting them), I still cant manage to get it. Is there any way to achieve that?
This is what I want to do:
.ONESHELL:
sub-recipe:
#read s;
#echo "$$s in sub-recipe";
main-recipe: sub-recipe
#echo "$$s in main-recipe"

There is no way to directly communicate shell variables between recipes in GNU Make. You can however have the recipe save the variable into a file and let other recipes depend on that file:
ONESHELL:
sub-recipe.var:
read s
echo $$s > $#
main-recipe: sub-recipe.var
cat sub-recipe.var
echo " in main-recipe"

Related

In makefile, how to store multi-line shell output in variable

I have a shell command where it outputs multiple lines. I want to store it in a variable in makefile for later processing in the target.
A simplified example:
I have this file called zfile1
#zfile1
some text
$echo 123
more text
$$$#&^$
more text
The makefile:
a:
#$(eval v1 = $(shell cat zfile1))
# need to process the variable here, example:
#echo "$(v1)"
# I want to prevent expansion of values in the file and print in multi-line
If you have GNU make 4.2 or above you can use the $(file <zfile1) function. See https://www.gnu.org/software/make/manual/html_node/File-Function.html
If you don't have a new-enough version of GNU make, you can't do it. Of course in your example there's no real need to put the contents of the file into a make variable at all: you can just have your recipe use the file itself. But maybe your real use-case isn't so simple.
ETA
You should never use either the make function eval or the make function shell in a recipe [1].
You can just write:
v1 := $(file <zfile1)
.ONESHELL:
a:
#echo "$(v1)"
You must have the .ONESHELL because otherwise each line of the recipe (after it expands into multiple lines) is considered a separate recipe line. Note that .ONESHELL is in effect for the entire make process so could cause other recipes to break if they rely on each line being invoked in a different shell.
Another option is to export the result into the environment, and use a shell variable like this:
export v1 := $(file <zfile1)
a:
#echo "$$v1"
There are probably better ways to do it but since you've only given us this example and not what you really want to do, that's about all we can say.
[1] There are times where it can be useful but if you have a sufficiently sophisticated requirement to need this you'll probably know how to use it.
I think you're making things too complicated.
Start by writing your recipes as proper self-contained shell scripts.
You can then either store the whole script in a file and run it from make, or you can include it directly in your makefile as a single logical line, as in the following:
a:
#v1=$$(< zfile1); \
echo $$v1
Note the need to "escape" the dollar sign by repeating it.
You could also use global make variables, depending on the actual logic of your real-world use.

How to perform a string replacement on a path in a Makefile?

I am trying to remove the path prefix. Here is a small example showing just the issue.
Makefile
dist_directory = ./dist
default: build
build: $(patsubst %.md, $(dist_directory)/%.html, $(wildcard *.md))
$(dist_directory)/%.html: %.md
#echo start
#echo $#
#echo ${$#//$(dist_directory)/}
#echo end
Create a file: touch stuff.md
Then build: make
The output is:
start
dist/stuff.html
end
The expected output is:
start
dist/stuff.html
/stuff.html
end
There are similar posts on Stack Exchange. However, they have not worked for me in a Makefile for some reason. I'm probably doing something wrong.
https://unix.stackexchange.com/questions/311758/remove-specific-word-in-variable
Remove a fixed prefix/suffix from a string in Bash
Remove substring matching pattern both in the beginning and the end of the variable
You have many issues here. The most fundamental one is that if you want to use shell variables you have to escape the dollar sign so that make doesn't interpret it. And, you can only use shell variable substitutions on shell variables, while $# is a make variable, so you need:
#foo='$#' ; echo $${foo//$(dist_directory)/}
The more subtle one is that make always uses /bin/sh (POSIX standard shell) when it invokes recipes, and the above syntax is specific to bash. One way around that would be to explicitly set SHELL := /bin/bash in your makefile to force make to use bash. Luckily that is not necessary because POSIX sh can also do this, as mentioned by Reda in another answer:
#foo='$#' ; echo $${###*/}
But even more, you don't need any of this because make sets the automatic variable $* to the part of the target that matches the stem (the %):
#echo $*.html
It also sets $(#F) to the filename part of the $# variable:
#echo $(#F)
ETA
If you want to do something very similar to your shell variable expansion using GNU make you can use:
#echo $(patsubst $(dist_directory)/%,%,$#)

Using variables in Makefile

Here is a simple Makefile.
FILENAME=test.`date +"%Y.%m.%d %H:%M:%S"`.txt
test:
#echo ${FILENAME}
#sleep 2s
#echo ${FILENAME}
The output of make test is
test.2013.02.18 15:30:23.txt
test.2013.02.18 15:30:25.txt
The problem is that FILENAME is being calculated each time it is used. I want it to be calculated only once and be the same while script is running. Am I doing it wrong?
GNU Make has two flavours of variables. You have used a recursively-expanded variable, but you want a simply-expanded variable. See http://www.gnu.org/software/make/manual/html_node/Flavors.html#Flavors
With your current Makefile, the variable contains the exact text test.date +"%Y.%m.%d %H:%M:%S".txt and every time you reference it that text gets substituted verbatim, and so your Makefile is equivalent to:
test:
#echo test.`date +"%Y.%m.%d %H:%M:%S"`.txt
#sleep 2s
#echo test.`date +"%Y.%m.%d %H:%M:%S"`.txt
Seen like this it should be obvious that the shell runs the date command twice.
To get the behaviour you want, you need to set a simply-expanded variable and you need to run the shell command at the point where you define it, which can't use backticks because they are shell metacharacters but Make ignores them, so you use Make's shell function instead:
FILENAME := test.$(shell date +"%Y.%m.%d %H:%M:%S").txt

Writing contents of makefile (>131000 chars) variable to a file?

How can I write the contents of a makefile variable to file, without invoking a shell command?
The problem is that the contents of the variable is possible longer than the shell allows for a command (i.e. longer than MAX_ARG_STRLEN (131072) characters).
In particular, in a makefile I have a variable containing a long list of filenames to process (including their absolute pathes for out-of-source builds). Now I need to write those filenames to a (temporary) file, which I can then pass to another command.
So far, we had a rule like ($COLLATED_FILES is the variable containing the paths):
$(outdir)/collated-files.tely: $(COLLATED_FILES)
$(LYS_TO_TELY) --name=$(outdir)/collated-files.tely --title="$(TITLE)" \
--author="$(AUTHOR)" $^
This breaks if COLLATED_FILES is longer than about 130000 characters, we get the error message:
make[2]: execvp: /bin/sh: Argument list too long
As a solution, we are now trying to write the contents of the variable to a file and use that file in the $(LYS_TO_TELY) command. Unfortunately, I have not yet found a way to do this without invoking the shell.
My attempts include:
$(outdir)/collated-files.list: $(COLLATED_FILES)
echo "" > $#
$(foreach f,$^,echo $f >> $#;)
But this also invokes all echo commands at once in a shell, so the shell command is just as long.
Is there any way to write the contents of $(COLLATED_FILES) to a file on disk without passing them on the command line to a shell command?
I also searched whether I could pipe the contents of the variable to the shell, but I couldn't find anything in that direction, either...
Assuming you are using GNU Make, there is the file function!
https://www.gnu.org/software/make/manual/html_node/File-Function.html
$(file op filename,text)
where op is either > or >>.
This requires GNU Make 4.0+
You could move whatever makefile code you use to build up the value of COLLATED_FILES to a trivial helper makefile, then invoke make recursively from your original makefile and use trivial shell redirection to capture the stdout of the recursive make invocation -- basically using make as a rudimentary text-processing tool in that context. For example, create a makefile called get_collated_files.mk with these contents:
COLLATED_FILES=abc
COLLATED_FILES+=def
COLLATED_FILES+=ghi
# ... etc ...
# Use $(info) to print the list to stdout. If you want each filename on a
# separate line, use this instead:
#
# $(foreach name,$(COLLATED_FILES),$(info $(name)))
$(info $(COLLATED_FILES))
all: ;##shell no-op to quell make complaints
Then, in your original makefile:
collated-files.list:
$(MAKE) -f get_collated_files.mk > $#
$(outdir)/collated-files.tely: collated-files.list
$(LYS_TO_TELY) --name=$(outdir)/collated-files.tely --title="$(TITLE)" \
--author="$(AUTHOR)" --filelist=collated-files.list
This will be quite a lot more efficient than using hundreds or thousands of individual echo invocations to append to the file one path at a time.
EDIT: One final option, if you really want to have each filename on a separate line, and you have a lot of control over how COLLATED_FILES is defined:
define NL
endef
COLLATED_FILES=abc
COLLATED_FILES+=$(NL)def
COLLATED_FILES+=$(NL)ghi
$(info $(COLLATED_FILES))
all: ;##no-op
This approach allows you to again use just one call to $(info), if that's important to you for some reason.
Here's a patch to gnu make that lets you directly write a variable into a file. It creates a new 'writefile' function, similar to the existing 'info' function, except it takes a filename argument and writes to the file:
https://savannah.gnu.org/bugs/?35384
It looks to me as if you should rethink your build design-- surely there's a better way than letting a variable get this big. But here's a way to do it:
# Make sure this doesn't collide with any of your other targets.
NAMES_TO_WRITE = $(addprefix write_,$(COLLATED_FILES))
collated-files.list: $(NAMES_TO_WRITE)
write_blank:
echo "" > collated-files.list
.PHONY: $(NAMES_TO_WRITE)
$(NAMES_TO_WRITE) : write_% : write_blank
echo $* >> collated-files.list

How to assign the output of a command to a Makefile variable

I need to execute some make rules conditionally, only if the Python installed is greater than a certain version (say 2.5).
I thought I could do something like executing:
python -c 'import sys; print int(sys.version_info >= (2,5))'
and then using the output ('1' if ok, '0' otherwise) in a ifeq make statement.
In a simple bash shell script it's just:
MY_VAR=`python -c 'import sys; print int(sys.version_info >= (2,5))'`
but that doesn't work in a Makefile.
Any suggestions? I could use any other sensible workaround to achieve this.
Use the Make shell builtin like in MY_VAR=$(shell echo whatever)
me#Zack:~$make
MY_VAR IS whatever
me#Zack:~$ cat Makefile
MY_VAR := $(shell echo whatever)
all:
#echo MY_VAR IS $(MY_VAR)
Beware of recipes like this
target:
MY_ID=$(GENERATE_ID);
echo $MY_ID;
It does two things wrong. The first line in the recipe is executed in a separate shell instance from the second line. The variable is lost in the meantime. Second thing wrong is that the $ is not escaped.
target:
MY_ID=$(GENERATE_ID); \
echo $$MY_ID;
Both problems have been fixed and the variable is useable. The backslash combines both lines to run in one single shell, hence the setting of the variable and the reading of the variable afterwords, works.
I realize the original post said how to get the results of a shell command into a MAKE variable, and this answer shows how to get it into a shell variable. But other readers may benefit.
One final improvement, if the consumer expects an "environment variable" to be set, then you have to export it.
my_shell_script
echo $MY_ID
would need this in the makefile
target:
export MY_ID=$(GENERATE_ID); \
./my_shell_script;
Hope that helps someone. In general, one should avoid doing any real work outside of recipes, because if someone use the makefile with '--dry-run' option, to only SEE what it will do, it won't have any undesirable side effects. Every $(shell) call is evaluated at compile time and some real work could accidentally be done. Better to leave the real work, like generating ids, to the inside of the recipes when possible.
Wrapping the assignment in an eval is working for me.
# dependency on .PHONY prevents Make from
# thinking there's `nothing to be done`
set_opts: .PHONY
$(eval DOCKER_OPTS = -v $(shell mktemp -d -p /scratch):/output)
With GNU Make, you can use shell and eval to store, run, and assign output from arbitrary command line invocations. The difference between the example below and those which use := is the := assignment happens once (when it is encountered) and for all. Recursively expanded variables set with = are a bit more "lazy"; references to other variables remain until the variable itself is referenced, and the subsequent recursive expansion takes place each time the variable is referenced, which is desirable for making "consistent, callable, snippets". See the manual on setting variables for more info.
# Generate a random number.
# This is not run initially.
GENERATE_ID = $(shell od -vAn -N2 -tu2 < /dev/urandom)
# Generate a random number, and assign it to MY_ID
# This is not run initially.
SET_ID = $(eval MY_ID=$(GENERATE_ID))
# You can use .PHONY to tell make that we aren't building a target output file
.PHONY: mytarget
mytarget:
# This is empty when we begin
#echo $(MY_ID)
# This recursively expands SET_ID, which calls the shell command and sets MY_ID
$(SET_ID)
# This will now be a random number
#echo $(MY_ID)
# Recursively expand SET_ID again, which calls the shell command (again) and sets MY_ID (again)
$(SET_ID)
# This will now be a different random number
#echo $(MY_ID)
Here's a bit more complicated example with piping and variable assignment inside recipe:
getpodname:
# Getting pod name
#eval $$(minikube docker-env) ;\
$(eval PODNAME=$(shell sh -c "kubectl get pods | grep profile-posts-api | grep Running" | awk '{print $$1}'))
echo $(PODNAME)
I'm writing an answer to increase visibility to the actual syntax that solves the problem. Unfortunately, what someone might see as trivial can become a very significant headache to someone looking for a simple answer to a reasonable question.
Put the following into the file "Makefile".
MY_VAR := $(shell python -c 'import sys; print int(sys.version_info >= (2,5))')
all:
#echo MY_VAR IS $(MY_VAR)
The behavior you would like to see is the following (assuming you have recent python installed).
make
MY_VAR IS 1
If you copy and paste the above text into the Makefile, will you get this? Probably not. You will probably get an error like what is reported here:
makefile:4: *** missing separator. Stop
Why: Because although I personally used a genuine tab, Stack Overflow (attempting to be helpful) converts my tab into a number of spaces. You, frustrated internet citizen, now copy this, thinking that you now have the same text that I used. The make command, now reads the spaces and finds that the "all" command is incorrectly formatted. So copy the above text, paste it, and then convert the whitespace before "#echo" to a tab, and this example should, at last, hopefully, work for you.
In the below example, I have stored the Makefile folder path to LOCAL_PKG_DIR and then use LOCAL_PKG_DIR variable in targets.
Makefile:
LOCAL_PKG_DIR := $(shell eval pwd)
.PHONY: print
print:
#echo $(LOCAL_PKG_DIR)
Terminal output:
$ make print
/home/amrit/folder
From the make manual
The shell assignment operator ‘!=’ can be used to execute a shell script and set a >variable to its output. This operator first evaluates the right-hand side, then passes >that result to the shell for execution. If the result of the execution ends in a >newline, that one newline is removed; all other newlines are replaced by spaces. The >resulting string is then placed into the named recursively-expanded variable. For >example:
hash != printf '\043'
file_list != find . -name '*.c'
source

Resources