GN 簡介

注意: 大多數開發人員應透過 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。這些工具之間有責任分工,對應到將執行建構作業分為兩個步驟:

  1. gn gen 會採用所有設定選項,並做出所有決策。它實際上只會在建構目錄中產生 .ninja 檔案。只有在變更設定或完全刪除建構目錄時,才需要手動執行這個步驟。一般來說,只有在 GN 檔案變更時才需要執行這項操作,而在增量建構中,如果 GN 檔案或設定變更,系統會自動執行這項操作。

  2. 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 程式碼就會與其描述的來源保持一致,但仍可為核心和使用者等執行不同的共用來源子集。