可重复构建与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

001.png
002.png

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,755评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,305评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,138评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,791评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,794评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,631评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,362评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,264评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,724评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,900评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,040评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,742评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,364评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,944评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,060评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,247评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,979评论 2 355

推荐阅读更多精彩内容

  • Go 1.11 Modules翻译自 Go 官方wiki # Go 1.11 Modules 根据[提议](htt...
    drawing818阅读 1,438评论 0 0
  • 一、背景 Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和...
    JFrog阅读 406评论 0 0
  • 最近由于换工作,开始交接工作。整理以前的工作内容,由于组内就我一个在做go和大数据。 所以开发没有规划,当时是怎么...
    若与阅读 241,455评论 10 110
  • Go Module是Go会在1.12中正式推出的包管理机制。 Go mod 简介 Golang一直存在一个被人诟病...
    会飞的鲶鱼阅读 47,869评论 1 14
  • golang初探之环境搭建 众所周知学习一门编程语言最重要的是入门。而入门第一步就是开发环境的搭建。下面就来简...
    青云刀歌阅读 784评论 2 1