新聞中心
最近,在研究 Gradle 和 Java 相關(guān)構(gòu)建的實(shí)現(xiàn),讓我對(duì)不同編程語(yǔ)言的應(yīng)用構(gòu)建燃起了一點(diǎn)點(diǎn)的興趣。

成都創(chuàng)新互聯(lián)公司公司2013年成立,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì)、外貿(mào)營(yíng)銷網(wǎng)站建設(shè)網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元豐都做網(wǎng)站,已為上家服務(wù),為豐都各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:18980820575
不同編程語(yǔ)言編寫的應(yīng)用,在它運(yùn)行的狀態(tài)下,會(huì)有不同的運(yùn)行機(jī)制,有的是以二進(jìn)制的方式運(yùn)行的,有運(yùn)行在編程語(yǔ)言的虛擬機(jī)之上。而構(gòu)建所做的事情呢,就是將那些我們寫給人類看的代碼,轉(zhuǎn)換為機(jī)器/程序能看懂的代碼。所以,構(gòu)建的本質(zhì)就是翻譯(~~復(fù)讀機(jī)~~)。
PS:本文旨在嘗試性的整理我所了解的構(gòu)建知識(shí)。部分內(nèi)容限于對(duì)某一些編程語(yǔ)言的理解有限,并非非常準(zhǔn)確。如有偏頗之此,希望大家指正。
引子 1:從 Java 的編譯說(shuō)起
絕大多數(shù)程序員都是從 hello, world! 開始自己復(fù)制、粘貼的人生生涯。對(duì)于那些剛上手 Java 的程序員也是類似的:
- javac HelloWorld.java
而當(dāng)我們依賴于其它的軟件包時(shí),就需要在編譯時(shí)和運(yùn)行時(shí)加入 classpath 來(lái)加入依賴項(xiàng)。于是,對(duì)應(yīng)的運(yùn)行命令就如下所示:
- java -classpath .:libs/joda-time-2.10.6.jar HelloWorld
這樣,我們就能得到預(yù)期的結(jié)果了:
- Hello, World
- Millisecond time: in.getMillis(): 1599284014762
而如果我們需要打成 jar 包就需要一個(gè)復(fù)雜一點(diǎn)的過(guò)程:
- jar cvfm hello.jar manifest.txt HelloWorld.class libs/*
這個(gè)過(guò)程中,涉及到幾個(gè)關(guān)鍵的要素:
工具鏈。即 java 和 javac,以及對(duì)應(yīng)的 Runtime 等。
構(gòu)建過(guò)程。即我要先執(zhí)行 javac 進(jìn)行編譯,再通過(guò) java 命令來(lái)啟動(dòng)應(yīng)用。
依賴管理。即我們的 joda-time-2.10.6.jar 的位置獲取等問(wèn)題,以及在打包時(shí)加入的過(guò)程。
源碼配置。即轉(zhuǎn)換過(guò)程中的 class 和 java
過(guò)程中的輸入和輸出。
引子 2:任務(wù)及任務(wù)的輸入和輸出
對(duì)于一個(gè)制品的構(gòu)建來(lái)說(shuō),我們往往會(huì)把它拆分為一系列的任務(wù),每個(gè)任務(wù)有自己的輸入和輸出。當(dāng)輸入發(fā)生變化的時(shí)候,需要變化對(duì)應(yīng)的輸出。緊接著,我們只需要對(duì)任務(wù)進(jìn)行編排即可:
- exports.build = series(
- clean,
- parallel(
- cssTranspile,
- series(jsTranspile, jsBundle)
- ),
- parallel(cssMinify, jsMinify),
- publish
- );
如上展示的是:哪些任務(wù)可以并行,哪些任務(wù)需要按順序執(zhí)行——也可以認(rèn)為是任務(wù)的依賴。
當(dāng)然了,還有一種任務(wù)是 watch 任務(wù),只用于開發(fā)時(shí),而非構(gòu)建時(shí)。如下是 Node.js 中的 Gulp 構(gòu)建工具的文件監(jiān)控示例:
- function javascript(cb) {
- // body omitted
- cb();
- }
- function scss(cb) {
- // body omitted
- cb();
- }
- watch('src/*.scss', scss);
- watch('src/*.js', series(javascript));
兩間結(jié)合之下,我們就會(huì)看到增量任務(wù)的概念:只針對(duì)修改的部分進(jìn)行編譯,以提升構(gòu)建效率。在這方面做得比較好的就是 Gradle ,看個(gè)官方的示例InputChanges:
- abstract class IncrementalReverseTask extends DefaultTask {
- @Incremental
- @InputDirectory
- abstract DirectoryProperty getInputDir()
- @OutputDirectory
- abstract DirectoryProperty getOutputDir()
- @TaskAction
- void execute(InputChanges inputChanges) {
- inputChanges.getFileChanges(inputDir).each { change ->
- if (change.fileType == FileType.DIRECTORY) return
- def targetFile = outputDir.file(change.normalizedPath).get().asFile
- if (change.changeType == ChangeType.REMOVED) {
- targetFile.delete()
- } else {
- targetFile.text = change.file.text.reverse()
- }
- }
- }
- }
同樣的,它也需要我們監(jiān)控對(duì)應(yīng)的輸入和輸出。稍有不同的是,Gradle 會(huì)對(duì)文件進(jìn)行索引,每次只提供變化的部分,讓我們根據(jù)自己的實(shí)際需要進(jìn)行處理。
增量構(gòu)建相關(guān)資源:
- tup 是用于 Linux、OSX 和 Windows 的基于文件的構(gòu)建系統(tǒng)。它輸入文件的更改列表和有向無(wú)環(huán)圖(DAG),然后處理DAG 以執(zhí)行更新依賴文件所需的適當(dāng)命令。
- ninja 是一個(gè)專注于速度的小型構(gòu)建系統(tǒng),類似于GNU Make。
- SCons 是一套由Python 語(yǔ)言編寫的開源構(gòu)建系統(tǒng),類似于GNU Make。
引子 3:可選的依賴管理(地獄)
關(guān)于依賴的管理槽點(diǎn),我已經(jīng)寫過(guò)一系列的文章,諸如于:管理依賴的 11 個(gè)策略、依賴孿生:低成本的依賴安全方案。
單純從構(gòu)建這件事情上,對(duì)于依賴的管理是可有可無(wú)的。出現(xiàn)這個(gè)狀況的主要原因是:歷史上的編程語(yǔ)言都不考慮這個(gè)問(wèn)題。所以,在古老的 C/C++ 語(yǔ)言中,構(gòu)建系統(tǒng)就是一個(gè)頭疼的問(wèn)題。當(dāng)然了,新晉的 Golang 也缺少良好的設(shè)計(jì)。
好在,對(duì)于依賴管理來(lái)說(shuō),這個(gè)過(guò)程并不復(fù)雜:
- 包命名和版本機(jī)制
- 包管理服務(wù)器
- 構(gòu)建和運(yùn)行時(shí)的依賴管理
- 包沖突處理
- ……
構(gòu)建的抽象
好了,有了上面的這一系列基礎(chǔ)知識(shí)之后,接下來(lái)我們就可以看看不同的構(gòu)建系統(tǒng)里,對(duì)于同一概念的抽象,整合了 Bazel、Gradle、Cargo、NPM 等之后有了一個(gè)基礎(chǔ)的抽象層次:
- 工作空間(workspace)。工作空間是一個(gè)或者多個(gè)軟件包的集成,它們可以共享依賴、輸出目錄配置等等。典型的有 Java 中的 Gradle settings.gradle、Rust 中的 Cargo 的 Cargo.toml 等。
- 倉(cāng)庫(kù)。倉(cāng)庫(kù)可以映射到 Git 的 repository 中,代表一個(gè)可獨(dú)立構(gòu)建的軟件。
- 包。最小的可執(zhí)行單位的項(xiàng)目結(jié)構(gòu)。
- 包布局。對(duì)應(yīng)于不同的語(yǔ)言、構(gòu)建系統(tǒng)來(lái)說(shuō),它用于定義代碼的存放位置和結(jié)構(gòu)。
- 制品。即構(gòu)建產(chǎn)生的產(chǎn)物,可能是可復(fù)用的軟件包,也可能是可運(yùn)行的應(yīng)用。
- 任務(wù)。定義構(gòu)建的規(guī)則,并執(zhí)行。
FAQ
為什么是沒有項(xiàng)目?在業(yè)務(wù)領(lǐng)域和技術(shù)領(lǐng)域,我們對(duì)于項(xiàng)目的定義存在著一定的歧義性。為了減少二義性,我們使用工作空間 + 倉(cāng)庫(kù)來(lái)解決這個(gè)問(wèn)題。工作空間可以視為一個(gè)完整的業(yè)務(wù)項(xiàng)目。而倉(cāng)庫(kù)呢,則是單一個(gè)的代碼庫(kù),可能是一個(gè)庫(kù),也可能是包含庫(kù)的完整工程。
現(xiàn)有的最佳方案是 Bazel。
工作區(qū)
工作空間是一個(gè)或者多個(gè)軟件包的集成,它們可以共享依賴、輸出目錄配置等等。典型的有 Java 中的 Gradle settings.gradle、Rust 中的 Cargo 的 Cargo.toml 等。
我們可以將其視為最終的產(chǎn)物,如 Android 生成的 APK,Rust 最后生成的可執(zhí)行文件。過(guò)程中,生成的共享的包都是為了支持這個(gè)工程的一部分。
先看 CMakeLists.txt 的目錄,我們?cè)诠ぷ鲄^(qū)的根節(jié)點(diǎn),定義了這個(gè)工程,并添加了 projectA 和 projectB。
- cmake_minimum_required(VERSION 3.2.2)
- project(globalProject)
- add_subdirectory(projectA)
- add_subdirectory(projectB)
以用于生成最后的構(gòu)建產(chǎn)物。相似的還有 Rust 中的 workspace:
- [workspace]
- members = [
- "adder",
- ]
又或者是前端的 Yarn 中的工作區(qū):
- {
- "private": true,
- "workspaces": ["workspace-a", "workspace-b"]
- }
它們做的都是相同的事情。
倉(cāng)庫(kù)
這個(gè)概念的再提取是來(lái)源于 Bazel。倉(cāng)庫(kù)是一系列包的合集,我們可以將其視為團(tuán)隊(duì)的邊界,從某種意義上可以看作是代碼倉(cāng)庫(kù)。對(duì)于一個(gè)龐大的工程來(lái)說(shuō),它的代碼來(lái)源是多種多樣的,來(lái)自組織內(nèi)的其它團(tuán)隊(duì),來(lái)自組織外的其它團(tuán)隊(duì)。每個(gè)獨(dú)立的部分,即是一個(gè)倉(cāng)庫(kù)。
值得注意的是,從最終產(chǎn)物來(lái)看,每個(gè)團(tuán)隊(duì)的產(chǎn)出都是倉(cāng)庫(kù),但是呢,在團(tuán)隊(duì)內(nèi)部,他們就是工作區(qū)。
讓我們看個(gè) Gradle 的多項(xiàng)目構(gòu)建示例(Android 工程):
- .
- ├── README.md
- ├── library_a
- ├── app
- │ ├── build.gradle
- │ └── src
- ├── build.gradle
- ├── local.properties
- ├── settings.gradle
- └── third-partys
- ├── ...
- ├── build.gradle
- └── settings.gradle
從目錄結(jié)構(gòu)來(lái)看,這個(gè)是一個(gè)工作區(qū),而在工作區(qū)呢,它包含了一些三方的代碼倉(cāng)庫(kù)(third-partys),以及自身的庫(kù) library_a 和應(yīng)用 app。
因此,在這里的 library_a 和 third-partys 的各個(gè)項(xiàng)目都算是倉(cāng)庫(kù)。
包
包是一系列代碼的合集,它可大可小。最主要的原因在于,因?yàn)闃?gòu)建時(shí),我們可能會(huì)把一個(gè)倉(cāng)庫(kù)(哪怕是最小的 Gradle 項(xiàng)目)產(chǎn)出多個(gè)包,如 Java 項(xiàng)目中的 src/main 和 src/test。
于是在諸如 bazel 這樣的構(gòu)建工具中,支持自定義的包:
- src/my/app/BUILD
- src/my/app/app.cc
- src/my/app/data/input.txt
- src/my/app/tests/BUILD
- src/my/app/tests/test.cc
對(duì)于一個(gè)包來(lái)說(shuō),往往我們還需要定義一系列的相關(guān)信息,如包名、依賴信息、入口等等。如 Bazel 中對(duì)于 Java 構(gòu)建的示例:
- java_binary(
- name = "ProjectRunner",
- srcs = ["src/main/java/com/phodal/ProjectRunner.java"],
- main_class = "com.phodal.ProjectRunner",
- deps = [":greeter"],
- )
這已經(jīng)實(shí)現(xiàn)了對(duì)于不同包的信息抽象。順帶的再看個(gè) Java 包中的 MANIFEST 的示例:
- Main-Class: HelloWorld
- Class-Path: libs/joda-time-2.10.6.jar
我們就可以知道之間的聯(lián)系。
包定義
在打包階段,我們以簡(jiǎn)單的形式定義了這個(gè)包——因?yàn)樗⒎悄敲粗匾覀円膊魂P(guān)心。而當(dāng)我們決定發(fā)布這個(gè)包到互聯(lián)網(wǎng)時(shí),我們就需要好好定義這個(gè)包。對(duì)應(yīng)的一些必要信息有:
- name
- version
- authors
- license
- description
- ……
這些信息用于在包管理中心展示,并向使用者提供包相關(guān)的信息等。不同的語(yǔ)言中使用的是不同的形式,Rust 使用了自定義的 toml,而諸如 Maven 倉(cāng)庫(kù)中則使用了 XML:
... ... ... ... ... ... ...
類似的在 NPM 的 package.json 中也使用了類似的字段: name、 verison 等信息。
而在這些編程語(yǔ)言中,這個(gè)東西就設(shè)計(jì)得過(guò)于簡(jiǎn)單了,如 Python 的 pip 中使用的 requirements.txt 來(lái)管理依賴,當(dāng)你要發(fā)布包的時(shí)候使用 setup.py 進(jìn)行配置。于是,你的應(yīng)用如果不發(fā)布,那就沒有包名了……。
包布局
構(gòu)建工具在設(shè)計(jì)的時(shí)候,會(huì)設(shè)計(jì)默認(rèn)的軟件包分層結(jié)構(gòu),這個(gè)分層架構(gòu)就是包布局(package layout)。構(gòu)建工具通過(guò)這個(gè)布局,來(lái)獲取所需的輸入源和配置等信息。它也包含了一些默認(rèn)的配置,如 src/main 指向了源碼的目錄, src/test 指向的是測(cè)試代碼(不會(huì)加入到制品中)
- ├── build.gradle
- └── src
- ├── main
- └── test
對(duì)于使用者來(lái)說(shuō),它們也可以針對(duì)于它們的需要擴(kuò)展這個(gè)布局,如 Gradle 里的 SourceSets:
- sourceSets {
- main {
- output.resourcesDir = file('out/bin')
- java.outputDir = file('out/bin')
- }
- }
對(duì)于其它語(yǔ)言也是類似的。但是呢,對(duì)于某些語(yǔ)言來(lái)說(shuō),并非有這么強(qiáng)的關(guān)聯(lián),如在 Golang 中,就沒有這么強(qiáng)的約束。只是呢,原先是默認(rèn)值,現(xiàn)在需要開發(fā)人員來(lái)手動(dòng)配置。
制品
制品是最終的構(gòu)建產(chǎn)物。同樣的,在不同的語(yǔ)言中有不同的命名方式。在 Gradle 中稱為 artifacts,在 Rust 中稱為 targets……。制品,主要涉及到的是各種文件的流轉(zhuǎn)及其流轉(zhuǎn)規(guī)則。
舉個(gè)簡(jiǎn)單的例子,一個(gè) jar 文件中必須包含一個(gè) MANIFEST.MF,以用于配置應(yīng)用程序、擴(kuò)展和類裝載器等相關(guān)信息。而相關(guān)的文件又會(huì)以 META-INF 的方式組織起來(lái)。
因此在整個(gè)制品的創(chuàng)建過(guò)程中,就是復(fù)制對(duì)應(yīng)的文件,進(jìn)行相應(yīng)的轉(zhuǎn)換,如 java -> .class,再?gòu)?fù)制到對(duì)應(yīng)的目錄,最后再打包在一起的過(guò)程。
任務(wù):規(guī)則引擎 + DSL
在上述我們看到的例子中,很多就是創(chuàng)建了自身的 DSL,而后用于構(gòu)建。只有這樣才能讓使用者得到最大的方便。這是一個(gè)相當(dāng)復(fù)雜的過(guò)程,它相當(dāng)于我們要設(shè)計(jì)一個(gè)和平臺(tái)、語(yǔ)言無(wú)關(guān)的 DSL。而這種演變方式有多種:
使用 API 抽象的內(nèi)部 DSL。諸如于 Webpack、Gulp 等實(shí)現(xiàn)。
自制的外部 DSL 語(yǔ)言。如 Gradle 所使用的 Groovy、多語(yǔ)言的 Bazel。
規(guī)則引擎本身是一組關(guān)于任務(wù)的 DSL,看個(gè) Gradle 的例子:
- task copyReportsDirForArchiving2(type: Copy) {
- from("$buildDir") {
- include "reports/**"
- }
- into "$buildDir/toArchive"
- }
它所做的事情就是復(fù)制。對(duì)應(yīng)的 Gradle 打包示例也是蠻簡(jiǎn)單的 DSL 抽象:
- task packageDistribution(type: Zip) {
- archiveFileName = "my-distribution.zip"
- destinationDirectory = file("$buildDir/dist")
- from "$buildDir/toArchive"
- }
Gradle 使用的就是外部 DSL。再看看 Webpack 的打包示例:
- module.exports = {
- entry: './path/to/my/entry/file.js',
- output: {
- filename: 'my-first-webpack.bundle.js',
- path: path.resolve(__dirname, 'dist')
- },
- module: {
- rules: [
- {
- test: /\.(js|jsx)$/,
- use: 'babel-loader'
- }
- ]
- },
- plugins: [
- new webpack.ProgressPlugin(),
- new HtmlWebpackPlugin({template: './src/index.html'})
- ]
- };
這里的 rules 就是一個(gè)簡(jiǎn)單的規(guī)則引擎(使用正則表達(dá)式來(lái)匹配)
兩種模式各自有自己的優(yōu)缺點(diǎn),復(fù)雜場(chǎng)景下,使用 DSL + 自定義的腳本更容易完成。
PS:看來(lái)有空,我也應(yīng)該寫一個(gè)的規(guī)則引擎
構(gòu)建的擴(kuò)展
對(duì)于主流的構(gòu)建系統(tǒng)來(lái)說(shuō),他們都支持不同形式的擴(kuò)展支持:
- 外部 DSL 擴(kuò)展
- 插件化的接口編程
- 項(xiàng)目?jī)?nèi)編程語(yǔ)言擴(kuò)展
- 項(xiàng)目外編程語(yǔ)言擴(kuò)展
大部分的東西,我們已經(jīng)在文中的先前部分提到了,這里就不重復(fù)描述了。
結(jié)論
應(yīng)用的構(gòu)建是一個(gè)相當(dāng)有意思的過(guò)程。
設(shè)計(jì)一個(gè)構(gòu)建系統(tǒng)也變得頗為有趣的。
參考資料:
- Gradle vs Bazel for JVM Projects
- Bazel: Concepts and terminology
- Yarn: Workspaces
- Gradle: Authoring Multi-Project Builds
- Cargo: Workspaces
- Gulp: Tasks
相關(guān)目的開源庫(kù):
- lerna A tool for managing JavaScript projects with multiple packages.
- bazel
- Blueprint is a meta-build system that reads in Blueprints files that describe modules that need to be built, and produces a Ninja manifest describing the commands that need to be run and their dependencies.
本文轉(zhuǎn)載自微信公眾號(hào)「phodal」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系phodal公眾號(hào)。
文章題目:聊一聊構(gòu)建的抽象
分享網(wǎng)址:http://fisionsoft.com.cn/article/dpgcojd.html


咨詢
建站咨詢
