利用 diy 主机搭建本地嵌入式 CI 系统(超详细)
本文介绍如何利用闲置设备搭建本地嵌入式CI系统,解决云CI在交叉编译、硬件调试和安全性方面的不足。基于Jenkins实现多架构构建、自动化工具链管理与高效流水线,兼顾成本、性能与安全。
用一台旧电脑,把嵌入式 CI 玩出花来 🛠️
你有没有经历过这样的场景:刚提交完一段代码,满心期待地等着 CI 跑完,结果等了十分钟才发现——编译失败是因为某个头文件路径写错了?更离谱的是,这个错误在本地明明能过。再一看日志,发现是云上 CI 使用的交叉编译器版本和你的不一致。
如果你做的是嵌入式开发,尤其是涉及 ARM Cortex-M、RISC-V 或者定制化 SoC 的项目,那你大概率已经对 GitHub Actions 那些“通用镜像”感到无力了。它们确实方便,但当你需要一个特定版本的 arm-none-eabi-gcc ,或者想在一个真实的树莓派上跑单元测试时,你就只能干瞪眼。
这时候你会想: 要是有个完全由我掌控的 CI 系统该多好?
好消息是——不需要买服务器、也不用付一分钱订阅费。只要有一台闲置的旧笔记本、一台吃灰的工控机,甚至是一块树莓派,你就能搭建一套真正属于自己的 本地嵌入式 CI 系统 。
别误会,这不是什么“极客炫技”。我们团队从去年开始就这么干了,现在每天自动构建超过 50 次固件,从 STM32 到 ESP32 再到 Jetson Nano 上的 AI 推理模型,全都跑在同一套流水线上。最关键的是: 整个系统零月租,数据不出内网,还能按需扩展。
今天我就带你一步步实现它,不讲虚的,只说实战中踩过的坑和验证有效的方案。
为什么商用 CI 在嵌入式面前“翻车”?
先泼一盆冷水:GitHub Actions 很强大,但它本质上是为 Web 和通用应用设计的。而嵌入式开发有几个“致命差异”,让它很难直接套用:
-
目标架构五花八门
你想编译一个运行在 RISC-V MCU 上的裸机程序?抱歉,大多数 SaaS CI 平台根本不支持这种架构的 runner。 -
工具链依赖复杂且敏感
嵌入式项目往往依赖特定版本的 GCC(比如必须用 9.2.1 版本才能兼容某款芯片),而云端环境通常只提供最新版或几个固定选项。 -
硬件级调试与烧录需求
我们有些项目需要在构建后自动通过 J-Link 烧录到开发板并运行自检脚本。这要求 CI Agent 能访问 USB 设备——但在云环境中这是不可能开放的权限。 -
网络延迟拖慢迭代速度
一次 Yocto 构建动辄几个 GB 的源码同步。如果每次都要从公网拉取,光下载就得七八分钟,根本谈不上“快速反馈”。 -
安全红线碰不得
客户项目的 bootloader 代码能上传到第三方平台吗?哪怕说是“私有仓库”,心理这关也过不去。
所以,当你的项目开始涉及真实硬件、定制工具链或多架构共存时, 把 CI 收回本地,几乎是必然选择。
Jenkins:那个被低估的“老将”
提到自建 CI,很多人第一反应是 GitLab CI 或 Drone,但我坚持推荐 Jenkins ——没错,就是那个看起来有点“复古”的蓝色图标系统。
别急着关页面。听我说完三个理由,你可能会重新考虑。
1. 它真的特别稳
我们这套系统从 2023 年 3 月上线至今,累计执行了 8,762 次构建任务,最长连续运行时间超过 120 天。期间唯一一次中断,是因为办公室停电……而不是 Jenkins 自身崩溃。
它的主从架构经过多年打磨,在资源调度、故障隔离方面表现非常成熟。相比之下,一些轻量级 CI 工具在并发高负载下容易出现任务卡死或内存溢出。
2. 插件生态没人能比
你知道 Jenkins 有多少个官方插件吗? 1,800+ 。随便举几个我们在嵌入式场景用到的:
Git Parameter:允许手动触发时选择分支/标签Build Timeout:防止某个 Makefile 卡住导致节点锁死Email Extension:构建失败立刻发邮件 + 控制台片段SSH Slaves:通过 SSH 连接树莓派作为 agentWarnings Next Generation:解析 cppcheck 输出并生成趋势图
这些都不是“锦上添花”,而是实打实提升可用性的关键组件。
3. Pipeline 脚本足够灵活
Jenkins Pipeline(尤其是声明式语法)虽然不像 YAML 那么简洁,但它基于 Groovy,意味着你可以写逻辑判断、异常处理、动态参数注入。
比如下面这段,会根据提交的分支名决定是否启用优化等级:
pipeline {
agent any
stages {
stage('Configure Optimization') {
steps {
script {
if (env.BRANCH_NAME == 'release') {
env.OPT_FLAG = '-O3'
} else {
env.OPT_FLAG = '-O0 -g'
}
echo "Using optimization: ${env.OPT_FLAG}"
}
}
}
stage('Build') {
steps {
sh "make CC=arm-none-eabi-gcc CFLAGS=${env.OPT_FLAG}"
}
}
}
}
这种灵活性,在纯配置文件驱动的 CI 中很难实现。
DIY 构建主机:不只是“废物利用”
很多人以为“DIY 主机”就是找个旧电脑装个 Linux 就完事了。其实不然。选对硬件,能让构建效率提升不止一倍。
我们的三类 Agent 节点
目前我们维护着三种类型的构建节点,分别承担不同角色:
| 类型 | 设备示例 | 规格 | 承担任务 |
|---|---|---|---|
| x86_64 高性能节点 | NUC11PAHi7 | i7-1165G7 / 16GB DDR4 / 512GB NVMe | 编译 Host 工具、文档生成、Docker 镜像打包 |
| ARM64 通用节点 | Raspberry Pi 4B (8GB) | BCM2711 / 8GB RAM / 千兆网口 | 裸机固件编译、静态分析 |
| AIoT 加速节点 | NVIDIA Jetson Orin Nano | 128-core GPU / 8GB LPDDR5 | AI 模型量化、推理测试、OTA 包生成 |
树莓派也能当 CI Agent?真的可以!
起初我也怀疑:一个售价不到 500 块的开发板,真能胜任持续集成的任务?
实测结果令人惊喜:用 Pi 4B 编译一个中等规模的 FreeRTOS 工程(约 300 个源文件),耗时仅比我的 MacBook Pro 多 18%。关键是它功耗低(满载不到 6W)、静音、体积小,适合 7×24 小时运行。
而且有个隐藏优势: 它本身就是目标架构设备 。我们可以直接在上面运行生成的二进制文件进行冒烟测试,而不是依赖 QEMU 模拟。
✅ 实践建议:给树莓派加装 M.2 HAT 扩展 NVMe 固态盘,I/O 性能提升显著。我们用三星 980 Pro 后,Yocto 构建时间下降了近 40%。
如何让 Jenkins 接管这些设备?
最简单的方式是使用 SSH 连接模式 。步骤如下:
-
在目标主机安装 OpenJDK:
bash sudo apt update && sudo apt install openjdk-11-jre-headless -y -
创建专用用户
jenkins-agent:bash sudo adduser --disabled-password --gecos '' jenkins-agent -
配置免密登录(Master 公钥放入 Agent 的
~/.ssh/authorized_keys) -
在 Jenkins Web 界面添加新节点:
- 名称:rpi4-arm-builder
- 标签:arm aarch64 low-power
- 启动方式:通过 SSH
- 主机:192.168.1.105
- 凭据:选择已配置的 SSH 密钥
保存后,Jenkins 会自动连接并在后台启动 agent 进程。状态变为“在线”即可用于构建。
⚠️ 注意事项:
- 确保/tmp分区足够大(至少 4GB),否则大型编译可能因空间不足失败。
- 如果设备位于 NAT 后(如家庭宽带),可改用 JNLP 模式,让 agent 主动连接 master。
交叉编译工具链:别再手动折腾了
这是我见过最多人“重复发明轮子”的地方。
有人每次重装系统就去 ARM 官网找链接复制粘贴;有人把整个 gcc-arm-none-eabi 文件夹拷进 U 盘传给同事;还有人干脆把工具链 commit 进 Git 仓库…… 😵
正确的做法应该是: 自动化安装 + 统一管理 。
方案一:预编译工具链 + 脚本部署(推荐新手)
ARM 官方提供了 GNU-RM 工具链的完整发布包,适合快速上手。
以下是我们使用的安装脚本(保存为 install-arm-gcc.sh ):
#!/usr/bin/env bash
set -e # 出错立即退出
VERSION="10.3-2021.10"
TARGET_DIR="/opt/gcc-arm-none-eabi-${VERSION}"
ARCHIVE="gcc-arm-none-eabi-${VERSION}-x86_64-linux.tar.bz2"
URL="https://developer.arm.com/-/media/Files/downloads/gnu-rm/${VERSION}/${ARCHIVE}"
echo "👉 正在安装 ARM Cortex-M 工具链 (${VERSION})..."
# 检查是否已存在
if [ -d "$TARGET_DIR" ]; then
echo "✅ 已检测到现有安装:${TARGET_DIR}"
exit 0
fi
# 下载并解压
sudo mkdir -p /opt
wget -q --show-progress "$URL" -O "/tmp/${ARCHIVE}"
sudo tar -xjf "/tmp/${ARCHIVE}" -C /opt/
rm "/tmp/${ARCHIVE}"
# 创建软链接便于引用
sudo ln -sf "${TARGET_DIR}" /opt/gcc-arm-none-eabi
# 添加环境变量(适用于 Jenkins Job)
echo "" >> ~/.profile
echo "# ARM Toolchain" >> ~/.profile
echo "export PATH=\"/opt/gcc-arm-none-eabi/bin:\$PATH\"" >> ~/.profile
echo "🎉 安装完成!请重启 shell 或执行 source ~/.profile"
把这个脚本放在内部 Git 仓库,任何新节点只需一条命令搞定:
curl -s http://internal.tools/install-arm-gcc.sh | sudo bash
方案二:用 crosstool-ng 构建专属工具链(进阶)
如果你的需求更复杂——比如要支持老旧芯片、启用特殊补丁、或集成 custom newlib——那就得自己构建工具链。
crosstool-ng 是目前最成熟的开源工具链构建框架。
简单流程如下:
git clone https://github.com/crosstool-ng/crosstool-ng
cd crosstool-ng
./configure --enable-local
make -j$(nproc)
# 配置目标架构
./ct-ng arm-cortex_m4-linux-gnueabihf
./ct-ng menuconfig # 可修改 GCC 版本、调试信息等
# 开始构建(耗时较长)
./ct-ng build
构建完成后,会在 ~/x-tools/ 下生成完整的工具链目录,可打包分发。
💡 小技巧:把构建过程放进 Docker 容器,避免污染宿主机环境。我们有现成的镜像
ourteam/ctng-builder:latest,随时 pull 就能用。
让 Pipeline 真正“懂”嵌入式
Jenkinsfile 写法千千万,但针对嵌入式项目,有几个最佳实践值得强调。
1. 使用标签精准匹配构建节点
不要让 ARM 固件跑到 x86 节点上去编译!通过 label 明确指定:
pipeline {
agent { label 'arm cortex-m' }
environment {
CROSS_COMPILE = 'arm-none-eabi-'
DEVICE_FAMILY = 'STM32F4'
}
stages {
// ...
}
}
然后在每个 Agent 设置对应的标签,比如树莓派设为 arm aarch64 cortex-m ,x86 节点设为 x86_64 host-tool 。
这样就能做到“哪里合适去哪里”。
2. 构建失败时自动通知责任人
光看 UI 不够快。我们配置了邮件和企业微信机器人双通道提醒:
post {
failure {
emailext (
subject: "🚨 构建失败:${env.JOB_NAME} [${env.BUILD_NUMBER}]",
body: """<p>构建失败,请尽快修复:</p>
<p>项目:<b>${env.JOB_NAME}</b></p>
<p>编号:<a href="${env.BUILD_URL}">${env.BUILD_NUMBER}</a></p>
<p>提交者:${getCommitAuthor()}</p>
<p>变更日志:<pre>${getChangeLog()}</pre></p>""",
recipientProviders: [developers(), requestor()]
)
// 发送到企业微信群
sh '''
curl -s -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" \
-H 'Content-Type: application/json' \
-d "{\"msgtype\": \"text\", \"text\": {\"content\": \"🔧 构建失败\\n项目: $JOB_NAME\\n编号: $BUILD_NUMBER\\n链接: $BUILD_URL\"}}"
'''
}
}
配合 Git Plugin 获取作者信息,问题能在 5 分钟内触达对应开发者。
3. 归档中间产物,方便排查问题
除了最终的 .bin 文件,我们也归档 .elf 、 .map 和编译日志:
stage('Archive Artifacts') {
steps {
archiveArtifacts allowEmptyArchive: true, artifacts: '''
build/*.bin,
build/*.elf,
build/*.map,
logs/build.log
'''
}
}
有一次,一个同事改了链接脚本导致堆栈溢出。我们直接下载 .map 文件查看内存分布,两分钟定位问题。如果没有归档,就得重新跑一遍构建——那可是 12 分钟。
网络与存储设计:别让 I/O 成瓶颈
很多人忽略了基础设施的重要性,结果“CI 跑得慢”全怪在 Jenkins 头上。
实际上,真正的瓶颈往往出现在这几个地方:
1. 存储一定要用 SSD
我们做过对比测试:同一个 Yocto 项目,在机械硬盘 vs NVMe SSD 上的构建时间分别是:
| 存储类型 | 构建时间 | IO Wait |
|---|---|---|
| HDD (7200rpm) | 23 min 41 s | ~12% |
| SATA SSD | 15 min 29 s | ~5% |
| NVMe SSD | 13 min 07 s | ~2% |
差距接近 45% !尤其在频繁读写临时文件( .o , .d , tmp ) 时,SSD 的优势非常明显。
✅ 建议:所有构建节点的根分区或
/home/jenkins-agent必须挂载在 SSD 上。
2. 局域网千兆起步,最好万兆
虽然单次代码同步不大,但如果你用了 submodule 或需要下载 vendor SDK,几百 MB 的传输很常见。
更重要的是: 构建产物归档、缓存共享、日志收集 都需要高速网络支撑。
我们的做法是:
- 所有设备接入华为 S5735S 交换机(支持 VLAN 和 QoS)
- Master 和 NFS 服务器之间启用 LACP 链路聚合
- 关键节点使用静态 IP(DHCP 保留),避免地址漂移
3. 使用 NFS 统一归档固件版本
我们搭建了一个简单的 NFS 服务,用于长期保存所有成功构建的固件:
# /etc/exports
/nfs/firmware *(rw,sync,no_subtree_check,no_root_squash)
在 Jenkins Pipeline 中自动上传:
sh '''
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
DEST_DIR=/nfs/firmware/${env.JOB_NAME}/${TIMESTAMP}
mkdir -p ${DEST_DIR}
cp build/*.bin ${DEST_DIR}/
echo "Firmware archived to: ${DEST_DIR}"
'''
现在任何人想回滚到三个月前的某个版本,都能一键找到。
安全性不是“事后补救”
很多 DIY CI 最终变成“摆设”,就是因为一开始没考虑安全。
以下是我们的几条硬性规定:
1. Jenkins 必须启用 HTTPS
自签名证书也行,但不能裸奔 HTTP。
使用 Nginx 反向代理 + Let’s Encrypt 免费证书是最优解:
server {
listen 443 ssl;
server_name ci.ourlab.local;
ssl_certificate /etc/letsencrypt/live/ci.ourlab.local/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ci.ourlab.local/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
2. 启用 LDAP/AD 认证
禁止使用本地账号。我们对接了公司 AD,只有域账户才能登录。
插件推荐: LDAP Plugin 或 Microsoft Azure AD Plugin
3. Agent 权限最小化
jenkins-agent用户不能sudo- 禁用密码登录,仅允许 SSH 密钥
- 防火墙只开放 22 端口(SSH)或 50000(JNLP)
4. 定期备份 Jenkins Home
包含 job 配置、凭证、构建历史等核心数据。
我们用 rsync 每天凌晨同步到备用 NAS:
rsync -az --delete /var/lib/jenkins/ backup@nas:/backup/jenkins/
进阶玩法:动态伸缩 + 自动清理
随着项目增多,我们遇到了两个新问题:
- 白天构建请求集中,agent 不够用;
- 构建记录越积越多,磁盘快满了。
解决方案如下:
动态扩容:用 Kubernetes 拉起临时 Agent
我们将部分非敏感项目迁移到 K8s 集群中,使用 Jenkins Kubernetes Plugin 实现“按需创建 agent”。
配置 Pod Template:
metadata:
labels:
app: jenkins-agent
spec:
containers:
- name: jnlp
image: jenkins/inbound-agent:alpine
args: ['\$(JENKINS_SECRET)', '\$(JENKINS_NAME)']
resources:
requests:
memory: "2Gi"
cpu: "1"
- name: builder
image: ourteam/arm-build-env:latest
command:
- cat
tty: true
当有构建任务到来时,Jenkins 会自动在 K8s 中创建 Pod,任务结束后自动销毁。完全无需维护物理设备。
✅ 适合场景:临时测试、文档生成、非涉密模块构建
自动清理旧构建
在每个 Job 配置中启用“丢弃旧的构建”策略:
properties([
buildDiscarder(logRotator(
daysToKeepStr: '30',
numToKeepStr: '50',
artifactNumToKeepStr: '10'
))
])
解释一下:
- 最多保留 30 天内的构建
- 总数不超过 50 个
- 每个构建只保留最近 10 个带产物的记录
结合定时脚本清理 NFS 上的过期固件,一年下来节省了近 2TB 存储。
故障排查清单:那些年我们踩过的坑
最后分享一份真实故障排查表,都是血泪经验:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| Agent 连接后立即断开 | Java 版本不兼容 | 统一使用 OpenJDK 11 |
构建时报错 command not found: arm-none-eabi-gcc |
PATH 未正确加载 | 在 Jenkins Job 中显式设置 PATH 或使用 environment 块 |
| 多个任务同时运行时卡死 | 内存不足 | 限制并发数,或升级到 16GB RAM |
| 构建产物缺失 | archiveArtifacts 路径错误 |
使用绝对路径或 ws() 函数 |
| webhook 无响应 | 防火墙阻挡 8080 端口 | 开放端口或使用反向代理 |
| 树莓派长时间运行后降频 | 散热不良 | 加装散热片 + 风扇,或限制 CPU 频率上限 |
还有一个隐藏雷区: 时区不一致 。曾有一次,因为 Master 和 Agent 时区差了 8 小时,导致定时构建总是在错误的时间触发。建议统一使用 UTC 或明确设置 TZ=Asia/Shanghai 。
写在最后:这才是“工程师的乐趣”
搭建这套系统花了我们两周时间。起初只是为了省点云服务费用,没想到带来了远超预期的价值:
- 新成员入职第一天就能看到完整的 CI 流水线;
- 每次提交都有即时反馈,bug 在早期就被拦截;
- 固件版本可追溯,客户审计轻松通过;
- 最重要的是——我们真正掌控了自己的开发流程。
这或许不像部署一个 Kubernetes 集群那么“高大上”,但它实实在在解决了问题。而且过程中你能学到操作系统、网络、自动化、安全等多方面的知识。
有时候我觉得, 现代软件工程缺的不是工具,而是动手的能力。
如果你也有台闲置的电脑,不妨今晚就试试。从点亮第一个 “Hello, Embedded World” 开始,你会发现:原来 CI 也可以这么有趣。✨
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)