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)
|