GNU從頭構(gòu)建系統(tǒng)實踐
在上一篇概念:GNU構(gòu)建系統(tǒng)和Autotool,我對GNU構(gòu)建系統(tǒng)從用戶視角和開發(fā)者視角分別進行了闡述。本篇從我的實踐總結(jié)的角度,并闡述如何從頭開始規(guī)劃一個基于GNU構(gòu)建系統(tǒng)的項目。事實上,隨著開發(fā)者對跨平臺認知的深入和完善,才能逐漸掌握GNU構(gòu)建。注意:本文的例子不依賴于任何IDE和編輯器。這樣讀者可以從根本上認識到每個文件的作用。
安裝autotools
需要安裝的工具包括autoconf、automake、libtool。
目錄結(jié)構(gòu)規(guī)劃
首先,我們需要規(guī)劃項目的目錄結(jié)構(gòu)。假設(shè),我們的項目叫g(shù)nu-build。設(shè)想如下目錄結(jié)構(gòu):
- 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
從上面的目錄結(jié)構(gòu)可以看出:
- 根目錄有一個configure.ac,這是構(gòu)建系統(tǒng)的核心文件之一,描述整個構(gòu)建的依賴和輸出,是configure腳本的原型。
- 每個目錄(包括根目錄)都有一個Makefile.am,這些文件是生成Makefile的主要來源。使用Makefile.am的優(yōu)點是可以結(jié)合configure.ac、比手動編寫Makefile方便很多。
- 在src目錄下放置源代碼,源代碼被分成common、core、test。common用來實現(xiàn)一些可重用的代碼,比如通用數(shù)據(jù)結(jié)構(gòu),內(nèi)存管理,異常的封裝;core用來放置直接編譯成可執(zhí)行程序的代碼,比如main.c等;test用于編寫單元測試程序。
- build目錄用于存放編譯過程中的臨時文件和編譯得到了目標文件。一般我們總是cd在build目錄中,并執(zhí)行../configure來configure,并在build目錄下make。這樣的話,由configure產(chǎn)生的文件不會污染源碼空間。我們需要做的只是在.gitignore中添加build/。
在使用autoreconf的過程中,還將在各個目錄下生成其他的文件(尤其是根目錄)?,F(xiàn)在我們只需要創(chuàng)建上述必要文件。
configure.ac可以通過在根目錄下執(zhí)行autoscan程序生成。如果你已經(jīng)有一些代碼了,使用autoscan生成configure.ac是個不錯的開始。
configure.ac的基本編寫
通用宏
每個configure.ac都需要如下兩行。分別說明需要的autoconf的最低版本,以及程序的包名、版本、bug反饋郵件地址。
- AC_PREREQ(2.59)
- AC_INIT([gnu-build], [1.0], [support@gnubuild.org])
configure.ac通篇幾乎都是采用這種類似函數(shù)調(diào)用的語法編寫,這些稱為宏的語句,會被autoconf工具識別,并展開成相應(yīng)的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程序的主要目的是檢測目標平臺的軟硬件環(huán)境,從而在實際調(diào)用make命令編譯程序前,對編譯工作進行一個預先的配置,這里的配置落實到底,主要就是生成Makefile和config.h:
- Makefile.am --> Makefile.in --> Makefile
- |
- configure*
- |
- config.h.in --> config.h
那么我們的程序必需要通過某種方式,得知環(huán)境的不同,從而通過預編譯做出響應(yīng)。這里的響應(yīng)主要分兩塊:
- 對于源代碼而言,通過config.h中的宏定義,來改變編譯行為。
- 對于Makefile.am而言,通過configure.ac導出的變量,來動態(tài)改變Makefile。
在后面的敘述中,可以通過代碼體會這兩點。所以這里,為了讓我們的源碼有能力根據(jù)環(huán)境來改變編譯行為,生成config.h通常是必要的。
另一個輸出宏是AC_CONFIG_FILES,針對這個例子,告訴autoconf,我們需要輸出Makefile文件:
- AC_CONFIG_FILES([Makefile
- src/Makefile
- src/core/Makefile
- src/common/Makefile
- src/test/Makefile
- ])
- AC_OUTPUT
注意到每個目錄都需要由對應(yīng)的Makefile文件,這是automake多目錄組織Makefile的通用做法。后面會講到如何編寫各個目錄下的Makefile.am。
AC_CONFIG_FILES一般跟AC_OUTPUT一起寫在configure.ac的最后部分。
automake聲明
為了配合automake,需要用AM_INIT_AUTOMAKE初始化automake:
- AM_INIT_AUTOMAKE([foreign])
這里foreign是個可選項,設(shè)置foreign跟調(diào)用automake --foreign是等價的,前一篇有講到。
libtool聲明
配合使用libtool,需要加入LT_INIT,這樣autoreconf會自動調(diào)用libtoolize
- LT_INIT
編譯器檢查
configure可以幫助我們檢查編譯和安裝過程中需要的系統(tǒng)工具是否存在。一般在進行其他檢查前,先做此類檢查。例如下面是一些常用的檢查:
- # 聲明語言為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從表現(xiàn)上與Makefile已經(jīng)十分接近,只差變量替換了。configure腳本執(zhí)行后,Makefile.in將最終轉(zhuǎn)變成Makefile。
子目錄引用
在本例中每個目錄下都有Makefile.am。根目錄的Makefile.am生成的Makefile將是make程序的默認入口,但是根目錄實際上并不包含任何需要構(gòu)建的文件。對于需要引用子目錄的Makefile來構(gòu)建的時候,使用SUBDIRS羅列包含其他Makefile.am的子目錄。因此,對于根目錄的Makefile.am只需要寫一行:
- SUBDIRS = src
同理,src目錄下的Makefile.am只需要
- SUBDIRS = common src test
定義目標
對于包含有源代碼文件的目錄。首先,我們需要定義編譯的目標,目標可能是庫文件或可執(zhí)行文件,目標又分為需要安裝和不需要安裝兩種。例如對于common目錄
下的源代碼,我們希望生成一個不需要安裝的庫文件(使用libtool),因為這個庫文件只在本項目內(nèi)使用,那么common/Makefile.am應(yīng)當這樣寫:
- 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時構(gòu)建),還可以自定義。我們著重討論前三種:
- bin:表示安裝到bindir目錄下,這種情況下會編譯出動態(tài)庫
- lib:表示安裝到libdir目錄下,這種情況下會編譯出動態(tài)庫
- noinst:表示不安裝,這種情況下會編譯出靜態(tài)庫,在其他目標引用該目標時將進行靜態(tài)鏈接
PRIMARY可以是PROGRAMS LIBRARIES LTLIBRARIES HEADERS SCRIPTS DATA。著重討論前三種:
- PROGRAMS:表示目標是可執(zhí)行文件
- LIBRARIES:表示目標是庫文件,通過后綴來區(qū)別靜態(tài)庫或動態(tài)庫
- LTLIBRARIES:表示是libtool庫文件,統(tǒng)一后綴為.la
與Makefile的思想一樣,目標的生成需要定義來源,通常目標是有一些源程序文件得到的。Makefile.am中只需定義xxx_SOURCES,后面跟隨構(gòu)建xxx這個目標需要的源代碼文件列表即可。注意到xxx是目標的名字,并且.字符需要使用_代替。
定義編譯選項
core目錄下需要生成可執(zhí)行目標,但是在鏈接時,需要用到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添加庫文件的引用,這種引用是靜態(tài)的還是動態(tài)的取決于引用的庫文件是否支持動態(tài)庫,如果支持動態(tài)庫,libtool優(yōu)先采用動態(tài)鏈接。而由于libcommon.la指定為noinst,所以不可能以動態(tài)鏈接的形式存在,這里必然是靜態(tài)鏈接。
$(top_builddir)引用的是make發(fā)生時的工作目錄,上文提到,我們將在build目錄下進行構(gòu)建,那么庫文件會生成在build目錄下,而不是源碼根目錄下,所以$(top_builddir)實際就是gnu-build/build目錄,而這樣可以很好的支持在另一個目錄中編譯程序。與之相對應(yīng)的是$(top_srcdir)對應(yīng)的是源碼的根目錄,即gnu-build目錄。
還有多個可以配置用于改變編譯和鏈接選項的配置項:
- xxx_LDADD:為鏈接器增加參數(shù),一般用于第三方庫的引用。比如-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在這里的地位是一個頂層的路徑,其他的路徑直接或間接與之有關(guān)。而prefix的默認值為/usr/local。所以可執(zhí)行程序默認總是安裝在/usr/local/bin。用戶總是可以在調(diào)用configure腳本時通過--prefix指定prefix。更詳細的路徑列表可以通過./configure --help了解。
開始構(gòu)建
填充一些源代碼后,就可以使用autoreconf了,只需要在根目錄下執(zhí)行autoreconf --install即可。
- [root@xxx gnu-build]# autoreconf --install
前一篇中,對autoreconf的整個過程和產(chǎn)生的文件做了詳盡的分析和闡述,讀者也應(yīng)該十分清楚這里將得到若干Makefile.in和common/config.h.in文件。
如果這個過程順利的話,就可以在build目錄下構(gòu)建了:
- # cd build
- # ../configure
- # make
這里configure后,會在build目錄下生成對應(yīng)位置的Makefile和common/config.h文件,而不是生成在源碼目錄中從而污染源碼
至此,你已經(jīng)完成了一個項目的基本構(gòu)建框架,后面的事情,就是逐步完善構(gòu)建對環(huán)境的依賴。
在configure.ac中配置環(huán)境檢查
autoconf為程序員提供的最為重要的功能就是提供了一種便捷、穩(wěn)定、可移植的方式,讓程序能在特定目標平臺和目標環(huán)境上安全的編譯運行程序。不過,autoconf只是提供了一些宏,用來簡化環(huán)境檢查。而究竟要檢查些什么,如何合理的利用這些宏完成目的,依舊是需要大量的積累的。筆者在這里對一些常用的宏進行一些介紹。
可執(zhí)行文件檢查
有些第三方庫在安裝到系統(tǒng)后,會附帶安裝若干可執(zhí)行程序,并可在環(huán)境變量的支持下直接運行。有時,我們通過檢查此類可執(zhí)行程序是否存在,來初步判斷該第三方庫是否已經(jīng)安裝在目標平臺。其中一種常用的宏是AC_CHECK_PROGS
- # 聲明一個變量PERL,檢查perl程序是否存在并可執(zhí)行
- # 如果不存在$PERL變量將是NOTFOUND,如果存在$PERL變量將是perl
- AC_CHECK_PROGS([PERL], [perl], [NOTFOUND])
- # 聲明一個變量TAR,檢查tar和gtar程序是否存在并可執(zhí)行
- # 如果不存在$TAR變量將是:,如果存在,第一個可用的程序名將賦值給$TAR
- AC_CHECK_PROGS([TAR], [tar gtar], [:])
GNU軟件有一種利用pkg-config,來進行自描述的機制。即可以通過注冊軟件自身(通常提供庫文件的軟件),讓pkg-config能夠返回庫文件的安裝路徑等信息,以便以一種統(tǒng)一的方式提供給調(diào)用程序。有些庫軟件附帶有獨立的config程序,比如pcre-config和apr-1-config。如果對這類庫提供軟件需要檢查依賴和編譯鏈接,通常可以通過AC_CHECK_PROGS來檢查config程序,從而得到編譯鏈接選項。
打印消息宏
打印消息可以作為調(diào)試手段,同時也可以在用戶在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的執(zhí)行,一般用于必需的編譯環(huán)境無法滿足時。
庫檢查宏
檢查某庫是否存在是最重要的功能,因為我們程序往往需要這些庫,甚至是庫中的某個函數(shù)的支持才能正確的運行。
使用AC_CHECK_LIB檢查庫以及其中的函數(shù)是否存在,該宏的原型為:
- AC_CHECK_LIB (library, function, [action-if-found],[action-if-not-found], [other-libraries])
- library:需要檢查的庫名,無需lib前綴,比如為了檢查libssl是否存在,這里需要傳入ssl
- function:這個庫中的某個函數(shù)名
- action-if-found:如果找到執(zhí)行某個動作,這個動作可以是另一個宏,可以是shell腳本。如果不指定這個參數(shù),默認在LIBS環(huán)境變量中增加-l選項,從而將在鏈接過程中將這個庫鏈接進來。比如-lssl。并且在config.h中定義一個宏HAVE_LIBlibrary,例如HAVE_LIBSSL。我們的代碼可以根據(jù)這個宏得知當前編譯環(huán)境是否提供libssl。
- action-if-not-found:如果找不到則執(zhí)行某個動作
通過下面幾個宏可以檢查系統(tǒng)是否包含某些頭文件,以及是否支持某些函數(shù):
- AC_CHECK_FUNCS:檢查是否支持某些函數(shù)。作為檢查的副作用,在config.h中會定義一個宏HAVE_funcs(全大寫)
- AC_CHECK_HEADERS:檢查是否支持某些頭文件。作為檢查的副作用,在config.h中會定義一個宏HAVE_header_H(全大寫)
來舉個例子,大家知道libiconv是一個可以在不同字符集間進行轉(zhuǎn)化的庫,如果我們的程序希望能夠在不同字符集間轉(zhuǎn)化的字符串的話,可以使用該庫。然而,在不同平臺上,該庫的移植方式有些區(qū)別。
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函數(shù),如果在Linux平臺上,通常該函數(shù)可以在沒有任何額外庫的情況下提供,所以HAVE_ICONV這個臨時變量將設(shè)置為yes。
- 接著通過shell的if測試判斷臨時變量HAVE_ICONV是否為yes。
- 如果已經(jīng)檢測到iconv,那么進一步檢查langinfo.h頭文件和nl_langinfo函數(shù),無論是否能檢查通過,由于使用了AC_MSG_WARN,所以configure并不會失敗退出,最多只是提示用戶警告。更重要的是,我們可以通過config.h中的宏,在代碼中得知是否支持頭文件和函數(shù),從而調(diào)整編譯分支。具體的在這個例子中這兩個宏分別為HAVE_LANGINFO_H和HAVE_NL_LANGINFO。
- 在非linux下可能需要額外的libiconv庫,所以在else分支中,立刻采用AC_CHECK_LIB檢測iconv庫,以及其中的libiconv_open函數(shù)。同樣的,如果存在,HAVE_ICONV這個臨時變量將設(shè)置為yes。
- 在接下來的if測試中,使用到了$LIBS變量,這是一個由編譯器支持的變量,表示在鏈接階段的額外庫參數(shù)。當我們檢測到libiconv后,就給這個變量臨時地添加-liconv。這樣接下來的AC_CHECK_FUNCS時,可以利用$LIBS在額外的庫中查找函數(shù)。
- 檢查langinfo.h頭文件,如果存在則再檢查nl_langinfo函數(shù);如果不存在,則檢查locale_charset函數(shù)。從邏輯上看,要么langinfo.h和nl_langinfo同時存在,要么有l(wèi)ocale_charset函數(shù),否則就終止configure。
- 最后重置$LIBS變量。
變量導出
configure腳本的檢測結(jié)果應(yīng)當有兩個主要出口,一是config.h,它幫助我們在源碼中創(chuàng)建編譯分支;二是Makefile.am,我們可以在Makefile.am中基于這些導出的變量,改變構(gòu)建方式。
有些宏可以自動幫我們導出到config.h,關(guān)于這一點上文已經(jīng)有所闡述了。而希望導出到Makefile.am則需要我們自己手動調(diào)用相關(guān)宏。這里主要有兩個宏:
- 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導出的情況下構(gòu)建:
- if HAVE_ICONV
- xm_charconv_LTLIBRARIES = xm_charconv.la
- ...
- endif
提供額外用戶參數(shù)支持
很多軟件都支持用戶在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定義該參數(shù)的值應(yīng)當是一個文件路徑(DIR要求一個目錄路徑),該宏需要提供一個默認值,這個例子中是${sysconfdir}/zz.conf,${sysconfdir}引用了${prefix}/etc,而$withval從命令行中引用--with-configfile的值。
最后我們通過AC_SUBST導出一個臨時變量。
上一節(jié)提到,導出的臨時變量可以在Makefile.am中引用,所以我們可以在Makefile.am中通過-D傳遞給代碼,從而在代碼中通過宏來引用:
- CFLAGS += -DCONFIGFILE=\"$(ZZ_CONFIGFILE)\"
總結(jié)
本文以一個例子,一步步使用GNU構(gòu)建系統(tǒng)來創(chuàng)建一個項目,并介紹了一些常用的檢測宏。事實上,autotool還有很多宏,甚至可以自定義宏。能否合理利用autotool取決于程序員對可移植性這個問題的經(jīng)驗和理解。