How to use gzip correctly in a makefile? - makefile

I'm a make newbie, so feel free to correct my syntax for this question.
I want to create a makefile that downloads a bunch of compressed files, uncompresses them and does something with them. Here's my toy example:
my_dir/*.txt.gz:
echo "# obtaining data"
mkdir -p my_dir
echo 1 > my_dir/file1.txt
gzip my_dir/file1.txt
echo 2 > my_dir/file2.txt
gzip my_dir/file2.txt
my_dir/*.txt: my_dir/*.txt.gz
echo "# unzipping data"
parallel gzip -d ::: my_dir/*.txt.gz
final.txt: my_dir/*.txt
echo "# doing something with data"
cat my_dir/*.txt > $#
clean_my_dir:
rm -rf my_dir
rm -f final.txt
When I run make final.txt I get what I expect (final.txt contains 1\n2), but if I run make final.txt again (without running make clean_my_dir) I get an error:
make final.txt
echo "# obtaining data"
# obtaining data
mkdir -p my_dir
echo 1 > my_dir/file1.txt
gzip my_dir/file1.txt
echo 2 > my_dir/file2.txt
gzip my_dir/file2.txt
echo "# unzipping data"
# unzipping data
parallel gzip -d ::: my_dir/*.txt.gz
echo "# unzipping data"
# unzipping data
parallel gzip -d ::: my_dir/*.txt.gz
gzip: can't stat: my_dir/*.txt.gz (my_dir/*.txt.gz.gz): No such file or directory
make: *** [my_dir/file2.txt] Error 1
I'm sure I'm making a mistake in the way I set up the targets and the prerequisites: I want final.txt to contain the contents of file1.txt and file2.txt, but these files are initially zipped. How do I tell make to call gzip when there are compressed files and to not do anything when the uncompressed files are available?

After a little experimentation I created to following makefile:
COMPRESSED_DIR = my_dir
UNCOMPRESSED_DIR = temp_dir
DEPS = \
$(UNCOMPRESSED_DIR)/file1.txt \
$(UNCOMPRESSED_DIR)/file2.txt
##
## Recipes how to obtain compressed files
## (may propably not so trivial in your intended application)
##
$(COMPRESSED_DIR)/file1.txt.gz:
#echo ""
#echo "# obtaining $#"
mkdir -p $(COMPRESSED_DIR)
echo 1 > $(COMPRESSED_DIR)/file1.txt
gzip $(COMPRESSED_DIR)/file1.txt
$(COMPRESSED_DIR)/file2.txt.gz:
#echo ""
#echo "# obtaining $#"
mkdir -p $(COMPRESSED_DIR)
echo 2 > $(COMPRESSED_DIR)/file2.txt
gzip $(COMPRESSED_DIR)/file2.txt
## or alternatively:
#$(COMPRESSED_DIR)/%.txt.gz:
# #echo ""
# #echo "# obtaining $#"
# mkdir -p $(COMPRESSED_DIR)
# echo $* > $(COMPRESSED_DIR)/$*.txt
# gzip $(COMPRESSED_DIR)/$*.txt
##
## Recipe to unpack compressed files
##
$(UNCOMPRESSED_DIR)/%.txt: $(COMPRESSED_DIR)/%.txt.gz
#echo ""
#echo "# unpacking $<..."
mkdir -p $(UNCOMPRESSED_DIR)
gzip -c -d $< > $#
##
## Recipe to make final.txt
##
final.txt: $(DEPS)
#echo ""
#echo "# doing something with data"
cat $(UNCOMPRESSED_DIR)/*.txt > final.txt
##
## Recipe to clean
##
clean:
rm -rf $(COMPRESSED_DIR)
rm -rf $(UNCOMPRESSED_DIR)
rm -f final.txt
As pointed out before gzip deletes the txt.gz file and therefore the recipe for creating the txt.gz files is re-executed if making final.txt again.
What I've done is placing the unpacked txt.gz files in a seperate directory and forcing gzip to keep the original file when unpacking. This leads to the following behavior:
If called the first time, the txt.gz files will be created and unpacked and final.txt will be created:
# obtaining my_dir/file1.txt.gz
mkdir -p my_dir
echo 1 > my_dir/file1.txt
gzip my_dir/file1.txt
# unpacking my_dir/file1.txt.gz...
mkdir -p temp_dir
gzip -c -d my_dir/file1.txt.gz > temp_dir/file1.txt
# obtaining my_dir/file2.txt.gz
mkdir -p my_dir
echo 2 > my_dir/file2.txt
gzip my_dir/file2.txt
# unpacking my_dir/file2.txt.gz...
mkdir -p temp_dir
gzip -c -d my_dir/file2.txt.gz > temp_dir/file2.txt
# doing something with data
cat temp_dir/*.txt > final.txt
If called again (without make clean) the output will be make:final.txt' is up to date.`. If a file of $(DEPS) is deleted the corresponding txt.gz file will be re-unpacked. If a .txt.gz file is deleted the .txt.gz file will be re-created and re-unpacked (since it is newer than the existing .txt file).
Alternativly if you've already got all the *.txt.gz files you want to process (and the makefile does not have to create them if they are missing) you can use the following makefile:
COMPRESSED_DIR = my_dir
UNCOMPRESSED_DIR = temp_dir
# get a list of all *.txt.gz files within $(COMPRESSED_DIR)
ARCHIVES = $(wildcard $(COMPRESSED_DIR)/*.txt.gz)
# do some magic to modify $(ARCHIVES) into a list of *.txt files to produce
DEPS = $(subst $(COMPRESSED_DIR),$(UNCOMPRESSED_DIR),$(subst .txt.gz,.txt,$(ARCHIVES)))
##
## Recipe to unpack compressed files
##
$(UNCOMPRESSED_DIR)/%.txt: $(COMPRESSED_DIR)/%.txt.gz
#echo ""
#echo "# unpacking $<..."
mkdir -p $(UNCOMPRESSED_DIR)
gzip -c -d $< > $#
##
## Recipe to make final.txt
##
final.txt: $(DEPS)
#echo "Archives:"
#echo $(ARCHIVES)
#echo ""
#echo "Deps:"
#echo $(DEPS)
#echo ""
#echo "# doing something with data"
cat $(UNCOMPRESSED_DIR)/*.txt > final.txt
##
## Recipe to clean
##
clean:
rm -rf $(UNCOMPRESSED_DIR)
rm -f final.txt
This makefile will uncompress all *.txt.gz files within COMPRESSED_DIR to UNCOMPRESSED_DIR and cat all *.txt files into final.txt.

You can do the gunzipping in one step:
my_dir:
[ -d $# ] || mkdir -p $#
my_dir/%.txt.gz: my_dir
#echo "# obtaining data for $#"
echo $* | gzip -c > $#
final.txt: my_dir/*.txt.gz
#echo "# doing something with data for $#"
gunzip -c $+ > $#
clean:
rm -rf my_dir final.txt
The dependency my_dir/*.txt.gz means that final.txt will be made from whatever matching files happen to be present at the time (and it will fail when none are present). To ensure the files are made when not present, you need to list them explicitly, e.g. (if GNU Make is used)
final.txt: $(foreach x,1 2 3 4 5 6 7 8,my_dir/$x.txt.gz)

Related

Makefile: how to use a command which randomly generates a file or leaves it unchanged

I have a command which sometimes updates a target file, sometimes it leaves it unchanged, but I cannot tell which one of those will happen. I would like to create a dependency that should run ONLY if the first file is updated. I could not do this. Here is a simple Makefile that reproduces this problem:
all: file2.txt
file1.txt:
#if [ ! -f file1.txt -o $(shell bash -c "expr \$$RANDOM \% 2") = 1 ]; \
then \
echo "update" >> file1.txt; \
echo "file1.txt overwritten"; \
else \
echo "file1.txt is unchanged"; \
fi
.PHONY: file1.txt
file2.txt: file1.txt
cp file1.txt file2.txt
The test in the rule for file1.txt will generate file1.txt if it does not exists or if a random number modulo 2 is 1. I would like to see cp file1.txt file2.txt executed if and only if file1.txt is overwritten.
By adding the .PHONY setting you are specifically telling make that no matter what that target is always considered out of date, so you're directly contradicting what you want to do.
You can use another method to handle this via a "force target", like this:
FORCE: ;
file1.txt: FORCE
...
and remove the .PHONY setting.

Makefile split string and pipe it to different target

I am trying to write a simple Makefile to build .expected files and compare them but I am failing.
APSSCHED=../../bin/apssched
BASE=.:../../base:../../examples
FLAGS=-DCOT
EXAMPLES=../../examples/
CASES=simple-binding1 simple-binding2
# skipping lines doesn't work ...
# run command and skip the first line
%.aps:
${APSSCHED} ${FLAGS} -p ${BASE} ${EXAMPLES}/$* | tail -n +2
# get all cases as an array to pipe it to different make targets
# maybe overcomplicating
cases:
echo ${CASES} | \
awk '{split($$0,numbers," ")} END {for(n in numbers){ print numbers[n] }}'
# create all .expected files from ${CASES}
build.expected:
$(MAKE) cases | xargs -n1 -I file /bin/bash -c '$(MAKE) file.build.expected'
# create single .expected file
%.build.expected:
$(MAKE) $*.aps > $*.expected
# compare result with
%.compare:
$(MAKE) $*.aps | diff $*.expected -
# run command for all cases and diff the result with corresponding expected
all:
$(MAKE) cases | xargs -n1 -I file /bin/bash -c '$(MAKE) file.compare'
clean.expected:
rm *.expected
Running make without any target and nothing happens.
echo simple-binding1 simple-binding2 | \
awk '{split($0,numbers," ")} END {for(n in numbers){ print numbers[n] }}'
simple-binding1
simple-binding2
I think the issue is with my cases target. I am not sure if I am on the right track.
I appreciate any help or hint.
I would avoid re-running make just to call a different target - it's a performance hit and may be unreliable (depending on rest of the Makefile) since separate calls may not be able to track dependencies correctly.
Moreover, I would avoid using | - every time a command is concatenated with pipe, exit code of piped command would be exit code of the last command. So a call like command | tail would return the exit code of tail (which would almost always succeed). Even if the command has failed, it would be covered with exit code 0 from tail and make will not detect the error and will not stop.
Thus said, I tried to rewrite your approach by just creating dependencies between the targets, like so:
$ cat Makefile
APSSCHED=../../bin/apssched
EXAMPLES=../../examples
BASE=.:../../base:$(EXAMPLES)
FLAGS=-DCOT
CASES=simple-binding1 simple-binding2
# Just for reproducing
$(EXAMPLES)/%.aps: ;
# Generate output and store it in a file
%.output: $(EXAMPLES)/%.aps
# echo is only for reproducing
echo $(APSSCHED) $(FLAGS) -p $(BASE) $< > $#
# Copy actual output as expected
%.expected: %.output
cp -f $< $#
# Compare actual output with expected
.PHONY: %.compare
%.compare: %.output | %.expected
diff $| $<
# Generate and verify all outputs
.PHONY: all
all: $(addsuffix .compare,$(CASES))
# Regenerate expected output
.PHONY: build.expected
build.expected: $(addsuffix .expected,$(CASES))
.PHONY: clean.expected
clean.expected:
-rm -f *.expected
Now the make build.expected will create expected output files, while make all or make will check the actual output against expected:
$ make build.expected
echo ../../bin/apssched -DCOT -p .:../../base:../../examples ../../examples/simple-binding1.aps > simple-binding1.output
cp -f simple-binding1.output simple-binding1.expected
echo ../../bin/apssched -DCOT -p .:../../base:../../examples ../../examples/simple-binding2.aps > simple-binding2.output
cp -f simple-binding2.output simple-binding2.expected
rm simple-binding1.output simple-binding2.output
$ make
echo ../../bin/apssched -DCOT -p .:../../base:../../examples ../../examples/simple-binding1.aps > simple-binding1.output
diff simple-binding1.expected simple-binding1.output
echo ../../bin/apssched -DCOT -p .:../../base:../../examples ../../examples/simple-binding2.aps > simple-binding2.output
diff simple-binding2.expected simple-binding2.output
rm simple-binding1.output simple-binding2.output

Makefile issue with calling another shell file for make

I have created a parent makefile. as below:
SHELL = /bin/bash
HOMEDIR = $(shell pwd)
PKGNAM = PARAMETIS
override VERSION = 4.0.3
YESDIR = $(shell echo $(#:install-%=%) | tr A-Z a-z)
NODIR = $(shell echo $(#:clean-%=%) | tr A-Z a-z)
install:
$(MAKE) install-$(VERSION)
install-%:
#if [ ! -e $(YESDIR) ]; then \
echo "Library $(PKGNAM) Version=$(YESDIR) does not exist"; \
elif [ -e $(YESDIR)/Install.sh ]; then \
echo "Installing $(PKGNAM) version=$(YESDIR)" ; \
cd $(YESDIR) ;\
$(SHELL) Install.sh $(HOMEDIR) 1 ;\
elif [ -e $(YESDIR)/Makefile ]; then \
cd $(YESDIR); \
$(MAKE); \
else \
echo "Installation instruction for $(#:install-%=%) Version=$(YESDIR) does not exist"; \
fi;
clean:
#$(MAKE) clean-$(VERSION)
clean-%:
#if [! -e ${NODIR} ]; then ;\
echo "Library does not exist $(PKGNAM) version=$(NODIR)" ; \
else \
cd $(NODIR) ;\
echo "Installing $(PKGNAM) version=$(NODIR)" ; \
$(SHELL) Install.sh $(HOMEDIR) 0 ;\
fi;
This makefile calls different bash files inside each version of the libraries directories to build them, the bash files can successfully build each library if I call it from the terminal, tho when I call them from my make file using,
make install
after it executes the install.sh and build the library, I get this error that
No rule to make target 'w'. Stop.
any idea why it happens and how can I get rid of it ?
HERE is the bash file if it helps:
if (test $2 = 1) then
make --silent -f Makefile config prefix=$1/exec
make --silent -f Makefile
make --silent -f Makefile install
elif (test $2 = 0) then
make --silent -f Makefile clean
fi
Thanks
The problem was caused by calling other Makefiles using -C in the library's makefile called in the Install.sh. This sets the MAKEFLAGS to w automatically. unfortunately, the developers of the library has made mistake in calling the makefiles as below:
$(MAKE) -C $(SUBDIR) $# $(MAKEFLAGS)
when the makefile is called from another makefile, this MAKEFLAGS are set to w, but in the bash called from the terminal they are empty. because developers have forgotten to add MAKEFLAGS= before the flags, it assumes that w is another target and because it is not defined it generates the error, I menotiond.
I solved the issue by changing their makefile as below:
$(MAKE) -c $(SUBDIR) $# MAKEFLAGS=$(MAKEFLAGS)
and now everything works as expected.

Getting the list of dependencies of a target

Is it possible to read the dependencies of a target inside a Makefile?
I would like to do something like the following:
.INTERMEDIATE: temp1.txt
.INTERMEDIATE: temp2.txt
print-intermediates:
#echo "Temp files: $(dependencies-of .INTERMEDIATE)"
The output of make print-intermediate would be
$ make print-intermediate
Temp files: temp1.txt temp2.txt
How can I access the list of dependencies of the .INTERMEDIATE target?
Recursive call of make.
There is no built-in function for this purpose. But there are some workarounds.
For example you can use recursive call of make with command line argument -p.
.INTERMEDIATE: temp1.txt
.INTERMEDIATE: temp2.txt
deps = $(shell $(MAKE) -qp none | sed -n "/$(strip $(1)):/ p;" | \
sed -e "s/$(strip $(1)): //g")
print-intermediates:
#echo "Temp files: " $(call deps,.INTERMEDIATE)
.PHONY: none
none:;

Makefile with dependency graph not known in advance

I'm trying to create a makefile (GNU make) that does the following:
A script generates a bunch of files--filenames not known in advance.
Each one of these files is converted to a different file.
After all are converted, all of these files are combined into a single output file.
How do I create a makefile with a "bellcurve"-patterned dependency graph, where the intermediate source and target files are not known in advance?
Conceptually I'm doing the following:
combined.pdf: $(filter combined.pdf, $(wildcard *.pdf))
cat *.pdf > combined.pdf
%.pdf: %.svg
cp $^ $#
$(wildcard *.svg):
# recipe is for simple example
# actually the *.svg files are not known in advance
echo a > a.svg
echo b > b.svg
echo c > c.svg
.PHONY: clean
clean:
${RM} *.svg *.pdf *.d
Of course this doesn't work: Make evaluates the targets and sources before it runs the target that actually creates the svg. Also, there's no way to make sure all svgs are converted before they are combined.
I realized I could create dependencies and include them into the makefile, but I had trouble getting this to work too:
.PHONY: clean
include deps.d
combined.pdf: deps.d
cat *.pdf > combined.pdf
%.pdf: %.svg
cp $^ $#
deps.d:
## recipe is for simple example
## actually the *.svg files are not known in advance
echo a > a.svg
echo b > b.svg
echo c > c.svg
## we know what files exist now, so we can establish dependencies
## "a.pdf : a.svg"
echo *.svg : > deps.d
## combined.pdf: a.pdf b.pdf c.pdf
ls *.svg \
| awk '{targetfn=$$0; sub(/\.svg$$/, ".pdf", targetfn); print targetfn, ":", $$0;}' \
>> deps.d
## combined.pdf: a.pdf b.pdf c.pdf
echo combined.pdf : $$(echo *.svg | sed -e 's/\.svg/\.pdf/g') >> deps.d
clean:
${RM} *.pdf *.svg *.d
However this still isn't connecting the dependency graph properly. When I run this, make quits as follows:
Makefile:3: deps.d: No such file or directory
echo a > a.svg
echo b > b.svg
echo c > c.svg
echo *.svg : > deps.d
ls *.svg \
| awk '{targetfn=$0; sub(/\.svg$/, ".pdf", targetfn); print targetfn, ":", $0;}' \
>> deps.d
echo combined.pdf : $(echo *.svg | sed -e 's/\.svg/\.pdf/g') >> deps.d
make: Nothing to be done for `a.svg'.
I still seem to have the problem that the make doesn't know about the rules in deps.d.
Also, this still doesn't solve the problem of building all the dependencies. I thought of using a marker file like this:
%.pdf: %.svg
cp $^ $#
## if all svgs are converted, touch a target allpdfs
if [ $(ls -1 *.svg | wc -l) -eq $(ls -1 *.pdf | grep -v combined\.pdf | wc -l) ]; touch allpdfs; fi
But there's no way to inform make that "allpdfs" may be created by this rule.
I'm surprised that moving the include directive makes a difference (what version of Make are you using?), but there is a simpler way. Your use of deps.d is in effect a recursive use of Make -- Make is arranging to execute itself a second time -- so we might as well make it official:
combined.pdf: ALL_SVGS
$(MAKE) ALL_PDFS
rm -f $# # just in case it exists already
cat *.pdf > $#
.PHONY: ALL_SVGS
ALL_SVGS:
# recipe is for simple example
# actually the *.svg files are not known in advance
echo a > a.svg
echo b > b.svg
echo c > c.svg
# These variables will be empty in the first execution of Make
SVGS = $(wildcard *.svg)
PDFS = $(patsubst %.svg,%.pdf,$(SVGS))
.PHONY: ALL_PDFS
ALL_PDFS: $(PDFS))
%.pdf: %.svg
cp $^ $#
This isn't an answer exactly, because I don't know why this works, but I discovered that if I move the include directive after the target that creates the included file, everything works.
I.e. do this:
deps.d:
....
include deps.d
Because my deps.d includes enough dependency information, there's no need to have an intermediate target allpdfs file. Everything Just Works, even with make -j.
However, I don't know why this works. The include documentation isn't enlightening me.
UPDATE
I noticed the following note at the very bottom of the make manual discussing Automatic Prerequisites:
Note that the ā€˜.dā€™ files contain target definitions; you should be sure to place the include directive after the first, default goal in your makefiles or run the risk of having a random object file become the default goal. See How Make Works.
So what happened is that the first rule inside the generated deps.d became the default target, causing the mysterious premature completion of the build. So the solution is just to make sure include directives are not before your intended default target.
I was just working on this exact problem in a slightly different setting. Here is a clean solution - no need for recursion and such (and you can tweak the sed if you like):
include deps.d
combined.pdf:
cat *.pdf > combined.pdf
%.pdf: %.svg
cp $^ $#
deps.d:
echo a > a.svg
echo b > b.svg
echo c > c.svg
echo 'combined.pdf:' *.svg | sed 's/\.svg/\.pdf/g' > deps.d
Enjoy!

Resources