的增量構(gòu)建引擎架構(gòu)與落地實(shí)踐)
2023年我把Buck2的源碼從GitHub拉下來(lái)的時(shí)候第一反應(yīng)是Meta真敢把一個(gè)每天支撐幾萬(wàn)開發(fā)者提交的內(nèi)部構(gòu)建系統(tǒng)直接開源。第二反應(yīng)才是這代碼結(jié)構(gòu)比想象中干凈太多。作為一個(gè)長(zhǎng)期跟Bazel、GN、CMake、sbt這些構(gòu)建系統(tǒng)打交道的工程師我一直在等一個(gè)真正“從底層設(shè)計(jì)就面向monorepo和遠(yuǎn)端執(zhí)行”的開源方案。Buck2的出現(xiàn)基本把構(gòu)建系統(tǒng)這潭水?dāng)嚮盍恕_@篇東西我不會(huì)去復(fù)述官方文檔也不會(huì)教你寫B(tài)UCK文件而是做一次“源碼視角”的盡調(diào)Buck2的架構(gòu)到底怎么分層、核心執(zhí)行模型長(zhǎng)什么樣、增量計(jì)算和緩存是怎么用Rust做到的、企業(yè)級(jí)的坑在哪里。如果你正在做構(gòu)建系統(tǒng)選型或者想在內(nèi)部推一套高性能跨語(yǔ)言構(gòu)建方案這篇文章應(yīng)該能幫你在幾個(gè)小時(shí)之內(nèi)把Buck2的骨架摸清楚。1. 為什么一個(gè)構(gòu)建引擎值得逐行看源碼構(gòu)建系統(tǒng)在多數(shù)技術(shù)團(tuán)隊(duì)眼里就是“裝好后沒人再碰的底層設(shè)施”。可一旦工程規(guī)模上來(lái)問(wèn)題就會(huì)變得非?,F(xiàn)實(shí)全量構(gòu)建要半小時(shí)以上、增量構(gòu)建失效頻繁、語(yǔ)言生態(tài)各搞一套、CI里跑一次流水線貴得離譜。這時(shí)候你會(huì)發(fā)現(xiàn)構(gòu)建引擎的選擇直接決定研發(fā)效率天花板。而Buck2不是又一個(gè)封裝腳本它是一個(gè)正經(jīng)的操作系統(tǒng)級(jí)“調(diào)度器”既要管進(jìn)程、管緩存、管網(wǎng)絡(luò)通信還要設(shè)計(jì)一套自己的增量計(jì)算模型。1.1 先搞清楚Buck2在解決什么Meta內(nèi)部之前的構(gòu)建系統(tǒng)叫Buck1是Java寫的設(shè)計(jì)思路借鑒了Bazel的前身Blaze。Buck1在Facebook大規(guī)模使用很多年后暴露出幾個(gè)挺棘手的問(wèn)題并發(fā)模型老化、緩存失效邏輯不夠精確、對(duì)跨語(yǔ)言項(xiàng)目支持不夠平滑。與其繼續(xù)修修補(bǔ)補(bǔ)Meta選擇用Rust從零寫一個(gè)Buck2。這里面的關(guān)鍵判斷是構(gòu)建系統(tǒng)本身就是一個(gè)“高頻并發(fā)、強(qiáng)緩存、狀態(tài)復(fù)雜”的系統(tǒng)軟件非常適合Rust這種能手動(dòng)控制內(nèi)存又不犧牲性能的語(yǔ)言。源碼里可以清楚看到這個(gè)目標(biāo)Buck2不是簡(jiǎn)單把“編譯命令”串起來(lái)而是把所有構(gòu)建任務(wù)抽象成可計(jì)算、可緩存、可分布執(zhí)行的“動(dòng)作”。在一個(gè)大型repo里你可以同時(shí)跑C、Python、Rust、TypeScript的構(gòu)建任務(wù)它們共享一套底層的緩存和調(diào)度機(jī)制。這才是“跨語(yǔ)言構(gòu)建引擎”的真正含義而不是提供一個(gè)能調(diào)用各種編譯器的腳本入口。1.2 與Bazel的本質(zhì)差異你可能會(huì)問(wèn)Bazel不也是干這個(gè)的嗎從用戶角度看Buck2和Bazel確實(shí)長(zhǎng)得像都是聲明式構(gòu)建規(guī)則、都有遠(yuǎn)端執(zhí)行協(xié)議、都用Starlark做擴(kuò)展語(yǔ)言。但源碼層面有一個(gè)非常核心的區(qū)別Buck2把“動(dòng)態(tài)依賴”作為一等公民。傳統(tǒng)構(gòu)建系統(tǒng)的靜態(tài)圖是需要提前把每個(gè)目標(biāo)的所有依賴聲明清楚Buck2則允許在執(zhí)行過(guò)程中根據(jù)中間結(jié)果繼續(xù)發(fā)現(xiàn)新的依賴。這個(gè)能力在處理“生成代碼再編譯”這類場(chǎng)景時(shí)極有價(jià)值。Bazel也有dynamic dependencies但支持度沒有Buck2這樣深入底層。DICE就是為這種動(dòng)態(tài)圖計(jì)算而生的后面我會(huì)專門拆DICE的實(shí)現(xiàn)思路。2. 頂層架構(gòu)從客戶端到后臺(tái)守護(hù)進(jìn)程很多人在用Buck2時(shí)會(huì)覺得它好快其實(shí)一部分原因是它不是一條命令跑完就結(jié)束而是有一個(gè)常駐后臺(tái)進(jìn)程類似開發(fā)服務(wù)器。你敲下buck2 build的時(shí)候?qū)嶋H是向這個(gè)后臺(tái)進(jìn)程發(fā)起一次RPC請(qǐng)求。這樣做的好處是解析過(guò)的BUCK文件、緩存的計(jì)算結(jié)果、做過(guò)指紋分析的文件狀態(tài)都能留在內(nèi)存里下一次構(gòu)建省掉大量冷啟動(dòng)開銷。2.1 客戶端、服務(wù)端和交互協(xié)議在Buck2源碼里倉(cāng)庫(kù)被劃分成多個(gè)crate其中最外層的是buck2_client和buck2_server。buck2_client負(fù)責(zé)命令行參數(shù)解析、啟動(dòng)后臺(tái)進(jìn)程、建立連接和整理用戶輸出buck2_server則是真正的構(gòu)建引擎后端它管理所有構(gòu)建狀態(tài)、執(zhí)行計(jì)算圖、調(diào)度遠(yuǎn)程執(zhí)行。二者之間的通信協(xié)議不是純文本JSON而是用protobuf定義的一套結(jié)構(gòu)化RPC。這樣做的好處是不同語(yǔ)言寫的客戶端也可以方便對(duì)接比如編輯器插件可以只調(diào)這些接口不需要直接和構(gòu)建內(nèi)核打交道。我第一次看到這個(gè)架構(gòu)時(shí)想到的是這不就是一個(gè)“構(gòu)建操作系統(tǒng)”嗎它的后臺(tái)服務(wù)管理內(nèi)存狀態(tài)和并發(fā)任務(wù)客戶端只是薄薄的一層翻譯殼。如果你是做CI集成的最要緊的是理解這個(gè)進(jìn)程模型——每次構(gòu)建都別簡(jiǎn)單用命令行加參數(shù)而是考慮復(fù)用同一個(gè)服務(wù)進(jìn)程或者至少要知道后臺(tái)進(jìn)程的資源占用和失效策略。2.2 DICE增量計(jì)算引擎是絕對(duì)核心如果說(shuō)Buck2是一臺(tái)汽車DICEDynamic Incremental Computation Engine就是它的發(fā)動(dòng)機(jī)。在源碼里dice這個(gè)crate不是一個(gè)輔助模塊它幾乎貫穿所有構(gòu)建階段。DICE的核心是“帶依賴追蹤的可緩存計(jì)算單元”你定義一些計(jì)算任務(wù)每個(gè)任務(wù)可以聲明它依賴哪些輸入DICE會(huì)記錄任務(wù)的結(jié)果和依賴關(guān)系當(dāng)輸入變化時(shí)DICE只重跑受影響的任務(wù)其他結(jié)果直接從緩存里拿。這個(gè)思路寫起來(lái)不復(fù)雜復(fù)雜的是在并發(fā)環(huán)境下做精確失效。Buck2的做法是給每個(gè)計(jì)算單元一個(gè)抽象的Key所有輸入都通過(guò)Key來(lái)訪問(wèn)底層再配合文件系統(tǒng)的change monitor和內(nèi)容hash做失效判斷。DICE還有事務(wù)機(jī)制可以把一組計(jì)算包裝成一個(gè)事務(wù)事務(wù)內(nèi)要么全部成功提交要么整體回滾這樣不會(huì)出現(xiàn)緩存里殘存半截狀態(tài)的臟數(shù)據(jù)。源碼里對(duì)事務(wù)邊界的處理非常細(xì)致這也是它比很多自研構(gòu)建緩存更穩(wěn)的核心原因。2.3 構(gòu)建階段解析、配置、分析、執(zhí)行Buck2的構(gòu)建流程可以分為幾個(gè)大的階段你可以在buck2_build_api這個(gè)crate里找到對(duì)應(yīng)的核心數(shù)據(jù)結(jié)構(gòu)。解析階段讀取BUCK文件用Starlark解釋器生成“目標(biāo)圖”的原始信息。配置階段把目標(biāo)適配到特定平臺(tái)和構(gòu)建模式比如Debug還是ReleaseAndroid還是iOS。分析階段展開規(guī)則生成具體的動(dòng)作列表和輸出聲明這個(gè)階段也是DICE介入最深的地方。執(zhí)行階段把動(dòng)作調(diào)度到本地或遠(yuǎn)端執(zhí)行校驗(yàn)輸出指紋最后產(chǎn)生最終產(chǎn)物。這四個(gè)階段不是嚴(yán)格串行的很多目標(biāo)可以在不同階段之間交叉推進(jìn)。源碼里大量使用了Future和async配合Rust的tokio運(yùn)行時(shí)可以讓不同目標(biāo)的解析、分析和執(zhí)行重疊進(jìn)行。這種設(shè)計(jì)讓Buck2在大型依賴圖上能壓榨出額外并發(fā)度也是它性能較好的原因之一。3. 核心源碼拆解緩存、指紋和遠(yuǎn)端執(zhí)行盡調(diào)一份源碼最重要的不是看它寫了多少注釋而是看它的核心模塊怎么處理邊界條件。Buck2里最值得花時(shí)間讀的東西第一是DICE的緩存與失效邏輯第二是指紋計(jì)算第三是遠(yuǎn)端執(zhí)行器的抽象。3.1 DICE的緩存如何做到精確失效很多構(gòu)建系統(tǒng)用“文件修改時(shí)間”判斷文件是否變化這在大型倉(cāng)庫(kù)里極不可靠。Buck2默認(rèn)會(huì)記錄文件的digest哈希按下map的key value變化來(lái)做失效。DICE在底層維護(hù)了類似“內(nèi)存狀態(tài)樹”的結(jié)構(gòu)每個(gè)計(jì)算Key對(duì)應(yīng)一個(gè)狀態(tài)狀態(tài)里存的是依賴版本和結(jié)果。當(dāng)某個(gè)依賴文件變更時(shí)變更事件會(huì)向上傳播所有依賴它的計(jì)算節(jié)點(diǎn)都可能失效。源碼里有個(gè)很關(guān)鍵的做法DICE不會(huì)立刻把所有失效任務(wù)全部重算而是等到某個(gè)下游任務(wù)真正需要這個(gè)結(jié)果時(shí)才懶加載式地計(jì)算。這個(gè)懶加載機(jī)制避免了構(gòu)建時(shí)“炸鍋式”重算只算真正被需要的部分。你在讀dice crate源碼時(shí)會(huì)看到大量關(guān)于“dirty/clean”狀態(tài)流轉(zhuǎn)的邏輯這就是Buck2增量構(gòu)建的核心價(jià)值。3.2 指紋Fingerprint與內(nèi)容尋址的設(shè)計(jì)動(dòng)作執(zhí)行完以后Buck2怎么知道這次執(zhí)行結(jié)果是不是和上次一樣答案是指紋機(jī)制。在源碼中動(dòng)作會(huì)聲明自己的輸入、環(huán)境變量、命令行參數(shù)等信息Buck2把它們組合成一個(gè)確定性指紋。執(zhí)行前它會(huì)先查緩存里有相同指紋的歷史結(jié)果如果有就直接復(fù)用輸出文件如果沒有才真正啟動(dòng)進(jìn)程去跑。這里有一個(gè)隱含但極重要的點(diǎn)對(duì)于“確定性”的要求非常嚴(yán)格。如果某個(gè)工具輸出了帶隨機(jī)時(shí)間戳的文件或者通過(guò)絕對(duì)路徑訪問(wèn)了未聲明的輸入指紋機(jī)制就會(huì)失效緩存命中率大幅下降。我在實(shí)戰(zhàn)中遇到過(guò)很多團(tuán)隊(duì)把Buck2接入復(fù)雜項(xiàng)目后發(fā)現(xiàn)構(gòu)建總是全量重新執(zhí)行最后定位到是代碼生成腳本讀了環(huán)境變量但沒有在action里聲明這個(gè)變量。解決方式就是所有環(huán)境變量依賴都要顯式列出來(lái)并且所有非確定性的輸出都要在動(dòng)作后做清洗。3.3 遠(yuǎn)端執(zhí)行不是簡(jiǎn)單的“把命令扔到別的機(jī)器”Buck2支持REAPIRemote Execution API這意味著你可以接上Bazel那套遠(yuǎn)端執(zhí)行服務(wù)比如Buildbarn、BuildGrid也可以通過(guò)Meta內(nèi)部的一些基礎(chǔ)設(shè)施。源碼里把遠(yuǎn)端執(zhí)行抽象成一個(gè)“ActionRunner”接口既能跑本地進(jìn)程也能把a(bǔ)ction發(fā)給遠(yuǎn)端集群。真正有意思的是它如何保證遠(yuǎn)端執(zhí)行的結(jié)果和本地一致。Buck2會(huì)要求動(dòng)作聲明“輸出目錄”和“輸出文件”遠(yuǎn)端執(zhí)行完成后后臺(tái)會(huì)把這些輸出文件流式傳回并重新校驗(yàn)內(nèi)容哈希。如果校驗(yàn)不一致這次執(zhí)行會(huì)被標(biāo)記失敗而不是默默吞掉。源碼層面還有“mingle”和“write”任務(wù)來(lái)處理大輸出文件的流式傳輸這個(gè)設(shè)計(jì)能避免把幾十GB的產(chǎn)物一次性塞進(jìn)內(nèi)存。3.4 跨語(yǔ)言支持靠的是規(guī)則和動(dòng)作抽象跨語(yǔ)言構(gòu)建的難點(diǎn)不是“能調(diào)用編譯器”而是“讓不同語(yǔ)言共享一套依賴調(diào)度和產(chǎn)物緩存”。Buck2的做法是把每種語(yǔ)言封裝成一個(gè)個(gè)規(guī)則rule規(guī)則內(nèi)部生成標(biāo)準(zhǔn)化的“動(dòng)作”。action就像一個(gè)大統(tǒng)一接口描述“要運(yùn)行什么命令、讀出哪些文件、寫出哪些文件”。底層緩存和執(zhí)行系統(tǒng)只關(guān)心action不關(guān)心它是來(lái)自C還是Rust規(guī)則。在源碼里buck2_build_api的action模塊是一個(gè)非常干凈的抽象層。只要某個(gè)語(yǔ)言前端最終能將編譯過(guò)程拆解成一組action它就能自動(dòng)獲得緩存、遠(yuǎn)端執(zhí)行、并發(fā)調(diào)度、失敗重試這套能力。所以你在用Buck2時(shí)說(shuō)“支持Python”并不是因?yàn)橐鎯?nèi)部硬編碼了Python邏輯而是提供了一套Python rule實(shí)現(xiàn)。這個(gè)可插拔架構(gòu)是Buck2跨語(yǔ)言能力的關(guān)鍵也是源碼里最有學(xué)習(xí)價(jià)值的部分。4. 源碼實(shí)證一個(gè)構(gòu)建請(qǐng)求從按下回車到產(chǎn)物輸出紙上談兵沒有意思我直接把一條buck2 build命令的流轉(zhuǎn)過(guò)程在源碼層面上串一下。這一節(jié)你就當(dāng)剛才打開了一個(gè)正在跑Buck2的倉(cāng)庫(kù)我們逐步看它內(nèi)部做了什么。4.1 命令行入口和后臺(tái)進(jìn)程拉起你在終端輸入buck2 build //foo:bar。buck2_client會(huì)先解析參數(shù)檢查.buckconfig和buck2.toml等配置文件。如果檢測(cè)到當(dāng)前項(xiàng)目根目錄下已經(jīng)有buck2 daemon在運(yùn)行它會(huì)直接通過(guò)Unix socket或TCP發(fā)送請(qǐng)求否則會(huì)啟動(dòng)一個(gè)新的daemon進(jìn)程。這個(gè)daemon就是buck2_server啟動(dòng)后它會(huì)加載一個(gè)“project context”包含項(xiàng)目根、配置片段、文件系統(tǒng)監(jiān)聽器。源碼里daemon的生命周期管理寫得很細(xì)包括空閑多少秒自動(dòng)退出、日志怎么輪轉(zhuǎn)、異常崩潰時(shí)怎么恢復(fù)。企業(yè)在做CI時(shí)最好顯式控制daemon的啟動(dòng)和回收不要讓每個(gè)job各起一個(gè)daemon否則內(nèi)存會(huì)被拖垮。4.2 目標(biāo)解析和BUCK文件處理然后服務(wù)端根據(jù)目標(biāo)模式//foo:bar去定位到foo/BUCK文件使用buck2_interpreter crate內(nèi)置的Starlark解釋器執(zhí)行這個(gè)文件。這個(gè)解釋器不是簡(jiǎn)單調(diào)用某個(gè)Python運(yùn)行時(shí)而是用Rust重實(shí)現(xiàn)的一個(gè)Starlark子集。因?yàn)锽UCK文件是聲明式定義目標(biāo)所以執(zhí)行BUCK文件不會(huì)產(chǎn)生任意副作用只會(huì)向一個(gè)包里注冊(cè)規(guī)則。源碼里目標(biāo)被表示為一個(gè)TargetNode它會(huì)記錄自己的規(guī)則類型、屬性、依賴列表。依賴列表里的每一項(xiàng)都是另一個(gè)目標(biāo)或顯式的filegroup。Buck2對(duì)循環(huán)依賴的檢測(cè)也在這一層它會(huì)在構(gòu)建圖構(gòu)建期間就報(bào)錯(cuò)而不是等執(zhí)行到一半才崩。我實(shí)際遇到過(guò)一些項(xiàng)目會(huì)通過(guò)動(dòng)態(tài)讀文件內(nèi)容來(lái)改變依賴這在Buck2里也能做但會(huì)加大指紋失效的復(fù)雜度能不用盡量不用。4.3 DICE事務(wù)和計(jì)算觸發(fā)接下來(lái)Buck2進(jìn)入了分析和執(zhí)行階段。這時(shí)會(huì)通過(guò)DICE發(fā)起一個(gè)事務(wù)請(qǐng)求目標(biāo)//foo:bar的“分析結(jié)果”。這個(gè)事務(wù)會(huì)從這個(gè)目標(biāo)開始向下展開依賴圖。對(duì)每一個(gè)目標(biāo)DICE會(huì)先看緩存里有沒有現(xiàn)成結(jié)果如果沒有它才去調(diào)用對(duì)應(yīng)的規(guī)則實(shí)現(xiàn)生成動(dòng)作。這里我特別要說(shuō)一下Buck2支持動(dòng)態(tài)依賴所以在分析階段它可能發(fā)現(xiàn)某個(gè)動(dòng)作的輸出會(huì)成為另一個(gè)動(dòng)作的輸入。這個(gè)“在運(yùn)行時(shí)發(fā)現(xiàn)依賴”的能力是DICE事務(wù)內(nèi)不斷擴(kuò)展圖的關(guān)鍵。源碼里有一個(gè)API叫DynamicLambda之類的機(jī)制允許規(guī)則在分析時(shí)根據(jù)中間結(jié)果返回新的目標(biāo)列表。這種設(shè)計(jì)對(duì)代碼生成場(chǎng)景非常友好但也意味著如果動(dòng)態(tài)依賴邏輯寫得太復(fù)雜調(diào)試起來(lái)會(huì)比較燒腦。4.4 動(dòng)作執(zhí)行和產(chǎn)物校驗(yàn)?zāi)繕?biāo)的分析結(jié)果最終會(huì)產(chǎn)生一個(gè)Action列表。Buck2會(huì)把這些action提交給執(zhí)行調(diào)度器。如果遠(yuǎn)端執(zhí)行開啟調(diào)度器會(huì)優(yōu)先發(fā)往遠(yuǎn)端如果沒開啟就在本地起子進(jìn)程。一個(gè)action的執(zhí)行會(huì)先做一次本地緩存查詢指紋匹配就直接log hit否則創(chuàng)建隔離的臨時(shí)執(zhí)行目錄跑命令收集輸出文件計(jì)算輸出指紋然后把結(jié)果寫回cas緩存。這個(gè)階段源碼里最需要注意的是“輸出文件聲明”的嚴(yán)格性。Buck2在做產(chǎn)物校驗(yàn)時(shí)會(huì)檢查動(dòng)作聲明了哪些輸出文件如果動(dòng)作偷偷寫到聲明之外的文件這些文件通常會(huì)被忽略或?qū)е戮彺娌灰恢?。我在調(diào)研過(guò)程中看到很多項(xiàng)目就因?yàn)槟硞€(gè)腳本在臨時(shí)目錄里生成了額外文件導(dǎo)致緩存命中率一直上不去。解決辦法是打開Buck2的日志看action的“output”列表把所有實(shí)際生成的文件都補(bǔ)充到out參數(shù)里。5. 企業(yè)級(jí)落地配置調(diào)優(yōu)、監(jiān)控與踩坑實(shí)錄源碼讀得再好最終還是要看能不能在企業(yè)里跑起來(lái)。這一部分是我在調(diào)研和試用過(guò)程中踩過(guò)的坑以及總結(jié)出來(lái)的可操作建議希望在選型時(shí)能幫你少走彎路。5.1 典型問(wèn)題與排查技巧速查現(xiàn)象可能原因排查/解決辦法構(gòu)建頻繁全量執(zhí)行action未完整聲明輸入文件/環(huán)境變量查看日志中action的fingerprint字段對(duì)比兩次構(gòu)建的指紋差異遠(yuǎn)端執(zhí)行結(jié)果和本地不一致動(dòng)作里有非確定性輸出比如時(shí)間戳在action前對(duì)時(shí)間戳白名單化或使用機(jī)器可復(fù)現(xiàn)的編譯參數(shù)后臺(tái)daemon內(nèi)存居高不下長(zhǎng)期運(yùn)行后緩存積累、日志膨脹定期主動(dòng)重啟daemon或調(diào)低daemon空閑回收時(shí)間異步動(dòng)態(tài)依賴導(dǎo)致卡死規(guī)則在動(dòng)態(tài)lambda里循環(huán)依賴檢查日志中的依賴展開棧對(duì)動(dòng)態(tài)依賴增加深度上限某些目標(biāo)總是cache miss輸出文件聲明遺漏或依賴未聲明用buck2 log打開執(zhí)行日志查看miss的fingerprint對(duì)比config平臺(tái)太多導(dǎo)致解析慢平臺(tái)配置導(dǎo)致配置階段矩陣爆炸減少統(tǒng)一配置變體盡量復(fù)用已有配置避免每目標(biāo)單獨(dú)定義新平臺(tái)這張表基本覆蓋了我見過(guò)的90%的Buck2使用問(wèn)題。核心思路永遠(yuǎn)是先看動(dòng)作指紋再看依賴聲明再看輸出聲明。指紋是最底層的事實(shí)所有緩存問(wèn)題最終都能在指紋層面找到答案。5.2 部署建議和架構(gòu)選型思考如果你要在公司內(nèi)部推廣Buck2我建議分三步走。第一步是選一條核心編譯鏈路做PoC比如一個(gè)C服務(wù)或Rust工程先讓它能通過(guò)Buck2構(gòu)建出和現(xiàn)有構(gòu)建系統(tǒng)一致的產(chǎn)物。第二步是把CI接入Buck2重點(diǎn)觀察緩存命中率、全量構(gòu)建時(shí)間、增量構(gòu)建時(shí)間以及遠(yuǎn)端執(zhí)行接入后的收益。第三步才是大規(guī)模推廣這時(shí)需要在意daemon的管理、remote cache容量、構(gòu)建日志聚合等企業(yè)級(jí)能力。Buck2的配置并不復(fù)雜核心是.buckconfig或buck2.toml里面可以配置緩存目錄、daemon超時(shí)、遠(yuǎn)端執(zhí)行地址等。我習(xí)慣把變更少、體積大的工具鏈配置放到頂層的配置里把項(xiàng)目特有的規(guī)則放到各個(gè)包下。一個(gè)容易忽略的地方是--unstable-*參數(shù)很多也有一些配置項(xiàng)在持續(xù)演進(jìn)所以你使用的版本一定要鎖定否則升級(jí)可能需要同步調(diào)整配置。5.3 源碼層面我最喜歡的設(shè)計(jì)細(xì)節(jié)最后說(shuō)兩個(gè)源碼里很有啟發(fā)的小細(xì)節(jié)它們可能不會(huì)寫進(jìn)官方博客但直接影響使用體驗(yàn)。第一件是Buck2對(duì)診斷信息的處理。構(gòu)建失敗時(shí)它會(huì)嘗試輸出“為什么失敗”的因果鏈包括失敗action的具體命令、輸出日志、指紋文件列表。這個(gè)診斷機(jī)制在源碼里做得非常深不是為了好看而是為了讓你沙里淘金時(shí)能快速定位問(wèn)題。第二件是Buck2對(duì)并發(fā)沖突的寬容度。Rust的ownership模型讓很多共享狀態(tài)不需要加鎖代碼里大量使用Arc和原子類型讓并發(fā)控制變得很清爽。如果你讀過(guò)其他JVM構(gòu)建工具的并發(fā)代碼再來(lái)看Buck2會(huì)在“爽”的同時(shí)意識(shí)到語(yǔ)言選擇對(duì)企業(yè)級(jí)軟件質(zhì)量的影響有多大。寫在最后的個(gè)人體會(huì)源碼盡調(diào)這件事最怕的就是只看了幾個(gè)模塊的名字就下結(jié)論。Buck2給我的整體感受是“架構(gòu)清晰、取舍果斷”它放棄了和Bazel完全兼容換來(lái)的是在動(dòng)態(tài)依賴和增量計(jì)算上可以走得更遠(yuǎn)它用Rust從零實(shí)現(xiàn)換來(lái)的是低層級(jí)的性能和內(nèi)存可控性。這套設(shè)計(jì)并不是萬(wàn)能藥它在中小型項(xiàng)目上的優(yōu)勢(shì)沒有大型monorepo那么明顯如果你單獨(dú)編譯一個(gè)小庫(kù)用Buck2反而顯得重但一旦項(xiàng)目規(guī)模和團(tuán)隊(duì)復(fù)雜度漲上來(lái)Buck2的價(jià)值就會(huì)快速放大。如果你后續(xù)想深入研究我會(huì)建議按這個(gè)順序讀源碼先從buck2_server的入口看起理解daemon啟動(dòng)流程然后讀dice crate里的狀態(tài)機(jī)和事務(wù)實(shí)現(xiàn)再讀buck2_build_api里的action和artifact定義最后再看buck2_interpreter里的Starlark集成。四層讀完你基本就能在別人面前用“源碼邏輯”來(lái)聊Buck2了。而我自己的體會(huì)是這種能靠源碼本身講清楚設(shè)計(jì)意圖的構(gòu)建引擎真的太稀缺了。