I.可重複構建
可重複構建即構建是可以重現的,如果給定相同的源代碼、構建環境和構建指令,任何人都可以重新構建一個BEP一致的相同副本。(想要詳細瞭解的可參考官網或者維基,以及Debian、Yacto和martinfowler)
這個背後的邏輯是,通過可復現的構建過程來驗證在此編譯過程中沒有人爲(故意、脅迫或其他影響導致)的引入漏洞和後門。這樣也很容易與任何一個第三方驗證機構達成共識,對可重複的過程中凸顯的偏差進行識別與審查。
達成可重複構建,需要滿足的條件包括:
- 1)一個確定輸出的構建系統,通過它來構建源代碼始終輸出相同的結果。例如,不會記錄當前的時間戳,輸出件是順序無關的,或者保障順序的。
- 2)一個清單記錄了構建環境和構建工具
- 3)用戶能夠有一個較方便的方法來重新創建這樣一個構建環境,執行構建過程,並最終驗證輸出是否與原始構建匹配。
II.Version Lock
顯而易見,如果達成了一個滿足可重複構建的構建系統(含構建環境和構建工具)。那麼會影響構建輸出的就只有源代碼本身及其依賴。
version lock顧名思義即版本鎖。通過一個file或者list來鎖定依賴的版本,從而確保依賴的穩定性。
下面分語言介紹其可重複構建和version-lock的情況
1.JAVA的可重複構建和version-lock
java的可重複構建可以參考JFrog提供的這個樣例,包含了maven和gradle兩種場景。
(1)Gradle的可重複構建和version-lock
gradle的可重複構建文檔
gradle的version-lock文檔,使用gradle.lockfile對範圍依賴進行鎖定。
在build.gradle裏配置
dependencyLocking {
lockAllConfigurations()
}
gradle.lockfile樣例
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
org.springframework:spring-beans:5.0.5.RELEASE=compileClasspath, runtimeClasspath
org.springframework:spring-core:5.0.5.RELEASE=compileClasspath, runtimeClasspath
org.springframework:spring-jcl:5.0.5.RELEASE=compileClasspath, runtimeClasspath
empty=annotationProcessor
buildscript本身的lockfile則是buildscript-gradle.lockfile
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.6.0=classpath
info.solidsoft.pitest:info.solidsoft.pitest.gradle.plugin:1.6.0=classpath
org.sonarqube:org.sonarqube.gradle.plugin:3.3=classpath
org.sonarsource.scanner.api:sonar-scanner-api:2.16.1.361=classpath
org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3=classpath
empty=
對應的build.gradle裏配置
buildscript {
repositories {
mavenCentral()
}
dependencyLocking {
lockAllConfigurations()
}
}
使用如下命令生成/更新鎖文件
(gradle or ./gradlew) dependencies --write-locks
(2)maven的可重複構建和version-lock
1)maven社區的進展
maven的可重複構建參考https://maven.apache.org/guides/mini/guide-reproducible-builds.html
其中強調
Notice: Reproducible Builds for Maven:
Require no version ranges in dependencies,
Generally give different results on Windows and Unix because of different newlines. (carriage return linefeed on Windows, linefeed on Unixes)
Generally depend on the major version of the JDK used to compile. (Even with source/target defined, each major JDK version changes the generated bytecode)
maven社區關於可重複構建的issue參考https://issues.apache.org/jira/browse/MNG-6276
其中,關於version-lock,社區認爲不在討論範圍內,將其放在了out of scope,也即是上面強調的no version ranges
2)snapshot與範圍依賴
如果當前工程的pom文件裏存在使用snapshot版本和使用範圍依賴[1.0,)的場景。
可以使用versions插件來進行批量修改,參考https://www.mojohaus.org/versions-maven-plugin/
pom文件裏添加如下插件
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.8.1</version>
</plugin>
使用命令:
mvn versions:lock-snapshots
會自動搜索當前工程pom(含子pom)裏的所有類似1.3-snapshot的版本號,把snapshot替換爲時間戳,變爲1.3-20210320.172306-4
使用命令
mvn versions:resolve-ranges
會自動搜索當前工程pom(含子pom)裏的所有類似[0.11,)的版本號,替換爲固定的版本號,變爲當前使用的版本例如0.31
執行命令後,當前工程的pom會自動變更爲固定版本號的pom,同時,與pom平級會生成一個pom.xml.versionsBackup文件保存之前的pom
上述方法的缺陷是,如果間接依賴的包存在範圍依賴或者snapshot,則不會處理爲固定的版本號。即A->B->C->D的情況下,如果B/C中配置了snapshot或者範圍依賴,在A使用version插件並不能處理下層的snapshot和範圍依賴。
要解決這個問題,可以考慮effective pom展開完整的依賴來確認有沒有下層的snapshot和範圍依賴。
maven的內部機制是通過pom展開所有的傳遞依賴,生成effective pom-->通過effective pom去maven中心倉下載-->執行編譯。因此,如果傳遞依賴中存在範圍依賴的情況,可以通過effective pom查看到
獲取effective pom的方法如下:
配置maven-help-plugin插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-help-plugin</artifactId>
<version>3.2.0</version>
</plugin>
執行命令
mvn help:effective-pom -Doutput=effective-pom.xml
展開的effctive-pom是包含了繼承自父pom的所有依賴和插件以及展開了傳遞依賴平鋪出來的所有依賴和插件。如果存在範圍依賴和snapshot,則很容易就能搜索到。也可以再用version插件處理effctive-pom,對pom裏的版本進行固化。
使用effctive-pom執行mvn package,最終打出來的jar包與原始的jar包是一致的。
3)其他version-lock插件
在github上也能搜索到其他的dependency-lock插件,本質上都是把實際用到的依賴信息(含固定的version號)記錄到對應的文本里,方便下次使用的時候check。沒有直接用來執行重複構建的能力。
https://github.com/vandmo/dependency-lock-maven-plugin該插件把依賴信息記錄到json文件,用來防止maven項目的依賴版本被意外變更。能用來檢查。
https://github.com/mpobjects/dependency-lock-plugin該插件把依賴信息記錄到dependencyManagement。
2.NodeJS的可重複構建和version-lock
nodejs的親兒子npm在可重複構建上幾乎可以認爲是抄了yarn的作業,yarn在早期就實現了離線鏡像和yarn.lock,使用yarn cli時會自動更新鎖文件,yarn install --frozen-lock-file
時則使用鎖文件構建。npm隨後才實現了一個shrinkwrap的特性以及--offline與--prefer-offline。
當前,npm使用package-lock.json來記錄完整的包依賴信息(包含了依賴包的實際版本、傳遞依賴和包的校驗和),在使用npm install
時更新package-lock.json而在使用npm ci
命令時使用package-lock.json進行構建。需要注意的是npm在打包發佈時並沒有打包package-lock.json,如果希望跟隨包發佈lock信息,需要使用npm-shrinkwrap.json。
3.Golang的可重複構建和version-lock
(1)可重複構建
golang在2016年就開始支持可重複構建issue #16860,在Change #173344支持-trimpath模式,並可以通過-ldflags= -buildid= (設置flag和buildid爲空字符串)來確保構建的二進制是可重複的。可以參考k8s、statusgo等支持可重複構建的實踐
(2)依賴管理與version-lock
Golang的包管理介紹和歷史參考包管理工具。在1.11支持go modules之前,可以通過dep來管理依賴,之後則推薦使用modules包管理模式。
在1.11版本之後,golang使用go.mod和go.sum來管理依賴,默認從https://proxy.golang.org下載mod。 它可以使用https://sum.golang.org 上的校驗和數據庫對模塊進行身份驗證。
go.sum的格式爲
<module> <version> <hash>
<module> <version>/go.mod <hash>
其中module是依賴的路徑,version是依賴的版本號。hash是以h1:
開頭的字符串,表示生成checksum的算法是第一版的hash算法(sha256)。
有些項目實際上並沒有 go.mod
這個文件,所以 Go 文檔裏提到這個 /go.mod
的 checksum,用了 "possibly synthesized" (也許是合成的)的說法。估計對於沒有 go.mod
的項目,Go 會嘗試生成一個可能的 go.mod
,並取它的 checksum。
如果只有對於 go.mod
的 checksum,那麼可能是因爲對應的依賴沒有單獨下載。比如用 vendor 管理起來的依賴,便只有 go.mod
的 checksum。
由於 go 的依賴管理揹負着沉重的歷史包袱,其 version 的規則較爲複雜。version的格式也比較多樣
如果項目沒有打 tag,會生成一個版本號,格式如下: v0.0.0-commit日期-commitID
比如 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
引用一個項目的特定分支,比如 develop branch,也會生成類似的版本號: v當前版本+1-commit日期-commitID
比如 github.com/DATA-DOG/go-sqlmock v1.3.4-0.20191205000432-012d92843b00 h1:Cnt/xQ9MO4BiAjZrVpl0BiqqtTJjXUkWhIqwuOCVtWo=
如果項目有用到 go module,那麼就是正常地用 tag 來作爲版本號。
比如 github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
。
如果項目打了 tag,但是沒有用到 go module,爲了跟用了 go module 的項目相區別,需要加個 +incompatible
的標誌。
比如 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
go.sum文件的本意是由於golang最初沒有類似maven、npm這樣的中心倉,而是分佈式管理的,引用的是各個git倉(既有github上的,也有不在這上面的)上的代碼。這樣缺乏一個可信賴的中心來校驗每個包的一致性。發佈者在 GitHub 上給自己的項目打上 0.1 的 tag 之後,依舊可以刪掉這個 tag ,提交不同的內容後再重新打個 0.1 的 tag。哪怕發佈者都是老實人,發佈平臺也可能作惡。所以只能在每個項目裏存儲自己依賴到的所有組件的 checksum,才能保證每個依賴不會被篡改。如果下次build時,這個tag被刪掉重新在另一個commit上打了。算出來的hash不同,build就會報錯。
要注意的是,go.sum並不是鎖文件。go.mod中的信息已經爲可重複的構建提供了足夠的version信息。go.sum記錄的是構建中所有直接和間接依賴項的校驗和。(因此go.sum列出的模塊經常會多於go.mod)
從當前最新的1.16版本開始,golang將modules模式改爲默認模式。同時,go.mod/go.sum默認爲只讀的,如果代碼中修改了依賴信息(例如依賴了更多的包),類似go build這樣的命令不會像之前的版本那樣直接修改go.mod/go.sum,而是報出一個error提醒。同時,go mod tidy和go get命令仍然能夠正常修改go.mod/go.sum。
4.Python的可重複構建和version-lock
python的可重複構建進展見issue29708,在PEP552解決了時間戳問題,建議使用python3.7+版本。
python用setup.py定義依賴,通過pip freeze > requirements.txt
生成固定版本的依賴包列表。需要注意的是pip freeze的是當前環境上的包,將當前環境上所有的pipy包(無論直接依賴還是間接依賴),都平鋪到requirements中,建議結合virtual environment使用。重複構建時,使用pip install -r requirements.txt
來獲取requirement.txt上定義的包。pip支持hash校驗。
也可以使用pip-compile、pipenv等第三方工具來生成對應的lock文件,這樣生成的文件既包含了間接依賴關係,也包含了hash值。pip-compile或者pipenv生成的lock文件中,同一個依賴件通常包含多個hash值,這是用於不同環境上的二進制的hash。在pip install --require-hashes -r requirements.txt
校驗時,只需匹配上任一hash即可。