+++++++++++++++++++++++++ 第三部分 Makefile 的工程組織 ++++++++++++++++++++++++
寫在前面的話:
如果庫1依賴於庫2,使用者只用lib1的話,我還沒看到,怎麼避免用 -l庫2。就是說 -l庫1的同時,也要-l庫2才能編譯通過。 我現在能想到的只有用 pkg-config 加上 .pc 檔案來免得使用者知道要依賴什麼。
上面介紹了 Automake(autotools) 的標準工程組織,下面對比以下,為了完成相近的工作,自己寫Makefile怎麼來組織工程,以及瞭解,為什麼需要autotools。
一、為什麼需要autotools
還是最先提到的兩個原因:
1. 如何移植?
不同平台的編譯器,參數不同,而Makefile必須指明這些,這對維護多個平台的人來說,是不小的學習和記憶負擔。而autotools的configure為你做了這一切。且automake嵌了libtool,使得在各個平台上編譯庫異常的統一。
2. 如何製做安裝包?
(1) 顯然,autotools利於製做以源碼發行的安裝包,因為寫Makefile.am的原則就是給變數賦值,大大簡化了Makefile的編寫。
(2) 對於開發人員而言,以比Makefile更友好的方式,讓你以想要的方式來編譯,製做,安裝你想處理的東西(見上面寫Makefile.am的原則,還有其他的豐富的特性,見GNU官方的automake.html)。
(3) 對於拿到安裝包的使用者而言,它可以用configure來決定很多參數,調整編譯和安裝方式。──── 即:讓使用者改Makefile是痛苦的,相對與用./configure指定參數。這樣使得你的安裝包更友好。
知道了autotools的優勢後,就知道對於怎麼樣的“簡單”包可以自己手寫Makefile,怎樣的最好用autotools。
二、Makefile的工程組織參考:
1. Makefile 原則:──── 總原則是:Makefile是寫規則,相比於Makefile.am是寫賦值!
(1) 非源檔案層的Makefile寫 make -C subdir 和 export 變數:
make -C subdir :這個是說進入 subdir 來執行裡面的make(當然裡面要有Makefile)。如果子目錄間有依賴關係,則這裡應調整順序來make -C
export 變數: 如果有上層Makefile的變數,希望傳給make -C subdir中,subdir的Makefile,則需要用export給出。可能用到的是將頂層目錄的絕對路徑傳給下層,因為下層可能希望這樣來找到什麼。
(2) 源檔案層寫“顯示規則”和“抽象規則”,並“自成整體”:
1) 顯式規則:就是一個庫或程式,依賴哪些objects,然後採取的動作是什麼。objects必須全部列出。一般用Make內建函數或內嵌shell來協助我們列出。
2) 抽象規則:就是.c.o 的規則,見下面例子檔案。
3) 自成整體:就是說比如你要引用另一個同級subdir下的標頭檔,你可以把這些標頭檔引用地統一寫到上層的Makefile,用export到處,然後下層的眾多Makefile就直接用了。也可以在下層的Makefile中自己維護這個。我選擇後者,因為這樣使得子目錄,可以單獨Make,不需要藉助上層Makefile。但是我在例子裡給出的,卻是在上層export的。
說明 ──── 為什麼不用隱式規則來產生.o:
隱式規則,比如對C檔案的是 $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $< -o $@,
而隱式規則是不夠的!因為它的規則裡,如main.o,只依賴於main.c,而不依賴於main.c中包含的標頭檔,以及標頭檔中包含的標頭檔。。。這樣,如果你的標頭檔修改了,不會導致重新make。為此,我們引入了 gcc 的 -MD -MF $*.d 參數,這個參數,使得編譯一個 main.c 時,會產生一個 main.d,裡面含有 main.o 所有的依賴,包含所有標頭檔和標頭檔中引用的標頭檔!然後再在Makefile中用 “include 那些.d 檔案” 把依賴關係包含進來!這樣完善了最終的Makefile。 而為了引入 -MD -MF $*.d 就需要自己寫.c -> .o的規則,即.c.o: 規則,寫法見下面。
2. 技巧集中:
(1) 所有的東西,請先用變數定義後再引用:
比如你要產生的庫名叫libhello.a,那麼在上面先用 LIB_NAME := libhello.a 來給出,然後下面用$(LIB_NAME)來引用之。
(2) 用內嵌shell,避免記憶Makefile的內建函數:
如,列出所有的objects依賴,因為所有的.o來自.c,可以:
OBJS := $(shell ls|grep '/.c'|sed 's//.c//.o/g')
如果用Make內建的函數,就要用到$(wildcard ...) 和 $(patsubst ...)
(3) 用-避免忽略錯誤:
上面提到 “include 那些.d”,但是,第一次Make的時候,make顯然會先完善這個Makefile,它看到這個include,會試圖去把.d們引入進來,但是這時,還沒有調用gcc呢,當然沒有.d,所以,make會退出。除非你用 - 加在include前面,就會忽略這個錯誤了。然後,當第二次make的時候,.d已經有了,由.d引入的規則就開始生效了。
(4) 用 @ 避免列印資訊:
在make的動作中,比如你寫 @mkdir -p $(BUILD_DIR) 即可以避免這個資訊在動作執行時被列印出來。
(5) 盡量用 $@ $< 和 $^:
下面的例子中出現了很多,尤其在VPATH的Makefile中,要用這些更好。
3. 例子
在這個例子中,頂層有一個總Makefile,然後有一個src目錄和build目錄。
src中放源碼,如果你在頂層 make all 或 make clean,則是針對src目錄。
build目錄是示範VPATH的make,如果你在頂層 make all-vpath 或 make clean-vpath,則是針對build目錄。
總體的Makefile(和src,build同級目錄):
# Makefile for the project
# It's intened to make everything you like, for example the src dir or other non-code things
export TOP_DIR := $(shell pwd) # 這個在下層的Makefile中有用到
SRC_DIR := src
BUILD_DIR := build
.PHONY: all clean
all:
@make -C $(SRC_DIR)
clean:
@make -C $(SRC_DIR) clean
all-vpath:
@make -C $(BUILD_DIR)
clean-vpath:
@make -C $(BUILD_DIR) clean
(1) src 目錄:
-------- src/Makefile: -------------
#MAKEFLAGS := -s
SRC_DIR := $(shell pwd)
INCLUDES := -I$(SRC_DIR)/libhello # use INCLUDES to contain -I (NOTICE: these names are made by me)
LDFLAGS := -L$(SRC_DIR)/libhello/build # use LDFLAGS to contain -L
MYFLAGS := -Wall -g -O3 # use MYFLAGS to contain other flags
export CFLAGS := $(INCLUDES) $(LDFLAGS) $(MYFLAGS) # use CFLAGS to contain all flags passed to gcc
export HEADER_DIR := $(SRC_DIR)/libhello # just for subdir Makefile convenience
export LIBS_DIR := $(SRC_DIR)/libhello/build # just for subdir Makefile convenience
.PHONY: all clean
all:
@echo '===== building libhello =====' # build libhello first, then bin
make -C $(SRC_DIR)/libhello
@echo
@echo '===== building binary ====='
make -C $(SRC_DIR)/bin
clean:
@echo '===== cleaning libhello ====='
make -C $(SRC_DIR)/libhello clean
@echo
@echo '===== cleaning binary ====='
make -C $(SRC_DIR)/bin clean
-------- src/libhello/Makefile: ---------
LIB_NAME := libhello.a
OBJS := $(shell ls|grep '/.c'|sed 's//.c//.o/g') # list all possible .o files' names for using in "explicit rule"
DEPS := $(shell ls|grep '/.c'|sed 's//.c//.d/g') # list all possible .d files' names for using in "include"
# the place to keep build process files
BUILD_DIR := build
# Use vpath to let make find .a and .o in $(BUILD_DIR), when "explicit rule" are checked by Make,
# because *.o are in the $(BUILD_DIR) dir and not in the dir where make enters(where Makefile exits)
# VPATH is used to find target and dependence files. but has no effect on the command line files
VPATH := $(BUILD_DIR)
# "explicit rule"
$(LIB_NAME): $(OBJS)
cd $(BUILD_DIR) && ar rcs $@ $(OBJS) # cd $(BUILD_DIR) because in .c.o we moved *.o to $(BUILD_DIR)
# "abstract rule"
.c.o:
@mkdir -p $(BUILD_DIR) # avoid failure if no $(BUILD_DIR) exists.
$(CC) $(CFLAGS) -MD -MF $*.d -c $< -o $@ # produce .d .o files
@mv $*.d $*.o $(BUILD_DIR) # move them to $(BUILD_DIR) to make source dir clean.
# include .o <- .c & .h dependence
-include $(addprefix $(BUILD_DIR)/, $(DEPS))
# PHONY target
.PHONY: clean
clean:
rm -rf $(BUILD_DIR)
---------- src/libhello/hello1.c hello2.c hello.h 和上面 automake例子一樣 -------
---------- src/bin/Makefile ------------
# for this file is similiar to that in libhello, we omit many comments.
PROG := kid
OBJS := $(shell ls|grep '/.c'|sed 's//.c//.o/g')
DEPS := $(shell ls|grep '/.c'|sed 's//.c//.d/g')
LIBS := $(shell ls $(LIBS_DIR)/*.a)
L_LIBS := -lhello -lm
# the place to keep build process files
BUILD_DIR := build
# vpath to let make find .a and .o in $(BUILD_DIR)
VPATH := $(BUILD_DIR)
$(PROG): $(OBJS) $(LIBS)
cd $(BUILD_DIR) && $(CC) $(OBJS) -o $@ $(CFLAGS) $(L_LIBS)
.c.o:
@mkdir -p build
$(CC) $(CFLAGS) -MD -MF $*.d -c $< -o $@
@mv $*.d $*.o $(BUILD_DIR)
-include $(addprefix $(BUILD_DIR)/, $(DEPS))
.PHONY: clean
clean:
rm -rf $(BUILD_DIR)
---------- src/bin/main.c 和上面Automake中的例子一樣 ----------
(2) build 目錄:
首先要說明,為了支援VPATH,你需要做什麼:
a. 在build中建立和src中Makefile層次一樣的分類樹結構,並放上基本相同的Makefile。要改的如下:
b. 用VPATH指出“顯示規則”和“抽象規則”中,dependences 所在的目錄在src中的什麼地方。這樣target: object一行
c. 對於其他的需要找檔案的地方,如 -I,還是需要你自己寫明,不能依靠VPATH,沒用的。
--------- build/Makefile -----------
.PHONY: all clean
all:
make -C src
clean:
make -C src clean
-------- build/src/Makefile ---------
#MAKEFLAGS := -s
INCLUDES := -I$(TOP_DIR)/src/libhello
LDFLAGS := -L$(TOP_DIR)/build/src/libhello
MYFLAGS := -Wall -g -O3
export CFLAGS := $(INCLUDES) $(LDFLAGS) $(MYFLAGS)
.PHONY: all clean
all:
@echo '===== building libhello ====='
make -C libhello
@echo
@echo '===== building binary ====='
make -C bin
clean:
@echo '===== cleaning libhello ====='
make -C libhello clean
@echo
@echo '===== cleaning binary ====='
make -C bin clean
---------- build/src/libhello/Makefile ---------
LIB_NAME := libhello.a
SRC_DIR := $(TOP_DIR)/src/libhello
OBJS := $(shell ls $(SRC_DIR)|grep '/.c'|sed 's//.c//.o/g')
DEPS := $(shell ls $(SRC_DIR)|grep '/.c'|sed 's//.c//.d/g')
# the place to keep build process files
BUILD_DIR := build
# vpath to let make find .o in $(SRC_DIR) when executing "explicit" and "abstract" rule.
# VPATH is used to find target and dependence files. but has no effect on the command line files
VPATH := $(SRC_DIR)
# explicit rule
$(LIB_NAME): $(OBJS)
ar rcs $@ $^
# abstract rule
.c.o:
$(CC) $(CFLAGS) -MD -MF $*.d -c $< -o $@
# include .o <- .c & .h dependence
-include $(DEPS)
# PHONY target
.PHONY: clean
clean:
rm -rf *.o *.d $(LIB_NAME)
----------- build/src/bin/Makefile ------------
PROG := kid
SRC_DIR := $(TOP_DIR)/src/bin
OBJS := $(shell ls $(SRC_DIR)|grep '/.c'|sed 's//.c//.o/g')
DEPS := $(shell ls $(SRC_DIR)|grep '/.c'|sed 's//.c//.d/g')
LIBS := $(shell ls $(TOP_DIR)/build/src/libhello/*.a)
L_LIBS := -lhello -lm
# vpath to let make find .o in $(SRC_DIR) when
# executing "explicit" and "abstract" rule.
VPATH := $(SRC_DIR)
# explicit rule
$(PROG): $(OBJS) $(LIBS)
$(CC) $^ -o $@ $(CFLAGS) $(L_LIBS)
# abstract rule
.c.o:
$(CC) $(CFLAGS) -MD -MF $*.d -c $< -o $@
-include $(DEPS)
.PHONY: clean
clean:
rm -rf *.o *.d $(PROG)