Ninja 構建系統

Ninja 構建系統

概述

Ninja 是一個構建系統,與 Make 類似。作爲輸入,你需要描述將源文件處理爲目標文件這一過程所需的命令。 Ninja 使用這些命令保持目標處於最新狀態。與其它一些構建系統不同,Ninja 的主要設計目標是速度。
我在參與 Google Chrome 項目時編寫了 Ninja。一開始,我將 Ninja 視作一個實驗——看看能不能讓 Chrome 構建的更快。爲了成功地構建 Chrome,Ninja 也有其它一些設計目標:Ninja 必須易於嵌入大型構建系統。
Ninja 獲得了相當的成功,逐漸取代了 Chrome 所使用的構建系統。Ninja 公開後,一些人貢獻了代碼,使得流行的 CMake 構建系統能夠生成 Ninja 文件。現在,Ninja 也被用來開發基於 CMake 的系統,如 LLVM 和 ReactOS。其它一些擁有定製構建系統的項目,如 TextMate,直接將 Ninja 作爲其構建目標。
2007 年至 2012 年間,我參與了 Chrome 項目,Ninja 則開始於 2010 年。對於像 Chrome 這樣的項目(如今有約 40000 個 C++ 代碼文件,生成的二進制文件約有 90 MB),有許多因素能對構建性能造成影響。我當時也接觸到了其中一些,涵蓋多機器分佈編譯到鏈接技巧。Ninja 起初只針對一個領域——構建的前端。也即介於開始構建到第一個編譯指令開始運行這一段時間的事。要理解這一階段爲何如此重要,有必要首先理解我們是怎樣看待 Chrome 本身的性能的。

Chrome 歷史小介

對 Chrome 的目標進行全面討論超出了本文的範疇。但速度是其中非常明確的一點。性能是整個計算機科學領域共同的追求。Chrome 幾乎用到了所有可能的計巧,從緩存到並行再到 just-in-time 編譯。還有啓動速度——在點擊了那個看起來有點無聊的圖標後 Chrome 要花多久在屏幕上顯示出來。
爲什麼要關心啓動速度?對於瀏覽器而言,迅速的啓動給人一種輕便的感覺,操作 Web 上的東西如同在本地打開一個文本文件一樣便捷。其實,人機交互領域已經深入地研究了延遲對情緒和思維的影響。像 Google 或 Amazon 這樣的互聯網公司對延遲非常關注,他們在探知延遲的影響方面也有一定的優勢。他們已有的實驗結果顯示,僅僅是毫秒級的延遲都會對人們使用網站或在網上購買商品的頻率產生可見的影響。這是微小的挫折感在潛意識中累積的結果。
Chrome 的快速啓動是通過 Chrome 最早期的某個工程師設計的技巧達成的。當他們剛搭建出程序框架,僅能在屏幕上顯示一個窗口時,他們就建立了基準測試,並在隋後的構建過程中對其進行追蹤。正如 Brett Wilson 所說:“規定非常簡單:這個測試不能變慢。”1隨着 Chrome 的代碼量不斷增長,維持這一基準測試需要付出額外的努力2——有時,某些計算工作會被推遲到真正需要的時候,或者,啓動時所需的數據是預先計算的。但是,對性能最首要的優化,也是給我印象最深刻的,是做得更少。
我起初加入 Chrome 團隊時,並未想到在構建工具方面着手。我的背景和平臺選擇是 Linux,而且我想要成爲一名 Linux 極客。爲了限定工作範圍,Chrome 項目起初是 Windows 專屬的。我認爲我的職則是幫助完成 Windows 版。如此,我再讓它在 Linux 上運行。
當我們開始向其它平臺移植時,第一個困難是選擇構建系統。彼時,Chrome 已經非常龐大(補充,實際上,Windows 版的 Chrome 在 2008 年發佈,當時任何移植都還沒有開始),啓圖從基於 Visual Studio 的 Windows 平臺構建系統大規模地切換到某個不同的構建悉統與當前的開發進程相沖突。這如同在使用一棟建築的同時替換其根基。
Chrome 團隊的某些成員想到了一個增量的解決方案,叫 GYP3。用它可以以一次一個子組件的方式生成 Chrome 已經在用的 Visual Studio 構建文件,以及其它平臺會用到的構建文件。
GYP 的輸入非常簡單:希望得到的輸出文件名稱,伴有純文本描述的源文件列表,偶爾會有定製的規則,諸如“處理每個 IDL 文件,生成一個額外的源文件”,也有一些條件動作(俐如,只在特定平臺使用某文件)。GYP 會通過這一高級描述生成各平臺原生的構建文件。4
在 Mac 上,“原生構建”意味着 Xcode 工程文件。但是,在 Linux 上,並沒有一個明確的優勢選項。我首先嚐試了 Scon,但我發現,通過 GYP 生成的 Scons 構建會在啓動前花 30 秒計算文件變更情況。我以識到 Chrome 的規模與 Linux kernel 相當,在 Linux kernel 中使用的一些技巧也應該對 Chrome 管用。我擼起袖子幹了起來,使 GYP 生成普通的 Makefiles 並使用了從 Kernel 的 Makefiles 借鑑的技巧。
於是,我無意間開始陷入構建系統的天坑。導致構建耗時的原因有許多,諸如緩慢的鏈接器以及缺乏並行能力,我把它們研究了個遍。使用 Makefile 的方法一開始相當快。但隨着我們向 Linux 移植越來越多的部份,構建中使用的文件量不斷增加,這個方法也變慢了。[5]
在我進行移植時,我發現構建過程的一部分讓人非常不爽:可能我修改了某個文件,運行 make,意識到漏打了一個分號,補上後再次運行 make,這之中每次等待都長到足以使我忘記我原本在做什麼。回想起我們對延遲的抗爭,“怎麼會這麼久?”,我自問,“應該沒有這麼多工作要做。”我開始了 Ninja,作爲一個實驗,看看我能簡化多少。

Ninja 的設計
在高層視角下,任何構建系統主要執行三項任務。(1)加載和分析構建目標,(2)計算出達到構建目標所需的步驟,(3)執行這些步驟。
爲了使第一步迅速,Ninja 在加載構建文件時只做最少量的工作。構建系統一般是面向人的,這意味着它們會提供一個方便的、高層的語法來表達構建目標。這同樣意味着在實際進行構建時,構建系統必須進一步處理指令:例如,在某一時刻,Visual Studio 必須基於其構建配置具體地決定輸出文件的去向,或某個文件必須由 C++ 還是 C 編譯器編譯。
因此,GYP 在生成 Visual Studio 時的工作實際上被控制在了將源文件列表轉譯到 Visual Studio 語法,剩下的工作統統交給 Visual Studio。有了 Ninja,我看到了機會,可以在 GYP 中做儘可能多的工作。在某種意義上,當 GYP 生成 Ninja 構建文件時,GYP 會進行上述的計算。GYP 隨後保存這一中間數據的一個快照——以一種 Ninja 可以快速讀取的格式,供後續構建使用。
Ninja 的構建文件使用的語言因此簡單到了不便於人類書寫的程度。沒有條件語句或基於文件拓展名的規則。相反,格式僅僅是一個列表,記錄確切路徑所產生的確切結果。這種文件可以被快速載入,幾乎不需要解釋。
最小化的設計產生了極大的靈活性。因爲 Ninja 缺少對高層次構建概念的瞭解(如輸出目錄或當前配置),Ninja 易於嵌入各種更大型的系統(例入,CMake,我們一會就會看到),這些系統對於構建該如何組織往往有不同的觀點。例如,Ninja 對於構建輸出是與源文件放在同一目錄中(有人認爲不怎麼“衛生”),還是放在另一個構建輸出目錄(有人認爲這樣會使別人疑惑)。在 Ninja 發佈很久以後,我終於想到了正確的比喻:如果將其它構建系統視爲編譯器,Ninja 則如同彙編器一般。

Ninja 做了什麼

如果 Ninja 將絕大部分工作推給了構建文件生成器,那自己還有什麼事呢?上述想法理論看上很不錯,但真實世界的需要永遠更復雜。Ninja 在開發過程中添加(也失去)了很多特性。不論何時,重要的問題都是“我們能做得更少嗎?”此處概述這如何運作。
在構建規則出錯時需要人去調試(構建)文件,所以 .ninja 構建文件是普通文本,與 Makefiles 類似。爲了增強可讀性.ninja 也支持一些抽象。

第一種抽象是“rule”,可以代表單個命令行調用,rule 定義後在不同的構建步驟間共享。這是 Ninja 語法的一個例子,聲明瞭一條名爲“compile”的 rule——這條 rule 會調用 gcc 編譯器,此外還有兩條 build 語句對特定文件使用了 compile。

rule compile
  command = gcc -Wall -c $in -o $out
build out/foo.o: compile src/foo.c
build out/bar.o: compile src/bar.c

第二種抽象是變量。在上面的例子中,那些以”" in 和 $out)。變量即可以表示命令的輸入,也可以表示命令的輸出,也可以給長字符串起一個短點的名字。這裏有一個compile定義,用一個變量表示編譯器的標誌:

cflags = -Wall
rule compile
  command = gcc $cflags -c $in -o $out

一條規則中使用的 變量 可以在單個 build 塊中被縮進表述的新定義覆蓋。繼續上面的例子,cflags 的值可以在單個文件處調整:

build out/file_with_extra_flags.o: compile src/baz.c
  cflags = -Wall -Wextra

rule 與函數很像,而且變量又酷似參數。這兩個簡單的功能使 Ninja 的語法與編程語言過於相似,這很危險——是“不做多餘事”的對立面。但他們可以減少重複字符串,這不僅對人非常有用,也有利於電腦計算,因爲減少了需要分析的文本量。

構建文件,一旦完成分析,就可以描繪出一幅依賴圖:最終的二進制輸出依賴於一組對象文件,這組對象文件中的每個都是編譯源文件的結果。特別地,這是一幅二分圖(bipartite graph),“結點”(輸入文件)指向“邊”(構建指令),構建指令再指向“結點”(輸出文件)[6]。構建過程就是遍歷這幅圖。

給定一個構建目標,Ninja 首先遍歷這幅圖以確定每條“邊”上輸入文件的狀態:即,輸入文件是否存在,以及被修改的時間。Ninja 隨即計算出一份計劃。計劃即是爲了保證最終目標處於最新狀態而必須執行的“邊”的集合,依據中間文件的修改時間判斷。最後,執行計劃,遍歷圖並將“邊”標記爲已執行,至此順利結束。

這些功能一就位,就可以建立起基於 Chrome 的參考基線:成功完成一次構建後再次運行 Ninja 所需的時間。即載入構建文件,驗正構建狀態,決定是否有工作要做所需的時間。這一基準只需要不到一秒。這是我的新啓動指標。但是,隨着 Chrome 日漸龐大,Ninja 必須持續變快,以保證這一指標不退化。

優化 Ninja

Ninja 最初的實現仔細地組織了數據結構,爲快速構建創造條件。但從優化的角度來說這並不是個聰明的想法。在程序完成之際,我想到,一個分析器(profiler)可以揭示哪些代碼對性能產生重要影響。[7]

這些年來,分析(profiling)的結果指向過程序的不同的區域。有時是單個熱點程序,可以微優化(micro-optimized)。更多時候,分析會指向一些更廣泛的問題,如,除非必要,不要分配或複製內存。也存在某些情型,採用更好的表示方法或數據結構可以獲得最好的效果。接下來是對 Ninja 的實現的簡單表述,以及圍繞 Ninja 性能的有趣故事。

解析(Parsing)

起初,Ninja 使用的是手寫的詞法分析器和遞歸下降分析器。我以爲語法足夠簡單了。事實證明,對於像 Chrome 這樣夠大的項目[8],僅僅是解析構建文件(拓展名以 .ninja 結尾)所消耗的時間都十分令人吃。

很快,最初用來解析單個字符的函數很快出現在分析結果中:

static bool IsIdentifierCharacter(char c) {
  return
    ('a' <= c && c <= 'z') ||
    ('A' <= c && c <= 'Z') ||
    // and so on...
}

一個簡單的調整就可以節省 200 毫秒——用一個有 256 個條目、以輸入字符爲索引的查找表替換這個函數。這樣一張表用 Python 代碼很好生成,像這樣:

cs = set()
for c in string.ascii_letters + string.digits + r'+,-./\_$':
    cs.add(ord(c))
for i in range(256):
    print '%d,' % (i in cs),

這個技巧使 Ninja 在相當一段時間裏保證了運行速度。最終我們轉移到了更正式的工具:re2c,PHP所用的詞法分析器(lexer)生成工具。它可以生成更復雜的查找表,和人無法理解的代碼。例如:

if (yych <= 'b') {
    if (yych == '`') goto yy24;
    if (yych <= 'a') goto yy21;
    // and so on...

當初採用文本格式作爲輸入格式是否是一個好主意?這一點依然不明確。或許最終我們會採用某種機器友好的格式作爲 Ninja 輸入文件的格式,這也許可以避免絕大部分解析工作。

規範化(Canonicalization)

Ninja 避免用字符串識別路徑。取而代之的是,Ninja 將它遇到的每個路徑映射到唯一的 Node 對象,在後續代碼中以 Node 對象表示這一路徑。複用 Node 對象保證了一個給定的路徑只在硬盤上檢查一次,檢查的結果(例如,修改時間)可在其它代碼中複用。

指向 Node 對象的指針如同這一路徑的唯一標識。如果想要測試兩個 Node 是否指向同一路徑,比較指針就足夠了,不需要進行昂貴的字符串比較。例如,在 Ninja 遍歷輸入文件構成的圖時,會保存一個 Node 依賴棧,以檢查依賴是否有環:如果 A 依賴 B,B依賴 C,而 C 又依賴 A。構建就無法進行。這個棧,代表一組文件,可以通過一個指針數組實現,可以使用指針相等性判斷檢查是否有重複。

爲了保證指向一個文件的路徑總是指向同一 Node,Ninja 必須可靠地將一個文件所有可能的名字映射到同一 Node 對象。這需要對輸入文件中提到的所有文件進行規範化(canonicalization),將像 foo/../bar.h 這樣的路徑轉換爲 bar.h。最初,Ninja 只是簡單地要求所有路徑以規範的形式給出,但由於幾個原因,這最後還是不行。一個原因是用戶指定的路徑(例如,在命令行輸入 ninja ./bar.h )應該能正確工作。另一個原因是變量的組合可能產生出不規範的路徑。最後,gcc 給出的依賴信息可能不規範。

於是,最終,Ninja 對路徑進處理。這也導致路徑功能成能成爲分析結果中的另一處熱點。原來的實現是以清晰而不是以性能爲重點編寫的,所以標準的優化技術,如移除雙循環(removing a double loop)或避免內存分配,作用顯著。

設計目標

這裏是Ninja的設計目標:
* 非常快(即瞬間)增量構建,即使是非常大的項目。
* 代碼如何構建的策略非常少。關於代碼如何構建不同的項目和高級構建系統有不同的意見;例如,構建目標是應該與代碼放在一起還是放在單獨的目錄裏?是否有爲項目構建一個可分發的“包”的規則?規避這些策略,而不是選擇,否則只會變得複雜。
* 獲取正確的依賴關係,在某些特定的情況下Makefiles很難獲取正確的依賴關係(例如:輸出隱式依賴於命令行生成,構建c代碼你需要使用gcc的-M標誌去生成其頭文件依賴)
* 當目標和遍歷衝突時我們選擇速度.

一些明確的 non-goals:

  • 方便人工編寫構建文件的語法。你應該使用其它程序來生成你的構建文件,這是我們可以迴避許多策略決定。

  • built-in rules. _Out of the box, Ninja has no rules for
    e.g. compiling C code._

  • build-time customization of the build. _Options belong in
    the program that generates the ninja files_.

  • build-time decision-making ability such as conditionals or search
    paths. Making decisions is slow.

重申一下,Ninja比其它構建系統要快,因爲它是異常的簡單。在你爲你的項目編寫.ninja文件時你必須告訴Ninja要它做什麼。

與Make比較

Ninja的定位非常清晰,就是達到更快的構建速度。

ninja的設計是對於make的缺陷的考慮,認爲make有下面幾點造成編譯速度過慢:

  • 隱式規則,make包含很多默認
  • 變量計算,比如編譯參與應該如何計算出來
  • 依賴對象計算

ninja認爲描述文件應該是這樣的:

  • 依賴必須顯式寫明(爲了方便可以產生依賴描述文件)
  • 沒有任何變量計算
  • 沒有默認規則,沒有任何默認值
    針對這點所以基本上可以認爲ninja就是make的最最精簡版。

ninja相對於make增加了下面這些功能:

  • 如果構建命令發生變化,那麼這個構建也會重新執行。
  • 所依賴的目錄在構建之前都已經創建了,如果不是這樣的話,我們執行命令之前都要去生成目錄。
  • 每條構建規則,除了執行命令之外,還允許有一個描述,真正執行打印這個描述而不是實際執行命令。
  • 每條規則的輸出都是buffered的,也就是說並行編譯,輸入內容不會被攪和在一起。
    構建工具太多了,我個人覺得make主要偏大衆化一點,可以進行各種隱式推導,比較靈活,每一條命令執行都有輸出。

而Ninja主要的設計目的是爲了像chromium這種大型項目,能夠顯著的提高編譯速度,一方面它去掉了各種計算和推導,把一些耗時的需要計算的東西去掉了,只留下簡單重要的部分,所以如果自己去寫build.ninja文件的話比較繁瑣,所以都是依賴於其它構建工具生成的,另一方面它每次輸出只輸出一個描述,而不是真正的命令執行輸出,真正的命令執行再後臺運行,只有警告和報錯信息纔會顯示出來,這也提高了它的速度。

Make vs Ninja Performance Comparison這篇文章對Make接Ninja進行測試對比。

在你的項目中使用ninja

Ninja 目前在Windows和類Unix系統都支持,雖然大部分測試都是在linux上完成的(並且在Linux上性能也最好),不過在MAC OS X和FreeBSD 上也都能很好的工作。
如果你的項目很小,Ninja的速度優勢可能不那麼明顯(然而,即使是小項目,Ninja的極其簡潔的語法和極其簡單的構建規則,也使你的項目能夠更加快速的構建)。換一句話說,如果你對你的項目編輯-編譯的循環時間感到滿意,那麼Ninja可能對你不會有太多的幫助。
還有許多其它的構建系統,比Ninja使用更加友好,功能也更加強大。作者覺得Ninja的設計受到了tup構建系統設計的影響,並認爲重做的設計非常聰明。
Ninja的好處是可以將它與智能的元構建系統結合起來使用。
gyp用於生成Google Chrome和相關項目(v8,node.js)的元構建文件建立系統,已被GN取代。 gyp可以爲Chrome支持的所有平臺生成Ninja文件。 有關詳細信息,請參閱Chromium Ninja文檔
CMake一個廣泛使用的元構建系統,在Linux上CMake 2.8.8版本可以生成Ninja文件。 較新版本的CMake支持在Windows和Mac OS X上生成Ninja文件。
其他:Ninja應該完善其他元構建軟件的支持,例如premake。 如果你在做這項工作,請讓我們知道!

運行Ninja

運行ninja。 默認情況下,它在當前目錄中查找名爲build.ninja的文件
並構建所有過期目標。 您可以在命令行參數中指定要構建的目標(文件)。

還有一個特殊的語法,目標^指定的目標作爲第一個輸出(如果存在)。 例如,如果您指定目標
foo.c ^, 那麼foo.o將首先被構建(假設你的構建文件中存在這些目標)。

ninja -h打印幫助。 許多Ninja的flags與Make是匹配的; 例如ninja -C build -j 20改成構建目錄並並行運行20個構建命令。 (注意
Ninja默認情況下以並行方式運行命令,所以通常你不需要傳遞-j。)
Ninja 默認基於系統中可用的 CPU 數量以併發方式執行指令。因爲同時運行的命令們的輸出可能混淆,Ninja 會在一個命令完成前緩存其輸出。從結果看,如同命令是串行的。
這種對命令輸出的控制使得 Ninja 可以小心控制總的輸出。在構建過程中 Ninja 顯示一行表示狀態;如果構建順利完成,Ninja 的全部輸出就只有一行。這不會使 Ninja 運行得更快,但可以使人感覺 Ninja 很快,這幾乎與在真實速度上的目標一樣重要。

環境變量

Ninja支持用一個環境變量來控制其行爲:
NINJA STATUS,在運行規則之前會打印進度狀態。
下面是幾個可用的佔位符:

%s:: The number of started edges.
%t:: The total number of edges that must be run to complete the build.
%p:: The percentage of started edges.
%r:: The number of currently running edges.
%u:: The number of remaining edges to start.
%f:: The number of finished edges.
%o:: Overall rate of finished edges per second
%c:: Current rate of finished edges per second (average over builds
specified by -j or its default)
%e:: Elapsed time in seconds. (Available since Ninja 1.2.)
%%:: A plain % character.

默認進度狀態爲 "[%f/%t] " (
注意結尾空格以與構建規則分開). 另一個可能的進度狀態的例子如下: "[%u/%r/%f] ".

擴展工具

在Ninja的開發過程中,命令行裏使用-t可以運行一些非常有用的工具,目前有以下一些工具可以使用:

query:: dump指定target的輸入和輸出.

browse:: 在Web瀏覽器中瀏覽依賴關係圖。 單擊文件將焦點切到該文件上,會顯示輸入和輸出。 這個
功能需要Python安裝。 默認使用端口8000並打開Web瀏覽器。 可以按照如下方式修改:

ninja -t browse --port=8000 --no-browser mytarget

graph::以自動圖形佈局工具graphviz的語法輸出一個文件。 使用方式如下:

ninja -t graph mytarget | dot -Tpng -ograph.png

+
在Ninja源代碼樹中,運行“ninja graph.png”命令將爲Ninja本身生成一張圖。 如果沒有指定目標則將爲
all目標生成。

targets:: output a list of targets either by rule or by depth. If used
like +ninja -t targets rule name+ it prints the list of targets
using the given rule to be built. If no rule is given, it prints the source
files (the leaves of the graph). If used like

+ninja -t targets depth _digit_+ it

prints the list of targets in a depth-first manner starting by the root
targets (the ones with no outputs). Indentation is used to mark dependencies.
If the depth is zero it prints all targets. If no arguments are provided

+ninja -t targets depth 1+ is assumed. In this mode targets may be listed

several times. If used like this +ninja -t targets all+ it
prints all the targets available without indentation and it is faster
than the depth mode.

commands:: given a list of targets, print a list of commands which, if
executed in order, may be used to rebuild those targets, assuming that all
output files are out of date.

clean:: remove built files. By default it removes all built files
except for those created by the generator. Adding the -g flag also
removes built files created by the generator (see <

編寫你自己的Ninja文件

概述

Ninja和Make非常相似。他執行一個文件之間的依賴圖,通過檢測文件修改時間,運行必要的命令來更新你的構建目標。
一個構建文件(默認文件名爲:build.ninja)提供一個rule(規則)表——長命令的簡短名稱,和運行編譯器的方式。同時,附帶提供build(構建)語句列表,表明通過rule如何構建文件——哪條規則應用於哪個輸入產生哪一個輸出。

從概念上講,build語句描述項目的依賴圖;而rule語句描述當給定一個圖的一條邊時,如何生成文件。

語法例子

這是一個用於驗證絕大部分語法的.ninja文件,將作爲後續描述相關的示例。具體內容,如下:

cflags = -Wall

rule cc
  command = gcc $cflags -c $in -o $out

build foo.o: cc foo.c

變量

ninja支持爲字符串聲明簡短可讀的名字。一個聲明的語法,如下:

cflags = -g

可以在=右邊使用,並通過$進行引用(類似shell和perl的語法)。具體形式,如下:

rule cc
  command = gcc $cflags -c $in -o $out

變量還可以用${in}($和成對的大括號)來引用。
當給定變量的值不能被修改,只能覆蓋(shadowed)時,變量更恰當的叫法是綁定(”bindings”)。

規則

規則爲命令行聲明一個簡短的名稱。他們由關鍵字rule和一個規則名稱打頭的行開始,然後緊跟着一組帶縮進格式的 variable = value行組成。
以上示例中聲明瞭一個名爲cc的rule,連同一個待運行的命令。在rule(規則)上下文中,command變量用於定義待執行的命令,$in展開(expands)爲輸入文件列表(foo.c),而$out爲命令的輸出文件列表(foo.o)。 參考手冊中羅列了所有特殊的變量。

Build 語句

build語句聲明輸入和輸出文件之間的一個關係。構建語句由關鍵字build開頭,格式爲

build outputs: rulename inputs

這樣的一個聲明,所有的輸出文件來源於輸入文件。當缺輸出文件或輸入文件變更時,Ninja將會運行此規則來重新生成輸出。
以上的簡單示例,描述了使用cc規則如何構建foo.o文件。
在build block範圍內(包括相關規則的執行),變量$in表示輸入列表,$out表示輸出列表。
一個構建語句,可以和rule一樣,緊跟一組帶縮進格式的key = value對。當在命令中變量執行時,這些變量將覆蓋(shadow)任何變量。比如:

cflags = -Wall -Werror
rule cc
  command = gcc $cflags -c $in -o $out

# 如果沒有指定,build的輸出將是$cflags
build foo.o: cc foo.c

# 但是,你可以在特殊的build中覆蓋cflags這樣的變量
build special.o: cc special.c
  cflags = -Wall

# cflags變量僅僅覆蓋了special.o的範圍
# 以下的子序列build行得到的是外部的(原始的)cflags
build bar.o: cc bar.c

如果你要從build語句傳遞更多的信息到rule規則(例如,如果規則需要知道”第一輸入文件的擴展名”),那麼請通過擴展變量傳遞,就像cflags一樣。

如果頂級Ninja文件使用build指定了任何輸出,並且它又過期了,那麼再爲構建用戶目標之前會先構建頂級文件裏的目標。

根據代碼生成Ninja文件

Ninja發行包中的misc/ninja_syntax.py是一個很小的python模塊,用於生成Ninja文件。你可以使用python,執行如

ninja.rule(name='foo', command='bar',depfile='$out.d')

的調用,生成合適的語法。如果這樣還不錯,可以將其整合到你的項目中。

更多細節

phony 規則

可以使用phony創建其它target(編譯構建目標)的別名。比如:

build foo: phony some/file/in/a/faraway/subdir/foo

這樣使得ninja foo構建更長的路徑。從語義上講,phony規則等同於一個沒有做任何操作的普通規則,但是phony規則通過特殊的方式進行處理,這樣當其運行時不會被打印,記日誌,也不作爲構建過程中打印出來的命令計數。
還可以用phony爲構建時可能還不存在的文件創建dummy目標。

默認目標

默認情況下,如果沒有在命令行中指定target,那麼Ninja將構建任何地方沒有作爲輸入命名的每一個輸出。可以通過default目標語句來重寫這個行爲。一個default語句,讓Ninja構建一個給定的輸出文件子集,如果命令行中沒有指定構建目標。
默認目標語句,由關鍵字default打頭,並且採用default targets的格式。一個default目標語句必須出現在,聲明這個目標作爲一個輸出文件的構建語句之後。他們是累積的(cumulative),所以可以使用多個default語句來擴展默認目標列表。比如:

default foo bar
default baz

This causes Ninja to build the foo, bar and baz targets by
default.

Ninja 日誌

Ninja構建日誌保存在構建過程的根目錄或.ninja文件中builddir變量對應的目錄的.ninja_log文件中。
一般而言,像上面這樣的微優化不如改變算法或處理方式的結構性優化有效。Ninja 的構建日誌就是這樣一個例子。
Linux kernel 構建系統的一部分會追蹤用於生成輸出的命令。考慮一個啓發性的例子:你將輸入文件 foo.c 編譯爲輸出文件 foo.o,隨後修改了構建文件導致應該用不同的編譯選項重新編譯 foo.c。從構建系統的角度看,爲了感知需要構建,必須要麼注意到 foo.o 依賴於構建文件(構建文件依賴於項目的組織,這也許意味着對構建文件的修改將導致整個項目的重新構建),或記錄生成每個輸出的命令,在每次構建時進行比較。
kernel(以及 Chrome Makefiles 和 Ninja)採用後一種方法。在構建時,Ninja 寫下一份構建日誌,記錄生成每個輸出的完整命令。[9]在後續構建中,Ninja 載入之前的構建日誌,通過比較當前命令與構建日誌中的命令來發現變更。就像加載構建文件或路徑規範化,這成爲了分析結果中的又一處熱點。
在進行了一些小優化後,Nico Weber,一個對 Ninja 貢獻了很多代碼的人,實現一種新的構建日誌格式。比起通常很長且需要大量時間進行解析的記錄命令,Ninja 取而代之以命令的哈希(hash)。在後續構建中,Ninja 比較將要執行的明令的哈希與記錄中的哈希。如果兩者不同,則可以確定輸出已過期。這一方法很成功。使用哈希急劇降低了構建日誌的大小——在 Mac OX X 上,從 200MB 降到 2MB——並使加載速度快了 20 倍。

版本兼容性

Available since Ninja 1.2.

Ninja version labels follow the standard major.minor.patch format,
where the major version is increased on backwards-incompatible
syntax/behavioral changes and the minor version is increased on new
behaviors. Your build.ninja may declare a 變量 named
ninja_required_version that asserts the minimum Ninja version
required to use the generated file. For example,

ninja_required_version = 1.1

declares that the build file relies on some feature that was
introduced in Ninja 1.1 (perhaps the pool syntax), and that
Ninja 1.1 or greater must be used to build. Unlike other Ninja
變量s, this version requirement is checked immediately when
the 變量 is encountered in parsing, so it’s best to put it
at the top of the build file.

Ninja always warns if the major versions of Ninja and the
ninja_required_version don’t match; a major version change hasn’t
come up yet so it’s difficult to predict what behavior might be
required.

文件依賴

還有另一種元數據(metadata)必需跨構建保存用。爲了正確構建 C/C++ 代碼,一個構建系統必需能感知頭文件間的依賴。假定 foo.c 包含一行 #inclue “bar.h” 。而 bar.h 自身又包含一行 #include “bar.h”。所有的三個文件都會影響後續編譯。例如,baz.h 的改變也會觸發 foo.o 的重新構建。

一些構建系統使用一個“頭文件掃描器”在構建時提取這部分依賴信息。但這個方法太慢,而且很難精確處理有 #ifdef 指令出現的情形。另一種選擇是要求構建文件正確地報告所有依賴,包括頭文件的依賴,但這對開發人員來說十分笨重:每次你添加或刪除 #include 語句時,都需要修改或重新生成構建文件。

一個有用的方法依賴於這樣的事實:在編譯時,gcc (以及微軟的 Visual Studio)可以給出在構建輸出時用到了哪些頭文件。這份信息,如同用於生成輸出的信息,可以被構建系統記錄和加載。由此,依賴可以被精確追蹤。在第一次編譯時,因爲還未有輸出,所有文件都會被編譯,故不需頭文件依賴。第一次編譯後,對於被某個輸出用到的任何文件如果發生更改(包括增加或刪除額外的依賴),就會導致重新構建。這保證了依賴信息的更新。

在編譯時,gcc 以 Makefile 的格式記下頭文件依賴。Ninja 包括一個解析器處理這一Makefile 語法(的簡化子集),並在下一次構建時載入這份依賴信息。在 Chrome 的最近一次構建,gcc 產生了共 90MB 的 Makefile,全部帶有必須規範化的引用路徑。

就像其它解析過程,通過使用 re2c 及儘可能地避免複製可以使性能有所提升,但就像 GYP 項目,這一解析工作可以不在關鍵時間路徑上完成。近期,我們在 Ninja 上的工作(在寫作本文時,這一工能已經完成,但還未發佈)是讓這一過程發生的早一些。

一旦 Ninja 開始執行構建指令,所有影響性能的工作都已完成,Ninja 在等待它啓動的命令完成的過程中近乎閒置。在處理頭文件依賴的新方法中,Ninja 利用這段時間處理 gcc 給出的 Makefile ,規範化路徑,將依賴處理爲一種可以快速識別的二進制格式。在下一次構建中,Ninja 只需要加載這一文件。改進非常劇烈,特別是在 Windows 上。(本章稍後討論這個)

“依賴日誌”需要儲存上千條路徑及路徑間的依賴。載入日誌和追加日誌都必須迅速。追加日誌操作應該是安全的,即使被打斷,比如構建被取消。

在考慮了一些類似於數據庫的方案後,我最終想到了一個簡單的實現:文件由記錄的序列組成,而記錄要麼是一個路徑,要麼是一個依賴列表。每個寫入文件的路徑都被賦於了一個整數序列號。故而依賴就是一列整數。爲了向文件添加依賴,Ninja 首先記錄下還沒有序列號的路徑,然後用這些序列號記錄依賴。在後續的構建載入這一文件時,Ninja 可以簡單地使用一個數組將序列號映射到對應的 Node 指針。

C/C++ 頭依賴

Ninja目前支持depfile和deps模式的C/C++頭文件依賴生成。 如

rule cc
  depfile = $out.d
  command = gcc -MMD -MF $out.d [other gcc flags here]

-MMD標識告訴gcc要生成頭文件依賴,-MF則說明要寫到哪裏。
deps按照編譯器的名詞來管理。具體如下:(針對微軟的VC:msvc)

rule cc
  deps = msvc
  command = cl /showIncludes -c $in /Fo$out

工作池

爲了支持併發作業,Ninja還支持pool的機制(和用-j並行模式一樣)。此處不詳細描述了。具體示例,如下:

# No more than 4 links at a time.
pool link_pool
  depth = 4

# No more than 1 heavy object at a time.
pool heavy_object_pool
  depth = 1

rule link
  ...
  pool = link_pool

rule cc
  ...

# The link_pool is used here. Only 4 links will run concurrently.
build foo.exe: link input.obj

# A build statement can be exempted from its rule's pool by setting an
# empty pool. This effectively puts the build statement back into the default
# pool, which has infinite depth.
build other.exe: link input.obj
  pool =

# A build statement can specify a pool directly.
# Only one of these builds will run at a time.
build heavy_object1.obj: cc heavy_obj1.cc
  pool = heavy_object_pool
build heavy_object2.obj: cc heavy_obj2.cc
  pool = heavy_object_pool

console

這裏有一個名爲console深度爲1的預定義池,池中的任何任務都可以直接訪問標準輸入、輸出和錯誤流並提供給Ninja,通常是連接到用戶的控制檯。這對於交互式任務或運行時間較長的任務比較有用。可以在控制檯上更新狀態(例如測試套件)。
當’console’池中的任務正在運行時,Ninja的正常輸出(如進度狀態和併發任務的輸出)將被緩衝起來直到控制檯任務運行完成。

Ninja 文件參考

一個Ninja文件是一系列聲明,聲明可以是下列之一:

  1. 規則聲明,以rulename開頭,然後是一些列的變量的定義;

  2. A build edge, which looks like +build output1 output2:
    rulename input1 input2+. +
    Implicit dependencies may be tacked on the end with +|
    dependency1 dependency2+. +
    Order-only dependencies may be tacked on the end with +||
    dependency1 dependency2+. (See <

語法

Ninja僅支持ASCII字符集。
註釋以爲#開始一直到行末。

新行是很重要的。像build foo bar的語句,是一堆空格分割分詞(token),到換行結束。一個分詞中的新行和空格必須進行轉譯。目前只有一個轉譯字符,$,其具有以下行爲:

$ followed by a newline

轉譯換行,讓當前行一直擴展到下一行。

$ followed by text

這是, 變量引用。

${varname}

這是,另$varname的另一種語法。

$ followed by space

這表示一個空格。(僅在path列表中,需要用空格分割文件名)

$:

這表示一個冒號。(僅在build行中需要。此時冒號終止輸出列表)

$$

這個表示,字面值的$。

一個build或default語句,最先被解析,作爲一個空格分割的文件名列表,然後每一個name都被展開。也就是說,變量中的一個空格將作爲被展開後文件名中的一個空格。

spaced = foo bar
build $spaced/baz other$ file: ...

在一個name = value語句中,value前的空白都會被去掉。出現跨行時,後續行起始的空白也會被去掉。

two_words_with_one_space = foo $
    bar
one_word_with_no_space = foo$
    bar

其他的空白,僅位於行開始處的很重要。如果一行的縮進比前一行多,那麼被人爲是其父邊界的一部分。如果縮進比前一行少,那他就關閉前一個邊界。

頂層變量

Ninja支持的頂層變量有builddir和ninja_required_version。具體說明,如下:

  • builddir: 構建的一些輸出文件的存放目錄。
  • ninja_required_version:指定滿足構建需求的最小Ninja版本。

Rule 變量

一個rule塊包含一個key = value的列表聲明,這直接影響規則的處理。以下是一些特殊的key:

  • command (required):
    待執行的命令。這個字符串 $variables被展開之後,被直接傳遞給sh -c,不經過Ninja翻譯。每一個規則只能包含一條command聲明。如果有多條命令,需要使用&&符號進行鏈接。
  • depfile: 指向一個可選的Makefile,其中包含額外的隱式依賴。這個明確的爲了支持C/C++的頭文件依賴。
  • deps: (1.3版本開始支持)如果存在,必須是gcc或msvc,來指定特殊的依賴。產生的數據庫保存在builddir指定目錄.ninja_deps文件中。
    msvc_deps_prefix: (1.5版本開始支持)定義必須從msvc的/showIncludes輸出中去掉的字符串。僅在deps = msvc而且使用非英語的Visual Studio版本時使用。
  • description: 命令的簡短描述,作爲命令運行時更好的打印輸出。打印整行還是對應的描述,由-v標記控制。如果一個命令執行失敗,整個命令行總是在命令輸出之前打印。
  • generator: 如果存在,指明這條規則是用來重複調用生成器程序。通過兩種特殊的方式,處理使用生成器規則構建文件:首先,如果命令行修改了,他們不會重新構建;其次,默認不會被清除。
    in: 空格分割的文件列表被作爲一個輸入傳遞給引用此rule的構建行,如果出現在命令中需要使用${in}(shell-quoted)。(提供$in僅僅爲了圖個方便,如果你需要文件列表的子集或變種,請構建一個新的變量,然後傳遞新的變量。)
  • in_newline: 和$in一樣,只是分割符爲換行而不是空格。(僅爲了和$rspfile_content一起使用,解決MSVC - linker使用固定大小的緩衝區處理輸入,而造成的一個bug。)
  • out: 空格分割的文件列表被作爲一個輸出傳遞給引用此rule的構建行,如果出現在命令中需要使用${out};
  • restat: 如果存在,引發Ninja在命令行執行完之後,重新統計命令的輸出。
  • rspfile, rspfile_content: 如果存在(兩個同時),Ninja將爲給定命令提供一個響應文件,比如,在調用命令之前將選定的字符串(rspfile_content)寫到給定的文件(rspfile),命令執行成功之後闡述文件。
    這個在Windows系統非常有用,因爲此時命令行的最大長度非常受限,必須使用響應文件替代。具體使用方式,如下:
rule link
  command = link.exe /OUT$out [usual link flags here] @$out.rsp
  rspfile = $out.rsp
  rspfile_content = $in

build myapp.exe: link a.obj b.obj [possibly many other .obj files]

command變量的解釋

在Unixes和Windows上命令行的行爲是不同的。

在Unixes上,命令是參數數組。 Ninja命令變量直接傳遞給sh -c,然後負責
將該字符串解釋爲argv數組。 因此引用規則由shell決定,你可以使用所有正常的shell
運算符,如&&鏈接多個命令,或VAR = value cmd 來設置環境變量。

在Windows上,命令是字符串,因此Ninja直接將command字符串
傳遞給CreateProcess。 (在常見情況下編譯器簡單執行這意味着有更少的開銷。)因此引用規則由被調用的程序確定,在Windows上通常由C庫提供。 如果你需要shell解釋命令(如使用&&來鏈接多個命令),使命令執行Windows shell前綴命令與cmd / c

構建輸出

有兩種稍微有點區別的輸出:

  1. 顯示輸出, 在build行會列出來,在rule規則中可以通過$out變量訪問。
    這是標準的輸出使用形式,例如一個編譯命令的目標文件。

  2. 隱式輸出, 在build行其語法格式如下,在build行的:out1 out2(在Ninja1.7版本開始支持).語義與顯式輸出相同,唯一的區別是隱式輸出不會出現在$out變量裏。這是爲了表示在命令行中沒有指定輸出的命令。

構建依賴

Ninja目前支持3種類型的構建依賴。分別是:

  • 羅列在build行中的顯式的依賴。他們可以作爲規則中的$in變量。這是標準依賴格式。
  • 從depfile屬性或構建語句末尾的| dep1 dep2語法獲得的隱式依賴。這個和顯式依賴一樣,但是不能在$in中使用(不可見)。
  • 通過構建行末|| dep1 dep2語法表示的次序唯一(Order-only)依賴。他們過期的時候,輸出不會被重新構建,直到他們被重建,但僅修改這種依賴不會引發輸出重建。

變量展開

變量在路徑(在build或default語句)和name = value右邊被展開。
當name = value語句被執行,右手邊的被立即展開(根據以下的規則),從此$name擴展爲被展開結果的靜態字符串。永遠也不會存在,你將需要使用雙轉譯("double-escape")來保護一 個值被第二次展開。
所有變量在解析過程,遇到的時候立即被展開,除了一個非常重要的例外:rule塊中的變量僅在規則被使用的時候才被展開,而不是聲明的時候。在以下的示例中,demo打印出"this is a demo of bar"而不是"this is a demo of $foo"。

rule demo
  command = echo "this is a demo of $foo"

build out: demo
  foo = bar

評估和邊界

頂層(Top-level)變量聲明的邊界,是相關的文件。
subninja關鍵自,用於包含另一個.ninja文件,其表示新的邊界。被包含的subninja文件可以使用父文件中的變量,在文件邊界中覆蓋他們的值,但是這不影響父文件中變量的值。
同時,可以用#include語句在當前邊界內,引入另一個.ninja文件。這個有點像C中的#include語句。
構建塊中聲明的變量的邊界,就是其所屬的塊。一個構建塊中展開的變量的所有查詢次序爲:

  • 特殊內建變量($in,$out);
  • build/rule塊中構建層的變量;
  • 構建行所在文件中的文件層變量(File-level);
  • 使用subninja關鍵字引入那個文件的(父)文件中的變量。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章