原文:實踐:GNU構建系統
在上一篇概念:GNU構建系統和Autotool,我對GNU構建系統從使用者視角和開發者視角分別進行了闡述。本篇從我的實踐總結的角度,並闡述如何從頭開始規劃一個基於GNU構建系統的專案。事實上,隨著開發者對跨平臺認知的深入和完善,才能逐漸掌握GNU構建。注意:本文的例子不依賴於任何IDE和編輯器。這樣讀者可以從根本上認識到每個檔案的作用。
安裝autotools
需要安裝的工具包括autoconf、automake、libtool。
目錄結構規劃
首先,我們需要規劃專案的目錄結構。假設,我們的專案叫gnu-build
。設想如下目錄結構:
gnu-build
|---build(用於編譯)
|---src
|---common
|---Makefile.am
|---pool.c
|---alloc.c
|---list.c
|...
|---core
|---Makefile.am
|---main.c
|...
|---test
|---Makefile.am
|---test.c
|...
|---Makefile.am
|---configure.ac
|---Makefile.am
|---.gitignore
從上面的目錄結構可以看出:
-
根目錄有一個
configure.ac
,這是構建系統的核心檔案之一,描述整個構建的依賴和輸出,是configure
指令碼的原型。 -
每個目錄(包括根目錄)都有一個
Makefile.am
,這些檔案是生成Makefile
的主要來源。使用Makefile.am的優點是可以結合configure.ac
、比手動編寫Makefile
方便很多。 -
在
src
目錄下放置原始碼,原始碼被分成common
、core
、test
。common
用來實現一些可重用的程式碼,比如通用資料結構,記憶體管理,異常的封裝;core
用來放置直接編譯成可執行程式的程式碼,比如main.c等;test
用於編寫單元測試程式。 -
build
目錄用於存放編譯過程中的臨時檔案和編譯得到了目標檔案。一般我們總是cd
在build
目錄中,並執行../configure
來configure
,並在build目錄下make。這樣的話,由configure
產生的檔案不會汙染原始碼空間。我們需要做的只是在.gitignore
中新增build/
。
在使用autoreconf的過程中,還將在各個目錄下生成其他的檔案(尤其是根目錄)。現在我們只需要建立上述必要檔案。
configure.ac
可以通過在根目錄下執行autoscan
程式生成。如果你已經有一些程式碼了,使用autoscan生成configure.ac是個不錯的開始。
configure.ac的基本編寫
通用巨集
每個configure.ac
都需要如下兩行。分別說明需要的autoconf的最低版本,以及程式的包名、版本、bug反饋郵件地址。
AC_PREREQ(2.59)
AC_INIT([gnu-build], [1.0], [support@gnubuild.org])
configure.ac
通篇幾乎都是採用這種類似函式呼叫的語法編寫,這些稱為巨集
的語句,會被autoconf工具識別,並展開成相應的shell指令碼,最終成為configure
指令碼。除此之外,也可以混合地直接編寫shell指令碼。autoconf預置了很多實用的巨集,可以減少工作量,後面你將看到巨集
的價值。
可以直接編寫shell指令碼,但是推薦儘量使用巨集。因為shell程式有很多種(sh,bash,ksh,csh…),想要寫出可移植的shell並不是件容易的事情。
接著,通常使用AC_CONFIG_SRCDIR
來定位一個原始碼檔案,如此一來,autoconf程式會檢查該檔案是否存在,以確保autoconf的工作目錄的正確性。這裡,我們指向src/core/main.c
。
AC_CONFIG_SRCDIR([src/core/main.c])
定義輸出的巨集
一般來說,都會編寫一個header
輸出定義。這是我們用到的第一個輸出指令。輸出指令告訴configure
,需要生成哪些檔案。AC_CONFIG_HEADERS
的含義是在指定的目錄生成.h
,一般叫做config.h
,你也可以指定其他名字。
AC_CONFIG_HEADERS([src/common/config.h])
那麼這個config.h
究竟有什麼用呢?回憶一下,configure
程式的主要目的是檢測目標平臺的軟硬體環境,從而在實際呼叫make
命令編譯程式前,對編譯工作進行一個預先的配置,這裡的配置落實到底,主要就是生成Makefile
和config.h
:
Makefile.am --> Makefile.in --> Makefile
|
configure*
|
config.h.in --> config.h
那麼我們的程式必需要通過某種方式,得知環境的不同,從而通過預編譯做出響應。這裡的響應主要分兩塊:
-
對於原始碼而言,通過
config.h
中的巨集定義,來改變編譯行為。 -
對於Makefile.am而言,通過
configure.ac
匯出的變數,來動態改變Makefile。
在後面的敘述中,可以通過程式碼體會這兩點。所以這裡,為了讓我們的原始碼有能力根據環境來改變編譯行為,生成config.h通常是必要的。
另一個輸出巨集是AC_CONFIG_FILES
,針對這個例子,告訴autoconf,我們需要輸出Makefile檔案:
AC_CONFIG_FILES([Makefile
src/Makefile
src/core/Makefile
src/common/Makefile
src/test/Makefile
])
AC_OUTPUT
注意到每個目錄都需要由對應的Makefile檔案,這是automake多目錄組織Makefile的通用做法。後面會講到如何編寫各個目錄下的Makefile.am
。
AC_CONFIG_FILES
一般跟AC_OUTPUT
一起寫在configure.ac
的最後部分。
automake宣告
為了配合automake,需要用AM_INIT_AUTOMAKE
初始化automake:
AM_INIT_AUTOMAKE([foreign])
這裡foreign
是個可選項,設定foreign
跟呼叫automake --foreign
是等價的,前一篇有講到。
libtool宣告
配合使用libtool,需要加入LT_INIT
,這樣autoreconf
會自動呼叫libtoolize
LT_INIT
編譯器檢查
configure可以幫助我們檢查編譯和安裝過程中需要的系統工具是否存在。一般在進行其他檢查前,先做此類檢查。例如下面是一些常用的檢查:
# 宣告語言為C
AC_LANG(C)
# 檢查cc
AC_PROG_CC
# 檢查預編譯器
AC_PROG_CXX
# 檢查ranlib
AC_PROG_RANLIB
# 檢查lex程式,gnu下通常叫flex
AC_PROG_LEX
# 檢查yacc,gnu下通常叫bison
AC_PROG_YACC
# 檢查sed
AC_PROG_SED
# 檢查install程式
AC_PROG_INSTALL
# 檢查ln -s
AC_PROG_LN_S
針對這個例子我們只需要檢查cc
,cxx
就可以了。
Makefile.am的基本編寫
Makefile.am
檔案是一種更高層次的Makefile,抽象程度更高,比Makefile更容易編寫,除了相容Makefile語法外,通常只需包含一些變數定義即可。automake程式負責解析,並生成Makefile.in
,而Makefile.in從表現上與Makefile已經十分接近,只差變數替換了。configure指令碼執行後,Makefile.in將最終轉變成Makefile。
子目錄引用
在本例中每個目錄下都有Makefile.am。根目錄的Makefile.am生成的Makefile將是make程式的預設入口,但是根目錄實際上並不包含任何需要構建的檔案。對於需要引用子目錄的Makefile來構建的時候,使用SUBDIRS
羅列包含其他Makefile.am的子目錄。因此,對於根目錄的Makefile.am只需要寫一行:
SUBDIRS = src
同理,src目錄下的Makefile.am只需要
SUBDIRS = common src test
定義目標
對於包含有原始碼檔案的目錄。首先,我們需要定義編譯的目標,目標可能是庫檔案或可執行檔案,目標又分為需要安裝和不需要安裝兩種。例如對於common目錄
下的原始碼,我們希望生成一個不需要安裝的庫檔案(使用libtool),因為這個庫檔案只在本專案內使用,那麼common/Makefile.am
應當這樣寫:
noinst_LTLIBRARIES = libcommon.la
libcommon_la_SOURCES = pool.c alloc.c list.c
定義了一個目標libcommon.la
。由於使用libtool,所以庫檔案必須以lib
開頭,字尾為.la
。
目標的基本格式為where_PRIMARY = targets ...
where
表示安裝位置,可選擇bin、lib、noinst、check(make check時構建),還可以自定義。我們著重討論前三種:
-
bin
:表示安裝到bindir目錄下,這種情況下會編譯出動態庫 -
lib
:表示安裝到libdir目錄下,這種情況下會編譯出動態庫 -
noinst
:表示不安裝,這種情況下會編譯出靜態庫,在其他目標引用該目標時將進行靜態連結
PRIMARY
可以是PROGRAMS
LIBRARIES
LTLIBRARIES
HEADERS
SCRIPTS
DATA
。著重討論前三種:
-
PROGRAMS
:表示目標是可執行檔案 -
LIBRARIES
:表示目標是庫檔案,通過字尾來區別靜態庫或動態庫 -
LTLIBRARIES
:表示是libtool庫檔案,統一字尾為.la
與Makefile的思想一樣,目標的生成需要定義來源,通常目標是有一些源程式檔案得到的。Makefile.am中只需定義xxx_SOURCES
,後面跟隨構建xxx這個目標需要的原始碼檔案列表即可。注意到xxx是目標的名字,並且.
字元需要使用_
代替。
定義編譯選項
core
目錄下需要生成可執行目標,但是在連結時,需要用到libcommon.la
,此時core/Makefile.am
可以寫成
bin_PROGRAMS = gnu-build
GNU_BUILD_SOURCES = main.c
GNU_BUILD_LIBADD = $(top_builddir)/src/common/libcommon.la
這裡多了一行GNU_BUILD_LIBADD
,target_LIBADD的形式表示為target新增庫檔案的引用,這種引用是靜態的還是動態的取決於引用的庫檔案是否支援動態庫,如果支援動態庫,libtool優先採用動態連結。而由於libcommon.la
指定為noinst
,所以不可能以動態連結的形式存在,這裡必然是靜態連結。
$(top_builddir)
引用的是make發生時的工作目錄,上文提到,我們將在build目錄下進行構建,那麼庫檔案會生成在build目錄下,而不是原始碼根目錄下,所以$(top_builddir)
實際就是gnu-build/build
目錄,而這樣可以很好的支援在另一個目錄中編譯程式。與之相對應的是$(top_srcdir)
對應的是原始碼的根目錄,即gnu-build
目錄。
還有多個可以配置用於改變編譯和連結選項的配置項:
-
xxx_LDADD:為連結器增加引數,一般用於第三方庫的引用。比如
-L
-l
-
xxx_LIBADD:宣告庫檔案引用,一般對於本專案中的庫檔案引用採用這種形式。
-
xxx_LDFLAG:連結器選項
-
xxx_CFLAGS:c編譯選項,如
-D
-I
-
xxx_CPPFLAGS:預編譯選項
-
xxx_CXXFLAGS: c++編譯選項
如果xxx是AM
,則表示全域性target都採用這個選項。
安裝路徑
剛剛提到的bindir
和libdir
是configure目錄體系下的,類似的路徑還有:
prefix /usr/local
exec-prefix {prefix}
bindir {exec-prefix}/bin
libdir {exec-prefix}/lib
includedir {prefix}/include
datarootdir {prefix}/share
datadir {datarootdir}
mandir {datarootdir}/man
infodir {datarootdir}/info
...
可以看到prefix
在這裡的地位是一個頂層的路徑,其他的路徑直接或間接與之有關。而prefix的預設值為/usr/local
。所以可執行程式預設總是安裝在/usr/local/bin
。使用者總是可以在呼叫configure
指令碼時通過--prefix
指定prefix。更詳細的路徑列表可以通過./configure --help
瞭解。
開始構建
填充一些原始碼後,就可以使用autoreconf了,只需要在根目錄下執行autoreconf --install
即可。
[root@xxx gnu-build]# autoreconf --install
前一篇中,對autoreconf的整個過程和產生的檔案做了詳盡的分析和闡述,讀者也應該十分清楚這裡將得到若干Makefile.in
和common/config.h.in
檔案。
如果這個過程順利的話,就可以在build目錄下構建了:
# cd build
# ../configure
# make
這裡configure後,會在build目錄下生成對應位置的Makefile和common/config.h檔案,而不是生成在原始碼目錄中從而汙染原始碼
至此,你已經完成了一個專案的基本構建框架,後面的事情,就是逐步完善構建對環境的依賴。
在configure.ac中配置環境檢查
autoconf
為程式設計師提供的最為重要的功能就是提供了一種便捷、穩定、可移植的方式,讓程式能在特定目標平臺和目標環境上安全的編譯執行程式。不過,autoconf
只是提供了一些巨集,用來簡化環境檢查。而究竟要檢查些什麼,如何合理的利用這些巨集完成目的,依舊是需要大量的積累的。筆者在這裡對一些常用的巨集進行一些介紹。
可執行檔案檢查
有些第三方庫在安裝到系統後,會附帶安裝若干可執行程式,並可在環境變數的支援下直接執行。有時,我們通過檢查此類可執行程式是否存在,來初步判斷該第三方庫是否已經安裝在目標平臺。其中一種常用的巨集是AC_CHECK_PROGS
# 宣告一個變數PERL,檢查perl程式是否存在並可執行
# 如果不存在$PERL變數將是NOTFOUND,如果存在$PERL變數將是perl
AC_CHECK_PROGS([PERL], [perl], [NOTFOUND])
# 宣告一個變數TAR,檢查tar和gtar程式是否存在並可執行
# 如果不存在$TAR變數將是:,如果存在,第一個可用的程式名將賦值給$TAR
AC_CHECK_PROGS([TAR], [tar gtar], [:])
GNU軟體有一種利用pkg-config,來進行自描述的機制。即可以通過註冊軟體自身(通常提供庫檔案的軟體),讓pkg-config能夠返回庫檔案的安裝路徑等資訊,以便以一種統一的方式提供給呼叫程式。有些庫軟體附帶有獨立的config程式,比如
pcre-config
和apr-1-config
。如果對這類庫提供軟體需要檢查依賴和編譯連結,通常可以通過AC_CHECK_PROGS
來檢查config程式,從而得到編譯連結選項。
列印訊息巨集
列印訊息可以作為除錯手段,同時也可以在使用者在configure過程中,給予提示資訊。
# error將終止configure
AC_MSG_ERROR([zlib is required])
# warn不會終止configure
AC_MSG_WARN([zlib is not found, xxx will not be support.])
注意到AC_MSG_ERROR
將中斷configure的執行,一般用於必需的編譯環境無法滿足時。
庫檢查巨集
檢查某庫是否存在是最重要的功能,因為我們程式往往需要這些庫,甚至是庫中的某個函式的支援才能正確的執行。
使用AC_CHECK_LIB
檢查庫以及其中的函式是否存在,該巨集的原型為:
AC_CHECK_LIB (library, function, [action-if-found],[action-if-not-found], [other-libraries])
-
library:需要檢查的庫名,無需
lib
字首,比如為了檢查libssl
是否存在,這裡需要傳入ssl
-
function:這個庫中的某個函式名
-
action-if-found:如果找到執行某個動作,這個動作可以是另一個巨集,可以是shell指令碼。如果不指定這個引數,預設在
LIBS
環境變數中增加-l
選項,從而將在連結過程中將這個庫連結進來。比如-lssl
。並且在config.h中定義一個巨集HAVE_LIBlibrary
,例如HAVE_LIBSSL
。我們的程式碼可以根據這個巨集得知當前編譯環境是否提供libssl
。 -
action-if-not-found:如果找不到則執行某個動作
通過下面幾個巨集可以檢查系統是否包含某些標頭檔案,以及是否支援某些函式:
-
AC_CHECK_FUNCS
:檢查是否支援某些函式。作為檢查的副作用,在config.h中會定義一個巨集HAVE_funcs
(全大寫) -
AC_CHECK_HEADERS
:檢查是否支援某些標頭檔案。作為檢查的副作用,在config.h中會定義一個巨集HAVE_header_H
(全大寫)
來舉個例子,大家知道libiconv
是一個可以在不同字符集間進行轉化的庫,如果我們的程式希望能夠在不同字符集間轉化的字串的話,可以使用該庫。然而,在不同平臺上,該庫的移植方式有些區別。
gnu的標準c庫(glibc)在很早的時候就把libiconv整合到了glibc中,因此在linux上可以無需額外的庫支援即可使用iconv
。然而,在非linux上,很可能需要額外的libiconv
庫。那麼如果在非linux的平臺上編寫可移植的程式,可以參考如下的巨集組合:
AC_CHECK_FUNCS(iconv_open, HAVE_ICONV=yes, [])
if test "x$HAVE_ICONV" = "xyes"; then
AC_CHECK_HEADERS(langinfo.h, [], AC_MSG_WARN([langinfo.h not found]))
AC_CHECK_FUNCS([nl_langinfo], [], [AC_MSG_WARN([nl_langinfo not found])])
else
AC_CHECK_LIB([iconv], [libiconv_open], [HAVE_ICONV=yes], [AC_MSG_WARN([no iconv found, will not build xm_charconv])])
if test "x$HAVE_ICONV" = "xyes"; then
LIBICONV="-liconv"
SAVED_LIBS=$LIBS
LIBS="$LIBS $LIBICONV"
AC_CHECK_HEADERS(langinfo.h,
AC_CHECK_FUNCS([nl_langinfo], [], [AC_MSG_ERROR([nl_langinfo not found in your libiconv])]),
AC_CHECK_FUNCS([locale_charset], [], [AC_MSG_ERROR([no langinfo.h nor locale_charset found in libiconv])]))
LIBS=$SAVED_LIBS
fi
fi
在這個例子中,我們可以看到許多技巧。我們來逐一解讀一下:
-
首先通過
AC_CHECK_FUNCS
檢查iconv_open
函式,如果在Linux平臺上,通常該函式可以在沒有任何額外庫的情況下提供,所以HAVE_ICONV
這個臨時變數將設定為yes
。 -
接著通過shell的
if
測試判斷臨時變數HAVE_ICONV
是否為yes
。 -
如果已經檢測到iconv,那麼進一步檢查
langinfo.h
標頭檔案和nl_langinfo
函式,無論是否能檢查通過,由於使用了AC_MSG_WARN
,所以configure並不會失敗退出,最多隻是提示使用者警告。更重要的是,我們可以通過config.h中的巨集,在程式碼中得知是否支援標頭檔案和函式,從而調整編譯分支。具體的在這個例子中這兩個巨集分別為HAVE_LANGINFO_H
和HAVE_NL_LANGINFO
。 -
在非linux下可能需要額外的libiconv庫,所以在
else
分支中,立刻採用AC_CHECK_LIB
檢測iconv
庫,以及其中的libiconv_open
函式。同樣的,如果存在,HAVE_ICONV
這個臨時變數將設定為yes
。 -
在接下來的if測試中,使用到了
$LIBS
變數,這是一個由編譯器支援的變數,表示在連結階段的額外庫引數。當我們檢測到libiconv後,就給這個變數臨時地新增-liconv
。這樣接下來的AC_CHECK_FUNCS
時,可以利用$LIBS
在額外的庫中查詢函式。 -
檢查
langinfo.h
標頭檔案,如果存在則再檢查nl_langinfo
函式;如果不存在,則檢查locale_charset
函式。從邏輯上看,要麼langinfo.h
和nl_langinfo
同時存在,要麼有locale_charset
函式,否則就終止configure。 -
最後重置
$LIBS
變數。
變數匯出
configure指令碼的檢測結果應當有兩個主要出口,一是config.h,它幫助我們在原始碼中建立編譯分支;二是Makefile.am
,我們可以在Makefile.am
中基於這些匯出的變數,改變構建方式。
有些巨集可以自動幫我們匯出到config.h
,關於這一點上文已經有所闡述了。而希望匯出到Makefile.am則需要我們自己手動呼叫相關巨集。這裡主要有兩個巨集:
-
AC_SUBST
:將一個臨時變數,匯出到Makefile.am。實際是在Makefile.in中宣告一個變數,並且在生成Makefile時,由configure指令碼對變數的值進行替換。 -
AM_CONDITIONAL
:由automake引入,可進行一個條件測試,從而決定是否匯出變數。
例如,針對上面iconv的例子,我們有個臨時變數HAVE_ICONV
,如果iconv在當前平臺可用,此時HAVE_ICONV
將會是yes
。所以可以使用AM_CONDITIONAL
匯出變數:
AM_CONDITIONAL([HAVE_ICONV], [test x$HAVE_ICONV != x])
或者無論如何都匯出HAVE_ICONV
AC_SUBST(HAVE_ICONV)
在Makefile.am中,我們可以對變數進行引用,這樣xm_charconv.la就將在HAVE_ICONV匯出的情況下構建:
if HAVE_ICONV
xm_charconv_LTLIBRARIES = xm_charconv.la
...
endif
提供額外使用者引數支援
很多軟體都支援使用者在configure階段,可通過--with-xxx
--enable-xxx
等命令列選項對軟體進行模組配置或編譯配置。以--with-xxx
為例,我們需要AC_ARG_WITH
巨集:
AC_ARG_WITH(configfile,
[ --with-configfile=FILE default config file to use],
[ ZZ_CONFIGFILE="$withval"],
[ ZZ_CONFIGFILE="${sysconfdir}/zz.conf"]
)
AC_SUBST(ZZ_CONFIGFILE)
FILE
定義該引數的值應當是一個檔案路徑(DIR
要求一個目錄路徑),該巨集需要提供一個預設值,這個例子中是${sysconfdir}/zz.conf
,${sysconfdir}
引用了${prefix}/etc
,而$withval
從命令列中引用--with-configfile
的值。
最後我們通過AC_SUBST
匯出一個臨時變數。
上一節提到,匯出的臨時變數可以在Makefile.am中引用,所以我們可以在Makefile.am中通過-D
傳遞給程式碼,從而在程式碼中通過巨集來引用:
CFLAGS += -DCONFIGFILE="$(ZZ_CONFIGFILE)"
總結
本文以一個例子,一步步使用GNU構建系統來建立一個專案,並介紹了一些常用的檢測巨集。事實上,autotool還有很多巨集,甚至可以自定義巨集。能否合理利用autotool取決於程式設計師對可移植性這個問題的經驗和理解。