|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionSmall C/C++ applications with a couple of modules are easy to manage. Developers can recompile them easily by calling the compiler directly, passing source files as arguments. That is a simple approach. However, when a project gets too complex with many source files it becomes necessary to have a tool that allows the developer to manage the project. The tool I'm talking about is the make command. The make command is used not only to help a developer compile applications, it can be used whenever you want to produce output files from several input files. This article is not a full tutorial, it focuses on C applications and how to use the make command and makefile to build them. There is a zip file with many samples in a directory structure. The most important files in the samples are the makefiles not the C source code. You should download the samples file and unzip it with the unzip command or any other preferred tool. So, for a better understanding of this article:
Contents
. Make Tool: Syntax Overviewmake command syntax is: make [options] [target] You can type make --help to see all options make command supports. In this article an explanation of all those options are not in the scope. The main point is makefile structure and how it works. target is a tag (or name defined) present in makefile. It will be described later in this article. make requires a makefile that tells it how your application should be built. The makefile often resides in the same directory as other source files and it can have any name you want. For instance, if your makefile is called run.mk then to execute make command type: make -f run.mk -f option tells make command the makefile name that should be processed. There are also two special names that makes -f option not necessary: makefile and Makefile. If you run make not passing a file name it will look first for a file called makefile. If that does not exist it will look for a file called Makefile. If you have two files in your directory one called makefile and other called Makefile and type: make <enter> make command will process the file called makefile. In that case, you should use -f option if you want make command processes Makefile. 2. Basic Syntax of Makefiles
Figure 1: Makefile general syntax
A make file consists of a set of targets, dependencies and rules. A target most of time is a file to be created/updated. target depends upon a set of source files or even others targets described in Dependency List. Rules are the necessary commands to create the target file by using Dependency List. As you see in figure 1 each command in the Rules part must be on lines that start with a TAB character. Space issue errors. Also, a space at end of the rule line may cause make issues an error message. The makefile is read by make command which determines target files to be built by comparing the dates and times (timestamp) of source files in Dependency List. If any dependency has a changed timestamp since the last build make command will execute the rule associated with the target. 2.1 Testing sample1It is time to make a simple test. Source code contains a directory called sample1. There are four files:
so, we have: mkfile.r # This is a very simple makefile
app: app.c inc_a.h
cc -o app app.c
and mkfile.w # This is a very simple makefile
app: app.c
cc -o app app.c
Apparently the difference between them seems irrelevant but in certain cases it is relevant. First, let us check the makefile's part:
To demonstrate how they work let us try the following sequence of commands (Figure 2):
Figure 2: sample1 sequence of commands.
Now you see why mkfile.w is considered a bad or incomplete makefile. It does not take into consideration inc_a.h module because it is not described in the dependency list of app target. 2.2 Testing sample2sample2 is another example of simple makefile but this time there is more than one single target. Again, there are 2 makefiles: mkfile.r and mkfile.w to demonstrate the right and the wrong way to write a makefile. As you notice, the final executable (app target) is formed by 3 object files: main.o, mod_a.o and mod_b.o. Each one is a target with its source files that represent its dependency list. app target is the main target or the target that will result the main executable file. Notice app dependency list. They are names of others targets. Both makefiles are complete. The main difference is the order the targets are placed in the makefile. So, we have: mkfile.r app: main.o mod_a.o mod_b.o
cc -o app main.o mod_a.o mod_b.o
main.o: main.c inc_a.h inc_b.h
cc -c main.c
mod_a.o: mod_a.c inc_a.h
cc -c mod_a.c
mod_b.o: mod_b.c inc_b.h
cc -c mod_b.c
and mkfile.w main.o: main.c inc_a.h inc_b.h
cc -c main.c
mod_a.o: mod_a.c inc_a.h
cc -c mod_a.c
mod_b.o: mod_b.c inc_b.h
cc -c mod_b.c
app: main.o mod_a.o mod_b.o
cc -o app main.o mod_a.o mod_b.o
Let us try the following sequence of commands ( Figure 3
):
Figure 3: sample2 sequence of commands.
So, what is wrong with mkfile.w ? Well, technically nothing when you inform the main target (figure 3 - item 6). However, when you do not inform a target the make command reads makefile from the beginning to find the first target to process. In the mkfile.w case, that target is main.o. main.o target only says to make to build main.o from main.c, inc_a.h and inc_b.h - there is nothing more related to do. Make will not read the next target. Note: the first target read determines how make must interpret all other targets and which order it must follow during the building process. So, the first target should be the main target and it might relate to one or more secondary targets to perform the build. Let us see app target. It is placed in different lines in both makefiles but they have identical syntax in both. So, item 3 and item 6 of figure 3 will produce the same result:
3. Phony Targets, Macros and Special CharactersSometimes a target does not mean a file but it might represent an action to be performed. When a target is not related to a file it is called phony target. For instance: getobj:
mv obj/*.o . 2>/dev/null
getobj target move all files with .o extension from obj directory to current directory -- not a big deal. However, you should be asking yourself: "What if there is no file in obj ?" That is a good question. In that case, the mv command would return an error that would be passed to the make command. Note: make command default behavior is to abort the processing when an error is detected while executing commands in rules. Of course, there will be situations that the obj directory will be empty. How will you avoid the make command from aborting when an error happens? You can use a special character - (minus) preceding the mv command. Thus: getobj:
-mv obj/*.o . 2>/dev/null
- Tells the make to ignore errors. There is another special character: @ - Tells make not to print the command to standard output before executing. You can combine both always preceding the command: getobj:
-@mv obj/*.o . 2>/dev/null
There is a special phony target called all where you can group several main targets and phony targets. all phony target is often used to lead make command while reading makefile. For instance: all: getobj app install putobj
The make command will execute the targets in sequence: getobj, app, install and putobj. Another interesting feature, make command supports is the concept of MACRO in makefiles. We can define a MACRO by writing: MACRONAME=value
and access the value of MACRONAME by writing either $(MACRONAME) or ${MACRONAME}. For instance: EXECPATH=./bin
INCPATH=./include
OBJPATH=./obj
CC=cc
CFLAGS=-g -Wall -I$(INCPATH)
While executing, make replaces $(MACRONAME) with the appropriated definition. Now we know what phony targets and macros are we can move to the next sample. 3.1 Testing sample3sample3 has a bit more complicated makefile that uses macros, phony targets and special characters. Also, when you list the sample3 directory you can see 3 sub-directories:
The .c sources are kept along with makefile in the sample3 root. When the makefile file name is makefile then, there is no need to use -f option in the make command. Files are separated in directories to make this sample more realistic. makefile listing follows:
Figure 4: sample3 makefile listing.
Of course, line numbers do not exist in makefile. I use them here only to make it easier to read the source. So, we have:
You should also notice the use of special characters (- and @) preceding the commands in getobj, putobj, install and cleanall. As explained before, - tells make to continue processing even an error occurs and @ tells make not to print command before executing it. Note: On install target every line finishes with a "\" and "\" character must be the last character in the line (no spaces after it) otherwise make might issue the following error: line xxx: syntax error: unexpected end of file Where xxx is the line considering the beginning of the block. In fact, every time you want to group commands with \ it must be the last character in the line. Let us try the following sequence of commands (Figure 5):
Figure 5: sample3 sequence of commands.
4. Suffix Rules: Simplifying Makefiles SyntaxWhen your project gets complex with many source files it is not practical to create targets representing each one of those source files. For instance, if your project got 20 .c files to build a single executable and each source uses the same set of compiler flags in building then, there should be such a way you can instruct make command to perform the same command to all source files. The way I'm talking about is called Suffix Rules or rules based upon file extension. For instance, the following suffix rule: .c.o:
cc -c $<
tells the make command: given a target file with .o extension there should be a dependency file with .c extension (same name -- only extension changes) that can be build. Thus, a file main.c will produce a file main.o. Notice .c.o is not a target but two extensions (.c and .o). The syntax of a suffix rule is:
Figure 6: Suffix Rule syntax
The special character $< will be explained later in this article. 4.1 Testing sample4 - mkfile1Let us try sample4. There are some makefiles to test. The first one, mkfile1: .c.o:
cc -c $<
You see it only contains a suffix rule definition, nothing more. Let us try the following sequence of commands (Figure 7):
Figure 7: sample4 sequence of commands - mkfile1.
There is a point to be understood here. mkfile1 only defines a suffix rule, no targets. So, why main.c is compiled ? The make command processed main.o as if the following target was defined in mkfile1: main.o: main.c
cc -c main.c
So, the suffix rule .c.o tells the make command: "for each xxxxx.o target there must be a xxxxx.c dependency to build." If the command was: make -f mkfile1 mod_x.o
make would return an error because there is no mod_x.c in the directory.
4.2 More Special CharactersWhat is that $< defined in suffix rule? That means name of current dependency. In the case of .c.o suffix rule, $< is replaced by xxxxx.c file when rule is executed. There are others:
These next samples are going to show the use of those characters. 4.3 Testing sample4 - mkfile2mkfile2 shows other way to use suffix rules. Now it only renames file.txt to file.log: .SUFFIXES: .txt .log
.txt.log:
@echo "Converting " $< " to " $*.log
mv $< $*.log
The keyword .SUFFIXES: tells the make command what are the file extensions that are going to be used in the makefile. In case of mkfile2, they are .txt and .log. Some extensions like .c and .o are default and do not need to be defined with .SUFFIXES: keyword. Let us try the following sequence of commands (Figure 8):
Figure 8: sample4 sequence of commands - mkfile2.
4.4 Testing sample4 - mkfile3 and mkfile4So far we saw how to define suffix rules but we did not use them in a real situation. So, let us move to a more realistic scenario by using the C source code in the sample4 directory. First let us try mkfile3: .c.o:
@echo "Compiling" $< "..."
cc -c $<
app: main.o mod_a.o mod_b.o
@echo "Building target" $@ "..."
cc -o app main.o mod_a.o mod_b.o
You see a suffix rule at beginning and the app target. Let us try the following sequence of commands (Figure 9):
Figure 9: sample4 sequence of commands - mkfile3.
What went wrong at item 7? Well, there is nothing saying to make command that inc_a.h is a dependency of main.c or mod_a.c. A solution is to write the dependencies for each object target: .c.o: @echo "Compiling" $< "..." cc -c $< app: main.o mod_a.o mod_b.o @echo "Building target" $@ "..." cc -o app main.o mod_a.o mod_b.o main.o: inc_a.h inc_b.h mod_a.o: inc_a.h mod_b.o: inc_b.h You can edit and add the last three lines and try the sequence of commands of figure 9 again. Do not forget to remove objects before testing it again: rm -f *.o app
Well, add the dependencies for each module indicating the exact include file each one includes is good but not practical. Imagine your project with 50 .c and 30 .h. That is a lot of typing! A more practical solution can be seen in mkfile4: OBJS=main.o mod_a.o mod_b.o
.c.o:
@echo "Compiling" $< "..."
cc -c $<
app: main.o mod_a.o mod_b.o
@echo "Building target" $@ "..."
cc -o app main.o mod_a.o mod_b.o
$(OBJS): inc_a.h inc_b.h
Let us try the following sequence of commands and see what happens (Figure 10):
Figure 10: sample4 sequence of commands - mkfile4.
Why was mod_b.c recompiled at item 5 mkfile4 defines inc_a.h as a dependency of mod_b.c and it is not. I mean, inc_a.h is not included by mod_b.c. In fact, makefile tells the make command that inc_a.h and inc_b.h are dependencies of all modules. I did mkfile4 that way because it is more practical and that does not mean there is any error. You can separate headers by module if you want. Tip: When working on big projects I used to put only master header files as dependency. That means, those headers that are included by all (or most) modules.
5. A Makefile Calling Others MakefilesWhen you are working in a large project with many different parts such as libraries, DLLs, executables, it is a good idea to split them in a directory structure by keeping the source of each module in its own directory. Thus, each source directory might have its own makefile and it can be called by a master makefile. The master makefile is kept into root directory and it changes into each sub-directory to invoke the module's makefile. It sounds simple and it really is. But there is a trick you should know when you force the make command to change into other directory. For instance, let us test the simple makefile: target1:
@pwd
cd dir_test
@pwd
By invoking make command the result is:
Figure 11: changing current directory -wrong way.
The pwd command prints the current directory. In that case is the /root directory. Notice the second pwd command print the same directory even after cd dir_test has been executed. What that means? You should know that most shell commands like (cp, mv and so on) force make command:
In fact, make creates three different instances of shell to process each of those commands. cd dir_test was executed successfully only in second instance of the shell created by make. The solution is the following: target1:
(pwd;cd dir_test;pwd)
See the result:
Figure 12: changing current directory -right way.
The brackets ensure that all commands are processed by a single shell - make command starts only one shell to execute all three commands. So, you can imagine what would happen when you do not use brackets and your makefile was: target1:
cd dir_test
make
See the result:
Figure 13: Recursive make calling.
You see what happens when you try it without brackets? It is calling the same makefile recursively. For instance, make[37] means the 37th instance of make command. 5.1 Testing sample5sample5 demonstrates how to call others makefiles via a master makefile. When you list sample5 directory you find:
master makefile listing: COND1=`stat app 2>/dev/null | grep Modify`
COND2=`stat ./application/app 2>/dev/null | grep Modify`
all: buildall getexec
buildall:
@echo "****** Invoking tstlib/makefile"
(cd tstlib; $(MAKE))
@echo "****** Invoking application/makefile"
(cd application; $(MAKE))
getexec:
@if [ "$(COND1)" != "$(COND2)" ];\
then\
echo "Getting new app!";\
cp -p ./application/app . 2>/dev/null;\
chmod 700 app;\
else\
echo "Nothing done!";\
fi
cleanall:
-rm -f app
@echo "****** Invoking tstlib/makefile"
@(cd tstlib; $(MAKE) cleanall)
@echo "****** Invoking appl/makefile"
@(cd application; $(MAKE) cleanall)
It is not a big deal. 4 phony targets. all target starts calling buildall target and you see make command is commanded to enter and try to build two different projects. Notice the $(MAKE) macro that is replaced by make word. $(MAKE) macro is default and do not need to be defined. cleanall target is used indirectly to remove all objects and executables. Notice @ character preceding open brackets to force make not to print the commands. Let us try the following sequence of commands (Figure 14):
Figure 14: sample5 sequence of commands
6. ConclusionAs you saw through this article, make command is a powerful tool that can help you a lot in your projects. As I said, this article does not intend to be a tutorial just a reference of basic aspects of how to write makefiles. There are lots of information on internet about make command and makefiles. Also, there are books that covers others features not present in this article. Hope this helps. HistoryFirst version.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||