作者: 鲨鱼辣钊
版本: 1.0
日期: 2026年8月6日
标签: ISO构建、lorax、genisoimage、createrepo、Linux发行版、UOS
适合人群: Linux发行版维护者、系统集成工程师、DevOps
摘要
本文说了什么:基于UOS系统,完整拆解一个 ISO 集成脚本 make_iso.sh 的实现——解析 yum 源地址、lorax 生成引导骨架、yumdownloader 按清单批量下载 RPM、createrepo 生成仓库元数据、genisoimage 打包 BIOS/UEFI 双启动 ISO,最后用 isohybrid / implantisomd5 完成 U 盘可写与介质校验。
实现了什么:一个一键脚本,输入一个可用的 yum 源,输出一个可直接安装系统的 ISO 镜像(含 md5 校验文件)。
目的是什么:沉淀发行版镜像集成的完整做法与关键参数含义,便于维护与移植;与《Pungi 添加双内核集成技术总结》互为补充——Pungi 适合 koji 驱动的全自动 compose,本脚本适合轻量、可控的手动集成场景。
一、背景:为什么要自己写 ISO 集成脚本
做 Linux 发行版维护,绕不开一件事:把一堆 RPM 包变成一张可安装的系统盘。
重量级方案是 Pungi / lorax 全家桶,配置驱动、阶段完整,但依赖 koji 构建体系和一整套 compose 配置,学习和维护成本都不低(相关问题可参考《Pungi 添加双内核集成技术总结》)。
而日常集成场景往往是:源已经有了(koji/Jenkins 构建产物),只差"收包 + 打包"这两步。这时用现成工具串一条轻量流水线更直接:
| 引导骨架 | lorax | 生成安装程序(anaconda)、isolinux / efiboot 等引导文件 |
| 收包 | yumdownloader | 按 pkglist 清单批量下载 RPM 到 BaseOS |
| 仓库元数据 | createrepo | 生成 repodata,-g 挂上 comps 分组 |
| ISO 打包 | genisoimage | 把目录打成 BIOS/UEFI 双启动 ISO |
| U盘可写 | isohybrid | 让 ISO 可直接 dd 到 U 盘启动 |
| 校验 | implantisomd5 / md5sum | 安装介质自检 + 交付校验文件 |
make_iso.sh 就是把这张表从头到尾串起来。
二、整体流程
先看 ISO 集成在整条交付流水线里的位置:

图上可以看到:bug 修复、koji 构建、Jenkins 自动化校验都通过之后,才进入"集成 ISO"这一步——也就是本脚本的职责。构建失败则检查报错修正后重来,不进入集成。
脚本内部的执行顺序:
#mermaid-svg-k4GKrDKbdQJd8bf0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-k4GKrDKbdQJd8bf0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-k4GKrDKbdQJd8bf0 .error-icon{fill:#552222;}#mermaid-svg-k4GKrDKbdQJd8bf0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-k4GKrDKbdQJd8bf0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-k4GKrDKbdQJd8bf0 .marker.cross{stroke:#333333;}#mermaid-svg-k4GKrDKbdQJd8bf0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-k4GKrDKbdQJd8bf0 p{margin:0;}#mermaid-svg-k4GKrDKbdQJd8bf0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-k4GKrDKbdQJd8bf0 .cluster-label text{fill:#333;}#mermaid-svg-k4GKrDKbdQJd8bf0 .cluster-label span{color:#333;}#mermaid-svg-k4GKrDKbdQJd8bf0 .cluster-label span p{background-color:transparent;}#mermaid-svg-k4GKrDKbdQJd8bf0 .label text,#mermaid-svg-k4GKrDKbdQJd8bf0 span{fill:#333;color:#333;}#mermaid-svg-k4GKrDKbdQJd8bf0 .node rect,#mermaid-svg-k4GKrDKbdQJd8bf0 .node circle,#mermaid-svg-k4GKrDKbdQJd8bf0 .node ellipse,#mermaid-svg-k4GKrDKbdQJd8bf0 .node polygon,#mermaid-svg-k4GKrDKbdQJd8bf0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-k4GKrDKbdQJd8bf0 .rough-node .label text,#mermaid-svg-k4GKrDKbdQJd8bf0 .node .label text,#mermaid-svg-k4GKrDKbdQJd8bf0 .image-shape .label,#mermaid-svg-k4GKrDKbdQJd8bf0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-k4GKrDKbdQJd8bf0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-k4GKrDKbdQJd8bf0 .rough-node .label,#mermaid-svg-k4GKrDKbdQJd8bf0 .node .label,#mermaid-svg-k4GKrDKbdQJd8bf0 .image-shape .label,#mermaid-svg-k4GKrDKbdQJd8bf0 .icon-shape .label{text-align:center;}#mermaid-svg-k4GKrDKbdQJd8bf0 .node.clickable{cursor:pointer;}#mermaid-svg-k4GKrDKbdQJd8bf0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-k4GKrDKbdQJd8bf0 .arrowheadPath{fill:#333333;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-k4GKrDKbdQJd8bf0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-k4GKrDKbdQJd8bf0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-k4GKrDKbdQJd8bf0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-k4GKrDKbdQJd8bf0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-k4GKrDKbdQJd8bf0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-k4GKrDKbdQJd8bf0 .cluster text{fill:#333;}#mermaid-svg-k4GKrDKbdQJd8bf0 .cluster span{color:#333;}#mermaid-svg-k4GKrDKbdQJd8bf0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-k4GKrDKbdQJd8bf0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-k4GKrDKbdQJd8bf0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-k4GKrDKbdQJd8bf0 .icon-shape,#mermaid-svg-k4GKrDKbdQJd8bf0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-k4GKrDKbdQJd8bf0 .icon-shape p,#mermaid-svg-k4GKrDKbdQJd8bf0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-k4GKrDKbdQJd8bf0 .icon-shape .label rect,#mermaid-svg-k4GKrDKbdQJd8bf0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-k4GKrDKbdQJd8bf0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-k4GKrDKbdQJd8bf0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-k4GKrDKbdQJd8bf0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是, 防覆盖
否
解析 comm.repo 取出 baseurl
lorax 生成引导骨架 outdir
outdir 是否已存在?
直接退出
yumdownloader 按 pkglist 批量下载 RPM
createrepo -g 生成 BaseOS 仓库元数据
复制 .treeinfo / AppStream 占位 / kernel-4.18
genisoimage 打包双启动 ISO
isohybrid –uefi 支持U盘启动
implantisomd5 植入校验 + 导出 md5 文件
一句话概括:lorax 搭骨架、yum 收血肉、genisoimage 装壳、isohybrid 开门。
三、目录结构与前置文件
脚本所在目录需要以下文件:
| make_iso.sh | 主脚本 | 在目录内执行(依赖 pwd) |
| pkglist | BaseOS 包清单 | 上千个包名,空格分隔一行 |
| comm.repo | yum 源配置 | baseurl= 指向构建源 |
| comps-BaseOS.x86_64.xml | BaseOS 分组定义 | createrepo -g 使用 |
| comps-AppStream.x86_64.xml | AppStream 分组定义 | 同上 |
| .treeinfo | 镜像元信息 | 覆盖 lorax 生成值,声明变体 |
| kernel-4.18/ | 额外集成的内核 | 双内核场景预留 |
| AppStream/ | AppStream 占位仓库 | 源未就绪时的临时方案 |
运行环境要求:
- root 权限(写入 /opt)
- 工具链:lorax、yumdownloader、createrepo、genisoimage、isohybrid、implantisomd5
- 一个网络可达的 yum 源(comm.repo 中配置)
comm.repo 用占位符管理源地址,脚本切环境只改这一个文件:
[comm]
name=comm
baseurl=${repo_url}
gpgcheck=0
四、脚本逐段解析
4.1 变量与源地址解析
workdir=`pwd`
date=$(date +%Y%m%d%H%M)
repodir=`cat $workdir/comm.repo |grep baseurl|awk -F "baseurl=" '{print $2}'`
outdir=/opt/$date-JC
basedir=$outdir/BaseOS/Packages
basecomps=comps-BaseOS.x86_64.xml
appcomps=comps-AppStream.x86_64.xml
AppStream_dir=$workdir/AppStream
isodir=/opt
arch=`uname -r |awk -F "." '{print $NF}'`
family=UnionTechOS
version=20
short=UnionTechOS
LABLE=UnionTechOS-20-x86_64
几个关键设计:
- outdir=/opt/$date-JC:用分钟级时间戳做目录名,每次构建产物隔离,天然保留历史版本
- repodir 从 comm.repo 里 awk 出来:源地址单一配置点,脚本本身不含任何硬编码源地址
- arch 从内核_release 末段取(如 x86_64),用于日志展示
- LABLE 是卷标:后面 lorax –volid 和 genisoimage -volid 共用,保证挂载名一致
4.2 lorax 生成引导骨架
echo lorax started to execute, arch:$arch
if [ ! -d "$outdir" ]; then
lorax –noupgrade -p "$family" -v "$version" –release=20 -r "$short" \\
–nomacboot –isfinal –volid="$LABLE" –rootfs-size=3 \\
-s "$repodir" "$outdir"
if [ $? != 0 ]; then
exit
fi
else
echo $outdir already exists
exit
fi
参数速查:
| -p / -v / –release / -r | 产品名、版本等发布信息,最终写入 os-release、.treeinfo 等镜像元数据 |
| –nomacboot | 不生成 mac 引导,服务器场景用不上 |
| –isfinal | 去掉"测试版"水印标记 |
| –volid | ISO 卷标 |
| –rootfs-size=3 | 安装环境 rootfs 大小(GB),够用即可,小了装不下、大了浪费 |
| -s | 源地址,lorax 从中拉取构建安装程序所需的包 |
注意两层保护:
这一步跑完,outdir 里就是安装程序的完整骨架:isolinux/、images/efiboot.img、.treeinfo 等,还差 RPM 包。
4.3 BaseOS:批量收包 + 生成 repodata
download_baseos(){
pkglist=`cat pkglist`
mkdir -p $basedir
yumdownloader -c $workdir/comm.repo –destdir $basedir $pkglist
cp $basecomps $outdir/BaseOS/
createrepo -g $basecomps $outdir/BaseOS/ || { echo "Createrepo failed"; exit 1; }
rm -f $outdir/BaseOS/$basecomps
}
三步:
createrepo 失败立即终止(|| { …; exit 1; }),理由同 lorax:元数据坏了的仓库打出来的 ISO 安装时必炸。
4.4 AppStream:占位策略(当前启用)
download_appstream 是与 4.3 对称的 AppStream 收包函数(用 yum –installroot –downloadonly 收包再搬出来),但当前被注释停用:
#AppStream is not ready, so use an empty warehouse temporarily.
# When ready, enable download_appstream and close empty_repo
empty_repo(){
\\cp -r $workdir/.treeinfo $AppStream_dir $workdir/kernel-4.18 $outdir/
}
download_baseos
#download_appstream
empty_repo
AppStream 源未就绪时,用 empty_repo 做三件事:
- 用自定义 .treeinfo 覆盖 lorax 生成的,声明镜像变体布局
- 拷入 AppStream/ 空仓库占位,保证 ISO 目录结构与正式版一致,安装程序不会因缺目录报错
- 拷入 kernel-4.18/——双内核集成的预留位,与《Pungi 添加双内核集成技术总结》解决的问题同源
两个细节:
- \\cp 前加反斜杠:绕过某些环境里 cp -i 的 alias,避免交互确认卡住脚本
- 开关式演进:AppStream 就绪后,启用 download_appstream、注释 empty_repo 即可,脚本结构不用动
4.5 genisoimage 打包双启动 ISO
if ! TEMPLATE="$($(head -n1 $(which lorax) | cut -c3-) -c 'import pylorax; print(pylorax.find_templates())')"; then
TEMPLATE=/usr/share/lorax
fi
/usr/bin/genisoimage -untranslated-filenames -volid $LABLE -J -joliet-long \\
-rational-rock -translation-table -input-charset utf-8 -x ./lost+found \\
-b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table \\
-eltorito-alt-boot -e images/efiboot.img -no-emul-boot \\
-o $workdir/UnionTechOS-20-x86_64-dvd1-$date.iso $outdir
开头的 TEMPLATE 探测是一段防御性代码:动态询问 pylorax 模板目录,失败回退 /usr/share/lorax(为将来引用模板留的口子,当前打包命令未直接使用)。
genisoimage 这行是全文信息量最大的一条命令,拆开看:
通用参数:
| -volid $LABLE | 卷标,与 lorax 保持一致 |
| -J / -joliet-long | Joliet 格式(Windows 下可读),允许长文件名 |
| -rational-rock / -translation-table | Rock Ridge 优化 + 文件名转换表(Linux 下保留大小写与权限) |
| -untranslated-filenames | 保留原始文件名 |
| -input-charset utf-8 | 输入文件名按 UTF-8 解析 |
| -x ./lost+found | 排除 lost+found 目录 |
BIOS 引导(第一个 El Torito 启动项):
| -b isolinux/isolinux.bin | 引导镜像文件 |
| -c isolinux/boot.cat | 启动目录文件 |
| -no-emul-boot | 不模拟软盘/硬盘,直接引导 |
| -boot-load-size 4 | 加载 4 个扇区,isolinux 的固定要求 |
| -boot-info-table | 给引导镜像打补丁表,isolinux 需要 |
UEFI 引导(第二个启动项):
| -eltorito-alt-boot | 开启备用启动目录,后面挂第二启动项 |
| -e images/efiboot.img | EFI 引导镜像(lorax 已生成) |
| -no-emul-boot | 同样不模拟 |
一套 ISO 同时塞进 BIOS 和 UEFI 两个启动入口,老机器新机器都能装——这就是"双启动 ISO"的全部秘密。
4.6 isohybrid 与校验
/usr/bin/isohybrid –uefi $workdir/UnionTechOS-20-x86_64-dvd1-$date.iso
/usr/bin/implantisomd5 –supported-iso $workdir/UnionTechOS-20-x86_64-dvd1-$date.iso
md5sum UnionTechOS-20-x86_64-dvd1-$date.iso > UnionTechOS-20-x86_64-dvd1-$date.iso.md5
- isohybrid –uefi:在 ISO 的 MBR/GPT 上做手脚,让它既能刻盘、也能直接 dd 进 U 盘启动(含 UEFI 分区表)
- implantisomd5:把 ISO 自身 md5 植进镜像,安装程序的 media check(介质自检)读的就是它
- md5sum > .md5:交付用的外部校验文件,下载方先验 md5 再刻盘
五、注意事项与常见坑
六、运行方式与环境验证
# 构建机上以 root 执行
cd /path/to/mkiso
./make_iso.sh
# 产物:当前目录 UnionTechOS-20-x86_64-dvd1-<时间戳>.iso 及同名 .md5
本机(普通开发机)做的验证:
$ bash -n make_iso.sh && echo "SYNTAX OK"
SYNTAX OK
$ genisoimage –version
genisoimage 1.1.11 (Linux)
即:脚本语法校验通过,打包工具 genisoimage 可用;lorax、yumdownloader、createrepo、isohybrid、implantisomd5 属构建机工具链,完整构建输出(各阶段日志、最终 ISO 大小与耗时)需在构建机以 root 实跑后回填,本文不虚构运行结果。
七、总结
- 流水线思维:lorax 骨架 → yumdownloader 收包 → createrepo 元数据 → genisoimage 双启动打包 → isohybrid/implantisomd5 收尾,每步单一职责、失败即停
- 配置与代码分离:源地址在 comm.repo、包清单在 pkglist、分组在 comps,脚本本身不含业务数据,移植只换配置文件
- 时间戳产物 + 防覆盖:构建历史天然留存,误跑不毁产物
- 占位演进:AppStream 未就绪用空仓库占位保结构一致,就绪后一个开关切换
- 双内核预留:kernel-4.18/ 的拷贝位与《Pungi 添加双内核集成技术总结》方案呼应
轻量集成用本脚本,全自动 compose 用 Pungi,两条路线按场景取舍即可。
网硕互联帮助中心




评论前必须登录!
注册