建構圖表融合

建構圖形融合功能是指單一建構叫用將以正確順序執行所有必要動作,因此每個動作的輸出都「新」,不會比輸入的值「新」

Fuchsia 使用以時間戳記為準的 Ninja 建構系統。Ninja 會將建構表示為輸入/輸出檔案圖表,以及接受輸入並產生輸出內容的動作。

執行建構作業時 (例如使用 fx build) 時,Ninja 會掃遍建構圖,並執行輸出內容不存在,或輸入內容在上次執行後變更的任何動作,所有動作皆以班上順序 (依附元件之前的依附元件) 進行。

不過,建構圖的「動作」並未經過驗證,因此無法達成承諾輸出較輸入值還要新的內容,這可能導致收斂問題。

常見根本原因

建立 Ninja 收斂問題的方法有很多種,不過,過往的經驗告訴我們,這些問題的常見根本原因。

未產生輸出內容

如果宣告建構動作是為了產生輸出內容,但實際上並未產生該輸出內容 (在某些情況下,也可能從未產生),則會導致收斂問題。舉例來說,動作可能會宣告在成功時產生戳記檔案,但無法產生或輕觸這個戳記檔案,或是將檔案儲存到錯誤的位置。

輸出內容過時 (並非所有輸入內容更新)

如果輸出內容比所有輸入內容多,Ninja 就會知道這是最新輸出內容。如果一或多個輸入在儲存後發生變更,Ninja 就會重複執行產生輸出內容所需的步驟。

不過,如果產生輸出內容的動作在輸入變更時並未更新輸出內容,就會產生永久過時狀態的外觀。

導致這種情況的常見錯誤為,當動作審查輸入內容時,最後判定他們沒有對輸出內容做任何變更/變更,但並未更新輸出上的修改時間戳記 (即「觸控」或「戳記」輸出內容)。

修改輸入內容

動作可以修改其輸入內容。動作的輸入通常應只以讀取權限開啟,但這並非問題需要撰寫。也就是說,如果您的動作需要修改輸入內容,則應在寫入任何輸出內容之前完成。或者,如果您需要在寫入輸出之後修改輸入內容,請務必在結束動作前更新輸出內容的時間戳記。否則,會將一或多個輸入內容更新為較一或多個輸出內容的新項目,導致 Ninja 誤以認為您的輸出內容過時。

在動作中修改輸入內容也可能會產生競爭狀況,導致重現問題不具確定性。如果多個動作都依附於相同的輸入內容,且其中一個動作會修改輸入內容,那麼只要其中一個動作產生的輸入時間戳記比任何動作的輸出內容長,建構作業就無法收斂。在依依附元件排序的執行作業中,無法保證獨立動作的相對順序。

避免修改輸入內容。

Ninja 建構系統會透過符號連結判斷時間戳記。當軟符號連結使用 Ninja 規則做為輸入依附元件或輸出時,這可能會產生令人意想不到的後果。符號連結本身的時間戳記 (相對於目的地) 不會考量過時程度和更新間隔。如需 stat()lstat() 的說明和示範,請參閱 ninja#1186。硬連結 (不含 -sln) 有問題,會導致多個參照指向同一個檔案系統物件,因此具有相同的時間戳記。

即使只是簡單的連結動作,也可能會造成問題。假設執行簡單的動作,輸入 src、輸出 $target_out_dir/dst 以及叫用的動作為 ln src $target_out_dir/dst。面面俱到,這項動作才能正確融合。但是,action() 的行為可能會遭到建構系統的其他位置覆寫,例如將動作與其他動作包裝在一起。因此,當 src 的時間戳記比包裝函式動作的指令碼時間更早時,內部動作可能不會整合為免人工管理,接著系統會將其視為比 dst (其輸出內容,也就是輸入的時間戳記) 更早發生。copy() 不會因相同的問題而遭到包裝,因此不會經歷相同問題。

在動作輸入和輸出中避免使用符號連結和硬式編碼。

如要建立副本,建議使用內建的 copy() 目標。

時間戳記精細程度

現代檔案系統會以奈秒解析方式儲存檔案 (例如上次修改時間) 上的時間戳記。有些較舊的執行階段 (例如 Python 2.7) 會以較低的解析度保留檔案時間戳記,例如毫秒。因此,動作可能會讀取輸入內容,並寫入輸出內容包含該「現在」的時間戳記,但實際上比輸入的時間戳記更早,例如,假設輸入和輸出內容都在同一毫秒寫入,且輸出的時間戳記在毫秒後遭到截斷。

在撰寫本文時,我們設有相關機制,可確保建構作業中的所有 Python 動作都是透過 Python 3.x 執行,部分以避免發生這個問題。

建構收斂診斷

我們提供下列工具,可以診斷建構的收斂問題:

  • 修訂版本佇列中的 Ninja 免人工檢查
  • 檔案系統存取動作追蹤

Ninja 免人工檢查

Fuchsia 的修訂版本佇列 (CQ) 會驗證變更不僅成功建構,還會讓建構系統保持在單一建構叫用中收縮為免人工管理的狀態。

CQ 建構融合錯誤的範例:

fuchsia confirm no-op
ninja build does not converge to a no-op

相同的版本會在 CQ 中執行,再將變更合併到來源樹狀結構,以確保變更不會破壞建構。成功完成建構後,CQ 會再次叫用 Ninja,並預期 Ninja 回報 "no work to do"。這可以做為聲音檢查,因為正確的建構圖應會「匯合」到免人工管理。

如果這項音效檢查失敗,CQ 就會在名為 fuchsia confirm no-op 的步驟中回報失敗。

重現 Ninja 收斂問題

將來源樹狀結構同步至您的變更時,請嘗試下列方法:

fx build

這個指令應會輸出:

ninja: no work to do.

如果情況並非如此,且正在執行實際建構動作,請再次執行相同的指令。如果第二次叫用仍未產生「沒有作用」,即代表您重現了問題。如果仍抵達「沒有工作」,請嘗試下列步驟:

# Clean your build cache
rm -rf out
# Set up the build specification again
fx set ...
# Build
fx build
# Build again, expecting no-op
fx build

排解 Ninja 收斂問題

CQ 結果頁面的失敗步驟 confirm no-op 下方會顯示幾個連結:

  • 執行作業詳細資料
  • 忍者 -d 說明 -n -v
  • 骯髒的路徑

ninja -d explain -n -v 連結顯示了您應該可以使用下列指令在本機重現的資訊:

fx ninja -C $(fx get-build-dir) -d explain -n -v

「骯髒路徑」的連結會顯示關聯性最高的相同資訊子集。畫面最可能開始下方顯示的文字檔:

ninja explain: output <...> doesn't exist
...

這個檔案的每一行都像骨牌積木。您應該先看看第一個開始執行額外工作鏈結的骨牌積木,藉此排解問題。舉例來說,在上述範例中,特定輸出檔案不存在,這會導致 Ninja 重新執行禁止產生此輸出內容的建構動作,然後又重新執行相依動作。

檔案系統存取追蹤

您也可以透過建構工具追蹤動作的檔案系統存取活動。過時或遺漏輸出內容的診斷結果如下:

Not all outputs of //your:label were written or touched, which can cause subsequent
build invocations to re-execute actions due to a missing file or old timestamp.

Required writes:
...

Missing outputs:
...

Stale outputs:
...

不允許寫入的輸入內容的診斷如下所示:

Unexpected file accesses building //your/target:label, following the order they are accessed:
(FileAccessType.WRITE /path/to/input-that-should-not-be-touched.txt)

與 Ninja 免人工檢查相比,這項檢查會針對每項個別動作完成,並在動作發生時立即診斷收訊問題的其中一個原因,而不是稍後透過完整的 fx build 指令診斷出其中一個原因。這個方法可以找出一些因為競爭狀況而難以重現的問題。

排解追蹤動作失敗問題

如要在本機啟用動作追蹤,請執行下列任一操作:

  • 執行 fx set ... --args=build_should_trace_actions=true
  • 執行 fx args,在編輯器中新增 build_should_trace_actions=true 儲存並結束

然後fx build //your/failing:target

根據動作指令碼或指令的內容檢查訊息中的檔案,確認這些檔案是否屬於其中一個常見問題類別。