編寫 GN 範本的最佳做法

總覽

在 Google 名單中,範本是新增至 GN 內建指定類型的方法。基本上,範本是 GN 建構可重複使用函式的主要方式。範本定義位於 .gni (GN 匯入) 檔案中,可匯入目標 .gn 檔案。

本文件詳細說明建立 GN 範本的最佳做法,且每個最佳做法均提供範例。除了「Fuchsia 建構系統政策」中列出的最佳做法之外,這些最佳做法也並非如此。

執行 fx gn help template 來取得詳細資訊和更完整的範例,請參閱「GN 語言和作業」一文,進一步瞭解 GN 功能。

範本

在「.gni」中定義範本,指定「BUILD.gn」中的目標

技術上,您可以將 .gniBUILD.gn 檔案都匯入,不過,最佳做法是在 .gni 檔案中定義範本,並在 .gn 檔案中定義目標。讓使用者清楚知道什麼是範本。使用者想要匯入範本以供使用,且不想匯入目標。

文件範本與引數

請記錄您的範本和引數,包括:

  • 範本用途與概念的一般說明。我們建議實際使用範例。
  • 所有參數都應妥善記錄。常見的轉接參數 (例如 depsvisibility) 是常用的參數,其中意義與內建 GN 規則的意義一致,而且不得列出其他資訊。
  • 如果範本會產生 metadata,,則應列出 data_keys

如要記錄範本,請在範本定義前插入註解區塊,以指定公開合約。

declare_args() {
  # The amount of bytes to allocate when creating a disk image.
  disk_image_size_bytes = 1024
}

# Defines a disk image file.
#
# Disk image files are used to boot the bar virtual machine.
#
# Example:
# ```
# disk_image("my_image") {
#   sources = [ "boot.img", "kernel.img" ]
#   sdk = false
# }
# ```
#
# Parameters
#
#  sources (required)
#    List of source files to include in the image.
#    Type: list(path)
#
#  sdk (optional)
#    This image is exported to the SDK.
#    Type: bool
#    Default: false
#
#  data_deps
#  deps
#  public_deps
#  testonly
#  visibility
#
# Metadata
#
#  files
#    Filenames present in this image.
template("disk_image") {
  ...
}

將工具納入單一動作範本

每項工具都有一個以 action 包裝的標準範本。這個範本的工作是將 GN 參數轉換為 args 工具,就是這麼簡單。這會設定工具周圍的封裝界線,例如將參數轉譯為引數。

請注意,在本範例中,我們在其中一個檔案和 template() 定義了 executable(),因為 範本和目標應分開

# //src/developer_tools/BUILD.gn
executable("copy_to_target_bin") {
  ...
}

# //src/developer_tools/cli.gni
template("copy_to_target") {
  compiled_action(target_name) {
    forward_variables_from(invoker, [
                                      "data_deps",
                                      "deps",
                                      "public_deps",
                                      "testonly",
                                      "visibility"
                                    ])
    assert(defined(invoker.sources), "Must specify sources")
    assert(defined(invoker.destinations), "Must specify destinations")
    tool = "//src/developer_tools:copy_to_target_bin"
    args = [ "--sources" ]
    foreach(source, sources) {
      args += [ rebase_path(source, root_build_dir) ]
    }
    args += [ "--destinations" ]
    foreach(destination, destinations) {
      args += [ rebase_path(destination, root_build_dir) ]
    }
  }
}

建議將範本設為私人

名稱以底線開頭的範本和變數 (例如 template("_private")) 會視為私人項目,不會顯示在 import() 的其他檔案中,但可在定義的相同檔案中使用。這對您的內部輔助範本或「本機全域變數」而言非常實用,用於在兩個範本之間共用邏輯時,輔助程式並對使用者沒有幫助。

template("coffee") {
  # Take coffee parameters like roast and sugar
  ...
  _beverage(target_name) {
    # Express in beverage terms like ingredients and temperature
    ...
  }
}

template("tea") {
  # Take tea parameters like loose leaf and cream
  ...
  _beverage(target_name) {
    # Express in beverage terms like ingredients and temperature
    ...
  }
}

# We don't want people directly defining new beverages.
# For instance they might add both sugar and salt to the ingredients list.
template("_beverage") {
  ...
}

有時您可能無法將範本設為私人,因為範本實際上需要在不同檔案中使用,但仍想要隱藏,因為我們不想直接使用。在這種情況下,您可以替換信號的強制執行,方法是將範本放在路徑 (例如 //build/internal/) 底下。

測試範本

編寫使用範本建構的測試,或是在測試過程中使用範本產生的檔案。

請勿仰賴他人的建構和測試來測試您的範本。自行測試可以讓範本更容易維護,因為驗證未來範本變更的速度較快,也更容易找出錯誤。

# //src/drinks/coffee.gni
template("coffee") {
  ...
}

# //src/drinks/tests/BUILD.gni
import("//src/drinks/coffee.gni")

coffee("coffee_for_test") {
  ...
}

test("coffee_test") {
  sources = [ "taste_coffee.cc" ]
  data_deps = [ ":coffee_for_test" ]
  ...
}

參數

為必要參數進行斷言

如果範本中有必要參數,assert 已定義參數。

當使用者忘記指定必要參數,且未定義任何宣告時,使用者就不會收到有關錯誤的明確說明。使用斷言可以讓您提供實用的錯誤訊息。

template("my_template") {
  forward_variables_from(invoker, [ "sources", "testonly", "visibility" ])
  assert(defined(sources),
      "A `sources` argument was missing when calling my_template($target_name)")
}

template("my_other_template") {
  forward_variables_from(invoker, [ "inputs", "testonly", "visibility" ])
  assert(defined(inputs) && inputs != [],
      "An `input` argument must be present and non-empty " +
      "when calling my_template($target_name)")
}

一律轉寄 testonly

在目標上設定 testonly 可避免將其用於非測試目標。如果範本不會將 testonly 轉送至內部目標,請執行下列操作:

  1. 內部目標可能無法建構,因為使用者可能會傳遞 testonly 依附元件。
  2. 如果使用者發現他們的 testonly 構件實際出現在實際工作環境的構件中,就會感到驚訝。

以下範例說明如何將 testonly 轉寄:

template("my_template") {
  action(target_name) {
    forward_variables_from(invoker, [ "testonly", "deps" ])
    ...
  }
}

my_template("my_target") {
  visibility = [ ... ]
  testonly = true
  ...
}

請注意,如果內部動作的父項範圍定義了 testonly,則 forward_variables_from(invoker, "*") 不會轉寄該動作,因為這樣會避免使用截斷變數。解決方法如下:

# Broken, doesn't forward `testonly`
template("my_template") {
  testonly = ...
  action(target_name) {
    forward_variables_from(invoker, "*")
    ...
  }
}

# Works
template("my_template") {
  testonly = ...
  action(target_name) {
    forward_variables_from(invoker, "*")
    testonly = testonly
    ...
  }
}

# Works
template("my_template") {
  testonly = ...
  action(target_name) {
    forward_variables_from(invoker, "*", [ "testonly" ])
    forward_variables_from(invoker, [ "testonly" ])
    ...
  }
}

唯一的例外是,因為 testonly = true 不應用於實際工作環境目標,所以採用硬式編碼的範本。例如:

template("a_test_template") {
  testonly = true
  ...
}

visibility 轉送至主要目標,並隱藏內部目標

GN 使用者應能在任何目標上設定 visibility

這項建議與「一律向前測試」類似,差別在於這個做法僅適用於主要目標 (名為 target_name 的目標)。其他目標應限制其 visibility,以免使用者依賴不屬於合約的內部目標。

template("my_template") {
  action("${target_name}_helper") {
    forward_variables_from(invoker, [ "testonly", "deps" ])
    visibility = [ ":*" ]
    ...
  }

  action(target_name) {
    forward_variables_from(invoker, [ "testonly", "visibility" ])
    deps = [ ":${target_name}_helper" ]
    ...
  }
}

如果轉寄 deps,請一併將 public_depsdata_deps 轉址

採用 deps 的所有內建規則,採用 public_depsdata_deps。 部分內建規則無法區分依附元件類型 (例如 action() 會以同樣的方式處理 depspublic_deps)。但依附於您所產生目標的依附關係可能 (例如,用於產生的 action()executable() 會以不同的方式處理遞移 depspublic_deps)。

template("my_template") {
  action(target_name) {
    forward_variables_from(invoker, [
                                       "data_deps",
                                       "deps",
                                       "public_deps",
                                       "testonly",
                                       "Visibility"
                                    ])
    ...
  }
}

目標名稱

定義名為 target_name 的內部目標

範本應定義至少一個名為 target_name 的目標。如此一來,使用者就能以名稱叫用範本,然後在依附元件中使用該名稱。

# //build/image.gni
template("image") {
  action(target_name) {
    ...
  }
}

# //src/some/project/BUILD.gn
import("//build/image.gni")

image("my_image") {
  ...
}

group("images") {
  deps = [ ":my_image", ... ]
}

target_name 是輸出名稱的理想預設值,但提供覆寫值

如果您的範本產生單一輸出內容,那麼使用目標名稱選擇輸出名稱就會是良好的預設行為。然而,目標名稱在目錄中不得重複,因此使用者不一定能同時將所需的名稱用於目標和輸出內容。

建議您為使用者提供覆寫值:

template("image") {
  forward_variables_from(invoker, [ "output_name", ... ])
  if (!defined(output_name)) {
    output_name = target_name
  }
  ...
}

在內部目標名稱前面加上 $target_name

GN 標籤不可重複,否則您會收到暫時性錯誤。如果同一個專案中的每個成員都遵循相同的命名慣例,發生衝突的可能性就會降低,而且會更容易將內部目標名稱與建立這些目標的名稱建立關聯。

template("boot_image") {
  generate_boot_manifest_action = "${target_name}_generate_boot_manifest"
  action(generate_boot_manifest_action) {
    ...
  }

  image(target_name) {
    ...
    deps += [ ":$generate_boot_manifest_action" ]
  }
}

不要從目標標籤推論輸出名稱

您可能會想假設目標名稱和輸出名稱之間的關係。以下範例將實際運作:

executable("bin") {
  ...
}

template("bin_runner") {
  compiled_action(target_name) {
    forward_variables_from(invoker, [ "testonly", "visibility" ])
    assert(defined(invoker.bin), "Must specify bin")
    deps = [ invoker.bin ]
    tool = root_out_dir + "/" + get_label_info(invoker.foo, "name")
    ...
  }
}

bin_runner("this_will_work") {
  bin = ":bin"
}

不過,以下示例會產品生成式錯誤:

executable("bin") {
  output_name = "my_binary"
  ...
}

template("bin_runner") {
  compiled_action(target_name) {
    forward_variables_from(invoker, [ "testonly", "visibility" ])
    assert(defined(invoker.bin), "Must specify bin")
    tool = root_out_dir + "/" + get_label_info(invoker.bin, "name")
    ...
  }
}

# This will produce a gen-time error saying that a file ".../bin" is needed
# by ":this_will_fail" with no rule to generate it.
bin_runner("this_will_fail") {
  bin = ":bin"
}

以下是解決這個問題的其中一種方式:

executable("bin") {
  output_name = "my_binary"
  ...
}

template("bin_runner") {
  compiled_action(target_name) {
    forward_variables_from(invoker, [ "testonly", "visibility" ])
    assert(defined(invoker.bin), "Must specify bin")
    tool = bin
    ...
  }
}

bin_runner("this_will_work") {
  bin = "$root_out_dir/my_binary"
}

GN 函式與產生

僅將 read_file() 與來源檔案搭配使用

read_file() 會在產生期間發生,無法安全用來讀取產生的檔案或建構輸出內容。此屬性可用來讀取來源檔案,例如讀取資訊清單檔案或用於填入建構依附元件的 JSON 檔案。值得注意的是,read_file() 無法與 generated_file()write_file() 搭配使用。

偏好使用 generated_file(),而非 write_file()

一般來說,建議使用 generated_file(),而非 write_file()generated_file() 提供額外功能並解決 write_file() 的部分挑戰。舉例來說,generated_file() 可以平行執行,而 write_file() 會在基因時依序完成。

這兩個指令的結構非常類似。舉例來說,您可以開啟 write_file() 的執行個體:

write_file("my_file", "My file contents")

轉換為 generated_file() 的執行個體:

generated_file("my_file") {
  outputs = [ "my_file" ]
  contents = "My file contents"
}

偏好來自 rebase_path() 的相對路徑

請一律在 rebase_path() 中指定 new_base,例如 rebase_path("foo/bar.txt", root_build_dir)。請避免使用其單參數格式,即 rebase_path("foo/bar.txt")

GN 的 rebase_path() 有三個參數,後兩個參數為選用。其單一參數表單會傳回絕對路徑,並已淘汰。請避免在建構範本和目標中使用這項屬性。new_base 的值因個案情況而異,其中 root_build_dir 是常見的選項,因為這是執行建構指令碼的位置。如要進一步瞭解 rebase_path(),請參閱其 GN 參考資料

當專案或建構輸出目錄的路徑變更時,相對路徑維持不變。相較於絕對路徑,這有幾個優點:

  • 避免從建構輸出內容的路徑中外洩潛在的機密資訊,藉此保護使用者隱私。
  • 提升已處理內容的快取效率。
  • 讓機器人可能彼此互動,例如某個機器人在跟其他機器人之間執行動作。

另請參閱:rebase_path(x) 會傳回經認定有害的絕對路徑?

模式和反面模式

目標輸出

使用 get_target_outputs() 擷取單一元素時,GN 不會讓您在指派前下標下清單。如要解決這個問題,您可以採用下列較不優雅的解決方法:

# Appending to a list is elegant
deps += get_target_outputs(":some_target")

# Extracting a single element to use in variable substitution - ugly but reliable
_outputs = get_target_outputs(":other_target")
output = _outputs[0]
message = "My favorite output is $output"

# This expression is invalid: `output = get_target_outputs(":other_target")[0]`
# GN won't let you subscript an rvalue.

此外,get_target_outputs() 也有一些令人痛苦的限制:

  • 僅支援 copy()generated_file()action() 目標。
  • 只能查詢同一個 BUILD.gn 檔案中定義的目標。

因此,您經常會在另一個 BUILD.gn 檔案中找到一個目標的輸出路徑以硬式編碼方式寫入。這會產生一份大膽的契約,合約中斷時,很難對中斷問題進行疑難排解。請盡可能避免執行這項作業,並盡可能加入大量內嵌說明文件。

檢查類型是否為字串

雖然 GN 不允許進行完整的類型檢查,但您可以透過編寫下列這行程式碼,檢查變數是否為字串,且只有字串:

if (var == "$var") {
  # Execute code conditional on `var` type being string
}

檢查 var 是否為單例模式清單

同樣地,您可以將變數檢查是否為單例模式清單,如下所示:

if (var == [var[0]]) {
  # Execute code conditional on `var` type being a singleton list
}

不過請注意,如果類型「不是」清單或空白,就會當機。

集合運算

GN 提供清單和範圍做為匯總資料類型,但不提供地圖或集合等關聯類型。有時候,清單會使用而非資料集。以下範例包含建構變數的清單,並檢查其中一個是否為「設定檔」變化版本:

if (variants + [ "profile" ] - [ "profile" ] != variants) {
  # Do something special for profile builds
  ...
}

這是反面模式。變數的定義如下:

variants = {
  profile = true
  asan = false
  ...
}

if (variants.profile) {
  # Do something special for profile builds
  ...
}

轉寄 "*"

forward_variables_from() 會將指定變數從指定範圍或任何封閉範圍複製到目前的範圍。除非您指定 "*",否則這個選項只會直接從指定範圍複製變數。而且絕不會去掉已存在於範圍中的變數 這屬於 Gen-time 錯誤

有時候,您會想複製叫用器中的所有內容,但要從任何封閉範圍複製的特定變數除外。您將遇到以下模式:

forward_variables_from(invoker, "*", [ "visibility" ])
forward_variables_from(invoker, [ "visibility" ])

exec_script()

GN 的內建函式 exec_script 是強化 GN 功能的強大工具。和 action() 一樣,exec_script() 可以叫用外部工具。與 action() 不同,exec_script() 可以透過建構產生作業「同步」叫用工具,也就是說,您可以在 BUILD.gn 邏輯中使用工具的輸出內容。

由於這會造成效能瓶頸 (即 fx set 時間較長),因此必須謹慎使用這項功能。詳情請參閱 Chromium 團隊的這份寫入說明

已在「//.gn」中設定許可清單。如要瞭解這份許可清單的變更,請參閱 OWNERS