實踐:GNU構建系統

原文:實踐:GNU構建系統html

在上一篇概念:GNU構建系統和Autotool,我對GNU構建系統從用戶視角和開發者視角分別進行了闡述。本篇從個人實踐總結的角度,並闡述如何從頭開始規劃一個基於GNU構建系統的項目。事實上,隨着開發者對跨平臺認知的深刻和完善,才能逐漸掌握GNU構建。注意:本文的例子不依賴於任何IDE和編輯器。這樣讀者能夠從根本上認識到每一個文件的做用。linux

安裝autotools

須要安裝的工具包括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

從上面的目錄結構能夠看出:程序員

  1. 根目錄有一個configure.ac,這是構建系統的核心文件之一,描述整個構建的依賴和輸出,是configure腳本的原型。shell

  2. 每一個目錄(包括根目錄)都有一個Makefile.am,這些文件是生成Makefile的主要來源。使用Makefile.am的優勢是能夠結合configure.ac、比手動編寫Makefile方便不少。安全

  3. src目錄下放置源代碼,源代碼被分紅commoncoretestcommon用來實現一些可重用的代碼,好比通用數據結構,內存管理,異常的封裝;core用來放置直接編譯成可執行程序的代碼,好比main.c等;test用於編寫單元測試程序。bash

  4. build目錄用於存放編譯過程當中的臨時文件和編譯獲得了目標文件。通常咱們老是cdbuild目錄中,並執行../configureconfigure,並在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命令編譯程序前,對編譯工做進行一個預先的配置,這裏的配置落實到底,主要就是生成Makefileconfig.h

Makefile.am --> Makefile.in --> Makefile
                             |
                           configure*
                             |
                config.h.in --> config.h

那麼咱們的程序必須要經過某種方式,得知環境的不一樣,從而經過預編譯作出響應。這裏的響應主要分兩塊:

  1. 對於源代碼而言,經過config.h中的宏定義,來改變編譯行爲。

  2. 對於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

針對這個例子咱們只須要檢查cccxx就能夠了。

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_LIBADDtarget_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都採用這個選項。

安裝路徑

剛剛提到的bindirlibdir是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.incommon/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-configapr-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

在這個例子中,咱們能夠看到許多技巧。咱們來逐一解讀一下:

  1. 首先經過AC_CHECK_FUNCS檢查iconv_open函數,若是在Linux平臺上,一般該函數能夠在沒有任何額外庫的狀況下提供,因此HAVE_ICONV這個臨時變量將設置爲yes

  2. 接着經過shell的if測試判斷臨時變量HAVE_ICONV是否爲yes

  3. 若是已經檢測到iconv,那麼進一步檢查langinfo.h頭文件和nl_langinfo函數,不管是否能檢查經過,因爲使用了AC_MSG_WARN,因此configure並不會失敗退出,最多隻是提示用戶警告。更重要的是,咱們能夠經過config.h中的宏,在代碼中得知是否支持頭文件和函數,從而調整編譯分支。具體的在這個例子中這兩個宏分別爲HAVE_LANGINFO_HHAVE_NL_LANGINFO

  4. 在非linux下可能須要額外的libiconv庫,因此在else分支中,馬上採用AC_CHECK_LIB檢測iconv庫,以及其中的libiconv_open函數。一樣的,若是存在,HAVE_ICONV這個臨時變量將設置爲yes

  5. 在接下來的if測試中,使用到了$LIBS變量,這是一個由編譯器支持的變量,表示在連接階段的額外庫參數。當咱們檢測到libiconv後,就給這個變量臨時地添加-liconv。這樣接下來的AC_CHECK_FUNCS時,能夠利用$LIBS在額外的庫中查找函數。

  6. 檢查langinfo.h頭文件,若是存在則再檢查nl_langinfo函數;若是不存在,則檢查locale_charset函數。從邏輯上看,要麼langinfo.hnl_langinfo同時存在,要麼有locale_charset函數,不然就終止configure。

  7. 最後重置$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取決於程序員對可移植性這個問題的經驗和理解。

相關文章
相關標籤/搜索