CSCI 265 makefile scratchpad:

designed as a rule-based system for automating sequences of instructions 
   needed to compile a collection of source code files into an executable

evolved into a highly flexible system for automating many tasks 
   (notably testing)

designed around ideas of targets you want to build,
   files those targets depend on (i.e. if the files have
   changes then the target needs to be rebuilt),
   and the commands needed to carry out the build


example: suppose myProgram.cpp uses code declared in someDefinitions.h, and anotherCodeFile.C these are compiled to create myExecutable using the command g++ myProgram.cpp anotherCodeFile.C -o myExecutable if any of the three files have changed, we need to rerun the compilation command to bring the executable up to date we can specify the rules for rebuilding myExecutable by creating a file named makefile, listing myExecutable as the target, listing the three files as dependencies, and providing the compilation command myExecutable: myProgram.cpp anotherCodeFile.C someDefinitions.h g++ myProgram.cpp anotherCodeFile.C -o myExecutable if, at the linux command line, we run the command make myExecutable the make utility will check the timestamps of the four files (myExecutable, myProgram.cpp, anotherCodeFile.C, someDefinitions.h) and if any of the three source code files have been altered since myExecutable was last updated then it runs the g++ command to bring myExecutable back up to date
partial compilation: rather than recompiling the whole thing at once, we can compile the two C++ files seperately, and link the two object files together as a final step: g++ -c myProgram.cpp -o myProgram.o g++ -c anotherCodeFile.C -o anotherCodeFile.o g++ myProgram.o anotherCodeFile.o -o myExecutable note the use of the .o suffix to indicate an object file, and the use of the -c option to tell the compiler that we are creating object files instead of full executables the reason for doing this is that, if only one of the two files change, we only need to recompile that one file (rather than both) then link the two of them together in systems with hundreds or thousands of files, recompiling only the necessary ones can save you a great deal of time building an executable our revised makefile will list the .o files as individual targets, specifying when they need to be revised and their update commands, as well as the revised compilation command for myExecutable myExecutable: myProgram.o anotherCodeFile.o someDefinitions.h g++ myProgram.o anotherCodeFile.o -o myExecutable myProgram.o: myProgram.cpp someDefinitions.h g++ -c myProgram.cpp anotherCodeFile.o: anotherCodeFile.cpp someDefinitions.h g++ -c anotherCodeFile.C if myProgram.cpp is altered then the top two g++ commands are run, (updating myProgram.o first, then myExecutable) if anotherCodeFile.C is altered then the first and third are run, (updating anotherCodeFile.o first, then myExecutable) and if someDefinitions.h is altered then all three are run (updating the .o files first, then myExecutable)
macros are used to create and use constants within the makefile, e.g. one to store which compiler we'll use, and another to store which compile time options we'll use this is usually done for much the same reasons one uses constants in languages like C++: to ensure there is only one place you need to make a change if a particular value is going to change (like which compiler you plan on using) Compiler = g++ CFlags = -Wall To insert the contents later in the makefile, use ${MacroName} where you want the value used, e.g. Compiler = g++ CFlags = -Wall myExecutable: myProgram.o anotherCodeFile.o someDefinitions.h ${Compiler} ${CFlags} myProgram.o anotherCodeFile.o -o myExecutable myProgram.o: myProgram.cpp someDefinitions.h ${Compiler} ${CFlags} -c myProgram.cpp anotherCodeFile.o: anotherCodeFile.cpp someDefinitions.h ${Compiler} ${CFlags} -c anotherCodeFile.C Note that you can override the variable value from the command line, e.g. make CFlags=-O myExecutable
Note that our variables here expand recursively, e.g. x=1 y=x z=y x=2 # at this point, $z evaluates to y, $($z) evaluates to x, and $($($z)) evaluates to 2 In GNU make you can also assign variable values using x := y which immediately assigns the current value, or x ?= y which only does the assignment if x does not already have a value
There are also a number of special variables available in rules $@ contains the name of the target $^ contains the entire prerequisite list for the current target $? contains the list of prereqs that are newer than the target $< contains the name of just the first prereq in the list ($@F) contains just the filename of the target (without the path) ($@D) contains just the path to the directory containing the target (no trailing /) ($ We can add comments to our makefile by prepending them with a #, e.g. # stuff on this line is a comment
You can also use the makefile to automate other tasks by creating a target that isn't associated with an actual file (since the file will never get created, make will always run the associated command) e.g. if I want 'make clean' to remove all the .o files I could add the target clean: rm -f myProgram.o anotherCodeFile.o These are referred to as "phony" targets, and we can explicity identify them as such to make, e.g.: .PHONY clean clean: rm -f myProgram.o anotherCodeFile.o If there were many object files, I could store them in a variable, e.g. ObjFiles = oneFile.o another.o stillAnother.o oneMore.o clean: rm -f ${ObjFiles}
By default, make echos all the executed commands to standard output, so one sees the command as well as the results We can turn this off by prepending the command with an @ symbol, often done with the echo command that prints a message on the screen @echo "Print something about what is going on here\n"
The -d option will display debug information about what your makefile is doing, e.g. make -d myExecutable
we can make multiple targets at once by listing them as dependencies of a higher order command, e.g. all: myExe1 anotherExecutable someOtherProg @echo "Updating a bunch of our executables"
we can use paths (relative or absolute) in addition to the names of targets and files, e.g. someDirectory/myExe: someDir/oneFile.cpp ../anotherDir/someFile.o g++ someDir/oneFile.cpp ../anotherDir/someFile.o -o someDirectory/myExe
You can have conditional statements in makefiles, using ifeq, else, and endif: ifeq(${Compiler},g++) @echo "g++ is the compiler" else @echo "g++ is not the compiler" endif In addition to ifeq there is also ifneq, ifdef, ifndef
loops can work off of lists or value ranges, e.g. (try with "make testloop1" or "make testloop2") MyList = W X Y Z N = 1 MaxValue = 10 testloop1: @for item in ${MyList} ; do \ echo $$item ; \ done testloop2: @for (( i=1; i<${MaxValue}; i++ )) ; do \ echo $$i ; \ done testloop3: @echo "enter lines of text, control-D to stop" ; \ @while read line ; do \ echo $$line ; \ done
you can read and use user input (though it is generally frowned upon in makefiles) echo "enter x"; \ read x; \ echo $$x
you can create pattern-based rules, e.g. for each target whose name ends in .o, make them depend on a file whose base name is the same, but with extension .c: %.o: %.c similarly you can assign variable values for targets with specific patterns, e.g. %.o: Flags = -O
We can use the wildcard function to get lists of matching filenames, e.g. $(wildcard src/*.cpp) We can also perform pattern substitution on lists of strings, e.g. $(patsubst src/%.c,obj/%.o,$(wildcard src/*.c))
With the 'touch' command, we can quickly alter the timestamp on a file without actually editing the file (e.g. to make a target 'out of date' or 'stale', especially useful when testing/debugging your makefiles) touch somefile
You can use the linux "test" command to check if a file or directory exists, e.g. test -f filename && echo "it exists" ; \ test -f filename || echo "no it doesnt" (exactly one of them will run, use -d for directories)
By default make (on our system) uses /bin/sh as the default shell for command processing. If you want to run a bash command you can either use bash -c "your command" or you can set bash as the shell for your target, e.g. mytarg: SHELL:=/bin/bash mytarg: # whatever it is you want to do ...
Make also supports automatic generation of dependencies, in .d files, which it can in turn use to determine which .o and executable files need to be recompiled if any source files change -- i.e. you don't have to fish through the #includes and build the dependency rules yourself. A simplistic sample makefile taking advantage of this is shown below
# auto-dependency generating makefile
#    - make generates dependency information in .d files (it creates),
#      which it then uses to track if/when it should recompile files
#      based on changes to the source code files
#    - limitations:
#      * hard-coded paths and file sets should be replaced
#        with appropriate variables or generated file lists

# identify all the source files and corresponding object files
Srcs := src/data.c src/myprog.c src/tester.c
Objs := obj/data.o obj/myprog.o obj/tester.o
Deps := dep/data.d dep/myprog.d dep/tester.d

# for each executable, we have manually specified which .o files it is built from
Prog := bin/myprogx
PObs := obj/data.o obj/myprog.o
Test := bin/testerx
TObs := obj/data.o obj/tester.o

# identify the compiler, flags, dependency, and output flags to use
#     -MMD generates the dependency info as a side effect of compilation
#     -MP adds a target for each prereq in the list
#     -MT sets the name of the target in the .d file
#       ($@ is the name of the current target)
#     -MF writes the specified file
CC = gcc
CFLAGS = -Wall -Wextra
DepFlags = -MT $@ -MMD -MP -MF dep/$*.d

# build all exes
all: $(Prog) $(Test)

# build rules for the executables
$(Prog): $(PObs)
	$(CC) $(CFLAGS) $(DepFlags) $(PObs) -o $@
$(Test): $(TObs)
	$(CC) $(CFLAGS) $(DepFlags) $(TObs) -o $@

# .o rules, builds each .o from corresponding .c, and generates a .d
#    $@ is the name of the current target,
#    $< is the name of the first dependency (the .c file here)
obj/%.o : src/%.c
obj/%.o : src/%.c dep/%.d hdr/%.h
	$(CC) $(CFLAGS) $(DepFlags) -c $< -o $@

# create a target for each dependency
$(Deps):

# include all the dependency files
#    (wildcard prevents failure on non-existent files)
include $(wildcard $(Deps))

# remove dependency, object, and executable files
.PHONY : clean
clean: ;
	rm -f $(Objs) $(Deps) $(Prog) $(Test)