make命令以及makefile
使用RCS與CVS進行源代碼控制
編寫手冊頁
使用patch與tar發布軟件
開發環境
多源代碼的問題
當我們編寫小程序時,許多人都是簡單的在編輯后通過重新編譯所有的文件重新構建我們的程序。然而,對於大程序,這種簡單構建方法的問題就變得明顯了。編輯-編譯-測試的循環時間將會變長。即使是最為耐心的程序員也會希望避免當只修改一個文件時而編譯所有的文件。
當創建多個頭文件並且在不同的源文件中包含多個頭文件時就會出現一個更為困難的問題。假設我們有三個頭文件a.h,b.h以及c.h,和C源文件main.c,2.c以及3.c。而我們會具有如下的包含關系:
/*
main.c */
#include “a.h”
...
/* 2.c */
#include “a.h”
#include
“b.h”
...
/* 3.c */
#include “b.h”
#include
“c.h”
...
如果程序員修改了c.h,文件main.c以及2.c並不需要重新編譯,因為他們並不依賴於這個頭文件。文件3.c依賴於c.h,所以如果c.h被修改了,那么他就應被重新編譯。然而,如果b.h被修改了,而程序員忘記重新編譯2.c,那么所得到的程序也許就不會正常工作。
make程序通過確保修改所影響的所有文件在需要時進行重新編譯來解決所有這些問題。
make命令與Makefile
然而,正如我們所看到的,make命令有許多內建的知識,他本身並不知道如何構建我們的程序。我們必須提供一個文件來告訴make我們的程序是如何組織的。這個文件就是makefile。
makefile文件通常與工程的源文件位於同一個目錄中。我們可以同時在我們機子上有多個不同的makefile文件。確實,如果我們有一個非常大的工程,我們也許會選擇對於工程的不同部分使用單獨的makefile文件進行管理。
make命令以及makefile文件的組合提供了一個強大的工具來進行工程的管理。他通常不僅用於控制源代碼的編譯,而且也用於准備手冊頁以及將程序安裝到目標目錄。
makefile文件的語法
一個makefile文件由一組依賴與規則組成。一個依賴具有一個目標(將要創建的文件)以及他所依賴的源文件集合。規則描述了由依賴文件如何創建目標文件。通常,目標文件是一個可執行文件。
makefile
文件由make命令來執行,從而決定目標文件或是要創建的文件,然后比較源文件的日期與時間用於決定調用哪條規則來構建目標。通常,在構建最終的目標之前必須創建一些中間目標。make命令使用makefile文件來確定目標構建的正確順序以及規則調用的正確順序。
make選項與參數
make程序本身有多個選項。最常用的三個為:
-k,這個選項會通知make命令,當發現一個錯誤時會繼續執行,而不是在檢測到第一個問題時立即退出。例如,我們可以使用這個選項檢測出哪個文件編譯失敗。
-n,這個選項會使得make命令打印出此命令將會做的事情而不會實際執行。
-f
<filename>,這個選項會通知make命令使用哪個文件作為makefile文件。如果我們沒有使用這個選項,make首先在會當前目錄查找名為makefile的文件。如果這個文件不存在,他就會查找Makefile。按約定,Linux程序使用Makefile。這會使用
makefile文件在一個滿是小寫字母名字的目錄中第一個出現。
要告訴make來構建一個特定目錄,通常是一個可執行文件,我們可以將目標名字作為參數傳遞給make命令。如果沒有這樣做,make就會試着構建makefile文件中的第一個目標。許多程序在其makefile文件中將
all指定為第一個目標,然后列出其他的all所依賴的目標。這個約定可以很清晰的指出當沒有指定目標時makefile應嘗試構建的默認目標。我們建議遵守這個約定。
依賴
依賴指出最終程序中的每一個文件如何與源文件相關。在我們本章開始時所顯示的例子中,我們指出我們最終的程序依賴於main.o,2.o以及3.o;類似的
main.o依賴於main.c與a.h等。所以main.會受到main.c和a.h修改的影響,如果這兩個文件中的任何一個發生變化,都需要通過重新編譯main.c來重新創建main.o。
在makefile文件中,我們編寫規則的格式如下:目標名,冒號,空格或是tab,然后是以空格或是tab分隔的用於創建目標文件的列表。我們例子中的依賴列表如下:
myapp:
main.o 2.o 3.o
main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h
c.h
這表明myapp依賴於main.o,2.o以及3.o,而main.o依賴於main.c以及a.h,等等。
依賴集合指出了一個層次顯示了源文件之間如何彼此相關。我們可以很容易的看出,如果b.h發生變化,那么我們需要重新修正2.o以及3.o,而2.o與3.o發生了變化,我們也需要重新構建myapp。
如果我們希望編構建多個文件,那么我們可以使用偽目標all。假設我們的程序由二進制文件myapp與手冊頁myapp.1構成。我們可以用下面的語句來指定目標:
all:
myapp
myapp.1
再一次聲明,如果我們沒有指定all目標,make只是簡單的創建他在makefile中查找到的第一個目標。
規則
makefile
文件中的第二部分指定了用於描述如何創建一個目標的規則。在我們前面部分的例子中,在make命令之后應該使用哪個命令來重新構建2.o呢?看起來也許只是簡單的使用gcc
-c
2.c就足夠了(而且,正如我們在后面將會看到的,make確實知道許多默認的規則),但是如果我們需要指定一個include目錄,或是設置用於以后調試的符號信息選項時應怎么辦呢?我們可以通過在makefile文件中顯示的指定規則來做到。
注:在這里我們需要提示makefile文件一個非常奇怪的語法:一個空格與一個tab之間有區別。所有規則所在的行必須以一個tab開始;而不能以一個空格開始。因為多個空格與一個tab看起來很相似,而且因為在絕大多數的其他的Linux程序中在空格與tab之間並沒有區別,所以如果不加區分就會出現問題。同時,makefile文件中一行結束處的空格也會使得make命令失敗。然而,這是一個歷史問題,而且已經有許多的makefile文件在嘗試改進這個問題,所以我們要小心。幸運的是,當
make命令不能工作時,通常一個很明顯的問題就是丟失了tab。
試驗--一個簡單的Makefile
大多數規則由本可以在命令行輸入的簡單命令組成。對於我們的例子而言,我們可以創建我們的第一個makefile文件,Makefile1:
myapp:
main.o 2.o 3.o
gcc -o myapp main.o 2.o 3.o
main.o: main.c a.h
gcc
-c main.c
2.o: 2.c a.h b.h
gcc -c 2.c
3.o: 3.c b.h c.h
gcc -c
3.c
我們需要使用-f選項來調用我們的make命令,因為我們的makefile文件並不是通常默認的makefile或是Makefile。如果我們在一個並不包含任何源碼的目錄中調用這個代碼,我們就會得到下面的信息:
$
make -f Makefile1
make: *** No rule to make target ‘main.c’, needed by
‘main.o’.
$
make
命令認為makefile文件中的第一個目錄myapp是我們希望創建的文件。然后他會查找其他的依賴,並且會確定需要一個名為main.c的文件。因為我們還沒有創建這個文件,而makefile也並不知道如何創建這個文件,所以make命令就會報告錯誤。下面我們創建這些源文件並且再次嘗試。因為我們對於結果並不感興趣,所以這些文件可以非常簡單。頭文件實際上為空,所以我們使用touch命令來創建這些文件。
$
touch a.h
$ touch b.h
$ touch c.h
main.c
包含main函數,並且會調用function_two與function_three。其他的兩個文件定義了function_two與
function_three。源文件包含#include行來指定頭文件,所以他們會依賴所包含的頭文件的內容。這並不是一個程序,但是我們在這里列出相應的部分:
/*
main.c */
#include <stdlib.h>
#include “a.h”
extern void
function_two();
extern void function_three();
int main()
{
function_two();
function_three();
exit (EXIT_SUCCESS);
}
/*
2.c */
#include “a.h”
#include “b.h”
void function_two() {
}
/*
3.c */
#include “b.h”
#include “c.h”
void function_three()
{
}
下面我們再試一次:
$ make
gcc -c
gcc -c
gcc -c
gcc
-o
$
這就是一次成功的make。
工作原理
make
命令處理makefile文件中的依賴部分,並且確定需要創建的文件以及創建的順序。盡管我們首先列出了如何創建myapp,make可以確定創建文件的正確順序。然后他會調用我們在這些規則中所指定的創建這些文件的命令。make命令會在執行相應的命令時顯示這些命令。現在我們可以測試我們的
makefile文件來確定他是否正確的處理了b.h的修改。
$ touch b.h
$ make -f Makefile1
gcc
-c 2.c
gcc -c 3.c
gcc -o myapp main.o 2.o
3.o
$
make命令已經讀入我們的makefile文件,確定重新構建myapp所需要的最小數量的命令,並且以正確的順序執行這些命令。下面我們來看一下如果我們刪除一個目標文件時會發生什么:
$
rm 2.o
$ make -f Makefile1
gcc -c 2.c
gcc -o myapp main.o 2.o
3.o
$
再一次,make正確的確定了所需要的動作。
Makefile中的注釋
makefile文件中的注釋以#開始,並且直到本行結束。正如在C源文件中一樣,makefile文件中的注釋有助於作者也其他人更好的理解文件編寫時所期望的作用。
Makefile中的宏
即使有make與makefile是管理多個源碼文件工程的強大工具。然而,對於由大量的文件所組成的工程來說,他們仍顯得龐大和不靈活。所以Makefile允許我們使用宏,從而我們可以將其寫為更為通用的形式。
我們在makefile文件中以MACRONAME=value的形式定義一個宏,然后以$(MACRONAME)或是${MACRONAME}的形式來訪問MACRONAME的值。一些版本的make也可以接受$MACRONAME的形式。我們也可以通過將=之后的部分留空來設置一個空的宏。
在makefile文件中,宏通常用於編譯的選項。通常,當正在開發一個程序時,我們通常並不會使用優化選項來進行編譯,但是卻需要包含調試信息。而對於發布一個程序通常是相反的情況:發布一個運行速度盡量快而不帶任何調試信息的二進制程序。
Makefile1
的另一個問題是他假定編譯器為gcc。在其他的Unix系統上,我們也許會使用cc或是c89。如果我們希望將我們的makefile運行在其他版本的
Unix系統上,或是如果我們在我們的系統上使用一個不同的編譯器,我們就需要修改makefile的多行來使其工作。宏是用於收集系統相關部分的好方法,從而可以很容易的進行修改。
宏通常是在makefile文件本身內部定義的,但是也可以通過帶有宏定義的make命令來指定,例如
make
CC=c89。類似這樣的命令行定義可以覆蓋makefile文件中的定義。當在makefile文件外部使用時,宏定義必須作為一個單獨的參數進行傳遞,所以要避免空格或是使用引號的形式:make
"CC =
c89"。
試驗--帶有宏的Makefile
下面是我們的makefile的一個修正版本,Makefile2,其中使用一些宏:
all:
myapp
# Which compiler
CC = gcc
# Where are include files
kept
INCLUDE = .
# Options for development
CFLAGS = -g -Wall -ansi
#
Options for release
# CFLAGS = -O -Wall -ansi
myapp: main.o 2.o 3.o
$(CC) -o myapp main.o 2.o 3.o
main.o: main.c a.h
$(CC) -I$(INCLUDE)
$(CFLAGS) -c main.c
2.o: 2.c a.h b.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c
2.c
3.o: 3.c b.h c.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c
3.c
如果我們刪除我們原有的安裝,而使用這個新的makefile來創建一個新的安裝,我們會得到下面的信息:
$ rm *.o
myapp
$ make -f Makefile2
gcc -I. -g -Wall -ansi -c main.c
gcc -I. -g
-Wall -ansi -c 2.c
gcc -I. -g -Wall -ansi -c 3.c
gcc -o myapp main.o 2.o
3.o
$
工作原理
make程序使用合適的定義來替換$(CC),$(CFLAGS)以及$(INCLUDE),與帶有#define形式的C編譯器類似。現在如是我們需要修改編譯命令,我們只需要修改makefile文件中的一行。
事實上,make有多個內部宏,從而我們可以更為簡潔的來使用。在下表中,我們列出其中最常用的一些;我們會在后的例子中看到他們的使用。這些宏中的每一個僅會在他們剛要被使用時進行擴展,所以宏的語義會隨着makefile文件的處理而變化。事實上,如果這些不以這樣的方進行工作,他們就不會太大的作用。
$?
距離當前目標最近一次修改的需求列表
$@ 當前目標的名字
$< 當前需求的名字
$*
不帶前綴的當前需求的名字
還有另外兩個有用的特殊字符,也許我們會在makefile文件中的命令前遇到:
-選擇make忽略所有錯誤。例如,如果我們希望創建一個目錄,但是希望忽略錯誤,也許是因為這個目錄已經存在了,我們只需要在mkdir命令之前帶有一個負號。我們將會在后面的章節中看到-的使用。
@告訴make在執行命令之前不要將命令輸出到標准輸出。如果我們希望使用echo來顯示一些指令時,這個字符就特別有用。
多個目標
通常需要構建多個目標文件,而不是一個目標文件,或者是在一個地方收集多個命令組。我們可以擴展我們的makefile文件來完成這些任務。下面我們添加一個"clean"選項來移除不需要的目標文件,以及一個"install"選項來將完成的程序移動到另一個不同的目錄中。
試驗--多個目標
下面是下一個版本的makefile,Makefile3。
all:
myapp
# Which compiler
CC = gcc
# Where to install
INSTDIR =
/usr/local/bin
# Where are include files kept
INCLUDE = .
# Options for
development
CFLAGS = -g -Wall -ansi
# Options for release
# CFLAGS = -O
-Wall -ansi
myapp: main.o 2.o 3.o
$(CC) -o myapp main.o 2.o
3.o
main.o: main.c a.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c main.c
2.o:
2.c a.h b.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c 2.c
3.o: 3.c b.h c.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c 3.c
clean:
-rm main.o 2.o
3.o
install: myapp
@if [ -d $(INSTDIR) ]; \
then \
cp
myapp $(INSTDIR);\
chmod a+x $(INSTDIR)/myapp;\
chmod og-w
$(INSTDIR)/myapp;\
echo “Installed in $(INSTDIR)”;\
else
\
echo “Sorry, $(INSTDIR) does not exist”;\
fi
在這個makefile中需要注意多個地方。首先,特殊目標all仍然只是指定myapp作為目標。所以,當我們執行make命令而沒有指定一個目標時,默認的行為就是構建目標myapp。
接下來需要特別注意的另外兩個目標,clean與install。clean目標使用rm命令來移除目標文件。這個命令以-開頭,這會使得make命令忽略命令的結果,所以make
clean總會成功,即使並沒有目標文件而rm命令返回一個錯誤。目標"clean"並沒有為clean指定任何所依賴的條件;clean之后的行是空的。所以這個目標總是被認為是最新的,並且如果clean被指定為一個目標,那么其規則總是被執行。
install目標依賴於
myapp,所以make會知道在執行其他命令運行install之前必須先創建myapp。install的規則由一些shell腳本命令所組成。因為
make會為執行規則而調用shell,並且第一條規則使用一個新的shell,所以我們必須添加反斜線,這樣所有的腳本命令就全在一個邏輯行,並且會全部傳遞給一個單獨的shell調用。這個命令以@開頭,這會通知make命令在執行規則之前不要在標准輸出上打印出命令。
install目標順序執行命令將程序安裝在其最終位置上。他在執行下一條命令之前並不會檢測前一條命令是否執行成功。如果只有前一條命令成功執行之后才可以執行后續的命令,那么我們必須使用&&符號將其聯合,如下所示:
@if
[ -d $(INSTDIR) ]; \
then \
cp myapp $(INSTDIR)
&&\
chmod a+x $(INSTDIR)/myapp && \
chmod og-w
$(INSTDIR/myapp && \
echo “Installed in $(INSTDIR)” ;\
else \
echo “Sorry, $(INSTDIR) does not exist” ; false ; \
fi
也許我們會回想起第2章的內容,這就是一個shell
"and"命令,而且其效果是只有前一條命令執行成功之后才會執行后續的命令。在這里我們並不會關心是否保證前一條命令執行成功,所以我們只是使用這種較為簡單的形式。
也許我們作為一個普通用戶並不具有在/usr/local/bin目錄下安裝程序的權限。我們可以修改makefile文件從而使用另一個不同的安裝目錄,或是改變這個目錄的權限,或者是在執行make
install之前切換到root用戶。
$ rm *.o myapp
$ make -f Makefile3
gcc -I. -g
-Wall -ansi -c main.c
gcc -I. -g -Wall -ansi -c 2.c
gcc -I. -g -Wall -ansi
-c 3.c
gcc -o myapp main.o 2.o 3.o
$ make -f Makefile3
make: Nothing to
be done for ‘all’.
$ rm myapp
$ make -f Makefile3 install
gcc -o myapp
main.o 2.o 3.o
Installed in /usr/local/bin
$ make -f Makefile3 clean
rm
main.o 2.o
3.o
$
工作原理
首先,我們刪除myapp與所有的目標文件。make命令會使用目標all,從而構建myapp。接下來我們再次運行make玲,因為myapp已經是最新的了,所以make不會做任何事情。然后我們刪除myapp並且運行make
install。這會重新構建這個二進制文件,並且將其拷貝到安裝目錄。最后我們運行make
clean,這會刪除目標文件。
內建規則
到目前為止,我們已經在makefile文件中確切的指定了如何執行過程的每一步。事實上,makefile有大量的內建規則從而可以很大程度的簡化makefile文件,特別是當我們有大量源文件的時候。下面我們創建foo.c,這是一個傳統的Hello
World程序。
#include <stdlib.h>
#include <stdio.h>
int
main()
{
printf(“Hello World\n”);
exit(EXIT_SUCCESS);
}
不指定makefile文件,我們嘗試使用make來編譯。
$ make
foo
cc foo.c -o
foo
$
正如我們所看到的,make知道如何調用編譯器,盡管在這種情況下,他選擇cc而不是gcc(在Linux下這可以正常工作,因為通常cc鏈接到gcc)。有時,這些內建規則是推斷規則(inference
rules)。默認的規則使用宏,所以通過為這些宏指定一個新值,我們可以改變默認的行為。
$ rm foo
$ make CC=gcc
CFLAGS=”-Wall -g” foo
gcc -Wall -g foo.c -o
foo
$
我們可以使用-p選項使得make打印出其內建規則。內建規則太多而不能在這里全部列出,但是下面是GNU版本的make的make
-p的簡短輸出,演示了其中的部分規則:
OUTPUT_OPTION = -o $@
COMPILE.c = $(CC) $(CFLAGS)
$(CPPFLAGS) $(TARGET_ARCH) -c
%.o: %.c
# commands to execute
(built-in):
$(COMPILE.c) $(OUTPUT_OPTION)
$<
我們現在可以通過指定構建目標文件的規則使用這些內建規則來簡化我們的makefile文件,所以makefile文件的相關部分簡化為:
main.o:
main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h
c.h
后綴與模式規則
我們所看到的內建規則使用后綴進行工作(與Windows和MS-DOS的文件名擴展相類似),所以當指定一個帶有擴展名的文件時,make知道應使用哪條規則來創建帶有不同擴展名的文件。在這里最通常的規則就是由以.c為結尾的文件創建以.o為結尾的文件。這個規則就是使用編譯器編譯文件,但是並不鏈接源文件。
有時我們需要能夠創建新規則。程序開發作者過去在一些源文件上需要使用不同的編譯器進行編譯:兩個在MS-DOS下,以及
Linux下的gcc。要滿足MS-DOS編譯器的要求,C++源文件而不是C源文件,需要以.cpp為后綴進行命名。不幸的是,現在Linux下使用的
make版本並沒有編譯.cpp文件的內建規則。(他確實具有一個在Unix下更為常見的.cc的規則)
所以或者是為每一個單獨的文件指定一個規則,或者是我們需要教給make一個新的規則來由以.cpp為擴展名的文件創建目標文件。假如我們在這個工程中有大量的源文件,指定一個新規則節省了大量的輸入工作,並且使得在工程中添加一個新源文件更為容易。
要添加一個新的后綴規則,我們首先在makefile文件中添加一行,告訴make新的后綴;然后我們就可以使用這個新的后綴來編寫一條規則。make使用下面的語法樣式來定義一條通用的規則來由具有舊后綴的文件創建具有新后綴的文件:
.<old_suffix>.<new_suffix>:
下面是我們的makefile文件中一條新的通用規則的代碼片段,用於將.cpp文件轉換為.o文件:
.SUFFIXES:
.cpp
.cpp.o:
$(CC) -xc++ $(CFLAGS) -I$(INCLUDE) -c
$<
特殊依賴.cpp.o:告訴make接下來的規則用於將以.cpp為后綴的文件轉換為以.o為后綴的文件。當我們編寫這個依賴時,我們使用特殊的宏名,因為我們並不知道我們將要轉換的實際文件名。要理解這條規則,我們只需要簡單的回憶起$<會擴展為起始文件名(帶有舊后綴)即可。注意,我們只是告訴
make如何由.cpp文件得到.o文件;make已經知道如何由一個目標文件獲得二進制可執行文件。
當我們調用make時,他使用我們的新規則由bar.cpp獲得bar.o,然后使用其內建規則由.o獲得一個可執行文件。-xc++標記用於告訴gcc這是一個C++源文件。
在近些時候,make知道如何處理帶有.cpp擴展名的C++源文件,但是當將一種文件類型轉換為另一種文件類型時,這個技術是十分有用的。
更為舊的make版本包含一個對應的語法用來達到同樣的效果,而且更好。例如,匹配規則使用通配符語法來匹配文件,而不是僅依賴於文件擴展名。
對於上面例子中與.cpp規則等同的模式規則如下:
%.cpp:
%o
$(CC) -xc++ $(CFLAGS) -I$(INCLUDE) -c
$<
使用make管理庫
當我們正處理一個大型工程時,使用庫來管理多個編譯產品通常是比較方便的。庫是文件,通常以.a為擴展名,包含一個目標文件的集合。make命令有一個處理庫的特殊語法,從而使得他們更易於管理。
這個語法就是lib
(file.o),這就意味着目標文件file.o存儲在庫lib.a中。make具有一個內建的規則用於管理庫,通常如下面的樣子:
.c.a:
$(CC) -c $(CFLAGS) $<
$(AR) $(ARFLAGS) $@
$*.o
宏$(AR)與$(ARFLAGS)通常分別默認為命令ar與選項rv。這個簡短的語法告訴make由一個.c文件得到.a庫,他必須執行兩條規則:
第一條規則是他必須編譯源文件並且生成一個目標文件
第二條規則是使用ar命令來修改庫,添加新的目標文件
所以,如果我們有一個庫fud,包含文件bas.o,在第一條規則中$<被替換為bas.c。在第二條規則中,$@被替換為庫fud.a,而$*被替換為bas。
試驗--管理庫
實際上,管理庫的規則的使用是相當簡單的。下面我們修改我們的程序,從而文件2.o與3.o保存在一個名為mylib.a的庫中。我們的makefile文件需要一些小的修改,所以Makefile5如下所示:
all:
myapp
# Which compiler
CC = gcc
# Where to install
INSTDIR =
/usr/local/bin
# Where are include files kept
INCLUDE = .
# Options for
development
CFLAGS = -g -Wall -ansi
# Options for release
# CFLAGS = -O
-Wall -ansi
# Local Libraries
MYLIB = mylib.a
myapp: main.o
$(MYLIB)
$(CC) -o myapp main.o $(MYLIB)
$(MYLIB): $(MYLIB)(2.o)
$(MYLIB)(3.o)
main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h
c.h
clean:
-rm main.o 2.o 3.o $(MYLIB)
install: myapp
@if [ -d
$(INSTDIR) ]; \
then \
cp myapp $(INSTDIR);\
chmod a+x
$(INSTDIR)/myapp;\
chmod og-w $(INSTDIR)/myapp;\
echo
“Installed in $(INSTDIR)”;\
else \
echo “Sorry, $(INSTDIR) does
not exist”;\
fi
在這里需要注意我們是如何使用默認規則來完成大多數工作的。現在讓我們來測試我們的新版本makefile文件。
$ rm -f
myapp *.o mylib.a
$ make -f Makefile5
gcc -g -Wall -ansi -c -o main.o
main.c
gcc -g -Wall -ansi -c -o 2.o 2.c
ar rv mylib.a 2.o
a -
2.o
gcc -g -Wall -ansi -c -o 3.o 3.c
ar rv mylib.a 3.o
a - 3.o
gcc
-o myapp main.o mylib.a
$ touch c.h
$ make -f Makefile5
gcc -g -Wall
-ansi -c -o 3.o 3.c
ar rv mylib.a 3.o
r - 3.o
gcc -o myapp main.o
mylib.a
$
工作原理
我們首先刪除所有的目標文件以及庫,並且允許make構建myapp,他通過編譯並且在使用庫鏈接main.o之前創建庫,從而創建myapp。然后我們測試3.o的測試規則,他會通知make,如果c.h發生變動,那么3.c必須進行重新編譯。他會正確的完成這些工作,在重新鏈接之前會編譯3.c並且更新庫,從而創建一個新的可執行文件myapp。
高級主題:Makefile與子目標
如果我們編寫一個大工程,有時將組成庫的文件由主文件分離並且存儲在一個子目錄中是十分方便的。使用make可以兩種方法來完成這個任務。
首先,我們在此子目錄可以有第二個makefile文件來編譯文件,將其存儲在一個庫中,然后將庫拷貝到上一層主目錄。在高層目錄中的主makefile文件然后有一條規則用於構建這個庫,其調用第二個makefile文件的語法如下:
mylib.a:
(cd
mylibdirectory;$(MAKE))
這就是說我們必須總是嘗試構建mylib.a。當make調用這條規則用於構建庫時,他會進入子目錄mylibdirectory,然后調用一個新的
make命令來管理庫。因為這會調用一個新的shell,使用makefile的程序並不會執行cd命令。然而,所調用的用於執行規則構建庫的shell
是在一個不同的目錄中。括號可以保證他們都會在一個單獨的shell中進行處理。
第二個方法是在一個單獨的makefile文件中使用一些額外的宏。這些額外的宏是通過在我們已經討論過的這些宏的基礎上添加D(對目錄而言)或是F(就文件而言)來生成的。然后我們可以用下面的規則來覆蓋內建的.c.o前綴規則:
.c.o:
$(CC) $(CFLAGS) -c $(@D)/$(<F) -o
$(@D)/$(@F)
來在子目錄中編譯文件並且將目標文件留下子目錄中。然后我們可以用如下的依賴與規則來更新當前目錄中的庫:
mylib.a:
mydir/2.o mydir/3.o
ar -rv mylib.a
$?
我們需要決定在我們自己的工程中我們更喜歡哪種方法。許多工程只是簡單的避免具有子目錄,但是這樣會導致在源碼目錄中有大量的文件。正如我們在前面的概覽中所看到的,我們在子目錄中使用make只是簡單的增加了復雜性。
GNU
make與gcc
如果我們正使用GNU make與GNU
gcc編譯器,還有兩個有趣的選項:
第一個就是make的-jN("jobs")選項。這會使用make同時執行N條命令。此時make可以同時調用多條規則,獨立的編譯工程的不同部分。依據於我們的系統配置,這對於我們重新編譯的時候是一個巨大的改進。如果我們有多個源文件,嘗試這個選項是很有價值的。通常而言,小的數字,例如-j3,是一個好的起點。如果我們與其他用戶共享我們的機器,那么要小心使用這個選項。其他用戶也許不會喜歡每次編譯時我們啟動大量的進程數。
另一個有用的選項就是gcc的-MM選項。這會產生一個適合於make的依賴列表。在一個具有大量源碼文件的工程中,每一個文件都會包含不同的頭文件組合,要正確的獲得依賴關系是非常困難的,但是卻是十分重要的。如果我們使用每一個源文件依賴於每一個頭文件,有時我們就會編譯不必須的文件。另一方面,如果我們忽略一些依賴,問題就會更為嚴重,因為我們會沒有編譯那些需要重新編譯的文件。
試驗--gcc
-MM
下面我們使用gcc的-MM選項來為我們的例子工程生成一個依賴列表:
$ gcc -MM main.c
2.c 3.c
main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h
c.h
$
工作原理
gcc編譯只是簡單的以適於插入一個makefile文件中的形式輸出所需要依賴行。我們所需要做的就是將輸出保存到一個臨時文件中,然后將其插入makefile文件中,從而得到一個完美的依賴規則集。如果我們有一個gcc的輸出拷貝,我們的依賴就沒有出錯的理由。
如果我們對於makefile文件十分自信,我們可以嘗試使用makedepend工具,這些執行與-MM選項類似的功能,但是會將依賴實際添加到指定的makefile文件的尾部。
在我們離開makefile話題之前,也許很值得指出我們並不是只能限制自己使用makefile來編譯代碼或是創建庫。我們可以使用他們來自動化任何任務,例如,有一個序列命令可以使得我們由一些輸入文件得到一個輸出文件。通常"非編譯器"用戶也許適用於調用awk或是sed來處理一些文件,或是生成手冊頁。我們可以自動化任何文件處理,只要是make由文件的日期與時間信息的修改可以處理的。
發布軟件
程序發布的主要問題就是要保證包含所有的文件以及確切的版本。幸運的是,網絡程序社區已經發展出一個健壯的方法集合可以很多的解決這個問題。這些方法包括:
使用在所有的Unix機器上均可用的標准工具將所有的組件文件打包進入一個包文件中
控制軟件包的版本號
包文件采用包含版本號的命名約定從而用戶可以很容易分辨出他們正在處理的版本
包中子目錄的使用可以保證當文件由包文件中解壓出來時,他們就會位於一個單獨的目錄中,這樣就不會弄混哪些包含在包文件中而哪些沒有
這些方法的發展就意味着程序可以很容易並且可靠的發布。程序安裝的簡便性是另一個問題,因為他會依賴於程序與所要安裝的系統,但是至少我們可以保證我們擁有合適的組件文件。
patch程序
當程序發布以后,幾乎是不可避免的出現用戶發現bug或是程序的作者希望執行程序增加或是更新的情況。當作者將程序作為二進制文件發布時,他們通常只是簡單的傳遞新的二進制文件。有時(更為經常),提供者只是簡單的放出程序的一個新版本,通常是晦澀引用以及關於程序修改內容的少量信息。
另一個方面,將我們的軟件作為一個源碼包進行發布是一個好主意,因為這可以允許用戶知道我們是如何實現的以及如何使用這些特性。也可以允許用戶檢測程序實際所做的工作並且可以重用部分源代碼。
然而,例如Linux內核這樣幾十兆壓縮源代碼的重量級程序,傳輸內核源碼的更新集合將會占用大量的資源,而事實上,在每個版本之間只有很少一部分的源代碼進行了修改。
幸運的,有一個實用程序可以解決這個問題:patch。他是由Larry
Wall編寫的,他同時也編寫了Perl程序語言。patch命令可以使得我們只發布兩個版本之間相區別的部分,這樣任何具有版本1文件以及一個由版本1
到版本2的區別文件的人就可以使用patch命令自己生成版本2文件。
如果我們由版本1文件開始,
This is file
one
line 2
line 3
there is no line 4, this is line 5
line
6
然后我們創建版本2文件,
This is file two
line 2
line 3
line
4
line 5
line 6
a new line 8
我們可以使用diff命令來創建一個區別列表:
$
diff file1.c file2.c > diffs
diffs文件包含如下內容:
1c1
< This is
file one
—
> This is file two
4c4,5
< there is no line 4, this
is line 5
—
> line 4
> line 5
5a7
> a new line
8
這實際上是將一個文件修改為另一個文件的編輯器命令集合。假設我們有文件file1.c以及diffs文件,我們可以使用patch命令來更新我們的文件:
$
patch file1.c diffs
Hmm... Looks like a normal diff to me...
Patching file
file1.c using Plan A...
Hunk #1 succeeded at 1.
Hunk #2 succeeded at
4.
Hunk #3 succeeded at
7.
done
$
現在patch命令就已經將文件file1.c修改為與file2.c相同的ywyr
patch還有一個小技巧:去補丁的能力。假如我們不喜歡這些更改並且退回我們原始的file1.c文件。沒有問題,只需要再次使用patch命令,使用-R選項即可。
$
patch -R file1.c diffs
Hmm... Looks like a normal diff to me...
Patching
file file1.c using Plan A...
Hunk #1 succeeded at 1.
Hunk #2 succeeded at
4.
Hunk #3 succeeded at
6.
done
$
file1.c已經回退到其原始狀態了。
patch還有許多其他的選項,但是通常很善於由其輸入來確定我們正在嘗試做什么,然后簡單的完成這些事情。如果patch命令失敗,他就會創建一個以.rej為擴展名包含不能進行補丁操作的部分。
當我們正在處理軟件補丁時,使用diff
-c選項是一個好主意,這會生成一個"context
diff"。這會在每一個修改之后與之后提供幾行文本,從而patch在應用補丁之前可以驗證內容匹配,而補丁文件也更容易閱讀。
其他的發布程序
Linux
程序與源碼通常以包含版本號的名字,並且以.tar.gz或是.tgz的擴展名進行發布。這些是使用gzip壓縮的TAR文件,也就是所熟知的
tarball。如果我們使用通常的tar,我們必須通過兩步來處理這些文件。下面我們來為我們的程序創建一個gzip文件。
$ tar cvf
myapp-1.0.tar main.c 2.c 3.c *.h myapp.1
Makefile5
main.c
2.c
3.c
a.h
b.h
c.h
myapp.1
Makefile5
$
現在我們有一個TAR文件。
$
ls -l *.tar
-rw-r--r-- 1 neil
$
我們可以使用gzip壓縮程序使其變得更小:
$
gzip myapp-1.0.tar
$ ls -l *.gz
-rw-r--r-- 1
neil
$
正如我們所看到的,其結果在尺寸上非常小。.tar.gz然后可以簡單的重命名為.tgz擴展名:
$ mv
myapp-1.0.tar.gz
myapp_v1.tgz
以逗點和三個字符進行重命名的習慣似乎是對Windows軟件的讓步,因為與Linux和Unix不同,Windows很依賴正在處理的正確的擴展名。要得到我們的文件,我們可以解壓並且由tar文件中釋放這些文件:
$
mv myapp_v1.tgz myapp-1.0.tar.gz
$ gzip -d myapp-1.0.tar.gz
$ tar xvf
myapp-1.0.tar
main.c
2.c
3.c
a.h
b.h
c.h
myapp.1
Makefile5
$
使用GNU版本的tar,事情會變得更為簡單--我們可以一步創建壓縮歸檔:
$
tar zcvf myapp_v1.tgz main.c 2.c 3.c *.h myapp.1
Makefile5
main.c
2.c
3.c
a.h
b.h
c.h
myapp.1
Makefile5
$
我們也可以進行簡單的解壓:
$
tar zxvf
myapp_v1.tgz
main.c
2.c
3.c
a.h
b.h
c.h
myapp.1
Makefile5
$
如果我們希望知道歸檔的內容而不實際的進行解壓,我們可以使用另一個不同的tar
ztvf選項來調用tar程序。
我們在前面使用tar作為例子,而並沒有描述除必須的選項以外的其他選項。下面我們簡單的看一下tar命令以及一些常用的選項。正如由例子中所看到的,基本語法為:
tar
[options] [list of
files]
列表中的第一個項目為目標,盡管我們只是處理文件,他可以處理設備。列表中的其他項目要添加到一個新的或是已存在的歸檔中,這依賴於我們所使用的選項。這個列表也可以包含目錄,在這種情況下,所有子目錄默認情況下都會包含在文件中。如果我們釋放文件,沒有必要指定名字,因為tar會保存完全路徑。
在這一部分,我們使用了六個不同選項的組合:
c:創建一個新的歸檔
f:指定目標是一個文件而不是一個設備
t:列出一個歸檔中的內容而不實際釋放
v:tar顯示出處理消息
x:由歸檔中釋放文件
z:在GNU
tar中使用gzip過濾歸檔
tar命令還有更多的選項允許我們對於命令的操作以及他所創建的文檔進行更好的控制。查看tar手冊頁可以得到更為詳細的信息。