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
社区的讨论邮件列表参考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文件里添加如下插件
org.codehaus.mojo
versions-maven-plugin
2.8.1
使用命令:
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插件
org.apache.maven.plugins
maven-help-plugin
3.2.0
执行命令
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等支持可重复构建的实践
https://github.com/kubernetes/kubernetes/issues/70131
https://github.com/status-im/status-go/issues/1185
https://blog.filippo.io/reproducing-go-binaries-byte-by-byte/
(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的格式为
/go.mod
其中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即可。