linux學習筆記-makefile的寫法

<h3 id="1">前言</h3>
  一個項目,擁有成百上千的源程序文件,那如何組織這些源碼文件的編譯和連接呢?此時就須要肯定整個工程的編譯連接規則,Makefile就是用來指定規則的。而make是一個解釋makefile中指令的命令工具,通常來講,大多數的IDE都有這個命令,好比:Delphi的make,Visual C++的nmake,Linux下GNU的make。git

<h3 id="1">Makefile介紹</h3>
  當咱們編譯工程時,只要輸入make就會去編譯了,那麼這個make命令執行的時候必定須要一個Makefile文件,經過這個Makefile告訴make命令須要怎麼樣的去編譯和連接程序。 github

  make的工做規則是:編程

1.若是這個工程沒有編譯過,那麼咱們的全部C文件都要編譯並被連接。
2.若是這個工程的某幾個C文件被修改,那麼咱們只編譯被修改的C文件,並連接目標程序。
3.若是這個工程的頭文件被改變了,那麼咱們須要編譯引用了這幾個頭文件的C文件,並連接目標程序。

  只要咱們的Makefile寫得夠好,全部的這一切,咱們只用一個make命令就能夠完成,make命令會自動智能地根據當前的文件修改的狀況來肯定哪些文件須要重編譯,從而本身編譯所須要的文件和連接目標程序。socket

<h3 id="3">Makefile規則</h3>
  在講述這個Makefile以前,仍是讓咱們先來粗略地看一看Makefile的規則。函數

target... : prerequisites ...
    command
     ...
     ...
  • target:也就是一個目標文件,能夠是Object File,也能夠是執行文件,也能夠是標籤。
  • prerequisites:就是,要生成那個target所須要的文件或是目標。
  • command:也就是make須要執行的命令。(任意的Shell命令)

  這是一個文件的依賴關係,也就是說,target這一個或多個的目標文件依賴於prerequisites中的文件,其生成規則定義在command中。說白一點就是說,prerequisites中若是有一個以上的文件比target文件要新的話,command所定義的命令就會被執行。這就是Makefile的規則。也就是Makefile中最核心的內容。
  說到底,Makefile的東西就是這樣一點,好像接下來不用再講任何東西了。呵呵。還不盡然,這是Makefile的主線和核心,但要寫好一個Makefile還不夠,我會之後面一點一點地結合個人工做經驗給你慢慢到來。內容還多着呢。:)
  咱們來看一個示例:工具

nty_http_server : nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o
        gcc -o nty_http_server nty_http_server.o nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o -lpthread
nty_coroutine.o:nty_coroutine.c nty_coroutine.h
        gcc -c nty_coroutine.c
nty_epoll.o:nty_epoll.c nty_coroutine.h
        gcc -c nty_epoll.c
nty_schedule.o:nty_schedule.c nty_coroutine.h
        gcc -c nty_schedule.c
nty_socket.o:nty_socket.c nty_coroutine.h
        gcc -c nty_socket.c
nty_http_server.o:nty_http_server.c nty_coroutine.h
        gcc -c nty_http_server.c

clean :
        rm -rf *.o

  在定義好依賴關係後,後續的那一行定義瞭如何生成目標文件的操做系統命令,必定要以一個Tab鍵做爲開頭。記住,make並無論命令是怎麼工做的,他只管執行所定義的命令。make會比較targets文件和prerequisites文件的修改日期,若是prerequisites文件的日期要比targets文件的日期要新,或者target不存在的話,那麼,make就會執行後續定義的命令。測試

這裏要說明一點的是,clean不是一個文件,它只不過是一個動做名字,有點像C語言中的lable同樣,其冒號後什麼也沒有,那麼,make就不會自動去找文件的依賴性,也就不會自動執行其後所定義的命令。要執行其後的命令,就要在make命令後明顯得指出這個lable的名字。這樣的方法很是有用,咱們能夠在一個makefile中定義不用的編譯或是和編譯無關的命令,好比程序的打包,程序的備份,等等。

<h3 id="4">make是如何工做的</h3>
  在默認的方式下,也就是咱們只輸入make命令。那麼,ui

  1. make會在當前目錄下找名字叫「Makefile」或「makefile」的文件。
  2. 若是找到,它會找文件中的第一個目標文件(target),在上面的例子中,他會找到「nty_http_server」這個文件,並把這個文件做爲最終的目標文件。
  3. 若是nty_http_server文件不存在,或是nty_http_server所依賴的後面的 .o 文件的文件修改時間要比nty_http_server這個文件新,那麼,他就會執行後面所定義的命令來生成nty_http_server這個文件。
  4. 若是nty_http_server所依賴的.o文件也存在,那麼make會在當前文件中找目標爲.o文件的依賴性,若是找到則再根據那一個規則生成.o文件。(這有點像一個堆棧的過程)
  5. 固然,你的C文件和H文件是存在的啦,因而make會生成 .o 文件,而後再用 .o 文件聲明make的終極任務,也就是執行文件nty_http_server了。

  這就是整個make的依賴性,make會一層又一層地去找文件的依賴關係,直到最終編譯出第一個目標文件。在找尋的過程當中,若是出現錯誤,好比最後被依賴的文件找不到,那麼make就會直接退出,並報錯,而對於所定義的命令的錯誤,或是編譯不成功,make根本不理。make只管文件的依賴性,即,若是在我找了依賴關係以後,冒號後面的文件仍是不在,那麼對不起,我就不工做啦。
  經過上述分析,咱們知道,像clean這種,沒有被第一個目標文件直接或間接關聯,那麼它後面所定義的命令將不會被自動執行,不過,咱們能夠顯示要make執行。即命令——「make clean」,以此來清除全部的目標文件,以便重編譯。
  因而在咱們編程中,若是這個工程已被編譯過了,當咱們修改了其中一個源文件,好比nty_coroutine.c,那麼根據咱們的依賴性,咱們的目標nty_coroutine.o會被重編譯(也就是在這個依性關係後面所定義的命令),因而nty_coroutine.o的文件也是最新的啦,因而nty_coroutine.o的文件修改時間要比nty_http_server要新,因此nty_http_serve也會被從新連接了(詳見nty_http_serve目標文件後定義的命令)。
  而若是咱們改變了「nty_coroutine.h」,那麼,nty_coroutine.o、nty_epoll.o、nty_schedule.o、nty_socket.o和nty_http_server.o都會被重編譯,而且,nty_http_server會被重連接。操作系統

<h3 id="5">Makefile使用變量</h3>
  在上面的例子中,咱們能夠看到[.o]文件的字符串被重複了兩次,若是咱們的工程須要加入一個新的[.o]文件,那麼咱們須要在兩個地方加(應該是三個地方,還有一個地方在clean中)。固然,咱們的makefile並不複雜,因此在兩個地方加也不累,但若是makefile變得複雜,那麼咱們就有可能會忘掉一個須要加入的地方,而致使編譯失敗。因此,爲了makefile的易維護,在makefile中咱們可使用變量。makefile的變量也就是一個字符串,理解成C語言中的宏可能會更好。

  好比,咱們聲明一個變量,叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJ,反正無論什麼啦,只要可以表示obj文件就好了。咱們在makefile一開始就這樣定義:

OBJECTS=nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o

  因而咱們的Makefile能夠修改爲這樣子:

OBJECTS=nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o
nty_http_server : $(OBJECTS)
        gcc -o nty_http_server $(OBJECTS) -lpthread
nty_coroutine.o:nty_coroutine.c nty_coroutine.h
        gcc -c nty_coroutine.c
nty_epoll.o:nty_epoll.c nty_coroutine.h
        gcc -c nty_epoll.c
nty_schedule.o:nty_schedule.c nty_coroutine.h
        gcc -c nty_schedule.c
nty_socket.o:nty_socket.c nty_coroutine.h
        gcc -c nty_socket.c
nty_http_server.o:nty_http_server.c nty_coroutine.h
        gcc -c nty_http_server.c

clean :
        rm -rf $(OBJECTS) nty_http_server

  因而若是有新的 .o 文件加入,咱們只需簡單地修改一下 OBJECTS 變量就能夠了。但是聰明的你應該也發現了這個Makefile仍是很差維護,由於每一個.o文件都須要顯示地寫出依賴和生成的命令,致使這個Makefile有不少的雷同,咱們有沒有更好的維護辦法呢?請看下一小節:

<h3 id="6">讓make自動推導</h3>
  GNU的make很強大,它能夠自動推導文件以及文件依賴關係後面的命令,因而咱們就不必去在每個[.o]文件後都寫上相似的命令,由於,咱們的make會自動識別,並本身推導命令。只要make看到一個[.o]文件,它就會自動的把[.c]文件加在依賴關係中,若是make找到一個whatever.o,那麼whatever.c,就會是whatever.o的依賴文件。而且 cc -c whatever.c 也會被推導出來,因而,咱們的makefile不再用寫得這麼複雜。咱們的是新的makefile又出爐了。

OBJECTS=nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o
nty_http_server : $(OBJECTS)
        gcc -o nty_http_server $(OBJECTS) -lpthread
nty_coroutine.o:nty_coroutine.h
nty_epoll.o:nty_coroutine.h
nty_schedule.o:nty_coroutine.h
nty_socket.o:nty_coroutine.h
nty_http_server.o:nty_coroutine.h

.PHONY : clean
clean :
        rm -rf $(OBJECTS) nty_http_server

  這種方法,也就是make的「隱晦規則」。上面文件內容中,「.PHONY」表示,clean是個僞目標文件。
  即然咱們的make能夠自動推導命令,那麼我看到那堆[.o]和[.h]的依賴就有點不爽,那麼多的重複的[.h],能不能把其收攏起來?預知後事如何,請聽下回分解。
  
<h3 id="7">另類風格的Makefile</h3>
  上一小節提到的問題是沒有問題,這個對於make來講很容易,誰叫它提供了自動推導命令和文件的功能呢?來看看最新風格的makefile吧。

OBJECTS=nty_coroutine.o nty_epoll.o nty_schedule.o nty_socket.o nty_http_server.o
nty_http_server : $(OBJECTS)
        gcc -o nty_http_server $(OBJECTS) -lpthread
$(OBJECTS):nty_coroutine.h

.PHONY : clean
clean :
        rm -rf $(OBJECTS) nty_http_server

  這種風格,讓咱們的makefile變得很簡單,但咱們的文件依賴關係就顯得有點凌亂了。魚和熊掌不可兼得。還看你的喜愛了。
  另外即便這個Makefile比較簡單,可是它依然比較笨拙,好比若是咱們須要添加新的源碼文件,都要來修改OBJECTS變量的定義,那怎麼辦呢?

<h3 id="8">make中的函數</h3>
先來看咱們的Makefile:

SOURCES=$(wildcard *.c)
OBJECTS=$(patsubst %.c,%.o,$(SOURCES))
PROGRAM=nty_http_server
CC=gcc
CFLAGS=-lpthread

$(PROGRAM) : $(OBJECTS)
        $(CC) -o $(PROGRAM) $(OBJECTS) $(CFLAGS)
$(OBJECTS):$(SOURCES)

.PHONY : clean
clean :
        rm -rf $(OBJECTS) $(PROGRAM)

  在 GNU Make 裏有一個叫 'wildcard' 的函 數,它有一個參數,功能是展開成一列全部符合由其參數描述的文 件名,文件間以空格間隔。你能夠像下面所示使用這個命令:

SOURCES= $(wildcard *.c)

  這行會產生一個全部以 '.c' 結尾的文件的列表,而後存入變量 SOURCES 裏。固然你不須要必定要把結果存入一個變量。
  notdir把展開的文件的路徑去掉,只顯示文件名而不包含其路徑信息,例如:

FILES =$(notdir $(SOURCES))

  這行的做用是把上面以'.c'結尾的文件的文件列表中附帶的路徑去掉,只顯示符合條件的文件名。
  patsubst( patten substitude, 匹配替換的縮寫)函數。它須要3個參數:第一個是一個須要匹配的式樣,第二個表示用什麼來替換它,第三個是一個須要被處理的由空格分隔的字列。例如,處理那個通過上面定義後的變量,

OBJS = $(patsubst %.c,%.o,$(SOURCES))

  這行將處理全部在 SOURCES列箇中的字(一列文件名),若是它的 結尾是 '.c' ,就用'.o' 把 '.c' 取代。注意這裏的 % 符號將匹配一個或多個字符,而它每次所匹配的字串叫作一個‘柄’(stem) 。在第二個參數裏, % 被解讀成用第一參數所匹配的那個柄。
make還有不少的函數,好比foreach,origin,call,if then else,addprefix,dir,notdir

  到這裏,咱們已經把Makefile改了5個版本,這個應該是能夠用於大型工程了吧???非也,非也,非也。好比在大型工程中,咱們通常會劃分模塊,不一樣的目錄管理不一樣的源碼文件,此時Makefile如何寫呢?

<h3 id="9">文件搜索</h3>
  在一些大的工程中,有大量的源文件,咱們一般的作法是把這許多的源文件分類,並存放在不一樣的目錄中。因此,當make須要去找尋文件的依賴關係時,你能夠在文件前加上路徑,但最好的方法是把一個路徑告訴make,讓make在自動去找。
  Makefile文件中的特殊變量「VPATH」就是完成這個功能的,若是沒有指明這個變量,make只會在當前的目錄中去找尋依賴文件和目標文件。若是定義了這個變量,那麼,make就會在噹噹前目錄找不到的狀況下,到所指定的目錄中去找尋文件了。

VPATH = src:../headers

  上面的的定義指定兩個目錄,「src」和「../headers」,make會按照這個順序進行搜索。目錄由「冒號」分隔。(固然,當前目錄永遠是最高優先搜索的地方)。
  另外一個設置文件搜索路徑的方法是使用make的「vpath」關鍵字(注意,它是全小寫的),這不是變量,這是一個make的關鍵字,這和上面提到的那個VPATH變量很相似,可是它更爲靈活。它能夠指定不一樣的文件在不一樣的搜索目錄中。這是一個很靈活的功能。它的使用方法有三種:

1. vpath < pattern> < directories>    爲符合模式< pattern>的文件指定搜索目錄<directories>。

2. vpath < pattern>                              清除符合模式< pattern>的文件的搜索目錄。

3. vpath                                                 清除全部已被設置好了的文件搜索目錄。

  vapth使用方法中的< pattern>須要包含「%」字符。「%」的意思是匹配零或若干字符,例如,「%.h」表示全部以「.h」結尾的文件。< pattern>指定了要搜索的文件集,而< directories>則指定了的文件集的搜索的目錄。例如:

vpath %.h ../headers

  該語句表示,要求make在「../headers」目錄下搜索全部以「.h」結尾的文件。(若是某文件在當前目錄沒有找到的話)。
  咱們能夠連續地使用vpath語句,以指定不一樣搜索策略。若是連續的vpath語句中出現了相同的< pattern>,或是被重複了的< pattern>,那麼,make會按照vpath語句的前後順序來執行搜索。如:

vpath %.c foo

   vpath %   blish

   vpath %.c bar

  其表示「.c」結尾的文件,先在「foo」目錄,而後是「blish」,最後是「bar」目錄。

vpath %.c foo:bar

   vpath %   blish

  而上面的語句則表示「.c」結尾的文件,先在「foo」目錄,而後是「bar」目錄,最後纔是「blish」目錄。
  到這裏,咱們已經把Makefile改了6個版本,這個應該是能夠用於大型工程了吧???非也,非也,非也。好比在大型工程中,咱們的目標文件通常會有多個,那如何實現一鍵式編譯,好比咱們在編譯thrift就用過,還好比咱們不少同窗搞過嵌入式開發,好比用於路由器開發的openwrt系統,那個系統一鍵make命令,就能把全部的包裏的目標編譯完,那這個是多麼神奇啊,Oh my god, what should i do?

<h3 id="9">絕對頂級的大型工程Makefile</h3>
  其實在Makefile中能夠包含別的Makefile,被包含的Makefile通常是定義了一些變量,讓經過這些變量控制其餘的Makefile的編譯。也能夠再包含多個包,從頂層的Makefile一直調用到全部包的Makefile,完成編譯工做。那這究竟是如何完成的,你們先移步到
https://github.com/zhiyong0804/RTSP_PullerModule.git
咱們看到頂級目錄的Makefile,內容以下:

LIVEMEDIA_DIR = liveMedia
GROUPSOCK_DIR = groupsock
USAGE_ENVIRONMENT_DIR = UsageEnvironment
BASIC_USAGE_ENVIRONMENT_DIR = BasicUsageEnvironment

PULLER_MODULE_DIR = PullerModule

all:
    cd $(LIVEMEDIA_DIR) ; $(MAKE)
    cd $(GROUPSOCK_DIR) ; $(MAKE)
    cd $(USAGE_ENVIRONMENT_DIR) ; $(MAKE)
    cd $(BASIC_USAGE_ENVIRONMENT_DIR) ; $(MAKE)

install:
    mkdir -p $(PULLER_MODULE_DIR)/lib
    cd $(LIVEMEDIA_DIR) ; $(MAKE) install
    cd $(GROUPSOCK_DIR) ; $(MAKE) install
    cd $(USAGE_ENVIRONMENT_DIR) ; $(MAKE) install
    cd $(BASIC_USAGE_ENVIRONMENT_DIR) ; $(MAKE) install

module:
    cd $(PULLER_MODULE_DIR) ; $(MAKE)
    cd $(PULLER_MODULE_DIR) ; $(MAKE) test

clean:
    cd $(LIVEMEDIA_DIR) ; $(MAKE) clean
    cd $(GROUPSOCK_DIR) ; $(MAKE) clean
    cd $(USAGE_ENVIRONMENT_DIR) ; $(MAKE) clean
    cd $(BASIC_USAGE_ENVIRONMENT_DIR) ; $(MAKE) clean
    cd $(PULLER_MODULE_DIR) ; $(MAKE) clean

cleanall:
    cd $(PULLER_MODULE_DIR) ; $(MAKE) cleanall

  從該文件咱們看到在all裏,有cd $(LIVEMEDIA_DIR) ; $(MAKE),這就是要進入到liveMedia子目錄執行make操做,固然在該Makefile還須要編譯groupsock、UsageEnvironment和BasicUsageEnvironment目錄,在module這個僞目標裏,再去編譯了PullerModule和PullerModule的測試工程。那麼咱們進入到groupsock目錄看看它的Makefile內容以下:

INCLUDES = -Iinclude -I../UsageEnvironment/include

include ../inc.mk

NAME = libgroupsock
ALL = $(NAME).$(LIB_SUFFIX)
all:    $(ALL)

.$(C).$(OBJ):
    $(C_COMPILER) -c $(C_FLAGS) $<
.$(CPP).$(OBJ):
    $(CPLUSPLUS_COMPILER) -c $(CPLUSPLUS_FLAGS) $<

GROUPSOCK_LIB_OBJS = GroupsockHelper.$(OBJ) GroupEId.$(OBJ) inet.$(OBJ) Groupsock.$(OBJ) NetInterface.$(OBJ) NetAddress.$(OBJ) IOHandlers.$(OBJ)

GroupsockHelper.$(CPP):    include/GroupsockHelper.hh
include/GroupsockHelper.hh:    include/NetAddress.hh
include/NetAddress.hh:    include/NetCommon.h
GroupEId.$(CPP):    include/GroupEId.hh
include/GroupEId.hh:    include/NetAddress.hh
inet.$(C):        include/NetCommon.h
Groupsock.$(CPP):    include/Groupsock.hh include/GroupsockHelper.hh include/TunnelEncaps.hh
include/Groupsock.hh:    include/groupsock_version.hh include/NetInterface.hh include/GroupEId.hh
include/NetInterface.hh:    include/NetAddress.hh
include/TunnelEncaps.hh:    include/NetAddress.hh
NetInterface.$(CPP):    include/NetInterface.hh include/GroupsockHelper.hh
NetAddress.$(CPP):    include/NetAddress.hh include/GroupsockHelper.hh
IOHandlers.$(CPP):    include/IOHandlers.hh include/TunnelEncaps.hh

libgroupsock.$(LIB_SUFFIX): $(GROUPSOCK_LIB_OBJS) \
    $(PLATFORM_SPECIFIC_LIB_OBJS)
    $(LIBRARY_LINK)$@ $(LIBRARY_LINK_OPTS) \
        $(GROUPSOCK_LIB_OBJS)

clean:
    -rm -rf *.$(OBJ) $(ALL) core *.core *~ include/*~

install: install1 $(INSTALL2)
install1: libgroupsock.$(LIB_SUFFIX)
      install -m 644 libgroupsock.$(LIB_SUFFIX) $(DESTDIR)$(LIBDIR)
install_shared_libraries: libgroupsock.$(LIB_SUFFIX)
      ln -s libgroupsock.$(LIB_SUFFIX) $(DESTDIR)$(LIBDIR)/libgroupsock.$(SHORT_LIB_SUFFIX)
      ln -s libgroupsock.$(LIB_SUFFIX) $(DESTDIR)$(LIBDIR)/libgroupsock.so

##### Any additional, platform-specific rules come here:

第三行include ../inc.mk包含了上一級目錄的inc.mk(對,沒有錯,咱們通常的include的Makefile習慣以.mk命名),爲何要include這個文件呢,這是由於C_FLAGS還有其它的變量都是定義在inc.mk裏,而這些變量對於其它的包(好比groupsock、UsageEnvironment和BasicUsageEnvironment)也是須要的,避免重複定義,咱們在inc.mk統必定義。OK,inc.mk文件內容以下:

PREFIX = ../PullerModule
LIBDIR = $(PREFIX)/lib

Debug = -g
Release = 

#### Change the following for your environment:
COMPILE_OPTS =    $(Debug)    $(INCLUDES)  -fPIC -I. -O2 -DSOCKLEN_T=socklen_t -D_LARGEFILE_SOURCE=1 -D_FILE_OFFSET_BITS=64
C =        c
C_COMPILER =        cc  # replace with "mipsel-linux-cc" for mipsel platform
C_FLAGS =        $(COMPILE_OPTS)
CPP =            cpp
CPLUSPLUS_COMPILER =    g++ # replace with "mipsel-linux-g++" for mipsel platform
CPLUSPLUS_FLAGS =    $(COMPILE_OPTS) -Wall -DBSD=1
OBJ =            o
LINK =            g++ -o  # replace with "mipsel-linux-g++ -o" for mipsel platform
LINK_OPTS =        -L.
CONSOLE_LINK_OPTS =    $(LINK_OPTS)
LIBRARY_LINK =        ar cr #replace with "mipsel-linux-ar cr" for mipsel platform
LIBRARY_LINK_OPTS =    
LIB_SUFFIX =            a
LIBS_FOR_CONSOLE_APPLICATION =
LIBS_FOR_GUI_APPLICATION =
EXE =
##### End of variables to change
相關文章
相關標籤/搜索