總覽
在 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 轉送至內部目標,請採取下列行動:
- 您的內部目標可能無法建構,因為使用者可能會傳遞
testonly依附元件給您。 - 使用者發現自己的
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。