編程語言的支撐體系:構建系統、IDE 和依賴管理
年關(annual review)將近,這一段時間,我在梳理 2020 年做的一些事情,并試著制定下一年的計劃。過程中,我發現我做的一些事情,或是工作相關,或是興趣上的探索,還都可以繼續總結出一些文章。在工作上,很多一部分做的事情就是編程語言的支撐體系。外加業余時間里,和同事一起花了一些時間在研究編程語言。在這幾部分的結合之下,我對于整個體系的端到端實現有一個整體的認識。
作為一個職業的程序員,在我們的職業生涯里,不可避免地要學習一個又一個的編程語言。雖然多數情況下,我們對于使用什么語言并沒有太多的選擇權。但是,當我們選擇一門語言時,都要考慮一系列的要素,比如:
- 構建系統
- IDE/Editor 支撐
- 依賴管理
- ……
PS:當然了,對于那些使用 C/C++ 的人來說,這些可能都是例外:他/她覺得自己不需要這些工具,需要的時候可以自己創造一個。所以,這些語言在很長的一段時間里,都缺乏良好的依賴管理工具。
故事開始之前,讓我們讓 Android 使用的開發和構建來講述這個過程。
從 Android 應用的開發與構建說起
在移動端開發上,雖比不上這個行業的諸多大佬,但我也算是頗有經驗的。而恰好一年中有一半的時間,都在相關的項目上。所以,我從宏觀上了解了整體的體系。
當我們開始一個新的移動應用時,會從 IDE 里通過模板創造一個嶄新的應用,又或者是從某個地方(如 GitHub)尋找合適的模板。而后,為驗證模板的有效性,我們通過執行 Gradle 的相關命令,完成一個應用的過程,運行這個 Demo。(PS:這一點與我們使用 Java 開發應用時,并沒有太大區別)。
這個過程中,發生了這么一些事情:
- IDE 通過某種通訊機制,與 Gradle 進行通訊,以執行對應的命令,如 build。
- Gradle 接收到 IDE 的指令后,解析 build.gradle 相關的內容,尋找是否存在對應的 Task,如這里的 build。
- 執行 build 時,首先要去解決依賴關系,如從對應的 Maven 倉庫中下載依賴。
- 隨后,真正地執行對應的構建任務,如調用 javac。
這個過程看上去非常簡單,但是背后還藏著諸多的細節問題。
構建與依賴管理
當我用 CLOC 工具統計了一下 Gradle 工具的源碼時,我才發現這個工具并不簡單。而進一步地,在半深入源碼之后,我發現構建系統還是頗為復雜的。一個簡單的 Java 應用就分為這么一些步驟:
- :compileJava UP-TO-DATE
- :processResources UP-TO-DATE
- :classes UP-TO-DATE
- :run
而當我們有依賴的時候,需要添加上 classpath,即將依賴添加到編譯的路徑中。而對于一些非 .jar 類型的依賴而言,如 .war,構建工具還要支持對他們的解析。因此,整體的過程就是:
- 判斷是否存在本地的依賴,如果沒有的話,從遠程獲取。如果有依賴沖突的話,解決這些沖突,或者報錯。
- 獲取依賴后,根據需要對依賴進行處理。如 Android 中的 aar 包的解壓等。
- 結合依賴,對源碼進行編譯
- 將所需要的 Java Resources 從依賴的 Jar 拷貝到指定目錄
- 打包構建后的產物到一個新的 jar 包中
這些只是表面上的一些工作。而為了更好地表述這個過程,需要抽象出一個 task 的概念,在這個概念里,一個 task 有輸入和輸出。如
- 解析依賴里。它的輸出是 build.gradle 文件,輸出是處理完的依賴路徑。
- 編譯任務里。它的輸入是源碼,輸出是 .class 文件。
- 打包任務里。它的輸入是一堆文件夾或者文件,輸出是一個 .jar 包。
- ……
于是,在有了這些基礎之后,為了加快構建,還需要緩存的機制。它對輸入和輸出進行計算,當兩者發生變化的時候,再進行編譯。否則就跳過這個任務。
而這些只是核心功能,在非核心的功能區里,還有諸如于 SDK 版本、多輸入多輸出的變體等等。
IDE 與構建系統
在那篇《編程語言的 IDE 支持》中,我們已經介紹了編程語言所需要的 IDE 功能,諸如于:
- 語法高亮
- 子系統關聯與集成
- 跳轉與引用分析
- 智能感知
- 重構
- 快速修復
- 結構化視圖
- ……
在這篇文章中,大概再回顧一下它與構建系統之間的關系。IDE 與構建系統一般會存在這種關聯:
- 解析構建系統中的任務。如 Gradle 提供的 task,又或者是 package.json 中的 scripts,并將它們顯式地展示出來,如 IDEA 中的 line marker,又或者是獨立的 Gradle pannel。
- 執行構建任務。即在 IDE 中的 UI 與構建命令相綁定,典型的如 IDEA 中的 Android 應用的構建。
- 動態修改構建系統(可選)。如 IDEA 中的更新依賴版本,它依賴于解析構建系統的 DSL,并更新對應的 DSL。
對應的有兩種機制可以與構建系統通訊:
- 由構建系統提供構建 API。如 Gradle Tooling API,在那篇《Gradle IDEA 的項目模型》中,我們實際上介紹了由構建系統主動向 IDE 提供模型的方式。
- 由 IDE 構造一遍構建系統。如 IDEA 對于 Node.js 的處理方式。
簡單來說,就是復雜的系統應該由構建系統提供機制,而簡單的構建系統則就不會有這樣的問題。
依賴管理的基礎設施
不同語言對于依賴的管理機制都有所不同,但是它們的原理都是相似的:
- 源碼包。即將源碼打包,并以特定的格式發布,適用于腳本語言
- 倉庫源。方式類似于源碼包,唯一不同的地方是借助于版本管理工具,如 Golang。
- 類二進制包。典型的是 Java
- 其它包。如 Maven 可以支持其它自制的包
最有意思的是Maven 的機制,我可以自制依賴,并上傳上去。而整個倉庫并不關心這個包的內容,我們只需要依賴于它定義的格式即可。如果我們考慮圍繞語言來設計依賴管理體系,那么可以考慮的是類似的方式,并借助于 Git 這樣的版本工具。這樣一來,我們就可以去中心化。
本文轉載自微信公眾號「 phodal」,可以通過以下二維碼關注。轉載本文請聯系 phodal公眾號。