注意: 大多數開發人員應透過
fx工作流程 (例如fx build和fx set) 與建構系統互動,而非直接執行 GN、Ninja 或 Bazel (即使透過fx gn或fx ninja等包裝函式也一樣)。
本節將介紹 GN 的術語和思考方式。這應該足以讓您瞭解 GN 的背景,以及 GN 在 Fuchsia 中的使用方式。GN (和 Fuchsia 建構作業) 比下文討論的內容更複雜,但一般開發人員不需要深入瞭解大部分內容。
GN 說明文件頁面「QuickStart」和「語言」提供更詳細的 GN 背景資訊,而「參考資料」則提供完整的語言說明文件。使用 gn help 指令,以互動方式列印個別主題的參考資料。Ninja 也有自己的說明文件。
在 Fuchsia 簽出中執行 jiri update 後,指令 fx gn 和 fx ninja 會提供預先建構二進位檔的存取權。
兩階段作業:gn和ninja
與make不同,gn只能呈現部分事實。顧名思義,GN 代表 Generate Ninja。這些工具之間有責任分工,對應到將執行建構作業分為兩個步驟:
gn gen會採用所有設定選項,並做出所有決策。它實際上只會在建構目錄中產生.ninja檔案。只有在變更設定或完全刪除建構目錄時,才需要手動執行這個步驟。一般來說,只有在 GN 檔案變更時才需要執行這項操作,而在增量建構中,如果 GN 檔案或設定變更,系統會自動執行這項操作。ninja會執行指令來編譯和連結等。它會處理增量建構和平行處理。每次變更來源檔案時,都要執行這個步驟,例如執行make。當相關的BUILD.gn檔案 (或其他相關檔案) 變更時,GN 會自動發出規則,再次執行gn gen以重新產生 Ninja 檔案,因此在首次建構後,ninja會處理大部分的變更。
相較於 GNU make 等工具,Ninja 非常簡單。它只會比較時間並執行指令,且輸入檔案是由機器 (而非人類) 寫入。不過,它內建了一些實用的功能,我們在 make 中會竭盡全力完成:
- 指令列變更時,重新建構每個檔案。只有在 GN 再次執行時,指令列才會真正變更。不過,Ninja 之後會智慧地執行遞增建構作業,重新執行已變更檔案的指令,而不會重新執行未變更的指令。
- 處理編譯器產生的依附元件檔案。Ninja 知道編譯器在
.d檔案中發出的 makefile 子集,並在 GN 指示時直接使用。 - 預設使用
-j$(getconf _NPROCESSORS_ONLN)執行。使用遠端建構服務時,您可以傳遞-j1進行序列化,或傳遞-j1024,但預設情況下,它會執行您通常需要的平行處理。 - 避免平行工作的
stdout/stderr輸出內容交錯。Ninja 會緩衝輸出,因此錯誤訊息不會因多個程序而混淆。 - 支援簡潔/詳細的指令輸出。根據預設,Ninja 會針對執行的每個指令,以冗長的進度計量器樣式發出簡短的
Kbuild樣式訊息。-v 切換鈕就像Kbuild中的 V=1,可顯示每個實際指令。
GN 是 Chromium 專案的一部分,旨在取代舊版建構系統。Fuchsia 沿用了這項工具,現在整個樹狀結構都使用這項工具做為主要建構系統。
建構目錄和 args.gn
Ninja 一律在建構目錄中執行。Ninja 執行的所有指令都是從建構目錄的根目錄執行。共同點是ninja -C build-dir。
GN 和 Ninja 都不會管你使用哪個建構目錄。通常會使用來源目錄的子目錄,且由於檔案路徑通常會重新設定為相對於建構目錄,因此如果您將建構目錄放在其他位置,提供給編譯器的檔案名稱會包含大量 ../,但應該可以正常運作。在 Chromium 中,使用來源目錄中的 out/_something_ 是常見的做法 (早於 GN 本身),而 Fuchsia 沿用了該預設值。不過,無論您選擇什麼建構目錄名稱,都不會造成任何影響,但 out 子目錄位於 Fuchsia 的頂層 .gitignore 檔案中。
基本指令為 gn gen build-dir。這會視需要建立 build-dir/,並填入目前設定的 Ninja 檔案。如果 build-dir/args.gn 存在,gn gen 會讀取該檔案,設定 GN 建構引數 (請見下方)。args.gn 是 GN 語法檔案,可將值指派給 GN 建構引數,覆寫任何硬式編碼預設值。也就是說,只要重複 gn gen build-dir,就能保留上次的操作。
您也可以將 --args=... 新增至 gn gen,或使用 gn args 指令設定建構引數。gn args 指令可讓您在 args.gn 檔案上執行 $EDITOR,並在退出編輯器時,使用新引數重新執行 gn gen。您也可以隨時編輯 args.gn,下次執行 Ninja 時,系統會重新產生建構檔案。
您也可以使用 fx set 指令設定引數,這會叫用 gn gen。例如,透過 fx set 將 foobar 設為 true:
$ fx set <your configuration> --args 'foobar=true'
詳情請參閱「GN 建構引數」。
GN 語法和格式
GN 語法不區分空白字元。x=1 y=2 等同於:
x = 1
y = 2
不過,GN 程式碼有一種真正的縮排和格式樣式。gn format 指令會將語法有效的 GN 程式碼重新格式化為標準樣式。編輯器語法支援 Emacs 和 Vim。Tricium 會強制執行標準格式,並大量重新格式化。如果您不喜歡格式,請在 upstream GN 中提交錯誤或進行變更,如果變更成功,我們會大量重新格式化所有內容,以符合新的標準。
來源路徑和 GN 標籤
GN 會使用 POSIX 樣式的路徑 (一律以字串表示) 來表示檔案和 GN 定義的實體。路徑可以是相對路徑,也就是相對於包含路徑字串所在 BUILD.gn 檔案的目錄。也可以是「source-absolute」,也就是相對於來源樹狀結構的根目錄。在 GN 中,來源絕對路徑開頭為 //。
在指令中最終使用來源路徑時,系統會將路徑轉譯為適合 OS 的路徑,這些路徑可以是絕對路徑,也可以是相對於建構目錄 (指令執行位置) 的路徑。
預先定義的變數用於來源路徑情境,以找出建構目錄的部分:
$root_build_dir是建構目錄本身$root_out_dir是目前工具鍊的子目錄 (請見下方)- 所有「頂層」目標都會放在這裡。在許多 GN 建構作業中,所有執行檔和程式庫都會放在這裡。
$target_out_dir是$root_out_dir的子目錄,用於存放目前BUILD.gn檔案中目標建構的檔案。這是物件檔案的存放位置。$target_gen_dir是建議放置生成程式碼的對應位置$root_gen_dir是存放這個子目錄以外所需生成程式碼的地方
GN 標籤是指 BUILD.gn 檔案中定義的項目。這些標籤是以來源路徑為基礎,且一律會顯示在 GN 字串中。GN 標籤的完整語法為 "dir:name",其中 dir 部分是命名特定 BUILD.gn 檔案的來源路徑。name 是指該檔案中以 target_type("name") { ... } 定義的目標。為求簡便,您可以定義與目錄同名的目標。沒有 : 部分的標籤 "//path/to/dir" 是 "//path/to/dir:dir" 的簡寫。這是最常見的情況。
依附關係圖和 BUILD.gn 檔案
GN 中的所有內容都以依附元件圖為基礎。有一個根 BUILD.gn 檔案。只有在該目錄中的標籤有依附元件時,系統才會讀取其他 BUILD.gn 檔案。
沒有萬用字元。每個目標都必須命名為其他目標的依附元件,才能建構。您可以在 ninja 指令列上指定個別目標,明確建構這些目標。否則,這些檔案必須位於 //:default 目標 (在根 BUILD.gn 檔案中名為 default) 的圖表中。
有一種通用的中繼目標類型稱為 group(),不對應至建構作業產生的檔案,而是用來妥善建構依附元件圖。default 等頂層目標通常是群組。您可以為硬體的所有驅動程式建立一個群組,為用途中的所有二進位檔建立一個群組,以此類推。
當某些程式碼在執行階段使用某個項目 (資料檔案、其他可執行檔等),但不會在建構時直接使用該檔案,則該檔案屬於使用該檔案的目標 data_deps 清單。這也足以將該項目放入 BOOTFS 映像檔的指定位置。
目標也可以加上 testonly = true 標籤,表示目標包含測試。GN 會防止非 testonly 的目標依附於
的目標,因此可在一定程度上控管測試二進位檔的最終
位置。
建構映像檔檔案是由一或多個 zbi() 目標驅動。這項操作會建構並使用 ZBI 主機工具,建立 ZBI。只要目標存在於依附元件圖中,就能放入這個映像檔,因此您可以為目標提供核心的依附元件,以及映像檔中所需的任何驅動程式或執行檔。
請注意,在 Ninja 檔案中定義的目標是以 BUILD.gn 檔案為精細程度,但預設或任何其他目標的依附元件圖表則是以個別目標為精細程度。因此,如果圖表中的 BUILD.gn 檔案預設有某些目標,該檔案 (和工具鍊,請參閱下文) 中的所有目標都會成為 Ninja 指令列上的目標,即使預設不會建構這些目標也一樣。
更多進階概念
GN 運算式語言和 GN 範圍
GN 是一種簡單、動態類型、指令式的語言,其最終目的只是產生宣告式 Ninja 規則。一切都圍繞著範圍,範圍既是語言的詞彙繫結建構,也是資料類型。
GN 值可以採用多種類型:
- 布林值,
true或false - 整數,帶正負號,採用一般十進位語法;不常使用
- 字串,一律加上「雙引號」(請參閱下方關於
$展開的附註) - 範圍 (以大括號括住):
{ ... };請見下方。 - 方括號中的值清單:
[ 1, true, "foo", { x=1 y=2 } ]是包含四個元素的清單。
值為動態類型,且沒有任何隱含類型強制轉型,但絕不會進行類型檢查。不同類型的值絕不會相等,但比較這些值並非錯誤。
字串常值會展開雙引號內的簡單 $var 或 ${var} 運算式。這是立即展開:當 var 是字串時,x${var}y 與 x +
var + y 相同。這樣一來,任何值都能以美化排版字串的形式呈現。
由英數字元和底線組成的 ID 可透過指派運算子填入範圍。使用 = 進行命令式指派,並透過 += 進行修改,這實際上就是 GN 語言的所有功能 (也有一些特殊方式可以產生副作用,例如用於偵錯的 print(),以及謹慎使用的 write_file())。
每個檔案在內部都以範圍表示,且沒有全域範圍。共用的「全域」可在 .gni 檔案中定義,並匯入至使用位置 (import("//path/to/something.gni"))。每個 .gni 檔案都會由每個工具鍊處理一次 (請參閱下文瞭解工具鍊),產生的範圍會複製到匯入檔案範圍。
目標宣告會引入子範圍:
foo = true
executable("target") {
foo = 12
}
# Outside the target, foo == true
GN 在診斷錯誤時非常嚴格,如果變數已定義但從未在範圍內使用,目標內部的範圍就像目標的關鍵字引數清單,會檢查引數名稱是否拼寫正確。如果省略必要引數,目標定義程式碼也可以使用 assert() 診斷錯誤。
值也可以是範圍。使用時,它的行為就像 struct:value.member。但範圍一律是 GN 程式碼區塊,執行後會產生一組名稱和值:
foo = {
x = global_tuning + 42
if (some_global && other_thing == "foobar") {
y = 2
}
}
這會一律定義 foo.x,但有時會定義 foo.y。
GN 工具鍊
GN 有一個稱為「工具鍊」的概念。這一切都會在幕後進行,開發人員不需要直接處理,但瞭解機制會有幫助。
這會封裝編譯器和預設編譯切換。這也是以不同方式編譯相同內容的唯一方法。Fuchsia 會有幾個工具鍊:
- 主機
- Vanilla 使用者空間 (使用預設
-fPIE編譯) - 使用者空間中的共用程式庫 (使用
-fPIC編譯) userboot- 核心
- ARM64 的核心實體位址模式 (使用
-mstrict-align編譯) - x86 的 32 位元核心實體位址模式 (使用
-m32編譯) - Gigaboot 的 UEFI
- 工具鍊也用於「變體」配置,這是我們允許選擇性為部分使用者空間啟用 ASan 或類似工具的方式。
每個工具鍊都由 GN 標籤識別。目標標籤的完整語法實際上是 //path/to/dir:name(//path/to/toolchain/label)。通常會省略工具鍊,並展開為 label($current_toolchain),也就是說,標籤參照通常位於同一個工具鍊中。
所有 GN 檔案都會在每個工具鍊中分別例項化。每個工具鍊可以設定不同的全域變數,因此 GN 程式碼可以使用 if
(is_kernel) 或 if (current_toolchain == some_toolchain) 等測試,在不同情境下採取不同行為。這樣一來,GN 程式碼就會與其描述的來源保持一致,但仍可為核心和使用者等執行不同的共用來源子集。