大家好。
mkosi
是systemd组开发的一个强大的通用镜像构建基础设施,使用mkosi
能轻松地构建客制化的镜像。
而真正更具吸引力的不止于此,通过mkosi
和所有systemd的不可变基础设施(systemd-sysusers、systemd-tmpfiles、systemd-sysupdate、systemd-sysext、systemd-confext、systemd-importd),我们可以得到贴合Poettering期望的ParticleOS范式的USR不可变系统。
我是Debian用户,以及狂热的systemd爱好者,不过很早就对AOSC有所耳闻,恰好AOSC也用apt
(*当然更推荐的是oma
吧),所以一直想试着用用。
前不久我在试着验证性地构建了Debian的USR不可变变体。结果证明使用手感相当不错,对mkosi
的使用也愈发自信了。遂现在试着用mkosi
构建AOSC试一试。
这个帖子更多地类似于一种日记和记录的性质,做这个完全是出于爱好,只会在摸鱼的时候玩一玩,所以有很大可能会变成坟或鸽掉,大家看个乐就好。帖子里提到的所有代码会被放在https://github.com/MoltenArmor/aosc-mkosi
,大家感兴趣可以随意使用。
我们从一个简单的基础mkosi.conf
配置开始,这里我们启用了增量构建(Incremental=yes
),启用了缓存(CacheDirectory=
),将AOSC视为Debian的变体(Distribution=debian
),不过将唯一的软件仓库URL设为AOSC的仓库(LocalMirror=https://repo.aosc.io/debs/
)。
此外,我们还通过内核命令行参数禁用了私以为用途不是很大的内核功能(systemd.ssh_auto=no security=none audit=0 mitigations=off
),并且禁用了安全启动,格式先设为Raw镜像(Format=disk
)试一试。
[Build]
History=yes
CacheDirectory=mkosi.cache
Incremental=yes
[Distribution]
Distribution=debian
LocalMirror=https://repo.aosc.io/debs/
Release=stable
RepositoryKeyCheck=no
Repositories=main
[Output]
OutputDirectory=mkosi.output
SplitArtifacts=no
Format=disk
ImageId=AOSC
[Content]
KernelCommandLine=rw logo.nologo systemd.ssh_auto=no security=none audit=0 mitigations=off
[Validation]
SecureBoot=no
SignExpectedPcr=no
SecureBootAutoEnroll=no
能不能构建呢?事实证明不行,原因是这样的:
看起来似乎原因在于AOSC的仓库中没有base-files
包。
原因在于,mkosi
在构建Debian的初始阶段,需要构建基本文件系统层次结构,这一阶段中硬编码了base-files
这个包。
base-files
包在Debian中提供基本的文件系统层次和必备文件,经过查证,AOSC似乎有一个类似的包aosc-aaa
。
解决问题的直接思路似乎是,通过某种手段为这个包设置一个base-files
的别名试一试能否解决问题。但是看起来操作似乎过于繁琐。
所以我们考虑直接暴力修改硬编码的这个包名试一试:
with tempfile.NamedTemporaryFile(mode="r") as f:
Apt.invoke(
context,
"install",
[
"-oDebug::pkgDPkgPm=1",
f"-oDPkg::Pre-Install-Pkgs::=cat >{workdir(Path(f.name))}",
"?essential",
"aosc-aaa",
],
options=["--bind", f.name, workdir(Path(f.name))],
)
事实证明还是不行,这就难受了。
得找时间看一下postrm
脚本的内容,如果解决不了的话,就只能用custom
方式 + 提供skeleton rootfs的方式构建了。
看起来是因为不存在/etc/group
和/etc/passwd
:
而事实上,aosc-aaa
包也确实没有提供这两个文件……
按道理说这也许应该是aosc-aaa
包应该解决的问题,不过我们不妨通过在mkosi.skeleton
中手动提供以绕过这个问题。
事实证明不能解决问题:
其原因在于aosc-aaa
似乎没有引入必要的依赖链(主要是没有引入bash
),导致preinst
脚本根本不能执行……要解决这个问题,要么我们得修改依赖链,要么我们得修改mkosi
内部硬编码的包……
虽然很遗憾……但是还是只能决定放弃从头构建,转而采用Rootfs + custom
发行版(不使用mkosi
内置的包管理器接口而是手动执行Shell脚本安装软件包)的方式。
看起来 https://releases.aosc.io/ 有多种Rootfs的Tar包可选,不过需要抽时间看看每个Rootfs Flavor的包列表……
感激不尽,如此一来问题就简化了不少,我们可以使用custom
发行版框架并在脚本中使用aoscbootstrap
——毕竟mkosi
本来就是debootrap
的Wrapper,这样做无非是需要手动重新实现一遍mkosi
内部编写的软件包安装逻辑,但是大部分mkosi
的功能,比如systemd-repart
分区、systemd-boot
的安装、UKI等还是可复用的。
我之后会抽时间看看的。
1 Like
我们回来了。
感谢 eatradish 的建议,这次我们来尝试用aoscbootstrap
构建Base rootfs。
首先要使用aoscbootstrap
,我们就需要用一个镜像来构建它,因此我们创建一个子镜像:
mkosi.images/aoscbootstrap
这个子镜像的配置是这样的:
[Content]
Packages=
build-essential
ca-certificates
cargo
pkg-config
libssl-dev
cmake
zlib1g-dev
liblzma-dev
libsolv-dev
libclang-dev
[Output]
Format=none
构建依赖包是手动测出来的,Format=none
可以避免该镜像输出任何产物,因为我们实际上并不需要,这个镜像仅仅用于构建Base Rootfs。
接下来我们要修改基础mkosi.conf
了,我们暂时把Distribution改回debian
,因为mkosi
不支持在子镜像中配置独立的Distribution。
由于cargo
的构建需要联网,因此我们启用WithNetwork=yes
。
[Build]
History=yes
CacheDirectory=mkosi.cache
Incremental=yes
WithNetwork=yes
[Distribution]
Distribution=debian
Release=testing
RepositoryKeyCheck=no
Repositories=contrib,non-free,non-free-firmware
[Output]
OutputDirectory=mkosi.output
SplitArtifacts=no
Format=disk
ImageId=AOSC
[Content]
KernelCommandLine=rw logo.nologo systemd.ssh_auto=no security=none audit=0 mitigations=off
[Validation]
SecureBoot=no
SignExpectedPcr=no
SecureBootAutoEnroll=no
这个配置肯定还要修改的,不过留待以后操作,我们来写构建脚本。
mkosi
支持构建中间产物的持久化,从而提高重复构建效率,要实现这一功能我们只需要在项目根目录下创建一个mkosi.builddir
:
├── LICENSE
├── README.md
├── mkosi.builddir # 这个
├── mkosi.conf
└── mkosi.images
└── aoscbootstrap
然后我们在mkosi.images/aoscbootstrap
中编写构建脚本。按道理说,我们至少需要两个脚本:
- 用于下载/更新源代码的脚本,这个通过
mkosi.sync
实现,该脚本是联网的。
- 用于执行构建的脚本,这个通过
mkosi.build
实现。
因此我们编写:
mkosi.images/aoscbootstrap/mkosi.sync
:
#!/bin/sh
set -ue
# Probably no use, but anyway...
[ -d "$SRCDIR/aoscbootstrap" ] && \
rm -rf "$SRCDIR/aoscbootstrap"
cd "$SRCDIR" && \
git clone https://github.com/AOSC-Dev/aoscbootstrap.git || exit 1
mkosi.images/aoscbootstrap/mkosi.build.chroot
:
#!/bin/sh
set -ue
cd "$CHROOT_SRCDIR/aoscbootstrap/" && \
cargo build --release --target-dir "$CHROOT_BUILDDIR/target"
cat << 'EOT' > "$CHROOT_BUILDDIR/target/release/aosc-mainline.toml"
stub-packages = [
"aosc-aaa",
"apt",
"gcc-runtime",
"tar",
"xz",
"gnupg",
"grep",
"ca-certs",
"iptables",
"shadow",
"systemd",
"keyutils"
]
base-packages = [
"bash-completion",
"bash-startup",
"iana-etc",
"libidn",
"tzdata"
]
EOT
cd "$CHROOT_BUILDDIR/target/release/" && \
# Let's just do stage1 because chroot inside sandbox will make /proc inaccessible.
./aoscbootstrap --stage1-only --config=aosc-mainline.toml stable "$CHROOT_SRCDIR/mkosi.output/base/" > aoscbootstrap.log
# Let's rename the stage 2 script.
script_fname="$(grep -F -m 1 'If you want to continue stage 2,' aoscbootstrap.log | grep -Eo '\.tmp[[:alnum:]]+')"
cd "$CHROOT_SRCDIR/mkosi.output/base/" && \
mv "${script_fname}" stage2_script.sh
测试一下发现构建过程没有问题:
接下来又是工作日,我们先到这里吧,可能下周末才有时间再搞了。
没忍住,今天晚上就来接着更新吧。
按道理说,我们的思路是:
- 构建一个Debian的小型镜像,在其中构建
aoscbootstrap
,并使用它释放出Base Tree(即Base Rootfs)。
- 因为在提供了Base Tree的情况下,
mkosi
会跳过自动构建Base Tree的步骤(即安装那个该死的base-files
的过程),所以我们可以轻易地进行之后的构建过程。
乍一看我们似乎使用mkosi.images
构建子镜像即可,但是实际上不行,为什么呢?
因为,mkosi
在设计上认为,子镜像和主镜像应当从一套软件树(即同一个软件仓库)中构建而来。如果两个镜像从两个不同的软件仓库构建而来,那么这在mkosi
看来就不存在父子关系。
所以我们的项目结构需要进行改变,变成这样:
├── LICENSE
├── README.md
├── base-rootfs # 这里构建Base Tree
│ ├── mkosi.build.chroot
│ ├── mkosi.builddir
│ ├── mkosi.conf
│ └── mkosi.sync
├── mkosi.conf
├── mkosi.postinst.chroot
├── mkosi.prepare.chroot
└── mkosi.sync
base-rootfs
目录下就是我们前面所有的研究成果。外层的这个才是我们的AOSC镜像的直接构建配置。
这个外层镜像的主配置如下:
[Build]
History=yes
CacheDirectory=base-rootfs/mkosi.cache # 我们使用一个Cache目录就好
Incremental=yes
WithNetwork=yes
[Distribution]
Distribution=debian
LocalMirror=https://repo.aosc.io/debs/
Release=stable
RepositoryKeyCheck=no
Repositories=main
[Output]
OutputDirectory=mkosi.output
Format=directory
[Content]
BaseTrees=base-rootfs/mkosi.output/rootfs # 这里我们设置使用Base Tree
KernelCommandLine=rw logo.nologo systemd.ssh_auto=no security=none audit=0 mitigations=off
Packages=
# 随便装点软件包
[Validation]
SecureBoot=no
SignExpectedPcr=no
SecureBootAutoEnroll=no
一目了然!
为了确保健壮性,我们创建mkosi.sync
脚本,用于“同步”根文件系统:
#!/bin/sh
set -ue
if ! [ -e "${SRCDIR}/base-rootfs/mkosi.output/rootfs/stage2_script.sh" ]; then
printf '%s\n' "You have not built the base-rootfs image! Please build it at first!"
exit 1
else
exit 0
fi
基本上这样就好了!但是还不够,经过探索发现,aoscbootstrap
的Stage 2执行会失败。为什么呢?原因在于在mkosi
的沙盒中,执行chroot
时会导致/proc
处于未挂载状态。
因此,我们必须将Stage 2脚本延迟执行。首先我们修改我们的Base Rootfs构建脚本base-rootfs/mkosi.build.chroot
:
......
cd "$CHROOT_BUILDDIR/target/release/" || exit 1
if ! [ -e "$CHROOT_SRCDIR/mkosi.output/rootfs/stage2_script.sh" ]; then
# Let's just do stage1 because chroot inside sandbox will make /proc inaccessible.
./aoscbootstrap --stage1-only --config=aosc-mainline.toml stable "$CHROOT_SRCDIR/mkosi.output/rootfs/" 2>&1 | tee aoscbootstrap.log
# 这里我们用REGEX抓Stage 2脚本的临时文件名
script_fname="$(grep -F -m 1 'If you want to continue stage 2,' aoscbootstrap.log | grep -Eo '\.tmp[[:alnum:]]+')"
else
exit 1
fi
cd "$CHROOT_SRCDIR/mkosi.output/rootfs/" && \
# 把脚本名归一化
mv "${script_fname}" stage2_script.sh
确保脚本名可知,这就可以了!我们接着在主镜像中写mkosi.postinst.chroot
:
#!/bin/sh
set -ue
cp --archive --update=none /etc/os-release /usr/lib/os-release
bash /stage2_script.sh
rm -f /stage2_script.sh
很好,这样应该就可以了!