My open source project distributes a Makefile. "make" by itself works fine as long as the user has Boost and OpenSSL installed. If not, he gets a compilation error.
I would like to show the user an error message with instructions on how to fix rather than have him discern the issue from the compiler output.
I've put together a little script to embed inside a Makefile that will do a quick and dirty compilation to validate if a prerequisite header file exists before allowing the core code to build. It shows an error message and aborts the compile if the code won't compile. It seems to work good.
# BOOST_INCLUDE := -I/home/jselbie/boost_1_51_0
all: myapp
testforboost.o:
#echo "Testing for the presence of Boost header files..."
#rm -f testforboost.o
#echo "#include <boost/shared_ptr.hpp> " | $(CXX) $(BOOST_INCLUDE) -x c++ -c - -o testforboost.o 2>testerr; true
#rm -f testerr
#if [ -e testforboost.o ];\
then \
echo "Validated Boost header files are available";\
else \
echo "* ********************************************";\
echo "* Error: Boost header files are not avaialble";\
echo "* Consult the README file on how to fix";\
echo "* ********************************************";\
exit 1;\
fi
myapp: testforboost.o
$(CXX) $(BOOST_INCLUDE) myapp.cpp -o myapp
Is my script a good way to do this? I'm under the assumption that it's portable beyond Linux (Solaris, BSD, MacOS). Or are there other standard practices for doing this? I know that Autotools can do similar things, but I'm not too excited about learning all of Autotools and re-writing my Makefiles.
In principle it's possible like that. But since you're only preprocessing, and given that you can use any command as a condition, it can be simplified to:
.PHONY: testforboost
testforboost:
#echo "Testing for the presence of Boost header files..."
#if echo "#include <boost/shared_ptr.hpp> " | $(CXX) -x c++ -E - >/dev/null 2>&1;\
then \
echo "Validated Boost header files are available";\
else \
echo "* ********************************************";\
echo "* Error: Boost header files are not avaialble";\
echo "* Consult the README file on how to fix";\
echo "* ********************************************";\
exit 1;\
fi
OTOH, since you have the boost include path in a variable, why not just look for the file directly? That would need some string manipulation. Probably hard in make, but with makepp it would be $(map $(BOOST_INCLUDE),s/^-I//)
Related
I'm trying to adapt the Example from the Official Documentation of GNU make to my Use Case:
GNU make - Example of a Conditional
libs_for_gcc = -lgnu
normal_libs =
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
foo: $(objects)
$(CC) -o foo $(objects) $(libs)
So, I created this Prototype:
libs_for_gcc="gcc libs"
normal_libs="normal libs"
libs=
all: do_nothing
#echo "all: done."
do_nothing:
#echo "do_nothing: done."
example:
#echo "example: go ..."
#echo "example - cc: '$(CC)'"
#echo "libs_for_gcc: $(libs_for_gcc)"
#echo "normal_libs: $(normal_libs)"
#echo "libs: $(libs)"
ifeq ($(CC),gcc)
libs=$(libs_for_gcc) && echo "libs: '$$libs'"
#echo "example - libs: '$(libs)'"
else
libs=$(normal_libs) && echo "libs: '$$libs'"
#echo example - libs: $(libs)
endif
#echo "example - libs: '$(libs)'"
#echo "example: done."
test: libs += " -Ddebug"
test: example foo
bar:
#echo "bar: go ..."
ifeq (${libs}, "")
#echo "bar - libs: empty"
#echo "assigning libs"
libs=$(libs_for_gcc)
else
#echo "bar - libs: not empty"
#echo "bar - libs: '${libs}'"
endif
#echo "bar - libs: '${libs}'"
#echo "bar: done."
foo:
#echo "foo: go ..."
ifneq ("$(libs)", "")
#echo "foo - libs: not empty"
#echo "foo - libs: '$(libs)'"
else
#echo "foo - libs: empty"
endif
#echo "foo: done."
Now when I run the Default Target with $ make
it just produces:
$ make
example: go ...
example - cc: 'cc'
libs_for_gcc: gcc libs
normal_libs: normal libs
libs:
libs="normal libs"
example - libs:
example - libs: ''
example: done.
I see that the value of libs was not changed as intended.
When I run the make bar Target it produces:
$ make bar
bar: go ...
bar - libs: not empty
bar - libs: ''
bar - libs: ''
bar: done.
Here libs is not empty but it has nothing inside.
And when I run the make foo target it produces:
$ make foo
foo: go ...
foo - libs: empty
foo: done.
Here libs is understood as empty
As I see that libs is not changed correctly I tried to change the syntax to:
example:
# [...]
ifeq ($(CC),gcc)
libs := $(libs_for_gcc)
#echo "example - libs: '$(libs)'"
else
libs := $(normal_libs)
#echo example - libs: $(libs)
endif
But then I get the GNU make Error:
$ make
example: go ...
example - cc: 'cc'
libs_for_gcc: gcc libs
normal_libs: normal libs
libs:
libs := "normal libs"
/bin/sh: 1: libs: not found
Makefile:7: recipe for target 'example' failed
make: *** [example] Error 127
I couldn't find any documentation about this behaviour so I appreciate any advise.
Edit:
Added all and test Targets.
Background:
The GNU make Command is an important part of the toolchain for packaging and deploying software and thus important in the daily work of System Administrators and DevOps Engineers.
The Debian and RPM Packaging uses GNU make to package software.
Makefile Driven Packaging
It runs the ./configure && make && make install command sequence.
The Travis CI Workflow uses GNU make for running testsuites.
C Language Automated Testing
It runs the ./configure && make && make test sequence.
All the completely different Use Case are managed by the same Makefile.
Now for my concrete Use Case I'm working on setting up the Travis CI Workflow Sequence to enable Automated Testing for my static linked Source Code Library.
So, contrary to the Packaging Sequence the Automated Testing Sequence requires Debug Features and advanced Output Evaluation to produce a meaningful Test.
I want the Test to check the Error Report and also the Memory Usage Report to alert me of any hidden errors.
Using the Advice about setting the Variable at the Target Declaration Line I was able to change libs for test, example and foo targets.
I also saw the important hint about the Bash Variable libs which is only valid on the same line:
$ make
do_nothing: done.
all: done.
$ make test
example: go ...
example - cc: 'cc'
libs_for_gcc: gcc libs
normal_libs: normal libs
libs: -Ddebug
libs="normal libs" && echo "libs: '$libs'" ;
libs: 'normal libs'
example - libs: -Ddebug
example - libs: ' -Ddebug'
example: done.
foo: go ...
foo - libs: empty
foo - libs: ' -Ddebug'
foo: done.
The Recipe libs=$(libs_for_gcc) && echo "libs: '$$libs'" shows that a new Bash Variable libs was created and it did not affect the GNU make Variable.
Still the Conditional ifneq ($(libs),) cannot detect that libs was already set for the test target.
The difference between your example and the one in the GNU make manual is that in the GNU make manual they are setting a make variable named libs (they are assigning the variable outside of any recipe).
In your usage you are assigning a shell variable named libs (you are assigning it inside the recipe, indented with a TAB).
That's why you get an error when you try to use :=, because that's a make assignment and is not a valid shell assignment.
Then in your echo you print the make variable $(libs) which has not been set at all. Further, every logical line of the recipe is run in inside it's own shell. So you are running the equivalent of:
/bin/sh -c 'libs="gcc libs"'
/bin/sh -c 'echo '
so even if you DID change your echo command to print the shell variable (via $$libs) it would be empty because the previous shell, where it was set, has exited.
You want to use the same syntax as in the example: take the assignment of the variable OUT of the recipe and set the make variable instead:
libs_for_gcc = gcc libs
normal_libs = normal libs
ifeq ($(CC),gcc)
libs = $(libs_for_gcc)
else
libs = $(normal_libs)
endif
example:
#echo "example: go ..."
#echo "example - cc: '$(CC)'"
#echo "libs_for_gcc: $(libs_for_gcc)"
#echo "normal_libs: $(normal_libs)"
#echo "libs: $(libs)"
#echo "example - libs: '$(libs)'"
#echo "example: done."
Finally I achieved to get my Makefile make test Target working.
make test Test Suite with GNU make
The key was to understand that there are 3 different contexts where a GNU make Variable can be assigned.
And that GNU make often needs helper functions because of some strange issues with Conditionals
These issues are mostly undocumented and only can be solved by Try and Error and searching on StackOverflow.com
1. The first, easiest and best documented context to assign a Variable is in Global Context as seen in the official documentation and the first answer.
GNU make - Example of a Conditional
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
The Global Variable ${libs} has global visibility.
2. The next context which is not that obvious but still documented in the Official Documentation is in the Target Definition Line
GNU make - Target-specific Variable Values
test: libs += " -Ddebug"
test: example foo
Here ${libs} is valid for the test Target and all dependent Targets that it calls.
But the only Condition is the Target itself.
3. To assign dynamically a Variable in the context of a Target is to use the $(eval ) Function in combination with the $(shell ) Function as seen in this StackOverflow.com Post:
Assign a Variable as Result of a Command
test: examples_debug
# [...]
$(eval error := $(shell cat ./demo_error.log))
ifeq ($(strip $(error)),)
#$(ECHO) "NO Error: '${error}'"
else
#$(ECHO) "Error detected: '${error}'"
$(error "Demo failed with Error [Code: '${error_code}']")
#exit 1
endif
Here ${error} is read from a File. Additionally the $(strip ) Function is required to be able to check if it is empty, which is some of the undocumented issues that GNU make has and are wierd to Bash Developers.
4. Another method that works but does not use a Makefile Variable and is somewhat bulky is evaluating the Variable entirely in Bash in 1 single Line which can be found at: Check Variable with Bash Command Recipe and was also hinted by the previous Answer.
Which would look like:
test: test_debug
# [...]
leak=`cat ./test_heap.log | grep -i "unfreed memory blocks" | awk '{print $$1}'` ; if [ $$leak -gt 1 ]; then echo "Memory Leaks: $$leak" && exit 1; else echo "Memory Leaks: NONE"; fi ;
Here $$leak is a Bash Variable and only valid within the same Line.
An approach that is somewhat similar to the GitHub Workflow Command Logic. (Actually is was directly ported from the GitHub Workflow for the same Project)
As about Evaluation of Conditionals there are many undocumented issues in GNU make that require Bash Workarounds to achieve the goal.
As documented at:
Preprocessing Numerical Values for the Makefile Evaluation
There are issues with comparing Numbers and it is impossible to compare against 0 which is extremely important Exit Code for Command Line Applications.
So the Workaround looked somewhat like:
test: test_debug
# Run the Testsuite Application
# [...]
$(eval error_code := $(shell export HEAPTRC="log=test_heap.log" ; echo -n "" >./test_heap.log ; ${wrkdir}/tests_hash-lists.dbg.run 2>./tests_error.log 1>./tests_exec.log ; echo "$$?"))
#$(ECHO) Application Tests: Execution finished with [${error_code}]
#Application Tests Execution Report
# [...]
$(eval is_error := $(shell if [ "${error_code}" = "0" ]; then echo false ; else echo true ; fi ;))
ifeq (${is_error}, true)
#$(ECHO) Exit Code non cero: '${error_code}'
#$(ECHO) "Tests failed with Error [Code: '${error_code}']"
#exit ${error_code}
endif
In this use case ${error_code} is tested with a Bash Conditional to populate the Helper Variable ${is_error} with true or false which then can be checked in GNU make.
Discussion:
The test Target cannot just exit on error.
For troubleshooting a failed Automated Test it is crucial to see the Exit Code, the Error Message and the Heap Report.
I want to check whether exists files with a certain extension in a makefile, however this piece of code does not work:
ejecutar: $(OUTPUT) clean
ifeq (,$(wildcard *.dat))
./$(OUTPUT) < $(OUTPUT).dat >$(OUTPUT).txt
else
./$(OUTPUT) < $(OUTPUT).dat >$(OUTPUT).txt
The error is said to be in the ifeq line.
The stuff in the recipe should be shell script, not Makefile syntax. Anything that Make interprets gets expanded as the Makefile is being read, where you typically want your recipe to examine things as they are when that specific recipe is executed. (This is a common beginner FAQ.)
Checking whether a wildcard matches any files in shell script is surprisingly unobvious, too.
ejecutar: $(OUTPUT) clean
set -- *.dat \
; if [ -e "$$1" ]; then \
./$< < $<.dat >$<.txt; \
else \
./$< < $<.dat >$<.txt; \
fi
It's also weird that your then and else cases are identical, but I'm not judging.
I have the following lines in my makefile:
.PHONY : clean
clean:
#echo "Running Clean"
$(shell if [ -e exe ]; then rm exe; else echo "no files"; fi)
When I run:
make clean
I get the following output on the shell
Running Clean
no files
make: no: Command not found
Makefile:22: recipe for target 'clean' failed
make: *** [clean] Error 127
Any suggestions?
The problem is the use of $(shell ...). What you want is:
.PHONY : clean
clean:
#echo "Running Clean"
#if [ -e exe ]; then rm exe; else echo "no files"; fi
As far as an explanation of what's going wrong -- when you first run the clean target, make will expand all make variables and functions in the recipes before it starts running them -- because $(shell ...) only has one $, this is considered a make function. Make runs the command, which outputs no files to stdout, and replaces the call with that string, and then starts executing the recipes... So now make sees the following:
clean:
#echo "Running Clean"
no files
When it tries to run no files, due to the lack of a #, it echos the line to the screen, and then passes the command to the shell. Because the shell doesn't recognize the keyword no it outputs the error you're seeing. Make itself then fails because the shell returned an error.
Hey all I'm the same guy who asked this question but I found an answer right after I posted this, I think I'll leave this up (unless this is against stackoverflow etiquette) in case someone else has the same problems. My solution was echoing the string to stdout.
$(shell if [ -e exe ]; then rm exe; else echo "no files" >&2; fi)
I have a Makefile that does performs a task if it hasn't happened in the last hour. It does so like this:
HOUR_FROM_NOW = $(shell perl -e '($$s,$$m,$$h,$$d,$$M)=localtime(time()+3600); printf("%02d%02d%02d%02d\n",$$M+1,$$d,$$h,$$m);')
NOW_FILE = $(shell mkdir -p .make; touch .make/now; echo .make/now )
.PHONY: externals
externals: $(PROJECTS:%=.make/proj_%)
.make/proj_%: $(NOW_FILE)
$(MAKE) -s $(*F)
touch -t $(HOUR_FROM_NOW) $#
.PHONY: $(PROJECTS)
$(PROJECTS):
# do stuff, specifically, clone git-repo if not exists, else pull latest
That part works great, except that I now get warnings:
make: Warning: File `.make/proj' has modification time 3.5e+03 s in the future
make: Nothing to be done for `externals'.
make: warning: Clock skew detected. Your build may be incomplete.
Anyone know how to suppress those warnings? (Or to do a periodic task in a makefile)
Most versions of touch I have come across can do some date time maths which allows for setting the timestamp of a file directly via the --date option.
That and the fact that variables assigned with := are only "evaluated once" makes this a bit easier to read.
HOUR_AGO := .make/hour_ago
__UGLY := $(shell mkdir -p .make && touch --date='1hour ago' $(HOUR_AGO))
# The preceding line will be executed once
.make/proj_%: .make/hour_ago | .make
$(MAKE) -s $(*F)
#touch $#
.make:
mkdir -p $#
I'm using something very similar to this to periodically refresh login tokens.
Never would have thought of it if it wasn't for Dave's answer though.
The directory is created by specifying it as a order-only-prerequisite
I suspect that the + 3600 is at fault. What happens if you remove it?
I thought and thought, and then the stupid-obvious solution hit me ...
Instead of setting timestamps in the future with HOUR_FROM_NOW, I use the real time and compare with HOUR_AGO_FILE ...
HOUR_AGO = $(shell perl -e '($$s,$$m,$$h,$$d,$$M)=localtime(time()-3600); printf("%02d%02d%02d%02d\n",$$M+1,$$d,$$h,$$m);')
HOUR_AGO_FILE = $(shell mkdir -p .make; touch -t $(HOUR_AGO) .make/hour_ago; echo .make/hour_ago )
.PHONY: externals
externals: $(PROJECTS:%=.make/proj_%)
.make/proj_%: $(HOUR_AGO_FILE)
$(MAKE) -s $(*F)
#touch $#
I am very new to Makefiles, so I am probably not doing this the best way (your input is much appreciated, since I would like to learn how/why mine is bad). Anyway, here is my problem:
I have a Daemon that I wrote for a program of mine and I am trying to install it with the Makefile (target is "install"). What the "install" target is supposed to do is move the daemon binary to a location, then move the "service script" to either /etc/init.d/ or /etc/rc.d/ (since different distros have different folders...). Here is my makefile so far:
all:
#echo "Making Components"
#cd Daemon; make
#echo "Components Built"
install:
#echo "Installing Components"
#mkdir -p /usr/lib/
#cp Daemon/myprog_d /usr/lib/myprog_d
-#test -d /etc/init.d && cp Scripts/myprog /etc/init.d/
-#test -d /etc/rc.d && cp Scripts/myprog /etc/rc.d/
-#test ! -d /etc/init.d -a ! -d /etc/rc.d && echo " Warning: Couldn't install script. Manually install Scripts/myprog"
#mkdir -p /var/log/
#echo "Installed."
uninstall:
#echo "Uninstalling Components"
#./Scripts/myprog stop > /dev/null
#rm -f /usr/lib/myprog_d
#echo "Uninstall complete"
clean:
#echo "Cleaning Components"
#cd Daemon; make clean
#echo "Clean complete"
As you can see, the "install" target tests to see if those two directories exist and, if they do, copies the script into them (I haven't yet done it to "uninstall", don't worry).
My first question: Is this the right way to do this? The "all" and "clean" targets work (there is another makefile in "Daemon/", as you can deduce), but I want to know if there is a cleaner way of doing this.
Secondly, because the "test" function returns non-zero, I had to do "-" before it so the error would be ignored. Unfortunately, that results in the "make install" output being:
Installing Components
make: [install] Error 1 (ignored)
make: [install] Error 1 (ignored)
Installed.
Which is very ugly and probably not good practice. What can I do in this case? (I have tried both -# and #-, but # will not suppress the "ignored" output)
Sincerely,
Chris
I'd do it this way:
#if [ -d /etc/init.d ]; then cp Scripts/myprog /etc/init.d/ ; fi
#if [ -d /etc/rc.d ]; then cp Scripts/myprog /etc/rc.d/ ; fi
And I'm a little confused by your next line (-#test ! -d /etc/init.d -a !...) but you can probably do it the same way.
That takes care of the error messages, but if you wanted to keep the makefile as it is, you could suppress them by running make -s.