Ninja 的運作方式

本頁將概略說明 Ninja 在 Fuchsia 的運作方式。

一般總覽

Fuchsia 建構系統使用 Ninja 平行啟動建構指令。下列步驟說明 Ninja 的行為:

  1. 從頂層 build.ninja 檔案載入 Ninja 建構方案,此檔案本身可包含其他幾個 .ninja 檔案。

    在 Fuchsia 版本中,這些設定檔是由 GN 建構工具建立。這項作業會在記憶體中建構依附元件圖表

  2. 載入 Ninja 建構記錄,以及 deps 記錄 (如果有的話)。

    這項作業會新增在先前成功的 Ninja 建構叫用期間發現的依附元件邊緣。這樣做可快速漸進式建構,但準確性可能會增加

  3. 決定要產生哪些建構輸出內容 (又稱為「目標」)。

    從指令列上命名的目標開始,以遞迴方式逐步觀察依附元件,確定哪些最終輸出內容和中繼輸出內容會根據輸入內容過時,因而需要重新建構。需要重新執行的指令會以有向非循環圖正確排序。

  4. 根據主機系統上的 CPU 數量 (或明確的 -j<count> 參數),平行啟動必要的建構指令。

    控制平行處理的另一種方式是使用 -l<max_load> 限制系統的最大載入值。如果 ninja 知道的輸入已更新,表示指令已準備就緒。

狀態顯示

在建構期間,Ninja 會同時啟動多個指令,而且根據預設,Ninja 會緩衝處理輸出內容 (stdout 和 stderr),直到完成為止。

Ninja 也會列印狀態行 (例如,使用 fx build 時),說明以下內容:

  • 已完成的指令數量。
  • 必須執行的指令總數,才能完成建構作業2
  • 目前執行的指令數量。
  • last-completed 指令的說明。通常包括一個小型記憶 (例如 ACTIONCXX),後面接著輸出目標清單。
[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 檔案定義,該檔案可包含具有 includesubninja 陳述式的其他 *.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 甚至導入「乾淨的建構柵欄」,以解決最棘手的問題。


  1. 具體來說,Ninja 依附元件圖表與 Bazel 動作圖表非常類似,而 Ninja 目標則對應到 Bazel File 物件。

  2. 如果 Ninja 判定部分指令的輸出內容是處於最新狀態,就會在建構期間減少這個數字。

  3. 基於歷史因素,Ninja 原始碼會使用名為 Node 的 C++ 類別來模型目標,以及名為 Edge 的 C++ 類別以模型動作。不過,閱讀程式碼時這通常是「強烈」混淆的來源,因此這份文件不會遵循這種誤導慣例。