編寫 GN 範本的最佳做法

總覽

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

本文詳細說明建立 GN 範本的最佳做法,並提供每個最佳做法的範例。除了「Fuchsia 建構系統政策」中列出的最佳做法外,也請遵循下列最佳做法。

如需更多資訊和更完整的範例,請執行 fx gn help template,並參閱「GN 語言和作業」一文,進一步瞭解 GN 功能。

範本

在 .gni 中定義範本,在 BUILD.gn 中定義目標

從技術上來說,您可以匯入 .gni 和 BUILD.gn 檔案。不過,最佳做法是在 .gni 檔案中定義範本,並在 .gn 檔案中定義目標。這樣一來,使用者就能清楚瞭解哪些是範本。使用者想匯入範本是為了使用,絕不會想匯入目標。

文件範本和引數

記錄範本和引數,包括:

  • 範本用途和所介紹概念的一般說明。建議提供實際用法範例。
  • 所有參數都應記錄在文件中。如果是常見且只是轉送的參數 (例如 deps 或 visibility),且意義與內建 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
  ...
}

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

這項建議與一律轉送 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_deps 和 data_deps

所有採用 deps 的內建規則都會採用 public_deps 和 data_deps。部分內建規則不會區分依附元件類型 (例如 action() 會同等處理 deps 和 public_deps)。但所產生目標的依附元件可能會區分 (例如,依附於所產生 action() 的 executable() 會以不同方式處理遞移 deps 和 public_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。