總裁兼 CEO, Gentoo Technologies, Inc.
2000 年 5 月
Daniel Robbins 在其最後一篇 Bash實例文章中詳細講述了 Gentoo Linux ebuild 系統,這個展示 bash能力的極佳范例。循序漸進地,他為您展示如何實現 ebuild系統,並觸及很多方便的 bash技術和設計策略。在本文末尾,您將很好地掌握制造完全基於 bash的應用所涉及的技術,並開始為自己的自動構建系統編碼。
進入 ebuild 系統
我真是一直期待著這第三篇、也是最後一篇 Bash 實例文章,因為既然已經在 第 1 篇和 第 2 篇 中講述了 bash 編程基礎,就可以集中講述象 bash 應用開發和程序設計這樣更高級的主題。在本文中,將通過我花了許多時間來編碼和細化的項目,Gentoo Linux ebuild 系統,來給您大量實際的、現實世界的 bash 開發經驗。
我是 Gentoo Linux(目前還是 beta 版的下一代 Linux OS)的首席設計師。我的主要責任之一就是確保所有二進制包(類似於 RPM)都正確創建並一起使用。正如您可能知道的,標准 Linux 系統不是由一棵統一的源樹組成(象 BSD),而實際上是由超過 25 個協同工作的核心包組成。這其中包括:
包 描述 linux實際內核util-linux與 Linux 相關的雜項程序集合e2fsprogs與 ext2 文件系統相關的實用程序集合glibcGNU C 庫每個包都位於各自的 tar 壓縮包中,並由不同的獨立開發人員或開發小組維護。要創建一個發行版,必須對每個包分別進行下載、編譯和打包處理。每次要修復、升級或改進包時,都必須重復編譯和打包步驟(並且,包確實更新得很快)。為了幫助消除創建和更新包所涉及的重復步驟,我創建了 ebuild 系統,該系統幾乎全用 bash 編寫。為了增加您的 bash 知識,我將循序漸進地為您演示如何實現該 ebuild 系統的解包和編譯部分。在解釋每一步時,還將討論為什麼要作出某些設計決定。在本文末尾,您不僅將極好地掌握大型 bash 編程項目,還實現了完整自動構建系統的很大一部分。
為什麼選擇 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 bashif [ -d work ]then # remove old work directory if it exists rm -rf workfimkdir workcd worktar xzf /usr/src/distfiles/sed-3.02.tar.gzcd sed-3.02./configure --prefix=/usrmake
使代碼通用
雖然可以使用這個自動編譯腳本,但它不是很靈活。基本上,bash 腳本只包含在命令行輸入的所有命令列表。雖然可以使用這種解決方案,但是,最好做一個只通過更改幾行就可以快速解包和編譯任何包的適用腳本。這樣,包維護人員將新包添加到發行版所需的工作就大為減少。讓我們先嘗試一下使用許多不同的環境變量來完成,使構建腳本更加適用:
#!/usr/bin/env bash# P is the package nameP=sed-3.02# A is the archive nameA=$.tar.gzexport ORIGDIR=`pwd`export WORKDIR=$/workexport SRCDIR=$/$if [ -z "$DISTDIR" ]then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfilesfiexport DISTDIRif [ -d $ ]then # remove old work directory if it exists rm -rf $fimkdir $cd $tar xzf $/$cd $./configure --prefix=/usrmake
已經向代碼中添加了很多環境變量,但是,它基本上還是執行同一功能。但是,如果現在要要編譯任何標准的 GNU 基於 autoconf 的源代碼 tar 壓縮包,只需簡單地將該文件復制到一個新文件(用合適的名稱來反映它所編譯的新包名),然後將 "$A" 和 "$P" 的值更改成新值即可。所有其它環境變量都自動調整成正確設置,並且腳本按預想工作。雖然這很方便,但是代碼還有改進余地。這段代碼比我們開始創建的 "transcript" 腳本要長很多。既然任何編程項目的目標之一是減少用戶復雜度,所以最好大幅度縮短代碼,或者至少更好地組織代碼。可以用一個巧妙的方法來做到這點 -- 將代碼拆成兩個單獨文件。將該文件存為 "sed-3.02.ebuild":
sed-3.02.ebuild#the sed ebuild file -- very simple!P=sed-3.02A=$.tar.gz
第一個文件不重要,只包含那些必須在每個包中配置的環境變量。下面是第二個文件,它包含操作的主要部分。將它存為 "ebuild",並使它成為可執行文件:
ebuild 腳本#!/usr/bin/env bashif [ $# -ne 1 ]then echo "one argument expected." exit 1fiif [ -e "" ]then source else echo "ebuild file not found." exit 1fiexport ORIGDIR=`pwd`export WORKDIR=$/workexport SRCDIR=$/$if [ -z "$DISTDIR" ]then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfilesfiexport DISTDIRif [ -d $ ]then # remove old work directory if it exists rm -rf $fimkdir $cd $tar xzf $/$cd $./configure --prefix=/usrmake
既然已經將構建系統拆成兩個文件,我敢打賭,您一定在想它的工作原理。基本上,要編譯 sed,輸入:
$ ./ebuild sed-3.02.ebuild
當執行 "ebuild" 時,它首先試圖 "source" 變量 ""。這是什麼意思?還記得 前一篇文章 所講的嗎:"" 是第一個命令行自變量 -- 在這裡,是 "sed-3.02.ebuild"。在 bash 中,"source" 命令從文件中讀入 bash 語句,然後執行它們,就好象它們直接出現在 "source" 命令所在的文件中一樣。因此,"source " 導致 "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.4aA=$.tar.gz
添加功能性
好,我們正在取得進展。但是,我還想添加某些額外功能性。我希望 ebuild 腳本再接受一個命令行自變量:"compile"、"unpack" 或 "all"。這個命令行自變量告訴 ebuild 腳本要執行構建過程的哪一步。通過這種方式,可以告訴 ebuild 解包檔案,但不進行編譯(以便在開始編譯之前查看源代碼檔案)。要做到這點,將添加一條 case 語句,該語句將測試 "",然後根據其值執行不同操作。代碼如下:
#!/usr/bin/env bashif [ $# -ne 2 ]then echo "Please specify two args - .ebuild file and unpack, compile or all" exit 1fiif [ -z "$DISTDIR" ]then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfilesfiexport DISTDIRebuild_unpack() { #make sure we're in the right directory cd $ if [ -d $ ] then rm -rf $ fi mkdir $ cd $ if [ ! -e $/$ ] then echo "$/$ does not exist. Please download first." exit 1 fi tar xzf $/$ echo "Unpacked $/$." #source is now correctly unpacked}ebuild_compile() { #make sure we're in the right directory cd $ if [ ! -d "$" ] then echo "$ does not exist -- please unpack first." exit 1 fi ./configure --prefix=/usr make }export ORIGDIR=`pwd`export WORKDIR=$/workif [ -e "" ]then source else echo "Ebuild file not found." exit 1fiexport SRCDIR=$/$case "" 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" 存在,如果不存在,則打印一條告訴用戶首先解包檔案然後退出的錯誤消息。如果願意,可以改變這種行為,以便在 "$SRCDIR" 不存在的情況下,ebuild 腳本將自動解包源代碼檔案。可以用以下代碼替換 ebuild_compile() 來做到這點:
ebuild_compile() 上的新代碼ebuild_compile() { #make sure we're in the right directory if [ ! -d "$" ] then ebuild_unpack fi cd $ ./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 $ if [ -e configure ] then #run configure script if it exists ./configure --prefix=/usr fi #run make make} ebuild_compile() { if [ ! -d "$" ] then echo "$ does not exist -- please unpack first." exit 1 fi #make sure we're in the right directory cd $ 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.18A=$.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 即可:
# /etc/ebuild.conf: set system-wide ebuild options in this file# MAKEOPTS are options passed to makeMAKEOPTS="-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 bashif [ $# -ne 2 ]then echo "Please specify ebuild file and unpack, compile or all" exit 1fisource /etc/ebuild.confif [ -z "$DISTDIR" ]then # set DISTDIR to /usr/src/distfiles if not already set DISTDIR=/usr/src/distfilesfiexport DISTDIRebuild_unpack() { #make sure we're in the right directory cd $ if [ -d $ ] then rm -rf $ fi mkdir $ cd $ if [ ! -e $/$ ] then echo "$/$ does not exist. Please download first." exit 1 fi tar xzf $/$ echo "Unpacked $/$." #source is now correctly unpacked}user_compile() { #we're already in $ 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 "$" ] then echo "$ does not exist -- please unpack first." exit 1 fi #make sure we're in the right directory cd $ user_compile}export ORIGDIR=`pwd`export WORKDIR=$/workif [ -e "" ]then source else echo "Ebuild file not found." exit 1fiexport SRCDIR=$/$case "" 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 來加快和增強開發項目。