[翻譯] 該把 makefile 請回來了

何苦一直重新發明輪子呢?

(本文譯自 Time for Makefiles to Make a Comeback,副標題為譯者自創)

許多開發者把 Make 與 makefile 遺留在時光的遙遠彼方,他們的優點被埋沒在一海票重新發明輪子的工具之中。夠了,可以停了,我們該離開這沒完沒了的旋轉木馬了。

如果你問開發者們「提到 Make 跟 makefile 的時候你們會想到什麼」,得到的答案大概會像這樣:C/C++、原生軟體專案、肥大、老舊。有些在 JavaScript 生態圈長大的年輕開發者甚至沒聽過 Make,不知道能夠利用這個已經存在許久、經過實戰驗證、且長期穩定的工具。我們有必要像是 Javascript 專案來來去去一樣,每 18 個月就重新學一套建置(build)工具嗎?

不理解 Unix 的人註定得重新發明輪子,而且還是爛輪子。
 
Usenet 上的名片檔,1987 年 11 月
 — Henry Spencer

這對現代的 JavaScript 開發有什麼影響?

現代的 JavaScript 專案常常為了新的語法功能,或是強類型檢查,而使用一些會編譯成 JavaScript 的第三方程式語言。我們會用 Browserify 把用到的 JavaScript 模組打包成一包 bundle,讓前端開發者能夠用模組讓開發過程比較 composition-based(就像 node.js 的後端開發者那樣)。我們會把 SASS 跟 LESS 編譯成瀏覽器看得懂的 CSS。我們也會為了加快下載速度減少頁面載入時間而 minimize JavaScript 檔案。

這些事情的共通點,在於他們都要去拉一堆檔案當輸入,把他們轉換成另外一堆產出檔案。而 Make 做這件事做得超級好。

Makefile 的功能是什麼?實際上 Makefile 只是在宣告要把某個(或某些)檔案轉換成另外一個(或一些)檔案,就這樣而已。Make 並不專屬於 C/C++,甚至不寫程式的時候也可以用的上 Make。你可以用 Makefile 把 markdown 文件轉換成出貨用的 HTML,或是把重要的檔案打包成 zip/tar 檔案,或是其他各種千奇百怪的用途。

因為 Makefile 本質只是宣告需要做哪些轉換,所以 Make 可以檢查 Makefile ,然後只去跑那些真的需要做的轉換。如果原始檔在上次轉換跑完之後沒有修改過,那麼這個檔案就沒必要又被轉換一次。在大型專案之中,這能夠大幅加快 build 速度,也能讓開發體驗更加流暢。

Make 已經存在超過 40 年,有相當多軟體是透過 Make 來 build 出來的。Make 經過長時間的實戰驗證,而且相當擅長完成他被設計來做的工作:把原始檔案轉換成輸出檔案,並且能夠用簡單易瞭的方式宣告檔案的相依性。Makefile 是純文字,只專注在要解決的問題不會包山包海,而且能夠透過呼叫 shell script 很輕易地整合外部工具。畢竟這是在 Unix 環境下誕生的工具,這不太意外。

有些人曾經看過在大型軟體專案中有著錯綜複雜的 Makefile,不過一般來說不需要把事情搞得很複雜。例如說,要 build 一個用 Typescript 寫的 NPM package(從 build 原始碼到打包 NPM package)其實很簡單:

雖然這個例子是給小型專案用的,但是對就算專案變得更大了,Make 依然一樣的好用。下面我舉個例子。

我因為工作參與了一個大型專案。這個專案是用 AWS Kinesis 跟 Lambda 搭建的無伺服器串流資料服務。這個服務本身是單獨一個 git repo,不同的 Lambda handler 會重複使用一些共用的函式庫。為了簡化些修正問題或新功能上線的流程,我們希望函式庫本身能夠單獨 deploy 上線,而不是每次都得要 deploy 整個專案。

我們的專案架構參考 StongLoop 上這篇關於模組化的 node.js 專案架構的文章。雖然我們用的是 TypeScript,這個架構對我們也很適用。

我們的專案架構看起來大概像這樣:

然而,隨著模組的數量和相依性不斷增加(大型企業軟體都這樣),這個做法變得很又肥又難用。我們得寫一狗票的「安裝前執行」「安裝後執行」「啟動前執行」script,要搞懂怎麼啟動整個專案是很痛苦的一件事情。把新的子專案整合進來是一屁股痛。如果不花一堆額外心力照顧的話,要重 build 專案就得整個專案從頭到尾重新 build 一次。

在把 gulp 之類的熱門流行火辣小鮮肉引入專案之前,我們決定先研究一下早就存在的 Make 能夠幹些什麼。這決定引發了我對 Make 如同滔滔江水一般的景仰(也讓我決定寫這篇文章)。

因為我們的專案很大,所以我們很在意要引入的專案建置軟體本身能不能應付大型需求。我發現 Make 在這一點做得非常好。雖然有些怪地方你要花點時間習慣,但我認為經驗不多的開發者也能夠把專案整合進 Make 的流程。

上面的專案的 makefile 看起來大概會像這樣:

如你所見,內容並不會很複雜。因為這是個後端服務,我們沒有用到 bowserify、LESS、或是 minification。但就算需有需要用到這些功能,整個流程還是可以很直覺。

如果你改動了 baz 專案,只有 baz 會被重 build 並重新 install 進子專案。只要再塞個 watcher 進來(例如 watch ‘npm run build’ src --wait=5 這樣的簡單 npm script)就能引入各種變化,讓本機開發過程更流暢。

優點

東西只有在需要的時候才會被執行,我很欣賞這一點。你甚至不需要弄個 incremental compiler。只要原始檔案沒被動過,就不需要重產輸出檔案。Make 會比較輸入跟輸出的修改時間決定是否需要重建輸出檔案。要整合現存的工具(例如 tsc 或 npm)也很容易,不用像是 code based 的建置工具那樣要等人家(或是自己動手)寫 wrapper。

跟其他 code-based 的工具(Grunt 或 Gulp)相比,Make 還有個比較不明顯的優勢:他是宣告式(declarative)的,不是指令式(imperative)的。你會專注在結果上(要得到什麼),而不是專注在如何完成這些工作。

Make 本身是獨立的工具,不像其他 code-based 工具一樣得處理一堆相依性。不但使用體驗會比較好,也確保軟體爛掉的機會比較低(例如新版的相依套件造成程式主要功能爛掉)。

缺點

是的,又要重新學習新工具跟新語言。不過軟體工程師本來就是拿錢做這些事情的,我們總是得學新工具跟新技術(以這個例子來說,舊工具跟舊技術 :p),每個月都有最新最棒的程式語言跟函式庫冒出來,我們得接受這個活到老學到老的過程。

不過請記住,在這裡我們學的是個泛用工具,可以在很多地方使用,可以用很久。Alton Brown 不用擔心,這是個多用途工具。Make 已經存在了四十年,目前還看不到他被拋棄的那一天。不管是 Grunt 或 Gulp 或下一個新潮好棒棒 Task Runer 應該都不敢說這種話。

不過有一點確實該注意, Make 一直以來都缺少對 Windows 的支援。引入 Make 可能會讓你的 Windows 使用者日子很難過。對某些專案來說這完全不能接受。不過微軟在 Satya Nadella 的領導下不斷地改頭換面,現在 Windows 也加入了 Linux 支援,這樣的擔心可能已經過時了。我認為這對軟體開發者來說是一大助力。

學 Make 現在正是時候

所以現在就開始學怎麼運用 Make 跟 makefile 吧。他們對現在的開發者來說仍然十分有用。不需要拿那些今天長這樣明天長得又不一樣的新潮 Task runner 來用。不需要被一整排的建置工具大軍壓得喘不過氣。你應該學習能夠長時間使用不退流行的工具。

是的,我們該把 Make 請回來了。