【技术尝试】用mkosi构建AOSC镜像

大家好。

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的包列表……

这几个文件在 aoscbootstrap/assets/etc-bootstrap.tar.xz at master · AOSC-Dev/aoscbootstrap · GitHub ,确实没有以软件包的形式分发(相比 mkosi 我们用自己写的 aoscbootstrap 来构建 rootfs)

1 Like

感激不尽,如此一来问题就简化了不少,我们可以使用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中编写构建脚本。按道理说,我们至少需要两个脚本:

  1. 用于下载/更新源代码的脚本,这个通过mkosi.sync实现,该脚本是联网的。
  2. 用于执行构建的脚本,这个通过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

测试一下发现构建过程没有问题:

接下来又是工作日,我们先到这里吧,可能下周末才有时间再搞了。

没忍住,今天晚上就来接着更新吧。

按道理说,我们的思路是:

  1. 构建一个Debian的小型镜像,在其中构建aoscbootstrap,并使用它释放出Base Tree(即Base Rootfs)。
  2. 因为在提供了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

很好,这样应该就可以了!

我们来构建试一试:

完美!

接下来要做的事就是调优了,包括:

  1. 找出最小化的Base Tree。
  2. 创建合适的Profile和额外配置,以及Match关系。