Software Build Systems: Make
The first build tool this book examines in detail is Make . You've already seen a basic example of using Make in Chapter 2, "A Make-Based Build System," and many developers are familiar with this popular tool. This chapter explores the syntax of Make-based build systems in more detail and presents a number of practical use cases.
Created in 1977, Make has revolutionized the way software is compiled. For many years, Make was the only build tool available; new tools created since that time (such as Ant, CMake, and SCons) introduced themselves as a "Make replacement." There's certainly no ignoring the valuable contribution Make has provided to the software industry.
Central to Make's operation is the concept of a rule, providing all the interfile dependency information needed to compile a program. A developer must specify the name of a target file to be compiled, as well as all the input files for the compilation. In addition, the rule contains one or more shell commands that generate the target file from the source files.
As an example, the following rule indicates that myprog is a generated file that is created by running the gcc command with the prog.c and lib.c files as input.
myprog: prog.c lib.c gcc –o myprog prog.c lib.c
If either of the last-modified time stamps of prog.c and lib.c are more recent than the time stamp of myprog, Make assumes that the developer has modified the source files since myprog was last compiled. As a result, it reruns the gcc command to regenerate myprog from the latest source files.
The developer who writes a build description file (known as a makefile) must carefully specify the dependencies among all files in the system, along with all the intermediate steps. In a large system with thousands of source files and a large array of file types (such as executable programs, data files, and object files), the number of rules can be extensive.
Despite its age, Make is still the most commonly used tool for building software. A large percentage of C/C++ projects use Make as their build tool, particularly for UNIX/Linux environments and older Microsoft Windows systems. Because of this popularity, university courses continue to teach the theory and practice of Make to prepare students for future employment. In the software industry, many developers have heard only about Make, not about the alternative tools.
This chapter focuses on the GNU version of Make because of the large number of platforms it supports. Before GNU Make became popular, each operating system vendor provided its own version of the Make tool that accepted a slightly different syntax than the other variants. Naturally, this made constructing a multiplatform build system difficult. You'll take a look at other Make implementations at the end of the chapter.
As a reminder, the goal of this chapter is to give you an appreciation for the features and capabilities of the GNU Make tool. You won't examine the tool in too much detail, but by the end of this chapter, you'll have a better appreciation of how to use the GNU Make tool and how a makefile is written. If you plan to use GNU Make in your own build system, you should first refer to the tool's own documentation .
Because of the complexity of GNU Make, you might find some of the discussion challenging to follow if you don't already have experience writing a makefile. Make sure you've read and understood Chapter 2, or at least be prepared to work through the examples in great detail. Make-based build systems can be difficult to understand.
The GNU Make Programming Language
The GNU Make tool is controlled by a user-written program script, stored in a file named Makefile. GNU Make provides a comprehensive programming language and gives a makefile developer enough functionality to describe the build process. You might find it useful to view the GNU Make language as three distinct programming languages integrated into one, each playing a slightly different role.
The three sublanguages are as follows:
File dependencies: A rule-based syntax for describing the dependency relationships between files. A Make program is "executed" by matching disk filenames against the rules that generate them. Instead of sequentially executing rules, GNU Make performs a pattern-matching operation to decide which rule to evaluate next.
myprog: prog.c lib.c
Shell commands: A list of shell commands encapsulated within each rule, to be executed if the target of the rule is "out-of-date." As with any shell script, each command invokes a separate program, such as ls, cat, or gcc. Commands are executed in the order they're listed and can use shell metacharacters to control sequencing and I/O redirection.
cp myfile yourfile && cp myfile1 yourfile1 md5 < myfile >>yourfile touch yourfile.done
String processing: A language for manipulating GNU Make variables, such as treating them as a list of values. This language uses the functional programming paradigm in which each function is passed one or more string values as input and returns a single string value as the result. By combining different function calls, complex expressions can be evaluated.
VARS := $(sort $(filter srcs-% cflags-%, $(.VARIABLES)))
With this combination of programming styles, it's possible to construct any type of build system, no matter how complex. Let's start by looking at GNU Make's syntax and basic concepts. Later you'll examine how these can apply to real-world build scenarios.
Makefile Rules to Construct the Dependency Graph
To reiterate, a makefile consists of a number of rules, each describing how to generate a particular target file from one or more prerequisite input files. If the target file is out of date with respect to the input files, the sequence of shell commands is executed to bring it up-to-date. "Out-of-date" refers to the time stamp on the file being older than the files it was derived from. Therefore, the input files must have been changed more recently.
As you saw in Chapter 2, the following makefile is a simplistic way of translating the calculator program's dependency graph into code that GNU Make can understand.
1 calculator: add.o calc.o mult.o sub.o 2 gcc -g -o calculator add.o calc.o mult.o sub.o 3 4 add.o: add.c numbers.h 5 gcc -g -c add.c 6 7 calc.o: calc.c numbers.h 8 gcc -g -c calc.c 9 10 mult.o: mult.c numbers.h 11 gcc -g -c mult.c 12 13 sub.o: sub.c numbers.h 14 gcc -g -c sub.c
Keep in mind that GNU Make's rule-based language doesn't execute sequentially, as would a program written in a procedural language. Instead, the whole mechanism is based on matching target filenames against whichever rule happens to match the name. As you see later, the target of a rule (the left side) can also contain wildcards and variable names, so locating a matching rule is not always a simple matter.
Let's not go into too much detail quite yet; this chapter later examines GNU Make's pattern-matching and rule-searching algorithm. First you'll learn about the different rules you can create.
Makefile Rule Types
In addition to the simple rules you've seen so far, you can express dependencies in several other ways, making it easier for makefile developers to get their job done. GNU Make is a flexible and powerful language with a number of syntactical features for expressing the relationship between files. Consider some examples:
Rules with multiple targets: The previous example had a single target file on the left side of the rule. However, the following syntactical shortcut is also allowed:
file1.o file2.o: source1.c source2.c source3.c ... commands go here ...Of course, this works only if both targets have the same set of prerequisites and can be generated by the same list of shell commands.
Rules with no prerequisites: Sometimes you want to define a target that doesn't depend on any prerequisites. You can use this approach to define pseudotargets that don't relate to actual disk files. In the following example, you're defining the help target to display a synopsis of the commands the developer can use:
.PHONY: help help: @echo "Usage: make all ARCH=[i386|mips]" @echo " make clean"If the developer types gmake help, no file named help exists on the disk, and the shell commands shouldn't proceed to create that file. Additionally, the shell commands are executed every time the help target is invoked because no time stamp checking needs to be performed. Note the use of the PHONY directive to indicate that GNU Make should always execute the rule, even if somebody accidentally left a file named help sitting in the current directory.
Rules with patterns in the filename: As you probably noticed, the previous calculator example contained a lot of repetition. For every object file, such as add.o, there was a dependency on the corresponding C file, such as add.c. Because there were four different source files, you had four different rules that all looked similar. You can use wildcard characters as a shortcut to specify that the target and prerequisite filenames must match.
%.o: %.c ... commands go here ...This example matches any pair of files in which the target ends with .o and the prerequisite both ends with .c and also starts with the same sequence of characters (known as the stem). In other words, a file stem .o can be generated from the file stem .c by executing the list of shell commands. When first asked to build the calculator target, GNU Make determines that calc.o, add.o, mult.o, and sub.o must all be generated and that this rule is capable of doing so.
Rules that apply only to certain files: To make the pattern matching in rules more useful, it's also possible to state which files the pattern applies to. For example:
a.o b.o: %.o: %.c echo This rule is for a.o and b.o c.o d.o: %.o: %.c echo This rule is for c.o and d.oBy being more specific about the list of files, you can create more elaborate build systems. For example, you might want some object files to be compiled with an x86-target compiler, whereas other object files must be compiled with a MIPS compiler. Although you haven't explored GNU Make variables in detail, this feature is a lot more useful if you have a list of several hundred files stored in a single variable.
Multiple rules with the same target: It's often more useful to split the list of prerequisites for a target across multiple rules than to define them all on the same line.
chunk.o: chunk.c gcc –c chunk.c chunk.o: chunk.h list.h data.hIn this example, the rule states that chunk.o is generated from chunk.c, and a separate rule states that chunk.o has dependencies on several other C header files. Only one of these rules can contain a set of shell commands; the other rule simply contributes to the list of prerequisites.
If you're curious about these and other ways of writing makefile rules, study the GNU Make reference manual for more examples.
As with any other language, writing a nontrivial program without using variables is difficult. The examples seen so far in this chapter have used hard-coded file names, but that won't work in a large build system with hundreds of files. Let's now see how GNU Make variables can simplify a makefile.
GNU Make variables are similar to those in other programming languages, but they have a few unique behaviors of their own. The rules are listed here:
- Variables are given a value by an assignment statement, such as X := 5. As you'll see shortly, several types of assignment exist, each with their own semantics.
- Variable values are referenced using the syntax $(X).
- All variables are of string type, with the valuecontaining zero or more characters. No mechanism exists for declaring variables before they're used, so assigning to them for the first time creates the variable.
- Variables have global scope, which means that all assignments and references to the variable X (within a single makefile) refer to the same variable.
- Variable names can contain upper- and lowercase letters, numbers, and punctuation symbols such as @, ^, <, and >. To make them more visible, this book typically uses uppercase letters in the examples, but that's not a requirement.
To illustrate these rules, consider a simple example. You shouldn't see any real surprises in this code, although an unusual feature is that strings don't require quotation marks around them. Instead, they simply consume the remainder of the input line, with the exception of anything after the comment (#) character.
1 FIRST := Hello there 2 SECOND := World # comments go here 3 MESSAGE := $(FIRST) $(SECOND) 4 FILES := add.c sub.c mult.c 5 $(info $(MESSAGE) – The files are $(FILES))
The last line, containing the $(info ...) directive, displays the following message on the output:
Hello there World – The files are add.c sub.c mult.c
Although this example shows only one type of assignment statement, several actually exist, each with its own semantics:
- Immediate evaluation: This is the case you've already seen, using the := operator. The right side of the assignment is fully evaluated to a constant string and then assigned to the variable listed on the left side. Most modern programming languages use this type of immediate evaluation in their assignment statements.
Deferred evaluation: This second type of assignment, using = instead of := enables you to defer the evaluation of variables until they're actually used instead of immediately converting them to a constant string. Now look at a case in which a variable is defined in terms of other variables.
1 CC := gcc 2 CFLAGS := -g 3 CCOMP = $(CC) $(CFLAGS) # observe the use of = 4 $(info Compiler is $(CCOMP)) 5 CC := i386-linux-gcc 6 $(info Compiler is $(CCOMP))
Note that line 3 uses deferred assignment (the = sign). When you execute this makefile, the right side of line 3 isn't evaluated until the CCOMP variable is actually used (which, in this case, is on lines 4 and 6). Given that the CC variable is modified on line 5, the value of CCOMP changes when it's used the second time.
$ gmake Compiler is gcc –g Compiler is i386-linux-gcc -g
This feature might seem a little awkward, but the capability to define variables and then modify individual parts of the variable later can be useful. You'll see this again when you look at GNU Make's built-in rules.
Conditional assignment: In a third situation, you assign a value if the variable doesn't already have one.
1 CFLAGS := -g 2 CFLAGS ?= -O 3 $(info CFLAGS is $(CFLAGS))In this case, you supply a default value for CFLAGS (on line 2), which is used if the user hasn't already provided a value earlier in the program (on line 1 here). Although this is an oversimplified example, this feature is useful when you include one makefile from within another, where the parent makefile might or might not want to explicitly define the CFLAGS variable. If it chooses not to, the default value is used.
Now let's look at some of the variables and rules built into the tool, making it easier to construct a makefile.
Built-In Variables and Rules
GNU Make provides built-in rules and variables to address common build system requirements. First examine automatic variables, so named because their value depends on the context in which they're used. Unlike many other programming languages, GNU Make variable names can contain punctuation symbols such as @, <, and ^.
- $@: Contains the filename of the current rule's target. Instead of hard-coding the name of the target into the sequence of shell commands, you use $@ to have it automatically inserted. This is handy when the rule uses wildcards to match the name of the target file and there's no specific name to be hard-coded.
- $<: Represents the first prerequisite of a rule. As shown in the following example, you use $@ to represent the target of the rule (the object file you're generating), and you use $< to represent the first source file in the list. (In this case, only one source file is mentioned in the rule.)
%.o: %.c gcc –c –o $@ $<
- $^: Similar to $<, but it evaluates to the complete list of prerequisites in the rule, with spaces between them.
- $(@D): Evaluates to the directory containing the target of the rule. For example, if the target is /home/john/work/src.c, then $(@D) evaluates to /home/john/work. This is useful when you have a shell command such as mkdir that needs to manipulate the target file's directory.
- $(@F): Similar to $(@D), but evaluates to the base name of the target file. If the target is /home/john/work/src.c, then $(@F) evaluates to src.c.
Of course, many more variables are available in GNU Make, but they aren't all listed here.
In addition to variables, GNU Make provides built-in rules. These are used for compiling C, C++, Yacc, and Fortran code, among others. Invoking GNU Make with the –p command-line option (gmake –p) shows you the rules built into the system. Here's the built-in rule for C compilation.
1 COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) –c 2 OUTPUT_OPTION = -o $@ 3 %.o: %.c 4 $(COMPILE.c) $(OUTPUT_OPTION) $<
This fragment shows a wildcard rule (lines 3 and 4) for generating .o files from the correspondingly named .c files. The automatic variables $@ and $< represent the target and prerequisite of the rule, which could be any matching pair of filenames that end in .o or .c, respectively. Notice also that line 1 of this rule uses deferred evaluation (the = sign), permitting developers to add their own values for CC, CFLAGS, CPPFLAGS, and TARGET_ARCH later. In theory, each time this wildcard rule is used, it could be with a different combination of flags, as set by the makefile developer.
As you saw in Chapter 2, the calculator example can be rewritten to take advantage of this built-in C compilation rule.
1 calculator: add.o calc.o mult.o sub.o 2 gcc -g -o calculator add.o calc.o mult.o sub.o 3 4 add.o calc.o mult.o sub.o: numbers.h
That is, you can remove all the makefile rules that specify how to compile a C source file into an object file, because the implicit rule handles that case. To make the code even more readable, you can then define and reference a number of variables:
1 SRCS = add.c calc.c mult.c sub.c 2 PROG = calculator 3 CC = gcc 4 CFLAGS = -g 5 OBJS = $(SRCS:.c=.o) 6 7 $(PROG): $(OBJS) 8 $(CC) $(CFLAGS) -o $@ $^ 9 10 $(OBJS): numbers.h
Note that CC and CFLAGS (lines 3 and 4) are implicitly inserted into the built-in C compilation rule that you saw earlier because COMPILE.c used deferred evaluation.
Line 5 uses some clever syntax to set OBJS to the same value as the SRCS variable (defined on line 1) but with all the .c extensions changed to .o. As you know from programming experience, it's a bad idea to list all the filenames twice, so you instead define one variable in terms of the other.
Line 7 is still required to link the final executable, but this time you're making use of variables instead of hard-coding filenames. Note that CC and CFLAGS are the same variables used when compiling source files into object files. If you decide to change to a different compiler or add new compilation flags, only lines 3 and 4 need to be modified.
Finally, line 10 states that all object files depend on numbers.h. This is shorter than the previous version, in which all the object files had to be listed.
Data Structures and Functions
All of GNU Make's variables are of string type but this needn't stop you from representing other data types, such as numbers, lists, and structures. The key to storing complex data is to find a way to represent information as a sequence of space-separated words. GNU Make has plenty of features for manipulating variables in this form.
The following are some typical data structures you might find yourself using:
1 PROG_NAME := my-calculator 2 LIST_OF_SRCS := calc.c main.c math.h lib.c 3 COLORS := red FF0000 green 00FF00 blue 0000FF purple FF00FF 4 ORDERS := 100 green cups 200 blue plates
Line 1 is a standard variable assignment of a simple string, and you'll see this type of assignment in almost every makefile. Line 2 is a common way of expressing lists of things, although, obviously, the elements of the list can't contain spaces. This can be painful if you were planning to store C:\Program Files in a list.
Lines 3 and 4 demonstrate more complex data structures that you probably won't use as often. For the ORDERS variable, element 1 is the quantity, element 2 is the color, and element 3 is the item to purchase. The pattern repeats itself for each additional order item. As long as you have a mechanism for extracting specific items out of a list, you can treat this variable like a structured data type.
Consider some of the most common functions for dealing with strings:
- words: Given a list as input, returns the number of space-separated words in that list. In this example, $(NUM_FILES) evaluates to 4.
NUM_FILES := $(words $(LIST_OF_SRCS))
- word: Given a list, extracts the nth word from that list. The list is 1-based, so $(SECOND_FILE) evaluates to main.c.
SECOND_FILE := $(word 2, $(LIST_OF_SRCS))
- filter: Returns the words from a list, which match a specific pattern. A common use is to select a subset of files that match a specific filename pattern (such as all C source files).
C_SRCS := $(filter %.c, $(LIST_OF_SRCS))
- patsubst: For each word in a list, replaces any that match a specific pattern with a replacement pattern. The % character identifies the part of each word that remains unchanged (the stem). Note that the first comma must not be followed by a space character; otherwise, the replacement list ends up with two spaces between each word.
OBJECTS := $(patsubst %.c,%.o, $(C_SRCS))This example is similar to the $(C_SRCS:.c=.o) syntax you've already seen, with the resulting list being calc.o math.o lib.o.
- addprefix: For each word in a list, prepends an additional string. In the following example, you add the objs/ prefix to each element in the $(OBJECTS) list.
OBJ_LIST := $(addprefix objs/, $(OBJECTS))In this case, $(OBJ_LIST) evaluates to objs/calc.o objs/main.o objs/lib.o.
- foreach: Visits each word in a list and constructs a new list containing the "mapped" values. The mapping expression can consist of any combination of GNU Make function calls. The following example is identical to the addprefix case, in that you're constructing a new list in which all the filenames are mapped to the expression obj/$(file).
OBJ_LIST_2 := $(foreach file, $(OBJECTS),objs/$(file))
- dir/notdir: Given a file's pathname, returns the directory name component or the filename component.
DEFN_PATH := src/headers/idl/interface.idl DEFN_DIR := $(dir $(DEFN_PATH)) DEFN_BASENAME := $(notdir $(DEFN_PATH))In this case, $(DEFN_DIR) evaluates to src/headers/idl/ (including the final /) and $(DEFN_BASENAME) evaluates to interface.idl.
- shell: Executes a shell command and returns the command's output as a string. The following example demonstrates a nonportable way of determining the owner of the /etc/passwd file. This assumes that the third word in the output of the ls –l command is the name of the file's owner.
PASSWD_OWNER := $(word 3, $(shell ls -l /etc/passwd))
In addition to these functions, and the many other functions listed in the GNU Make documentation, certain language features are designed to keep GNU Make programs short and concise.
First, the concept of a macro enables you to associate a name with a complex GNU Make expression and to pass arguments into that expression. This enables you to write your own GNU Make functions, effectively extending the basic language. The following code defines a macro named file_size that returns the number of bytes in a file (again, this is nonportable). You use the $(1) syntax to reference the first parameter of the $(call) expression.
file_size = $(word 5, $(shell ls -l $(1))) PASSWD_SIZE := $(call file_size,/etc/passwd)
Another shortcut is to define a canned sequence of shell commands by using the define directive. When specifying the shell commands to be executed in GNU Make rule, you call upon that canned sequence instead of writing it out every time.
define start-banner @echo ============== @echo Starting build @echo ============== endef .PHONY: all all: $(start-banner) $(MAKE) -C lib1
These language features, and many more discussed in the GNU Make documentation, make it possible to construct powerful makefile-based build systems.
Understanding Program Flow
This discussion of the GNU Make programming language finishes with a study of how a GNU Make program flows—that is, in which sequence the makefile is scanned and interpreted, and in which order the various parts of the program are executed. You've seen many of GNU Make's language features, but you also need to understand how and when these features are called into action.
You'll explore three topics that are somewhat unrelated, except that they all deal with the flow of a GNU Make program:
Parsing a makefile: Parsing a makefile involves two main phases: reading the makefile to build the dependency graph and then executing the compilation commands. Recall that a makefile is essentially a text-based representation of the dependency graph, which itself is a mathematical structure showing the relationship between files.
Controlling the parsing process: GNU Make provides a number of features for controlling how you include a submakefile, or conditionally compile parts of the makefile.
Executing the rules: The rule execution algorithm decides the order in which rules are applied and the corresponding shell commands are executed.
Parsing a Makefile
For the first topic, consider what happens when a developer invokes the gmake command:
The makefile parsing phase: The makefile is parsed and validated, and the full dependency graph is constructed. All rules are scanned, all variable assignments are performed, and all variables and functions are evaluated. Any problems that occur in the definition of rules or the construction of the dependency graph are reported at this time.
The rule execution phase: When the entire dependency graph is in memory, GNU Make examines the time stamps on all the files to determine which files (if any) are out of date. If it finds any such targets, the appropriate shell commands are executed to bring those targets up-to-date. Any problems that occur within the shell commands are reported at this time.
Although in many cases you don't need to be aware of these phases, this next example illustrates the difference between the two. Again, keep in mind that variables are assigned in the first phase and shell commands are executed in the second phase.
1 X := Hello World 2 3 print: 4 echo X is $(X) 5 6 X := Goodbye
This example should seem straightforward, although you might be surprised to see the result of invoking the print target:
$ gmake print X is Goodbye
The reason is that line 4 (a shell command) is simply saved until the second phase, and $(X) is not evaluated at all. This means that the second assignment on line 6 dictates the value of $(X) to be used when the shell command is finally evaluated.
If you're going to become a makefile expert, it's important to feel comfortable with the operation of these two phases. Much of your build system's functionality can be implemented either by using GNU Make functions (processed during the first phase) or as part of a shell script (processed during the second phase). Also, when you need to debug your makefile problems, you must understand the distinction between the two phases because different problems arise at each point in time.
Controlling the Parsing Process
Next, you must consider some additional flow-control features in GNU Make that impact the execution of a GNU Make-based program.
File inclusion: Similar to how C and C++ use the #include directive, GNU Make enables you to read additional files as if they were part of the main makefile. Any rules and variables defined inside the included file are treated as if they're actually written inside the main file.
FILES := src1.c src2.c include prog.mk # content of prog.mk textually # inserted here src1.o src2.o: src.hAs you've seen, this approach can be used to include a framework file containing reusable sections of code. You'll see another practical case of file inclusion later in this chapter.
Conditional compilation: Similar to C/C++'s #ifdef directive, you can conditionally include or exclude parts of the makefile. This inclusion is done within the first phase of the makefile parsing, so the conditional expressions need to be pretty simple (instead of using shell commands).
CFLAGS := -DPATH="/usr/local" ifdef DEBUG CFLAGS += -g # debug case if DEBUG is defined else CFLAGS += -O # non-debug case if DEBUG not defined endif
Executing the Rules
Finally, let's examine the algorithm GNU Make uses to construct a dependency graph, and see how the execution of the makefile flows as a result. Consider the main steps (with some of the detail left out for convenience).
- The developer who invokes GNU Make (with the gmake shell command) must specify which target to build. This is typically the name of an executable program, although you can also create pseudotargets such as all or install that don't relate to actual disk files. If the developer doesn't state which target file to build, GNU Make attempts to build the first target listed in the makefile (such as calculator).
- If GNU Make locates a rule to generate the target file, it examines each of the prerequisites listed in that rule and treats them recursively as targets. This ensures that each file used as an input to a compilation tool is itself up-to-date. For example, before linking add.o and calc.o into the calculator executable program, GNU Make recursively searches for rules that have add.o or calc.o on the left side.
- If a rule is found for the target you're trying to satisfy (either the user-specified target or one that was found recursively), you have two options:
- If the target file for the rule doesn't yet exist (there's currently no disk file with that name), the rule's shell command sequence is executed and the file is created for the first time. This is often the case when you're compiling a completely fresh source tree and no object files have yet been created.
- On the other hand, if the target file already exists on the disk, the time stamp on each of the prerequisite files is examined to see if any are newer than the target file. If so, you proceed to regenerate the target, thereby making it newer than the input files.
- If step 3 fails, meaning that the makefile doesn't contain a suitable rule to generate a target file, you also have two options:
- If the target file exists on the disk (but there's no rule to regenerate it), GNU Make can only assume that this is a source file that was handwritten by the developer. This is where the rule recursion stops.
- If the target file doesn't exist on the disk, GNU Make aborts with an error and the build fails. GNU Make doesn't know how to regenerate the file, and because it doesn't already exist on disk, you can't proceed any further.
Throughout this process, GNU Make doesn't preserve any state between invocations and doesn't maintain a database of file time stamps. It determines whether a file has changed by comparing the time stamps between the target and its prerequisites. As you'll see in later chapters, build tools that record time stamps in a database can detect changes only by looking at that one file.
Although you've explored a number of GNU Make features, you need to learn more before you can create your own build system. The ultimate authority on GNU Make syntax and semantics is the online reference document ; this is fairly tough going for beginners, though, so you'll probably want to start with a more introductory guide . For more advanced best practices for using GNU Make, refer to  in References.
To simplify the construction of a makefile, consider using the GNU Make Standard Library  which adds an extra layer of language support for logical operators; manipulation of lists, strings, and sets; and basic arithmetic.
Now let's study how to use the GNU Make language to address common build system scenarios.