《Autotools - GNU Autoconf, Automake與Libtool實踐者指南》第二章


  在第一章中,我給出了GNU Autotools和一些資源的概述,可以幫助降低所需要的學習曲線來掌握它們。在這一章節中,我們會退一小步,調查可用於任何工程的項目組織技術,不僅僅使用Autotools。


  當你完成閱讀這一章節,你應該會熟悉普通的make目標,知曉它們爲何存在。你應該也會對工程組織方式有一個堅實的理解。當你完成這一章節,你會是很好地在通往Automake專家的路上。

  這一章節提供的信息最初來自兩個資源:
 > GNU編碼標準(GCS),見http://www.gnu.org/prep/standards/
 > 文件系統等級標準(FHS),見http://www.pathname.com/fhs/

  如果你想溫習你的make語法,你會發現GNU make手冊非常有用。如果你特別喜歡可移植make語法(你應該很可能是的),查看make的POSIX手冊頁。


創建一個新的項目目錄結構



  當你爲一個開源軟件工程建立編譯系統時,有兩個問題你需要問自己:
 > 目標平臺?
 > 用戶期望?

  第二個問題回答比較困難。首先,讓我們將問題範圍縮窄爲可控制的。你真正需要問的是:我的用戶期望我的編譯系統是怎麼樣的。有經驗的開源軟件開發者熟悉這些期望,通過下載、解壓、編譯和安裝成千個軟件包。最終,他們開始直觀地知道用戶期望的編譯系統。但是,即使如此,軟件包配置,編譯和安裝的過程變化很廣,因此,定義任何固定的常態是非常困難的。

  你可以諮詢自由軟件基金會(FSF),GNU項目的發起者,已經爲你做了很多收集資料的工作,而不是自己開展一個每一種編譯系統的調查。FSF是獲取關於自由、開源軟件方面信息最佳的來源之一,包括GCS,GCS涉及寬範圍的主題,關於編寫、發佈和分發自由、開源軟件。當設計一個管理打包、編譯和安裝軟件的系統時,許多問題需要考慮,GCS考慮了其中的絕大多數。


項目結構


  我們將開始一個樣例項目並在此基礎上構建,作爲我們繼續探索源碼級軟件分發的旅途。我將我們的項目稱爲Jupiter,我會使用下列命令創建一個工程目錄結構:
$ cd projects
$ mkdir -p jupiter/src
$ touch jupiter/Makefile
$ touch jupiter/src/Makefile
$ touch jupiter/src/main.c
$ cd jupiter
$
  現在我們有一個源碼目錄稱爲src,一個C源碼文件稱爲main.c,和爲我們項目中的兩個目錄各一個Makefile文件。大家都知道一個成功的開源軟件項目的關鍵是演進。開始比較下,根據需要增長---當你有時間和傾向。

  讓我們以編譯和清理項目作爲開始。頂層Makefile僅僅遞歸地傳遞請求到src/Makefile。這構成了一個相當常見的編譯系統類型,稱爲遞歸編譯系統,之所以這麼命名是因爲,make文件遞歸地調用子目錄裏的make文件。

all clean jupiter:
  cd src && $(MAKE) $@
.PHONY: all clean
列表2-1 Makefile:頂層make文件初始草稿
all: jupiter
jupiter: main.c
  gcc -g -O0 -o $@ main.c
clean:
  -rm jupiter
.PHONY: all clean
列表2-2 src/Makefile:src目錄下make文件的首個草稿
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char * argv[])
{
  printf("Hello from %s!\n", argv[0]);
  return 0;
}
列表2-3 src/main.c:項目中一個源碼的第一版


創建一個源分佈存檔


  爲了讓我們的用戶獲取到Jupiter的源碼,我們將創建和發佈一個軟件源存檔---一個壓縮包。我們可以寫一個單獨的腳本來創建壓縮包,但是因爲我們可以使用僞目標在make文件中創建任意的功能集,讓我們設計設計一個make目標來執行這一任務。爲發佈版構建一個源碼檔案通常與dist目標相關。

  當我們設計一個新的make目標是,我們需要考慮它的功能是在工程make文件中被分佈還是在一個單一的位置被處理。通常情況下, 經驗法則是利用遞歸編譯系統的本性,允許每個目錄管理一個過程中它自己的部分。我們已這麼做,當我們傳遞編譯jupiter程序的控制到src目錄時。然而,從一個目錄結構構建一個壓縮檔案不是一個遞歸過程。因爲這個原因,我們不得不在兩個make文件中的一箇中執行完整的任務。

  全局操作通常在工程目錄結構中的頂層make文件中被處理。我們添加dist目標到頂層make文件,如列表2-12所示。
package = jupiter
version = 1.0
tarname = $(package)
distdir = $(tarname)-$(version)
all clean jupiter:
	cd src && $(MAKE) $@

dist: $(distdir).tar.gz

$(distdir).tar.gz: $(distdir)
	tar chof - $(distdir) | gzip -9 -c > $@
	rm -rf $(distdir)

$(distdir):
	mkdir -p $(distdir)/src
	cp Makefile $(distdir)
	cp src/Makefile $(distdir)/src
	cp src/main.c $(distdir)/src

.PHONY: all clean dist
列表2-12: Makefile:添加dist目標到頂層make文件

  我已將dist目標的功能分成了三個獨立地規則,爲的是可閱讀性,模塊化和可維護性。在任何軟件工程處理中,這是一個需要遵循的重要的經驗法則:從較小的構建大過程,在有用的地方重用較小的過程。

  我們並不希望對象文件和可執行文件被存放在壓縮檔案中,因此我們需要構建一個鏡像目錄,其中確切地包含我們需要附帶的,包括在編譯和安裝過程中需要的任何文件和任何添加的文檔或license文件。不幸的是,這大大增加了單獨拷貝命令的使用。


強制一個規則運行


  如果鏡像目錄(jupiter-1.0)已經存在,當你執行make dist時,make不會試圖去創建它。dist目標不會拷貝任何文件,jupiter會是個空文件。更糟糕的是,如果來自前一次試圖存檔的鏡像目錄任然存在,新的壓縮存檔會包含來自前一次存檔的舊的源代碼。

  問題是$(distdir)目標是個真實的目標但是沒有依賴,這意味着只要它存在,make就認爲它是最新的。我們可能添加$(distdir)目標到.PHONY規則來強制在每次make dist時重編譯它,但是它不是個僞目標---它是個真實的文件系統目標。合適的方式是確保$(distdir)目標總是被重編譯,確保在make試圖構建它時不存在。一種完成這個的方式是創建一個總是會執行的僞目標,添加那個目標到$(distdir)目標的依賴鏈中。這種類型目標的常用名是FORCE,我已在列表2-13中實現了這一想法。
...
$(distdir).tar.gz: $(distdir)
	tar chof - $(distdir) | gzip -9 -c > $@
	rm -rf $(distdir)

$(distdir): FORCE
	mkdir -p $(distdir)/src
	cp Makefile $(distdir)
	cp src/Makefile $(distdir)/src
	cp src/main.c $(distdir)/src

FORCE:
	-rm $(distdir).tar.gz >/dev/null 2>&1
	-rm -rf $(distdir) >/dev/null 2>&1

.PHONY: FORCE all clean dist
列表2-13 Makfile:使用FORCE目標

  FORCE規則的命令每次都會被執行,因爲FORCE是一個僞目標。因爲我們使得FORCE是$(distdir)目標的依賴,我們有機會刪除任何先前創建的文件和目錄,然後開始讓make評估是否應該執行$(distdir)的命令。

前導控制字符


  一個命令上的前導破折號(-)告訴make不必關心它所前導命令的執行狀態。通常,當make遇到一個命令返回非零狀態碼到shell,它會停止執行並顯示一個錯誤信息---但是如果你使用了一個前導破則好,它會忽略錯誤並繼續。我在FORCE規則中rm命令前使用破折號,是因爲如果我試圖刪除一個不存在的文件,rm會返回錯誤。

  注意,我在打包規則的rm命令前,並沒有使用前導破折號。因爲我想知道rm如果有錯誤---如果它不成功,應該有非常大的錯誤,因爲前面的命令應該已根據此目錄創建了一個打包命令了。



自動測試一個發佈版


  構建歸檔目錄的規則可能是make文件中最讓人沮喪的規則,因爲它包含命令來拷貝獨立文件到發佈目錄。在項目中每次我們修改文件結構,我們不得不在我們的頂層make文件中更新這一規則,否則我們會破壞dist目標。但是我們沒有什麼跟多可以做---我們已經使得規則儘可能簡單。現在我們不得不記住來恰當地管理這一過程。

  儘管不幸,破壞dist目標不是最糟糕的事情。最爲糟糕的是,dist目標在工作,但是實際上並沒有拷貝所有需要的文件到壓縮包中。實際上,遠非如此,沒有一個錯誤會產生,因爲添加文件到一個工程是一個更爲常見的活動,相比移動或刪除它們。新文件沒有被拷貝,但是dist規則沒有注意到差別。

  有一種方式來執行在dist目標上的一種自檢。我們可以創建另一個稱爲distcheck的僞目標,做我們用戶確切會做的事:解壓壓縮包和編譯工程。我們可以在一個臨時目錄裏用此規則的名利執行這一任務。如果編譯過程失敗,distcheck目標會終止,告訴我們在發佈版中忘記了一些重要的東西。

  列表2-14顯示了在頂層make文件中需要實現distcheck目標的修改。
...
distcheck: $(distdir).tar.gz
	gzip -cd $(distdir).tar.gz | tar xvf -
	cd $(distdir) && $(MAKE) all
	cd $(distdir) && $(MAKE) clean
	rm -rf $(distdir)
	@echo "*** Package $(distdir).tar.gz is ready for distribution."
...
.PHONY: FORCE all clean dist distcheck
列表2-14 Makefile:添加一個distcheck目標到頂層make文件

  distcheck目標依賴於壓縮包自己,因此構建壓縮包的規則先執行。make然後執行distcheck命令,解壓剛構建的壓縮包,遞歸運行遞歸目錄裏的make命令。如果那個過程成功,它打印一條表示你的用戶不怎麼可能在此壓縮包使用中遇到問題的信息。

  現在所有你得做是,記住,在向大家發佈你的壓縮包之前,執行make distcheck。



單元測試


  合適的單元測試是份艱苦的工作,但是最後是有回報的。那些做這件事的人,已經學了一課關於延遲享樂的價值。

  一個良好的編譯系統應該包含合適的單元測試。爲測試一個構建最爲常用的目標是check目標,因此我們會繼續,以通常的方式添加它。實際的單元測試應該可能會放在src/Makefile中,因爲那是被構建的jupiter可執行文件所在處,因此我們會從頂層make文件向下傳遞check目標

  但是我們在check規則中放什麼命令呢?jupiter是個相當簡單的程序,它打印一條信息。我們使用grep工具來測試jupiter實際上是輸出了這樣一條字符串。

  列表2-15和列表2-16分別闡述了頂層和src目錄下的make文件的修改。
...
all clean check jupiter:
  cd src && $(MAKE) $@
...
.PHONY: FORCE all clean check dist distcheck
列表2-15 Makefile:傳遞check目標到src/Makefile
...
check: all
  ./jupiter | grep "Hello from .*jupiter!"
  @echo "*** ALL TESTS PASSED ***"
...
.PHONY: all clean check
列表2-16 src/Makefile:在check目標中實現單元測試

  注意check目標依賴於all。我們不能真正測試我們的產品除非他們最新的,反映了已近做的任何源碼或構建系統的修改。如果用戶需要測試產品,他想要產品存在,並且是最新的。我們能夠確保它們存在並且是最新的,通過添加all到check的依賴列表中。

  對於我們的編譯系統,我們可以做一個更多的提升:我們可以在distcheck規則中添加check到由make執行的目標列表,在make all和make clean命令之間,如列表2-17所示。

...
distcheck: $(distdir).tar.gz
  gzip -cd $(distdir).tar.gz | tar xvf -
  cd $(distdir) && $(MAKE) all
  cd $(distdir) && $(MAKE) check
  cd $(distdir) && $(MAKE) clean
  rm -rf $(distdir)
@echo "*** Package $(distdir).tar.gz is ready for distribution."
...
列表2-17 Makefile: 添加check目標到$(MAKE)命令

  現在我們可以運行make distcheck,它會測試軟件包附帶的整個編譯系統。



安裝產品


  安裝在Jupiter項目中顯得那麼不重要,因爲只有一個程序,大多數用戶會猜到並安裝它。然而,複雜的項目就會引起用戶的恐慌,當涉及把用戶和系統二進制文件、庫、頭文件,和文檔包括手冊、PDF文件,和或多或少的README,AUTHORS,NEWS,INSTALL,COPYING等。

  當創建一個發佈版軟件包時,可能不是一個內在的遞歸過程,安裝確實是,因此我們允許工程中每個子目錄管理它自己組件的安裝。爲了這麼做,我們需要同時修改頂層和src層make文件。修改頂層make文件是簡單地:因爲沒有產品被安裝在頂層目錄,我們將會以通常的方式傳遞責任到src/Makefile。

  添加install目標的修改顯示在列表2-18和2-19中。
...
all clean check install jupiter:
  cd src && $(MAKE) $@
...
.PHONY: FORCE all clean check dist distcheck install
列表2-18 Makefile:傳遞install目標到src/Makefile
...
install:
  cp jupiter /usr/bin
  chown root:root /usr/bin/jupiter
  chmod +x /usr/bin/jupiter
.PHONY: all clean check install
列表2-19 src/Makefile:實現install目標

安裝選擇


  上面的install目標是好的,但是當考慮安裝的位置時,可以有更加靈活的方式---指定安裝路徑。
  我們目前編譯系統的另一個問題是,爲了安裝文件,我們必須做很多材料。大多數Unix系統提供一個系統級程序---通常是一個shell腳本---稱爲install允許用戶指定被安裝文件的多種屬性。這一工具的恰當使用,可以簡化一些Jupiter的安裝,因此當我們添加位置靈活性時,我們可能可以使用install工具。這些修改顯示在列表2-20和2-21中。
...
prefix=/usr/local
export prefix

all clean check install jupiter:
  cd src && $(MAKE) $@
...
列表2-20 Makefile:添加一個prefix變量
...
install:
	install -d $(prefix)/bin
	install -m 0755 jupiter $(prefix)/bin
...
列表2-21: src/Makefile:在install目標中使用prefix變量

  注意的是,我只在頂層make文件中聲明和賦值prefix變量,但是在src/Makefile中引用。我可以這麼做是因爲我在頂層Makefile中使用修飾語export,這一修飾語輸出make變量到shell。這一make的特性允許我們定義我們所有的用戶變量到一個明顯的位置---頂層Makefile的開始。

  注意:GNU make允許你在賦值行使用export關鍵詞,但是這一語法在其它版本的make中不可移植。

  我已在makefile中定義prefix變量爲/usr/local,make允許你在命令行定義make變量,以這種方式:

$ sudo make prefix=/usr install
...
  記住,在命令行定義的變量覆蓋定義在makefile中定義的。因此,用戶需要安裝jupiter到/usr/bin目錄的,現在就可以有在make命令行指定這一方面的選項。

  有了這一系統,我們的用戶可能安裝jupiter到任意所選擇目錄下的bin目錄中。實際上,這是我們在列表2-21中添加install -d $(prefix)/bin的理由---如果不存在安裝目錄bin,這一命令創建它。既然我們允許用戶在make命令行定義prefix,我們實際上無法知道用戶會把jupiter安裝在哪個目錄;因此,我們必須爲位置不存在的可能性做好準備。


卸載一個軟件包


  列表2-22和列表2-23顯示了添加一個uninstall目標到兩個make文件中的情況。
...
all clean install uninstall jupiter:
	cd src && $(MAKE) $@
...
.PHONY: FORCE all clean dist distcheck install uninstall
列表2-22 Makefile:添加uninstall目標到頂層makefile
...
uninstall:
	-rm $(prefix)/bin/jupiter
.PHONY: all clean check install uninstall
列表2-23 src/Makefile:添加uninstall目標到src級makefile

  在我們修改安裝過程時,現在有兩個位置我們需要更新:install和uninstall目標。在第五章,我會向你展示使用GNU Automake如何以一種更爲簡單的方式重寫這個makefile。

測試安裝和卸載

  現在讓我們添加一些代碼到我們的distcheck目標,來測試install和uninstall目標的功能。列表2-24顯示了在頂層Makefile中的必要修改。

...
distcheck: $(distdir).tar.gz
    gzip -cd $(distdir).tar.gz | tar xvf -
    cd $(distdir) && $(MAKE) all
    cd $(distdir) && $(MAKE) check
    cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst install
    cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst uninstall
    cd $(distdir) && $(MAKE) clean
    rm -rf $(distdir)
@echo "*** Package $(distdir).tar.gz is ready for distribution."
...
列表2-24 Makefile: 爲install和uninstall目標添加distcheck測試

  注意,在$$(PWD)變量應用中,我使用了兩個美元符號,確保make使用命令行的其餘部分傳遞變量引用到shell,而不是在執行命令前擴展。我希望這個變量被shell解引用,而不是make工具。

  

  我們可以或多或少地寫一個通用測試,檢查我們已安裝的是否已被合適地移除了。列表2-25顯示了爲這一測試所增加的。

...
distcheck: $(distdir).tar.gz
  gzip -cd $(distdir).tar.gz | tar xvf -
  cd $(distdir) && $(MAKE) all
  cd $(distdir) && $(MAKE) check
  cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst install
  cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst uninstall
  @remaining="`find $${PWD}/$(distdir)/_inst -type f | wc -l`"; \
  if test "$${remaining}" -ne 0; then \
    echo "*** $${remaining} file(s) remaining in stage directory!"; \
    exit 1; \
  fi
  cd $(distdir) && $(MAKE) clean
  rm -rf $(distdir)
  @echo "*** Package $(distdir).tar.gz is ready for distribution."
...
  測試首先生成了一個稱爲remaining的數值,代表了安裝目錄中常規文件的數目。如果這個值不是零,它會打印一條信息到控制檯,顯示有多少個文件被uninstall命令落下,然後它以錯誤退出。

  我並不想通過打印嵌入的echo聲明來提醒人們,除非它應該被執行時,因此我用@前綴整個測試語句,從而使make不會打印代碼到stdout。因爲make認爲這五行代碼是一個單一的命令(代碼中用\拼接),唯一抑制打印echo申明的方法是抑制打印整個命令。

  這裏的代碼只是檢查常規文件。如果你的安裝過程創建了任何軟鏈接,如果它們被落下,這個測試程序不會注意到。在安裝過程中構建的目錄結果被落下在原地,因爲檢查代碼不知道一個子目錄是屬於系統的還是工程的。uninstall規則命令可以知道哪些目錄是工程相關的,併合適地刪除它們,但是我不想添加工程相關知識到distcheck測試。



支持標準目標和變量



  除了我已提到的,GNU編碼標準列出了一些重要的目標和變量,你應該在你的項目中去支持---主要是因爲你的用戶會期望有它們的支持。

  對GCS中的一些章節應該持懷疑態度,除非你在一個GNU發起的項目上工作。例如,你很可能不太關心第五章中關於C源代碼格式的建議。你的用戶當然也不會在意,因此你可以使用你希望的任何源代碼格式風格。

  那並不是說對於非GNU開源項目,第五章中的所有部分都是沒有價值的。例如,“系統類型之間的可移植性”和“CPU之間的可移植性”子部分,提供了關於C源代碼可移植性方面極好的信息。“國際化”子部分給你一些關於使用GNU軟件國際化你的項目的有用建議。

  第六章討論了GNU方式的文檔,第六章的一些部分描述了項目中經常會有的多種頂層文本文件,例如AUTHORS,NEWS, INSTALL, README和ChangeLog文件。在任何聲譽良好的項目中,這些都是受過良好薰陶的開源軟件用戶期望看到的所有信息點。

  GCS文檔中,真正有用的信息開始於第七章:“發佈流程”。作爲一個維護者,這一章對你來說是關鍵,因爲它定義了你的用戶會期望的項目編譯系統。第七章包含了軟件包在源碼級發佈版中提供的用戶選項的事實標準。

標準目標


  GCS中第七章的子部分“配置應該如何工作”定義了配置過程。“Makfile慣例”子部分涉及了用戶在開源軟件包中所期望的所有標準目標和許多標準變量。GCS定義的標準目標如下:
all        install              install-html
install-dvi      install-pdf          install-ps
install-strip    uninstall            clean
distclean        mostlyclean          maintainer-clean
TAGS             info                 dvi
html             pdf                  ps
dist             check                installcheck
installdirs
  你不需要支持所有的目標,但是你應該考慮支持對你項目有意義的目標。例如,如果你編譯和安裝HTML頁面,你應該考慮支持html和install-html目標。Autotools項目支持這些,並且更多。一些目標對最終用戶有用,然而另一些只是對項目維護者有用。

標準變量


  你應該支持的變量如下面表中所示。大多數變量以多個方面被定義,最終只有一個:prefix。因爲一個標準名稱的缺少,我稱這些prefix變量。大多數可以被歸類爲參考標準位置的安裝目錄變量,但也有一些例外,例如srcdir。表2-1列出了這些prefix變量和它們的默認值。

Variable         Default Value

prefix          /usr/local
exec_prefix             $(prefix)
bindir                  $(exec_prefix)/bin
sbindir                 $(exec_prefix)/sbin
libexecdir              $(exec_prefix)/libexec
datarootdir             $(prefix)/share
datadir                 $(datarootdir)
sysconfdir              $(prefix)/etc
sharedstatedir          $(prefix)/com
localstatedir           $(prefix)/var
includedir              $(prefix)/include
oldincludedir           /usr/include
docdir                  $(datarootdir)/doc/$(package)
infodir                 $(datarootdir)/info
htmldir                 $(docdir)
dvidir                  $(docdir)
pdfdir                  $(docdir)
psdir                   $(docdir)
libdir                  $(exec_prefix)/lib
lispdir                 $(datarootdir)/emacs/site-lisp
localedir               $(datarootdir)/locale
mandir                  $(datarootdir)/man
manNdir                 $(mandir)/manN (N = 1..9)
manext                  .1
manNext                 .N (N = 1..9)
srcdir                  The source-tree directory corresponding to the
                        current directory in the build tree
  基於Autotools的項目自動地支持這些和其它有用變量;Automake提供了地它們的全面支持,而Autoconf的支持更爲有限。如果你編寫你自己的make文件和編譯系統,你應該在編譯和安裝過程中儘可能多地支持它們。

添加位置變量到Jupiter


  列表2-26和列表2-27顯示了頂層Makefile和src下Makefile中的修改。

...
prefix = /usr/local
exec_prefix = $(prefix)
bindir = $(exec_prefix)/bin
export prefix
export exec_prefix
export bindir
...

列表2-26 Makefile: 添加bindir

...
install:
install -d $(bindir)
install -m 0755 jupiter $(bindir)
uninstall:
-rm $(bindir)/jupiter
...

列表2-27 src/Makefile: 添加bindir

  儘管我們在src/Makfile中只是用了bindir,我們必須export prefix,exec_prefix和bindir,因爲bindir根據exec_prefix形式定義,後者根據prefix定義。當我們運行install命令時,首先展開bindir到$(exec_prefix)/bin,然後到$(prefix)/bin,最終到/usr/local/bin。因此,src/Makefile在此過程中需要訪問所有三個變量。

  在多個層次修改前綴變量的能力對於一個Linux發行版軟件包打包者來說特別有用,他需要安裝軟件包到非常特定的系統位置。例如,一個發行版打包者可以使用下面的命令來修改安裝前綴到/usr,和系統配置目錄到/etc。
$ make prefix=/usr sysconfdir=/etc install
...
  沒有多層次修改前綴變量的能力,配置文件最終會在/usr/etc,因爲$(sysconfdir)的默認值是$(prefix)/etc。


項目進入Linux發行版


  在GCS的第七部分,包含了一小部分來討論支持分階段安裝。爲支持分階段安裝,所有你需要的只是一個稱爲DESTDIR的變量,作爲一類超級前綴到你所有的安裝產品。列表2-28列出了需要的修改。
...
install:
  install -d $(DESTDIR)$(bindir)
  install -m 0755 jupiter $(DESTDIR)$(bindir)
uninstall:
  -rm $(DESTDIR)$(bindir)/jupiter
...
列表2-28 src/Makefile: 添加分階段編譯功能

  你不必定義一個DESTDIR的默認值,因爲如果它未定義,它會被擴展爲一個空字符串,這對它所考慮的目錄沒有影響。

  我沒有必要添加$(DESTDIR)到uninstall規則的rm命令,因爲對於軟件包管理器,它們並不關心你的軟件包如何卸載。軟件包管理器例如RPM,使用它們自己的規則從一個系統中移除產品,這些規則基於一個軟件包管理數據庫,而不是你的uninstall目標。

  然而,處於對稱性和完整性,添加$(DESTDIR)到uninstall並沒有壞處。另外,爲了distcheck目標的完整性,我們需要它。修改如下:

...
distcheck: $(distdir).tar.gz
  gzip -cd $(distdir).tar.gz | tar xvf -
  cd $(distdir) && $(MAKE) all
  cd $(distdir) && $(MAKE) check
  cd $(distdir) && $(MAKE) DESTDIR=$${PWD}/_inst install
  cd $(distdir) && $(MAKE) DESTDIR=$${PWD}/_inst uninstall
  @remaining="`find $${PWD}/$(distdir)/_inst -type f | wc -l`"; \
  if test "$${remaining}" -ne 0; then \
    echo "*** $${remaining} file(s) remaining in stage directory!"; \
    exit 1; \
  fi
  cd $(distdir) && $(MAKE) clean
  rm -rf $(distdir)
  @echo "*** Package $(distdir).tar.gz is ready for distribution."
...
列表2-29 Makefile: 在distcheck目標中使用DESTDIR


  在install和uninstall命令中修改prefix到DESTDIR允許我們恰當地測試一個完整的安裝目錄等級,如我們馬上會看到的。

  在這一點,一個RPM特定文件可以提供下列文本作爲Jupiter軟件包的安裝命令:

%install
make prefix=/usr DESTDIR=%BUILDROOT install
  不要關心軟件包管理器的文件格式。只要注意通過DESTDIR變量所提供的階段性安裝功能。

  你可能會疑惑爲何prefix變量不能提供這一功能。一方面,在系統級安裝中,不是每一個路徑是相對prefix定義的。系統配置目錄(sysconfdir),例如,通常被軟件包管理器定義爲/etc。你可以在表2-1中看到,sysconfdir的默認定義是$(prefix)/etc,因此唯一的方式會解析到/etc的是如果你明確的在configure或make命令行中設置它。如果你用那種方式配置它,DESTDIR變量會在分階段安裝過程中影響sysconfdir的基礎位置。在本章後續部分和接下來的兩章中,這麼做的理由會變得更加清晰。



編譯與安裝的前綴覆蓋


  在此,我想稍稍離題解釋一個難懂(或者說至少是不明顯的)的概念,關於定義在GCS中的prefix和其它路徑變量。在前面的例子中,我在make install命令行使用prefix覆蓋,像這樣:

$ make prefix=/usr install
...

  我想強調的問題是:make all和make install使用一個prefix覆蓋之間的差別。在我們樣例的make文件中,我們已試圖避免在任何與安裝無關的目標中使用前綴,因此,對於你來說,可能不會那麼清除知道一個前綴在編譯階段中會是那麼有用。然而,前綴變量在編譯階段可以是非常有用的,在編譯時替換源碼中的路徑,如列表2-30所示。
program: main.c
  gcc -DCFGDIR="\"$(sysconfdir)\"" -o $@ main.c

列表2-30  在編譯時把替代路徑放入源碼


  在這個例子中,我在用於main.c的編譯器命令行中定義了一個稱爲CFGDIR的C預處理器變量。在main.c中,大概有些代碼如列表2-31中所示。
#ifndef CFGDIR
# define CFGDIR "/etc"
#endif
const char cfgdir[FILENAME_MAX] = CFGDIR;
列表2-31 在編譯時替代CFGDIR

  在後面的代碼中,你可能會使用C全局標量cfgdir來訪問應用程序的配置文件。
  Linux發佈版軟件包管理器通常爲在RPM特定文件中的編譯和安裝使用不同的前綴覆蓋。在編譯階段,實際運行時目錄被人工編碼到可執行文件,使用如列表2-32所示的命令行。

%build
%setup
./configure prefix=/usr sysconfdir=/etc
make

列表2-32 一個編譯源碼樹的RPM特定文件的一部分

  注意伴隨着prefix,我們已明確指定sysconfdir,因爲,如前面講到的,系統配置目錄通常是在系統前綴目錄結構的外面。軟件包管理器安裝這些可執行文件到一個階段性目錄,從而使在編譯二進制軟件包時,可以將它們拷貝出它們的安裝路徑。相應的安裝命令可能像列表2-33中所示。

%install
make DESTDIR=%BUILDROOT% install

列表2-33 一個RPM特定文件的安裝部分

  在安裝期間使用DESTDIR會暫時覆蓋所有安裝前綴變量,因此,你不必記住在你配置期間你已覆蓋了哪些變量。給定如列表2-32中所示配置命令,如列表2-33中所示方式使用DESTDIR,具有與列表2-34所示代碼相同的效果。

%install
make prefix=%BUILDROOT%/usr sysconfdir=%BUILDROOT%/etc install

列表2-34 在安裝期間覆蓋默認sysconfdir


  這裏的關鍵點是我之前談及的。絕不要把你的install目標寫到Makfile中編譯所有或部分產品的目標上去。安裝功能應該被限制到拷貝文件,如果可能的話。另外,如果你的用戶使用前綴覆蓋,他們不能訪問你的階段安裝特性。


  另一個這種限制安裝功能方式的理由是,它允許用戶作爲一個組安裝軟件包集到一個獨立的位置,然後在合適的位置創建鏈接到實際的文件。一些人喜歡這麼做,當測試一個軟件包時,並希望跟蹤所有它的組件。



用戶變量


  GCS定義了一個變量集,對於用戶來說是神聖的。這些變量應該會被GNU編譯系統引用,但是決不能被GNU編譯系統修改。這些所謂的用戶變量包括在那些在表2-2中所列用於C和C++程序的變量。

Variable             Purpose
CC                   A reference to the system C compiler
CFLAGS               Desired C compiler flags
CXX                  A reference to the system C++ compiler
CXXFLAGS             Desired C++ compiler flags
LDFLAGS              Desired linker flags
CPPFLAGS             Desired C/C++ preprocessor flags
. . .

表2-2 一些用戶變量和他們的意圖

  你可以在GNU Make手冊中的“被隱含規則使用的變量”部分找到一個關於程序名和標誌變量的相對完整的列表。對我我們的意圖,表2-2所示標量足夠了,但是對於一個更爲複雜的make文件,你應該熟悉GNU Make手冊中所列的更爲複雜的列表。


  爲了在我們的make文件中使用這些變量,我們只需要用$(CC)替換gcc。我們會對CFLAGS和CPPFLAGS做相同的事,儘管CPPFLAGS默認值是空。CFLAGS變量也沒有默認值,但是這是個好的時機來添加一個。我喜歡使用-g選項來編譯對象,-O0來禁止對調試期間的編譯進行優化。對src/Makefile的更新如列表2-52中所示。

...
CFLAGS = -g -O0
...
jupiter: main.c
  $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ main.c
...

列表2-35 src/Makefile: 添加合適的用戶變量

  這時可行的,因爲make工具允許這樣的變量被命令行的選項覆蓋。例如,爲了切換編譯器和設置一些編譯器的命令行選項,用戶只需輸入下列信息:

$ make CC=gcc3 CFLAGS='-g -O2' CPPFLAGS=-dtest
  在這個例子中,我們的用戶已經決定使用GCC的版本3替代版本4,生成調試符號,使用第二級優化來優化他的代碼。他也決定使能test選項,通過一個C預處理器定義的使用。注意,如果這些變量是在make命令行設置的,這很明顯等價於Bourne-shell語法,不會如期望的那樣工作:
$ CC=gcc3 CFLAGS='-g -O2' CPPFLAGS=-dtest make
  理由是,我們僅僅在本地環境中設置環境變量,通過shell傳遞給make工具。記住,環境變量不會自動覆蓋這些在make文件中的設置。爲了我們需要的功能,我們可以在我們的make文件中使用一些GNU特定make語法,如列表2-36所示。
...
CFLAGS ?= -g -O0
...
列表2-36 在一個make文件中使用GNU make特定的查詢賦值操作符(?=)

  ?=操作符是一個GNU make特定的運算符,在make文件中只會設置哪些尚未在其它地方設置過的變量。這意味着,我們現在可以覆蓋這些特殊的變量設置,通過在環境中設置。但別忘了這僅僅在GNU make中工作。總的來說,最好在make命令行中設置make變量。



配置你的軟件包


  GCS在第七部分的“配置應該如何工作”子部分中講述了配置過程。到此爲止,我們在僅使用make文件的情況下,對Jupiter做任何我們想要的,因此,你可能會疑惑配置到底是爲了什麼。在GCS這一部分的開始段落,回答了我們的問題:

  每一個GNU發佈版應該附帶一個稱爲configure的shell腳本。這一腳本給出了你想爲程序編譯的機器和系統的類型。configure腳本必須記錄配置選項,從而影響編譯。


  一個典型的配置腳本的主要任務如下:
 > 從包含替代變量的模板生成文件;
 > 根據項目源碼生成一個C語言頭文件(config.h);
 > 爲一個特定的make環境設置用戶選項(調試標誌等);
 > 設置多個軟件包選項作爲環境變量;
 > 測試工具、庫和頭文件的存在性.


  對於複雜的項目,配置腳本經常從一個或多個由項目開發者維護的模板生成項目make文件。這些模板包含配置變量,以一種容易識別和替換的格式。配置腳本用配置過程中決定的變量值替換這些變量---要麼從用戶指定的命令行選項,或者是從一個平臺環境的完整分析。這個分析細化到檢查某種系統或軟件包頭文件和庫的存在性,爲需要的程序和工具搜索系統路徑,甚至運行設計的小程序來掌握shell、C編譯器或需要的庫的特性集。

  在過去,變量替換的工具選擇是sed流編輯器。一個簡單的sed命令通過單次掃過文件可以替換在make文件模板中所有的配置變量。然而,Autoconf 2.62或更新的版本選擇awk代替sed用於這一過程。awk程序提供跟多的功能允許很多變量的高效替換。



總結


  我們現在通過手寫的方式已創建了一個完整的工程編譯系統,有一個重要的例外:我們沒有根據GNU編碼標準中指定的設計標準,設計一個configure腳本。我們可以做,但是這會佔據很多文本頁面。我還是簡單的繼續一個Autoconf的討論,而不是花費時間和精力去做這事,Autoconf允許我們構建其中一個腳本少到2到3行代碼。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章