前言
感謝您抽出時間閱讀有關 SCons 的內容。SCons 是一款下一代軟件構建工具,或者稱為 make 工具,即一種用于構建軟件(或其他文件)并在底層輸入文件發生更改時使已構建的軟件保持最新狀態的軟件實用程序。
SCons 最顯著的特點是其配置文件實際上是用 Python 編程語言編寫的腳本。這與大多數其他構建工具形成鮮明對比,后者通常會發明一種新語言來配置構建過程。當然,學習 SCons 仍然需要一定的過程,因為您必須知道調用哪些函數才能正確設置構建過程,但對于任何看過 Python 腳本的人來說,所使用的底層語法應該是熟悉的。
矛盾的是,使用 Python 作為配置文件格式使 SCons 比其他構建工具的晦澀語言更容易被非程序員學習,而這些晦澀語言通常是程序員為其他程序員發明的。這在很大程度上歸功于 Python 的一致性和可讀性,而這正是 Python 的標志。事實證明,將一種真正的腳本語言作為配置文件的基礎,恰好使更有經驗的程序員能夠根據需要輕松地使用構建工具完成更復雜的任務。
1. SCons 原則
在設計和實現 SCons 時,我們努力遵循以下幾個首要原則:
正確性
首先也是最重要的,默認情況下,SCons 保證構建的正確性,即使這意味著要稍微犧牲一些性能。我們致力于確保無論被構建軟件的結構如何、編寫方式如何,或者用于構建它的工具多么特殊,構建過程都是正確的。
性能
在保證構建正確的前提下,我們努力使 SCons 盡可能快速地構建軟件。特別是,在為了保證構建正確性而可能需要減慢 SCons 默認行為的任何地方,我們也嘗試通過優化選項使其易于加速,這些選項允許您在所有極端情況下犧牲保證的正確性,以換取通常情況下更快的構建速度。
便利性
SCons 試圖在合理的范圍內盡可能為您提供開箱即用的功能,包括在您的系統上檢測正確的工具并正確使用它們來構建軟件。
簡而言之,我們努力使 SCons 只需 “做正確的事”,就能正確構建軟件,同時盡量減少麻煩。
2. 關于本指南完整性的一點說明
在閱讀本指南時,有一點需要注意:與許多開源軟件一樣,SCons 的文檔并不總是與可用功能保持同步更新。換句話說,SCons 能做的很多事情在本用戶指南中尚未涵蓋。(仔細想想,這也描述了許多專有軟件,不是嗎?)
盡管本用戶指南并不像我們希望的那樣完整,但我們的開發過程確實強調確保 SCons 的手冊頁與新功能保持同步更新。因此,如果您試圖弄清楚如何實現 SCons 支持的某項功能,但在此處找不到足夠的(或任何)信息,那么查看手冊頁以確定該信息是否包含在內是值得的。如果您這樣做了,也許您甚至可以考慮為用戶指南貢獻一個章節,這樣下一個尋找該信息的人就不必經歷同樣的過程了……?
3. 致謝
如果沒有許多人的幫助,SCons 就不會存在,其中許多人可能甚至沒有意識到他們提供了幫助或啟發。因此,不分先后順序,冒著遺漏某人的風險:
首先,SCons 要深深感謝 Bob Sidebotham,他是經典的基于 Perl 的 Cons 工具的原作者,Bob 大約在 1996 年首次將其發布給世界。Bob 在經典 Cons 上的工作提供了底層架構和使用真正的腳本語言指定構建配置的模型。我在 Cons 上的實際工作經驗為 SCons 的許多設計決策提供了信息,包括改進的并行構建支持、使用戶能夠輕松定義 Builder 對象,以及將構建引擎與包裝接口分離。
Greg Wilson 在 2000 年 2 月發起 Software Carpentry 設計競賽時,對將 SCons 作為一個真正的項目啟動起到了關鍵作用。沒有那次推動,將經典 Cons 架構的優勢與 Python 的可讀性相結合可能僅僅停留在一個好主意上。
整個 SCons 團隊的合作絕對非常愉快,如果沒有過去幾年人們貢獻的精力、熱情和時間,SCons 不可能成為如此有用的工具。Chad Austin、Anthony Roach、Bill Deegan、Charles Crain、Steve Leblanc、Greg Noel、Gary Oberbrunner、Greg Spencer 和 Christoph Wiedemann 組成的 “核心團隊” 在審查我的(和其他)更改并在問題進入代碼庫之前發現問題方面做得非常出色。特別值得注意的技術貢獻包括:Anthony 在任務引擎方面出色而創新的工作為 SCons 提供了極其優越的并行構建模型;Charles 是關鍵 Node 基礎設施的大師;Christoph 在 Configure 基礎設施方面的工作增加了類似 Autoconf 的關鍵功能;Greg 為 Microsoft Visual Studio 提供了出色的支持。
特別感謝 David Snopek 貢獻了他的底層 “Autoscons” 代碼,該代碼構成了 Christoph 實現 Configure 功能的基礎。考慮到 David 最初在 GPL 下發布該代碼,而 SCons 在限制性較小的 MIT 風格許可證下發布,他將此代碼提供給 SCons 非常慷慨。
感謝 Peter Miller 開發的出色變更管理系統 Aegis,它從一開始就為 SCons 項目提供了穩健的開發方法,并向我展示了如何將增量回歸測試集成到實際開發周期中(早在極限編程出現之前很多年)。
最后,感謝 Guido van Rossum 開發的優雅腳本語言,它不僅是 SCons 實現的基礎,也是其接口本身的基礎。
4. 聯系方式
聯系參與 SCons 的人員(包括作者)的最佳方式是通過 SCons 郵件列表。
如果您想詢問有關如何使用 SCons 的一般問題,請發送電子郵件至 scons-users@scons.org。
如果您想直接聯系 SCons 開發社區,請發送電子郵件至 scons-dev@scons.org。
如果您想接收有關 SCons 的公告,請加入低流量的 announce@scons.tigris.org 郵件列表。
第 1 章 構建和安裝 SCons
本章將引導您完成在系統上安裝 SCons 的基本步驟,以及在沒有預構建包可用(或者只是更喜歡自己構建的靈活性)的情況下構建 SCons 的過程。不過,在此之前,本章還將描述在系統上安裝 Python 的基本步驟,以防這是必要的。幸運的是,SCons 和 Python 在幾乎任何系統上都非常容易安裝,而且許多系統已經預裝了 Python。
1.1 安裝 Python
由于 SCons 是用 Python 編寫的,因此在使用 SCons 之前,您需要在系統上安裝 Python。在嘗試安裝 Python 之前,您應該通過在系統的命令行提示符下輸入 python -V(大寫的 'V')或 python --version 來檢查系統上是否已經有 Python 可用。對于 Linux/Unix/MacOS/BSD 類型的系統,如下所示:
bash
$ python -V
Python 3.7.1
在 Windows 系統的 cmd shell 或 PowerShell 中(注意 PowerShell 需要拼寫為 "python.exe" 而不是 "python"):
bash
C:\>python -V
Python 3.7.1
如果系統上未安裝 Python,您將看到一條錯誤消息,例如 "command not found"(在 UNIX 或 Linux 上)或 "‘python’ is not recognized as an internal or external command, operable progam or batch file"(在 Windows 上)。在這種情況下,您需要先安裝 Python 才能安裝 SCons。
下載和安裝 Python 的標準信息位置是Download Python | Python.org。請查看該頁面及相關鏈接以開始安裝。
對于 Linux 系統,Python 幾乎肯定可以作為受支持的包使用,可能默認已安裝;這通常比通過其他方式安裝更可取,也比從源代碼安裝更容易。許多此類系統為 Python 2 和 Python 3 提供單獨的包。如果您需要使用的發行版未提供的版本,從源代碼構建仍然是一個有用的選擇。
SCons 可以與 Python 2.7.x 或 Python 3.5 及更高版本一起使用。如果您需要安裝 Python 并且可以選擇,我們建議使用可用的最新 Python 版本。較新的 Python 有顯著的改進,有助于提高 SCons 的性能。
1.2 安裝 SCons
安裝 SCons 的標準方法是從 Python 包索引(PyPi)安裝:
bash
% python -m pip install scons
如果您不想安裝到 Python 系統位置,或者沒有這樣做的權限,您可以添加一個標志來安裝到您自己賬戶的特定位置:
bash
% python -m pip install --user scons
SCons 已預先打包,可在許多 Linux 系統上安裝。檢查您的包安裝系統,看看是否有可用的 SCons 包。如果可用,許多人更喜歡安裝發行版原生包,因為它們提供了一個集中的管理和更新點。一些發行版提供兩個 SCons 包,一個使用 Python 2,另一個使用 Python 3。如果您需要的 SCons 特定版本與可用包不同,pip 有一個版本選項,或者您可以按照下一節中的說明操作。
1.3 在任何系統上構建和安裝 SCons
如果您的系統沒有預構建的 SCons 包,并且使用 pip 安裝不合適,那么您仍然可以使用原生 Python distutils 包輕松構建和安裝 SCons。
第一步是從 SCons 下載頁面http://www.scons.org/download.html下載 scons-3.1.2.tar.gz 或 scons-3.1.2.zip。
使用 Linux 或 UNIX 上的 tar 等實用程序,或 Windows 上的 WinZip 等實用程序解壓縮下載的歸檔文件。這將創建一個名為 scons-3.1.2 的目錄,通常在您的本地目錄中。然后將工作目錄更改為該目錄,并執行以下命令安裝 SCons:
bash
# cd scons-3.1.2
# python setup.py install
這將構建 SCons,將 scons 腳本安裝到用于運行 setup.py 的 Python 的 scripts 目錄(/usr/local/bin 或 C:\Python27\Scripts),并將 SCons 構建引擎安裝到所用 Python 的相應庫目錄(/usr/local/lib/scons 或 C:\Python27\scons)。因為這些是系統目錄,所以您可能需要 root 權限(在 Linux 或 UNIX 上)或管理員權限(在 Windows 上)才能以這種方式安裝 SCons。
1.3.1 并排構建和安裝多個版本的 SCons
SCons 的 setup.py 腳本有一些擴展,支持在并排位置輕松安裝多個版本的 SCons。例如,這使得在將正式構建過程遷移到新版本之前,更容易下載和試驗不同版本的 SCons。
要在特定版本的位置安裝 SCons,請在調用 setup.py 時添加 --version-lib 選項:
bash
# python setup.py install --version-lib
例如,這將把 SCons 構建引擎安裝到 /usr/lib/scons-3.1.2 或 C:\Python27\scons-3.1.2 目錄中。
如果您第一次安裝 SCons 時使用了 --version-lib 選項,則不必在每次安裝新版本時都指定它。SCons 的 setup.py 腳本會檢測特定版本的目錄名稱,并假設您希望將所有版本安裝到特定版本的目錄中。您可以在將來通過顯式指定 --standalone-lib 選項來覆蓋該假設。
1.3.2 安裝 SCons 到其他位置
您可以通過指定 --prefix = 選項將 SCons 安裝到默認位置以外的其他位置:
bash
# python setup.py install --prefix=/opt/scons
這會將 scons 腳本安裝到 /opt/scons/bin,將構建引擎安裝到 /opt/scons/lib/scons。
請注意,您可以同時指定 --prefix = 和 --version-lib 選項,在這種情況下,setup.py 會將構建引擎安裝到相對于指定前綴的特定版本目錄中。在上例中添加 --version-lib 會將構建引擎安裝到 /opt/scons/lib/scons-3.1.2。
1.3.3 無管理權限構建和安裝 SCons
如果您沒有權限將 SCons 安裝到系統位置,只需使用 --prefix = 選項將其安裝到您選擇的位置。例如,要將 SCons 安裝到相對于用戶目錄的適當位置,將腳本安裝到HOME/bin,將構建引擎安裝到 $HOME/lib/scons,只需鍵入:
bash
$ python setup.py install --prefix=$HOME
當然,您可以指定任何其他您喜歡的位置,如果您想相對于指定前綴安裝特定版本的目錄,可以使用 --version-lib 選項。
這也可用于試驗比系統位置中安裝的版本更新的 SCons 版本。當然,您安裝較新版本 scons 腳本的位置(上例中的 $HOME/bin)必須在包含系統安裝版本 scons 腳本的目錄之前配置在您的 PATH 變量中。
第 2 章 簡單構建
在本章中,您將看到幾個使用 SCons 進行非常簡單的構建配置的示例,這些示例將展示使用 SCons 在不同類型的系統上從幾種不同的編程語言構建程序是多么容易。
2.1 構建簡單的 C/C++ 程序
這是著名的 C 語言 "Hello, World!" 程序:
c
int
main()
{printf("Hello, world!\n");
}
以下是使用 SCons 構建它的方法。在名為 SConstruct 的文件中輸入以下內容:
python
運行
Program('hello.c')
這個最小的配置文件為 SCons 提供了兩條信息:您想要構建什么(一個可執行程序),以及您想要從哪個輸入文件構建它(hello.c 文件)。Program 是一個 builder_method,一個 Python 調用,告訴 SCons 您想要構建一個可執行程序。
就這樣。現在運行 scons 命令來構建程序。在像 Linux 或 UNIX 這樣符合 POSIX 標準的系統上,您會看到類似以下內容:
bash
% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cc -o hello.o -c hello.c
cc -o hello hello.o
scons: done building targets.
在帶有 Microsoft Visual C++ 編譯器的 Windows 系統上,您會看到類似以下內容:
bash
C:\>scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
scons: done building targets.
首先,請注意您只需要指定源文件的名稱,SCons 會正確推斷出要從源文件名的基名構建的目標文件和可執行文件的名稱。
其次,請注意相同的輸入 SConstruct 文件無需任何更改,即可在兩個系統上生成正確的輸出文件名:在 POSIX 系統上是 hello.o 和 hello,在 Windows 系統上是 hello.obj 和 hello.exe。這是 SCons 使編寫可移植軟件構建極其容易的一個簡單示例。
(注意,在本指南的所有示例中,我們不會同時提供 POSIX 和 Windows 的輸出;只需記住,除非另有說明,任何示例都應在兩種類型的系統上同樣有效。)
2.2 構建目標文件
Program 構建器方法只是 SCons 提供的用于構建不同類型文件的眾多構建器方法之一。另一個是 Object 構建器方法,它告訴 SCons 從指定的源文件構建一個目標文件:
python
運行
Object('hello.c')
現在,當您運行 scons 命令來構建程序時,它將在 POSIX 系統上只構建 hello.o 目標文件:
bash
% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cc -o hello.o -c hello.c
scons: done building targets.
在 Windows 系統上(使用 Microsoft Visual C++ 編譯器)只構建 hello.obj 目標文件:
bash
C:\>scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fohello.obj /c hello.c /nologo
scons: done building targets.
2.3 簡單的 Java 構建
SCons 也使 Java 構建變得極其容易。但是,與 Program 和 Object 構建器方法不同,Java 構建器方法要求您指定一個目標目錄的名稱,您希望將類文件放在該目錄中,然后指定.java 文件所在的源目錄:
python
運行
Java('classes', 'src')
如果 src 目錄包含一個單一的 hello.java 文件,那么運行 scons 命令的輸出將如下所示(在 POSIX 系統上):
bash
% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
javac -d classes -sourcepath src src/hello.java
scons: done building targets.
我們將在第 26 章 "Java 構建" 中更詳細地介紹 Java 構建,包括構建 Java 歸檔文件(.jar)和其他類型的文件。
2.4 構建后清理
使用 SCons 時,無需添加特殊命令或目標名稱來在構建后進行清理。相反,您只需在調用 SCons 時使用 - c 或 --clean 選項,SCons 就會刪除相應的已構建文件。因此,如果我們構建上面的示例,然后之后調用 scons -c,POSIX 上的輸出如下所示:
bash
% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cc -o hello.o -c hello.c
cc -o hello hello.o
scons: done building targets.
% scons -c
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed hello.o
Removed hello
scons: done cleaning targets.
Windows 上的輸出如下所示:
bash
C:\>scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
scons: done building targets.
C:\>scons -c
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed hello.obj
Removed hello.exe
scons: done cleaning targets.
請注意,SCons 會更改其輸出,告訴您它正在 "Cleaning targets ..." 和 "done cleaning targets"。
2.5 SConstruct 文件
如果您習慣使用像 Make 這樣的構建系統,那么您已經知道 SConstruct 文件是 SCons 相當于 Makefile 的文件。也就是說,SConstruct 文件是 SCons 讀取以控制構建的輸入文件。
2.5.1 SConstruct 文件是 Python 腳本
然而,SConstruct 文件和 Makefile 之間有一個重要區別:SConstruct 文件實際上是一個 Python 腳本。如果您還不熟悉 Python,也不用擔心。本用戶指南將逐步向您介紹使用 SCons 有效所需了解的相對少量的 Python 知識。而且 Python 非常容易學習。
使用 Python 作為腳本語言的一個方面是,您可以使用 Python 的注釋約定在 SConstruct 文件中添加注釋;也就是說,'#' 和行尾之間的所有內容都將被忽略:
python
運行
# Arrange to build the "hello" program.
Program('hello.c') # "hello.c" is the source file.
在本指南的其余部分中,您將看到能夠使用真正腳本語言的強大功能可以大大簡化解決實際構建中復雜需求的方案。
2.5.2 SCons 函數與順序無關
SConstruct 文件與普通 Python 腳本不完全相同,而更像 Makefile 的一個重要方面是,在 SConstruct 文件中調用 SCons 函數的順序不會影響 SCons 實際構建您希望它構建的程序和目標文件的順序。換句話說,當您調用 Program 構建器(或任何其他構建器方法)時,您并不是告訴 SCons 在調用構建器方法的那一刻構建程序。相反,您是告訴 SCons 構建您想要的程序,例如,一個從名為 hello.c 的文件構建的程序,而何時構建該程序(以及任何其他文件)則由 SCons 決定。(我們將在下面的第 6 章 "依賴關系" 中了解更多關于 SCons 如何決定何時需要構建或重新構建文件的信息。)
SCons 通過打印狀態消息來反映調用像 Program 這樣的構建器方法與實際構建程序之間的區別,這些消息指示何時 "只是讀取" SConstruct 文件,以及何時實際構建目標文件。這是為了明確 SCons 何時執行構成 SConstruct 文件的 Python 語句,以及何時 SCons 實際執行命令或其他操作來構建必要的文件。
讓我們通過一個例子來澄清這一點。Python 有一個 print 語句,可以將一串字符打印到屏幕上。如果我們在調用 Program 構建器方法的周圍添加 print 語句:
python
運行
print("Calling Program('hello.c')")
Program('hello.c')
print("Calling Program('goodbye.c')")
Program('goodbye.c')
print("Finished calling Program()")
然后當我們執行 SCons 時,我們會在關于讀取 SConscript 文件的消息之間看到 print 語句的輸出,這表明此時正在執行 Python 語句:
bash
% scons
scons: Reading SConscript files ...
Calling Program('hello.c')
Calling Program('goodbye.c')
Finished calling Program()
scons: done reading SConscript files.
scons: Building targets ...
cc -o goodbye.o -c goodbye.c
cc -o goodbye goodbye.o
cc -o hello.o -c hello.c
cc -o hello hello.o
scons: done building targets.
還要注意,SCons 首先構建了 goodbye 程序,即使 "reading SConscript" 輸出顯示我們在 SConstruct 文件中首先調用了 Program ('hello.c')。
2.6 減少 SCons 輸出的冗長性
您已經看到 SCons 如何打印一些關于它正在做什么的消息,圍繞著用于構建軟件的實際命令:
bash
C:\>scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
scons: done building targets.
這些消息強調了 SCons 執行工作的順序:首先讀取并執行所有配置文件(通常稱為 SConscript 文件),然后才構建目標文件。這些消息的其他好處包括,有助于區分在讀取配置文件時發生的錯誤和在構建目標時發生的錯誤。
當然,一個缺點是這些消息會使輸出變得混亂。幸運的是,在調用 SCons 時使用 - Q 選項可以輕松禁用它們:
bash
C:\>scons -Q
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
由于我們希望本用戶指南專注于 SCons 實際在做什么,因此我們將使用 - Q 選項從本指南所有剩余示例的輸出中刪除這些消息。
第 3 章 構建中不太簡單的事情
在本章中,您將看到幾個使用 SCons 進行非常簡單的構建配置的示例,這些示例將展示使用 SCons 在不同類型的系統上從幾種不同的編程語言構建程序是多么容易。
3.1 指定目標(輸出)文件的名稱
您已經看到,當您調用 Program 構建器方法時,它會以與源文件相同的基名構建生成的程序。也就是說,以下調用從 hello.c 源文件構建可執行程序,將在 POSIX 系統上構建名為 hello 的可執行程序,在 Windows 系統上構建名為 hello.exe 的可執行程序:
python
運行
Program('hello.c')
如果您想構建一個名稱與源文件名的基名不同的程序,只需將目標文件名放在源文件名的左側:
python
運行
Program('new_hello', 'hello.c')
(SCons 要求目標文件名在前,源文件名在后,這樣順序就模仿了大多數編程語言(包括 Python)中的賦值語句:"program = source files"。)
現在,在 POSIX 系統上運行時,SCons 將構建一個名為 new_hello 的可執行程序:
bash
% scons -Q
cc -o hello.o -c hello.c
cc -o new_hello hello.o
在 Windows 系統上,SCons 將構建一個名為 new_hello.exe 的可執行程序:
bash
C:\>scons -Q
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:new_hello.exe hello.obj
embedManifestExeCheck(target, source, env)
3.2 編譯多個源文件
您剛剛看到了如何配置 SCons 從單個源文件編譯程序。當然,更常見的情況是,您需要從許多輸入源文件而不是一個源文件構建程序。為此,您需要將源文件放在 Python 列表中(用方括號括起來),如下所示:
python
運行
Program(['prog.c', 'file1.c', 'file2.c'])
上述示例的構建過程如下所示:
bash
% scons -Q
cc -o file1.o -c file1.c
cc -o file2.o -c file2.c
cc -o prog.o -c prog.c
cc -o prog prog.o file1.o file2.o
請注意,SCons 從列表中指定的第一個源文件推斷輸出程序名稱;也就是說,因為第一個源文件是 prog.c,SCons 將生成的程序命名為 prog(或在 Windows 系統上為 prog.exe)。如果您想指定不同的程序名稱,那么(如我們在上一節中所見)您將源文件列表向右滑動,為輸出程序文件名騰出空間。(SCons 將輸出文件名放在源文件名的左側,以便順序模仿賦值語句:"program = source files"。)這使我們的示例變為:
python
運行
Program('program', ['prog.c', 'file1.c', 'file2.c'])
在 Linux 上,此示例的構建過程如下所示:
bash
% scons -Q
cc -o file1.o -c file1.c
cc -o file2.o -c file2.c
cc -o prog.o -c prog.c
cc -o program prog.o file1.o file2.o
或在 Windows 上:
bash
C:\>scons -Q
cl /Fofile1.obj /c file1.c /nologo
cl /Fofile2.obj /c file2.c /nologo
cl /Foprog.obj /c prog.c /nologo
link /nologo /OUT:program.exe prog.obj file1.obj file2.obj
embedManifestExeCheck(target, source, env)
3.3 使用 Glob 創建文件列表
您還可以使用 Glob 函數查找所有匹配特定模板的文件,使用標準的 shell 模式匹配字符 *、? 和 [abc] 來匹配 a、b 或 c 中的任何一個。也支持 [!abc],用于匹配除 a、b 或 c 之外的任何字符。這使得許多多源文件構建變得相當容易:
python
運行
Program('program', Glob('*.c'))
SCons 手冊頁詳細介紹了如何在變體目錄(見下面的第 16 章 "變體構建")和存儲庫(見下面的第 22 章 "從代碼存儲庫構建")中使用 Glob,排除某些文件以及返回字符串而不是節點。
3.4 指定單個文件與文件列表
我們現在向您展示了兩種指定程序源的方法,一種是使用文件列表:
python
運行
Program('hello', ['file1.c', 'file2.c'])
另一種是使用單個文件:
python
運行
Program('hello', 'hello.c')
您實際上也可以將單個文件名放在列表中,出于一致性考慮,您可能更喜歡這樣做:
python
運行
Program('hello', ['hello.c'])
SCons 函數將接受任一形式的單個文件名。實際上,在內部,SCons 將所有輸入視為文件列表,但允許您省略方括號,以便在只有單個文件名時減少一些輸入。
重要提示
雖然 SCons 函數對于單個文件名是使用字符串還是列表比較寬容,但 Python 本身對于列表和字符串的處理更為嚴格。因此,在 SCons 允許使用字符串或列表的地方:
python
運行
# 以下兩個調用都能正確工作:
Program('program1', 'program1.c')
Program('program2', ['program2.c'])
嘗試執行混合字符串和列表的 "Python 操作" 會導致錯誤或產生不正確的結果:
python
運行
common_sources = ['file1.c', 'file2.c']# 以下代碼不正確,會產生Python錯誤
# 因為它試圖將字符串添加到列表中:
Program('program1', common_sources + 'program1.c')# 以下代碼正確工作,因為它將兩個列表相加形成另一個列表。
Program('program2', common_sources + ['program2.c'])
3.5 使文件列表更易讀
使用 Python 列表表示源文件的一個缺點是,每個文件名必須用引號(單引號或雙引號)括起來。當文件名列表很長時,這可能會變得繁瑣且難以閱讀。幸運的是,SCons 和 Python 提供了多種方法來確保 SConstruct 文件保持易讀性。
為了更輕松地處理長文件名列表,SCons 提供了一個 Split 函數,它接受一個用引號引起來的文件名列表,文件名之間用空格或其他空白字符分隔,并將其轉換為單獨的文件名列表。使用 Split 函數將前面的示例轉換為:
python
運行
Program('program', Split('main.c file1.c file2.c'))
(如果您已經熟悉 Python,您會意識到這類似于 Python 標準字符串模塊中的 split () 方法。然而,與字符串的 split () 成員函數不同,Split 函數不要求輸入為字符串,它會將單個非字符串對象包裝在列表中,或者如果參數已經是列表,則原封不動地返回。這在確保可以將任意值傳遞給 SCons 函數而無需手動檢查變量類型時非常有用。)
將對 Split 函數的調用放在 Program 調用內部也可能有點笨拙。一個更易讀的替代方法是將 Split 調用的輸出賦給一個變量名,然后在調用 Program 函數時使用該變量:
python
運行
src_files = Split('main.c file1.c file2.c')
Program('program', src_files)
最后,Split 函數不關心引號字符串中的文件名之間有多少空白字符。這允許您創建跨越多行的文件名列表,這通常使編輯更容易:
python
運行
src_files = Split("""main.cfile1.cfile2.c""")
Program('program', src_files)
(注意在這個示例中,我們使用了 Python 的 "三引號" 語法,它允許字符串包含多行。三個引號可以是單引號或雙引號。)
3.6 關鍵字參數
SCons 還允許您使用 Python 關鍵字參數來標識輸出文件和輸入源文件。輸出文件稱為 target,源文件(邏輯上)稱為 source。Python 語法如下:
python
運行
src_files = Split('main.c file1.c file2.c')
Program(target = 'program', source = src_files)
因為關鍵字明確標識了每個參數是什么,所以如果您愿意,實際上可以顛倒順序:
python
運行
src_files = Split('main.c file1.c file2.c')
Program(source = src_files, target = 'program')
您是否選擇使用關鍵字參數來標識目標文件和源文件,以及使用關鍵字時指定它們的順序,純粹是個人選擇;無論哪種方式,SCons 的功能都是相同的。
3.7 編譯多個程序
為了在同一個 SConstruct 文件中編譯多個程序,只需多次調用 Program 方法,為每個需要構建的程序調用一次:
python
運行
Program('foo.c')
Program('bar', ['bar1.c', 'bar2.c'])
然后 SCons 將按以下方式構建程序:
bash
% scons -Q
cc -o bar1.o -c bar1.c
cc -o bar2.o -c bar2.c
cc -o bar bar1.o bar2.o
cc -o foo.o -c foo.c
cc -o foo foo.o
請注意,SCons 不一定按照您在 SConstruct 文件中指定的順序構建程序。但是,SCons 確實認識到必須先構建各個目標文件,然后才能構建生成的程序。我們將在下面的 "依賴關系" 部分更詳細地討論這一點。
3.8 在多個程序之間共享源文件
通過在多個程序之間共享源文件來重用代碼是很常見的。一種方法是從公共源文件創建一個庫,然后可以將其鏈接到生成的程序中。(創建庫將在下面的第 4 章 "使用庫進行構建和鏈接" 中討論。)
在多個程序之間共享源文件的一種更直接但可能不太方便的方法是,簡單地將公共文件包含在每個程序的源文件列表中:
python
運行
Program(Split('foo.c common1.c common2.c'))
Program('bar', Split('bar1.c bar2.c common1.c common2.c'))
SCons 認識到 common1.c 和 common2.c 源文件的目標文件各自只需要構建一次,即使生成的目標文件各自都被鏈接到兩個生成的可執行程序中:
bash
% scons -Q
cc -o bar1.o -c bar1.c
cc -o bar2.o -c bar2.c
cc -o common1.o -c common1.c
cc -o common2.o -c common2.c
cc -o bar bar1.o bar2.o common1.o common2.o
cc -o foo.o -c foo.c
cc -o foo foo.o common1.o common2.o
如果兩個或多個程序共享許多公共源文件,那么當您需要更改公共文件列表時,在每個程序的列表中重復公共文件可能會成為一個維護問題。您可以通過創建一個單獨的 Python 列表來保存公共文件名,并使用 Python + 運算符將其與其他列表連接起來,從而簡化此操作:
python
運行
common = ['common1.c', 'common2.c']
foo_files = ['foo.c'] + common
bar_files = ['bar1.c', 'bar2.c'] + common
Program('foo', foo_files)
Program('bar', bar_files)
這在功能上等同于前面的示例。
3.9 調用構建器時覆蓋構建變量
在調用構建器方法時,可以通過傳遞額外的關鍵字參數來覆蓋或添加構建變量。這些被覆蓋或添加的變量僅在構建目標時有效,因此它們不會影響構建的其他部分。例如,如果您只想為一個程序添加額外的庫:
python
運行
env.Program('hello', 'hello.c', LIBS=['gl', 'glut'])
或者生成具有非標準后綴的共享庫:
python
運行
env.SharedLibrary('word', 'word.cpp',SHLIBSUFFIX='.ocx',LIBSUFFIXES=['.ocx'])
也可以在覆蓋中使用 parse_flags 關鍵字參數,將命令行風格的參數合并到適當的構建變量中(請參閱 env.MergeFlags)。
此示例將 'include' 添加到,將添加到CPPDEFINES,將 'm' 添加到 $LIBS。
python
運行
env = Program('hello', 'hello.c', parse_flags='-Iinclude -DEBUG -lm')
在調用構建器操作期間,環境不會被克隆,而是創建一個 OverrideEnvironment (),它比整個 Environment () 更輕量級。
第四章:使用庫進行構建和鏈接
將軟件的各個部分收集到一個或多個庫中,以此來組織大型軟件項目,這種做法往往很實用。SCons 能讓你輕松創建庫,并在程序里使用這些庫。
4.1 構建庫
要構建自己的庫,可使用Library
而非Program
來進行指定:
python
運行
Library('foo', ['f1.c', 'f2.c', 'f3.c'])
SCons 會依據你所用的系統,添加合適的庫前綴和后綴。所以,在 POSIX 或者 Linux 系統上,上述示例的構建過程如下(盡管并非所有系統都會調用 ranlib):
bash
% scons -Q
cc -o f1.o -c f1.c
cc -o f2.o -c f2.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o
ranlib libfoo.a
在 Windows 系統上,構建上述示例的情況如下:
bash
C:\>scons -Q
cl /Fof1.obj /c f1.c /nologo
cl /Fof2.obj /c f2.c /nologo
cl /Fof3.obj /c f3.c /nologo
lib /nologo /OUT:foo.lib f1.obj f2.obj f3.obj
庫的目標名稱規則和程序的類似:要是你沒有明確指定目標庫的名稱,SCons 會從所指定的第一個源文件的名稱推導出一個名稱,而且如果你沒有添加文件前綴和后綴,SCons 會添加上合適的。
4.1.1 從源代碼或目標文件構建庫
前面的示例展示了如何從源文件列表構建庫。不過,你也能在調用Library
時傳入目標文件,它會正確識別出這些是目標文件。實際上,你可以在源列表中隨意混合源代碼文件和目標文件:
python
運行
Library('foo', ['f1.c', 'f2.o', 'f3.c', 'f4.o'])
SCons 會明白,在創建最終庫之前,只需把源代碼文件編譯成目標文件:
bash
% scons -Q
cc -o f1.o -c f1.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o f4.o
ranlib libfoo.a
當然,在這個示例中,要使構建成功,目標文件必須已經存在。關于如何顯式構建目標文件并將其包含到庫中,可參考下面的第五章 “節點對象”。
4.1.2 顯式構建靜態庫:StaticLibrary 構建器
Library
函數會構建傳統的靜態庫。要是你想明確指定所構建庫的類型,可以使用StaticLibrary
函數來替代Library
:
python
運行
StaticLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])
StaticLibrary
函數和Library
函數在功能上并無差異。
4.1.3 構建共享(DLL)庫:SharedLibrary 構建器
如果你想構建共享庫(在 POSIX 系統上)或者 DLL 文件(在 Windows 系統上),可以使用SharedLibrary
函數:
python
運行
SharedLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])
在 POSIX 系統上的輸出:
bash
% scons -Q
cc -o f1.os -c f1.c
cc -o f2.os -c f2.c
cc -o f3.os -c f3.c
cc -o libfoo.so -shared f1.os f2.os f3.os
在 Windows 系統上的輸出:
bash
C:\>scons -Q
cl /Fof1.obj /c f1.c /nologo
cl /Fof2.obj /c f2.c /nologo
cl /Fof3.obj /c f3.c /nologo
link /nologo /dll /out:foo.dll /implib:foo.lib f1.obj f2.obj f3.obj
RegServerFunc(target, source, env)
embedManifestDllCheck(target, source, env)
再次注意,SCons 會正確處理輸出文件的構建,在 POSIX 編譯時添加-shared
選項,在 Windows 上添加/dll
選項。
4.2 與庫鏈接
通常,你構建庫是為了將其與一個或多個程序進行鏈接。要將庫與程序鏈接,可在$LIBS
構造變量中指定庫,在$LIBPATH
構造變量中指定查找庫的目錄:
python
運行
Library('foo', ['f1.c', 'f2.c', 'f3.c'])
Program('prog.c', LIBS=['foo', 'bar'], LIBPATH='.')
當然要注意,你無需指定庫的前綴(如lib
)或后綴(如.a
或.lib
)。SCons 會為當前系統使用正確的前綴或后綴。
在 POSIX 或 Linux 系統上,構建上述示例的情況如下:
bash
% scons -Q
cc -o f1.o -c f1.c
cc -o f2.o -c f2.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o
ranlib libfoo.a
cc -o prog.o -c prog.c
cc -o prog prog.o -L. -lfoo -lbar
在 Windows 系統上,構建上述示例的情況如下:
bash
C:\>scons -Q
cl /Fof1.obj /c f1.c /nologo
cl /Fof2.obj /c f2.c /nologo
cl /Fof3.obj /c f3.c /nologo
lib /nologo /OUT:foo.lib f1.obj f2.obj f3.obj
cl /Foprog.obj /c prog.c /nologo
link /nologo /OUT:prog.exe /LIBPATH:. foo.lib bar.lib prog.obj
embedManifestExeCheck(target, source, env)
和往常一樣,要注意 SCons 會處理好每個系統上與指定庫鏈接所需的正確命令行構建。
還要注意,如果只需鏈接單個庫,你可以用單個字符串而非 Python 列表來指定庫名,所以:
python
運行
Program('prog.c', LIBS='foo', LIBPATH='.')
這等同于:
python
運行
Program('prog.c', LIBS=['foo'], LIBPATH='.')
這和 SCons 處理用字符串或列表來指定單個源文件的方式類似。
4.3 查找庫:$LIBPATH 構造變量
默認情況下,鏈接器只會在系統定義的特定目錄中查找庫。SCons 知道如何在你通過$LIBPATH
構造變量指定的目錄中查找庫。$LIBPATH
由目錄名列表組成,如下所示:
python
運行
Program('prog.c', LIBS = 'm',LIBPATH = ['/usr/lib', '/usr/local/lib'])
建議使用 Python 列表,因為這樣在各系統間具有可移植性。或者,你也可以把所有目錄名放在一個字符串中,用系統特定的路徑分隔符分隔:在 POSIX 系統上用冒號:
python
運行
LIBPATH = '/usr/lib:/usr/local/lib'
在 Windows 系統上用分號:
python
運行
LIBPATH = 'C:\\lib;D:\\lib'
(注意,在 Windows 路徑名中,Python 要求反斜杠分隔符在字符串中進行轉義。)
執行鏈接器時,SCons 會創建合適的標志,讓鏈接器在與 SCons 相同的目錄中查找庫。所以在 POSIX 或 Linux 系統上,構建上述示例的情況如下:
bash
% scons -Q
cc -o prog.o -c prog.c
cc -o prog prog.o -L/usr/lib -L/usr/local/lib -lm
在 Windows 系統上,構建上述示例的情況如下:
bash
C:\>scons -Q
cl /Foprog.obj /c prog.c /nologo
link /nologo /OUT:prog.exe /LIBPATH:\usr\lib /LIBPATH:\usr\local\lib m.lib prog.obj
embedManifestExeCheck(target, source, env)
再次注意,SCons 會處理好創建正確命令行選項的特定系統細節。
第五章:節點對象
在內部,SCons 把它所知道的所有文件和目錄都表示為節點。這些內部對象(并非目標文件)可通過多種方式使用,從而讓你的 SConscript 文件具備可移植性且易于閱讀。
5.1 構建器方法返回目標節點列表
所有構建器方法都會返回一個節點對象列表,這些節點對象標識了將要構建的目標文件。可以將這些返回的節點作為參數傳遞給其他構建器方法。
例如,假設我們想用不同的選項來構建組成一個程序的兩個目標文件。這意味著要為每個目標文件分別調用一次Object
構建器,并指定所需的選項:
python
運行
Object('hello.c', CCFLAGS='-DHELLO')
Object('goodbye.c', CCFLAGS='-DGOODBYE')
將這些目標文件組合成最終程序的一種方法是,在調用Program
構建器時將目標文件的名稱列為源文件:
python
運行
Object('hello.c', CCFLAGS='-DHELLO')
Object('goodbye.c', CCFLAGS='-DGOODBYE')
Program(['hello.o', 'goodbye.o'])
用字符串指定名稱存在一個問題,那就是我們的 SConstruct 文件在不同操作系統之間不再具有可移植性。例如,它在 Windows 上無法工作,因為 Windows 上的目標文件名為hello.obj
和goodbye.obj
,而不是hello.o
和goodbye.o
。
更好的解決辦法是,把調用Object
構建器返回的目標列表賦值給變量,然后在調用Program
構建器時將這些變量連接起來:
python
運行
hello_list = Object('hello.c', CCFLAGS='-DHELLO')
goodbye_list = Object('goodbye.c', CCFLAGS='-DGOODBYE')
Program(hello_list + goodbye_list)
這讓我們的 SConstruct 文件再次具備了可移植性,在 Linux 上的構建輸出如下:
bash
% scons -Q
cc -o goodbye.o -c -DGOODBYE goodbye.c
cc -o hello.o -c -DHELLO hello.c
cc -o hello hello.o goodbye.o
在 Windows 上:
bash
C:\>scons -Q
cl /Fogoodbye.obj /c goodbye.c -DGOODBYE
cl /Fohello.obj /c hello.c -DHELLO
link /nologo /OUT:hello.exe hello.obj goodbye.obj
embedManifestExeCheck(target, source, env)
在本指南的后續內容中,我們會看到使用構建器方法返回的節點列表的示例。
5.2 顯式創建文件和目錄節點
值得一提的是,SCons 對表示文件的節點和表示目錄的節點做了明確區分。SCons 支持File
和Dir
函數,它們分別返回文件或目錄節點:
python
運行
hello_c = File('hello.c')
Program(hello_c)classes = Dir('classes')
Java(classes, 'src')
通常,你無需直接調用File
或Dir
,因為調用構建器方法時會自動將字符串視為文件或目錄的名稱,并為你將其轉換為節點對象。在需要明確告知 SCons 傳遞給構建器或其他函數的節點類型,或者明確引用目錄樹中的特定文件時,File
和Dir
函數就會派上用場。
有時,你可能需要引用文件系統中的某個條目,但事先并不清楚它是文件還是目錄。針對這種情況,SCons 還支持Entry
函數,該函數返回的節點既可以表示文件,也可以表示目錄。
python
運行
xyzzy = Entry('xyzzy')
返回的xyzzy
節點在首次被構建器方法或其他需要文件節點或目錄節點的函數使用時,會被轉換為文件節點或目錄節點。
5.3 打印節點文件名
使用節點最常見的操作之一是用它來打印該節點所代表的文件名。不過要記住,由于調用構建器返回的對象是一個節點列表,所以你必須使用 Python 下標從列表中獲取單個節點。例如,下面的 SConstruct 文件:
python
運行
object_list = Object('hello.c')
program_list = Program(object_list)
print("The object file is: %s"%object_list[0])
print("The program file is: %s"%program_list[0])
在 POSIX 系統上會打印出以下文件名:
bash
% scons -Q
The object file is: hello.o
The program file is: hello
cc -o hello.o -c hello.c
cc -o hello hello.o
在 Windows 系統上會打印出以下文件名:
bash
C:\>scons -Q
The object file is: hello.obj
The program file is: hello.exe
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
注意,在上述示例中,object_list[0]
從列表中提取出一個實際的節點對象,Python 的print
語句會將該對象轉換為字符串進行打印。
5.4 將節點的文件名用作字符串
上一節中所描述的打印節點名稱之所以可行,是因為節點對象的字符串表示形式就是文件名。如果你想對文件名進行打印以外的操作,可以使用 Python 的內置函數str
來獲取它。例如,如果你想在讀取和執行 SConstruct 文件時,使用 Python 的os.path.exists
來判斷某個文件是否存在,可以按如下方式獲取字符串:
python
運行
import os.path
program_list = Program('hello.c')
program_name = str(program_list[0])
if not os.path.exists(program_name):print("%s does not exist!"%program_name)
在 POSIX 系統上,這段代碼的執行情況如下:
bash
% scons -Q
hello does not exist!
cc -o hello.o -c hello.c
cc -o hello hello.o
5.5 GetBuildPath:從節點或字符串獲取路徑
env.GetBuildPath(file_or_list)
會返回節點或表示路徑的字符串的路徑。它也可以接受節點和 / 或字符串的列表,并返回路徑列表。如果傳入單個節點,結果與調用str(node)
相同(見上文)。字符串中可以嵌入構造變量,這些變量會像往常一樣使用調用環境的變量集進行展開。路徑可以是文件或目錄,并且不一定需要存在。
python
運行
env=Environment(VAR="value")
n=File("foo.c")
print(env.GetBuildPath([n, "sub/dir/$VAR"]))
這將打印以下文件名:
bash
% scons -Q
['foo.c', 'sub/dir/value']
scons: `.' is up to date.
還有一個函數版本的GetBuildPath
,可以在不使用環境的情況下調用;它使用默認的 SCons 環境對任何字符串參數進行替換。
第六章:依賴關系
到目前為止,我們已經了解了 SCons 如何處理一次性構建。但像 SCons 這樣的構建工具的一個主要功能是,當源文件發生變化時,只重新構建必要的部分 —— 換句話說,SCons 不應浪費時間去重建不需要重建的東西。只需在構建完我們簡單的 hello 示例后再次調用 SCons,你就能看到這一點在起作用:
bash
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
scons: `.' is up to date.
第二次執行時,SCons 會意識到 hello 程序相對于當前的 hello.c 源文件是最新的,從而避免重新構建它。在命令行上明確指定 hello 程序的名稱,你可以更清楚地看到這一點:
bash
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
注意,SCons 只對命令行上明確命名的目標文件報告 “...is up to date”,以避免輸出混亂。
6.1 確定輸入文件何時發生變化:Decider 函數
避免不必要重建的另一個方面是構建工具的基本行為,即當輸入文件發生變化時重建相關內容,以使構建的軟件保持最新狀態。默認情況下,SCons 通過計算每個文件內容的 MD5 簽名或校驗和來跟蹤這一點,不過你也可以輕松配置 SCons 改為使用修改時間(或時間戳)。你甚至可以指定自己的 Python 函數來判斷輸入文件是否發生了變化。
6.1.1 使用 MD5 簽名確定文件是否發生變化
默認情況下,SCons 根據文件內容的 MD5 校驗和而不是文件的修改時間來跟蹤文件是否發生了變化。這意味著,如果你習慣了 Make 通過更新文件的修改時間(例如使用 touch 命令)來強制重建的慣例,可能會對 SCons 的默認行為感到驚訝:
bash
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% touch hello.c
% scons -Q hello
scons: `hello' is up to date.
即使文件的修改時間發生了變化,SCons 也會意識到 hello.c 文件的內容沒有變化,因此不需要重建 hello 程序。這避免了不必要的重建,例如,當有人重寫了文件內容但沒有實際更改時。但是,如果文件的內容確實發生了變化,SCons 會檢測到變化并根據需要重建程序:
bash
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% [CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
注意,如果你愿意,可以使用 Decider 函數顯式指定這種默認行為(MD5 簽名):
python
運行
Program('hello.c')
Decider('MD5')
調用 Decider 函數時,你也可以使用字符串 'content' 作為 'MD5' 的同義詞。
6.1.1.1 使用 MD5 簽名的影響
使用 MD5 簽名來確定輸入文件是否發生變化有一個意想不到的好處:如果源文件的更改方式使得重建后的目標文件內容與上次構建時完全相同,那么依賴于這個重建但未更改的目標文件的任何 "下游" 目標文件實際上不需要重建。
因此,如果用戶只更改了 hello.c 文件中的注釋,那么重建的 hello.o 文件將與之前構建的完全相同(假設編譯器不會在目標文件中放入任何特定于構建的信息)。SCons 會意識到不需要重建 hello 程序:
bash
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% [CHANGE A COMMENT IN hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
scons: `hello' is up to date.
本質上,當 SCons 意識到目標文件重建后的內容與上次構建完全相同時,它會 "短路" 任何依賴構建。這確實需要一些額外的處理時間來讀取目標 (hello.o) 文件的內容,但在避免的重建過程非常耗時的情況下,通常可以節省時間。
6.1.2 使用時間戳確定文件是否發生變化
如果你愿意,你可以配置 SCons 在決定是否需要重建目標時使用文件的修改時間而不是文件內容。SCons 提供了兩種方法來使用時間戳確定輸入文件自上次構建目標以來是否發生了變化。
最常見的使用時間戳的方法與 Make 相同:即,如果源文件的修改時間比目標文件新,SCons 就會決定必須重建目標。為此,可以如下調用 Decider 函數:
python
運行
Object('hello.c')
Decider('timestamp-newer')
這使得 SCons 在更新文件的修改時間(例如使用 touch 命令)時表現得像 Make:
bash
% scons -Q hello.o
cc -o hello.o -c hello.c
% touch hello.c
% scons -Q hello.o
cc -o hello.o -c hello.c
實際上,由于這種行為與 Make 的行為相同,因此在調用 Decider 函數時,你也可以使用字符串 'make' 作為 'timestamp-newer' 的同義詞:
python
運行
Object('hello.c')
Decider('make')
完全像 Make 一樣使用時間戳的一個缺點是,如果輸入文件的修改時間突然變得比目標文件舊,目標文件將不會被重建。例如,如果從備份存檔中恢復了源文件的舊副本,就可能發生這種情況。恢復的文件內容可能與上次構建依賴目標時不同,但由于源文件的修改時間不比目標新,目標不會被重建。
由于 SCons 實際上在構建目標時存儲了有關源文件時間戳的信息,因此它可以通過檢查源文件時間戳的精確匹配來處理這種情況,而不僅僅是檢查源文件是否比目標文件新。為此,在調用 Decider 函數時指定參數 'timestamp-match':
python
運行
Object('hello.c')
Decider('timestamp-match')
以這種方式配置后,只要源文件的修改時間發生變化,SCons 就會重建目標。因此,如果我們使用 touch -t 選項將 hello.c 的修改時間更改為舊日期(1989 年 1 月 1 日),SCons 仍會重建目標文件:
bash
% scons -Q hello.o
cc -o hello.o -c hello.c
% touch -t 198901010000 hello.c
% scons -Q hello.o
cc -o hello.o -c hello.c
一般來說,選擇 timestamp-newer 而不是 timestamp-match 的唯一原因是,如果你有特定的理由需要這種類似 Make 的行為,即在修改后的源文件比目標舊時不重建目標。
6.1.3 使用 MD 簽名和時間戳確定文件是否發生變化
作為一種性能增強,SCons 提供了一種使用文件內容的 MD5 校驗和的方法,但僅在文件的時間戳發生變化時才讀取這些內容。為此,使用 'MD5-timestamp' 參數調用 Decider 函數:
python
運行
Program('hello.c')
Decider('MD5-timestamp')
如此配置后,SCons 的行為仍將與使用 Decider ('MD5') 時相同:
bash
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% touch hello.c
% scons -Q hello
scons: `hello' is up to date.
% edit hello.c[CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
然而,在上述輸出中,當構建是最新的時,第二次調用 SCons 只需查看 hello.c 文件的修改時間,而不必打開它并對其內容進行 MD5 校驗和計算。這可以顯著加快許多最新構建的速度。
使用 Decider ('MD5-timestamp') 的唯一缺點是,如果源文件在上次 SCons 構建文件后的一秒內被修改,SCons 將不會重建目標文件。在大多數開發者編程時,這實際上不是問題,因為不太可能有人在構建后一秒內快速思考并對源文件進行實質性更改。然而,某些構建腳本或持續集成工具可能依賴于自動應用文件更改然后盡快重建的能力,在這種情況下,使用 Decider ('MD5-timestamp') 可能不合適。
6.1.4 編寫自己的自定義 Decider 函數
我們傳遞給 Decider 函數的不同字符串值,本質上是 SCons 用來選擇幾個特定內部函數之一的,這些內部函數實現了各種方法來判斷自目標文件構建以來依賴項(通常是源文件)是否發生了變化。事實證明,你也可以提供自己的函數來判斷依賴項是否發生了變化。
例如,假設我們有一個輸入文件,其中包含大量特定格式的數據,用于重建許多不同的目標文件,但每個目標文件實際上只依賴于輸入文件的一個特定部分。我們希望每個目標文件只依賴于輸入文件的該部分。然而,由于輸入文件可能包含大量數據,我們希望僅在其時間戳發生變化時才打開輸入文件。這可以通過一個自定義 Decider 函數來實現,該函數可能如下所示:
python
運行
Program('hello.c')
def decide_if_changed(dependency, target, prev_ni, repo_node=None):if dependency.get_timestamp() != prev_ni.timestamp:dep = str(dependency)tgt = str(target)if specific_part_of_file_has_changed(dep, tgt):return Truereturn False
Decider(decide_if_changed)
注意,在函數定義中,依賴項(輸入文件)是第一個參數,然后是目標。這兩個參數都作為 SCons Node 對象傳遞給函數,我們使用 Python 的 str () 將它們轉換為字符串。
第三個參數 prev_ni 是一個對象,它保存了上次構建目標時記錄的關于依賴項的簽名或時間戳信息。prev_ni 對象可以保存不同的信息,具體取決于 dependency 參數所代表的內容類型。對于普通文件,prev_ni 對象具有以下屬性:
第四個參數 repo_node 是在比較 BuildInfo 時如果不為 None 則使用的 Node。這通常僅在目標節點僅存在于存儲庫中時設置。
- .csig:上次構建目標時依賴文件內容的內容簽名或 MD5 校驗和。
- .size:上次構建目標時依賴文件的大小(以字節為單位)。
- .timestamp:上次構建目標時依賴文件的修改時間。
注意,在自定義 Decider 函數中忽略某些參數是完全正常的,如果它們不影響你判斷依賴文件是否發生變化的方式。
另一件需要注意的事情是,上述三個屬性在首次運行時可能不存在。如果沒有先前的構建,則沒有創建任何目標,并且還不存在.sconsign DB 文件。因此,你應該始終檢查所討論的 prev_ni 屬性是否可用。
我們最后給出一個基于 csig 的 decider 函數的小例子。注意每次函數調用時如何通過 get_csig 初始化依賴文件的簽名信息(這是必需的!)。
python
運行
env = Environment()def config_file_decider(dependency, target, prev_ni, repo_node=None):import os.path# 我們總是必須初始化.csig值...dep_csig = dependency.get_csig()# .csig可能不存在,因為還沒有構建目標...if 'csig' not in dir(prev_ni):return True# 目標文件可能還不存在if not os.path.exists(str(target.abspath)):return Trueif dep_csig != prev_ni.csig:# 源文件有一些更改 => 更新已安裝的文件return Truereturn Falsedef update_file():f = open("test.txt","a")f.write("some line\n")f.close()update_file()# 激活我們自己的decider函數
env.Decider(config_file_decider)env.Install("install","test.txt")
6.1.5 混合使用不同的方法確定文件是否發生變化
前面的例子都展示了調用全局 Decider 函數來配置 SCons 做出的所有依賴決策。然而,有時你希望能夠為不同的目標配置不同的決策方法。當需要這樣做時,你可以使用 env.Decider 方法只影響使用特定構建環境構建的目標的配置決策。
例如,如果我們想從同一個源使用 MD5 校驗和構建一個程序,使用文件修改時間構建另一個程序,我們可以這樣配置:
python
運行
env1 = Environment(CPPPATH = ['.'])
env2 = env1.Clone()
env2.Decider('timestamp-match')
env1.Program('prog-MD5', 'program1.c')
env2.Program('prog-timestamp', 'program2.c')
如果兩個程序都包含同一個 inc.h 文件,那么更新 inc.h 的修改時間(使用 touch 命令)將只導致 prog-timestamp 被重建:
bash
% scons -Q
cc -o program1.o -c -I. program1.c
cc -o prog-MD5 program1.o
cc -o program2.o -c -I. program2.c
cc -o prog-timestamp program2.o
% touch inc.h
% scons -Q
cc -o program2.o -c -I. program2.c
cc -o prog-timestamp program2.o
6.2 確定輸入文件何時發生變化的舊函數
SCons 仍然支持兩個曾經是配置判斷輸入文件是否發生變化的主要方法的函數。這些函數在 SCons 2.0 版本中已被正式棄用,不建議使用,主要是因為它們依賴于源文件和目標文件處理方式之間有些令人困惑的區別。這里記錄這些函數主要是以防你在較舊的 SConscript 文件中遇到它們。
6.3 隱式依賴:$CPPPATH 構造變量
現在假設我們的 "Hello, World!" 程序實際上有一個 #include 行,在編譯時包含 hello.h 文件:
c
#include <hello.h>
int
main()
{printf("Hello, %s!\n", string);
}
為了完整起見,hello.h 文件如下所示:
c
#define string "world"
在這種情況下,我們希望 SCons 認識到,如果 hello.h 文件的內容發生變化,hello 程序必須重新編譯。為此,我們需要像這樣修改 SConstruct 文件:
python
運行
Program('hello.c', CPPPATH = '.')
$CPPPATH 值告訴 SCons 在當前目錄('.')中查找 C 源文件(.c 或.h 文件)包含的任何文件。在 SConstruct 文件中進行此賦值后:
bash
% scons -Q hello
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% [CHANGE THE CONTENTS OF hello.h]
% scons -Q hello
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
首先,注意 SCons 從 $CPPPATH 變量添加了 - I. 參數,以便編譯能夠在本地目錄中找到 hello.h 文件。
其次,要意識到 SCons 知道必須重建 hello 程序,因為它掃描 hello.c 文件的內容,查找指示在編譯中包含另一個文件的 #include 行。SCons 將這些記錄為目標文件的隱式依賴項。因此,當 hello.h 文件發生變化時,SCons 意識到 hello.c 文件包含它,并重建依賴于 hello.c 和 hello.h 文件的最終 hello 程序。
與變量一樣,CPPPATH 變量可以是目錄列表,也可以是由系統特定路徑分隔字符分隔的字符串(POSIX/Linux 上為 ':',Windows 上為 ';')。無論哪種方式,SCons 都會創建正確的命令行選項,因此以下示例:
python
運行
Program('hello.c', CPPPATH = ['include', '/home/project/inc'])
在 POSIX 或 Linux 上看起來像這樣:
bash
% scons -Q hello
cc -o hello.o -c -Iinclude -I/home/project/inc hello.c
cc -o hello hello.o
在 Windows 上看起來像這樣:
bash
C:\>scons -Q hello.exe
cl /Fohello.obj /c hello.c /nologo /Iinclude /I\home\project\inc
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
6.4 緩存隱式依賴
掃描每個文件的 #include 行確實需要一些額外的處理時間。當你對大型系統進行完整構建時,掃描時間通常只占構建總時間的很小一部分。然而,當你重建大型系統的全部或部分時,你最有可能注意到掃描時間:SCons 可能會花費一些額外的時間來 "思考" 必須構建什么,然后才發出第一個構建命令(或決定一切都是最新的,無需重建)。
實際上,讓 SCons 掃描文件相對于追蹤因不正確依賴而引入的細微問題所可能損失的時間來說是節省時間的。盡管如此,SCons 掃描文件時的 "等待時間" 可能會讓等待構建完成的單個開發者感到煩惱。因此,SCons 允許你緩存其掃描器找到的隱式依賴項,以供后續構建使用。你可以通過在命令行上指定 --implicit-cache 選項來做到這一點:
bash
% scons -Q --implicit-cache hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
如果你不想每次都在命令行上指定 --implicit-cache,可以通過在 SConscript 文件中設置 implicit_cache 選項,使其成為你構建的默認行為:
python
運行
SetOption('implicit_cache', 1)
SCons 默認不會像這樣緩存隱式依賴項,因為 --implicit-cache 會導致 SCons 簡單地使用上次運行時存儲的隱式依賴項,而不檢查這些依賴項是否仍然正確。具體來說,這意味著 --implicit-cache 會指示 SCons 在以下情況下不能 "正確" 重建:
- 當使用 --implicit-cache 時,SCons 將忽略對搜索路徑(如或LIBPATH)可能做的任何更改。如果 $CPPPATH 的更改通常會導致使用來自不同目錄的同名不同文件,這可能會導致 SCons 不重建文件。
- 當使用 --implicit-cache 時,如果在搜索路徑中比上次找到文件的目錄更早的目錄中添加了同名文件,SCons 將不會檢測到。
6.4.1 --implicit-deps-changed 選項
在使用緩存的隱式依賴項時,有時你想 "重新開始",讓 SCons 重新掃描它之前緩存了依賴項的文件。例如,如果你最近安裝了用于編譯的新版本外部代碼,外部頭文件將發生變化,之前緩存的隱式依賴項將過時。你可以通過使用 --implicit-deps-changed 選項運行 SCons 來更新它們:
bash
% scons -Q --implicit-deps-changed hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
在這種情況下,SCons 將重新掃描所有隱式依賴項,并緩存更新后的信息副本。
6.4.2 --implicit-deps-unchanged 選項
默認情況下,在緩存依賴項時,SCons 會注意到文件何時被修改,并重新掃描文件以獲取任何更新的隱式依賴項信息。然而,有時你可能想強制 SCons 使用緩存的隱式依賴項,即使源文件發生了變化。例如,當你更改了源文件但知道你沒有更改任何 #include 行時,這可以加快構建速度。在這種情況下,你可以使用 --implicit-deps-unchanged 選項:
bash
% scons -Q --implicit-deps-unchanged hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
在這種情況下,SCons 將假設緩存的隱式依賴項是正確的,并且不會費心重新掃描已更改的文件。對于源文件進行小的增量更改后的典型構建,節省的時間可能不多,但有時每一點性能改進都很重要。
6.5 顯式依賴:Depends 函數
有時,一個文件依賴于另一個文件,而這個依賴關系未被 SCons 掃描器檢測到。對于這種情況,SCons 允許你顯式指定一個文件依賴于另一個文件,并且當被依賴的文件發生變化時,依賴它的文件必須重新構建。這是通過 Depends 方法來指定的:
python
運行
hello = Program('hello.c')
Depends(hello, 'other_file')
plaintext
% scons -Q hello
cc -c hello.c -o hello.o
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% edit other_file[CHANGE THE CONTENTS OF other_file]
% scons -Q hello
cc -c hello.c -o hello.o
cc -o hello hello.o
請注意,依賴項(Depends 的第二個參數)也可以是一個節點對象列表(例如,由調用構建器返回的列表):
python
運行
hello = Program('hello.c')
goodbye = Program('goodbye.c')
Depends(hello, goodbye)
在這種情況下,依賴項將在目標之前構建:
plaintext
% scons -Q hello
cc -c goodbye.c -o goodbye.o
cc -o goodbye goodbye.o
cc -c hello.c -o hello.o
cc -o hello hello.o
6.6 來自外部文件的依賴:ParseDepends 函數
SCons 對許多語言都有內置的掃描器。有時,由于掃描器實現的限制,這些掃描器無法提取某些隱式依賴項。
下面的示例說明了一個內置的 C 掃描器無法提取對頭文件的隱式依賴的情況。
c
#define FOO_HEADER <foo.h>
#include FOO_HEADERint main() {return FOO;
}
plaintext
% scons -Q
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
% [CHANGE CONTENTS OF foo.h]
% scons -Q
scons: `.' is up to date.
顯然,掃描器不知道頭文件的依賴關系。由于不是一個功能齊全的 C 預處理器,掃描器不會展開宏。
在這些情況下,你也可以使用編譯器來提取隱式依賴項。ParseDepends 可以解析編譯器輸出格式的內容,并顯式建立所有列出的依賴關系。
下面的示例使用 ParseDepends 來處理編譯器生成的依賴文件,該文件是在編譯目標文件時作為副作用生成的:
python
運行
obj = Object('hello.c', CCFLAGS='-MD -MF hello.d', CPPPATH='.')
SideEffect('hello.d', obj)
ParseDepends('hello.d')
Program('hello', obj)
plaintext
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
% [CHANGE CONTENTS OF foo.h]
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
從編譯器生成的.d 文件解析依賴項存在一個 “先有雞還是先有蛋” 的問題,這會導致不必要的重新構建:
plaintext
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
% scons -Q --debug=explain
scons: rebuilding `hello.o' because `foo.h' is a new dependency
cc -o hello.o -c -MD -MF hello.d -I. hello.c
% scons -Q
scons: `.' is up to date.
在第一次通過時,在編譯目標文件時生成依賴文件。此時,SCons 不知道對 foo.h 的依賴。在第二次通過時,由于檢測到 foo.h 作為新的依賴項,目標文件會重新生成。
ParseDepends 會在調用時立即讀取指定的文件,如果文件不存在則直接返回。在構建過程中生成的依賴文件不會自動再次解析。因此,在同一構建過程中,編譯器提取的依賴項不會存儲在簽名數據庫中。ParseDepends 的這個限制會導致不必要的重新編譯。因此,只有在沒有針對所使用語言的掃描器,或者掃描器對于特定任務不夠強大時,才應使用 ParseDepends。
6.7 忽略依賴:Ignore 函數
有時,即使依賴文件發生變化,也有理由不重新構建程序。在這種情況下,你可以明確告訴 SCons 忽略某個依賴,如下所示:
python
運行
hello_obj=Object('hello.c')
hello = Program(hello_obj)
Ignore(hello_obj, 'hello.h')
plaintext
% scons -Q hello
cc -c -o hello.o hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% edit hello.h[CHANGE THE CONTENTS OF hello.h]
% scons -Q hello
scons: `hello' is up to date.
現在,上面的示例有點人為設計的感覺,因為很難想象在實際情況中,如果 hello.h 文件發生變化,你卻不想重新構建 hello 程序。一個更現實的例子可能是,如果 hello 程序在一個由多個系統共享的目錄中構建,而這些系統的 stdio.h 包含文件的副本不同。在這種情況下,SCons 會注意到不同系統的 stdio.h 副本之間的差異,并且每次你切換系統時都會重新構建 hello 程序。你可以通過以下方式避免這些重新構建:
python
運行
hello = Program('hello.c', CPPPATH=['/usr/include'])
Ignore(hello, '/usr/include/stdio.h')
Ignore 還可用于防止默認情況下構建生成的文件。這是因為目錄依賴于其內容。因此,要從默認構建中忽略生成的文件,你可以指定目錄應忽略該生成的文件。請注意,如果用戶在 scons 命令行中明確請求目標,或者該文件是另一個被請求和 / 或默認構建的文件的依賴項,則該文件仍將被構建。
python
運行
hello_obj=Object('hello.c')
hello = Program(hello_obj)
Ignore('.',[hello,hello_obj])
plaintext
% scons -Q
scons: `.' is up to date.
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
6.8 僅順序依賴:Requires 函數
偶爾,指定某個文件或目錄在必要時必須在其他目標構建之前構建或創建,但該文件或目錄的更改不需要重新構建目標,這可能會很有用。這種關系被稱為僅順序依賴,因為它只影響構建的順序 —— 目標之前的依賴項 —— 但它不是嚴格的依賴關系,因為目標不應該因依賴文件的更改而改變。
例如,假設你想每次運行構建時創建一個文件,該文件標識構建的時間、版本號等,并且該文件包含在你構建的每個程序中。版本文件的內容每次構建都會改變。如果你指定一個正常的依賴關系,那么依賴該文件的每個程序每次運行 SCons 時都會重新構建。例如,我們可以在 SConstruct 文件中使用一些 Python 代碼,每次運行 SCons 時創建一個新的 version.c 文件,其中包含一個包含當前日期的字符串,然后通過在源文件中列出 version.c 來將程序與生成的目標文件鏈接:
python
運行
import timeversion_c_text = """
char *date = "%s";
""" % time.ctime(time.time())
open('version.c', 'w').write(version_c_text)hello = Program(['hello.c','version.c'])
如果我們將 version.c 列為實際的源文件,那么每次運行 SCons 時,version.o 文件都會重新構建(因為 SConstruct 文件本身會更改 version.c 的內容),并且 hello 可執行文件每次都會重新鏈接(因為 version.o 文件會更改):
plaintext
% scons -Q hello
cc -o hello.o -c hello.c
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
(請注意,為了使上述示例正常工作,我們在每次運行之間暫停一秒鐘,以便 SConstruct 文件創建的 version.c 文件中的時間字符串比上一次運行晚一秒。)
一種解決方案是使用 Requires 函數指定 version.o 必須在鏈接步驟使用它之前重新構建,但 version.o 的更改實際上不應導致 hello 可執行文件重新鏈接:
python
運行
import timeversion_c_text = """
char *date = "%s";
""" % time.ctime(time.time())
open('version.c', 'w').write(version_c_text)version_obj = Object('version.c')hello = Program('hello.c',LINKFLAGS = str(version_obj[0]))Requires(hello, version_obj)
請注意,因為我們不能再將 version.c 列為 hello 程序的源文件之一,所以我們必須找到其他方法將其添加到鏈接命令行中。在這個示例中,我們有點取巧,將目標文件名(從 Object 調用返回的 version_obj 列表中提取)放入變量中,因為LINKFLAGS 已經包含在 $LINKCOM 命令行中。
通過這些更改,我們得到了所需的行為,即只有當 hello.c 發生變化時才重新鏈接 hello 可執行文件,即使 version.o 被重新構建(因為 SConstruct 文件每次運行時仍會直接更改 version.c 的內容):
plaintext
% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.
% sleep 1
% [CHANGE THE CONTENTS OF hello.c]
% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.
6.9 AlwaysBuild 函數
SCons 處理依賴的方式也可能受到 AlwaysBuild 方法的影響。當一個文件傳遞給 AlwaysBuild 方法時,如下所示:
python
運行
hello = Program('hello.c')
AlwaysBuild(hello)
那么指定的目標文件(在我們的示例中是 hello)在遍歷依賴圖時,每次評估該目標文件時,都會被認為是過時的并重新構建:
plaintext
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
cc -o hello hello.o
AlwaysBuild 函數的名稱有點誤導性,因為它實際上并不意味著每次調用 SCons 時目標文件都會重新構建。相反,它意味著在評估命令行上指定的目標(及其依賴項)時,只要遇到目標文件,該目標就會被重新構建。因此,如果在命令行上指定了其他目標,并且該目標本身不依賴于 AlwaysBuild 目標,那么只有當它相對于其依賴項過時的時候才會被重新構建:
plaintext
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello.o
scons: `hello.o' is up to date.
第七章 環境
環境是一組值的集合,這些值會影響程序的執行方式。SCons 區分三種不同類型的環境,這些環境會影響 SCons 自身的行為(取決于 SConscript 文件中的配置),以及它執行的編譯器和其他工具:
- 外部環境:外部環境是用戶運行 SCons 時其環境中的一組變量。這些變量可以通過 Python 的 os.environ 字典在 SConscript 文件中使用。請參閱下面的 7.1 節 “使用外部環境中的值”。
- 構建環境:構建環境是在 SConscript 文件中創建的一個獨特對象,它包含影響 SCons 決定使用何種操作來構建目標的一些值,甚至可以定義從哪些源文件構建哪些目標。SCons 最強大的功能之一是能夠創建多個構建環境,包括從現有構建環境克隆一個新的、自定義的構建環境的能力。請參閱下面的 7.2 節 “構建環境”。
- 執行環境:執行環境是 SCons 在執行外部命令(如編譯器或鏈接器)以構建一個或多個目標時設置的值。請注意,這與外部環境不同(見上文)。請參閱下面的 7.3 節 “控制發出命令的執行環境”。
與 Make 不同,SCons 不會自動在不同環境之間復制或導入值(顯式克隆構建環境的情況除外,克隆的構建環境會從其父環境繼承值)。這是一個經過深思熟慮的設計選擇,以確保在默認情況下,無論用戶外部環境中的值如何,構建都是可重復的。這避免了一類構建問題,即開發人員的本地構建能夠成功,是因為自定義變量設置導致使用了不同的編譯器或構建選項,但簽入的更改會破壞官方構建,因為它使用了不同的環境變量設置。
請注意,SConscript 編寫者可以輕松地安排在不同環境之間復制或導入變量,這通常非常有用(甚至是完全必要的),以便開發人員能夠以適當的方式自定義構建。重點不是在不同環境之間復制變量是不好的并且必須始終避免。相反,構建系統的實現者應該有意識地選擇如何以及何時將變量從一個環境導入另一個環境,并在使構建可重復和使用方便之間做出明智的決策,以達到正確的平衡。
7.1 使用外部環境中的值
用戶在執行 SCons 時生效的外部環境變量設置可通過普通的 Python os.environ 字典獲取。這意味著,如果您想在 SConscript 文件中使用用戶外部環境中的值,則必須添加 import os 語句。
python
運行
import os
更有用的是,您可以在 SConscript 文件中使用 os.environ 字典,用用戶外部環境中的值來初始化構建環境。有關如何執行此操作的信息,請參閱下一節 7.2 “構建環境”。
7.2 構建環境
在大型復雜系統中,很少有所有軟件都需要以相同的方式進行構建。例如,不同的源文件可能需要在命令行上啟用不同的選項,或者不同的可執行程序需要與不同的庫進行鏈接。SCons 通過允許您創建和配置多個構建環境來控制軟件的構建方式,從而滿足這些不同的構建要求。構建環境是一個對象,它具有許多相關的構建變量,每個變量都有一個名稱和一個值。(構建環境還附帶一組 Builder 方法,我們將在后面詳細了解這些方法。)
7.2.1 創建構建環境:Environment 函數
構建環境是通過 Environment 方法創建的:
python
運行
env = Environment()
默認情況下,SCons 會根據在系統上找到的工具,為每個新的構建環境初始化一組構建變量,以及使用這些工具所需的默認構建器方法集。構建變量使用描述 C 編譯器、Fortran 編譯器、鏈接器等的命令行的值進行初始化。
當您初始化構建環境時,您可以設置環境的構建變量的值,以控制程序的構建方式。例如:
python
運行
env = Environment(CC = 'gcc',CCFLAGS = '-O2')env.Program('foo.c')
在這個示例中,構建環境仍然使用相同的默認構建變量值進行初始化,只是用戶明確指定使用 GNU C 編譯器 gcc,并進一步指定在編譯目標文件時應使用 - O2(優化級別二)標志。換句話說,和CCFLAGS 的顯式初始化會覆蓋新創建的構建環境中的默認值。因此,這個示例的運行結果如下:
plaintext
% scons -Q
gcc -o foo.o -c -O2 foo.c
gcc -o foo foo.o
7.2.2 從構建環境中獲取值
您可以使用訪問 Python 字典中各個命名項的常規語法來獲取單個構建變量:
python
運行
env = Environment()
print("CC is: %s"%env['CC'])
這個示例 SConstruct 文件不會構建任何內容,但由于它實際上是一個 Python 腳本,它會為我們打印 $CC 的值:
plaintext
% scons -Q
CC is: cc
scons: `.' is up to date.
構建環境實際上是一個具有相關方法和屬性的對象。如果您只想直接訪問構建變量的字典,可以使用 Dictionary 方法獲取它:
python
運行
env = Environment(FOO='foo', BAR='bar')
cvars = env.Dictionary()
for key in ['OBJSUFFIX', 'LIBSUFFIX', 'PROGSUFFIX']:print("key = %s, value = %s" % (key, cvars[key]))
這個 SConstruct 文件將為我們在 POSIX 系統上打印指定的字典項,如下所示:
plaintext
% scons -Q
key = OBJSUFFIX, value = .o
key = LIBSUFFIX, value = .a
key = PROGSUFFIX, value =
scons: `.' is up to date.
在 Windows 上:
plaintext
C:\>scons -Q
key = OBJSUFFIX, value = .obj
key = LIBSUFFIX, value = .lib
key = PROGSUFFIX, value = .exe
scons: `.' is up to date.
如果您想循環并打印構建環境中所有構建變量的值,按排序順序執行此操作的 Python 代碼可能如下所示:
python
運行
env = Environment()
for item in sorted(env.Dictionary().items()):print("construction variable = '%s', value = '%s'" % item)
需要注意的是,對于前面的示例,實際上有一個構建環境方法可以更簡單地完成相同的事情,并且還會嘗試很好地格式化輸出:
python
運行
env = Environment()
print(env.Dump())
7.2.3. 從構建環境展開變量值:subst 方法
從構建環境獲取信息的另一種方法是對包含構建變量名(以?$
?開頭)的字符串使用?subst
?方法。舉個簡單的例子,上一節中使用?env['CC']
?獲取?$CC
?值的代碼也可以這樣寫:
python
env = Environment()
print("CC is: %s" % env.subst('$CC'))
使用?subst
?展開字符串的一個優勢在于,結果中的構建變量會被遞歸展開,直到字符串中不再有可展開的變量。例如,直接獲取?$CCCOM
?的值:
python
env = Environment(CCFLAGS='-DFOO')
print("CCCOM is: %s" % env['CCCOM'])
這會輸出?$CCCOM
?未展開的值,顯示其中仍需展開的構建變量:
plaintext
% scons -Q
CCCOM is: $CC $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS -c -o $TARGET $SOURCES
scons: `.' is up to date.
然而,對?$CCCOM
?調用?subst
?方法:
python
env = Environment(CCFLAGS='-DFOO')
print("CCCOM is: %s" % env.subst('$CCCOM'))
會遞歸展開所有以?$
(美元符號)開頭的構建變量,顯示最終的輸出結果:
plaintext
% scons -Q
CCCOM is: gcc -DFOO -c -o
scons: `.' is up to date.
注意,由于這里并非在實際構建過程中展開變量,因此?$TARGET
?和?$SOURCES
?沒有對應的目標文件或源文件可供展開。
7.2.4. 處理變量展開問題
當展開構建變量時發生問題,默認情況下該變量會被展開為空字符串?''
,并且不會導致 SCons 構建失敗。
python
env = Environment()
print("value is: %s" % env.subst('->$MISSING<-'))
輸出結果:
plaintext
% scons -Q
value is: -><-
scons: `.' is up to date.
可以使用?AllowSubstExceptions
?函數更改這種默認行為。當變量展開出現問題時會拋出異常,而?AllowSubstExceptions
?函數可以控制哪些異常是致命的,哪些可以被安全忽略。默認情況下,NameError
和?IndexError
?這兩種異常會被允許發生:SCons 會捕獲這些異常,將變量展開為空字符串,然后繼續執行。如果要求所有構建變量名必須存在,并且不允許越界索引,可以不帶任何參數調用?AllowSubstExceptions
:
python
AllowSubstExceptions()
env = Environment()
print("value is: %s" % env.subst('->$MISSING<-'))
此時輸出結果:
plaintext
% scons -Q
scons: *** NameError `MISSING' trying to evaluate `$MISSING'
File "/home/my/project/SConstruct", line 3, in <module>
該函數也可用于允許其他可能出現的異常,特別是在使用?${...}
?構建變量語法時。例如,除了默認允許的異常外,還可以允許變量展開中出現除零異常:
python
AllowSubstExceptions(IndexError, NameError, ZeroDivisionError)
env = Environment()
print("value is: %s" % env.subst('->${1 / 0}<-'))
輸出結果:
plaintext
% scons -Q
value is: -><-
scons: `.' is up to date.
如果多次調用?AllowSubstExceptions
,每次調用都會完全覆蓋之前允許的異常列表。
7.2.5. 控制默認構建環境:DefaultEnvironment 函數
到目前為止介紹的所有構建器函數(如?Program
?和?Library
)都使用一個構建環境,該環境包含 SCons 默認配置的各種編譯器和其他工具的設置,或者是 SCons 在系統上發現的工具設置。如果不將這些函數作為特定構建環境的方法調用,它們將使用默認構建環境。默認構建環境的目標是讓許多配置 "開箱即用",使用現成的工具以最少的配置更改來構建軟件。
如果需要,可以使用?DefaultEnvironment
?函數控制默認構建環境,通過傳遞關鍵字參數來初始化各種設置:
python
DefaultEnvironment(CC='/usr/local/bin/gcc')
按上述方式配置后,所有對?Program
?或?Object
?構建器的調用都將使用?/usr/local/bin/gcc
?編譯器來構建目標文件。
DefaultEnvironment
?函數返回初始化后的默認構建環境對象,該對象可以像其他構建環境一樣進行操作(注意,默認環境類似于單例模式 —— 它只能有一個實例 —— 因此關鍵字參數僅在首次調用時處理。后續的任何調用都會返回現有的對象)。因此,以下代碼與前面的示例等效,將?$CC
?變量設置為?/usr/local/bin/gcc
,但在默認構建環境初始化后作為單獨的步驟進行:
python
env = DefaultEnvironment()
env['CC'] = '/usr/local/bin/gcc'
DefaultEnvironment
?函數的一個非常常見的用途是加速 SCons 初始化。為了讓大多數默認配置 "開箱即用",SCons 實際上會在本地系統中搜索已安裝的編譯器和其他實用工具。這種搜索可能需要時間,特別是在文件系統較慢或基于網絡的系統上。如果您知道要配置哪些編譯器和 / 或其他實用工具,可以通過指定一些特定的工具模塊來控制 SCons 執行的搜索,以初始化默認構建環境:
python
env = DefaultEnvironment(tools=['gcc', 'gnulink'],CC='/usr/local/bin/gcc')
上面的示例告訴 SCons 明確配置默認環境,使用其正常的 GNU 編譯器和 GNU 鏈接器設置(無需搜索它們或任何其他實用工具),并特別使用?/usr/local/bin/gcc
?處的編譯器。
7.2.6. 多個構建環境
構建環境的真正優勢在于您可以根據需要創建任意數量的不同環境,每個環境都針對構建軟件或其他文件的不同方式進行定制。例如,如果我們需要使用?-O2
?標志構建一個程序,同時使用?-g
(調試)標志構建另一個程序,我們可以這樣做:
python
opt = Environment(CCFLAGS='-O2')
dbg = Environment(CCFLAGS='-g')opt.Program('foo', 'foo.c')
dbg.Program('bar', 'bar.c')
執行結果:
plaintext
% scons -Q
cc -o bar.o -c -g bar.c
cc -o bar bar.o
cc -o foo.o -c -O2 foo.c
cc -o foo foo.o
我們甚至可以使用多個構建環境來構建單個程序的多個版本。但是,如果只是簡單地嘗試在兩個環境中使用?Program
?構建器,如下所示:
python
opt = Environment(CCFLAGS='-O2')
dbg = Environment(CCFLAGS='-g')opt.Program('foo', 'foo.c')
dbg.Program('foo', 'foo.c')
SCons 會生成以下錯誤:
plaintext
% scons -Q
scons: *** Two environments with different actions were specified for the same target: foo.o
File "/home/my/project/SConstruct", line 6, in <module>
這是因為兩個?Program
?調用都隱式地告訴 SCons 生成一個名為?foo.o
?的目標文件,一個使用?-O2
?的?$CCFLAGS
?值,另一個使用?-g
?的?$CCFLAGS
?值。SCons 不能簡單地決定其中一個應該優先于另一個,因此會生成錯誤。為了避免這個問題,我們必須使用?Object
?構建器顯式指定每個環境將?foo.c
?編譯為單獨命名的目標文件,如下所示:
python
opt = Environment(CCFLAGS='-O2')
dbg = Environment(CCFLAGS='-g')o = opt.Object('foo-opt', 'foo.c')
opt.Program(o)d = dbg.Object('foo-dbg', 'foo.c')
dbg.Program(d)
注意,每次調用?Object
?構建器都會返回一個值,這是一個表示將被構建的目標文件的 SCons 內部對象。然后,我們將該對象用作?Program
?構建器的輸入。這避免了在多個位置顯式指定目標文件名,并使 SConstruct 文件緊湊易讀。我們的 SCons 輸出如下所示:
plaintext
% scons -Q
cc -o foo-dbg.o -c -g foo.c
cc -o foo-dbg foo-dbg.o
cc -o foo-opt.o -c -O2 foo.c
cc -o foo-opt foo-opt.o
7.2.7. 復制構建環境:Clone 方法
有時,您希望多個構建環境共享一個或多個變量的相同值。與其在創建每個構建環境時重復所有公共變量,不如使用?Clone
?方法創建構建環境的副本。
與創建構建環境的?Environment
?調用一樣,Clone
?方法接受構建變量賦值,這些賦值將覆蓋被復制構建環境中的值。例如,假設我們想使用 gcc 創建一個程序的三個版本:一個優化版本、一個調試版本和一個無特殊標志的版本。我們可以通過創建一個將?$CC
?設置為 gcc 的 "基礎" 構建環境,然后創建兩個副本,一個設置用于優化的?$CCFLAGS
,另一個設置用于調試的?$CCFLAGS
:
python
env = Environment(CC='gcc')
opt = env.Clone(CCFLAGS='-O2')
dbg = env.Clone(CCFLAGS='-g')env.Program('foo', 'foo.c')o = opt.Object('foo-opt', 'foo.c')
opt.Program(o)d = dbg.Object('foo-dbg', 'foo.c')
dbg.Program(d)
然后我們的輸出將如下所示:
plaintext
% scons -Q
gcc -o foo.o -c foo.c
gcc -o foo foo.o
gcc -o foo-dbg.o -c -g foo.c
gcc -o foo-dbg foo-dbg.o
gcc -o foo-opt.o -c -O2 foo.c
gcc -o foo-opt foo-opt.o
7.2.8. 替換值:Replace 方法
您可以使用?Replace
?方法替換現有的構建變量值:
python
env = Environment(CCFLAGS='-DDEFINE1')
env.Replace(CCFLAGS='-DDEFINE2')
env.Program('foo.c')
替換值(在上面的例子中是?-DDEFINE2
)完全替換了構建環境中的值:
plaintext
% scons -Q
cc -o foo.o -c -DDEFINE2 foo.c
cc -o foo foo.o
您可以安全地為構建環境中不存在的構建變量調用?Replace
:
python
env = Environment()
env.Replace(NEW_VARIABLE='xyzzy')
print("NEW_VARIABLE = %s" % env['NEW_VARIABLE'])
在這種情況下,構建變量只是被添加到構建環境中:
plaintext
% scons -Q
NEW_VARIABLE = xyzzy
scons: `.' is up to date.
由于變量在構建環境實際用于構建目標之前不會被展開,并且由于 SCons 函數和方法調用是順序無關的,最后一次替換 "勝出" 并用于構建所有目標,無論?Replace()
?調用與構建器方法調用的穿插順序如何:
python
env = Environment(CCFLAGS='-DDEFINE1')
print("CCFLAGS = %s" % env['CCFLAGS'])
env.Program('foo.c')env.Replace(CCFLAGS='-DDEFINE2')
print("CCFLAGS = %s" % env['CCFLAGS'])
env.Program('bar.c')
如果我們不帶?-Q
?選項運行 scons,替換實際發生的時間與目標被構建的時間之間的關系就變得明顯了:
plaintext
% scons
scons: Reading SConscript files ...
CCFLAGS = -DDEFINE1
CCFLAGS = -DDEFINE2
scons: done reading SConscript files.
scons: Building targets ...
cc -o bar.o -c -DDEFINE2 bar.c
cc -o bar bar.o
cc -o foo.o -c -DDEFINE2 foo.c
cc -o foo foo.o
scons: done building targets.
由于替換發生在讀取 SConscript 文件時,當構建?foo.o
?目標時,$CCFLAGS
?變量已經被設置為?-DDEFINE2
,即使對?Replace
?方法的調用直到 SConscript 文件的后面才發生。
7.2.9. 僅在未定義時設置值:SetDefault 方法
有時,能夠指定只有在構建環境尚未定義某個構建變量時才將其設置為某個值是很有用的。您可以使用?SetDefault
?方法來做到這一點,該方法的行為類似于 Python 字典對象的?set_default
?方法:
python
env.SetDefault(SPECIAL_FLAG='-extra-option')
這在編寫自己的工具模塊以將變量應用于構建環境時特別有用。
7.2.10. 追加到值的末尾:Append 方法
您可以使用?Append
?方法將一個值追加到現有的構建變量:
python
env = Environment(CCFLAGS=['-DMY_VALUE'])
env.Append(CCFLAGS=['-DLAST'])
env.Program('foo.c')
SCons 在編譯目標文件時會同時提供?-DMY_VALUE
?和?-DLAST
?標志:
plaintext
% scons -Q
cc -o foo.o -c -DMY_VALUE -DLAST foo.c
cc -o foo foo.o
如果構建變量尚不存在,Append
?方法將創建它:
python
env = Environment()
env.Append(NEW_VARIABLE='added')
print("NEW_VARIABLE = %s" % env['NEW_VARIABLE'])
輸出結果:
plaintext
% scons -Q
NEW_VARIABLE = added
scons: `.' is up to date.
注意,Append
?函數會 "智能" 地處理新值如何追加到舊值。如果兩者都是字符串,則簡單地將前一個字符串和新字符串連接起來。同樣,如果兩者都是列表,則將列表連接起來。但是,如果一個是字符串而另一個是列表,則將字符串作為新元素添加到列表中。
7.2.11. 追加唯一值:AppendUnique 方法
有時,僅在現有構建變量不包含某個值時添加該值是有用的。這可以通過?AppendUnique
?方法實現:
python
env.AppendUnique(CCFLAGS=['-g'])
在上面的例子中,只有當?$CCFLAGS
?變量尚未包含?-g
?值時,才會添加?-g
。
7.2.12. 追加到值的開頭:Prepend 方法
您可以使用?Prepend
?方法將一個值追加到現有構建變量的開頭:
python
env = Environment(CCFLAGS=['-DMY_VALUE'])
env.Prepend(CCFLAGS=['-DFIRST'])
env.Program('foo.c')
SCons 在編譯目標文件時會同時提供?-DFIRST
?和?-DMY_VALUE
?標志:
plaintext
% scons -Q
cc -o foo.o -c -DFIRST -DMY_VALUE foo.c
cc -o foo foo.o
如果構建變量尚不存在,Prepend
?方法將創建它:
python
env = Environment()
env.Prepend(NEW_VARIABLE='added')
print("NEW_VARIABLE = %s" % env['NEW_VARIABLE'])
輸出結果:
plaintext
% scons -Q
NEW_VARIABLE = added
scons: `.' is up to date.
與?Append
?函數一樣,Prepend
?函數也會 "智能" 地處理新值如何追加到舊值。如果兩者都是字符串,則簡單地將前一個字符串和新字符串連接起來。同樣,如果兩者都是列表,則將列表連接起來。但是,如果一個是字符串而另一個是列表,則將字符串作為新元素添加到列表中。
7.2.13. 前置唯一值:PrependUnique 方法
有時,僅在現有值不包含要添加的值時,將新值添加到構建變量的開頭是有用的。這可以通過?PrependUnique
?方法實現:
python
env.PrependUnique(CCFLAGS=['-g'])
在上面的例子中,只有當?$CCFLAGS
?變量尚未包含?-g
?值時,才會添加?-g
。
7.3. 控制執行命令的環境
當 SCons 構建目標文件時,它不會使用您執行 SCons 時的相同外部環境來執行命令。相反,它使用存儲在?$ENV
?構建變量中的字典作為執行命令的外部環境。
這種行為的最重要影響是,控制操作系統在哪里查找命令和實用工具的?PATH
?環境變量與您調用 SCons 的外部環境中的?PATH
?不同。這意味著 SCons 默認情況下不一定能找到您可以從命令行執行的所有工具。
在 POSIX 系統上,PATH
?環境變量的默認值是?/usr/local/bin:/bin:/usr/bin
。在 Windows 系統上,PATH
?環境變量的默認值來自命令解釋器的 Windows 注冊表值。如果您想執行任何不在這些默認位置的命令(編譯器、鏈接器等),則需要在構建環境的?$ENV
?字典中設置?PATH
?值。
最簡單的方法是在創建構建環境時顯式初始化該值;以下是一種實現方式:
python
path = ['/usr/local/bin', '/bin', '/usr/bin']
env = Environment(ENV={'PATH': path})
以這種方式將字典分配給?$ENV
?構建變量會完全重置外部環境,因此執行外部命令時設置的唯一變量將是?PATH
值。如果您想使用?$ENV
?中的其余值而只設置?PATH
?值,最直接的方法可能是:
python
env['ENV']['PATH'] = ['/usr/local/bin', '/bin', '/usr/bin']
請注意,SCons 確實允許您以字符串形式定義?PATH
?中的目錄,用系統的路徑分隔符字符分隔(POSIX 系統上為?:
,Windows 上為?;
):
python
env['ENV']['PATH'] = '/usr/local/bin:/bin:/usr/bin'
但這樣做會降低您的 SConscript 文件的可移植性(盡管在這種情況下這可能不是一個大問題,因為您列出的目錄可能是特定于系統的)。
7.3.1. 從外部環境傳播 PATH
您可能希望將外部?PATH
?傳播到命令的執行環境。您可以通過使用?os.environ
?字典中的?PATH
?值初始化?PATH
?變量來實現這一點,os.environ
?是 Python 讓您訪問外部環境的方式:
python
import os
env = Environment(ENV={'PATH': os.environ['PATH']})
或者,您可能會發現將整個外部環境傳播到命令的執行環境更容易。這樣編碼比顯式選擇?PATH
?值更簡單:
python
import os
env = Environment(ENV=os.environ)
這兩種方法都能保證 SCons 能夠執行您可以從命令行執行的任何命令。缺點是,如果構建由環境中?PATH
?值不同的人運行,構建行為可能會有所不同 —— 例如,如果?/bin
?和?/usr/local/bin
?目錄都有不同的?cc
?命令,那么使用哪個命令來編譯程序將取決于用戶?PATH
?變量中哪個目錄排在前面。
7.3.2. 在執行環境中添加到 PATH 值
操作執行環境中的變量的最常見要求之一是將一個或多個自定義目錄添加到類似 Linux 或 POSIX 系統上的?$PATH
?變量或 Windows 上的?%PATH%
?變量的搜索路徑中,以便 SCons 在嘗試執行本地安裝的編譯器或其他實用工具來更新目標時能夠找到它們。SCons 提供了?PrependENVPath
?和?AppendENVPath
?函數,使向執行變量添加內容變得方便。您可以通過指定要添加值的變量,然后指定值本身來調用這些函數。因此,要將一些?/usr/local
?目錄添加到?$PATH
?和?$LIB
?變量,您可以這樣做:
python
env = Environment(ENV=os.environ)
env.PrependENVPath('PATH', '/usr/local/bin')
env.AppendENVPath('LIB', '/usr/local/lib')
請注意,添加的值是字符串,如果您想向?$PATH
?這樣的變量添加多個目錄,則必須在字符串中包含路徑分隔符字符(Linux 或 POSIX 上為?:
,Windows 上為?;
)。
7.4. 使用 toolpath 查找外部工具
7.4.1. 默認工具搜索路徑
通常在從構建環境使用工具時,默認會檢查幾個不同的搜索位置。這包括 SCons 內置的?SCons/Tools/
?目錄以及相對于根 SConstruct 文件的?site_scons/site_tools
?目錄。
python
# 內置工具或位于 site_tools 中的工具
env = Environment(tools=['SomeTool'])
env.SomeTool(targets, sources)# 默認情況下搜索位置包括
SCons/Tool/SomeTool.py
SCons/Tool/SomeTool/__init__.py
./site_scons/site_tools/SomeTool.py
./site_scons/site_tools/SomeTool/__init__.py
7.4.2. 為 toolpath 提供外部目錄
在某些情況下,您可能希望指定不同的位置來搜索工具。Environment 構造函數包含一個名為 toolpath 的選項,可用于添加額外的搜索目錄。
python
# 位于 toolpath 目錄選項中的工具
env = Environment(tools=['SomeTool'], toolpath=['/opt/SomeToolPath', '/opt/SomeToolPath2'])
env.SomeTool(targets, sources)# 此示例中的搜索位置將包括:
/opt/SomeToolPath/SomeTool.py
/opt/SomeToolPath/SomeTool/__init__.py
/opt/SomeToolPath2/SomeTool.py
/opt/SomeToolPath2/SomeTool/__init__.py
SCons/Tool/SomeTool.py
SCons/Tool/SomeTool/__init__.py
./site_scons/site_tools/SomeTool.py
./site_scons/site_tools/SomeTool/__init__.py
7.4.3. toolpath 中的嵌套工具
SCons 3.0 現在支持構建器位于 toolpath 的子目錄 / 子包中的功能。這類似于 Python 中的命名空間。使用嵌套或命名空間工具,我們可以使用點符號來指定工具所在的子目錄。
python
# 命名空間目標
env = Environment(tools=['SubDir1.SubDir2.SomeTool'], toolpath=['/opt/SomeToolPath'])
env.SomeTool(targets, sources)# 在此示例中,搜索位置將包括
/opt/SomeToolPath/SubDir1/SubDir2/SomeTool.py
/opt/SomeToolPath/SubDir1/SubDir2/SomeTool/__init__.py
SCons/Tool/SubDir1/SubDir2/SomeTool.py
SCons/Tool/SubDir1/SubDir2/SomeTool/__init__.py
./site_scons/site_tools/SubDir1/SubDir2/SomeTool.py
./site_scons/site_tools/SubDir1/SubDir2/SomeTool/__init__.py
對于 Python 2,在子目錄中創建工具時需要注意,每個目錄中都需要有一個?__init__.py
?文件。這個文件可以是空的。這與 Python 從子目錄(包)加載模塊時使用的約束相同。對于 Python 3,這似乎不再是必需的。
7.4.4. 在 toolpath 中使用 sys.path
如果我們想在 SCons 外部的 sys.path 上訪問工具(例如通過 pip 包管理器安裝的工具),一種方法是在 toolpath 中使用 sys.path。使用這種方法需要注意的一點是,sys.path 有時可能包含指向 .egg 文件的路徑而不是目錄。因此,我們需要用這種方法過濾掉那些路徑。
python
# 在 toolpath 中使用 sys.path 的命名空間目標
searchpaths = []
for item in sys.path:if os.path.isdir(item):searchpaths.append(item)env = Environment(tools=['someinstalledpackage.SomeTool'], toolpath=searchpaths)
env.SomeTool(targets, sources)
通過在 toolpath 參數中使用 sys.path 并使用嵌套語法,我們可以讓 SCons 在通過 pip 安裝的包中搜索工具。
python
# 對于 Windows,根據 Python 版本和安裝目錄,可能是這樣的路徑
C:\Python35\Lib\site-packages\someinstalledpackage\SomeTool.py
C:\Python35\Lib\site-packages\someinstalledpackage\SomeTool\__init__.py# 對于 Linux,可能是這樣的路徑
/usr/lib/python3/dist-packages/someinstalledpackage/SomeTool.py
/usr/lib/python3/dist-packages/someinstalledpackage/SomeTool/__init__.py
7.4.5. 使用 PyPackageDir 函數添加到 toolpath
在某些情況下,您可能希望使用位于已安裝的外部 pip 包中的工具。這可以通過在 toolpath 中使用 sys.path 來實現。然而,在這種情況下,您需要為工具名稱提供一個前綴,以指示它在 sys.path 中的位置。
python
searchpaths = []
for item in sys.path:if os.path.isdir(item):searchpaths.append(item)
env = Environment(tools=['tools_example.subdir1.subdir2.SomeTool'], toolpath=searchpaths)
env.SomeTool(targets, sources)
為了避免在工具名稱中使用前綴或過濾 sys.path 中的目錄,我們可以使用?PyPackageDir(modulename)
?函數來定位 Python 包的目錄。PyPackageDir
?返回一個 Dir 對象,表示作為參數指定的 Python 包 / 模塊的目錄路徑。
python
# 使用 sys.path 的命名空間目標
env = Environment(tools=['SomeTool'], toolpath=[PyPackageDir('tools_example.subdir1.subdir2')])
env.SomeTool(targets, sources)
第 8 章。自動將命令行選項放入構建變量
本章介紹構建環境的?MergeFlags
、ParseFlags
?和?ParseConfig
?方法。
8.1. 將選項合并到環境中:MergeFlags 函數
SCons 構建環境有一個?MergeFlags
?方法,它將一個值的字典合并到構建環境中。MergeFlags
?將字典中的每個值視為一個選項列表,就像傳遞給命令(如編譯器或鏈接器)的選項一樣。如果選項已經存在于構建環境變量中,MergeFlags
?不會重復添加。
MergeFlags
?在合并選項時會智能處理。當合并選項到任何名稱以?PATH
?結尾的變量時,MergeFlags
?會保留選項的最左出現項,因為在典型的目錄路徑列表中,第一個出現的項 “勝出”。當合并選項到其他變量名時,MergeFlags
?會保留選項的最右出現項,因為在典型的命令行選項列表中,最后出現的項 “勝出”。
python
運行
env = Environment()
env.Append(CCFLAGS='-option -O3 -O1')
flags = { 'CCFLAGS' : '-whatever -O3' }
env.MergeFlags(flags)
print(env['CCFLAGS'])
輸出:
plaintext
% scons -Q
['-option', '-O1', '-whatever', '-O3']
scons: `.' is up to date.
請注意,$CCFLAGS
?的默認值是一個 SCons 內部對象,它會自動將我們指定為字符串的選項轉換為列表。
python
運行
env = Environment()
env.Append(CPPPATH = ['/include', '/usr/local/include', '/usr/include'])
flags = { 'CPPPATH' : ['/usr/opt/include', '/usr/local/include'] }
env.MergeFlags(flags)
print(env['CPPPATH'])
輸出:
plaintext
% scons -Q
['/include', '/usr/local/include', '/usr/include', '/usr/opt/include']
scons: `.' is up to date.
請注意,$CPPPATH
?的默認值是一個普通的 Python 列表,所以我們必須在傳遞給?MergeFlags
?函數的字典中把它的值指定為列表。
如果傳遞給?MergeFlags
?的不是字典,它會調用?ParseFlags
?方法將其轉換為字典。
python
運行
env = Environment()
env.Append(CCFLAGS='-option -O3 -O1')
env.Append(CPPPATH=['/include', '/usr/local/include', '/usr/include'])
env.MergeFlags('-whatever -I/usr/opt/include -O3 -I/usr/local/include')
print(env['CCFLAGS'])
print(env['CPPPATH'])
輸出:
plaintext
% scons -Q
['-option', '-O1', '-whatever', '-O3']
['/include', '/usr/local/include', '/usr/include', '/usr/opt/include']
scons: `.' is up to date.
在上面的組合示例中,ParseFlags
?會將選項分類到相應的變量中,并返回一個字典,供?MergeFlags
?應用到指定構建環境的構建變量中。
8.2. 將編譯參數分離到各自的變量中:ParseFlags 函數
在構建程序時,SCons 有一系列令人眼花繚亂的構建變量用于不同類型的選項。有時你可能不確定對于某個特定選項應該使用哪個變量。
SCons 構建環境有一個?ParseFlags
?方法,它接受一組典型的命令行選項,并將它們分配到適當的構建變量中。從歷史上看,它的創建是為了支持?ParseConfig
?方法,所以它專注于 GNU 編譯器集合(GCC)用于 C 和 C++ 工具鏈的選項。
ParseFlags
?返回一個字典,其中包含分配到各自構建變量中的選項。通常,這個字典會傳遞給?MergeFlags
,將選項合并到構建環境中,但如果需要提供額外功能,也可以編輯這個字典。(請注意,如果不打算編輯這些標志,直接使用選項調用?MergeFlags
?將避免額外的步驟。)
python
運行
from __future__ import print_function
env = Environment()
d = env.ParseFlags("-I/opt/include -L/opt/lib -lfoo")
for k,v in sorted(d.items()):if v:print(k, v)
env.MergeFlags(d)
env.Program('f1.c')
輸出:
plaintext
% scons -Q
CPPPATH ['/opt/include']
LIBPATH ['/opt/lib']
LIBS ['foo']
cc -o f1.o -c -I/opt/include f1.c
cc -o f1 f1.o -L/opt/lib -lfoo
請注意,如果選項僅限于像上面那樣的通用類型,它們將被正確轉換為其他平臺類型:
plaintext
C:\>scons -Q
CPPPATH ['/opt/include']
LIBPATH ['/opt/lib']
LIBS ['foo']
cl /Fof1.obj /c f1.c /nologo /I\opt\include
link /nologo /OUT:f1.exe /LIBPATH:\opt\lib foo.lib f1.obj
embedManifestExeCheck(target, source, env)
由于假設這些標志用于 GCC 工具鏈,無法識別的標志將被放置在?$CCFLAGS
?中,所以它們將用于 C 和 C++ 編譯:
python
運行
from __future__ import print_function
env = Environment()
d = env.ParseFlags("-whatever")
for k,v in sorted(d.items()):if v:print(k, v)
env.MergeFlags(d)
env.Program('f1.c')
輸出:
plaintext
% scons -Q
CCFLAGS -whatever
cc -o f1.o -c -whatever f1.c
cc -o f1 f1.o
ParseFlags
?也接受一個(遞歸的)字符串列表作為輸入;在處理字符串之前,列表會被展平:
python
運行
from __future__ import print_function
env = Environment()
d = env.ParseFlags(["-I/opt/include", ["-L/opt/lib", "-lfoo"]])
for k,v in sorted(d.items()):if v:print(k, v)
env.MergeFlags(d)
env.Program('f1.c')
輸出:
plaintext
% scons -Q
CPPPATH ['/opt/include']
LIBPATH ['/opt/lib']
LIBS ['foo']
cc -o f1.o -c -I/opt/include f1.c
cc -o f1 f1.o -L/opt/lib -lfoo
如果一個字符串以 “!”(感嘆號,通常稱為 “bang”)開頭,該字符串將被傳遞給 shell 執行。然后解析命令的輸出:
python
運行
from __future__ import print_function
env = Environment()
d = env.ParseFlags(["!echo -I/opt/include", "!echo -L/opt/lib", "-lfoo"])
for k,v in sorted(d.items()):if v:print(k, v)
env.MergeFlags(d)
env.Program('f1.c')
輸出:
plaintext
% scons -Q
CPPPATH ['/opt/include']
LIBPATH ['/opt/lib']
LIBS ['foo']
cc -o f1.o -c -I/opt/include f1.c
cc -o f1 f1.o -L/opt/lib -lfoo
ParseFlags
?會定期更新以識別新的選項;有關當前識別的選項的詳細信息,請查閱手冊頁。
8.3. 查找已安裝庫的信息:ParseConfig 函數
在 POSIX 系統上,配置正確的選項來構建與庫(特別是共享庫)一起工作的程序可能非常復雜。為了幫助解決這種情況,各種名稱以?config
?結尾的實用工具會返回使用這些庫所需的 GNU 編譯器集合(GCC)的命令行選項;例如,使用名為?lib
?的庫的命令行選項可以通過調用名為?lib-config
?的實用工具找到。
較新的慣例是,這些選項可以從通用的?pkg-config
?程序中獲得,該程序具有通用的框架、錯誤處理等,因此所有包創建者需要做的就是為他的特定包提供一組字符串。
SCons 構建環境有一個?ParseConfig
?方法,它執行一個?config
?實用工具(可以是?pkg-config
?或更特定的實用工具),并根據指定命令返回的命令行選項在環境中配置適當的構建變量。
python
運行
env = Environment()
env['CPPPATH'] = ['/lib/compat']
env.ParseConfig("pkg-config x11 --cflags --libs")
print(env['CPPPATH'])
SCons 將執行指定的命令字符串,解析生成的標志,并將標志添加到適當的環境變量中。
plaintext
% scons -Q
['/lib/compat', '/usr/X11/include']
scons: `.' is up to date.
在上面的示例中,SCons 將包含目錄添加到了?CPPPATH
?中。(根據?pkg-config
?命令發出的其他標志,其他變量也可能會被擴展。)
請注意,選項會使用?MergeFlags
?方法與現有選項合并,所以每個選項在構建變量中只會出現一次:
python
運行
env = Environment()
env.ParseConfig("pkg-config x11 --cflags --libs")
env.ParseConfig("pkg-config x11 --cflags --libs")
print(env['CPPPATH'])
輸出:
plaintext
% scons -Q
['/usr/X11/include']
scons: `.' is up to date.
第 9 章。控制構建輸出
創建可用的構建配置的一個關鍵方面是提供良好的構建輸出,以便用戶可以輕松理解構建在做什么,并獲取有關如何控制構建的信息。SCons 提供了幾種控制構建配置輸出的方法,以幫助使構建更有用和易于理解。
9.1. 提供構建幫助:Help 函數
能夠向用戶提供一些描述特定目標、構建選項等的幫助信息通常非常有用,這些信息可用于您的構建。SCons 提供了?Help
?函數,允許您指定此幫助文本:
python
運行
Help("""
Type: 'scons program' to build the production program,'scons debug' to build the debug version.
""")
也可以指定?append
?標志:
python
運行
Help("""
Type: 'scons program' to build the production program,'scons debug' to build the debug version.
""", append=True)
(請注意上面使用的 Python 三引號語法,對于指定像幫助文本這樣的多行字符串非常方便。)
當 SConstruct 或 SConscript 文件中包含對?Help
?函數的調用時,指定的幫助文本將在響應?SCons -h
?選項時顯示:
plaintext
% scons -h
scons: Reading SConscript files ...
scons: done reading SConscript files.Type: 'scons program' to build the production program,'scons debug' to build the debug version.Use scons -H for help about command-line options.
SConscript 文件可能包含對?Help
?函數的多個調用,在這種情況下,顯示時指定的文本將被連接起來。這允許您在多個 SConscript 文件中拆分幫助文本。在這種情況下,調用 SConscript 文件的順序將決定調用?Help
?函數的順序,這將決定各個文本片段連接的順序。
當與?AddOption
?一起使用時,Help("text", append=False)
?將覆蓋與?AddOption()
?相關的任何幫助輸出。要保留?AddOption()
?的幫助輸出,請設置?append=True
。
另一個用途是使幫助文本取決于某些變量。例如,假設您只想在實際在 Windows 上運行時顯示有關構建僅適用于 Windows 版本程序的一行信息。以下是一個 SConstruct 文件示例:
python
運行
env = Environment()Help("\nType: 'scons program' to build the production program.\n")if env['PLATFORM'] == 'win32':Help("\nType: 'scons windebug' to build the Windows debug version.\n")
在 Windows 上,將顯示完整的幫助文本:
plaintext
C:\>scons -h
scons: Reading SConscript files ...
scons: done reading SConscript files.Type: 'scons program' to build the production program.Type: 'scons windebug' to build the Windows debug version.Use scons -H for help about command-line options.
但在 Linux 或 UNIX 系統上,將只顯示相關選項:
plaintext
% scons -h
scons: Reading SConscript files ...
scons: done reading SConscript files.Type: 'scons program' to build the production program.Use scons -H for help about command-line options.
如果 SConstruct 或 SConscript 文件中沒有?Help
?文本,SCons 將恢復顯示其標準列表,該列表描述了 SCons 命令行選項。無論何時使用?-H
?選項,此列表也會始終顯示。
9.2. 控制 SCons 打印構建命令的方式:$*COMSTR 變量
有時,執行編譯目標文件或鏈接程序(或構建其他目標)的命令可能會很長,長到用戶很難從命令本身中區分錯誤消息或其他重要的構建輸出。所有指定用于構建各種類型目標文件的命令行的默認?$*COM
?變量都有一個對應的?$*COMSTR
?變量,可以將其設置為在構建目標時顯示的替代字符串。
例如,假設您希望 SCons 在編譯目標文件時顯示 “Compiling” 消息,在鏈接可執行文件時顯示 “Linking” 消息。您可以編寫一個 SConstruct 文件,如下所示:
python
運行
env = Environment(CCCOMSTR = "Compiling $TARGET",LINKCOMSTR = "Linking $TARGET")
env.Program('foo.c')
這將產生以下輸出:
plaintext
% scons -Q
Compiling foo.o
Linking foo
SCons 會對?$*COMSTR
?變量進行完整的變量替換,所以它們可以訪問所有標準變量,如?$TARGET
、$SOURCES
等,以及在用于構建特定目標的構建環境中配置的任何構建變量。
當然,有時能夠查看 SCons 為構建目標執行的確切命令仍然很重要。例如,您可能只是需要驗證 SCons 是否配置為向編譯器提供正確的選項,或者開發人員可能希望剪切并粘貼編譯命令以添加一些自定義測試的選項。
一種常見的方法是在 SConstruct 文件中添加對?VERBOSE
?命令行變量的支持,以控制 SCons 是否應該打印實際的命令行或簡短的配置摘要。一個簡單的配置可能如下所示:
python
運行
env = Environment()
if ARGUMENTS.get('VERBOSE') != '1':env['CCCOMSTR'] = "Compiling $TARGET"env['LINKCOMSTR'] = "Linking $TARGET"
env.Program('foo.c')
通過僅在用戶在命令行上指定?VERBOSE=1
?時設置適當的?$*COMSTR
?變量,用戶可以控制 SCons 顯示這些特定命令行的方式:
plaintext
% scons -Q
Compiling foo.o
Linking foo
% scons -Q -c
Removed foo.o
Removed foo
% scons -Q VERBOSE=1
cc -o foo.o -c foo.c
cc -o foo foo.o
9.3. 提供構建進度輸出:Progress 函數
提供良好構建輸出的另一個方面是向用戶反饋 SCons 在做什么,即使當前沒有進行任何構建。對于大型構建尤其如此,當大多數目標已經是最新的時候。因為 SCons 可能需要很長時間來絕對確保每個目標相對于許多依賴文件都是最新的,所以用戶很容易錯誤地認為 SCons 掛起了或者構建存在其他問題。
處理這種情況的一種方法是配置 SCons 打印一些內容,讓用戶知道它正在 “思考” 什么。Progress
?函數允許您指定一個字符串,當 SCons 遍歷依賴圖以確定哪些目標是最新的或不是最新的時,該字符串將為每個它 “考慮” 的文件打印。
python
運行
Progress('Evaluating $TARGET\n')
Program('f1.c')
Program('f2.c')
請注意,Progress
?函數不會像 Python 的?print
?語句那樣自動在字符串末尾打印換行符,我們必須指定我們希望在配置字符串末尾打印的?\n
。這樣配置后,當 SCons 遍歷依賴圖時,它將為遇到的每個文件打印 “Evaluating”:
plaintext
% scons -Q
Evaluating SConstruct
Evaluating f1.c
Evaluating f1.o
cc -o f1.o -c f1.c
Evaluating f1
cc -o f1 f1.o
Evaluating f2.c
Evaluating f2.o
cc -o f2.o -c f2.c
Evaluating f2
cc -o f2 f2.o
Evaluating.
當然,通常您不希望在構建輸出中添加所有這些額外的行,因為這可能會使用戶難以找到錯誤或其他重要消息。顯示此進度的更有用的方法可能是將文件名直接打印到用戶的屏幕上,而不是打印構建輸出的標準輸出流,并使用回車符?\r
,以便每個文件名在同一行上重新打印。這樣的配置可能如下所示:
python
運行
Progress('$TARGET\r',file=open('/dev/tty', 'w'),overwrite=True)
Program('f1.c')
Program('f2.c')
【這段先看英文吧。。。】
Note that we also specified the overwrite=True
argument to the Progress
function, which causes SCons to "wipe out" the previous string with space characters before printing the next Progress
string. Without the overwrite=True
argument, a shorter file name would not overwrite all of the charactes in a longer file name that precedes it, making it difficult to tell what the actual file name is on the output. Also note that we opened up the /dev/tty
file for direct access (on POSIX) to the user's screen. On Windows, the equivalent would be to open the con:
file name.
Also, it's important to know that although you can use $TARGET
to substitute the name of the node in the string, the Progress
function does not perform general variable substitution (because there's not necessarily a construction environment involved in evaluating a node like a source file, for example).
You can also specify a list of strings to the Progress
function, in which case SCons will display each string in turn. This can be used to implement a "spinner" by having SCons cycle through a sequence of strings:
Progress(['-\r', '\\\r', '|\r', '/\r'], interval=5)
Program('f1.c')
Program('f2.c')
請注意,在這里我們還使用了?interval=
?關鍵字參數,使得 SCons 僅在每評估五個節點時打印一個新的 “旋轉” 字符串。使用?interval=
?計數,即使是像上面示例中使用?$TARGET
?的字符串,也是減少 SCons 打印進度字符串的工作量的好方法,同時仍然向用戶反饋 SCons 仍在評估構建。
最后,你可以通過將 Python 函數(或其他可調用的 Python 對象)傳遞給?Progress
?函數,直接控制如何打印每個評估的節點。你的函數將針對每個評估的節點被調用,使你能夠實現更復雜的邏輯,例如添加計數器:
python
運行
screen = open('/dev/tty', 'w')
count = 0
def progress_function(node):count += 1screen.write('Node %4d: %s\r' % (count, node))Progress(progress_function)
當然,如果你愿意,你可以完全忽略函數的?node
?參數,只打印一個計數,或者任何你想要的內容。
(請注意,這里有一個明顯的后續問題:你如何找到將被評估的節點總數,以便你可以告訴用戶構建離完成有多近?不幸的是,一般情況下,沒有好的方法來做到這一點,除非讓 SCons 兩次評估其依賴圖,第一次計算總數,第二次實際構建目標。這是必要的,因為你無法提前知道用戶實際請求構建的是哪個目標。例如,整個構建可能由數千個節點組成,但用戶可能特別要求只構建單個目標文件。)
9.4. 打印詳細的構建狀態:GetBuildFailures 函數
SCons 和大多數構建工具一樣,構建成功時向 shell 返回狀態碼 0,失敗時返回非零狀態碼。有時在運行結束時提供有關構建狀態的更多信息是很有用的,例如打印一條信息性消息、發送一封電子郵件,或者通知那個破壞構建的可憐家伙。
SCons 提供了?GetBuildFailures
?方法,你可以在 Python 的?atexit
?函數中使用它來獲取一個對象列表,這些對象描述了在嘗試構建目標時失敗的操作。如果你使用?-j
,可能會有多個失敗的操作。下面是一個簡單的示例:
python
運行
import atexitdef print_build_failures():from SCons.Script import GetBuildFailuresfor bf in GetBuildFailures():print("%s failed: %s" % (bf.node, bf.errstr))
atexit.register(print_build_failures)
atexit.register
?調用將?print_build_failures
?注冊為一個?atexit
?回調函數,在 SCons 退出之前被調用。當該函數被調用時,它調用?GetBuildFailures
?來獲取失敗對象的列表。有關返回對象的詳細內容,請參閱手冊頁;一些更有用的屬性是?.node
、.errstr
、.filename
?和?.command
。filename
?不一定與?node
?是同一個文件;node
?是發生錯誤時正在構建的目標,而?filename
?是實際導致錯誤的文件或目錄。注意:只能在構建結束時調用?GetBuildFailures
;在其他任何時候調用它都是未定義的。
下面是一個更完整的示例,展示了如何將?GetBuildFailures
?的每個元素轉換為字符串:
python
運行
# 如果在命令行上傳遞了 fail=1,則使構建失敗
if ARGUMENTS.get('fail', 0):Command('target', 'source', ['/bin/false'])def bf_to_str(bf):"""以有用的方式將 GetBuildFailures() 的一個元素轉換為字符串。"""import SCons.Errorsif bf is None: # 未知目標在列表中產生 Nonereturn '(unknown tgt)'elif isinstance(bf, SCons.Errors.StopError):return str(bf)elif bf.node:return str(bf.node) + ': ' + bf.errstrelif bf.filename:return bf.filename + ': ' + bf.errstrreturn 'unknown failure: ' + bf.errstr
import atexitdef build_status():"""將構建狀態轉換為一個二元組,(status, msg)。"""from SCons.Script import GetBuildFailuresbf = GetBuildFailures()if bf:# bf 通常是一個構建失敗的列表;如果一個元素是 None,# 是因為存在 scons 一無所知的目標。status = 'failed'failures_message = "\n".join(["Failed building %s" % bf_to_str(x)for x in bf if x is not None])else:# 如果 bf 是 None,構建成功完成。status = 'ok'failures_message = ''return (status, failures_message)def display_build_status():"""顯示構建狀態。由 atexit 調用。在這里你可以做各種復雜的事情。"""status, failures_message = build_status()if status == 'failed':print("FAILED!!!!") # 可以顯示警報、發出鈴聲等。elif status == 'ok':print("Build succeeded.")print(failures_message)atexit.register(display_build_status)
當運行此代碼時,你將看到相應的輸出:
plaintext
% scons -Q
scons: `.' is up to date.
Build succeeded.
% scons -Q fail=1
scons: *** [target] Source `source' not found, needed by target `target'.
FAILED!!!!
Failed building target: Source `source' not found, needed by target `target'.
第 10 章。從命令行控制構建
SCons 為 SConscript 文件的編寫者提供了多種方法,讓運行 SCons 的用戶能夠對構建執行進行大量控制。用戶可以在命令行上指定的參數分為以下三種類型:
選項
命令行選項總是以一個或兩個連字符(-
)字符開頭。SCons 提供了一些方法,讓你在 SConscript 文件中檢查和設置選項值,以及定義自己的自定義選項的能力。請參閱下面的 10.1 節 “命令行選項”。
變量
任何包含等號(=
)的命令行參數都被視為形式為?variable=value
?的變量設置。SCons 提供了對所有命令行變量設置的直接訪問,能夠將命令行變量設置應用于構建環境,以及配置特定類型變量(布爾值、路徑名等)的函數,并自動驗證用戶指定的值。請參閱下面的 10.2 節 “命令行 variable=value 構建變量”。
目標
任何既不是選項也不是變量設置(不以連字符開頭且不包含等號)的命令行參數都被視為用戶(大概)希望 SCons 構建的目標。這是一個表示要構建的目標的 Node 對象列表。SCons 提供了對指定目標列表的訪問,以及從 SConscript 文件中設置默認目標列表的方法。請參閱下面的 10.3 節 “命令行目標”。
10.1. 命令行選項
SCons 有許多控制其行為的命令行選項。SCons 命令行選項總是以一個或兩個連字符(-
)字符開頭。
10.1.1. 無需每次都指定命令行選項:SCONSFLAGS 環境變量
用戶可能會發現每次運行 SCons 時都要提供相同的命令行選項。例如,你可能會發現指定?-j 2
?的值以使 SCons 并行運行最多兩個構建命令可以節省時間。為了避免每次都手動輸入?-j 2
,你可以將外部環境變量?SCONSFLAGS
?設置為一個包含你希望 SCons 使用的命令行選項的字符串。
例如,如果你使用的是與 Bourne shell 兼容的 POSIX shell,并且你總是希望 SCons 使用?-Q
?選項,你可以如下設置?SCONSFLAGS
?環境變量:
plaintext
% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...... [build output] ...
scons: done building targets.
% export SCONSFLAGS="-Q"
% scons... [build output] ...
在 POSIX 系統上使用 csh 風格 shell 的用戶可以如下設置?SCONSFLAGS
?環境變量:
plaintext
$ setenv SCONSFLAGS "-Q"
Windows 用戶通常可以在系統屬性窗口的相應選項卡中設置?SCONSFLAGS
。
10.1.2. 獲取由命令行選項設置的值:GetOption 函數
SCons 提供了?GetOption
?函數來獲取由各種命令行選項設置的值。此函數的一個常見用途是檢查是否指定了?-h
?或?--help
?選項。通常,SCons 在讀取所有 SConscript 文件之后才會打印其幫助文本,因為在源樹層次結構中的某個子 SConscript 文件中可能添加了幫助文本。當然,讀取所有 SConscript 文件會花費額外的時間。
如果你知道你的配置在子 SConscript 文件中沒有定義任何額外的幫助文本,你可以通過使用?GetOption
?函數來加快為用戶提供的命令行幫助,僅在用戶未指定?-h
?或?--help
?選項時加載子 SConscript 文件,如下所示:
python
運行
if not GetOption('help'):SConscript('src/SConscript', export='env')
一般來說,你傳遞給?GetOption
?函數以獲取命令行選項設置值的字符串與 “最常見的” 長選項名(以兩個連字符開頭)相同,盡管也有一些例外。SCons 命令行選項列表以及用于獲取這些選項的?GetOption
?字符串,請參閱下面的 10.1.4 節 “用于獲取或設置 SCons 命令行選項值的字符串”。
10.1.3. 設置命令行選項的值:SetOption 函數
你還可以在 SConscript 文件中使用?SetOption
?函數來設置 SCons 命令行選項的值。用于設置 SCons 命令行選項值的字符串,請參閱下面的 10.1.4 節 “用于獲取或設置 SCons 命令行選項值的字符串”。
SetOption
?函數的一個用途是為?-j
?或?--jobs
?選項指定一個值,這樣用戶無需手動指定該選項即可獲得并行構建的性能提升。一個復雜的因素是?-j
?選項的最佳值在一定程度上取決于系統。一個大致的指導原則是,系統的處理器越多,你希望設置的?-j
?值越高,以便利用 CPU 的數量。
例如,假設你的開發系統的管理員已經標準化了將?NUM_CPU
?環境變量設置為每個系統上的處理器數量。一段 Python 代碼可以訪問該環境變量并使用?SetOption
?函數提供適當的靈活性:
python
運行
import os
num_cpu = int(os.environ.get('NUM_CPU', 2))
SetOption('num_jobs', num_cpu)
print("running with -j %s"%GetOption('num_jobs'))
上面的代碼片段將?--jobs
?選項的值設置為?$NUM_CPU
?環境變量中指定的值。(這是字符串拼寫與命令行選項不同的例外情況之一。由于歷史原因,用于獲取或設置?--jobs
?值的字符串是?num_jobs
。)此示例中的代碼打印?num_jobs
?值用于說明目的。它使用默認值 2 來提供一些最小的并行性,即使在單處理器系統上也是如此:
plaintext
% scons -Q
running with -j 2
scons: `.' is up to date.
但是如果設置了?$NUM_CPU
?環境變量,則我們將其用作默認的作業數量:
plaintext
% export NUM_CPU="4"
% scons -Q
running with -j 4
scons: `.' is up to date.
但是用戶在命令行上指定的任何顯式?-j
?或?--jobs
?值將優先使用,無論是否設置了?$NUM_CPU
?環境變量:
plaintext
% scons -Q -j 7
running with -j 7
scons: `.' is up to date.
% export NUM_CPU="4"
% scons -Q -j 3
running with -j 3
scons: `.' is up to date.
10.1.4. 用于獲取或設置 SCons 命令行選項值的字符串
你可以傳遞給?GetOption
?和?SetOption
?函數的字符串通常對應于第一個長格式選項名(以兩個連字符開頭:--
),將任何剩余的連字符字符替換為下劃線。
字符串的完整列表以及它們對應的變量如下:
10.1.5. 添加自定義命令行選項:AddOption 函數
SCons 還允許你使用?AddOption
?函數定義自己的命令行選項。AddOption
?函數接受與標準 Python 庫模塊?optparse
?中的?add_option
?方法相同的參數。
一旦你使用?AddOption
?函數添加了自定義命令行選項,該選項的值(如果有)可以立即使用標準的?GetOption
?函數獲取。目前不支持對使用?AddOption
?添加的選項使用?SetOption
。
使用此功能的一個有用示例是為用戶提供?--prefix
?選項:
python
運行
AddOption('--prefix',dest='prefix',type='string',nargs=1,action='store',metavar='DIR',help='installation prefix')env = Environment(PREFIX = GetOption('prefix'))installed_foo = env.Install('$PREFIX/usr/bin', 'foo.in')
Default(installed_foo)
上面的代碼使用?GetOption
?函數將?$PREFIX
?構建變量設置為用戶使用?--prefix
?命令行選項指定的任何值。因為如果未初始化,$PREFIX
?將擴展為空字符串,所以在沒有?--prefix
?選項的情況下運行 SCons 會將文件安裝到?/usr/bin/
?目錄中:
plaintext
% scons -Q -n
Install file: "foo.in" as "/usr/bin/foo.in"
但是在命令行上指定?--prefix=/tmp/install
?會導致文件安裝到?/tmp/install/usr/bin/
?目錄中:
plaintext
% scons -Q -n --prefix=/tmp/install
Install file: "foo.in" as "/tmp/install/usr/bin/foo.in"
注意:
與長選項用空格分隔的選項參數(而不是用?=
?分隔),scons 無法正確解析。雖然?--input=ARG
?顯然是?opt
后跟?arg
,但對于?--input ARG
,在沒有說明的情況下無法判斷?ARG
?是屬于?input
?選項的參數還是位置參數。scons 將位置參數視為可在 SConscript 中使用的命令行構建選項或命令行目標(有關詳細信息,請參閱緊隨其后的部分)。因此,必須在處理 SConscript 之前收集它們。由于提供解析任何歧義的處理指令的?AddOption
?調用發生在 SConscript 中,scons 無法及時知道以這種方式添加的選項,并且會發生意外情況,例如將選項參數分配為目標和 / 或由于缺少選項參數而引發異常。
因此,在調用 scons 時應避免這種用法。對于單參數選項,在命令行上使用?--input=ARG
?形式。對于多參數選項(nargs
?大于 1),在?AddOption
?調用中設置?nargs
?為 1,并且可以:將選項參數組合成一個帶分隔符的單詞,并在你自己的代碼中解析結果(請參閱內置的?--debug
?選項,該選項允許將多個參數指定為一個用逗號分隔的單詞,以獲取這種用法的示例);或者通過設置?action='append'
?允許多次指定選項。這兩種方法可以同時支持。
10.2. 命令行 variable=value 構建變量
10.2. 命令行 variable=value 構建變量
你可能希望通過允許用戶在命令行上指定 variable=value 的值來控制構建的各個方面。例如,假設你希望用戶能夠通過如下方式運行 SCons 來構建程序的調試版本:
% scons -Q debug=1
SCons 提供了一個 ARGUMENTS 字典,用于存儲來自命令行的所有 variable=value 賦值。這使你能夠根據命令行上的規范修改構建的各個方面。(請注意,除非你希望要求用戶始終指定一個變量,否則你可能需要使用 Python 的 ARGUMENTS.get () 函數,該函數允許你指定一個默認值,以便在命令行上未指定時使用。)
以下代碼根據 ARGUMENTS 字典中設置的 debug 標志來設置 $CCFLAGS 構建變量:
env = Environment()
debug = ARGUMENTS.get('debug', 0)
if int(debug):
env.Append(CCFLAGS = '-g')
env.Program('prog.c')
這會導致當在命令行上使用 debug=1 時使用 -g 編譯器選項:
% scons -Q debug=0
cc -o prog.o -c prog.c
cc -o prog prog.o
% scons -Q debug=0
scons:?.
?是最新的。
% scons -Q debug=1
cc -o prog.o -c -g prog.c
cc -o prog prog.o
% scons -Q debug=1
scons:?.
?是最新的。
請注意,SCons 會跟蹤用于構建目標文件的最后一個值,因此只有當 debug 參數的值發生變化時,才會正確地重新構建目標文件和可執行文件。
ARGUMENTS 字典有兩個小缺點。首先,因為它是一個字典,所以每個指定的關鍵字只能存儲一個值,因此只會 “記住” 命令行上每個關鍵字的最后一個設置。如果用戶應該能夠為給定的關鍵字在命令行上指定多個值,那么 ARGUMENTS 字典就不適用。其次,它不保留指定變量設置的順序,如果希望配置根據命令行上指定構建變量設置的順序表現不同,這就會成為一個問題。
為了滿足這些要求,SCons 提供了一個 ARGLIST 變量,它允許你直接訪問命令行上的 variable=value 設置,并且按照它們被指定的確切順序,同時不會刪除任何重復的設置。ARGUMENTS 變量中的每個元素本身是一個包含關鍵字和設置值的二元列表,你必須遍歷 ARGLIST 的元素,或者以其他方式從中選擇,以按照適合你配置的方式處理你想要的特定設置。例如,以下代碼允許用戶通過在命令行上指定多個 define= 設置來向 CPPDEFINES 構建變量添加內容:
cppdefines = []
for key, value in ARGLIST:
if key == 'define':
cppdefines.append(value)
env = Environment(CPPDEFINES = cppdefines)
env.Object('prog.c')
會產生以下輸出:
% scons -Q define=FOO
cc -o prog.o -c -DFOO prog.c
% scons -Q define=FOO define=BAR
cc -o prog.o -c -DFOO -DBAR prog.c
請注意,ARGLIST 和 ARGUMENTS 變量不會相互干擾,只是提供了不同的視角來查看用戶在命令行上指定的 variable=value 設置。你可以在同一個 SCons 配置中同時使用這兩個變量。一般來說,ARGUMENTS 字典使用起來更方便(因為你可以通過字典訪問來獲取變量設置),而 ARGLIST 列表更靈活(因為你可以檢查用戶命令行變量設置的特定順序)。
10.2.1. 控制命令行構建變量
能夠使用像 debug=1 這樣的命令行構建變量很方便,但編寫特定的 Python 代碼來識別每個這樣的變量、檢查錯誤并提供適當的消息,以及將值應用于構建變量可能會很麻煩。為了幫助解決這個問題,SCons 支持一個類來輕松定義此類構建變量,以及一種將構建變量應用于構建環境的機制。這使你能夠控制構建變量如何影響構建環境。
例如,假設你希望用戶在為發布版本構建程序時在命令行上設置 RELEASE 構建變量,并且該變量的值應該通過適當的 -D 選項(或其他命令行選項)添加到命令行,以便將值傳遞給 C 編譯器。以下是通過在 $CPPDEFINES 構建變量的字典中設置適當的值來實現的方法:
vars = Variables (None, ARGUMENTS)
vars.Add ('RELEASE', ' 設置為 1 以構建發布版本 ', 0)
env = Environment (variables = vars,
CPPDEFINES={'RELEASE_BUILD' : '${RELEASE}'})
env.Program (['foo.c', 'bar.c'])
這個 SConstruct 文件首先創建一個 Variables 對象,它使用來自命令行選項字典 ARGUMENTS 的值(調用 vars = Variables (None, ARGUMENTS))。然后,它使用對象的 Add 方法來指示 RELEASE 變量可以在命令行上設置,并且其默認值為 0(Add 方法的第三個參數)。第二個參數是一行幫助文本;我們將在下一節中學習如何使用它。
然后,我們將創建的 Variables 對象作為 variables 關鍵字參數傳遞給用于創建構建環境的 Environment 調用。這允許用戶在命令行上設置 RELEASE 構建變量,并使該變量顯示在用于從 C 源文件構建每個對象的命令行中:
% scons -Q RELEASE=1
cc -o bar.o -c -DRELEASE_BUILD=1 bar.c
cc -o foo.o -c -DRELEASE_BUILD=1 foo.c
cc -o foo foo.o bar.o
注意:在 SCons 版本 0.98.1 之前,這些構建變量被稱為 “命令行構建選項”。該類實際上被命名為 Options 類,并且在下面的部分中,各種函數被命名為 BoolOption、EnumOption、ListOption、PathOption、PackageOption 和 AddOptions。這些舊名稱仍然有效,并且你可能會在較舊的 SConscript 文件中遇到它們,但自 SCons 2.0 版本起它們已被正式棄用。
10.2.2. 為命令行構建變量提供幫助
為了使命令行構建變量最有用,理想情況下,你希望提供一些幫助文本,以便在用戶運行 scons -h 時描述可用的變量。你可以手動編寫此文本,但 SCons 提供了一種更簡單的方法。Variables 對象支持 GenerateHelpText 方法,顧名思義,該方法會生成描述已添加到其中的各種變量的文本。然后,你將此方法的輸出傳遞給 Help 函數:
vars = Variables (None, ARGUMENTS)
vars.Add ('RELEASE', ' 設置為 1 以構建發布版本 ', 0)
env = Environment (variables = vars)
Help (vars.GenerateHelpText (env))
現在,當使用 -h 選項時,SCons 將顯示一些有用的文本:
% scons -Q -h
RELEASE:設置為 1 以構建發布版本
默認值:0
當前值:0
使用 scons -H 獲取有關命令行選項的幫助。
10.2.3. 從文件讀取構建變量
為用戶提供一種在命令行上指定構建變量值的方法很有用,但如果用戶每次運行 SCons 時都必須指定該變量,可能會很麻煩。我們可以通過在創建 Variables 對象時提供文件名,讓用戶在本地文件中提供自定義的構建變量設置:
vars = Variables ('custom.py')
vars.Add ('RELEASE', ' 設置為 1 以構建發布版本 ', 0)
env = Environment (variables = vars,
CPPDEFINES={'RELEASE_BUILD' : '${RELEASE}'})
env.Program (['foo.c', 'bar.c'])
Help (vars.GenerateHelpText (env))
然后,用戶可以通過在 custom.py 文件中設置它來控制 RELEASE 變量:
RELEASE = 1
請注意,這個文件實際上像 Python 腳本一樣被執行。現在當我們運行 SCons 時:
% scons -Q
cc -o bar.o -c -DRELEASE_BUILD=1 bar.c
cc -o foo.o -c -DRELEASE_BUILD=1 foo.c
cc -o foo foo.o bar.o
如果我們將 custom.py 的內容更改為:
RELEASE = 0
目標文件將使用新變量進行適當的重新構建:
% scons -Q
cc -o bar.o -c -DRELEASE_BUILD=0 bar.c
cc -o foo.o -c -DRELEASE_BUILD=0 foo.c
cc -o foo foo.o bar.o
10.2.4. 預定義的構建變量函數
SCons 提供了許多函數,為各種類型的命令行構建變量提供現成的行為。
10.2.4.1. 布爾值:BoolVariable 構建變量函數
能夠指定一個控制簡單布爾變量(值為真或假)的變量通常很方便。如果能夠適應不同用戶對表示真或假值的不同偏好,那就更方便了。BoolVariable 函數可以輕松適應這些常見的真或假表示。
BoolVariable 函數接受三個參數:構建變量的名稱、構建變量的默認值以及變量的幫助字符串。然后它返回適當的信息,以便傳遞給 Variables 對象的 Add 方法,如下所示:
vars = Variables ('custom.py')
vars.Add (BoolVariable ('RELEASE', ' 設置為構建發布版本 ', 0))
env = Environment (variables = vars,
CPPDEFINES={'RELEASE_BUILD' : '${RELEASE}'})
env.Program ('foo.c')
有了這個構建變量,現在可以通過將 RELEASE 變量設置為 yes 或 t 來啟用它:
% scons -Q RELEASE=yes foo.o
cc -o foo.o -c -DRELEASE_BUILD=True foo.c
% scons -Q RELEASE=t foo.o
cc -o foo.o -c -DRELEASE_BUILD=True foo.c
其他等同于真的值包括 y、1、on 和 all。
相反,現在可以通過將 RELEASE 設置為 no 或 f 來賦予其假值:
% scons -Q RELEASE=no foo.o
cc -o foo.o -c -DRELEASE_BUILD=False foo.c
% scons -Q RELEASE=f foo.o
cc -o foo.o -c -DRELEASE_BUILD=False foo.c
其他等同于假的值包括 n、0、off 和 none。
最后,如果用戶嘗試指定任何其他值,SCons 將提供適當的錯誤消息:
% scons -Q RELEASE=bad_value foo.o
scons: *** 轉換選項時出錯:RELEASE
布爾選項的無效值:bad_value
文件 “/home/my/project/SConstruct”,第 4 行,在 <module> 中
10.2.4.2. 從列表中選擇單個值:EnumVariable 構建變量函數
假設我們希望用戶能夠設置一個 COLOR 變量,用于選擇應用程序顯示的背景顏色,但我們希望將選擇限制在一組特定的允許顏色中。可以使用 EnumVariable 輕松設置,它除了變量名稱、默認值和幫助文本參數外,還接受一個 allowed_values 列表:
vars = Variables ('custom.py')
vars.Add (EnumVariable ('COLOR', ' 設置背景顏色 ', 'red',
allowed_values=('red', 'green', 'blue')))
env = Environment (variables = vars,
CPPDEFINES={'COLOR' : '"${COLOR}"'})
env.Program ('foo.c')
現在用戶可以明確地將 COLOR 構建變量設置為任何指定的允許值:
% scons -Q COLOR=red foo.o
cc -o foo.o -c -DCOLOR="red" foo.c
% scons -Q COLOR=blue foo.o
cc -o foo.o -c -DCOLOR="blue" foo.c
% scons -Q COLOR=green foo.o
cc -o foo.o -c -DCOLOR="green" foo.c
但幾乎更重要的是,嘗試將 COLOR 設置為不在列表中的值會生成錯誤消息:
% scons -Q COLOR=magenta foo.o
scons: *** 選項 COLOR 的值無效:magenta。有效值為:('red', 'green', 'blue')
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中
EnumVariable 函數還支持一種將備用名稱映射到允許值的方法。例如,假設我們希望允許用戶使用單詞 navy 作為 blue 的同義詞。我們可以通過添加一個映射字典來實現,該字典將其鍵值映射到所需的合法值:
vars = Variables ('custom.py')
vars.Add (EnumVariable ('COLOR', ' 設置背景顏色 ', 'red',
allowed_values=('red', 'green', 'blue'),
map={'navy':'blue'}))
env = Environment (variables = vars,
CPPDEFINES={'COLOR' : '"${COLOR}"'})
env.Program ('foo.c')
如預期的那樣,用戶可以在命令行上使用 navy,并且 SCons 在使用 COLOR 變量構建目標時會將其轉換為 blue:
% scons -Q COLOR=navy foo.o
cc -o foo.o -c -DCOLOR="blue" foo.c
默認情況下,使用 EnumVariable 函數時,與合法值僅在大小寫方面不同的參數被視為非法值:
% scons -Q COLOR=Red foo.o
scons: *** 選項 COLOR 的值無效:Red。有效值為:('red', 'green', 'blue')
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中
% scons -Q COLOR=BLUE foo.o
scons: *** 選項 COLOR 的值無效:BLUE。有效值為:('red', 'green', 'blue')
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中
% scons -Q COLOR=nAvY foo.o
scons: *** 選項 COLOR 的值無效:nAvY。有效值為:('red', 'green', 'blue')
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中
EnumVariable 函數可以接受一個額外的 ignorecase 關鍵字參數,當設置為 1 時,告訴 SCons 在指定值時允許大小寫差異:
vars = Variables ('custom.py')
vars.Add (EnumVariable ('COLOR', ' 設置背景顏色 ', 'red',
allowed_values=('red', 'green', 'blue'),
map={'navy':'blue'},
ignorecase=1))
env = Environment (variables = vars,
CPPDEFINES={'COLOR' : '"${COLOR}"'})
env.Program ('foo.c')
會產生以下輸出:
% scons -Q COLOR=Red foo.o
cc -o foo.o -c -DCOLOR="Red" foo.c
% scons -Q COLOR=BLUE foo.o
cc -o foo.o -c -DCOLOR="BLUE" foo.c
% scons -Q COLOR=nAvY foo.o
cc -o foo.o -c -DCOLOR="blue" foo.c
% scons -Q COLOR=green foo.o
cc -o foo.o -c -DCOLOR="green" foo.c
請注意,ignorecase 值為 1 時會保留用戶提供的大小寫拼寫。如果你希望 SCons 無論用戶使用的大小寫如何都將名稱轉換為小寫,請指定 ignorecase 值為 2:
vars = Variables ('custom.py')
vars.Add (EnumVariable ('COLOR', ' 設置背景顏色 ', 'red',
allowed_values=('red', 'green', 'blue'),
map={'navy':'blue'},
ignorecase=2))
env = Environment (variables = vars,
CPPDEFINES={'COLOR' : '"${COLOR}"'})
env.Program ('foo.c')
現在 SCons 將使用 red、green 或 blue 的值,無論用戶在命令行上如何拼寫這些值:
% scons -Q COLOR=Red foo.o
cc -o foo.o -c -DCOLOR="red" foo.c
% scons -Q COLOR=nAvY foo.o
cc -o foo.o -c -DCOLOR="blue" foo.c
% scons -Q COLOR=GREEN foo.o
cc -o foo.o -c -DCOLOR="green" foo.c
10.2.4.3. 從列表中選擇多個值:ListVariable 構建變量函數
你可能希望允許用戶控制構建變量的另一種方式是指定一個或多個合法值的列表。SCons 通過 ListVariable 函數支持這種方式。例如,如果我們希望用戶能夠將 COLORS 變量設置為一個或多個合法值列表中的值:
vars = Variables ('custom.py')
vars.Add (ListVariable ('COLORS', ' 顏色列表 ', 0,
['red', 'green', 'blue']))
env = Environment (variables = vars,
CPPDEFINES={'COLORS' : '"${COLORS}"'})
env.Program ('foo.c')
現在用戶可以指定一個用逗號分隔的合法值列表,這些值將被轉換為用空格分隔的列表,以便傳遞給任何構建命令:
% scons -Q COLORS=red,blue foo.o
cc -o foo.o -c -DCOLORS="red blue" foo.c
% scons -Q COLORS=blue,green,red foo.o
cc -o foo.o -c -DCOLORS="blue green red" foo.c
此外,ListVariable 函數允許用戶分別指定 all 或 none 關鍵字來選擇所有合法值或不選擇任何值:
% scons -Q COLORS=all foo.o
cc -o foo.o -c -DCOLORS="red green blue" foo.c
% scons -Q COLORS=none foo.o
cc -o foo.o -c -DCOLORS="" foo.c
當然,非法值仍然會生成錯誤消息:
% scons -Q COLORS=magenta foo.o
scons: *** 轉換選項時出錯:COLORS
選項的無效值:magenta
文件 “/home/my/project/SConstruct”,第 5 行,在 <module> 中
10.2.4.4. 路徑名:PathVariable 構建變量函數
10.2.4.4. 路徑名稱:PathVariable 構建變量函數
SCons 支持 PathVariable 函數,以便輕松創建一個構建變量來控制預期的路徑名稱。例如,如果您需要在預處理器中定義一個變量來控制配置文件的位置:
vars = Variables ('custom.py')
vars.Add (PathVariable ('CONFIG',
' 配置文件的路徑 ',
'/etc/my_config'))
env = Environment (variables = vars,
CPPDEFINES={'CONFIG_FILE' : '"$CONFIG"'})
env.Program ('foo.c')
這樣一來,用戶可以根據需要在命令行上覆蓋 CONFIG 構建變量:
% scons -Q foo.o
cc -o foo.o -c -DCONFIG_FILE="/etc/my_config" foo.c
% scons -Q CONFIG=/usr/local/etc/other_config foo.o
scons: `foo.o' 是最新的。
默認情況下,PathVariable 會檢查指定的路徑是否存在,如果不存在則生成一個錯誤:
% scons -Q CONFIG=/does/not/exist foo.o
scons: *** 選項 CONFIG 的路徑不存在: /does/not/exist
文件 "/home/my/project/SConstruct", 第 6 行,在 <module> 中
PathVariable 提供了一些方法,您可以使用這些方法來改變這種行為。如果您希望確保任何指定的路徑實際上是文件而不是目錄,可以使用 PathVariable.PathIsFile 方法:
vars = Variables ('custom.py')
vars.Add (PathVariable ('CONFIG',
' 配置文件的路徑 ',
'/etc/my_config',
PathVariable.PathIsFile))
env = Environment (variables = vars,
CPPDEFINES={'CONFIG_FILE' : '"$CONFIG"'})
env.Program ('foo.c')
相反,為了確保任何指定的路徑是目錄而不是文件,可以使用 PathVariable.PathIsDir 方法:
vars = Variables ('custom.py')
vars.Add (PathVariable ('DBDIR',
' 數據庫目錄的路徑 ',
'/var/my_dbdir',
PathVariable.PathIsDir))
env = Environment (variables = vars,
CPPDEFINES={'DBDIR' : '"$DBDIR"'})
env.Program ('foo.c')
如果您希望確保任何指定的路徑是目錄,并且如果目錄不存在則創建該目錄,可以使用 PathVariable.PathIsDirCreate 方法:
vars = Variables ('custom.py')
vars.Add (PathVariable ('DBDIR',
' 數據庫目錄的路徑 ',
'/var/my_dbdir',
PathVariable.PathIsDirCreate))
env = Environment (variables = vars,
CPPDEFINES={'DBDIR' : '"$DBDIR"'})
env.Program ('foo.c')
最后,如果您不在乎路徑是否存在,是文件還是目錄,可以使用 PathVariable.PathAccept 方法來接受用戶提供的任何路徑:
vars = Variables ('custom.py')
vars.Add (PathVariable ('OUTPUT',
' 輸出文件或目錄的路徑 ',
None,
PathVariable.PathAccept))
env = Environment (variables = vars,
CPPDEFINES={'OUTPUT' : '"$OUTPUT"'})
env.Program ('foo.c')
10.2.4.5. 啟用 / 禁用路徑名稱:PackageVariable 構建變量函數
有時,您希望給予用戶對路徑名稱變量更多的控制權,除了允許他們提供顯式的路徑名稱外,還允許他們使用 yes 或 no 關鍵字來顯式啟用或禁用路徑名稱。SCons 支持 PackageVariable 函數來實現這一點:
vars = Variables ('custom.py')
vars.Add (PackageVariable ('PACKAGE',
' 包的位置 ',
'/opt/location'))
env = Environment (variables = vars,
CPPDEFINES={'PACKAGE' : '"$PACKAGE"'})
env.Program ('foo.c')
當 SConscript 文件使用 PackageVariable 函數時,用戶現在仍然可以使用默認值或提供覆蓋的路徑名稱,但現在可以顯式地將指定的變量設置為一個值,該值表示應該啟用(在這種情況下使用默認值)或禁用該包:
% scons -Q foo.o
cc -o foo.o -c -DPACKAGE="/opt/location" foo.c
% scons -Q PACKAGE=/usr/local/location foo.o
cc -o foo.o -c -DPACKAGE="/usr/local/location" foo.c
% scons -Q PACKAGE=yes foo.o
cc -o foo.o -c -DPACKAGE="True" foo.c
% scons -Q PACKAGE=no foo.o
cc -o foo.o -c -DPACKAGE="False" foo.c
10.2.5. 一次性添加多個命令行構建變量
最后,SCons 提供了一種方法,可以一次性向 Variables 對象添加多個構建變量。您無需多次調用 Add 方法,而是可以使用 AddVariables 方法,并傳入要添加到對象的構建變量列表。每個構建變量可以指定為一個參數元組(就像您傳遞給 Add 方法本身的參數一樣),或者指定為對預打包的命令行構建變量的預定義函數的調用。順序任意:
vars = Variables ()
vars.AddVariables (
('RELEASE', ' 設置為 1 以構建發布版本 ', 0),
('CONFIG', ' 配置文件 ', '/etc/my_config'),
BoolVariable ('warnings', ' 使用 - Wall 等進行編譯 ', 1),
EnumVariable ('debug', ' 調試輸出和符號 ', 'no',
allowed_values=('yes', 'no', 'full'),
map={}, ignorecase=0), # 區分大小寫
ListVariable ('shared',
' 要構建為共享庫的庫 ',
'all',
names = list_of_libs),
PackageVariable ('x11',
' 使用在此處安裝的 X11(yes 表示在某些位置搜索)',
'yes'),
PathVariable ('qtdir', 'Qt 的安裝根目錄位置 ', qtdir),
)
10.2.6. 處理未知的命令行構建變量:UnknownVariables 函數
當然,用戶偶爾可能會在命令行設置中拼錯變量名。對于用戶在命令行上指定的任何未知變量,SCons 不會生成錯誤或警告。(這在很大程度上是因為您可能會直接使用 ARGUMENTS 字典來處理參數,因此在一般情況下,SCons 無法知道給定的 “拼錯” 的變量是否真的是未知的,是否存在潛在問題,或者您的 SConscript 文件是否會用一些 Python 代碼直接處理它。)
然而,如果您使用 Variables 對象來定義一組特定的命令行構建變量,并期望用戶能夠設置這些變量,那么如果用戶提供的變量設置不在 Variables 對象已知的已定義變量名列表中,您可能希望提供自己的錯誤消息或警告。您可以通過調用 Variables 對象的 UnknownVariables 方法來實現這一點:
vars = Variables (None)
vars.Add ('RELEASE', ' 設置為 1 以構建發布版本 ', 0)
env = Environment (variables = vars,
CPPDEFINES={'RELEASE_BUILD' : '${RELEASE}'})
unknown = vars.UnknownVariables ()
if unknown:
print ("Unknown variables: % s"% unknown.keys ())
Exit (1)
env.Program ('foo.c')
UnknownVariables 方法返回一個字典,其中包含用戶在命令行上指定的、不在 Variables 對象已知的已定義變量(通過 Variables 對象的 Add 方法指定的變量)列表中的任何變量的關鍵字和值。在上面的示例中,我們檢查 UnknownVariables 返回的字典是否非空,如果是,則打印包含未知變量名稱的 Python 列表,然后調用 Exit 函數來終止 SCons:
% scons -Q NOT_KNOWN=foo
Unknown variables: ['NOT_KNOWN']
當然,您可以以任何適合您的構建配置的方式處理 UnknownVariables 函數返回的字典中的項,包括僅打印警告消息而不退出、在某個地方記錄錯誤等。
請注意,您必須在使用 Environment 調用的 variables = 關鍵字參數將 Variables 對象應用于構建環境之后,再調用 UnknownVariables。
10.3. 命令行目標
10.3.1. 獲取命令行目標:COMMAND_LINE_TARGETS 變量
SCons 支持 COMMAND_LINE_TARGETS 變量,讓您可以獲取用戶在命令行上指定的目標列表。您可以以任何您希望的方式使用這些目標來操作構建過程。舉個簡單的例子,假設您希望在構建特定程序時向用戶打印一個提醒。您可以通過檢查 COMMAND_LINE_TARGETS 列表中的目標來實現這一點:
if 'bar' in COMMAND_LINE_TARGETS:
print("Don't forget to copy `bar' to the archive!")
Default(Program('foo.c'))
Program('bar.c')
然后,使用默認目標運行 SCons 時,它會像往常一樣工作,但在命令行上顯式指定 bar 目標時會生成警告消息:
% scons -Q
cc -o foo.o -c foo.c
cc -o foo foo.o
% scons -Q bar
Don't forget to copy `bar' to the archive!
cc -o bar.o -c bar.c
cc -o bar bar.o
COMMAND_LINE_TARGETS 變量的另一個實際用途可能是,如果請求了特定目標,則僅讀取某些子 SConscript 文件來加速構建過程。
10.3.2. 控制默認目標:Default 函數
您可以控制 SCons 默認構建的目標,也就是說,當命令行上沒有指定目標時。如前所述,除非您在命令行上顯式指定一個或多個目標,否則 SCons 通常會構建當前目錄中或其下的每個目標。然而,有時您可能希望指定默認情況下只構建某些程序或某些目錄中的程序。您可以使用 Default 函數來實現這一點:
env = Environment()
hello = env.Program('hello.c')
env.Program('goodbye.c')
Default(hello)
這個 SConstruct 文件知道如何構建兩個程序,hello 和 goodbye,但默認情況下只構建 hello 程序:
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
scons: `hello' 是最新的。
% scons -Q goodbye
cc -o goodbye.o -c goodbye.c
cc -o goodbye goodbye.o
請注意,即使您在 SConstruct 文件中使用了 Default 函數,您仍然可以在命令行上顯式指定當前目錄 (.),告訴 SCons 構建當前目錄中(或其下)的所有內容:
% scons -Q .
cc -o goodbye.o -c goodbye.c
cc -o goodbye goodbye.o
cc -o hello.o -c hello.c
cc -o hello hello.o
您也可以多次調用 Default 函數,在這種情況下,每次調用都會將目標添加到默認構建的目標列表中:
env = Environment()
prog1 = env.Program('prog1.c')
Default(prog1)
prog2 = env.Program('prog2.c')
prog3 = env.Program('prog3.c')
Default(prog3)
或者,您可以在對 Default 函數的單次調用中指定多個目標:
env = Environment()
prog1 = env.Program('prog1.c')
prog2 = env.Program('prog2.c')
prog3 = env.Program('prog3.c')
Default(prog1, prog3)
這最后兩個示例中的任何一個默認情況下都只會構建 prog1 和 prog3 程序:
% scons -Q
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
cc -o prog3.o -c prog3.c
cc -o prog3 prog3.o
% scons -Q .
cc -o prog2.o -c prog2.c
cc -o prog2 prog2.o
您可以將一個目錄作為參數傳遞給 Default:
env = Environment()
env.Program(['prog1/main.c', 'prog1/foo.c'])
env.Program(['prog2/main.c', 'prog2/bar.c'])
Default('prog1')
在這種情況下,默認情況下只會構建該目錄中的目標:
% scons -Q
cc -o prog1/foo.o -c prog1/foo.c
cc -o prog1/main.o -c prog1/main.c
cc -o prog1/main prog1/main.o prog1/foo.o
% scons -Q
scons: `prog1' 是最新的。
% scons -Q .
cc -o prog2/bar.o -c prog2/bar.c
cc -o prog2/main.o -c prog2/main.c
cc -o prog2/main prog2/main.o prog2/bar.o
最后,如果由于某種原因您不希望默認構建任何目標,您可以使用 Python 的 None 變量:
env = Environment()
prog1 = env.Program('prog1.c')
prog2 = env.Program('prog2.c')
Default(None)
這將產生如下的構建輸出:
% scons -Q
scons: *** 沒有指定目標,也沒有找到 Default () 目標。停止。
未找到要構建的內容
% scons -Q .
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
cc -o prog2.o -c prog2.c
cc -o prog2 prog2.o
10.3.2.1. 獲取默認目標列表:DEFAULT_TARGETS 變量
SCons 支持 DEFAULT_TARGETS 變量,讓您可以獲取通過調用 Default 函數或方法指定的當前默認目標列表。DEFAULT_TARGETS 變量與 COMMAND_LINE_TARGETS 變量有兩個重要區別。首先,DEFAULT_TARGETS 變量是一個 SCons 內部節點的列表,所以如果您想打印它們或查找特定的目標名稱,需要將列表元素轉換為字符串。您可以通過在列表推導式中調用 str 函數輕松實現這一點:
prog1 = Program('prog1.c')
Default(prog1)
print("DEFAULT_TARGETS is %s" % [str(t) for t in DEFAULT_TARGETS])
(請記住,對 DEFAULT_TARGETS 列表的所有操作都發生在 SCons 讀取 SConscript 文件的第一階段,如果我們在運行 SCons 時省略 -Q 標志,這一點會很明顯:)
% scons
scons: 正在讀取 SConscript 文件...
DEFAULT_TARGETS is ['prog1']
scons: 完成讀取 SConscript 文件。
scons: 正在構建目標...
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
scons: 完成構建目標。
其次,DEFAULT_TARGETS 列表的內容會根據對 Default 函數的調用而變化,從下面的 SConstruct 文件中可以看出:
prog1 = Program('prog1.c')
Default(prog1)
print("DEFAULT_TARGETS is now %s" % [str(t) for t in DEFAULT_TARGETS])
prog2 = Program('prog2.c')
Default(prog2)
print("DEFAULT_TARGETS is now %s" % [str(t) for t in DEFAULT_TARGETS])
這將產生如下輸出:
% scons
scons: 正在讀取 SConscript 文件...
DEFAULT_TARGETS is now ['prog1']
DEFAULT_TARGETS is now ['prog1', 'prog2']
scons: 完成讀取 SConscript 文件。
scons: 正在構建目標...
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
cc -o prog2.o -c prog2.c
cc -o prog2 prog2.o
scons: 完成構建目標。
實際上,這意味著您需要注意調用 Default 函數和引用 DEFAULT_TARGETS 列表的順序,以確保在添加您期望在其中找到的默認目標之前,不會檢查該列表。
10.3.3. 獲取構建目標列表,無論目標來源如何:BUILD_TARGETS 變量
我們已經介紹了 COMMAND_LINE_TARGETS 變量,它包含在命令行上指定的目標列表,以及 DEFAULT_TARGETS 變量,它包含通過調用 Default 方法或函數指定的目標列表。然而,有時您想要一個 SCons 將嘗試構建的目標列表,無論這些目標是來自命令行還是 Default 調用。您可以手動編寫代碼實現,如下所示:
if COMMAND_LINE_TARGETS:
targets = COMMAND_LINE_TARGETS
else:
targets = DEFAULT_TARGETS
不過,SCons 提供了一個方便的 BUILD_TARGETS 變量,無需進行這種手動操作。本質上,BUILD_TARGETS 變量包含命令行目標的列表(如果有指定的話),如果沒有指定命令行目標,它包含通過 Default 方法或函數指定的目標列表。
因為 BUILD_TARGETS 可能包含一個 SCons 節點的列表,所以如果您想打印它們或查找特定的目標名稱,必須將列表元素轉換為字符串,就像 DEFAULT_TARGETS 列表一樣:
prog1 = Program('prog1.c')
Program('prog2.c')
Default(prog1)
print ("BUILD_TARGETS is %s" % [str(t) for t in BUILD_TARGETS])
請注意,BUILD_TARGETS 的值會根據命令行上是否指定了目標而變化 —— 只有在沒有 COMMAND_LINE_TARGETS 時,BUILD_TARGETS 才會從 DEFAULT_TARGETS 中獲取目標:
% scons -Q
BUILD_TARGETS is ['prog1']
cc -o prog1.o -c prog1.c
cc -o prog1 prog1.o
% scons -Q prog2
BUILD_TARGETS is ['prog2']
cc -o prog2.o -c prog2.c
cc -o prog2 prog2.o
% scons -Q -c .
BUILD_TARGETS is ['.']
刪除 prog1.o
刪除 prog1
刪除 prog2.o
刪除 prog2
[2] 實際上,AddOption 函數是使用 optparse.OptionParser 的一個子類來實現的。
第11章 向其他目錄安裝文件:安裝構建器
程序構建完成后,通常需要將其安裝到其他目錄供公共使用。通過Install方法可將程序或其他文件復制到目標目錄:
```python
env = Environment()
hello = env.Program('hello.c')
env.Install('/usr/bin', hello)
```
需注意,安裝文件仍被視為一種"構建"行為。由于SCons默認在當前目錄及其子目錄下構建文件,若如示例所示需將文件安裝到頂層SConstruct目錄樹之外的路徑(如/usr/bin),必須顯式指定目標目錄(或更高級目錄如/):
```bash
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q /usr/bin
Install file: "hello" as "/usr/bin/hello"
```
為避免記憶(和輸入)具體安裝路徑的繁瑣,可使用Alias函數創建偽目標。例如定義名為install的別名指向目標目錄:
```python
env = Environment()
hello = env.Program('hello.c')
env.Install('/usr/bin', hello)
env.Alias('install', '/usr/bin')
```
此時可通過更自然的方式安裝程序:
```bash
% scons -Q install
Install file: "hello" as "/usr/bin/hello"
```
11.1 向目錄安裝多個文件
多次調用Install函數即可安裝多個文件:
```python
env = Environment()
hello = env.Program('hello.c')
goodbye = env.Program('goodbye.c')
env.Install('/usr/bin', hello)
env.Install('/usr/bin', goodbye)
```
或更簡潔地使用文件列表(與其他構建器用法一致):
```python
env.Install('/usr/bin', [hello, goodbye])
```
兩種方式執行效果:
```bash
% scons -Q install
Install file: "goodbye" as "/usr/bin/goodbye"
Install file: "hello" as "/usr/bin/hello"
```
11.2 重命名安裝文件
Install方法默認保留原文件名。如需重命名,應使用InstallAs函數:
```python
env.InstallAs('/usr/bin/hello-new', hello)
```
安裝效果:
```bash
% scons -Q install
Install file: "hello" as "/usr/bin/hello-new"
```
11.3 批量重命名安裝文件
安裝多個重命名文件時,可多次調用InstallAs或使用等長列表:
```python
env.InstallAs(['/usr/bin/hello-new', '/usr/bin/goodbye-new'],
????????????? [hello, goodbye])
```
執行時將按順序對應復制:
```bash
% scons -Q install
Install file: "goodbye" as "/usr/bin/goodbye-new"
Install file: "hello" as "/usr/bin/hello-new"
```
11.4 安裝版本化共享庫
當通過SHLIBVERSION變量創建共享庫時,SCons會自動生成版本符號鏈接。此時應使用InstallVersionedLib安裝庫及其鏈接:
```python
foo = env.SharedLibrary("foo", "foo.c", SHLIBVERSION="1.2.3")
env.InstallVersionedLib("lib", foo)
```
在Linux系統將生成libfoo.so.1.2.3及指向它的libfoo.so和libfoo.so.1符號鏈接。
第12章 跨平臺文件系統操作
SCons提供多種跨平臺操作工廠函數,用于復制/移動/刪除文件目錄等操作。這些工廠函數返回可延遲執行的動作對象。
12.1 復制文件/目錄:Copy工廠
使用Copy工廠與Command構建器結合可實現文件復制:
```python
Command("file.out", "file.in", Copy("$TARGET", "$SOURCE"))
```
執行時自動展開變量:
```bash
% scons -Q
Copy("file.out", "file.in")
```
也可顯式指定路徑:
```python
Command("file.out", [], Copy("$TARGET", "file.in"))
```
Copy工廠特別適用于多步驟操作,例如處理需修改原文件的工具:
```python
Command("file.out", "file.in", [
??? Copy("tempfile", "$SOURCE"),
??? "modify tempfile",
??? Copy("$TARGET", "tempfile")
])
```
輸出序列:
```bash
Copy("tempfile", "file.in")
modify tempfile
Copy("file.out", "tempfile")
```
第三個參數控制符號鏈接復制方式:
- `True`:淺復制為新鏈接
- `False`:復制鏈接目標實體
12.2 刪除文件/目錄:Delete工廠
Delete工廠用法類似Copy,可用于清理臨時文件:
```python
Command("file.out", "file.in", [
??? Delete("tempfile"),
??? Copy("tempfile", "$SOURCE"),
??? "modify tempfile",
??? Copy("$TARGET", "tempfile")
])
```
執行流程:
```bash
Delete("tempfile")
Copy("tempfile", "file.in")
modify tempfile
Copy("file.out", "tempfile")
```
注意:SCons默認會在執行操作前刪除目標文件,通常無需顯式調用Delete。特別要避免誤用`Delete("$SOURCE")`。
12.3 移動/重命名文件:Move工廠
Move工廠可實現文件移動:
```python
Command("file.out", "file.in", [
??? Copy("tempfile", "$SOURCE"),
??? "modify tempfile",
??? Move("$TARGET", "tempfile")
])
```
執行效果:
```bash
Copy("tempfile", "file.in")
modify tempfile
Move("file.out", "tempfile")
```
12.4 更新文件時間戳:Touch工廠
Touch工廠用于更新文件修改時間:
```python
Command("file.out", "file.in", [
??? Copy("$TARGET", "$SOURCE"),
??? Touch("$TARGET")
])
```
輸出:
```bash
Copy("file.out", "file.in")
Touch("file.out")
```
12.5 創建目錄:Mkdir工廠
Mkdir工廠創建目錄,適合臨時工作區場景:
```python
Command("file.out", "file.in", [
??? Delete("tempdir"),
??? Mkdir("tempdir"),
??? Copy("tempdir/${SOURCE.file}", "$SOURCE"),
??? "process tempdir",
??? Move("$TARGET", "tempdir/output_file"),
??? Delete("tempdir")
])
```
12.6 修改文件權限:Chmod工廠
Chmod工廠修改權限,參數應為八進制數:
```python
Command("file.out", "file.in", [
??? Copy("$TARGET", "$SOURCE"),
??? Chmod("$TARGET", 0o755)
])
```
執行:
```bash
Copy("file.out", "file.in")
Chmod("file.out", 0755)
```
12.7 立即執行操作:Execute函數
Execute函數可在讀取SConscript時立即執行操作,例如確保目錄存在:
```python
Execute(Mkdir('/tmp/my_temp_directory'))
```
輸出顯示立即執行:
```bash
scons: Reading SConscript files...
Mkdir("/tmp/my_temp_directory")
```
相比直接調用os.mkdir(),此方式會正確處理`-n`/`-q`參數。若需檢查執行狀態可通過返回值判斷:
```python
if Execute(Mkdir('/tmp/my_temp_directory')):
??? Exit(1)
```
[注3] 因歷史原因Copy被用于環境復制函數,否則本應是實現文件復構建器的理想名稱。
第13章 控制目標文件刪除
SCons在兩種情況下會刪除目標文件:1) 重建前刪除舊版本 2) 使用`-c`清理時。Precious和NoClean函數可分別抑制這兩種行為。
13.1 防止構建時刪除:Precious函數
默認情況下SCons在重建前刪除目標。對于需增量更新的庫文件,可通過Precious保護:
```python
env = Environment(RANLIBCOM='')
lib = env.Library('foo', ['f1.c', 'f2.c', 'f3.c'])
env.Precious(lib)
```
此時SCons不會刪除原有庫文件:
```bash
ar rc libfoo.a f1.o f2.o f3.o
```
但`-c`清理時仍會刪除Precious標記的文件。
13.2 防止清理時刪除:NoClean函數
NoClean可防止`-c`清理時刪除指定目標,例如保留最終庫文件:
```python
env.NoClean(lib)
```
清理時將保留庫文件:
```bash
Removed f1.o
Removed f2.o
Removed f3.o
```
13.3 清理附加文件:Clean函數
Clean函數可注冊需隨目標清理的附加文件(如日志文件):
```python
t = Command('foo.out', 'foo.in', 'build -o $TARGET $SOURCE')
Clean(t, 'foo.log')
```
清理時將同步刪除:
```bash
Removed foo.out
Removed foo.log
```
第14章 分層構建
大型軟件項目的源代碼通常不會集中存放在單一目錄中,而是采用分層目錄結構組織。使用SCons管理此類項目時,需要通過SConscript函數創建分層構建腳本體系。
14.1 SConscript腳本文件
項目頂層的構建腳本名為SConstruct。頂層腳本可通過SConscript函數引入下級構建腳本,這些下級腳本同樣可以繼續引入更底層的腳本。按照慣例,這些下級構建腳本通常命名為SConscript。例如:
```python
SConscript([
??? 'drivers/display/SConscript',
??? 'drivers/mouse/SConscript',
??? 'parser/SConscript',
??? 'utilities/SConscript'
])
```
此例中SConstruct顯式列出了所有下級腳本。也可采用中間層SConscript文件組織,例如drivers目錄下的中間腳本:
```python
SConscript([
??? 'display/SConscript',
??? 'mouse/SConscript'
])
```
具體采用何種組織方式,可根據項目需求靈活選擇。
14.2 路徑相對于SConscript所在目錄
下級SConscript文件中所有路徑均相對于該腳本所在目錄進行解析。這種機制使得構建規則可以與源代碼保持在相同目錄層級,便于維護。例如:
頂層SConstruct:
```python
SConscript(['prog1/SConscript', 'prog2/SConscript'])
```
prog1/SConscript內容:
```python
env = Environment()
env.Program('prog1', ['main.c', 'foo1.c', 'foo2.c'])
```
構建時SCons始終在頂層目錄執行:
```bash
cc -o prog1/foo1.o -c prog1/foo1.c
cc -o prog1/prog1 prog1/main.o prog1/foo1.o prog1/foo2.o
```
14.3 使用頂層相對路徑
在子目錄腳本中引用其他目錄文件時,可通過`#`前綴指定相對于頂層目錄的路徑:
```python
env.Program('prog', ['main.c', '#lib/foo1.c', 'foo2.c'])
```
14.4 共享構建環境
為避免在每個子腳本重復初始化相同的構建環境,可通過Export/Import機制共享變量:
父腳本:
```python
env = Environment(CCFLAGS='-O2')
Export('env')
```
子腳本:
```python
Import('env')
env.Program('prog', ['prog.c'])
```
14.5 從子腳本返回值
通過Return函數可實現子腳本向父腳本傳遞數據,例如收集多個子目錄的編譯結果:
父腳本:
```python
objs = []
for subdir in ['foo', 'bar']:
??? objs += SConscript(f'{subdir}/SConscript')
env.Library('libprog', objs)
```
子腳本:
```python
Import('env')
obj = env.Object('foo.c')
Return('obj')
```
第15章 分離源碼與構建目錄
通常需要將構建產物與源代碼分離存放。SCons通過variant_dir機制支持此需求。
15.1 使用SConscript的variant_dir參數
最直接的方式是在調用SConscript時指定variant_dir:
```python
SConscript('src/SConscript', variant_dir='build')
```
此時構建產物將生成在build目錄,同時SCons會自動將源文件復制到構建目錄:
```bash
cc -o build/hello.o -c build/hello.c
```
15.2 禁用源文件復制
大多數情況下可通過duplicate=0禁用源文件復制:
```python
SConscript('src/SConscript', variant_dir='build', duplicate=0)
```
此時構建行為變為:
```bash
cc -c src/hello.c -o build/hello.o
```
15.3 VariantDir函數
直接使用VariantDir函數也可實現相同效果:
```python
VariantDir('build', 'src', duplicate=0)
env.Program('build/hello.c')
```
15.4 結合Glob函數
文件名通配在variant_dir環境下同樣適用:
```python
# src/SConscript
env.Program('hello', Glob('*.c'))
```
第16章 多平臺變體構建
利用variant_dir可輕松實現跨平臺構建,例如同時支持Windows和Linux:
```python
platform = ARGUMENTS.get('OS', Platform())
env = Environment(PLATFORM=platform)
SConscript('src/SConscript', variant_dir=f'build/{platform}')
```
執行示例:
```bash
scons -Q OS=linux?? # 構建Linux版本
scons -Q OS=windows # 構建Windows版本
```
第17章 使用gettext實現國際化與本地化
gettext工具集為基于SCons的項目提供了國際化支持,其構建器可自動化翻譯文件的生成與更新,管理方式與autotools類似。
17.1 環境準備
建議操作系統配置支持多語言環境(如en_US、de_DE和pl_PL),并安裝GNU gettext工具包。推薦使用poedit編輯器處理翻譯文件。
17.2 基礎項目示例
以"Hello world"程序為例,原始代碼:
```c
#include <stdio.h>
int main() {
??? printf("Hello world\n");
}
```
國際化改造后的代碼需添加gettext支持:
```c
#include <libintl.h>
int main() {
??? setlocale(LC_ALL, "");
??? printf(gettext("Hello world\n"));
}
```
17.3 SCons構建配置
完整的SConstruct配置示例:
```python
env = Environment(tools=['default', 'gettext'])
env['XGETTEXTFLAGS'] = [
??? '--package-name=hello',
??? '--package-version=1.0'
]
po = env.Translate(["en","de","pl"], ["hello.c"], POAUTOINIT=1)
mo = env.MOFiles(po)
InstallAs("locale/en/LC_MESSAGES/hello.mo", "en.mo")
```
工作流程:
1. 執行`scons po-update`生成翻譯模板(.pot)和語言文件(.po)
2. 使用poedit編輯各語言po文件(如將德語翻譯為"Hallo Welt!\n")
3. 執行`scons`編譯mo文件并安裝到locale目錄
運行時通過環境變量切換語言:
```bash
LANG=de_DE.UTF-8 ./hello? # 輸出德語版本
```
17.4 翻譯文件維護
當源代碼變更時:
- 新增國際化字符串會自動合并到po文件
- 未修改的翻譯文件不會觸發重建
- 僅實際變化的翻譯會重新編譯
第18章 自定義構建器
18.1 基礎命令構建器
創建執行外部命令的構建器:
```python
bld = Builder(action='foobuild < $SOURCE > $TARGET')
env = Environment(BUILDERS={'Foo': bld})
env.Foo('output', 'input')
```
18.2 構建器注冊方式
可通過多種方式注冊構建器:
```python
# 方式1:追加到現有構建器
env.Append(BUILDERS={'Foo': bld})
# 方式2:直接字典操作
env['BUILDERS']['Foo'] = bld
```
18.3 自動化文件后綴
通過suffix參數自動處理文件擴展名:
```python
bld = Builder(
??? action='foobuild < $SOURCE > $TARGET',
??? suffix='.foo',
??? src_suffix='.input'
)
```
18.4 Python函數構建器
使用Python函數代替外部命令:
```python
def build_fn(target, source, env):
??? with open(str(target[0]), 'w') as t:
??????? t.write(open(str(source[0])).read())
??? return 0
bld = Builder(action=build_fn)
```
18.5 動態命令生成器
通過generator動態生成構建命令:
```python
def cmd_generator(source, target, env, for_signature):
??? return f'foobuild {source[0]} > {target[0]}'
bld = Builder(generator=cmd_generator)
```
18.6 目標/源文件修改器
使用emitter動態調整文件列表:
```python
def modify_files(target, source, env):
??? return target + ['new_target'], source + ['new_source']
bld = Builder(emitter=modify_files)
```
18.7. 自定義構建器和工具的放置位置
site_scons 目錄為您提供了一個放置 Python 模塊和包的地方,您可以將這些模塊和包導入到您的 SConscript 文件(site_scons)中;還可以放置能集成到 SCons 中的附加工具(site_scons/site_tools);此外,還有一個 site_scons/site_init.py 文件,該文件會在任何 SConstruct 或 SConscript 文件之前被讀取,這使您能夠改變 SCons 的默認行為。
每個系統類型(Windows、Mac、Linux 等)都會在一組標準目錄中搜索 site_scons;具體細節請查看手冊頁。頂級 SConstruct 的 site_scons 目錄總是最后被搜索,并且它的目錄會被放置在工具路徑的首位,因此它會覆蓋其他所有目錄中的工具。
如果您從某個地方(例如 SCons 維基或第三方)獲取了一個工具,并且想在您的項目中使用它,那么 site_scons 目錄是放置該工具的最簡單的地方。工具有兩種類型:一種是對環境進行操作的 Python 函數;另一種是包含 exists () 和 generate () 兩個函數的 Python 模塊或包。
單函數工具可以直接包含在您的 site_scons/site_init.py 文件中,在該文件中它會被解析并可供使用。例如,您可以有一個像這樣的 site_scons/site_init.py 文件:
def TOOL_ADD_HEADER (env):
""" 一個從向源文件添加頭信息的工具HEADER" > $TARGET',
'cat $SOURCE >> $TARGET'])
env.Append (BUILDERS = {'AddHeader' : add_header})
env ['HEADER'] = '' # 設置默認值
以及一個像這樣的 SConstruct 文件:
使用來自 site_scons/site_init.py 的 TOOL_ADD_HEADER
env=Environment(tools=['default', TOOL_ADD_HEADER], HEADER="=====")
env.AddHeader('tgt', 'src')
TOOL_ADD_HEADER 工具方法會被調用,以將 AddHeader 工具添加到環境中。
一個功能更完整、帶有 exists () 和 generate () 方法的工具可以作為一個模塊安裝在 site_scons/site_tools/toolname.py 文件中,或者作為一個包安裝在 site_scons/site_tools/toolname 目錄中。如果使用包,exists () 和 generate () 方法會在 site_scons/site_tools/toolname/init.py 文件中。(在上述所有情況中,toolname 會被工具的實際名稱所替換。)由于 site_scons/site_tools 會自動添加到工具搜索路徑的開頭,所以在那里找到的任何工具對所有環境都可用。此外,在那里找到的工具會覆蓋同名的內置工具,所以如果您需要改變內置工具的行為,site_scons 為您提供了所需的鉤子。
許多人有一組實用的 Python 函數庫,他們希望將其包含在 SConscripts 中;只需將該模塊放在 site_scons/my_utils.py 或您選擇的任何有效的 Python 模塊名稱下即可。例如,您可以在 site_scons/my_utils.py 中執行以下操作來添加 build_id 和 MakeWorkDir 函數:
from SCons.Script import * # 用于 Execute 和 Mkdir
def build_id ():
"""返回一個構建 ID(存根版本)"""
return "100"
def MakeWorkDir (workdir):
"""立即創建指定的目錄"""
Execute (Mkdir (workdir))
然后在您的 SConscript 或構建中的任何子 SConscript 中,您可以導入 my_utils 并使用它:
import my_utils
print("build_id=" + my_utils.build_id())
my_utils.MakeWorkDir('/tmp/work')
請注意,盡管您可以將這個庫放在 site_scons/site_init.py 中,但放在那里并不比放在 site_scons/my_utils.py 中更好,因為您仍然需要將該模塊導入到您的 SConscript 中。還要注意,為了在除 SConstruct 或 SConscript 之外的任何文件中引用 SCons 命名空間中的對象,如 Environment、Mkdir 或 Execute,您始終需要執行以下操作:
from SCons.Script import *
在 site_scons 中的模塊(如 site_scons/site_init.py)中也是如此。
您可以使用任何用戶級或機器級的站點目錄,如~/.scons/site_scons 來代替./site_scons,或者使用 --site-dir 選項來指向您自己的目錄。site_init.py 和 site_tools 將位于該目錄下。如果您根本不想使用 site_scons 目錄,即使它存在,也可以使用 --no-site-dir 選項。
第 19 章。不編寫構建器:命令構建器
創建一個構建器并將其附加到構建環境中,當您想要重用操作來構建相同類型的多個文件時,這提供了很大的靈活性。然而,如果您只需要執行一個特定的命令來構建單個文件(或一組文件),那么這種方式可能會很麻煩。對于這些情況,SCons 支持一個命令構建器,它安排執行一個特定的操作來構建特定的文件。這看起來很像其他構建器(如 Program、Object 等),但它接受一個額外的參數,即用于構建文件的要執行的命令:
env = Environment()
env.Command('foo.out', 'foo.in', "sed 's/x/y/' < $SOURCE > $TARGET")
執行時,SCons 會運行指定的命令,并按預期替換和TARGET:
% scons -Q
sed 's/x/y/' < foo.in > foo.out
這通常比創建一個構建器對象并將其添加到構建環境的 $BUILDERS 變量中更方便。
請注意,您為命令構建器指定的操作可以是任何合法的 SCons 操作,例如一個 Python 函數:
env = Environment()
def build(target, source, env):
進行構建所需的任何操作
return None
env.Command('foo.out', 'foo.in', build)
執行情況如下:
% scons -Q
build(["foo.out"], ["foo.in"])
請注意,從 SCons 1.1 開始,和TARGET 在源文件和目標文件中都會被展開,所以您可以這樣寫:
env.Command('${SOURCE.basename}.out', 'foo.in', build)
這與前面的示例效果相同,但可以避免重復編寫代碼。
第 20 章。偽構建器:AddMethod 函數
AddMethod 函數用于向環境中添加一個方法。它通常用于添加一個 “偽構建器”,這是一個看起來像構建器的函數,但它會包裝對多個其他構建器的調用,或者在調用一個或多個構建器之前對其參數進行處理。在下面的示例中,我們希望將程序安裝到標準的 /usr/bin 目錄層次結構中,但也將其復制到一個本地的 install/bin 目錄中,從該目錄中可以構建一個包:
def install_in_bin_dirs (env, source):
"""將源文件安裝到兩個 bin 目錄中"""
i1 = env.Install ("BIN",source)i2=env.Install("LOCALBIN", source)
return [i1 [0], i2 [0]] # 返回一個列表,就像普通構建器一樣
env = Environment (BIN='/usr/bin', LOCALBIN='#install/bin')
env.AddMethod (install_in_bin_dirs, "InstallInBinDirs")
env.InstallInBinDirs (Program ('hello.c')) # 將 hello 安裝到兩個 bin 目錄中
這會產生以下結果:
% scons -Q /
cc -o hello.o -c hello.c
cc -o hello hello.o
將文件 “hello” 安裝為 “/usr/bin/hello”
將文件 “hello” 安裝為 “install/bin/hello”
如前所述,偽構建器在解析參數方面比普通構建器提供了更大的靈活性。下一個示例展示了一個偽構建器,它有一個命名參數用于修改文件名,還有一個單獨的參數用于資源文件(而不是讓構建器通過文件擴展名來確定)。這個示例還演示了使用全局 AddMethod 函數向全局 Environment 類添加一個方法,這樣它將在所有隨后創建的環境中使用。
def BuildTestProg (env, testfile, resourcefile, testdir="tests"):
""" 構建測試程序;
在源文件和目標文件前加上 “test_”,
并將目標文件放入 testdir 中。"""
srcfile = "test_% s.c" % testfile
target = "% s/test_% s" % (testdir, testfile)
if env ['PLATFORM'] == 'win32':
resfile = env.RES (resourcefile)
p = env.Program (target, [srcfile, resfile])
else:
p = env.Program (target, srcfile)
return p
AddMethod (Environment, BuildTestProg)
env = Environment()
env.BuildTestProg('stuff', resourcefile='res.rc')
在 Linux 上,這會產生以下結果:
% scons -Q
cc -o test_stuff.o -c test_stuff.c
cc -o tests/test_stuff test_stuff.o
在 Windows 上,結果如下:
C:>scons -Q
rc /nologo /fores.res res.rc
cl /Fotest_stuff.obj /c test_stuff.c /nologo
link /nologo /OUT:tests\test_stuff.exe test_stuff.obj res.res
embedManifestExeCheck(target, source, env)
使用 AddMethod 比直接向構建環境添加實例方法更好,因為它會作為一個合適的方法被調用,并且因為 AddMethod 提供了將該方法復制到構建環境實例的任何克隆中的功能。
第 21 章。編寫掃描器
SCons 有內置的掃描器,這些掃描器知道如何在 C、Fortran 和 IDL 源文件中查找與從這些文件構建的目標所依賴的其他文件相關的信息 —— 例如,對于使用 C 預處理器的文件,掃描器會查找源文件中使用 #include 指令指定的.h 文件。您可以使用 SCons 創建內置掃描器時所使用的相同機制,為 SCons 無法 “開箱即用” 進行掃描的文件類型編寫您自己的掃描器。
21.1. 一個簡單的掃描器示例
例如,假設我們想要為.foo 文件創建一個簡單的掃描器。一個.foo 文件包含一些將被處理的文本,并且可以在以 include 開頭后跟文件名的行中包含其他文件:
include filename.foo
掃描文件將由一個您必須提供的 Python 函數來處理。以下是一個使用 Python 的 re 模塊來掃描我們示例中 include 行的函數:
import re
include_re = re.compile(r'^include\s+(\S+)$', re.M)
def kfile_scan(node, env, path, arg):
contents = node.get_text_contents()
return env.File(include_re.findall(contents))
需要注意的是,您必須從掃描器函數中返回一個 File 節點的列表,僅返回文件名的簡單字符串是不行的。就像我們在這里展示的示例一樣,您可以使用當前環境的 File 函數,以便根據文件名序列(帶有相對路徑)動態創建節點。
掃描器函數必須接受指定的四個參數,并返回一個隱式依賴項的列表。據推測,這些依賴項是通過檢查文件內容找到的,不過該函數可以執行任何操作來生成依賴項列表。
node
一個表示正在掃描的文件的 SCons 節點對象。可以使用 str () 函數將節點轉換為字符串來獲取文件的路徑名,或者可以使用內部的 SCons get_text_contents () 對象方法來獲取文件內容。
env
對此次掃描有效的構建環境。掃描器函數可以選擇使用此環境中的構建變量來影響其行為。
path
一個目錄列表,構成了此掃描器用于查找包含文件的搜索路徑。這就是 SCons 處理和LIBPATH 變量的方式。
arg
一個可選參數,您可以選擇讓各種掃描器實例將其傳遞給此掃描器函數。
可以使用 Scanner 函數創建一個 Scanner 對象,該函數通常接受一個 skeys 參數,用于將文件后綴類型與此掃描器相關聯。然后,必須使用 Append 方法將 Scanner 對象與構建環境的 $SCANNERS 構建變量相關聯:
kscan = Scanner(function = kfile_scan,
skeys = ['.k'])
env.Append(SCANNERS = kscan)
當我們把所有內容放在一起時,看起來像這樣:
import re
include_re = re.compile(r'^include\s+(\S+)$', re.M)
def kfile_scan(node, env, path):
contents = node.get_text_contents()
includes = include_re.findall(contents)
return env.File(includes)
kscan = Scanner(function = kfile_scan,
scons = ['.k'])
env = Environment(ENV = {'PATH' : '/usr/local/bin'})
env.Append(SCANNERS = kscan)
env.Command('foo', 'foo.k', 'kprocess < $SOURCES > $TARGET')
21.2. 向掃描器添加搜索路徑:FindPathDirs
許多掃描器需要使用路徑變量來搜索包含文件或依賴項;這就是和LIBPATH 的工作方式。搜索路徑會作為 path 參數傳遞給您的掃描器。路徑變量可以是節點列表、用分號分隔的字符串,甚至可以包含需要展開的 SCons 變量。幸運的是,SCons 提供了 FindPathDirs 函數,該函數本身返回一個函數,用于在調用掃描器時將給定的路徑(作為 SCons 構建變量名給出)展開為路徑列表。例如,推遲到那個時候進行計算可以使路徑包含引用,而對于每個掃描的文件,TARGET 引用可能會有所不同。
使用 FindPathDirs 非常簡單。繼續上面的示例,使用 KPATH 作為帶有搜索路徑的構建變量(類似于 $CPPPATH),我們只需修改 Scanner 構造函數調用,以包含一個 path 關鍵字參數:
kscan = Scanner(function = kfile_scan,
skeys = ['.k'],
path_function = FindPathDirs('KPATH'))
FindPathDirs 返回一個可調用對象,調用該對象時,它將本質上展開 env ['KPATH'] 中的元素,并告訴掃描器在這些目錄中進行搜索。它還會正確地將相關的存儲庫和變體目錄添加到搜索列表中。順便說一下,返回的方法會以一種高效的方式存儲路徑,因此即使可能需要進行變量替換,查找速度也會很快。這一點很重要,因為在典型的構建過程中會掃描許多文件。
21.3. 將掃描器與構建器一起使用
使用掃描器的一種方法是與構建器結合使用。構建器有兩個可選參數,我們可以使用 source_scanner 和 target_scanner。
def kfile_scan(node, env, path, arg):
contents = node.get_text_contents()
return env.File(include_re.findall(contents))
kscan = Scanner(function = kfile_scan,
skeys = ['.k'],
path_function = FindPathDirs('KPATH'))
def build_function(target, source, env):
從 “source” 構建 “target” 的代碼
return None
bld = Builder(action = build_function,
suffix = '.foo',
source_scanner = kscan
src_suffix = '.input')
env = Environment(BUILDERS = {'Foo' : bld})
env.Foo('file')
一個發射器函數可以在構建器被觸發時修改傳遞給動作函數的源文件或目標文件列表。
掃描器函數不會影響構建動作期間構建器看到的源文件或目標文件列表。然而,掃描器函數會影響是否應該重新構建構建器(例如,如果掃描器所處理的文件中的任何一個發生了變化)。
第 22 章 從代碼倉庫構建
通常,一個軟件項目會有一個或多個中央倉庫,這些倉庫是包含源代碼或派生文件,或者兩者都有的目錄樹。通過讓 SCons 使用來自一個或多個代碼倉庫的文件在本地構建樹中構建文件,您可以避免對文件進行額外的不必要的重新構建。
22.1 Repository 方法
允許多個程序員使用存儲在中央可訪問倉庫(源代碼樹的目錄副本)中的源文件和 / 或派生文件來構建軟件通常是很有用的。(請注意,這不是像 BitKeeper、CVS 或 Subversion 這樣的源代碼管理系統所維護的那種倉庫。)您可以使用 Repository 方法告訴 SCons 按順序在一個或多個中央代碼倉庫中搜索任何在本地構建樹中不存在的源文件和派生文件:
python
運行
env = Environment()
env.Program('hello.c')
Repository('/usr/repository1', '/usr/repository2')
多次調用 Repository 方法會簡單地將倉庫添加到 SCons 維護的全局列表中,但 SCons 會自動從列表中刪除當前目錄和任何不存在的目錄。
22.2 在倉庫中查找源文件
上述示例指定 SCons 將首先在 /usr/repository1 樹中搜索文件,然后在 /usr/repository2 樹中搜索文件。SCons 期望它搜索的任何文件在相對于頂級目錄的相同位置被找到。在上述示例中,如果在本地構建樹中沒有找到 hello.c 文件,SCons 將首先搜索 /usr/repository1/hello.c 文件,然后搜索 /usr/repository2/hello.c 文件來使用它。
因此,對于上述 SConstruct 文件,如果 hello.c 文件存在于本地構建目錄中,SCons 將正常重新構建 hello 程序:
plaintext
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
然而,如果本地沒有 hello.c 文件,但在 /usr/repository1 中有一個,SCons 將使用在倉庫中找到的源文件重新編譯 hello 程序:
plaintext
% scons -Q
cc -o hello.o -c /usr/repository1/hello.c
cc -o hello hello.o
類似地,如果本地沒有 hello.c 文件,/usr/repository1 中也沒有,但在 /usr/repository2 中有一個:
plaintext
% scons -Q
cc -o hello.o -c /usr/repository2/hello.c
cc -o hello hello.o
22.3 在倉庫中查找 #include 文件
我們已經知道,SCons 會掃描源文件的內容以查找 #include 文件名,并意識到從該源文件構建的目標也依賴于 #include 文件。對于 $CPPPATH 列表中的每個目錄,SCons 實際上會在任何倉庫樹中的相應目錄中進行搜索,并對在倉庫目錄中找到的任何 #include 文件建立正確的依賴關系。
然而,除非 C 編譯器也知道倉庫樹中的這些目錄,否則它將無法找到 #include 文件。例如,如果前面示例中的 hello.c 文件在其當前目錄中包含 hello.h 文件,并且 hello.h 文件僅存在于倉庫中:
plaintext
% scons -Q
cc -o hello.o -c hello.c
hello.c:1: hello.h: No such file or directory
為了通知 C 編譯器關于倉庫的信息,SCons 會為列表中的每個目錄向編譯命令添加適當的標志。所以如果我們像這樣將當前目錄添加到構建環境的CPPPATH 中:
python
運行
env = Environment(CPPPATH = ['.'])
env.Program('hello.c')
Repository('/usr/repository1')
然后重新執行 SCons 會得到:
plaintext
% scons -Q
cc -o hello.o -c -I. -I/usr/repository1 hello.c
cc -o hello hello.o
對于 C 預處理器來說,-I 選項的順序復制了 SCons 用于其自身依賴分析的倉庫目錄搜索路徑。如果有多個倉庫和多個目錄,會將倉庫目錄添加到每個CPPPATH 目錄的開頭,迅速增加 - I 標志的數量。例如,如果 $CPPPATH 包含三個目錄(并且倉庫路徑名較短!):
python
運行
env = Environment(CPPPATH = ['dir1', 'dir2', 'dir3'])
env.Program('hello.c')
Repository('/r1', '/r2')
然后在命令行上最終會有九個 - I 選項,即三個(對應每個 $CPPPATH 目錄)乘以三個(本地目錄加上兩個倉庫):
plaintext
% scons -Q
cc -o hello.o -c -Idir1 -I/r1/dir1 -I/r2/dir1 -Idir2 -I/r1/dir2 -I/r2/dir2 -Idir3 -I/r1/dir3 -I/r2/dir3 hello.c
cc -o hello hello.o
22.3.1 倉庫中 #include 文件的限制
SCons 依賴于 C 編譯器的 - I 選項來控制預處理器搜索倉庫目錄以查找 #include 文件的順序。然而,這會導致 C 預處理器處理包含雙引號文件名的 #include 行的方式出現問題。
如我們所見,如果本地目錄中不存在 hello.c 文件,SCons 將從倉庫中編譯該文件。然而,如果倉庫中的 hello.c 文件包含一個帶有雙引號文件名的 #include 行:
c
#include "hello.h"
int
main(int argc, char *argv[])
{printf(HELLO_MESSAGE);return (0);
}
然后,即使命令行將 - I 作為第一個選項指定,C 預處理器也會首先使用倉庫目錄中的 hello.h 文件,即使本地目錄中有一個 hello.h 文件:
plaintext
% scons -Q
cc -o hello.o -c -I. -I/usr/repository1 /usr/repository1/hello.c
cc -o hello hello.o
C 預處理器的這種行為 —— 總是首先在與源文件相同的目錄中搜索雙引號的 #include 文件,然后才搜索 - I 指定的目錄 —— 一般來說是無法改變的。換句話說,如果您想以這種方式使用代碼倉庫,就必須接受這個限制。有三種方法可以解決 C 預處理器的這種行為:
- 一些現代版本的 C 編譯器確實有一個選項來禁用或控制這種行為。如果是這樣,在您的構建環境中向(或CXXFLAGS 或兩者)添加該選項。確保該選項用于所有使用 C 預處理的構建環境!
- 將所有的 #include "file.h" 改為 #include <file.h>。使用尖括號的 #include 不會有相同的行為 —— 預處理器會首先搜索 - I 指定的目錄中的 #include 文件 —— 這使得 SCons 可以直接控制 C 預處理器將搜索的目錄列表。
- 要求所有從倉庫進行編譯的人簽出并處理整個文件目錄,而不是單個文件。(如果您在源代碼控制系統的命令周圍使用本地包裝腳本,您可以在那里添加邏輯來強制實施這個限制。)
22.4 在倉庫中查找 SConstruct 文件
SCons 還會在倉庫中搜索 SConstruct 文件和任何指定的 SConscript 文件。不過,這會帶來一個問題:如果 SConstruct 文件本身包含關于倉庫路徑名的信息,SCons 如何在倉庫樹中搜索 SConstruct 文件呢?為了解決這個問題,SCons 允許您使用 - Y 選項在命令行上指定倉庫目錄:
plaintext
% scons -Q -Y /usr/repository1 -Y /usr/repository2
在查找源文件或派生文件時,SCons 將首先搜索命令行上指定的倉庫,然后搜索 SConstruct 或 SConscript 文件中指定的倉庫。
22.5 在倉庫中查找派生文件
如果一個倉庫不僅包含源文件,還包含派生文件(如目標文件、庫文件或可執行文件),SCons 將執行其正常的 MD5 簽名計算,以確定倉庫中的派生文件是否是最新的,或者是否必須在本地構建目錄中重新構建該派生文件。為了使 SCons 的簽名計算正確工作,倉庫樹必須包含 SCons 用于跟蹤簽名信息的.sconsign 文件。
通常,這可以由構建集成人員完成,他們會在倉庫中運行 SCons 以創建所有派生文件和.sconsign 文件,或者他們會在單獨的構建目錄中運行 SCons 并將生成的樹復制到所需的倉庫中:
plaintext
% cd /usr/repository1
% scons -Q
cc -o file1.o -c file1.c
cc -o file2.o -c file2.c
cc -o hello.o -c hello.c
cc -o hello hello.o file1.o file2.o
(請注意,即使 SConstruct 文件將 /usr/repository1 列為倉庫,這樣做也是安全的,因為 SCons 會在該調用中從其倉庫列表中刪除當前構建目錄。)
現在,在倉庫已填充的情況下,我們只需要創建我們當前感興趣的一個本地源文件,并使用 - Y 選項告訴 SCons 從倉庫中獲取它需要的任何其他文件:
plaintext
% cd $HOME/build
% edit hello.c
% scons -Q -Y /usr/repository1
cc -c -o hello.o hello.c
cc -o hello hello.o /usr/repository1/file1.o /usr/repository1/file2.o
請注意,SCons 意識到它不需要重新構建本地的 file1.o 和 file2.o 文件,而是使用倉庫中已編譯的文件。
22.6 保證文件的本地副本
如果倉庫樹包含構建的完整結果,并且我們嘗試從倉庫構建,而本地樹中沒有任何文件,會發生一些相當令人驚訝的事情:
plaintext
% mkdir $HOME/build2
% cd $HOME/build2
% scons -Q -Y /usr/all/repository hello
scons: `hello' is up-to-date.
為什么 SCons 會說 hello 程序是最新的,而本地構建目錄中并沒有 hello 程序呢?因為倉庫(而不是本地目錄)中包含最新的 hello 程序,并且 SCons 正確地確定不需要對該文件的最新副本進行任何重建操作。
然而,很多時候您希望確保文件的本地副本始終存在。例如,打包或測試腳本可能會假設某些生成的文件在本地存在。要告訴 SCons 在本地構建目錄中復制倉庫中任何最新的文件,可以使用 Local 函數:
python
運行
env = Environment()
hello = env.Program('hello.c')
Local(hello)
然后,如果我們運行相同的命令,SCons 將從倉庫副本中復制一個程序的本地副本,并告訴您它正在這樣做:
plaintext
% scons -Y /usr/all/repository hello
Local copy of hello from /usr/all/repository/hello
scons: `hello' is up-to-date.
(請注意,因為制作本地副本的操作不被視為對 hello 文件的 “構建”,所以 SCons 仍然會報告它是最新的。)
第 23 章 多平臺配置(類似 Autoconf 的功能)
SCons 對多平臺構建配置的集成支持類似于 GNU Autoconf 提供的支持,例如確定本地系統上哪些庫或頭文件可用。本節描述如何使用 SCons 的這個功能。
注意:本章仍在開發中,所以并不是所有內容都能得到很好的解釋。有關更多信息,請查看 SCons 的手冊頁。
23.1 配置上下文
SCons 中多平臺構建配置的基本框架是通過調用 Configure 函數將配置上下文附加到構建環境,對庫、函數、頭文件等執行一些檢查,然后調用配置上下文的 Finish 方法來完成配置:
python
運行
env = Environment()
conf = Configure(env)
# 對庫、頭文件等的檢查放在這里!
env = conf.Finish()
SCons 提供了一些基本檢查,以及一種添加您自己的自定義檢查的機制。
請注意,SCons 使用其自身的依賴機制來確定何時需要運行檢查 —— 也就是說,SCons 不會每次調用時都運行檢查,而是緩存先前檢查返回的值,并且除非某些內容發生變化,否則會使用緩存的值。這在處理跨平臺構建問題時節省了開發人員大量的時間。
接下來的部分描述 SCons 支持的基本檢查,以及如何添加您自己的自定義檢查。
23.2 檢查頭文件是否存在
測試頭文件是否存在需要知道頭文件的語言。配置上下文有一個 CheckCHeader 方法,用于檢查 C 頭文件是否存在:
python
運行
env = Environment()
conf = Configure(env)
if not conf.CheckCHeader('math.h'):print 'Math.h must be installed!'Exit(1)
if conf.CheckCHeader('foo.h'):conf.env.Append('-DHAS_FOO_H')
env = conf.Finish()
請注意,您可以選擇在給定的頭文件不存在時終止構建,或者根據頭文件的存在情況修改構建環境。
如果您需要檢查 C++ 頭文件是否存在,可以使用 CheckCXXHeader 方法:
python
運行
env = Environment()
conf = Configure(env)
if not conf.CheckCXXHeader('vector.h'):print 'vector.h must be installed!'Exit(1)
env = conf.Finish()
23.3 檢查函數是否可用
使用 CheckFunc 方法檢查特定函數是否可用:
python
運行
env = Environment()
conf = Configure(env)
if not conf.CheckFunc('strcpy'):print 'Did not find strcpy(), using local version'conf.env.Append(CPPDEFINES = '-Dstrcpy=my_local_strcpy')
env = conf.Finish()
23.4 檢查庫是否可用
使用 CheckLib 方法檢查庫是否可用。您只需要指定庫的基本名稱,不需要添加 lib 前綴或.a 或.lib 后綴:
python
運行
env = Environment()
conf = Configure(env)
if not conf.CheckLib('m'):print 'Did not find libm.a or m.lib, exiting!'Exit(1)
env = conf.Finish()
因為成功使用庫的能力通常取決于是否能夠訪問描述庫接口的頭文件,所以您可以使用 CheckLibWithHeader 方法同時檢查庫和頭文件:
python
運行
env = Environment()
conf = Configure(env)
if not conf.CheckLibWithHeader('m', 'math.h', 'c'):print 'Did not find libm.a or m.lib, exiting!'Exit(1)
env = conf.Finish()
這本質上是分別調用 CheckHeader 和 CheckLib 函數的簡寫形式。
23.5 檢查 typedef 是否可用
使用 CheckType 方法檢查 typedef 是否可用:
python
運行
env = Environment()
conf = Configure(env)
if not conf.CheckType('off_t'):print 'Did not find off_t typedef, assuming int'conf.env.Append(CCFLAGS = '-Doff_t=int')
env = conf.Finish()
您還可以添加一個字符串,該字符串將放置在用于檢查 typedef 的測試文件的開頭。這提供了一種指定為找到 typedef 必須包含的文件的方法:
python
運行
env = Environment()
conf = Configure(env)
if not conf.CheckType('off_t', '#include <sys/types.h>\n'):print 'Did not find off_t typedef, assuming int'conf.env.Append(CCFLAGS = '-Doff_t=int')
env = conf.Finish()
23.6 檢查數據類型的大小
使用 CheckTypeSize 方法檢查數據類型的大小:
python
運行
env = Environment()
conf = Configure(env)
int_size = conf.CheckTypeSize('unsigned int')
print 'sizeof unsigned int is', int_size
env = conf.Finish()
plaintext
% scons -Q
sizeof unsigned int is 4
scons: `.' is up to date.
23.7 檢查程序是否存在
使用 CheckProg 方法檢查程序是否存在:
python
運行
env = Environment()
conf = Configure(env)
if not conf.CheckProg('foobar'):print 'Unable to find the program foobar on the system'Exit(1)
env = conf.Finish()
23.8 添加您自己的自定義檢查
自定義檢查是一個 Python 函數,用于檢查運行系統上是否存在特定條件,通常使用 SCons 提供的方法來處理檢查編譯是否成功、鏈接是否成功、程序是否可運行等細節。一個簡單的檢查特定庫是否存在的自定義檢查可能如下所示:
python
運行
mylib_test_source_file = """
#include <mylib.h>
int main(int argc, char **argv)
{MyLibrary mylib(argc, argv);return 0;
}
"""def CheckMyLibrary(context):context.Message('Checking for MyLibrary...')result = context.TryLink(mylib_test_source_file, '.c')context.Result(result)return result
Message 和 Result 方法通常應該在自定義檢查的開始和結束時使用,以讓用戶知道正在進行的操作:Message 調用打印指定的消息(不帶尾隨換行符),Result 調用在檢查成功時打印 yes,失敗時打印 no。TryLink 方法實際測試指定的程序文本是否能成功鏈接。
(請注意,自定義檢查可以根據您選擇傳遞的任何參數修改其檢查,或者通過使用或修改上下文.env 屬性中的配置上下文環境來進行修改。)
然后,通過將一個字典傳遞給 Configure 調用,將這個自定義檢查函數附加到配置上下文,該字典將檢查的名稱映射到基礎函數:
python
運行
env = Environment()
conf = Configure(env, custom_tests = {'CheckMyLibrary' : CheckMyLibrary})
通常,您會希望檢查的名稱和函數名稱相同,就像我們在這里所做的那樣,以避免潛在的混淆。
然后,我們可以將這些部分組合起來并實際調用 CheckMyLibrary 檢查,如下所示:
python
運行
mylib_test_source_file = """
#include <mylib.h>
int main(int argc, char **argv)
{MyLibrary mylib(argc, argv);return 0;
}
"""def CheckMyLibrary(context):context.Message('Checking for MyLibrary... ')result = context.TryLink(mylib_test_source_file, '.c')context.Result(result)return resultenv = Environment()
conf = Configure(env, custom_tests = {'CheckMyLibrary' : CheckMyLibrary})
if not conf.CheckMyLibrary():print 'MyLibrary is not installed!'Exit(1)
env = conf.Finish()# We would then add actual calls like Program() to build
# something using the "env" construction environment.
If MyLibrary is not installed on the system, the output will look like:
% scons
scons: Reading SConscript file ...
Checking for MyLibrary... no
MyLibrary is not installed!
If MyLibrary is installed, the output will look like:
% scons
scons: Reading SConscript file ...
Checking for MyLibrary... yes
scons: done reading SConscript
scons: Building targets ......
23.9. 清理目標時不進行配置
使用前面章節描述的多平臺配置時,即使調用scons -c
?來清理目標,也會運行配置命令:
plaintext
% scons -Q -c
Checking for MyLibrary... yes
Removed foo.o
Removed foo
雖然在刪除目標時運行平臺檢查不會造成什么危害,但通常這是不必要的。您可以使用GetOption
?方法來檢查命令行上是否調用了-c
(清理)選項,從而避免這種情況:
python
運行
env = Environment()
if not env.GetOption('clean'):conf = Configure(env, custom_tests = {'CheckMyLibrary' : CheckMyLibrary})if not conf.CheckMyLibrary():print 'MyLibrary is not installed!'Exit(1)env = conf.Finish()
plaintext
% scons -Q -c
Removed foo.o
Removed foo
第 24 章。緩存已構建的文件
在多開發者的軟件項目中,有時允許開發者共享他們構建的派生文件可以大大加快每個開發者的構建速度。SCons 使得這變得簡單且可靠。
24.1 指定共享緩存目錄
要啟用派生文件的共享,可以在任何 SConscript 文件中使用CacheDir
?函數:
python
運行
CacheDir('/usr/local/build_cache')
請注意,您指定的目錄必須已經存在,并且所有共享派生文件的開發者都能夠對其進行讀寫操作。它還應該位于所有構建都能夠訪問的某個中央位置。在開發者使用單獨系統(如個人工作站)進行構建的環境中,這個目錄通常會位于共享或通過 NFS 掛載的文件系統上。
以下是具體的情況:當構建指定了CacheDir
?時,每次構建一個文件,該文件及其 MD5 構建簽名都會存儲在共享緩存目錄中。[4] 在后續的構建中,在調用操作來構建一個文件之前,SCons 會檢查共享緩存目錄,查看是否存在具有完全相同構建簽名的文件。如果存在,派生文件將不會在本地構建,而是會從共享緩存目錄復制到本地構建目錄,如下所示:
plaintext
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q
Retrieved `hello.o' from cache
Retrieved `hello' from cache
請注意,即使您將 SCons 配置為使用時間戳來判斷文件是否為最新版本,CacheDir
?功能仍然會為共享緩存文件名計算 MD5 構建簽名。(有關Decider
?函數的信息,請參閱第 6 章 “依賴關系”。)因此,使用CacheDir
可能會減少或消除使用時間戳進行最新判斷所帶來的潛在性能提升。
24.2 保持構建輸出的一致性
使用共享緩存的一個潛在缺點是,SCons 打印的輸出在每次調用時可能不一致,因為任何給定的文件可能在一次構建時被重建,而在下一次從共享緩存中檢索。這會使分析構建輸出變得更加困難,特別是對于期望每次輸出一致的自動化腳本。
然而,如果您使用--cache-show
?選項,即使從共享緩存中檢索文件,SCons 也會打印用于構建該文件的命令行。這使得每次運行構建時的構建輸出都是一致的:
plaintext
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q --cache-show
cc -o hello.o -c hello.c
cc -o hello hello.o
當然,這樣做的權衡是您不再知道 SCons 是從緩存中檢索派生文件還是在本地重建它。
24.3 對特定文件不使用共享緩存
您可能希望在配置中對某些特定文件禁用緩存。例如,如果您只想將可執行文件放入中央緩存,而不包括中間目標文件,您可以使用NoCache
?函數來指定目標文件不應被緩存:
python
運行
env = Environment()
obj = env.Object('hello.c')
env.Program('hello.c')
CacheDir('cache')
NoCache('hello.o')
然后,當您在清理已構建的目標后運行scons
?時,它將在本地重新編譯目標文件(因為它不存在于共享緩存目錄中),但仍然會意識到共享緩存目錄中包含一個最新的可執行程序,可以檢索該程序而無需重新鏈接:
plaintext
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q
cc -o hello.o -c hello.c
Retrieved `hello' from cache
24.4 禁用共享緩存
從共享緩存中檢索已構建的文件通常比重新構建文件節省大量時間,但節省的時間(甚至是否能節省時間)在很大程度上取決于您的系統或網絡配置。例如,通過繁忙的網絡從繁忙的服務器檢索緩存文件可能比在本地重新構建文件更慢。
在這些情況下,您可以指定--cache-disable
?命令行選項,告訴 SCons 不從共享緩存目錄中檢索已構建的文件:
plaintext
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q
Retrieved `hello.o' from cache
Retrieved `hello' from cache
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q --cache-disable
cc -o hello.o -c hello.c
cc -o hello hello.o
24.5 用已構建的文件填充共享緩存
有時,您可能在本地構建樹中已經構建了一個或多個派生文件,并希望將其提供給其他進行構建的人。例如,您可能會發現,在禁用緩存的情況下執行集成構建(如前一節所述),并在集成構建成功完成后將已構建的文件填充到共享緩存目錄中會更有效。這樣,緩存中只會填充屬于完整、成功構建的派生文件,而不會填充在調試集成問題時可能會被覆蓋的文件。
在這種情況下,您可以使用--cache-force
?選項告訴 SCons 將所有派生文件放入緩存,即使這些文件已經在您的本地樹中由之前的調用構建過:
plaintext
% scons -Q --cache-disable
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q -c
Removed hello.o
Removed hello
% scons -Q --cache-disable
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q --cache-force
scons: `.' is up to date.
% scons -Q
scons: `.' is up to date.
請注意,上述示例運行表明--cache-disable
?選項會避免將已構建的hello.o
?和hello
?文件放入緩存,但在使用--cache-force
?選項后,這些文件會被放入緩存,以便下一次調用時檢索。
24.6 最小化緩存競爭:--random
?選項
如果您允許多個構建同時更新共享緩存目錄,同時進行的兩個構建有時會以相同的順序嘗試構建相同的文件,從而產生 “競爭”。例如,如果您要將多個文件鏈接到一個可執行程序中:
python
運行
Program('prog',['f1.c', 'f2.c', 'f3.c', 'f4.c', 'f5.c'])
SCons 通常會按照正常的排序順序構建程序所依賴的輸入目標文件:
plaintext
% scons -Q
cc -o f3.o -c f3.c
cc -o f4.o -c f4.c
cc -o f1.o -c f1.c
cc -o f2.o -c f2.c
cc -o f5.o -c f5.c
cc -o prog f1.o f2.o f3.o f4.o f5.o
但是,如果同時進行兩個這樣的構建,它們可能會幾乎同時檢查緩存,并都決定必須重建f1.o
?并將其推入共享緩存目錄,然后又都決定必須重建f2.o
(并將其推入共享緩存目錄),接著又都決定必須重建f3.o
…… 這不會導致任何實際的構建問題 —— 兩個構建都會成功,生成正確的輸出文件,并填充緩存 —— 但這確實是一種浪費精力的情況。
為了緩解這種對緩存的競爭,您可以使用--random
?命令行選項告訴 SCons 以隨機順序構建依賴項:
plaintext
% scons -Q --randomcc -o f3.o -c f3.ccc -o f1.o -c f1.ccc -o f5.o -c f5.ccc -o f2.o -c f2.ccc -o f4.o -c f4.ccc -o prog f1.o f2.o f3.o f4.o f5.o
使用--random
?選項的多個構建通常會以不同的隨機順序構建它們的依賴項,這會最小化對共享緩存目錄中同名文件的大量競爭的可能性。多個同時進行的構建偶爾可能仍然會競爭構建相同的目標文件,但長時間的低效競爭應該很少發生。
當然,請注意--random
?選項會導致 SCons 打印的輸出在每次調用時不一致,這在嘗試比較不同構建運行的輸出時可能會成為一個問題。
如果您想確保依賴項以隨機順序構建,而不必在每個命令行上指定--random
,您可以在任何 SConscript 文件中使用SetOption
?函數來設置隨機選項:
python
運行
SetOption('random', 1)
Program('prog',['f1.c', 'f2.c', 'f3.c', 'f4.c', 'f5.c'])
[4] 實際上,MD5 簽名被用作存儲內容的共享緩存目錄中文件的名稱。
第 25 章。別名目標
我們已經了解了如何使用Alias
?函數創建一個名為install
?的目標:
python
運行
env = Environment()
hello = env.Program('hello.c')
env.Install('/usr/bin', hello)
env.Alias('install', '/usr/bin')
然后,您可以在命令行上使用這個別名,更自然地告訴 SCons 您想要安裝文件:
plaintext
% scons -Q install
cc -o hello.o -c hello.c
cc -o hello hello.o
Install file: "hello" as "/usr/bin/hello"
不過,與其他構建器方法一樣,Alias
?方法會返回一個表示正在構建的別名的對象。然后,您可以將這個對象用作另一個構建器的輸入。如果您將這樣的對象用作對Alias
?構建器的另一次調用的輸入,這會特別有用,這樣您就可以創建一個嵌套別名的層次結構:
python
運行
env = Environment()
p = env.Program('foo.c')
l = env.Library('bar.c')
env.Install('/usr/bin', p)
env.Install('/usr/lib', l)
ib = env.Alias('install-bin', '/usr/bin')
il = env.Alias('install-lib', '/usr/lib')
env.Alias('install', [ib, il])
這個示例定義了單獨的install
、install-bin
?和install-lib
?別名,使您能夠更精細地控制要安裝的內容:
plaintext
% scons -Q install-bin
cc -o foo.o -c foo.c
cc -o foo foo.o
Install file: "foo" as "/usr/bin/foo"
% scons -Q install-lib
cc -o bar.o -c bar.c
ar rc libbar.a bar.o
ranlib libbar.a
Install file: "libbar.a" as "/usr/lib/libbar.a"
% scons -Q -c /
Removed foo.o
Removed foo
Removed /usr/bin/foo
Removed bar.o
Removed libbar.a
Removed /usr/lib/libbar.a
% scons -Q install
cc -o foo.o -c foo.c
cc -o foo foo.o
Install file: "foo" as "/usr/bin/foo"
cc -o bar.o -c bar.c
ar rc libbar.a bar.o
ranlib libbar.a
Install file: "libbar.a" as "/usr/lib/libbar.a"