本頁將概略說明 Ninja 在 Fuchsia 的運作方式。
一般總覽
Fuchsia 建構系統使用 Ninja 平行啟動建構指令。下列步驟說明 Ninja 的行為:
從頂層
build.ninja
檔案載入 Ninja 建構方案,此檔案本身可包含其他幾個.ninja
檔案。載入 Ninja 建構記錄,以及 deps 記錄 (如果有的話)。
這項作業會新增在先前成功的 Ninja 建構叫用期間發現的依附元件邊緣。這樣做可快速漸進式建構,但準確性可能會增加。
決定要產生哪些建構輸出內容 (又稱為「目標」)。
從指令列上命名的目標開始,以遞迴方式逐步觀察依附元件,確定哪些最終輸出內容和中繼輸出內容會根據輸入內容過時,因而需要重新建構。需要重新執行的指令會以有向非循環圖正確排序。
根據主機系統上的 CPU 數量 (或明確的
-j<count>
參數),平行啟動必要的建構指令。控制平行處理的另一種方式是使用
-l<max_load>
限制系統的最大載入值。如果ninja
知道的輸入已更新,表示指令已準備就緒。
狀態顯示
在建構期間,Ninja 會同時啟動多個指令,而且根據預設,Ninja 會緩衝處理輸出內容 (stdout 和 stderr),直到完成為止。
Ninja 也會列印狀態行 (例如,使用 fx build
時),說明以下內容:
- 已完成的指令數量。
- 必須執行的指令總數,才能完成建構作業2
- 目前執行的指令數量。
- last-completed 指令的說明。通常包括一個小型記憶 (例如
ACTION
或CXX
),後面接著輸出目標清單。
[102/345](36) ACTION path/to/some/build/artifact
以上範例表示目前已完成 102 指令 (共 345 個),且目前 Ninja 啟動了 36 個平行指令,而 path/to/some/build/artifact
是要產生的最新建構構件。
您可以設定 NINJA_STATUS
環境變數,藉此自訂狀態行的內容。
如有任何指令產生一些輸出內容或輸出失敗,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.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
,但順序並不重要。換句話說,您可以在產生 foo.so
的指令之前執行產生 program
的指令。在這個範例中,如果二進位檔只會在執行階段透過 dlopen()
載入程式庫,這個做法就有效。
以休息最佳化的方式減少重新建構
如果輸出檔案的內容未變更,部分指令可能不會變更其輸出檔案的時間戳記。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 deps 記錄」) 進行記錄。在下一個 Ninja 叫用時,系統會自動載入 deps 記錄,並將所有記錄的隱含輸入內容新增至依附元件圖表。
舉例來說,這對於 C++ 編譯指令來說非常實用,可以列出所有內含標頭,甚至是未在對應的 .ninja
檔案中明確列出的標頭。如果開發人員修改了這類標頭,下次 Ninja 叫用就會發現變更,並導致對應的 C++ 來源及其任何依附元件重新編譯。
方法是在規則定義中加入 depfile
變數宣告,如下所示:
rule cc
depfile = $out.d
command = gcc -MD -MF $out.d [other gcc flags here]
請注意,根據預設,Ninja 會在 depfile
擷取到二進位 deps 記錄後將其移除。如要檢查記錄的是哪些 depfile
依附元件,請採取下列任一做法:
執行
ninja -C <build_dir> -t deps <target>
,其中<build_dir>
是建構目錄,<target>
則是相對於<build_dir>
的輸出檔案路徑。-t deps
選項會叫用 Ninja 工具,以便輸出此輸出檔案的 deps 記錄內容。但是請注意,deps 記錄是只能附加二進位資料的檔案,因此會在多個 Ninja 建構叫用中累計
depfile
依附元件,因此這可能會列出比上次指令產生的隱含依附元件更多。移除建構構件,然後使用
-d keepdepfile
選項叫用ninja
,這會強制 Ninja 將所有依附元件檔案留在建構目錄中 (一旦將內容複製到二進位檔 deps 記錄之後)。這可讓您手動檢查內容,例如:$ rm $BUILD_DIR/foo.o $ ninja -C $BUILD_DIR -d keepdepfile foo.o $ cat $BUILD_DIR/foo.o.d
請注意,確切的
depfile
路徑取決於規則定義。依照慣例,大多數指令只會在第一個輸出路徑後方加上.d
後置字串,但 Ninja 不會強制執行這項操作。
修正檔案修正相關問題
當建構計畫沒有任何變更時,Deps 記錄運作效能極佳,因為 Ninja 會在下一個漸進式建構作業中,偵測到 depfile
列出隱含輸入內容時已變更的隱含輸入內容,然後重建依附於這些輸入內容的任何項目。
不過,當建構方案有所變更時,Ninja 依附元件記錄中的項目可能會變成「過時」,並在下一個 Ninja 叫用的依附元件圖表中新增錯誤邊緣。有時候,這些方法會「中斷」下一個建構叫用。尤其是當依附元件從建構計畫中移除,但仍記錄在 deps 記錄時。
在實務上,這會導致本機或基礎架構建構工具 (在 CQ 或 CI) 中發生隨機的漸進式建構作業失敗。此外,目前確實有方法可解決問題,因為 deps 記錄是 Ninja 的設計的一部分,所以無法偵測「過時」項目 (因為舊建構方案的確切詳細資料一旦使用就會消失)。
常見的解決方法是執行乾淨的版本。Fuchsia 甚至導入「乾淨的建構柵欄」,以解決最棘手的問題。
-
具體來說,Ninja 依附元件圖表與 Bazel 動作圖表非常類似,而 Ninja 目標則對應到 Bazel File 物件。
-
如果 Ninja 判定部分指令的輸出內容是處於最新狀態,就會在建構期間減少這個數字。
-
基於歷史因素,Ninja 原始碼會使用名為
Node
的 C++ 類別來模型目標,以及名為Edge
的 C++ 類別以模型動作。不過,閱讀程式碼時這通常是「強烈」混淆的來源,因此這份文件不會遵循這種誤導慣例。