The thing with complex projects is that they very often require complex build scripts. The build script for a given project can be a mix of Bash, Perl, and Make scripts. It is often convenient to write a script that ensures that all the project’s dependencies are installed, of the right version, or that builds them if necessary.
We often also need much simpler things to be done, like generating a build number, from within the makefile script. If you use makefiles, you know just how fun they are to hack and you probably do the same as I do: minimally modify the same makefile you wrote back in 1995 (or “borrowed” from somewhere else.) In many cases, that makes perfect sense: it does just what it has to do. In this week’s post, I show how to interface (although minimally) Bash from Make.
Calling make from Bash isn’t a problem: make is a well behaved program like any other to which you can pass parameters and that returns appropriate errors codes for success and failure. The other way around isn’t seen as often, but is about as easy.
Let us suppose we want to generate a build number for the current build. Writing a Bash script that reads the content of a file, increments it, writes it back and returns it would go something like this:
#!/bin/bash #filename: buildnumber.sh if [ -e build.number.dat] then build_number=$((`cat build.number.dat`+1)) else build_number=1000 fi echo $build_number | tee build.number.dat
Invoking the script buildnumber.sh from the command like would simply print a number, starting at 1000, incrementing by 1 each time you invoke it. To call a shell script—or any other command—you use Make’s shell macro. The Make shell macro works pretty much as Bash’s back-quotes ` `: it calls the command enclosed and transforms its output (on stdout) into a string that is assignable to a variable. Adding buildnumber.sh to your Makefile could be performed as follows:
project: $(COMPILER) $(CFLAG) \ -D__BUILD_NUMBER__=$(shell buildnumber.sh) \ -c $(SRC) $(LINKER) $(LFLAG) $(LIBS) $(OBJECT) -o my_project
This will cause buildnumber.sh to be invoked every time you build your project: target. In this first example, buildnumber.sh doesn’t take any arguments, but you could pass it arguments if you needed to. The call would then become something like:
project: $(COMPILER) $(CFLAG) \ -D__BUILD_NUMBER__=$(shell buildnumber.sh $(VARS)) \ -c $(SRC) $(LINKER) $(LFLAG) $(LIBS) $(OBJECT) -o my_project
where VARS is some variable set in the Makefile environment.
This script has a major problem. It will increment the build number even if the compilation fails, which may or may not make sense for you. Let us modify the buildnumber.sh script to include a backup of the current build number:
#!/bin/bash #filename: buildnumber.sh if [ -e build.number.dat] then build_number=$((`cat build.number.dat`+1)) cp build.number.dat build.number.dat.saved else build_number=1000 fi echo $build_number | tee build.number.dat
This versions backups the current build number before overwriting it with the new build number. We must now modify the Makefile to rollback build numbers if build fails for some reason. The Makefile now contains:
project: if $(COMPILER) $(CFLAG) \ -D__BUILD_NUMBER__=$(shell buildnumber.sh) \ -c $(SRC) && \ $(LINKER) $(LFLAG) $(LIBS) $(OBJECT) -o my_project ; \ then \ echo "success!" ; \ else \ echo "failed!" ; \ cp build.number.dat.saved build.number.dat ; \ fi
The if in Make works a lot like Bash’s if construct. Commands return “false” if they return an error, so they can be combined using the && or || operators. In the above modification, if either the compilation or the linkage fails, it echoes failed!, but more importantly, it reverts the build number so that this failed build doesn’t cause the number to be incremented.