本頁面將概述 Ninja 在 Fuchsia 中的運作方式。
注意: 大多數開發人員應透過
fx工作流程 (例如fx build和fx set) 與建構系統互動,而不是直接執行 GN、Ninja 或 Bazel (即使是透過fx gn或fx ninja等包裝函式)。
一般總覽
Fuchsia 建構系統會使用 Ninja 平行啟動建構指令。Ninja 的行為如下:
從頂層
build.ninja檔案載入 Ninja 建構計畫,該檔案本身可以包含多個其他.ninja檔案。載入 Ninja 建構記錄和依附元件記錄 (如有)。
這項作業會新增先前成功叫用 Ninja 建構時發現的依附元件邊緣。這樣可以快速進行漸進式建構,但正確性會受到影響。
判斷需要產生哪些建構輸出內容 (又稱為「目標」)。
從指令列中命名的目標開始,以遞迴方式走訪其依附元件,判斷相對於輸入內容,哪些最終和中繼輸出內容已過時,因此需要重建。需要重新執行的指令會以有向非循環圖的正確順序排列。
根據主機系統上的 CPU 數量 (或明確的
-j<count>參數),平行啟動必要的建構指令。如要控管平行處理,也可以使用
-l<max_load>限制系統的最大負載值。當ninja知道的輸入內容更新完畢,即可執行指令。
狀態顯示
在建構期間,Ninja 會平行啟動多個指令,並預設會緩衝處理輸出內容 (stdout 和 stderr),直到完成為止。
Ninja 也會列印狀態行 (例如使用 fx build 時),說明下列事項:
- 已完成的指令數量。
- 完成建構作業必須執行的指令總數2
- 目前執行的指令數量。
- last-completed 指令的說明,通常包含一小段助記符 (例如
ACTION或CXX),後面接著輸出目標清單。
[102/345](36) ACTION path/to/some/build/artifact
上述範例表示目前已完成 345 個指令中的 102 個,且 Ninja 目前啟動了 36 個平行指令,而 path/to/some/build/artifact 是最新產生的建構構件。
您可以設定 NINJA_STATUS 環境變數,自訂狀態列的內容。
如果任何指令產生輸出內容或失敗,Ninja 會更新狀態列,顯示該指令的說明,然後列印輸出內容或錯誤訊息。接著,Ninja 會繼續列印狀態列,例如:
[102/345](24) ACTION path/to/some/build/artifact
<output of the command which generated 'path/to/some/build/artifact'>
[101/345](23) ACTION path/to/another/build/artifact
在實務上,大多數的編譯器警告都是以這種方式列印。
做為特殊例外,如果指令位於特殊 console 集區,就能直接列印到終端機。這對於需要列印自身狀態更新的長時間執行指令很有用。
Ninja 會確保一次只能啟動一個控制台指令,並暫停更新自己的狀態列,直到完成為止。不過,請注意,其他非控制台指令仍會在背景平行執行,且輸出內容會緩衝處理。Fuchsia 建構作業會對所有叫用 Bazel 的指令使用這項功能,因為這些指令往往很長,且 Bazel 會向終端機提供自己的狀態更新。
顯示 Fuchsia 專屬狀態
做為 Fuchsia 專屬的特別改良措施,Ninja 會顯示最舊的長期執行指令表格,以及執行時間,方便您瞭解建構期間的狀況。這項功能僅適用於智慧終端機。
在環境中設定 NINJA_STATUS_MAX_COMMANDS=<count>,即可變更顯示的指令數量。fx build預設為 4,如下所示:
[0/28477](260) STAMP host_x64/obj/tools/configc/configc_sdk_meta_generated_file.stamp
0.4s | STAMP obj/sdk/zircon_sysroot_meta_verify.stamp
0.4s | CXX obj/BUILD_DIR/fidling/gen/sdk/fidl/fuchsia.me...chsia.media/cpp/fuchsia.media_cpp_common.common_types.cc.o
0.4s | CXX obj/BUILD_DIR/fidling/gen/sdk/fidl/fuchsia.me...fuchsia.media/cpp/fuchsia.media_cpp.natural_messaging.cc.o
0.4s | CXX obj/BUILD_DIR/fidling/gen/sdk/fidl/fuchsia.me...dia/cpp/fuchsia.media_cpp_natural_types.natural_types.cc.o
詳情請參閱「Fuchsia 功能:待處理指令的狀態」。
Ninja 建構依附元件圖表
Ninja 從建構計畫建構的圖表只包含兩種節點3:
目標節點:目標節點只會對應至 Ninja 已知的檔案路徑。該路徑一律與建構目錄相對。
動作節點:動作節點會模擬要執行的單一指令,從一組指定的輸入檔案產生輸出檔案。
請注意下列資訊:
如果目標節點不是任何動作節點的輸出內容,則稱為來源檔案。
如果「目標」節點不是任何「動作」節點的輸入內容,就必須是特定「動作」節點的輸出內容,這類節點稱為最終輸出內容。
既是動作的輸出,也是另一個動作的輸入的目標節點,稱為中繼目標或中繼輸出。
每個動作都可以指向圖表中的零或多個輸入目標節點。
每個動作在圖表中可以有一或多個輸出目標節點。 動作不得沒有輸出,否則 Ninja 不知道何時執行指令。
動作節點沒有名稱,因此在叫用 Ninja 時無法直接參照動作節點,只能參照檔案路徑 (即目標)。
Ninja 建構計畫
Ninja 建構計畫是由建構目錄頂端的 build.ninja 檔案定義,其中可包含其他具有 include 或 subninja 陳述式的 *.ninja 檔案。以下是其最重要功能的摘要 (如需完整詳細資料,請參閱 Ninja 手冊)。
在 .ninja 檔案中,動作節點是透過 build 陳述式定義:
build <outputs>: <rule_name> <inputs>
<outputs> 是輸出路徑清單,<inputs> 是輸入路徑清單,<rule_name> 則是 Ninja 規則的名稱,可做為配方,製作要執行的最終指令。規則是由特殊的 rule 陳述式定義:
rule <rule_name>
command = <command expression>
<command expression> 可以包含特殊 $in 和 $out 關鍵字,這些關鍵字會擴展為對應建構規則的輸入和輸出清單。
rule copy_file
command = cp -f $in $out
build output.txt: copy_file input.txt
上述範例是簡單的建構計畫,告訴 Ninja 要建構 output.txt,必須執行 cp -f input.txt output.txt 指令。
隱含輸出
指令可能會有額外輸出內容,但這些內容不得出現在 $out 擴充功能中。你可以使用 | 分隔符號,將這些內容與露骨內容分開。
rule copy_file
command = cp -f $in $out && touch $out.stamp
build output.txt | output.txt.stamp: copy_file input.txt
上述範例會告知 Ninja,建構 output.txt 的指令會將 input.txt 複製到其中,並建立 output.txt.stamp 檔案。
隱含輸入內容
同樣地,您也可以在建構陳述式的右側使用 |,告知 Ninja 某些輸入內容不應從 $in 運算式展開。
rule cxx_compile
command = c++ -c $in -o $out
build foo.o: cxx_compile foo.cc | foo.h
上例會告知 Ninja,即使編譯器指令中未明確顯示 foo.h,編譯 foo.cc 時仍會使用 foo.h 做為輸入內容。
僅限訂單的輸入內容
您可以告知 Ninja 某些檔案路徑是某些輸出的執行階段依附元件,因此應「一併」建構。這會使用 build 陳述式右側的 || 分隔符,且一律須出現在任何潛在的 | 分隔符之後 (如有)。
rule cxx_binary
command = c++ -o $out $in -ldl
rule cxx_shared_library
command = c++ -shared -o $out $in
build foo.so: cxx_shared_library:
build program: cxx_binary main.cc || libfoo.so
上述範例會告知 Ninja,每當需要建構 program 時,也需要建構 foo.so,但順序並不重要。換句話說,您可以先執行產生 program 的指令,再執行產生 foo.so 的指令。在本範例中,如果二進位檔只會在執行階段透過 dlopen() 載入程式庫,這項做法就適用。
透過 restat 最佳化減少重建次數
如果內容沒有變更,部分指令可能不會變更輸出檔案的時間戳記。Ninja 可利用這點,減少建構呼叫期間要執行的指令總數。
如要支援這項功能,規則定義必須將特殊 restat 變數設為非空白值。這會導致 Ninja 在執行指令後,重新統計指令的輸出內容。如果輸出內容的修改時間沒有變更,Ninja 會視為從未需要建構該輸出內容,並從待處理指令清單中移除使用該輸出內容做為輸入內容的任何指令。
# A rule to invoke the create_manifest.py script that processes some input
# and generates a manifest as output. `restat` is set to indicate that the
# script will not update $out's timestamp if the file exists and its content
# is already correct.
rule create_manifest
command = ../../create_manifest.py --input $in --output $out
restat = 1
build package_manifest.json: create_manifest package_list.txt
build package_archive.zip: create_archive package_manifest.json
在上述範例中,如果開發人員變更 package_list.txt 的方式不會改變 package_manifest.json 輸出檔案,則不需要重新產生最終 package_archive.zip。為支援這項功能,每當 Ninja 執行指令時,都會為每個輸出檔案記錄摘要,包括指令的雜湊和最新輸入的時間戳記,並儲存在 $BUILD_DIR/.ninja_build 的特殊檔案中,稱為 Ninja 建構記錄。
在下一次叫用 Ninja 時,系統會使用建構記錄時間戳記 (如果較新),而非檔案系統時間戳記,判斷是否需要重新產生檔案。因此,在上述範例中,package_list.txt 的較新時間戳記會與 package_manifest.json 建立關聯,即使檔案系統時間戳記較舊也一樣。如果沒有這項功能,Ninja 會在每次叫用建構時,嘗試重建資訊清單檔案。
使用 depfile 在建構時探索隱含輸入內容
Ninja 啟動的指令可以產生特殊的依附元件檔案 (簡稱 depfile),列出額外 隱含 輸入內容,也就是指令的輸入內容,但不會出現在建構計畫中。Ninja 會讀取這項資訊,並記錄在名為 $BUILD_DIR/.ninja_deps 的二進位檔案中,也就是所謂的「Ninja 依附元件記錄檔」。下次叫用 Ninja 時,系統會自動載入依附元件記錄,並將所有記錄的隱含輸入內容新增至依附元件圖表。
舉例來說,這項功能適用於 C++ 編譯指令,可列出所有包含的標頭,即使這些標頭未明確列在對應的 .ninja 檔案中也一樣。如果開發人員修改這類標頭,下一次叫用 Ninja 時會看到變更,並導致重新編譯對應的 C++ 來源和任何依附元件。
方法是在規則定義中加入 depfile 變數宣告,如下所示:
rule cc
depfile = $out.d
command = gcc -MD -MF $out.d [other gcc flags here]
請注意,depfile 預設會在擷取至二進位檔依附元件記錄後,由 Ninja 移除。如要檢查記錄了哪些 depfile 依附元件,請採取下列任一做法:
執行
ninja -C <build_dir> -t deps <target>,其中<build_dir>是建構目錄,<target>是輸出檔案的路徑 (相對於<build_dir>)。-t deps選項會叫用 Ninja 工具,列印這個輸出檔案的依附元件記錄內容。但請注意,deps 記錄是只能附加的二進位檔案,因此會在多次叫用 Ninja 建構時累積
depfile依附元件,因此列出的隱含依附元件可能會比上次指令產生的依附元件還多。移除建構構件,然後使用
-d keepdepfile選項叫用ninja,這會強制 Ninja 將所有依附元件檔案留在建構目錄中 (將內容複製到二進位檔依附元件記錄後)。這樣一來,您就能手動檢查內容,例如:$ rm $BUILD_DIR/foo.o $ ninja -C $BUILD_DIR -d keepdepfile foo.o $ cat $BUILD_DIR/foo.o.d請注意,確切的
depfile路徑取決於規則定義。按照慣例,大多數指令只會在第一個輸出路徑附加.d後置字串,但 Ninja 不會強制執行這項操作。
depfile 的正確性問題
如果建構計畫沒有任何變更,deps 記錄檔會非常實用,因為 Ninja 會在下一個增量建構中偵測到哪些 depfile 列出的隱含輸入內容發生變更,並重建任何依附於這些內容的項目。
不過,當建構計畫變更時,Ninja 依附元件記錄中的項目可能會過時,並在下一次 Ninja 呼叫的依附元件圖表中加入不正確的邊緣。有時,這些項目會中斷下一次建構呼叫。如果依附元件從建構計畫中移除,但仍記錄在依附元件記錄中,就特別容易發生這種情況。
實際上,這會導致隨機的增量建構失敗,可能發生在本地或基礎架構建構工具 (CQ 或 CI 中)。很遺憾的是,目前無法解決這個問題,因為 deps 記錄是 Ninja 設計的一部分,而且無法偵測「過時」的項目 (因為舊建構計畫的確切詳細資料在使用時早已消失)。
通常的解決方法是執行簡潔式建構作業。Fuchsia 甚至實作了「簡潔式建構柵欄」,以解決最棘手的情況。
-
更具體來說,Ninja 依附元件圖表與 Bazel 動作圖表非常相似,而 Ninja 目標則對應至 Bazel File 物件。↩
-
如果 Ninja 判斷某些指令的輸出內容為最新版本,這個數字在建構期間可能會減少。↩
-
基於歷史原因,Ninja 原始碼使用名為
Node的 C++ 類別來模擬目標,並使用名為Edge的 C++ 類別來模擬動作。不過,讀取程式碼時,這類做法經常會造成極大的 混淆,因此本文不會採用這種誤導性的慣例。 ↩