[翻譯] Makefile - 失落的藝術
譯者吐槽:儘管 Makefile 似乎與前端漸行漸遠,不過還是有很多神奇海螺一般的魔法值得我們去入門,本文與前端的內容相結合進行了一番介紹和示例科普,適合所有對 Makefile 一臉懵逼的小夥伴。
原文:http://www.olioapps.com/blog/the-lost-art-of-the-makefile/
我做過許多 JavaScript 項目。JavaScript 目前的潮流是使用用 JavaScript 書寫和配置的構建工具像 Gulp 或者 Webpack。我想要討論的是 Make 的長處(尤其是 GNU Make)。
Make 是一種通用的構建工具,它自40年前推出以來一直在不斷完善和改進。 Make 非常擅長簡潔的表現構建步驟,此外它並不特定用於 JavaScript 項目。 它非常適合增量構建,在更改大型項目中的一個或兩個文件後重新構建時,Make 可以節省大量時間。
Make 已經存在了足夠長的時間來解決那些新出現的構建工具現在剛剛發現的問題。
儘管標題上稱之為失落的藝術,但實際上 Make 仍然被廣泛的使用著。不過我認為它在 JavaScript 中代表性仍顯不足。你會更常見於 C/C++ 之類的的項目。
我的猜測是,JavaScript 社區中的大部分並沒有一個 Unix 編程的背景,也一直沒有一個很好的機會去了解 Make 有哪些功能。
在這裡,我將會提供一個簡單的入門。我會用我自己的 JavaScript 項目來介紹 Makefile 的內容。
你可以在這裡找到完整的文件。
什麼時候需要堅持使用 Webpack
Webpack 所做的工作非常的專業化。如果你正在編寫一個前端應用,並且你需要打包代碼,你的確需要 Webpack(或者相似的 Parcel)。
另一方面,如果你的需求很一般時,Make 是一個非常好的工具。我在編寫客戶端或者服務端庫、Node App 時使用 Make。在那些情況下,我沒有從 Webpack 的特殊功能中受益。
為什麼 JavaScript 需要一個構建的步驟
讓我們快速解決一下這個問題:為什麼有些人希望在打包代碼的過程中加入構建這個環節。
我希望能夠編寫同時針對瀏覽器和最新穩定版 Node 代碼的 Stage 4 ECMAScript。我也希望我的代碼中能包含 Flow 類型注釋,我希望對我的代碼進行處理,並且可以轉換為純 JavaScript。所以我使用 Make 來調用 Babel 轉換代碼。
介紹 Makefile
Make 會在當前文件夾尋找一個叫做 Makefile
的文件。一個 Makefile
是一系列像下述的列表:
target_file: prerequisite_file1 prerequisite_file2 shell command to build target_file (必須用 tab 縮進而不是空格) another shell command (這些命令被稱為菜單)
除非你另行制定,Make 會假定目標(在這個例子中是 target_file
)和條件(prerequisite_file1
和 prerequisite_file2
)是文件或者目錄。你可以要求 Make 由命令行構建一個目標:
$ make target_file
如果 target_file
不存在,或者 prerequisite_file1
或 prerequisite_file2
在 target_file
最近一次構建後被修改了,Make 將會執行給定的 shell 腳本。不過在此之前 Make 將會先檢查在 Makefile 中是不是有針對 prerequisite_file1
和 prerequisite_file2
的內容,並且按需構建或重新構建。
Makefile 規則的一個實際例子
一個最小的項目可能有一個叫做 src/index.js
的文件。我們需要一個規則來告訴 Make 轉換該文件並將結果寫入 lib/index.js。但是 Make會以相反的方式看待這件事:Make 希望被告知需要的結果,然後使用規則來指定如何產生這個結果。所以我們編寫這個 Makefile 的規則的目標是 lib/index.js
,條件是 src/index.js
:
lib/index.js: src/index.js mkdir -p $(dir $@) babel $< --out-file $@ --source-maps
這個菜單通過 babel 轉換 src/index.js
生成 lib/index.js
。Makefile 的菜單中的 shell 命令與你在 bash 中輸入的內容幾乎完全相同——但是請注意,Make 替換變數和在命令前以 $
開頭表達式將會被執行。你可以通過加倍 $
來轉義(比如說 cd $$HOME
)。在上面的菜單中有兩個特殊的變數 $<
是對條件列表的簡稱(本例中為 src/index.js
),$@
是目標的簡稱(本例中為 lib/index.js
)。我們會在瞬間明白為什麼這些變數是不可或缺的。
mkdir -p
行在 lib/
目錄不存在的情況下創建行。函數 dir 從文件路徑中提取目錄部分,因此 $(dir $@)
讀作「包含被 $@
引用的文件的目錄路徑」。
廣義規則
當我們向項目中添加更多的文件時,為每個 JavaScript 文件編寫一個 Makefile 目標將非常的繁瑣,目標和條件可以使用通配符來創建一個模式:
lib/%: src/% mkdir -p $(dir $@) babel $< --out-file $@ --source-maps
這告訴 Make 任何的由 lib/
開頭的文件路徑可以用給定的步驟來構建,並且目標依賴於 src/
下的相對應的路徑。無論什麼字元串在 Make 中的目標中用 %
表示,他會替換條件中的相同位置的 %
字元串。現在我們更清楚為什麼變數 $<
和 $@
是必須的了:在規則調用之前,我們並不知道這些變數的值。
為什麼分別為每個源文件調用 Babel
通過一次調用,Babel 就可以傳輸目標樹中的所有文件。但上面的規則會為 src
下的每個文件運行 babel,每次運行 babel 時都會有一些啟動時間的開銷,所以在進行一次提交中多次調用 babel 會更慢。但是,由於 Make 的增量構建能力,它將跳過 lib/
下已經有最新結果的文件。我們運行增量構建的次數遠遠多於完整構建,所以我非常欣賞這種加速。
編輯:Hacker News 中的幾位評論者(falcolas, Jtsummers, jlg23, nzoschke)指出 Make 可以並行的執行任務。因為 Make 規則中明確的列出了每個目標的依賴關係,所以知道哪些任務可以安全的並行運行。使用 make --jobs=4
命令可以一次執行最多 4 個 Babel 實例,這可以抵消為每個源文件運行單獨的 Babel 實例的性能損失。
定位 Babel
我在 Makefile 的上述的規則中做了一點微小的改動:
babel := node_modules/.bin/babellib/%: src/% mkdir -p $(dir $@) $(babel) $< --out-file $@ --source-maps
babel
可執行文件由 babel-cli
這個 npm 包提供。我更喜歡將 babel-cli
安裝作為項目的 dev dependency,這會導致 babel 的可執行文件安裝在路徑 node_modules/.bin/babel
上。這樣,任何想要構建我項目的人都不必採用特殊步驟去安裝一個全局的 babel-cli
,但是大多數機器上,babel
並不會配置在可執行的 $PATH
中,為了避免輸入可執行文件的路徑,我將 babel 的位置分配給 Makefile 中的變數(babel := node_modules/.bin/babel
),並且使用 Make 的變數替換將該路徑作為菜單中的命令。
(專業提示:您可以將 node_modules/.bin
添加到您的 shell 的 $PATH
中,像這樣:PATH="node_modules/.bin:$PATH"
。這樣可以很容易的運行當前目錄中項目依賴項安裝的可執行文件。與項目一起安裝的優先順序會高於全局安裝的可執行文件,當你運行 npm 腳本時,npm 會自動進行這個 $PATH
優先順序得陶正。不過我總是在 Makefile 中聲明 babel 的 path,因為我不想假設任何人做了同樣的設置,而且我也並不是總想從 npm 腳本中運行 make。)
轉換整個項目
用如下的規則來轉換 JavaScript 源文件:
$ make lib/index.js # outputs lib/index.js and lib/index.js.map
Make 在 Makefile(lib/%
) 中找到匹配的項目,展開通配符,通過展開 src/%
來找到匹配的源文件,並運行 babel。但是你可能不想為每個源文件手動運行 make
。你想要的只是輸入 make
並讓它轉換所有的源文件。記住 Make 需要被告知你想要的結果。為此,首先需要計算一份所有源文件的列表,並且將其分配給一個變數:
src_files := $(shell find src/ -name *.js)
在這個任務右側的表達式使用了 Make 內置的 shell 函數來運行外部de shell 命令。在這種情況下,我們應該使用 find 命令來遞歸的列出 src/
下面所有擴展名為 .js
的文件。您可以使用另一個命令,比如 [fd]
[]——不過 find
更可能已經被安裝在您同事的工作站和你的 CI 伺服器中。
這下我們拿到了我們所有的文件列表。但是我們需要告訴 Make 我們需要的文件。對於每一個在 src
下面的文件,我們都希望在 lib
下面有一個對應的轉換後的文件。我們可以通過將 Make 的 patsubst
函數應用到每個源文件中來計算該列表:
transpiled_files := $(patsubst src/%,lib/%,$(src_files))
替換表達式使用 %
作為通配符,與我們之前編寫的規則相同,寫起來更容易了。
現在我們可以定義一個列出我們想要的文件作為條件的目標。當我們請求該目標時,Make 會自動為每個源文件建立一個轉義的結果:
all: $(transpiled_files)
目標名 all
是特殊的:當你在運行時沒有指定 make 的目標時,他會執行 all
作為默認值。這是當目標不是文件或者目錄時的特殊情況——all
只是一個標籤。你需要像這樣在你的 Makefile 中聲明非文件的目標,這樣 Make 就不會浪費時間,也不會為了在你的項目中找到匹配的文件而感到困惑。
.PHONY: all clean
哦,是的!或許你想要一種方法來刪除已經構建的內容,以便你可以開始乾淨的構建,有了這個目標你可以運行 make clean
來達到這個效果:
clean: rm -rf lib
在 package.json 改變時自動安裝 node modules
Make 非常強大,足以完成任何你可以想像的任務。你是否曾經提交更新給一個項目,並且在進行一些 debug 後發現你是忘了運行 yarn install
來更新你的依賴關係?你可以通過 Make 來做到!當你運行 yarn install
時,nodemodules 目錄將會創建或者更新。你可以在 Make 中添加一條規則將 nodemodules 作為目標來更新 node_modules。
node_modules 的狀態取決於 package.json 和 yarn.lock 的內容,所以這些文件應該作為條件被列出:
node_modules: package.json yarn.lock yarn install # could be replaced with `npm install` if you prefer
這個針對 all 目標的變更添加了 node_modules 作為條件。
現在,當且僅當自上一次構建後 package.json 或者 yarn.lock 發生了變化時,Make 才會運行 yarn install
。我在 $(transpiled_files)
之前放上了 node_modules
,以防萬一新的依賴可能會包含 babel 模塊的更新,這樣會影響到項目文件的構建。
監聽文件並且重構更新
每個構建工具都應該有適用於快速開發的監聽文件變更的選項。你可以通過 Make 與通用文件監聽工具的組合來達到這個效果:
$ yarn global add watch$ watch make src/
記得注意你沒有監聽 lib/
目錄,否則的話你會陷入一個無盡的構建循環中。
使用 Make 來分配 Flow 類型定義
我在上文中提到我也經常使用 flow 來檢查我的項目,我想使用純 JavaScript,但我也希望任何使用我的庫的人使用 Flow 來從我的類型注釋中獲益。Flow 支持查找一個使用了 .js.flow
的文件擴展名。比如,當你導入一個名為 User 的模塊時,JavaScript 運行庫將查找名為 User.js 的文件,而 Flow 將在同一目錄中另外查找名為 User.js.flow 的文件。該文件應該是具有類型注釋的原始源文件。我的 Makefile 將 src/
下的每個文件複製到 lib/
的響應路徑,並根據此規則添加 .flow
擴展名。
lib/%.js.flow: src/%.js mkdir -p $(dir $@) cp $< $@
為了確保 Flow 中所有的源文件按照此步驟運行,我計算了 .flow
的文件列表,我期望得到與我們預期相一致的結果:
flow_files := $(patsubst %.js,%.js.flow,$(transpiled_files))
另外,我在所有的任務的條件中包含了 flow_files
:
all: node_modules $(flow_files) $(transpiled_files)
進階
Make 還有很多我在這裡沒有提及的功能。例如,Make 支持可以為特別複雜的用例計算規則的宏,用 Makefile 可以委託目標給其他 Makefile。這在分發 Make 庫中非常有用。也可以用於構建過程中涉及到構建多個子項目組合在一起的多層項目,GNU Make Manual 可以找到更多有用的信息。
推薦閱讀: