編寫 GN 範本的最佳做法

總覽

在 GN 中,範本提供方法,可新增至 GN 的內建目標類型。基本上,範本是 GN 建構可重複使用函式的主要方式。範本定義會放在可匯入目標 .gn 檔案的 .gni (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,僅此而已。這會在工具周圍設定封裝界線,以處理將參數轉換為引數等詳細資料。

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

# //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
  ...
}

請注意,如果內部動作的上層範圍定義了 testonlyforward_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

這項建議與一律轉送 testonly 類似,但只適用於主要目標 (名為 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) returning absolute paths considered harmful?

模式和反模式

目標輸出內容

使用 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 提供清單和範圍做為匯總資料類型,但不提供關聯類型,例如對應或集合。有時會使用清單而非集合。以下範例包含建構變數清單,並檢查其中一個是否為「profile」變數:

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() 會將指定變數從指定範圍 或任何封閉範圍複製到目前範圍。除非您指定 "*",否則只會直接複製指定範圍的變數。而且絕不會覆寫範圍內已有的變數,否則會發生產生時間錯誤。

有時您會想複製呼叫端的所有內容,但要從任何封閉範圍複製特定變數。您會遇到以下模式:

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