How to use makefile variable outside of a rule - makefile

Right now I am learning how to use makefiles and am having trouble with variables inside rules. My goal is to parse a file containing names of other files, save that in a variable, and use the variable as a "target pattern" to run a rule on each file. Here is a simple example of my problem (I'm writing the file names in to make it simpler).
When I run this:
variable = file1.txt file2.txt file3.txt
run : $(variable)
$(variable): %.txt:
echo File is $#
I get the following, which is what I want:
$ make run
echo File is file1.txt
File is file1.txt
echo File is file2.txt
File is file2.txt
echo File is file3.txt
File is file3.txt
The problem is I want to define variable by parsing a file, and I want that file to be a dependency because, if it does not exist, I will make it using another rule in the makefile. So when I define the variable in a rule like this (which I know is there from my echo):
target1 :
$(eval variable = file1.txt file2.txt file3.txt)
echo $(variable)
run : $(variable)
$(variable): %.txt:
echo File is $#
I get the following:
$ make target1
echo file1.txt file2.txt file3.txt
file1.txt file2.txt file3.txt
And this:
$ make run
make: Nothing to be done for `run'.
So my question is how to define the variable in a rule and and use it in something line this:
$(variable): %.txt:
echo File is $#
I've tried reading through the manual and Googling but I just can't figure this out. I also imagine there is some simple answer I am missing. Thanks so much for your help! :)

If you can control the content of the target1 created file this is easy: just make the format of that file be a make variable assignment, then use the include command, like this:
run:
include target1
run: $(variable)
$(variable): %.txt:
echo File is $#
target1:
echo 'variable = file1.txt file2.txt file3.txt' > $#
Through the magic of automatically generated makefiles make will recreate that target file then re-invoke itself automatically.

Related

Lists on Makefile

How can I get single elements from a list defined in a Makefile. For example:
FILES:= file0 \
file1
all: $(FILES)
$(FILES):
echo "write on file 0 something" > file0
echo "write on file 1 something else" > file1
Now I need to write something like this (using the first element of the list):
echo "write on file 0 something" > "${FILES[0]}"
Thanks in advance
This is not right:
$(FILES):
echo "write on file 0 something" > file0
echo "write on file 1 something else" > file1
You seem to be assuming that this syntax means that one invocation of this recipe will build all the output files. That's not what it means. It means that make will try to build each target and to build it, it will run a separate instance of the recipe. It's the same as writing:
file0:
echo "write on file 0 something" > file0
echo "write on file 1 something else" > file1
file1:
echo "write on file 0 something" > file0
echo "write on file 1 something else" > file1
Since this makefile doesn't really do anything useful we can't advise you on how to fix it.
But the answer to your question, assuming you're using GNU make (you don't say) might be found in the GNU make function documentation, specifically this section.
A potential hint:, $# will refer to the target that is currently being built. So if you have:
FILES := file0 file1
$(FILES):
#echo Compiling $#
it will output
Compiling file0
Compiling file1
(assuming both files need to be rebuilt). Now, if you want to have a file specific string attached to each file, you can use token pasting to create target specific variables as so:
FILES := file0 file1
STRING_file0 := write on file 0 something
STRING_file1 := write on file 1 something else
$(FILES):
#echo "$(STRING_$#)" > $#
In this case it will first expand the $# within the braces, which results in #echo "$(STRING_file0)" > $# for example, and then it would expand $(STRING_file0) to be write on file 0 something. Finally it would expand the second $# to be file0, and would therefore pass
#echo "write on file 0 something" > file0
on the shell when it is building file0. I think this is the behavior you're after. This can, of course be done with functions as well as #MadScientist suggested.

Using Makefile for editing files rather than creating them

I was thinking about using Make for small checks for my dev setup. One thing I want is to check that a particular string exists in a file before doing some action. If I wanted to create the entire file it would be trivial
action: filename
...
filename:
echo 'blah' >> filename
But how can this logic be applied to actions, like grep? My dependency isn't that a file exists, it's that the file has correct content.
I'm asking specifically about Make and not other solutions like chef/puppet
You can run any shell commands you want in a make recipe. As many of them as you want also.
So if you need to run grep before doing something else just do that.
Just remember that every line in a recipe is run in its own shell session so they don't share state.
So this:
action: filename
...
filename:
grep -qw blah $# || echo 'blah' > $#
runs grep on filename (via the automatic variable for the current target $#) looking for whole words and quitting on the first match (-q).
If grep finds blah then it will return success and the || will short-circuit and the recipe is done. If grep fails then the || will trigger and the echo will run.
You might be tempted to do things that require the inverse logic do X only if Y is true:
filename:
grep -qw blah $# && echo blah2 > $#
but that doesn't work correctly. When grep fails the && short-circuits and make sees a recipe failure and bails the make process out with an error.
You need this instead.
filename:
! grep -qw blah $# || echo blah2 > $#
to invert the logic and ensure that the "failure" from grep is seen as success as far as make is concerned.
That all being said in this specific example if filename exists at all then that recipe won't ever run as it has no prerequisites so make will always consider it up to date. To work around that you need to give the file a prerequisite that will force it to be considered out of date. Specifically a force target.
Don't follow the advice about .PHONY for this case though. .PHONY targets should never be prerequisites of non-.PHONY targets.
Expanding on what #john wrote I got the following to work:
TEST_FILE=filename
.PHONY: ${TEST_FILE}
string=testing
filecheck=$(shell grep -qw ${string} ${TEST_FILE} || echo ${TEST_FILE})
all: ${filecheck}
${TEST_FILE}:
echo 'changing the file'
echo ${string} >> ${TEST_FILE}
Here the file on which I'm operating is a .PHONY target. I think that's ok because I'm actually not creating the file, just modifying it. This will work if the file does not exist, or exists without the needed string.
You could add a test in the target's recipe (As Etan posted before I could complete this answer...). If you do want to do this using just make logic, you could do something along the lines of:
actions: $(if $(shell grep -q $$string filename && echo y),filename,)
filename:
echo blah >> $#
If filename contains the string, then there will be an actions: filename dependency, and filename will be built when you build actions. Notice, though that this will check whether the string exists in filename at the time the makefile is parsed -- if filename is generated, or modified in this makefile, then it would not effect whether the action is run. If you want to test right before overwriting the file, then you would use a bash if statement in the recipe itself.

Makefile, how to have prerequisites given by function with target as argument

I have a list of files contained in another file. I want to use this list as prerequisite for some target and for doing so I use a function that reads the list from file.
The problem is that I have different lists for different targets so I need to pass the target as argument to the function that reads the list. Something like that (that does not work):
getlist = $(shell cat $1)
tmp%: $(call getlist, %)
#cat file1 > $#
#cat file2 >> $#
file%:
#touch $#
#echo "$#" > $#
clean:
#rm file1 file2 tmp
where the list for building the tmp1 file is in the 1 file, the one for building the tmp2 file is in the 2 file and so on and so forth.
If I have instead tmp1: $(call figlist, 1) all works, but I need something capable of treating different file names.
If needed for the solution I can also change the way I named the files.

Makefile pattern rule with variable in target

I'm using Gnu Make 3.81, and getting an error trying to match a pattern rule that also has a variable in it.
Here's the smallest example I could come up with:
YYMMDD:=$(shell date +%y%m%d)
TMP_DIR:=/tmp/$(YYMMDD)
# create a temporary directory, and put a "source" file in it
$(TMP_DIR):
mkdir $(TMP_DIR)
echo something > $(TMP_DIR)/somefile.orig
# to build an "object" file in the temp dir, process the "source" file
$(TMP_DIR)/%.new: $(TMP_DIR)/%.orig
wc -l $< > $#
atarget: $(TMP_DIR) $(TMP_DIR)/somefile.new
Then when I run make atarget, I get:
mkdir /tmp/141021
echo something > /tmp/141021/somefile.orig
make: *** No rule to make target `/tmp/141021/somefile.new', needed by `atarget'. Stop.
Shouldn't this work? it seems like the pattern rule should match this just fine.
It's because make doesn't know that the .orig file exists: you have a rule that builds $(TMP_DIR) but make doesn't know that this rule also builds $(TMP_DIR)/somefile.orig. So when make is trying to match the pattern rule it will see that the .orig file doesn't exist and it doesn't have any way that it knows how to make that file, so the pattern doesn't match, and after that there's no way to build the .new file.
You should write:
$(TMP_DIR)/%.orig:
mkdir -p $(TMP_DIR)
echo $* > $#
then it will work.

Creating a rule for a file type

How can I create a rule in a makefile for a specific filetype so if I write something like
result.txt: first.txt second.txt
in a makefile and it will concatenate prerequisites.
.txt:
cat $? > $#
doesn't work
Since this seems to have helped, I am reposting it as an answer, with #eriktous' comment added in.
%.txt:
cat $? >$#
If you don't want to process just the changed files, use $^ instead of $?.

Resources