可重複構建與Version lock

I.可重複構建

可重複構建即構建是可以重現的,如果給定相同的源代碼、構建環境和構建指令,任何人都可以重新構建一個BEP一致的相同副本。(想要詳細瞭解的可參考官網或者維基,以及DebianYactomartinfowler)

這個背後的邏輯是,通過可復現的構建過程來驗證在此編譯過程中沒有人爲(故意、脅迫或其他影響導致)的引入漏洞和後門。這樣也很容易與任何一個第三方驗證機構達成共識,對可重複的過程中凸顯的偏差進行識別與審查。

達成可重複構建,需要滿足的條件包括:
  • 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

社區的討論郵件列表參考https://lists.apache.org/thread.html/82000b2cb44263685e19dd1b202b7384479b62028a26e3904ab1b409%40%3Cdev.maven.apache.org%3E

其中,關於version-lock,社區認爲不在討論範圍內,將其放在了out of scope,也即是上面強調的no version ranges

https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=74682318#Reproducible/VerifiableBuilds-Outofscope

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。沒有直接用來執行重複構建的能力。

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即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章