可重复构建与Version lock

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

001.png
002.png

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

你可能感兴趣的:(可重复构建与Version lock)