編寫 GN 範本的最佳做法

總覽

在 GN 中,範本可讓您將範本新增至 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。 就這樣系統會在工具周圍設定封裝界線,用於查看詳細資訊 例如將參數轉譯為引數

請注意,本範例會在一個檔案中定義 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 的範本 這些 API 不得用於實際工作環境目標例如:

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)。但還是取決於 可能會成為目標 (例如 executable() 會根據產生的action() 處理遞移型 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() 會將指定的變數複製到目前的 範圍。除非您 請指定 "*",這樣就只會直接複製變數 設定自訂範圍同時,也絕不會清除 您所屬的地區,那屬於一般錯誤。

有時候,您想從叫用者複製所有資料,但以下情況除外: 您想從任何像素字串中複製的特定變數 範圍。畫面上會出現以下模式:

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 使用時,請謹慎使用。 如需更多資訊,請參閱 這份寫作

已在「//.gn」中設定許可清單。如要瞭解異動內容,請參閱 OWNERS 已加入許可清單。