# 第?22?章?Makefile基礎
**目錄**
+ [1\. 基本規則](ch22s01.html)
+ [2\. 隱含規則和模式規則](ch22s02.html)
+ [3\. 變量](ch22s03.html)
+ [4\. 自動處理頭文件的依賴關系](ch22s04.html)
+ [5\. 常用的`make`命令行選項](ch22s05.html)
## 1.?基本規則
除了Hello World這種極簡單的程序之外,一般的程序都是由多個源文件編譯鏈接而成的,這些源文件的處理步驟通常用Makefile來管理。Makefile起什么作用呢?我們先看一個例子,這個例子由[例?12.3 “用深度優先搜索解迷宮問題”](ch12s03.html#stackqueue.dfs)改寫而成:
```
/* main.c */
#include <stdio.h>
#include "main.h"
#include "stack.h"
#include "maze.h"
struct point predecessor[MAX_ROW][MAX_COL] = {
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
};
void visit(int row, int col, struct point pre)
{
struct point visit_point = { row, col };
maze[row][col] = 2;
predecessor[row][col] = pre;
push(visit_point);
}
int main(void)
{
struct point p = { 0, 0 };
maze[p.row][p.col] = 2;
push(p);
while (!is_empty()) {
p = pop();
if (p.row == MAX_ROW - 1 /* goal */
&& p.col == MAX_COL - 1)
break;
if (p.col+1 < MAX_COL /* right */
&& maze[p.row][p.col+1] == 0)
visit(p.row, p.col+1, p);
if (p.row+1 < MAX_ROW /* down */
&& maze[p.row+1][p.col] == 0)
visit(p.row+1, p.col, p);
if (p.col-1 >= 0 /* left */
&& maze[p.row][p.col-1] == 0)
visit(p.row, p.col-1, p);
if (p.row-1 >= 0 /* up */
&& maze[p.row-1][p.col] == 0)
visit(p.row-1, p.col, p);
print_maze();
}
if (p.row == MAX_ROW - 1 && p.col == MAX_COL - 1) {
printf("(%d, %d)\n", p.row, p.col);
while (predecessor[p.row][p.col].row != -1) {
p = predecessor[p.row][p.col];
printf("(%d, %d)\n", p.row, p.col);
}
} else
printf("No path!\n");
return 0;
}
```
我們把堆棧和迷宮的代碼分別轉移到模塊`stack.c`和`maze.c`中,`main.c`包含它們提供的頭文件`stack.h`和`maze.h`。
```
/* main.h */
#ifndef MAIN_H
#define MAIN_H
typedef struct point { int row, col; } item_t;
#define MAX_ROW 5
#define MAX_COL 5
#endif
```
在`main.h`中定義了一個類型和兩個常量,`main.c`、`stack.c`和`maze.c`都要用到這些定義,都要包含這個頭文件。
```
/* stack.c */
#include "stack.h"
static item_t stack[512];
static int top = 0;
void push(item_t p)
{
stack[top++] = p;
}
item_t pop(void)
{
return stack[--top];
}
int is_empty(void)
{
return top == 0;
}
```
```
/* stack.h */
#ifndef STACK_H
#define STACK_H
#include "main.h" /* provides definition for item_t */
extern void push(item_t);
extern item_t pop(void);
extern int is_empty(void);
#endif
```
[例?12.3 “用深度優先搜索解迷宮問題”](ch12s03.html#stackqueue.dfs)中的堆棧規定死了只能放`char`型數據,現在我們做進一步抽象,堆棧中放`item_t`類型的數據,`item_t`可以定義為任意類型,只要它能夠通過函數的參數和返回值傳遞并且支持賦值操作就行。這也是一種避免硬編碼的策略,`stack.c`中多次使用`item_t`類型,要改變它的定義只需改變`main.h`中的一行代碼。
```
/* maze.c */
#include <stdio.h>
#include "maze.h"
int maze[MAX_ROW][MAX_COL] = {
0, 1, 0, 0, 0,
0, 1, 0, 1, 0,
0, 0, 0, 0, 0,
0, 1, 1, 1, 0,
0, 0, 0, 1, 0,
};
void print_maze(void)
{
int i, j;
for (i = 0; i < MAX_ROW; i++) {
for (j = 0; j < MAX_COL; j++)
printf("%d ", maze[i][j]);
putchar('\n');
}
printf("*********\n");
}
```
```
/* maze.h */
#ifndef MAZE_H
#define MAZE_H
#include "main.h" /* provides defintion for MAX_ROW and MAX_COL */
extern int maze[MAX_ROW][MAX_COL];
void print_maze(void);
#endif
```
`maze.c`中定義了一個`maze`數組和一個`print_maze`函數,需要在頭文件`maze.h`中聲明,以便提供給`main.c`使用,注意`print_maze`的聲明可以不加`extern`,而`maze`的聲明必須加`extern`。
這些源文件可以這樣編譯:
```
$ gcc main.c stack.c maze.c -o main
```
但這不是個好辦法,如果編譯之后又對`maze.c`做了修改,又要把所有源文件編譯一遍,即使`main.c`、`stack.c`和那些頭文件都沒有修改也要跟著重新編譯。一個大型的軟件項目往往由上千個源文件組成,全部編譯一遍需要幾個小時,只改一個源文件就要求全部重新編譯肯定是不合理的。
這樣編譯也許更好一些:
```
$ gcc -c main.c
$ gcc -c stack.c
$ gcc -c maze.c
$ gcc main.o stack.o maze.o -o main
```
如果編譯之后又對`maze.c`做了修改,要重新編譯只需要做兩步:
```
$ gcc -c maze.c
$ gcc main.o stack.o maze.o -o main
```
這樣又有一個問題,每次編譯敲的命令都不一樣,很容易出錯,比如我修改了三個源文件,可能有一個忘了重新編譯,結果編譯完了修改沒生效,運行時出了Bug還滿世界找原因呢。更復雜的問題是,假如我改了`main.h`怎么辦?所有包含`main.h`的源文件都需要重新編譯,我得挨個找哪些源文件包含了`main.h`,有的還很不明顯,例如`stack.c`包含了`stack.h`,而后者包含了`main.h`。可見手動處理這些問題非常容易出錯,那有沒有自動的解決辦法呢?有,就是寫一個`Makefile`文件和源代碼放在同一個目錄下:
```
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main.o: main.c main.h stack.h maze.h
gcc -c main.c
stack.o: stack.c stack.h main.h
gcc -c stack.c
maze.o: maze.c maze.h main.h
gcc -c maze.c
```
然后在這個目錄下運行`make`編譯:
```
$ make
gcc -c main.c
gcc -c stack.c
gcc -c maze.c
gcc main.o stack.o maze.o -o main
```
`make`命令會自動讀取當前目錄下的`Makefile`文件<sup>[[33](#ftn.id2803212)]</sup>,完成相應的編譯步驟。Makefile由一組規則(Rule)組成,每條規則的格式是:
```
target ... : prerequisites ...
command1
command2
...
```
例如:
```
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
```
`main`是這條規則的目標(Target),`main.o`、`stack.o`和`maze.o`是這條規則的條件(Prerequisite)。目標和條件之間的關系是:_欲更新目標,必須首先更新它的所有條件;所有條件中只要有一個條件被更新了,目標也必須隨之被更新_。所謂“更新”就是執行一遍規則中的命令列表,命令列表中的每條命令必須以一個Tab開頭,注意不能是空格,Makefile的格式不像C語言的縮進那么隨意,對于Makefile中的每個以Tab開頭的命令,`make`會創建一個Shell進程去執行它。
對于上面這個例子,`make`執行如下步驟:
1. 嘗試更新Makefile中第一條規則的目標`main`,第一條規則的目標稱為缺省目標,只要缺省目標更新了就算完成任務了,其它工作都是為這個目的而做的。由于我們是第一次編譯,`main`文件還沒生成,顯然需要更新,但規則說必須先更新了`main.o`、`stack.o`和`maze.o`這三個條件,然后才能更新`main`。
2. 所以`make`會進一步查找以這三個條件為目標的規則,這些目標文件也沒有生成,也需要更新,所以執行相應的命令(`gcc -c main.c`、`gcc -c stack.c`和`gcc -c maze.c`)更新它們。
3. 最后執行`gcc main.o stack.o maze.o -o main`更新`main`。
如果沒有做任何改動,再次運行`make`:
```
$ make
make: `main' is up to date.
```
`make`會提示缺省目標已經是最新的了,不需要執行任何命令更新它。再做個實驗,如果修改了`maze.h`(比如加個無關痛癢的空格)再運行`make`:
```
$ make
gcc -c main.c
gcc -c maze.c
gcc main.o stack.o maze.o -o main
```
`make`會自動選擇那些受影響的源文件重新編譯,不受影響的源文件則不重新編譯,這是怎么做到的呢?
1. `make`仍然嘗試更新缺省目標,首先檢查目標`main`是否需要更新,這就要檢查三個條件`main.o`、`stack.o`和`maze.o`是否需要更新。
2. `make`會進一步查找以這三個條件為目標的規則,然后發現`main.o`和`maze.o`需要更新,因為它們都有一個條件是`maze.h`,而這個文件的修改時間比`main.o`和`maze.o`晚,所以執行相應的命令更新`main.o`和`maze.o`。
3. 既然`main`的三個條件中有兩個被更新過了,那么`main`也需要更新,所以執行命令`gcc main.o stack.o maze.o -o main`更新`main`。
現在總結一下Makefile的規則,請讀者結合上面的例子理解。如果一條規則的目標屬于以下情況之一,就稱為需要更新:
* 目標沒有生成。
* 某個條件需要更新。
* 某個條件的修改時間比目標晚。
在一條規則被執行之前,規則的條件可能處于以下三種狀態之一:
* 需要更新。能夠找到以該條件為目標的規則,并且該規則中目標需要更新。
* 不需要更新。能夠找到以該條件為目標的規則,但是該規則中目標不需要更新;或者不能找到以該條件為目標的規則,并且該條件已經生成。
* 錯誤。不能找到以該條件為目標的規則,并且該條件沒有生成。
執行一條規則A的步驟如下:
1. 檢查它的每個條件P:
* 如果P需要更新,就執行以P為目標的規則B。之后,無論是否生成文件P,都認為P已被更新。
* 如果找不到規則B,并且文件P已存在,表示P不需要更新。
* 如果找不到規則B,并且文件P不存在,則報錯退出。
2. 在檢查完規則A的所有條件后,檢查它的目標T,如果屬于以下情況之一,就執行它的命令列表:
* 文件T不存在。
* 文件T存在,但是某個條件的修改時間比它晚。
* 某個條件P已被更新(并不一定生成文件P)。
通常Makefile都會有一個`clean`規則,用于清除編譯過程中產生的二進制文件,保留源文件:
```
clean:
@echo "cleanning project"
-rm main *.o
@echo "clean completed"
```
把這條規則添加到我們的Makefile末尾,然后執行這條規則:
```
$ make clean
cleanning project
rm main *.o
clean completed
```
如果在`make`的命令行中指定一個目標(例如`clean`),則更新這個目標,如果不指定目標則更新Makefile中第一條規則的目標(缺省目標)。
和前面介紹的規則不同,`clean`目標不依賴于任何條件,并且執行它的命令列表不會生成`clean`這個文件,剛才說過,只要執行了命令列表就算更新了目標,即使目標并沒有生成也算。在這個例子還演示了命令前面加`@`和`-`字符的效果:如果`make`執行的命令前面加了`@`字符,則不顯示命令本身而只顯示它的結果;通常`make`執行的命令如果出錯(該命令的退出狀態非0)就立刻終止,不再執行后續命令,但如果命令前面加了`-`號,即使這條命令出錯,`make`也會繼續執行后續命令。通常`rm`命令和`mkdir`命令前面要加`-`號,因為`rm`要刪除的文件可能不存在,`mkdir`要創建的目錄可能已存在,這兩個命令都有可能出錯,但這種錯誤是應該忽略的。例如上面已經執行過一遍`make clean`,再執行一遍就沒有文件可刪了,這時`rm`會報錯,但`make`忽略這一錯誤,繼續執行后面的`echo`命令:
```
$ make clean
cleanning project
rm main *.o
rm: cannot remove `main': No such file or directory
rm: cannot remove `*.o': No such file or directory
make: [clean] Error 1 (ignored)
clean completed
```
讀者可以把命令前面的`@`和`-`去掉再試試,對比一下結果有何不同。這里還有一個問題,如果當前目錄下存在一個文件叫`clean`會怎么樣呢?
```
$ touch clean
$ make clean
make: `clean' is up to date.
```
如果存在`clean`這個文件,`clean`目標又不依賴于任何條件,`make`就認為它不需要更新了。而我們希望把`clean`當作一個特殊的名字使用,不管它存在不存在都要更新,可以添一條特殊規則,把`clean`聲明為一個偽目標:
```
.PHONY: clean
```
這條規則沒有命令列表。類似`.PHONY`這種`make`內建的特殊目標還有很多,各有不同的用途,詳見[[GNUmake]](bi01.html#bibli.make "Managing Projects with GNU make")。在C語言中要求變量和函數先聲明后使用,而Makefile不太一樣,這條規則寫在`clean:`規則的后面也行,也能起到聲明`clean`是偽目標的作用:
```
clean:
@echo "cleanning project"
-rm main *.o
@echo "clean completed"
.PHONY: clean
```
當然寫在前面也行。`gcc`處理一個C程序分為預處理和編譯兩個階段,類似地,`make`處理Makefile的過程也分為兩個階段:
1. 首先從前到后讀取所有規則,建立起一個完整的依賴關系圖,例如:
**圖?22.1.?Makefile的依賴關系圖**

2. 然后從缺省目標或者命令行指定的目標開始,根據依賴關系圖選擇適當的規則執行,執行Makefile中的規則和執行C代碼不一樣,并不是從前到后按順序執行,也不是所有規則都要執行一遍,例如`make`缺省目標時不會更新`clean`目標,因為從上圖可以看出,它跟缺省目標沒有任何依賴關系。
`clean`目標是一個約定俗成的名字,在所有軟件項目的Makefile中都表示清除編譯生成的文件,類似這樣的約定俗成的目標名字有:
* `all`,執行主要的編譯工作,通常用作缺省目標。
* `install`,執行編譯后的安裝工作,把可執行文件、配置文件、文檔等分別拷到不同的安裝目錄。
* `clean`,刪除編譯生成的二進制文件。
* `distclean`,不僅刪除編譯生成的二進制文件,也刪除其它生成的文件,例如配置文件和格式轉換后的文檔,執行`make distclean`之后應該清除所有這些文件,只留下源文件。
* * *
<sup>[[33](#id2803212)]</sup> 只要符合本章所描述的語法的文件我們都叫它Makefile,而它的文件名則不一定是`Makefile`。事實上,執行`make`命令時,是按照`GNUmakefile`、`makefile`、`Makefile`的順序找到第一個存在的文件并執行它,不過還是建議使用`Makefile`做文件名。除了GNU `make`,有些UNIX系統的`make`命令不是GNU `make`,不會查找`GNUmakefile`這個文件名,如果你寫的Makefile包含GNU `make`的特殊語法,可以起名為`GNUmakefile`,否則不建議用這個文件名。
## 2.?隱含規則和模式規則
上一節的Makefile寫得中規中矩,比較繁瑣,是為了講清楚基本概念,其實Makefile有很多靈活的寫法,可以寫得更簡潔,同時減少出錯的可能。本節我們來看看這樣一個例子還有哪些改進的余地。
一個目標依賴的所有條件不一定非得寫在一條規則中,也可以拆開寫,例如:
```
main.o: main.h stack.h maze.h
main.o: main.c
gcc -c main.c
```
就相當于:
```
main.o: main.c main.h stack.h maze.h
gcc -c main.c
```
如果一個目標拆開寫多條規則,其中只有一條規則允許有命令列表,其它規則應該沒有命令列表,否則`make`會報警告并且采用最后一條規則的命令列表。
這樣我們的例子可以改寫成:
```
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main.o: main.h stack.h maze.h
stack.o: stack.h main.h
maze.o: maze.h main.h
main.o: main.c
gcc -c main.c
stack.o: stack.c
gcc -c stack.c
maze.o: maze.c
gcc -c maze.c
clean:
-rm main *.o
.PHONY: clean
```
這不是比原來更繁瑣了嗎?現在可以把提出來的三條規則刪去,寫成:
```
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main.o: main.h stack.h maze.h
stack.o: stack.h main.h
maze.o: maze.h main.h
clean:
-rm main *.o
.PHONY: clean
```
這就比原來簡單多了。可是現在`main.o`、`stack.o`和`maze.o`這三個目標連編譯命令都沒有了,怎么編譯的呢?試試看:
```
$ make
cc -c -o main.o main.c
cc -c -o stack.o stack.c
cc -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main
```
現在解釋一下前三條編譯命令是怎么來。如果一個目標在Makefile中的所有規則都沒有命令列表,`make`會嘗試在內建的隱含規則(Implicit Rule)數據庫中查找適用的規則。`make`的隱含規則數據庫可以用`make -p`命令打印,打印出來的格式也是Makefile的格式,包括很多變量和規則,其中和我們這個例子有關的隱含規則有:
```
# default
OUTPUT_OPTION = -o $@
# default
CC = cc
# default
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
%.o: %.c
# commands to execute (built-in):
$(COMPILE.c) $(OUTPUT_OPTION) $<
```
`#`號在Makefile中表示單行注釋,就像C語言的`//`注釋一樣。`CC`是一個Makefile變量,用`CC = cc`定義和賦值,用`$(CC)`取它的值,其值應該是`cc`。Makefile變量像C的宏定義一樣,代表一串字符,在取值的地方展開。`cc`是一個符號鏈接,通常指向`gcc`,在有些UNIX系統上可能指向另外一種C編譯器。
```
$ which cc
/usr/bin/cc
$ ls -l /usr/bin/cc
lrwxrwxrwx 1 root root 20 2008-07-04 05:59 /usr/bin/cc -> /etc/alternatives/cc
$ ls -l /etc/alternatives/cc
lrwxrwxrwx 1 root root 12 2008-11-01 09:10 /etc/alternatives/cc -> /usr/bin/gcc
```
`CFLAGS`這個變量沒有定義,`$(CFLAGS)`展開是空,`CPPFLAGS`和`TARGET_ARCH`也是如此。這樣`$(COMPILE.c)`展開應該是`cc?空?空?空?-c`,去掉所有的“空”得到`cc????-c`,注意中間留下4個空格,所以`%.o: %.c`規則的命令`$(COMPILE.c)?$(OUTPUT_OPTION)?$<`展開之后是`cc????-c?-o?$@?$<`,和上面的編譯命令已經很接近了。
`$@`和`$<`是兩個特殊的變量,`$@`的取值為規則中的目標,`$<`的取值為規則中的第一個條件。`%.o: %.c`是一種特殊的規則,稱為模式規則(Pattern Rule)。現在回顧一下整個過程,在我們的Makefile中以`main.o`為目標的規則都沒有命令列表,所以`make`會查找隱含規則,發現隱含規則中有這樣一條模式規則適用,`main.o`符合`%.o`的模式,現在`%`就代表`main`(稱為`main.o`這個名字的Stem),再替換到`%.c`中就是`main.c`。所以這條模式規則相當于:
```
main.o: main.c
cc -c -o main.o main.c
```
隨后,在處理`stack.o`目標時又用到這條模式規則,這時又相當于:
```
stack.o: stack.c
cc -c -o stack.o stack.c
```
`maze.o`也同樣處理。這三條規則可以由`make`的隱含規則推導出來,所以不必寫在Makefile中。
先前我們寫Makefile都是以目標為中心,一個目標依賴于若干條件,現在換個角度,以條件為中心,Makefile還可以這么寫:
```
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main.o stack.o maze.o: main.h
main.o maze.o: maze.h
main.o stack.o: stack.h
clean:
-rm main *.o
.PHONY: clean
```
我們知道,寫規則的目的是讓`make`建立依賴關系圖,不管怎么寫,只要把所有的依賴關系都描述清楚了就行。對于多目標的規則,`make`會拆成幾條單目標的規則來處理,例如
```
target1 target2: prerequisite1 prerequisite2
command $< -o $@
```
這樣一條規則相當于:
```
target1: prerequisite1 prerequisite2
command prerequisite1 -o target1
target2: prerequisite1 prerequisite2
command prerequisite1 -o target2
```
注意兩條規則的命令列表是一樣的,但`$@`的取值不同。
## 3.?變量
這一節我們詳細看看Makefile中關于變量的語法規則。先看一個簡單的例子:
```
foo = $(bar)
bar = Huh?
all:
@echo $(foo)
```
我們執行`make`將會打出`Huh?`。當`make`讀到`foo = $(bar)`時,確定`foo`的值是`$(bar)`,但并不立即展開`$(bar)`,然后讀到`bar = Huh?`,確定`bar`的值是`Huh?`,然后在執行規則`all:`的命令列表時才需要展開`$(foo)`,得到`$(bar)`,再展開`$(bar)`,得到`Huh?`。因此,雖然`bar`的定義寫在`foo`之后,`$(foo)`展開還是能夠取到`$(bar)`的值。
這種特性有好處也有壞處。好處是我們可以把變量的值推遲到后面定義,例如:
```
main.o: main.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c $<
CC = gcc
CFLAGS = -O -g
CPPFLAGS = -Iinclude
```
編譯命令可以展開成`gcc -O -g -Iinclude -c main.c`。通常把`CFLAGS`定義成一些編譯選項,例如`-O`、`-g`等,而把`CPPFLAGS`定義成一些預處理選項,例如`-D`、`-I`等。用`=`號定義變量的延遲展開特性也有壞處,就是有可能寫出無窮遞歸的定義,例如`CFLAGS = $(CFLAGS) -O`,或者:
```
A = $(B)
B = $(A)
```
當然,`make`有能力檢測出這樣的錯誤而不會陷入死循環。有時候我們希望`make`在遇到變量定義時立即展開,可以用`:=`運算符,例如:
```
x := foo
y := $(x) bar
all:
@echo "-$(y)-"
```
當`make`讀到`y := $(x) bar`定義時,立即把`$(x)`展開,使變量`y`的取值是`foo bar`,如果把這兩行顛倒過來:
```
y := $(x) bar
x := foo
```
那么當`make`讀到`y := $(x) bar`時,`x`還沒有定義,展開為空值,所以`y`的取值是`?bar`,注意`bar`前面有個空格。一個變量的定義從`=`后面的第一個非空白字符開始(從`$(x)`的`$`開始),包括后面的所有字符,直到注釋或換行之前結束。如果要定義一個變量的值是一個空格,可以這樣:
```
nullstring :=
space := $(nullstring) # end of the line
```
`nullstring`的值為空,`space`的值是一個空格,后面寫個注釋是為了增加可讀性,如果不寫注釋就換行,則很難看出`$(nullstring)`后面有個空格。
還有一個比較有用的賦值運算符是`?=`,例如`foo ?= $(bar)`的意思是:如果`foo`沒有定義過,那么`?=`相當于`=`,定義`foo`的值是`$(bar)`,但不立即展開;如果先前已經定義了`foo`,則什么也不做,不會給`foo`重新賦值。
`+=`運算符可以給變量追加值,例如:
```
objects = main.o
objects += $(foo)
foo = foo.o bar.o
```
`object`是用`=`定義的,`+=`仍然保持`=`的特性,`objects`的值是`main.o $(foo)`(注意`$(foo)`前面自動添一個空格),但不立即展開,等到后面需要展開`$(objects)`時會展開成`main.o foo.o bar.o`。
再比如:
```
objects := main.o
objects += $(foo)
foo = foo.o bar.o
```
`object`是用`:=`定義的,`+=`保持`:=`的特性,`objects`的值是`main.o $(foo)`,立即展開得到`main.o` (這時`foo`還沒定義),注意`main.o`后面的空格仍保留。
如果變量還沒有定義過就直接用`+=`賦值,那么`+=`相當于`=`。
上一節我們用到了特殊變量`$@`和`$<`,這兩個變量的特點是不需要給它們賦值,在不同的上下文中它們自動取不同的值。常用的特殊變量有:
* `$@`,表示規則中的目標。
* `$<`,表示規則中的第一個條件。
* `$?`,表示規則中所有比目標新的條件,組成一個列表,以空格分隔。
* `$^`,表示規則中的所有條件,組成一個列表,以空格分隔。
例如前面寫過的這條規則:
```
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
```
可以改寫成:
```
main: main.o stack.o maze.o
gcc $^ -o $@
```
這樣即使以后又往條件里添加了新的目標文件,編譯命令也不需要修改,減少了出錯的可能。
`$?`變量也很有用,有時候希望只對更新過的條件進行操作,例如有一個庫文件`libsome.a`依賴于幾個目標文件:
```
libsome.a: foo.o bar.o lose.o win.o
ar r libsome.a $?
ranlib libsome.a
```
這樣,只有更新過的目標文件才需要重新打包到`libsome.a`中,沒更新過的目標文件原本已經在`libsome.a`中了,不必重新打包。
在上一節我們看到`make`的隱含規則數據庫中用到了很多變量,有些變量沒有定義(例如`CFLAGS`),有些變量定義了缺省值(例如`CC`),我們寫Makefile時可以重新定義這些變量的值,也可以在缺省值的基礎上追加。以下列舉一些常用的變量,請讀者體會其中的規律。
`AR`
靜態庫打包命令的名字,缺省值是`ar`。
`ARFLAGS`
靜態庫打包命令的選項,缺省值是`rv`。
`AS`
匯編器的名字,缺省值是`as`。
`ASFLAGS`
匯編器的選項,沒有定義。
CC
C編譯器的名字,缺省值是`cc`。
CFLAGS
C編譯器的選項,沒有定義。
CXX
C++編譯器的名字,缺省值是`g++`。
CXXFLAGS
C++編譯器的選項,沒有定義。
CPP
C預處理器的名字,缺省值是`$(CC) -E`。
CPPFLAGS
C預處理器的選項,沒有定義。
LD
鏈接器的名字,缺省值是`ld`。
LDFLAGS
鏈接器的選項,沒有定義。
TARGET_ARCH
和目標平臺相關的命令行選項,沒有定義。
OUTPUT_OPTION
輸出的命令行選項,缺省值是`-o $@`。
LINK.o
把`.o`文件鏈接在一起的命令行,缺省值是`$(CC) $(LDFLAGS) $(TARGET_ARCH)`。
LINK.c
把`.c`文件鏈接在一起的命令行,缺省值是`$(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)`。
LINK.cc
把`.cc`文件(C++源文件)鏈接在一起的命令行,缺省值是`$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)`。
COMPILE.c
編譯`.c`文件的命令行,缺省值是`$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c`。
COMPILE.cc
編譯`.cc`文件的命令行,缺省值是`$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c`。
RM
刪除命令的名字,缺省值是`rm -f`。
## 4.?自動處理頭文件的依賴關系
現在我們的Makefile寫成這樣:
```
all: main
main: main.o stack.o maze.o
gcc $^ -o $@
main.o: main.h stack.h maze.h
stack.o: stack.h main.h
maze.o: maze.h main.h
clean:
-rm main *.o
.PHONY: clean
```
按照慣例,用`all`做缺省目標。現在還有一點比較麻煩,在寫`main.o`、`stack.o`和`maze.o`這三個目標的規則時要查看源代碼,找出它們依賴于哪些頭文件,這很容易出錯,一是因為有的頭文件包含在另一個頭文件中,在寫規則時很容易遺漏,二是如果以后修改源代碼改變了依賴關系,很可能忘記修改Makefile的規則。為了解決這個問題,可以用`gcc`的`-M`選項自動生成目標文件和源文件的依賴關系:
```
$ gcc -M main.c
main.o: main.c /usr/include/stdio.h /usr/include/features.h \
/usr/include/sys/cdefs.h /usr/include/bits/wordsize.h \
/usr/include/gnu/stubs.h /usr/include/gnu/stubs-32.h \
/usr/lib/gcc/i486-linux-gnu/4.3.2/include/stddef.h \
/usr/include/bits/types.h /usr/include/bits/typesizes.h \
/usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \
/usr/lib/gcc/i486-linux-gnu/4.3.2/include/stdarg.h \
/usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h main.h \
stack.h maze.h
```
`-M`選項把`stdio.h`以及它所包含的系統頭文件也找出來了,如果我們不需要輸出系統頭文件的依賴關系,可以用`-MM`選項:
```
$ gcc -MM *.c
main.o: main.c main.h stack.h maze.h
maze.o: maze.c maze.h main.h
stack.o: stack.c stack.h main.h
```
接下來的問題是怎么把這些規則包含到Makefile中,GNU `make`的官方手冊建議這樣寫:
```
all: main
main: main.o stack.o maze.o
gcc $^ -o $@
clean:
-rm main *.o
.PHONY: clean
sources = main.c stack.c maze.c
include $(sources:.c=.d)
%.d: %.c
set -e; rm -f $@; \
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
```
`sources`變量包含我們要編譯的所有`.c`文件,`$(sources:.c=.d)`是一個變量替換語法,把`sources`變量中每一項的`.c`替換成`.d`,所以`include`這一句相當于:
```
include main.d stack.d maze.d
```
類似于C語言的`#include`指示,這里的`include`表示包含三個文件`main.d`、`stack.d`和`maze.d`,這三個文件也應該符合Makefile的語法。如果現在你的工作目錄是干凈的,只有`.c`文件、`.h`文件和`Makefile`,運行`make`的結果是:
```
$ make
Makefile:13: main.d: No such file or directory
Makefile:13: stack.d: No such file or directory
Makefile:13: maze.d: No such file or directory
set -e; rm -f maze.d; \
cc -MM maze.c > maze.d.$$; \
sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
rm -f maze.d.$$
set -e; rm -f stack.d; \
cc -MM stack.c > stack.d.$$; \
sed 's,\(stack\)\.o[ :]*,\1.o stack.d : ,g' < stack.d.$$ > stack.d; \
rm -f stack.d.$$
set -e; rm -f main.d; \
cc -MM main.c > main.d.$$; \
sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.$$ > main.d; \
rm -f main.d.$$
cc -c -o main.o main.c
cc -c -o stack.o stack.c
cc -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main
```
一開始找不到`.d`文件,所以`make`會報警告。但是`make`會把`include`的文件名也當作目標來嘗試更新,而這些目標適用模式規則`%.d: %c`,所以執行它的命令列表,比如生成`maze.d`的命令:
```
set -e; rm -f maze.d; \
cc -MM maze.c > maze.d.$$; \
sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
rm -f maze.d.$$
```
注意,雖然在Makefile中這個命令寫了四行,但其實是一條命令,`make`只創建一個Shell進程執行這條命令,這條命令分為5個子命令,用`;`號隔開,并且為了美觀,用續行符`\`拆成四行來寫。執行步驟為:
1. `set -e`命令設置當前Shell進程為這樣的狀態:如果它執行的任何一條命令的退出狀態非零則立刻終止,不再執行后續命令。
2. 把原來的`maze.d`刪掉。
3. 重新生成`maze.c`的依賴關系,保存成文件`maze.d.1234`(假設當前Shell進程的id是1234)。注意,在Makefile中`$`有特殊含義,如果要表示它的字面意思則需要寫兩個$,所以Makefile中的四個$傳給Shell變成兩個$,兩個$在Shell中表示當前進程的id,一般用它給臨時文件起名,以保證文件名唯一。
4. 這個`sed`命令比較復雜,就不細講了,主要作用是查找替換。`maze.d.1234`的內容應該是`maze.o: maze.c maze.h main.h`,經過`sed`處理之后存為`maze.d`,其內容是`maze.o maze.d: maze.c maze.h main.h`。
5. 最后把臨時文件`maze.d.1234`刪掉。
不管是Makefile本身還是被它包含的文件,只要有一個文件在`make`過程中被更新了,`make`就會重新讀取整個Makefile以及被它包含的所有文件,現在`main.d`、`stack.d`和`maze.d`都生成了,就可以正常包含進來了(假如這時還沒有生成,`make`就要報錯而不是報警告了),相當于在Makefile中添了三條規則:
```
main.o main.d: main.c main.h stack.h maze.h
maze.o maze.d: maze.c maze.h main.h
stack.o stack.d: stack.c stack.h main.h
```
如果我在`main.c`中加了一行`#include "foo.h"`,那么:
1、`main.c`的修改日期變了,根據規則`main.o main.d: main.c main.h stack.h maze.h`要重新生成`main.o`和`main.d`。生成`main.o`的規則有兩條:
```
main.o: main.c main.h stack.h maze.h
%.o: %.c
# commands to execute (built-in):
$(COMPILE.c) $(OUTPUT_OPTION) $<
```
第一條是把規則`main.o main.d: main.c main.h stack.h maze.h`拆開寫得到的,第二條是隱含規則,因此執行`cc`命令重新編譯`main.o`。生成`main.d`的規則也有兩條:
```
main.d: main.c main.h stack.h maze.h
%.d: %.c
set -e; rm -f $@; \
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
```
因此`main.d`的內容被更新為`main.o main.d: main.c main.h stack.h maze.h foo.h`。
2、由于`main.d`被Makefile包含,`main.d`被更新又導致`make`重新讀取整個Makefile,把新的`main.d`包含進來,于是新的依賴關系生效了。
## 5.?常用的`make`命令行選項
`-n`選項只打印要執行的命令,而不會真的執行命令,這個選項有助于我們檢查Makefile寫得是否正確,由于Makefile不是順序執行的,用這個選項可以先看看命令的執行順序,確認無誤了再真正執行命令。
`-C`選項可以切換到另一個目錄執行那個目錄下的Makefile,比如先退到上一級目錄再執行我們的Makefile(假設我們的源代碼都放在`testmake`目錄下):
```
$ cd ..
$ make -C testmake
make: Entering directory `/home/akaedu/testmake'
cc -c -o main.o main.c
cc -c -o stack.o stack.c
cc -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main
make: Leaving directory `/home/akaedu/testmake'
```
一些規模較大的項目會把不同的模塊或子系統的源代碼放在不同的子目錄中,然后在每個子目錄下都寫一個該目錄的Makefile,然后在一個總的Makefile中用`make -C`命令執行每個子目錄下的Makefile。例如Linux內核源代碼根目錄下有`Makefile`,子目錄`fs`、`net`等也有各自的`Makefile`,二級子目錄`fs/ramfs`、`net/ipv4`等也有各自的`Makefile`。
在`make`命令行也可以用`=`或`:=`定義變量,如果這次編譯我想加調試選項`-g`,但我不想每次編譯都加`-g`選項,可以在命令行定義`CFLAGS`變量,而不必修改Makefile編譯完了再改回來:
```
$ make CFLAGS=-g
cc -g -c -o main.o main.c
cc -g -c -o stack.o stack.c
cc -g -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main
```
如果在Makefile中也定義了`CFLAGS`變量,則命令行的值覆蓋Makefile中的值。
- Linux C編程一站式學習
- 歷史
- 前言
- 部分?I.?C語言入門
- 第?1?章?程序的基本概念
- 第?2?章?常量、變量和表達式
- 第?3?章?簡單函數
- 第?4?章?分支語句
- 第?5?章?深入理解函數
- 第?6?章?循環語句
- 第?7?章?結構體
- 第?8?章?數組
- 第?9?章?編碼風格
- 第?10?章?gdb
- 第?11?章?排序與查找
- 第?12?章?棧與隊列
- 第?13?章?本階段總結
- 部分?II.?C語言本質
- 第?14?章?計算機中數的表示
- 第?15?章?數據類型詳解
- 第?16?章?運算符詳解
- 第?17?章?計算機體系結構基礎
- 第?18?章?x86匯編程序基礎
- 第?19?章?匯編與C之間的關系
- 第?20?章?鏈接詳解
- 第?21?章?預處理
- 第?22?章?Makefile基礎
- 第?23?章?指針
- 第?24?章?函數接口
- 第?25?章?C標準庫
- 第?26?章?鏈表、二叉樹和哈希表
- 第?27?章?本階段總結
- 部分?III.?Linux系統編程
- 第?28?章?文件與I/O
- 第?29?章?文件系統
- 第?30?章?進程
- 第?31?章?Shell腳本
- 第?32?章?正則表達式
- 第?33?章?信號
- 第?34?章?終端、作業控制與守護進程
- 第?35?章?線程
- 第?36?章?TCP/IP協議基礎
- 第?37?章?socket編程
- 附錄?A.?字符編碼
- 附錄?B.?GNU Free Documentation License Version 1.3, 3 November 2008
- 參考書目
- 索引