原文:實踐:GNU構建系統html
在上一篇概念:GNU構建系統和Autotool,我對GNU構建系統從用戶視角和開發者視角分別進行了闡述。本篇從個人實踐總結的角度,並闡述如何從頭開始規劃一個基於GNU構建系統的項目。事實上,隨着開發者對跨平臺認知的深刻和完善,才能逐漸掌握GNU構建。注意:本文的例子不依賴於任何IDE和編輯器。這樣讀者能夠從根本上認識到每一個文件的做用。linux
須要安裝的工具包括autoconf、automake、libtool。c++
首先,咱們須要規劃項目的目錄結構。假設,咱們的項目叫gnu-build
。設想以下目錄結構:git
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
腳本的原型。shell
每一個目錄(包括根目錄)都有一個Makefile.am
,這些文件是生成Makefile
的主要來源。使用Makefile.am的優勢是能夠結合configure.ac
、比手動編寫Makefile
方便不少。安全
在src
目錄下放置源代碼,源代碼被分紅common
、core
、test
。common
用來實現一些可重用的代碼,好比通用數據結構,內存管理,異常的封裝;core
用來放置直接編譯成可執行程序的代碼,好比main.c等;test
用於編寫單元測試程序。bash
build
目錄用於存放編譯過程當中的臨時文件和編譯獲得了目標文件。通常咱們老是cd
在build
目錄中,並執行../configure
來configure
,並在build目錄下make。這樣的話,由configure
產生的文件不會污染源碼空間。咱們須要作的只是在.gitignore
中添加build/
。數據結構
在使用autoreconf的過程當中,還將在各個目錄下生成其餘的文件(尤爲是根目錄)。如今咱們只須要建立上述必要文件。框架
configure.ac
能夠經過在根目錄下執行autoscan
程序生成。若是你已經有一些代碼了,使用autoscan生成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,須要用AM_INIT_AUTOMAKE
初始化automake:
AM_INIT_AUTOMAKE([foreign])
這裏foreign
是個可選項,設置foreign
跟調用automake --foreign
是等價的,前一篇有講到。
配合使用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,抽象程度更高,比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文件,而不是生成在源碼目錄中從而污染源碼
至此,你已經完成了一個項目的基本構建框架,後面的事情,就是逐步完善構建對環境的依賴。
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取決於程序員對可移植性這個問題的經驗和理解。