GN 簡介

注意: 大多數開發人員應透過 fx 工作流程 (例如 fx buildfx set) 與建構系統互動,而不是直接執行 GN、Ninja 或 Bazel (即使是透過 fx gnfx ninja 等包裝函式)。

本節將介紹 GN 的術語和思維方式。這應該足以讓您瞭解 GN 的基本概念,以及 GN 在 Fuchsia 中的用途。GN (和 Fuchsia 建構版本) 比下文討論的內容更複雜,但一般開發人員不需要深入瞭解大部分內容。

GN 說明文件頁面「QuickStart」和「語言」提供 GN 的詳細背景資訊,而「參考資料」則包含完整的語言說明文件。使用 gn help 指令可列印個別主題的參考資料。Ninja 也有自己的說明文件。

執行 jiri update 後,Fuchsia 結帳程序中的 fx gnfx ninja 指令會提供預先建構的二進位檔存取權。

兩階段作業:gnninja

make 不同,gn 只能提供一半的資訊。從名稱即可看出:GN 代表「Generate Ninja」(生成 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 繼承了這項工具,現在樹狀結構中都使用 GN 做為主要建構系統。

建構目錄和 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 setfoobar 設為「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 會強制執行標準格式,並進行大量重新格式化。如果不喜歡這種格式,請提出錯誤或在上游 GN 中進行變更,如果變更生效,我們會大量重新格式化所有內容,以符合新的標準。

來源路徑和 GN 標籤

GN 會使用 POSIX 樣式的路徑 (一律以字串表示),用於檔案和參照 GN 定義的實體。路徑可以是相對路徑,也就是相對於包含路徑字串的 BUILD.gn 檔案的目錄。路徑也可以是「來源絕對路徑」,也就是相對於來源樹狀結構的根目錄。在 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 值可以是下列任一類型:

  • 布林值,truefalse
  • 整數,以一般十進位語法簽署,不常用
  • 字串,一律以「雙引號」括住 (請參閱下方的 $ 擴展注意事項)
  • 範圍 (以大括號括住):{ ... };請參閱下文。
  • 以方括號括住的值清單:[ 1, true, "foo", { x=1 y=2 } ] 是四個元素的清單。

值是動態型別,沒有任何隱含型別強制轉換,但從來沒有型別檢查。不同型別的值永遠不會比較為相等,但比較這些值不會產生錯誤。

字串常值會展開雙引號內的簡單 $var${var} 運算式。這是立即擴展:當 var 是字串時,x${var}yx + 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() 診斷錯誤。

值也可以是範圍。這樣一來,您使用值時,值就會像結構體一樣: 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 的多重開機 (使用 -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 程式碼會保留在所描述的來源中,但仍可為核心和使用者等執行不同的共用來源子集。