日期:2006-10-25 做者:Daniel Robbins 來自:IBM DW中國html
Daniel Robbins 在其最後一篇 Bash實例文章中詳細講述了 Gentoo Linux ebuild 系統,這個展現 bash能力的極佳範例。按部就班地,他爲您展現如何實現 ebuild系統,並觸及不少方便的 bash技術和設計策略。在本文末尾,您將很好地掌握製造徹底基於 bash的應用所涉及的技術,並開始爲本身的自動構建系統編碼。linux
進入 ebuild 系統shell
我真是一直期待着這第三篇、也是最後一篇 Bash 實例文章,由於既然已經在 第 1 篇和 第 2 篇 中講述了 bash 編程基礎,就能夠集中講述象 bash 應用開發和程序設計這樣更高級的主題。在本文中,將經過我花了許多時間來編碼和細化的項目,Gentoo Linux ebuild 系統,來給您大量實際的、現實世界的 bash 開發經驗。編程
我是 Gentoo Linux(目前仍是 beta 版的下一代 Linux OS)的首席設計師。個人主要責任之一就是確保全部二進制包(相似於 RPM)都正確建立並一塊兒使用。正如您可能知道的,標準 Linux 系統不是由一棵統一的源樹組成(象 BSD),而其實是由超過 25 個協同工做的核心包組成。這其中包括:bash
包 | 描述 |
---|---|
linux | 實際內核 |
util-linux | 與 Linux 相關的雜項程序集合 |
e2fsprogs | 與 ext2 文件系統相關的實用程序集合 |
glibc | GNU C 庫 |
每一個包都位於各自的 tar 壓縮包中,並由不一樣的獨立開發人員或開發小組維護。要建立一個發行版,必須對每一個包分別進行下載、編譯和打包處理。每次要修復、升級或改進包時,都必須重複編譯和打包步驟(而且,包確實更新得很快)。爲了幫助消除建立和更新包所涉及的重複步驟,我建立了 ebuild 系統,該系統幾乎全用 bash 編寫。爲了增長您的 bash 知識,我將按部就班地爲您演示如何實現該 ebuild 系統的解包和編譯部分。在解釋每一步時,還將討論爲何要做出某些設計決定。在本文末尾,您不只將極好地掌握大型 bash 編程項目,還實現了完整自動構建系統的很大一部分。ide
爲何選擇 bash?模塊化
Bash 是 Gentoo Linux ebuild 系統的基本組件。選擇它作爲 ebuild 的主要語言有幾個緣由。首先,其語法不復雜,而且爲人們所熟悉,這特別適合於調用外部程序。自動構建系統是自動調用外部程序的「膠合代碼」,而 bash 很是適合於這種類型的應用。第二,Bash 對函數的支持容許 ebuild 系統使用模塊化、易於理解的代碼。第三,ebuild 系統利用了 bash 對環境變量的支持,容許包維護人員和開發人員在運行時對其進行方便的在線配置。函數
構建過程回顧工具
在討論 ebuild 系統以前,讓咱們回顧一下編譯和安裝包都牽涉些什麼。例如,讓咱們看一下 "sed" 包,這個做爲全部 Linux 版本一部分的標準 GNU 文本流編輯實用程序。首先,下載源代碼 tar 壓縮包 (sed-3.02.tar.gz)(請參閱 參考資料 )。咱們將把這個檔案存儲在 /usr/src/distfiles 中,將使用環境變量 "$DISTDIR" 來引用該目錄。"$DISTDIR" 是全部原始源代碼 tar 壓縮包所在的目錄,它是一個大型源代碼庫。測試
下一步是建立名爲 "work" 的臨時目錄,該目錄存放已經解壓的源代碼。之後將使用 "$WORKDIR" 環境變量引用該目錄。要作到這點,進入有寫權限的目錄,而後輸入:
將 sed 解壓縮到臨時目錄
$ mkdir work $ cd work $ tar xzf /usr/src/distfiles/sed-3.02.tar.gz
而後,解壓縮 tar 壓縮包,建立一個包含全部源代碼、名爲 sed-3.02 的目錄。之後將使用環境變量 "$SRCDIR" 引用 sed-3.02 目錄。要編譯程序,輸入:
將 sed 解壓縮到臨時目錄
$ cd sed-3.02 $ ./configure --prefix=/usr (autoconf 生成適當的 make 文件,這要花一些時間) $ make (從源代碼編譯包,也要花一點時間)
由於在本文中只講述解包和編譯步驟,因此將略過 "make install" 步驟。若是要編寫 bash 腳原本執行全部這些步驟,則代碼可能相似於:
要執行解包/編譯過程的樣本 bash 腳本
#!/usr/bin/env bash if [ -d work ] then # remove old work directory if it exists rm -rf work fi mkdir work cd work tar xzf /usr/src/distfiles/sed-3.02.tar.gz cd sed-3.02 ./configure --prefix=/usr make
使代碼通用
雖然可使用這個自動編譯腳本,但它不是很靈活。基本上,bash 腳本只包含在命令行輸入的全部命令列表。雖然可使用這種解決方案,可是,最好作一個只經過更改幾行就能夠快速解包和編譯任何包的適用腳本。這樣,包維護人員將新包添加到發行版所需的工做就大爲減小。讓咱們先嚐試一下使用許多不一樣的環境變量來完成,使構建腳本更加適用:
新的、更通用的腳本
#!/usr/bin/env bash # P is the package name P=sed-3.02 # A is the archive name A=${P}.tar.gz export ORIGDIR=`pwd` export WORKDIR=${ORIGDIR}/work export SRCDIR=${WORKDIR}/${P} if [ -z "$DISTDIR" ] then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfiles fi export DISTDIR if [ -d ${WORKDIR} ] then # remove old work directory if it exists rm -rf ${WORKDIR} fi mkdir ${WORKDIR} cd ${WORKDIR} tar xzf ${DISTDIR}/${A} cd ${SRCDIR} ./configure --prefix=/usr make
已經向代碼中添加了不少環境變量,可是,它基本上仍是執行同一功能。可是,若是如今要要編譯任何標準的 GNU 基於 autoconf 的源代碼 tar 壓縮包,只需簡單地將該文件複製到一個新文件(用合適的名稱來反映它所編譯的新包名),而後將 "$A" 和 "$P" 的值更改爲新值便可。全部其它環境變量都自動調整成正確設置,而且腳本按預想工做。雖然這很方便,可是代碼還有改進餘地。這段代碼比咱們開始建立的 "transcript" 腳本要長不少。既然任何編程項目的目標之一是減小用戶複雜度,因此最好大幅度縮短代碼,或者至少更好地組織代碼。能夠用一個巧妙的方法來作到這點 -- 將代碼拆成兩個單獨文件。將該文件存爲 "sed-3.02.ebuild":
sed-3.02.ebuild
#the sed ebuild file -- very simple! P=sed-3.02 A=${P}.tar.gz
第一個文件不重要,只包含那些必須在每一個包中配置的環境變量。下面是第二個文件,它包含操做的主要部分。將它存爲 "ebuild",並使它成爲可執行文件:
ebuild 腳本
#!/usr/bin/env bash if [ $# -ne 1 ] then echo "one argument expected." exit 1 fi if [ -e "$1" ] then source $1 else echo "ebuild file $1 not found." exit 1 fi export ORIGDIR=`pwd` export WORKDIR=${ORIGDIR}/work export SRCDIR=${WORKDIR}/${P} if [ -z "$DISTDIR" ] then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfiles fi export DISTDIR if [ -d ${WORKDIR} ] then # remove old work directory if it exists rm -rf ${WORKDIR} fi mkdir ${WORKDIR} cd ${WORKDIR} tar xzf ${DISTDIR}/${A} cd ${SRCDIR} ./configure --prefix=/usr make
既然已經將構建系統拆成兩個文件,我敢打賭,您必定在想它的工做原理。基本上,要編譯 sed,輸入:
$ ./ebuild sed-3.02.ebuild
當執行 "ebuild" 時,它首先試圖 "source" 變量 "$1"。這是什麼意思?還記得 前一篇文章 所講的嗎:"$1" 是第一個命令行自變量 -- 在這裏,是 "sed-3.02.ebuild"。在 bash 中,"source" 命令從文件中讀入 bash 語句,而後執行它們,就好象它們直接出如今 "source" 命令所在的文件中同樣。所以,"source ${1}" 致使 "ebuild" 腳本執行在 "sed-3.02.ebuild" 中定義 "$P" 和 "$A" 的命令。這種設計更改確實方便,由於若是要編譯另外一個程序,而不是 sed,能夠簡單地建立一個新的 .ebuild 文件,而後將其做爲自變量傳遞給 "ebuild" 腳本。經過這種方式,.ebuild 文件最終很是簡單,而將 ebuild 系統複雜的操做部分存在一處,即 "ebuild" 腳本中。經過這種方式,只需編輯 "ebuild" 腳本就能夠升級或加強 ebuild 系統,同時將實現細節保留在 ebuild 文件以外。這裏有一個 gzip 的樣本 ebuild 文件:
gzip-1.2.4a.ebuild
#another really simple ebuild script! P=gzip-1.2.4a A=${P}.tar.gz
添加功能性
好,咱們正在取得進展。可是,我還想添加某些額外功能性。我但願 ebuild 腳本再接受一個命令行自變量:"compile"、"unpack" 或 "all"。這個命令行自變量告訴 ebuild 腳本要執行構建過程的哪一步。經過這種方式,能夠告訴 ebuild 解包檔案,但不進行編譯(以便在開始編譯以前查看源代碼檔案)。要作到這點,將添加一條 case 語句,該語句將測試 "$2",而後根據其值執行不一樣操做。代碼以下:
ebuild,修定本 2
#!/usr/bin/env bash if [ $# -ne 2 ] then echo "Please specify two args - .ebuild file and unpack, compile or all" exit 1 fi if [ -z "$DISTDIR" ] then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfiles fi export DISTDIR ebuild_unpack() { #make sure we're in the right directory cd ${ORIGDIR} if [ -d ${WORKDIR} ] then rm -rf ${WORKDIR} fi mkdir ${WORKDIR} cd ${WORKDIR} if [ ! -e ${DISTDIR}/${A} ] then echo "${DISTDIR}/${A} does not exist. Please download first." exit 1 fi tar xzf ${DISTDIR}/${A} echo "Unpacked ${DISTDIR}/${A}." #source is now correctly unpacked } ebuild_compile() { #make sure we're in the right directory cd ${SRCDIR} if [ ! -d "${SRCDIR}" ] then echo "${SRCDIR} does not exist -- please unpack first." exit 1 fi ./configure --prefix=/usr make } export ORIGDIR=`pwd` export WORKDIR=${ORIGDIR}/work if [ -e "$1" ] then source $1 else echo "Ebuild file $1 not found." exit 1 fi export SRCDIR=${WORKDIR}/${P} case "${2}" in unpack) ebuild_unpack ;; compile) ebuild_compile ;; all) ebuild_unpack ebuild_compile ;; *) echo "Please specify unpack, compile or all as the second arg" exit 1 ;; esac
已經作了不少改動,下面來回顧一下。首先,將編譯和解包步驟放入各自的函數中,其函數名分別爲 ebuild_compile() 和 ebuild_unpack()。這是個好的步驟,由於代碼正變得愈來愈複雜,而新函數提供了必定的模塊性,使代碼更有條理。在每一個函數的第一行,顯式 "cd" 到想要的目錄,由於,隨着代碼變得愈來愈模塊化而不是線形化,出現疏忽而在錯誤的當前工做目錄中執行函數的可能性也變大。"cd" 命令顯式地使咱們處於正確的位置,並防止之後出現錯誤 - 這是重要的步驟,特別是在函數中刪除文件時更是如此。
另外,還在 ebuild_compile() 函數的開始處添加了一個有用的檢查。如今,它檢查以確保 "$SRCDIR" 存在,若是不存在,則打印一條告訴用戶首先解包檔案而後退出的錯誤消息。若是願意,能夠改變這種行爲,以便在 "amp;$SRCDIR" 不存在的狀況下,ebuild 腳本將自動解包源代碼檔案。能夠用如下代碼替換 ebuild_compile() 來作到這點:
ebuild_compile() 上的新代碼
ebuild_compile() { #make sure we're in the right directory if [ ! -d "${SRCDIR}" ] then ebuild_unpack fi cd ${SRCDIR} ./configure --prefix=/usr make }
ebuild 腳本第二版中最明顯的改動之一就是代碼末尾新的 case 語句。這條 case 語句只是檢查第二個命令行自變量,而後根據其值執行正確操做。若是如今輸入:
$ ebuild sed-3.02.ebuild
就會獲得一條錯誤消息。如今須要告訴 ebuild 作什麼,以下所示:
$ ebuild sed-3.02.ebuild unpack
或
$ ebuild sed-3.02.ebuild compile
或
$ ebuild sed-3.02.ebuild all
若是提供上面所列以外的第二個命令行自變量,將獲得一條錯誤消息(* 子句),而後,程序退出。
使代碼模塊化
既然代碼很高級而且實用,您可能很想建立幾個更高級的 ebuild 腳本,以解包和編譯所喜好的程序。若是這樣作,早晚會遇到一些不使用 autoconf ("./configure") 的源代碼,或者可能遇到其它使用非標準編譯過程的腳本。須要再對 ebuild 系統作一些改動,以適應這些程序。可是在作以前,最好先想一下如何完成。
將 "./configure --prefix=/usr; make" 硬編碼到編譯階段的妙處之一是:大多數時候,它能夠正確工做。可是,還必須使 ebuild 系統適應那些不使用 autoconf 或正常 make 文件的源代碼。要解決這個問題,建議 ebuild 腳本缺省執行如下操做:
./configure --prefix=/usr
make
既然 ebuild 只在 configure 實際存在時才運行它,如今能夠自動地適應那些不使用 autoconf 但有標準 make 文件的程序。可是,在簡單的 "make" 對某些源代碼無效時該怎麼辦?須要一些處理這些狀況的特定代碼來覆蓋合理的缺省值。要作到這一點,將把 ebuild_compile() 函數轉換成兩個函數。第一個函數(可將其當成「父」函數)的名稱還是 ebuild_compile()。可是,將有一個名爲 user_compile() 的新函數,該函數只包含合理的缺省操做:
拆成兩個函數的 ebuild_compile()
user_compile() { #we're already in ${SRCDIR} if [ -e configure ] then #run configure script if it exists ./configure --prefix=/usr fi #run make make } ebuild_compile() { if [ ! -d "${SRCDIR}" ] then echo "${SRCDIR} does not exist -- please unpack first." exit 1 fi #make sure we're in the right directory cd ${SRCDIR} user_compile }
如今這樣作的緣由可能還不是很明顯,可是,再忍耐一下。雖然這段代碼與 ebuild 前一版的工做方式幾乎相同,可是如今能夠作一些之前沒法作的 -- 能夠在 sed-3.02.ebuild 中覆蓋 user_compile()。所以,若是缺省的 user_compile() 不知足要求,能夠在 .ebuild 文件中定義一個新的,使其包含編譯包所必需的命令。例如,這裏有一個 e2fsprogs-1.18 的 ebuild 文件,它須要一個略有不一樣的 "./configure" 行:
e2fsprogs-1.18.ebuild
#this ebuild file overrides the default user_compile() P=e2fsprogs-1.18 A=${P}.tar.gz user_compile() { ./configure --enable-elf-shlibs make }
如今,將徹底按照咱們但願的方式編譯 e2fsprogs。可是,對於大多數包來講,能夠省略 .ebuild 文件中的任何定製 user_compile() 函數,而使用缺省的 user_compile() 函數。
ebuild 腳本又怎樣知道要使用哪一個 user_compile() 函數呢?實際上,這很簡單。ebuild 腳本中,在執行 e2fsprogs-1.18.ebuild 文件以前定義缺省 user_compile() 函數。若是在 e2fsprogs-1.18.ebuild 中有一個 user_compile(),則它覆蓋前面定義的缺省版本。若是沒有,則使用缺省 user_compile() 函數。
這是好工具,咱們已經添加了不少靈活性,而無需任何複雜代碼(若是不須要的話)。在這裏就不講了,可是,還應該對 ebuild_unpack() 作相似修改,以便用戶能夠覆蓋缺省解包過程。若是要作任何修補,或者文件包含在多個檔案中,則這很是方便。還有個好主意是修改解包代碼,以便它能夠缺省識別由 bzip2 壓縮的 tar 壓縮包。
配置文件
目前爲止,已經講了不少不方便的 bash 技術,如今再講一個。一般,若是程序在 /etc 中有一個配置文件是很方便的。幸運的是,用 bash 作到這點很容易。只需建立如下文件,而後並其存爲 /etc/ebuild.conf 便可:
/ect/ebuild.conf
# /etc/ebuild.conf: set system-wide ebuild options in this file # MAKEOPTS are options passed to make MAKEOPTS="-j2"
在該例中,只包括了一個配置選項,可是,您能夠包括更多。bash 的一個妙處是:經過執行該文件,就能夠對它進行語法分析。在大多數解釋型語言中,均可以使用這個設計竅門。執行 /etc/ebuild.conf 以後,在 ebuild 腳本中定義 "$MAKEOPTS"。將利用它容許用戶向 make 傳遞選項。一般,將使用該選項來容許用戶告訴 ebuild 執行 並行 make。
什麼是並行 make?
爲了提升多處理器系統的編譯速度,make 支持並行編譯程序。這意味着,make 同時編譯用戶指定數目的源文件(以便使用多處理器系統中的額外處理器),而不是一次只編譯一個源文件。經過向 make 傳遞 -j # 選項來啓用並行 make,以下所示:
make -j4 MAKE="make -j4"
這行代碼指示 make 同時編譯四個程序。 MAKE="make -j4"
自變量告訴 make,向其啓動的任何子 make 進程傳遞 -j4 選項。
這裏是 ebuild 程序的最終版本:
ebuild,最終版本
#!/usr/bin/env bash if [ $# -ne 2 ] then echo "Please specify ebuild file and unpack, compile or all" exit 1 fi source /etc/ebuild.conf if [ -z "$DISTDIR" ] then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfiles fi export DISTDIR ebuild_unpack() { #make sure we're in the right directory cd ${ORIGDIR} if [ -d ${WORKDIR} ] then rm -rf ${WORKDIR} fi mkdir ${WORKDIR} cd ${WORKDIR} if [ ! -e ${DISTDIR}/${A} ] then echo "${DISTDIR}/${A} does not exist. Please download first." exit 1 fi tar xzf ${DISTDIR}/${A} echo "Unpacked ${DISTDIR}/${A}." #source is now correctly unpacked } user_compile() { #we're already in ${SRCDIR} if [ -e configure ] then #run configure script if it exists ./configure --prefix=/usr fi #run make make $MAKEOPTS MAKE="make $MAKEOPTS" } ebuild_compile() { if [ ! -d "${SRCDIR}" ] then echo "${SRCDIR} does not exist -- please unpack first." exit 1 fi #make sure we're in the right directory cd ${SRCDIR} user_compile } export ORIGDIR=`pwd` export WORKDIR=${ORIGDIR}/work if [ -e "$1" ] then source $1 else echo "Ebuild file $1 not found." exit 1 fi export SRCDIR=${WORKDIR}/${P} case "${2}" in unpack) ebuild_unpack ;; compile) ebuild_compile ;; all) ebuild_unpack ebuild_compile ;; *) echo "Please specify unpack, compile or all as the second arg" exit 1 ;; esac
請注意,在文件的開始部分執行 /etc/ebuild.conf。另外,還要注意,在缺省 user_compile() 函數中使用 "$MAKEOPTS"。您可能在想,這管用嗎 - 畢竟,在執行實際上事先定義 "$MAKEOPTS" 的 /etc/ebuild.conf 以前,咱們引用了 "$MAKEOPTS"。對咱們來講幸運的是,這沒有問題,由於變量擴展只在執行 user_compile() 時才發生。在執行 user_compile() 時,已經執行了 /etc/ebuild.conf,而且 "$MAKEOPTS" 也被設置成正確的值。
結束語
本文已經講述了不少 bash 編程技術,可是,只觸及到 bash 能力的一些皮毛。例如,Gentoo Linux ebuild 產品不只自動解包和編譯每一個包,還能夠:
另外,ebuild 系統產品還有幾個全局配置選項,容許用戶指定選項,例如在編譯過程當中使用什麼優化標誌,在那些支持它的包中是否應該缺省啓用可選的包支持(例如 GNOME 和 slang)。
顯然,bash 能夠實現的功能遠比本系列文章中所觸及的要多。關於這個難以想象的工具,但願您已經學到了不少,並鼓舞您使用 bash 來加快和加強開發項目。