Let's say we have a single C source file foo.c:
The typical way to compile the file to an executable, foo is:
gcc -o foo foo.c
To be more precise, the above invocation of gcc invokes the preprocessor to process the #include, #define and other such directives, compiles foo.c to an object file, and then links this object file with a few small stub objects to produce the final executable, foo.
We can additionally specify a few flags to make the compiler stricter. A common set of flags is:
gcc -o foo -Wall -Wextra -Werror foo.c
Now, the compilation aborts, as the -Wall option causes the compiler to warn about unused variables, with the -Werror converting the warnings to errors:
gcc -o foo -Wall -Wextra -Werror foo.c
(out) foo.c: In function ‘main’:
(out) foo.c:16:10: error: unused parameter ‘argc’ [-Werror=unused-parameter]
(out) main(int argc, char *argv[])
(out) ^~~~
(out) foo.c:16:22: error: unused parameter ‘argv’ [-Werror=unused-parameter]
(out) main(int argc, char *argv[])
We can easily fix the warnings by adding (void)argc; (void)argv; at the start of the main function; the following is the corrected code: foo.c
In more detail, the common -W (warn) flags are:
If we repeatedly make changes to foo.c (or if we compile several source files as part of our executable), then manually invoking the above gcc command is tiresome and error-prone. Hence, Makefiles.
In makefile terminology, the line
foo: foo.c
is a rule, where
foo is the target and foo.c is the prerequisite.
Each rule then contains one or more commands, called the recipe (here,
the gcc -o foo ...
line), which specifies how to create the
target from the prerequisites. Note that each line (command) of the
recipe must start with a tab character.
When we enter the command make, make looks for a file named Makefile in the current directory. make then invokes a rule's recipe any time its target is out-of-date with respect to one of its prerequisites (in other words, any time we modify foo.c).
We can generalize our Makefile a little by using variables. A variable simply stores a string of text that we can use in our rules.
Here, CFLAGS
is a variable that
holds our compilation flags. We access the value of the variable by using the syntax
$(CFLAGS)
The $@
and $^
are
automatic variables
-- variables that make computes afresh for
each rule that is executed, based on the target and prerequisites of
the rule. Note that automatic variables can only be used
in the recipe portion of a rule.
We use two common automatic variables in our rule above:
It is good practice to include a special rule, conventionally called clean, to delete any build artifacts (in our running example, the only artifact is the executable that we build, foo).
As with any rule, we can explicitly invoke the clean rule by specifying the target's name as an argument to 'make': make clean. (By default, make without any arguments will try to build the first target in the Makefile (here, foo)).
Since there is no file named clean, make clean will always invoke the rule's recipe, hoping to create this very file, which of course, it never does. That's the trick.
However, there's a fly in the ointment: if by some strange chance we do have a file called clean in our directory, then make clean will never run; the rule has no prerequisites, and thus the clean file is always up-to-date.
To make our intention clear to make that clean is a fake rule, and not a file -- even if such a file called clean exists -- we add clean as a prerequisite of the special target, .PHONY. By doing so, make clean will run the recipe regardless of whether we have a file named clean.
Let's say that we now have the following files:
In other words, a and b are two programs, each of which uses foo and bar.
A simple Makefile for this project is:
Note that we create a PHONY target called all to represent both programs. While this Makefile is simple and easy to read, it has the drawback that foo.c and bar.c are compiled twice: once for a and once for b. For a small project, this is no big deal; for a large project, it can waste a lot of time.
Rather than recompile foo.c and bar.c twice, we can create rules for compiling these to their respective .o files:
Note the use of the automatic variable
$<
,
which represents a rule's first prerequisite.
This approach is more efficient, but there is a lot of redundancy in the rules. For a small number of source files, the redundancy is manageable; for larger projects, it becomes an obstacle to making changes.
To remove some of the redundancy, we can use static pattern rules. Per the manual, "Static pattern rules are rules which specify multiple targets and construct the prerequisite names for each target based on the target name." The general form is
targets ...: target-pattern: prereq-patterns ...:
recipe
...
Each target is matched against the target-pattern. The target-pattern uses the '%' character to capture a portion of the target name; this captured string is called the stem. The '%' character can then be used in the prereq-patterns to substitute the stem.
The following Makefile demonstrates the use of static pattern rules for
our project. This new Makefile at first appears cryptic, but becomes
easier to understand with practice. Also note the use of the variable
$(CC)
which is a
predefined variable
that expands to the system's default C compiler.
For our course, the above example Makefiles will usually suffice. However, if you are interested in learning more, the official GNU Make Manual is an outstanding reference and a fairly quick read.
The following books are also helpful:
and