前面的文章,初步介绍了Jenkins的FreeStyle的构建方式,本篇来介绍Pipeline(声明式流水线)以及如何在Docker环境中运行构建
1 Docker环境的准备
Docker的使用,可以参考之前的文章:《如何在Ubuntu安装Docker》
为了演示的内容更加丰富,这次的测试项目稍微复杂一些,测试的项目是在:https://gitee.com/xxpcb/linux-cpp-useful-demo,这是早前的一个代码仓库,这次在这个仓库基础上,新建一个gtest分支,增加单元测试功能(使用Goole test工具,目前这个gtest代码是让AI生成的,本篇暂不展示介绍gtest代码,后面有机会再详细介绍)。
1.1 验证docker中的编译环境
我先在Ubuntu中运行这个单元测试,确保代码没问题
然后在我之前创建的docker中再运行试一下:
这里报错了,应该是我这个docker中没有gtest相关的库,使用如下指令安装:
apt-get update
apt-get install -y libgtest-dev python3 pkg-config
然后就正常运行了:
1.2 将环境ok的docker打包成镜像
1.2.1 打包命令
将当前的docker打包成一个新的镜像,指令是:
sudo docker commit build-arm64-env-v2 build-ci:v1
这样刚才的编译环境就会被打包进新的镜像,后面配置Jenkins时,它是要使用镜像来临时创建一个新的docker来运行代码编译,而不是使用我自己创建的这个docker
1.2.2 验证镜像是否自带gtest相关环境
验证新的镜像创建容器是否会自带gtest,Jenkins 每次起的正是这种全新容器
sudo docker run --rm build-ci:v1 bash -c '
test -d /usr/include/gtest && echo "gtest headers OK" || echo "MISSING"
ls /usr/lib/x86_64-linux-gnu/libgtest*.a
for c in cmake g++ make git; do command -v $c || echo "MISSING $c"; done
'
这条命令的整体功能是:不带目录挂载、一次性、纯只读的镜像自检,用全新的临时容器确认 build-ci:v1 里 gtest 头文件、静态库和四个工具都在
参数含义:
sudo:以 root 调用 docker CLI,因为普通用户不在 docker 组、读不到
/var/run/docker.sockdocker run:从镜像新建并启动一个容器,注意和 docker exec(进已运行容器)是两回事
--rm:容器进程退出就自动删除容器(只删容器可写层,不删镜像)
build-ci:v1:镜像名:tag;没写 registry 前缀表示本地镜像,v1 是刚才 docker commit 固化的那个版本
bash -c '...':覆盖镜像 CMD,把引号里整段字符串交给容器内 bash 解释执行
引号里的三条检查:
test -d /usr/include/gtest && echo "gtest headers OK" || echo "MISSING"
-d 判断目录存在,&& 成功打印 OK
ls /usr/lib/x86_64-linux-gnu/libgtest*.a
列出静态库,期望是有 libgtest.a 和 libgtest_main.a;* 由容器内 bash 做 glob
for c in cmake g++ make git; do command -v $c || echo "MISSING $c"; done
循环四个工具,command -v 是 shell 内置、找到就打印绝对路径,找不到返回非 0 触发 MISSING
1.2.3 验证是否能跑gtest 程序
再测试下编一个 gtest 程序是否能跑起来
sudo docker run --rm build-ci:v1 bash -c '
printf "#include <gtest/gtest.h>nTEST(Demo, Basic) { EXPECT_EQ(1+1, 2); }n" > /tmp/t.cpp
g++ /tmp/t.cpp -lgtest -lgtest_main -lpthread -o /tmp/t && /tmp/t
'
前半部分和上一条一样,这里只分析引号里的两条:
printf "#include <gtest/gtest.h>nTEST(Demo, Basic) { EXPECT_EQ(1+1, 2); }n" > /tmp/t.cpp
printf "...":按格式串输出文本
"...n...n":双引号里的 n 是换行转义
> /tmp/t.cpp:重定向,把 stdout 写进容器内 /tmp/t.cpp(覆盖写)生成出来就是这两行 C++
#include <gtest/gtest.h>
TEST(Demo, Basic) { EXPECT_EQ(1+1, 2); }
再看第2条:
g++ /tmp/t.cpp -lgtest -lgtest_main -lpthread -o /tmp/t && /tmp/t
g++ /tmp/t.cpp:编译+链接一步到位(没写 -c),输入是刚生成的源码
-lgtest:链接 gtest 库。ld 的 -lxxx 会去找 libxxx.so / libxxx.a,这里命中上一条探测到的 /usr/lib/x86_64-linux-gnu/libgtest.a 静态库
-lgtest_main:链接 gtest 自带的 main(内部调用 RUN_ALL_TESTS()),所以源码不用写 main
-lpthread:链接 POSIX 线程库,gtest 内部用到线程/互斥
-o /tmp/t:指定输出可执行文件名;不写默认是
a.out&& /tmp/t:shell 短路:编译成功(退出码 0)才运行生成的程序,编译失败就不会误跑旧二进制
验证的结果如下:
[ PASSED ] 1 test.说明这一层到此封好了。build-ci:v1已经是个自洽的编译环境:gtest 头文件、两个静态库、cmake/g++/make/git 全在,与 Jenkins 每次起临时容器的场景完全一致
1.2.4 验证用 jenkins 身份模拟一次挂载回写
然后再验证一下,用 jenkins 身份模拟一次挂载回写
sudo mkdir -p /tmp/ws-test && sudo chown jenkins:jenkins /tmp/ws-test
sudo -u jenkins docker run --rm
--user "$(id -u jenkins):$(id -g jenkins)"
-v /tmp/ws-test:/work -w /work build-ci:v1
bash -c 'mkdir -p build && echo ok > build/result.txt && id'
ls -ln /tmp/ws-test/build/result.txt
第一句的含义是:创建/tmp/ws-test目录,并把归属权交给jenkins:jenkins
再看第二句:
sudo -u jenkins:以 jenkins 服务账号(不是 root)执行,模拟 Jenkins 构建时的真实身份
docker run --rm:新建一次性容器,退出即删--user(等价 -u)指定容器内进程的 uid:gid两个 $(...) 是命令替换,由宿主机当前 shell(登录的 xxpcb)执行,去查 jenkins 这个用户的数字 uid/gid,实测
130:137-v 宿主机路径:容器路径:bind mount,默认读写(没写 :ro),模拟 Jenkins 的 $WORKSPACE 挂载
-w /work:设置容器内工作目录(覆盖镜像 WORKDIR)
build-ci:v1:本地构建镜像
bash -c '...':覆盖 CMD,整段交给容器内 bash
mkdir -p build:在 /work 下建目录,-p 表示已存在不报错、父目录可一并创建。这是测试 uid 130 对挂载目录有没有写权限
echo ok > build/result.txt:写一个内容为 ok 的文件,制造可检查的产物id:打印容器内当前 uid/gid/groups,用来核对 --user 是否真的生效
第三句的含义就是查看刚才创建的result.txt是否存在
上面报错意思是 jenkins 用户还不在 docker 组,使用如下指令后,可以看到就正常了。
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
第一句:把 jenkins 追加进 docker 组(改的是磁盘上的 /etc/group,-a 保证是追加不会清空原组)
sudo:以 root 执行;修改 /etc/group、/etc/gshadow 属于特权操作
usermod:修改已有用户账号的属性(如果是要新建用户是 useradd)。
jenkins 是 apt 装 jenkins 包时自动创建的系统账号
-a:--append,追加模式,必须和 -G 搭配-G docker
:--groups docker,设置附加组列表;docker 是目标组名
jenkins:被修改的用户名(末尾的位置参数)
第二句:重启 Jenkins 让新进程重新读取组
systemctl:systemd 的主控制命令,用来管理服务单元
restart: 先 stop 再 start,进程会被整个换掉
jenkins:unit 名,systemd 会自动补 .service,等价 jenkins.service
2 Jenkins配置
仍然通过下面地址进入Jenkins
http://localhost:8080
这次使用Pipeline的方式配置项目,另外会增加参数构建的功能,可选git分支和是否进行单元测试。
先安装要用到的插件,右上角齿轮,Plugins ,确认有 Pipeline、Pipeline: Stage View、Git Parameter。
然后新建任务,名字 linux-cpp-useful-demo-ci, 类型选 Pipeline。
下一步是加参数:勾选 This project is parameterized
Choice Parameter:Name BRANCH,选项每行一个 feature/gtest、master
Boolean Parameter:Name RUN_GTEST,默认值勾上,描述写“是否执行 GTest 单元测试”
页面底部 Pipeline → Definition 选 Pipeline script(内联,不是 from SCM),写入这段 Declarative Pipeline(声明式流水线)脚本:
pipeline {
agent any
stages {
stage('检出代码') {
steps {
checkout([$class: 'GitSCM',
branches: [[name: "refs/heads/${params.BRANCH}"]],
userRemoteConfigs: [[url: 'https://gitee.com/xxpcb/linux-cpp-useful-demo.git']]])
}
}
stage('容器内编译与单测') {
when { expression { return params.RUN_GTEST } }
steps {
sh '''
set -e
docker run --rm
--user "$(id -u):$(id -g)"
-v "$WORKSPACE:/work" -w /work
build-ci:v1
bash /work/GTest/run_all_tests.sh
'''
}
}
stage('归档报告') {
steps {
archiveArtifacts artifacts: 'GTest/coverage/*.html, GTest/coverage/coverage_summary.txt, GTest/build/*/results.xml',
allowEmptyArchive: true
}
}
}
}
说明:
pipeline { }:Declarative Pipeline 的根块,里面只能放 agent / stages/ post / options / parameters等规定段落
agent any:在任意可用 executor 上执行,Jenkins 自动分配工作区
/var/lib/jenkins/workspace/<任务名>stages { }:阶段容器,内部 stage 按顺序串行,前一个失败后面默认不跑
stage 1「检出代码」 — checkout([$class: 'GitSCM', ...]) 是 Git 插件的检出步骤
branches: [[name: "refs/heads/${params.BRANCH}"]]:List of Map 指定要检出的 ref
refs/heads/ 完整路径对 feature/gtest 这种带斜杠分支最稳。
${params.BRANCH} 是 Groovy 插值(双引号才插值),读网页参数。
userRemoteConfigs: [[url: '...']]:远端仓库列表;公开仓库所以没有 credentialsId,私有仓库要加。
stage 2「容器内编译与单测」
when { expression { return params.RUN_GTEST } }:条件执行
RUN_GTEST 是 Boolean Parameter,返回 false 时整个 stage 跳过
steps { sh '''...''' }:在 Agent 上执行 shell里面的 shell 逐行:
set -e:任一命令失败立即退出并返回非 0 , Jenkins 判该 stage 失败
docker run --rm:一次性临时容器,跑完即删
--user "$(id -u):$(id -g)":容器内进程 uid/gid = 当前 sh 执行者(Jenkins 服务账号)
-v "$WORKSPACE:/work":把 Jenkins 工作区挂进容器 /work;
$WORKSPACE 是 Jenkins 注入的环境变量
-w /work:容器内工作目录,脚本里的相对路径才落对地方
build-ci:v1:构建镜像
bash /work/GTest/run_all_tests.sh:执行仓库自带脚本
stage 3「归档报告」 — archiveArtifacts 把工作区里匹配的文件复制到/var/lib/jenkins/jobs/<job>/builds/<n>/archive/,网页上就出现可下载链接
逗号分隔的三个 glob 对应 run_all_tests.sh 的产物allowEmptyArchive: true 表示一个文件都没匹配到也不判失败
这样就不需要在项目代码中写 Jenkinsfile
3 Jenkins构建
3.1 执行构建
点左边的Build with Parameters,这里就可以选择git分支,以及是否运行gtest
运行效果如下,这次有运行的每个阶段的运行时间:拉取代码,容器内编译与单测,归档报告
3.2 查看构建的的一些log信息
鼠标放到“容器内编译与单测”的22s处,会显示一个log图标,点击图标,就会显示具体的log内容,如下:
查看“Console Output”,会有完整的Jenkins的构建log
向下看,可以看到有临时启动docker的指令
刚才演示了勾选GTEST的构建,如果没勾选,构建就会跳过该过程,如下图:
另外注意,上面的xml就是各个文件代码的单元测试覆盖率报告
4 总结
本篇介绍了Jenkins使用Pipeline的方式构建项目,以及如何在Docker环境中运行构建,通过gitee上的一个代码仓库,演示带参数构建(选分支,选构建模块)的过程。
20