密封建構動作

Fuchsia 的建構系統使用工具來追蹤建構動作執行的檔案系統動作,以便偵測是否正確偵測建構動作,並完整指出其輸入和輸出內容。

如果您遇到類似以下的錯誤,請繼續閱讀本指南:

Unexpected file accesses building //some/target:label ...
(FileAccessType.READ /path/to/file/not/declared/as/input)

或者,如果您查看 action()action_foreach() 目標,如下所示:

action("foo") {
  ...
  hermetic_deps = false
}

建構圖形正確性

系統會將建構作業定義為有向非循環圖,讓動作的輸入內容會流入這些變數,輸出內容也會從這些圖表流出。舉例來說,如果將 .cc 檔案編譯成 .o 檔案的動作,則系統會將來源檔案做為輸入,並將物件檔案做為輸出內容。編譯中使用的 .h 標頭都會視為同一動作的輸入內容。

此圖形表示法可確保建構系統可以正確執行漸進式建構。漸進式建構作業是指在執行建構作業後,某些動作的輸入內容已變更,而現在系統要求重新建構建構系統。在漸進式建構作業中,建構系統會嘗試以最少所需的工作量執行所需工作,只重新建構輸入內容變更的動作 (無論是因為使用者對來源所做的修改,或是因需要重新執行的其他動作的輸出內容變更)。

針對建構圖中的任何動作,系統必須為所有輸入和輸出內容進行狀態,才能正確顯示建構圖,並讓動作可保持一致。不過,其驗證作業並未由基礎建構系統 Ninja 進行驗證。建構動作會在使用者的本機環境中執行,並具備整個檔案系統的完整存取權,包括來源樹狀結構和 out/ 目錄中的所有檔案,因此不受沙箱機制保護,且可連線至任何位置。

未能宣告輸入內容,會導致在更新輸入內容時,無法重新執行動作 (以及所有下游)。如未宣告做為其他動作的輸入內容,就會產生相關動作之間的競爭條件,在此情況下,單一建構叫用可能會錯過時間戳記更新,並顯示為無法在單一叫用中交涉失敗 (請參閱 Ninja no-op)。

如果您在閱讀這些內容時,可能表示您處理的建構動作無法完整呈現其一或多項的輸入或輸出內容。

使用自訂動作擴充建構

開發人員可以使用 GN 中繼建構系統,在 BUILD.gn 檔案中定義自訂動作。方法是使用 actionaction_foreach。自訂動作可讓開發人員在建構時叫用自訂工具,並將這些工具連結至依附元件圖表,以便在建構期間叫用工具,並在輸入內容有變時,正確地重新叫用漸進式建構作業。

動作會使用下列參數來狀態相應的輸入內容:

  • script:要執行的工具。這通常是 Python 指令碼,但可以是任何可以在主機上執行的程式。
  • inputs:在工具中做為資料輸入的檔案。例如,如果工具壓縮檔案,則要壓縮的檔案會列為輸入項目。
  • sources:系統會將其視為與 inputs 相同。只有語意不同,因為 sources 通常用於工具的 script 所用的其他檔案,例如依附的 Python 或指令碼程式庫。

動作會使用以下參數來狀態其輸出內容:

  • outputs:每個動作都必須產生至少一個輸出檔案。不會產生輸出檔案的動作 (例如會驗證特定輸入是否正確無誤的動作) 通常會產生「戳記檔案」,用來代表該動作已執行且可以空白的指標。

Dep 檔案

如果在執行動作之前不知道動作的部分輸入內容,則可以另外指定 depfile。分割檔案清單輸入動作的一或多個輸出內容,這些輸入項目在執行階段找到的。depfile 的格式是一或多行,如下所示:

[output_file1] [output_file2...]: [input_file1] [input_file2...]

depfile 中的所有路徑都必須相對於 root_build_dir (已設為動作的目前工作目錄)。另請參閱:優先採用 rebase_path() 的相對路徑

編譯器等工具應該能支援以解壓縮檔案的形式,發出編譯使用的所有檔案的追蹤記錄。

偵測非密封動作的檔案系統動作追蹤

Fuchsia 建構系統會使用檔案系統動作追蹤工具,偵測動作是否讀取或寫入未列為 BUILD.gn 檔案或解碼器檔案 (如上所示) 的讀取或寫入檔案。這可取代執行動作的沙箱,也可作為排序的執行階段清理器。

如果您讀取這個頁面,可能表示系統發生錯誤。該錯誤將明確列出哪些檔案已讀取或寫入,但並未在 BUILD.gn 或依附元件檔案中指定為輸入/輸出。您應修正這些遺漏,並嘗試重新建構,直到錯誤持續發生為止。

您需要確保已啟用動作追蹤功能,才能在本機建構中重現這個錯誤:

fx set what --args=build_should_trace_actions=true

或互動方式執行 fx args、新增 build_should_trace_actions=true 行、儲存並結束

請注意,如果您的動作沒有意義定義,且您尚未修正,那麼在嘗試重建動作時,可能就不會遇到錯誤。由於動作並未定義結構,因此可能無法正確從漸進式建構中取得 (也就是您要解決的問題的一部分)。如要強制執行所有建構動作,您必須先清除建構作業的輸出快取:

fx clean

根據預設,CQ 會對所有變更執行這些密封檢查。方法是使用上述的 build_should_trace_actions=true 引數,讓開發人員可在本機重現完全相同的追蹤建構。

略過密封動作檢查

以下是目前密封不通的動作,會設定下列參數:

action("foo") {
  ...
  # TODO(https://fxbug.dev/xxxxx): delete the line below and fix this
  hermetic_deps = false
}

這麼做會略過上述檢查。如果發現動作出現這個抑制情形,請移除抑制內容、嘗試按照上述方法重現問題,並修正問題。

如果不立即修正錯誤,請以「[hermetic]」做為錯誤標題,並在說明中加入失敗建構動作的追蹤記錄輸出內容。如果您知道存取違規事件的來源,請留下註解。

常見問題及修正方式

缺少輸入/輸出內容

有時在建構期間可知道輸入/輸出,但並未指定,或指定錯誤。這類問題很常見,而且可以輕鬆解決。 例如:

動作執行階段才有已知的輸入來源

如上所述,有時並非所有輸入內容在建構時都會知道,因此無法在 BUILD.gn 定義中指定。這就是「depfiles」的用途。

您可以在這裡查看範例,瞭解如何修正建構動作以產生依附元件:

輸入/輸出內容缺少的動作引數

建構動作通常是指令碼,會將特定檔案路徑做為引數。

action("foo") {
  script = "concatenate.py"
  outputs = [ "$target_out_dir/file1_file2.txt" ]
  args = [
    "--concat-from",
    rebase_path("data/file1.txt", root_build_dir),
    rebase_path("data/file2.txt", root_build_dir),
    "--output",
  ] + outputs
}

在上述情況下,您會收到 concatenate.pydata/file1.txtdata/file2.txt 讀取的動作追蹤程式錯誤。錯誤很容易發現,因為您可以看到這些路徑是以引數形式傳遞至指令碼,但是並未列為輸入或輸出。就技術上來說,採用引數可以傳遞路徑,但實際上並未讓指令碼讀取/寫入這些路徑,但這種情況很不可能。

解決方法如下:

action("foo") {
  script = "concatenate.py"
  sources = [
    "data/file1.txt",
    "data/file2.txt",
  ]
  outputs = [ "$target_out_dir/file1_file2.txt" ]
  args = [
    "--concat-from",
  ] + rebase_path(sources, root_build_dir) + [
    "--output",
  ] + outputs
}

展開檔案中的引數

Python 指令碼是將檔案內容展開為引數 (也稱為「回應檔案」) 的常見模式,您可以在 BUILD.gn 中找到:

action("foo") {
   script = "myaction.py"
   args = [ "@" + rebase_path(args_file, root_build_dir) ]
   ...
}

然後在相關聯的 Python 檔案 myaction.py 中找到含有 fromfile_prefix_chars 的引數剖析器:

def main():
    parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
    args = parser.parse_args()
    ...

上述問題是 Python 指令碼在執行階段讀取 args_file,且應指定為輸入內容。修正方法如下:

action("foo") {
   script = "myaction.py"
   inputs = [ args_file ]
   args = [ "@" + rebase_path(args_file, root_build_dir) ]
   ...
}

如果您需要從 GN 中的清單中快速填入這類檔案,可以使用 write_file()

action("foo") {
  args_file = "${target_gen_dir}/${target_name}.args"
  write_file(args_file, a_very_long_list_of_args)
  args = [ "@" + rebase_path(args_file, root_build_dir) ]
  ...
}

附註,GN 提供了 response_file_contents (而非 write_file) 做為簡便的替代方案。不過,由於基於 Ninja 的錯誤,我們目前不允許在建構作業中使用 response_file_contents

建立及刪除暫存檔

在建構動作中,這是建立暫存檔案的常見模式。請勿將暫存檔案列為輸出,只要建立暫存檔的相同動作也會在傳回前刪除暫存檔案。

暫存檔案應儲存在 target_out_dirtarget_gen_dir 底下。我們不建議使用全域臨時儲存空間 (例如 /tmp$TMPDIR),或在結帳/輸出目錄以外的任何讀取和寫入作業,因為這麼做可能會導致建構失敗的疑難排解程序更加困難,因為檔案可能需要從檔案系統的其他位置復原,才能判斷發生了什麼問題。

建立及刪除臨時目錄

有時需要在暫存目錄中建立暫存檔案。再次提醒您,只要建立暫存目錄的動作也會在傳回前以遞迴方式刪除該目錄。

shutil.rmtree 是用於刪除臨時目錄的常用函式。然而,由於追蹤器的限制,這有時會導致意外讀取。另請參閱:「問題 75057:在動作追蹤器中妥善處理從 openil.rmtree 刪除目錄」一節。

要突破這項限制的一種方法,就是只建立暫存檔案,而非臨時目錄。暫存檔案應在 target_out_dirtarget_gen_dir 下寫入。

這種情況有時會發生無法修改的情況,例如臨時目錄是由外部建構工具建立,而該外部建構工具也無法修改。在此情況下,替代方法是為臨時目錄 (例如 __untraced_foo_tmp_outputs__) 設定特殊名稱,並將這些目錄加入動作追蹤程式中。追蹤程式會忽略對這個特殊目錄中檔案的存取權。有鑑於此,這項功能不應輕微使用。

舉例來說,假設 bar.py 一律會刪除傳遞給它的 --tmp-dir 中的所有檔案,然後重新填入:

action(target_name) {
  script = "bar.py"
  args = [
    "--tmp-dir"
    rebase_path("${target_gen_dir}/${target_name}/__untraced_bar_tmp_outputs__", root_build_dir)
  ]
  ...
}

然後在動作追蹤器中,在 ignored_path_parts 中新增項目:

ignored_path_parts = {
  # Comment with clear explanation on why this is necessary,
  # preferably with a link to an associated bug for more context.
  "__untraced_bar_tmp_outputs__",
  ...
}

在 CQ 中回報的錯誤,無法在本機重現

首先,請確認您使用的是上述建構引數 build_should_trace_actions=true

如果 CQ 回報 Python 檔案意外讀取到 action_tracer.py 意外讀取,但您無法在本機環境中重現這個問題,原因可能是編譯完成的是整個樹狀結構的 __pycache__ 目錄中快取的 Python 檔案 (例如 find third_party -type d -name __pycache__)。快速解決方法是刪除這些目錄中的所有 *.pyc 檔案。這種偽陰性的原因是檔案系統從未開啟原始 .py 檔案,因此不會回報為已觸碰,因此不會觸發密學檢查。

Python 以外的檔案類型可能會因為類似原因而無法重現。

另請參閱:「開啟專案中的密封動作