GNU Make
GNU Make manual
This next section should reassure those who saw the above gcc commands as being a cumbersome way to do things compared to using a graphical IDE. Fortunately, the utility GNU Make is here for that purpose and in fact it's general purpose enough that you could use it to manage any project, not necessarily code, that needs to update files based on the contents of certain other files changing.
When you run 'make', the utility is going to look for a makefile in the current directory. This could be named 'makefile', 'GNUmakefile', or 'Makefile', the latter being the most common (the capitalization is to group it with other special 'non-source' files like 'README', etc.). This file will inform make of the dependencies in your project, and the 'recipes' to update them.
Make Rules
We will start with a basic example:
This makefile divides into three 'rules', each indicated by a target file: all, runme, and clean. After 'target:' comes an optional list of dependencies or 'prerequisites'; if these files do not exist make will attempt to create them or find a rule to make each prerequisite. The indented lines below the rule headers constitute the 'recipe' used update the target indicated. Each of these lines will be issued in order in the shell. Keep in mind recipe lines have to be indented otherwise it is an error.$ nano Makefile $ cat Makefile all: runme runme: mymain.o mycode.o g++ -Wall -g -o runme mymain.o mycode.o clean: rm -f *.o rm -f runme $ make clean rm -f *.o rm -f runme $ make g++ -c -o mymain.o mymain.cc g++ -c -o mycode.o mycode.cc g++ -Wall -g -o runme mycode.o mymain.o $ ls Makefile mycode.cc mycode.hh mycode.o mymain.cc mymain.o README runme
In the above example, when the 'make' command is issued with no arguments, it defaults to the 'all' rule. Here, we specified prerequisite for 'all' as another target, 'runme'. Make will check to see if a file with that name already exists and whether it is up to date based on its dependencies. (If no file exists the recipe will always be run).
Looking at the rule for runme we have two prerequisites 'mymain.o' and 'mycode.o'. We did not specify rules for mymain.o, but make knows that a .o file requires the compilation of a source file (.c, .cpp, .cc etc.), and so looks and finds mymain.cc.
As a result of running make the first time, you can see that each object file was created with g++. However since we didn't define a rule for .o files, we don't get the same options we are using for the executable recipe (-Wall and -g).
When the prerequisite object files for runme are up to date, make proceeds to the recipe line immediately following which creates the file 'runme' as output with a g++ command.
See how make behaves when changes are made to the source files:
First is the result you see if everything is up to date. Next we updated the timestamp mymain.o manually. We didn't actually change any source code yet, but make sees this file is newer than runme and recompiles/links it. After that we update some source code, and when make runs it sees this and recompiles mycode.o (and leaves the mymain.o alone since it is up to date) and then recompiles the executable 'runme'.$ make make: Nothing to be done for `all'. $ touch mymain.o $ make g++ -Wall -g -o runme mycode.o mymain.o $ sudo nano mycode.cc $ make g++ -c -o mycode.o mycode.cc g++ -Wall -g -o runme mycode.o mymain.o
Since the rule 'all' only has the one prerequisite, it is the same as explicitly using the command 'make runme'. Because make checks for a file with a name matching the target, ie 'runme', it is important to output the file runme in the recipe to avoid recompiling: if the file isn't present, the recipe will always be run.
In light of the above, the third rule 'clean' is self explanatory. It is a 'phony' target that really is just a quick way to execute the recipe it specifies, without the need of any dependencies. The rm option '-f' ('force') is used to ignore the error generated when no file can be found to remove.
Note if there happens to be a file named 'clean' in the directory, nothing will be done. This is probably not going to be an issue but you can add the line '.PHONY: clean' somewhere above the rule definition in the makefile to make it always work.$ make clean rm -f *.o rm -f runme
make Variables
Using the above makefile, if we wanted to make changes to the executable name or the dependencies, we would have to make changes in more than one place for each change. To avoid this we can define variables and use those variables throughout the file:
The syntax '$(variable)' dereferences the variable to the value previously assigned. In the recipe line for our executable there are two automatic variables introduced: '$@' refers to the target of the rule, and '$^' to all the prerequisites. Using automatic variables is optional; in this case it works the same as if the line was:$ cat Makefile # define the variables CC = g++ CFLAGS = -Wall -g EXEC = runme OBJECTS = mymain.o mycode.o # define the rules all: $(EXEC) $(EXEC): $(OBJECTS) $(CC) $(CFLAGS) -o $@ $^ clean: rm -f *.o rm -f $(EXEC)
Note that it is conventional to match the flags variable to the utility variable, but it is a standard exception to see 'CFLAGS' instead of 'CCFLAGS'.$(CC) $(CFLAGS) -o $(EXEC) $(OBJECTS)
Pattern Rules
At this point we still haven't defined a rule for compiling object files from their sources; it's being handled automatically for us. To make our own we will use a 'pattern rule':
In a pattern rule, the (non-empty) part of the target filename corresponding to the stem character, '%', is substituted everywhere '%' appears in the prerequisites. This would fail if you were to specify a %.o file when a matching %.cc file did not already exist since make has no implicit rule for making such a file. Notice that a new automatic variable is used here: $< will only resolve to the first prerequisite if there is more than one provided.$ cat Makefile # define the variables CC = g++ CFLAGS = -Wall -g CPPFLAGS = LDFLAGS = EXEC = runme OBJECTS = mymain.o mycode.o # define the rules all: $(EXEC) $(EXEC): $(OBJECTS) $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $^ %.o: %.cc $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< clean: rm -f *.o rm -f $(EXEC)
You may encounter the syntax
which has the same meaning but is considered obsolete..cc.o: $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $<
Don't be thrown off by the extra variables CPPFLAGS, and LDFLAGS added to the makefile. When they are assigned to nothing, they will not affect anything, but they are there by convention to provide an easy way to specify Preprocessor and Linker options. You may have noticed that the '-c' option wasn't included in the CFLAGS variable. The make manual specifies that if an option is required for a recipe to work, it should appear explicitly in the recipe, rather than allowing the user to accidentally break things by changing the flags variable.
Substitution Reference, Pattern Substitution, and Wildcards
Wildcards are already used above in the clean target; '*.o' specifies all the files in the directory with extension '.o'. Be aware that if you try assigning a wildcard like this to a variable, the variable literally becomes that wildcard (*.o), not the expanded list of files. It will be expanded in some cases, but usually this will cause unforseen problems. Instead, use the wildcard function to assign variables:
This makefile will now automatically assign OBJECTS as a list of .o files based on the existing .cc source files in the directory. To change all the .cc extensions in SOURCES to .o extensions, we are using a syntax called 'substitution reference': $(variable:suffixa=suffixb). This form requires that these replacements are at the ends of the filenames. Alternatively, you can use patterns by including a '%' stem:$cat Makefile # define the variables CC = g++ CPPFLAGS = CFLAGS = -Wall -g LDFLAGS = EXEC = runme HEADERS = $(wildcard *.hh) SOURCES = $(wildcard *.cc) OBJECTS = $(SOURCES:.cc=.o) # define the rules all: $(EXEC) $(EXEC): $(OBJECTS) $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $^ %.o: %.cc $(HEADERS) $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< clean: rm -f *.o rm -f $(EXEC)
The result is the same as above. An alternative way of defining OBJECTS:OBJECTS = $(SOURCES:%.cc=%.o)
The function patsubst replaces the pattern of the first argument with the pattern in the second argument where it occurs in the text provided as a third argument: $(patsubst pattern,replacement,text). Note that a change to any header will result in a recompilation of all source files. The solution is to create explicit rules for source files and specify only the headers they include as prerequisites.OBJECTS = $(patsubst %.cc, %.o, $(wildcard *.cc))
Debug and Release Configurations
If you are working on a piece of code you may want to build with debug by default, but have a rule to compile a release version as well:
By default 'all' will build a debug version. In order to allow the possibility of debug and release versions in the same directory we specify the variable DOBJECTS as a list of filenames ending with .do to differentiate them from release object files. We also added a rule and a line to clean to handle these files. The '+=' is an append operator for variables. Here it is used in target-specific variable values. When we compile the debug output (DOUT), the debug option -g is appended to CFLAGS and a command-line definition DEBUG for the Preprocessor. Similarly for the release output (ROUT) we define NDEBUG (no debug) (a variable that is looked for by system headers such as$ cat Makefile # define the variables CC = g++ CFLAGS = -Wall CPPFLAGS = LDFLAGS = DOUT = runme_debug ROUT = runme HEADERS = $(wildcard *.hh ) SOURCES = $(wildcard *.cc ) DOBJECTS = $(SOURCES:.cc=.do) ROBJECTS = $(SOURCES:.cc=.o) # define the rules all: $(DOUT) release: $(ROUT) $(DOUT): CFLAGS += -g $(DOUT): CPPFLAGS += -DDEBUG=1 $(DOUT): $(DOBJECTS) $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $^ $(ROUT): CPPFLAGS += -DNDEBUG=1 $(ROUT): $(ROBJECTS) $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $^ %.o: %.cc $(HEADERS) $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< %.do: %.cc $(HEADERS) $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< debug: $(DOUT) ./$(DOUT) run: release ./$(ROUT) clean: rm -f *.o rm -f *.do rm -f $(DOUT) rm -f $(ROUT)
Because of the failed assertion on line 7 of mymain.cc, the debug version fails but the release version does not trigger the assertion (because NDEBUG was defined on the command-line which instructs the assert header to 'do nothing').$ cat mymain.cc #include#include #include "mycode.hh" int main(){ using namespace std; assert(0); cout << "Hello World\n"; cout << "mycode foo(): " << foo() << endl; return 0; } $ make clean rm -f *.o rm -f *.do rm -f runme_debug rm -f runme $ make g++ -c -DDEBUG=1 -Wall -g -o mycode.do mycode.cc g++ -c -DDEBUG=1 -Wall -g -o mymain.do mymain.cc g++ -Wall -g -o runme_debug mycode.do mymain.do $ make debug ./runme_debug runme_debug: mymain.cc:7: int main(): Assertion `0' failed. make: *** [debug] Aborted $ make release g++ -c -DNDEBUG=1 -Wall -o mycode.o mycode.cc g++ -c -DNDEBUG=1 -Wall -o mymain.o mymain.cc g++ -Wall -o runme mycode.o mymain.o $ make run ./runme Hello World mycode foo(): -11 $ ls Makefile mycode.cc mycode.do mycode.hh mycode.o mymain.cc mymain.do mymain.o README runme runme_debug
Directories & Order-only Prerequisites
Working from a single directory may not be feasible for a more complex build. Here we add a directory for source files (src/), and instruct the makefile to create separate directories for builds (build/debug/ and build/release/):
Four new variables are used throughout the makefile: SRC_DIR, BUILD_DIR, DEBUG_DIR, and RELEASE_DIR. First the proper directories are assigned to the debug and release targets (build/debug/runme_debug and build/release/runme). Next, in the wildcard function for HEADERS and SOURCES we specify the source directory. When creating the DOBJECTS and OBJECTS variables, new directory variables are used to specify source and object directories in the substitution reference formula. This gives us our lists of objects; one list for each directory. Note it is no longer necessary to use the non-standard '.do' extension to differentiate from release object files since they will be built in separate directories. No changes are required to be made to the DOUT and ROUT rules. For the object file rules, we now have to include the directories in the pattern substitution formulae for object files and source files to prevent errors. One last addition to the object file rules is the addition of order-only prerequisites. These rules for directory creation (added above in the makefile) will be executed if the directories don't exist, but using the syntax '|' to specify them as 'order-only' prerequisites means that if the directories time-stamp is updated it will not trigger a re-build of the targets like a normal prerequisite would (in this case the targets are the object files). The only other change has been to change the clean recipe to remove the build directory recursively.$ cat Makefile # define the variables SHELL = /bin/sh CC = g++ CFLAGS = -Wall CPPFLAGS = LDFLAGS = SRC_DIR = src/ BUILD_DIR = build/ DEBUG_DIR = $(BUILD_DIR)debug/ RELEASE_DIR = $(BUILD_DIR)release/ DOUT = $(DEBUG_DIR)runme_debug ROUT = $(RELEASE_DIR)runme HEADERS = $(wildcard $(SRC_DIR)*.hh ) SOURCES = $(wildcard $(SRC_DIR)*.cc ) DOBJECTS = $(SOURCES:$(SRC_DIR)%.cc=$(DEBUG_DIR)%.o) OBJECTS = $(SOURCES:$(SRC_DIR)%.cc=$(RELEASE_DIR)%.o) # define the rules all: $(DOUT) release: $(ROUT) $(DEBUG_DIR): mkdir -p $@ $(RELEASE_DIR): mkdir -p $@ $(DOUT): CFLAGS += -g $(DOUT): CPPFLAGS += -DDEBUG=1 $(DOUT): $(DOBJECTS) $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $^ $(ROUT): CPPFLAGS += -DNDEBUG=1 $(ROUT): $(OBJECTS) $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $^ $(DEBUG_DIR)%.o: $(SRC_DIR)%.cc $(HEADERS) | $(DEBUG_DIR) $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< $(RELEASE_DIR)%.o: $(SRC_DIR)%.cc $(HEADERS) | $(RELEASE_DIR) $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< debug: $(DOUT) ./$(DOUT) run: release ./$(ROUT) test: all valgrind $(DOUT) clean: rm -rf $(BUILD_DIR) spearman@darkstar ~/Documents/myproject $ ls Makefile README Sconstruct src spearman@darkstar ~/Documents/myproject $ make mkdir -p build/debug g++ -c -DDEBUG=1 -Wall -g -o build/debug/mycode.o src/mycode.cc g++ -c -DDEBUG=1 -Wall -g -o build/debug/mymain.o src/mymain.cc g++ -Wall -g -o build/debug/runme_debug build/debug/mycode.o build/debug/mymain.o spearman@darkstar ~/Documents/myproject $ ls Makefile README build src
Further Changes
At this point the makefile should work fairly well for small or medium sized C++ projects. Larger projects will benefit from defining headers explicitly for each source file to avoid re-building the entire project every time you edit a header file. In the next section an alternative utility called SCons will be introduced which can track these kinds of dependencies automatically. Part 4: SCons
No comments:
Post a Comment