MKOSI笔记

MKOSI-INITRD

mkosi-initrd是使用MKOSI进行Initrd构建的工具。

1
mkosi-initrd [选项]

可用选项有:

  • -k|--kernel-version=:使用的内核版本,默认使用$(uname -r)
  • -t|--format=:输出格式,可选值有cpiouki,默认使用cpio
  • -g|--generic:生成通用Initrd,即可以在不同的硬件平台上启动当前系统的Initrd
  • -o|--output=:输出文件名前缀,默认为initrd
  • -O|--output-dir=:输出的目录,默认为工作目录。
  • --profile:启用的额外Initrd配置,默认不启用。
    • lvm:LVM支持。
  • --debug:Debug输出。
  • --debug-sandbox:使用strace运行mkosi-sandbox

它本质上是一个使用了特殊的默认配置的MKOSI Wrapper脚本(见mkosi.conf),在执行时,它会获取当前主机的内核版本(或用户指定的内核版本),并将当前内核的/usr/lib/modules/版本号目录映射到镜像中进行构建。

该命令行工具由MKOSI的Pypi包提供,对于发行版来说,它们也可以使用Kernel-install脚本进行集成(参考setup-mkosi)。

MKOSI-ADDON

使用MKOSI构建UKI插件的工具。它会包含当前系统环境中的/etc/crypttab中标记为x-initrd.attach的条目,以及/etc/kernel/cmdline的内容。

1
mkosi-addon [选项]

可用选项有:

  • --kernel-version=:使用的内核版本,默认使用$(uname -r)
  • -o|--output=:输出文件名前缀,默认为initrd
  • -O|--output-dir=:输出的目录,默认为工作目录。
  • --debug-sandbox:使用strace运行mkosi-sandbox

MKOSI-SANDBOX

mkosi-sandbox用于在沙箱中执行命令:

1
mkosi-sandbox [选项] [命令] [参数]

命令默认为bash

接受的选项有:

  • --tmpfs 目录:在沙箱中创建Tmpfs。
  • --dev 目录:将宿主环境的Devfs映射到到沙箱中的指定目录。
  • --proc 目录:将宿主环境的Procfs映射到到沙箱中的指定目录。
  • --dir 目录:在沙箱中递归创建指定目录。
  • --bind|--bind-try|--ro-bind|--ro-bind-try 宿主目录 沙箱目录:将宿主环境的目录(只读)绑定挂载到沙箱的目录。
  • --symlink 路径 链接:在沙箱中创建软链接。
  • --write 数据 路径:在沙箱中写入指定文件。
  • --overlay-lowerdir 主机目录:设置下一个Overlayfs的下层目录。
  • --overlay-upperdir 主机目录|tmpfs:设置下一个Overlayfs的上层目录。
  • --overlay-workdir 主机目录:设置下一个Overlayfs挂载的工作目录。
  • --overlay 沙箱目录:在沙箱中挂载Overlayfs。
  • --unsetenv 变量名:在沙箱中去掉环境变量。
  • --setenv 变量名 值:在沙箱中设置环境变量。
  • --chdir 目录:启动沙箱时的工作目录。
  • --same-dir:启动沙箱时的工作目录为当前宿主机所处的目录。
  • --become-root:成为沙箱内的Root用户并获取CAP_SYS_ADMIN,默认为普通用户。
  • --suppress-chown:禁止使用chown()系统调用。
  • --unshare-net:隔离网络命名空间。
  • --unshare-ipc:隔离IPC命名空间。
  • --pack-fds:将继承的文件描述符打包暴露到$LISTEN_FDS环境变量中。

实例:

1
mkosi-sandbox --bind / / --same-dir --become-root bash --login
1
2
3
4
5
6
7
8
9
10
11
12
mkosi-sandbox \
--ro-bind /usr /usr \
--symlink usr/bin /bin \
--symlink usr/bin /bin \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--symlink usr/sbin /sbin \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
--become-root \
id

MKOSI

Linux发行版 = Base Files(Base Rootfs) + Essential Packages + Package Manager:

  • Debian/Ubuntu = debootstrap(base-files + base-passwd) + "Essential: yes" Packages + apt
  • Fedora/CentOS/RHEL = yum|dnf --installroot(setup + basesystem + filesystem) + @core Package Group + yum|dnf
  • Arch = pacstrap(base) + base Meta Package + pacman
  • Gentoo = stage 3 Image + @system Set + emerge

mkosi是建立在dnf --installrootdebootstrappacstrap等工具之上的,快速构建客制化的系统镜像并进行测试的工具。

“镜像”在这里是一个通用概念,这意味着MKOSI可以生成以下类型的“镜像”:

  • 使用GPT分区表的磁盘镜像,可以克隆到磁盘中。
  • CPIO归档,用于作为Initrd使用。
  • USI(Unified System Image,即将完整的操作系统打包进UKI的镜像)
  • Sysext、Confext、Portable Service镜像
  • 用于容器的目录树镜像

安装

mkosi在各大发行版的软件源中有提供:

1
2
3
4
5
# Debian
apt install mkosi

# RHEL
dnf install mkosi

也可以使用pip安装:

1
pipx install git+https://mirror.ghproxy.com/https://github.com/systemd/mkosi.git

提示:pipx安装的程序在venv中运行,因此不可以使用系统Python解释执行。

语法

1
mkosi [选项] 子命令

构建配置文件

mkosi的任何命令都基于特定的规则读取当前目录下的,或是mkosi/目录下的配置文件,优先级顺序分别是:

  • 最优先:命令行选项。
  • 当前目录下的mkosi.local.conf配置文件和mkosi.local目录下的配置文件。该文件仅用于本地配置,不应该共享。
  • -C|--directory=指定目录(默认为当前目录)下的mkosi.conf配置文件。
  • --directory=指定目录(默认为当前目录)下的mkosi.conf.d/目录内的配置文件。
  • 用户指定的Profile对应的mkosi.profiles/里面的子目录配置。
  • mkosi.images/里面的子镜像的配置。

子命令

mkosi由多种子命令组成,这些命令其实都是一些外部工具的简易接口,它们大部分支持通过--分隔传递一些含有特殊意义的参数:

  • summary:打印配置总结。
    • 默认情况下:打印最新构建的配置总结,也就是.mkosi-private/history/latest.json的内容。
    • -f:强制重新解析配置文件并打印当前配置文件的配置总结。
  • build:从配置文件中读取配置并开始构建镜像。
    • --之后的命令行参数会被传递给mkosi.build脚本。
  • sysupdate:(需要宿主机的systemd版本大于v257)执行systemd-sysupdate,其中,--transfer-source=设为输出目录(默认为mkosi.output/),--definition=设为SysupdateDirectory=的值(默认为mkosi.sysupdate/)。
    • 要求:MKOSI使用的Sysupdate配置中,必须设置PathRelativeTo=explicitPath=/才能正常使用sysupdate子命令进行更新,这会使得配置中的Path=使用mkosi.output/(或OutputDirectory=指定的目录)作为根。
    • v27以后不再需要)这同时也意味着,SplitArtifacts=partitions必须启用以对分区进行更新,而且更新内容(分区镜像文件、EFI程序、Initrd)文件名必须与Sysupdate配置中的MatchPattern=匹配。
      • v27以后,MKOSI会在执行mkosi sysupdate时隐式自动地调用systemd-repart对镜像进行分割。
    • --之后的命令行参数会被传递为systemd-sysupdate的额外参数。
  • shell:(如果还没有构建)先构建镜像,然后调用systemd-nspawnChroot到镜像内。
    • 仅适用于QCow2=no且未压缩的镜像。
    • --之后的命令行参数会被在镜像中执行。
  • boot:(如果还没有构建)先构建镜像,然后调用systemd-nspawn以容器形式启动镜像。
    • 仅适用于QCow2=no且未压缩的镜像。
    • --之后的命令行参数会被传递为执行的命令行。
  • vm:(如果还没有构建)先构建镜像,然后调用qemusystemd-vmspawn以虚拟机形式启动镜像。
    • 对于目录树,MKOSI使用virtiofsd引导。
    • 任何--之后的命令行参数都会作为命令行参数被传递给qemusystemd-vmspawn
  • serve:(如果还没有构建)先构建镜像,然后调用内置的HTTP服务器在8081端口上暴露工作目录。
  • (仅用于disk镜像)burn 块设备:(如果还没有构建)先构建镜像,然后将镜像扩展到目标块设备的大小,再烧录到指定的块设备上。
  • genkey:创建密钥。
  • ssh:对于Ssh=yes的镜像,通过ssh连接到镜像中。
    • --之后的命令行参数会被传递为ssh的额外参数。
  • journalctl:获取镜像内的日志。
    • --之后的命令行参数会被传递为journalctl的额外参数。
  • coredumpctl:获取镜像内的内核转储文件。
    • --之后的命令行参数会被传递为coredumpctl的额外参数。
  • box:在构建沙箱中执行命令。
    • --之后的命令行参数会被传递为执行的命令行。
  • clean:清理之前的构建产品。
    • clean的作用范围由执行时生效Dependencies=决定,它会清理整个依赖链条上的所有镜像,而非仅仅清理指定的单个目标。因此用--dependency X clean时,X的所有依赖镜像也会被一并清理。所以请不要依赖clean清理单个构建产物,你应该直接执行rm
    • 加一次-f|--force,会导致同时清理镜像缓存和Tools Tree。
    • 加两次-f|--force,会导致同时清理软件包缓存。
  • bump:如果启用了版本管理(提供--image-version=命令行选项或是ImageVersion=配置项),那么刷新mkosi.version文件中的镜像版本,如果mkosi.bump文件存在,那么会执行该文件并使用它的输出,否则使用MKOSI的内置逻辑(版本数字+1)。
    • 执行该命令时必须确保mkosi.version文件存在,第一次执行时可以使用bash mkosi.bump > mkosi.version
    • 在启用了History=yes的情况下,更新版本前需要执行mkosi clean
  • genkey:生成安全启动密钥。
  • init:安装MKOSI的tmpfiles配置文件。如果XDG_CONFIG_HOME=/XDG_CONFIG_HOME=/root,那么会安装到/etc/tmpfiles.d/,否则会安装到$XDG_CONFIG_HOME/user-tmpfiles.d/

命令行选项

这里提到的命令行选项是和构建配置完全无关的:

  • -f|--force:进行指定操作前,不加分辨地重新构建镜像。如果该选项出现两次,那么还会不加分辨地清除缓存,这在进行系统更新时很有用。
  • -R|--run-build-scripts:仅仅只执行mkosi.build脚本(类似于Format=none的行为)。
  • -B|--auto-bump:执行操作后自动刷新版本号。
  • -C|--directory=:指定工作目录,默认为当前工作目录。
  • --debug:启用Debug级输出。
  • --debug-shell:执行操作失败时,在/work/src目录下启动一个Debug Shell用于排错。
  • --debug-workspace:执行操作失败时,当前工作空间的内容不会删除。
  • --debug-sandbox:使用strace运行mkosi-sandbox

构建配置

构建配置文件符合.ini格式,每一行都是Key=Value的形式,每一个配置项都有等价的命令行选项,转换方式是SomeSettingsome-setting。路径配置在使用相对路径时,取相对于该配置文件本身所在目录的路径。

配置中存在以下可用的占位符:

含义 占位符
Distribution= %d
Release= %r
Architecture= %a
Format= %t
Output= %o
OutputDirectory= %O
ImageId= %i
ImageVersion= %v
Profiles= %p
配置文件所在目录 %C
工作目录 %P
MKOSI的执行目录 %D
当前镜像的镜像名 %I

具体的构建配置有:

[Match]段与[TriggerMatch]

匹配构建时包含当前配置文件的特定条件。每个配置项除了使用=连接键值对外,还可以使用=|连接键值对,多个使用=连接键值对的配置项之间是逻辑与关系,必须全部满足;多个使用=|连接键值对的配置项之间是逻辑或关系,满足一个即可。

可用配置项有:

  • Profiles=:使用的Profile。
  • Distribution=:构建的发行版。
  • Release=:构建的发行版版本。
  • Architecture=:构建的架构。可用的值有alphaarcarmarm64ia64loongarch64mips64-lemips-lepariscppcppc64ppc64-leriscv32riscv64s390s390xtilegxx86x86-64uefi
  • HostArchitecture=:主机的架构。
  • Image=:构建的镜像名。子镜像名是它在mkosi.images/下的目录名,主镜像名总是main
  • ImageId=:构建的镜像ID。
  • ImageVersion=:构建的镜像版本。
  • Bootable=:构建的镜像是否可引导。
  • Format=:构建的镜像格式。
  • ToolsTreeDistribution=:使用的Tools Tree发行版。
  • PathExists=:主机上存在指定目录。
  • SystemdVersion=:主机上的systemd版本。

[Match]段之间是纯粹的AND关系,也就是说,不同的[Match]段必须全部满足。而[TriggerMatch]段之间是OR关系,也就是说,不同的[TriggerMatch]段中,只要有一个[TriggerMatch]段满足即可。

[Assert]段与[TriggerAssert]

强限制构建时必须满足特定条件。一旦不满足,则立刻发生错误并报告问题。适用于对用户进行友好的限制。

可用配置项和[Match]完全一致。[TriggerAssert]段和[Assert]段的关系也与[Match]段和[TriggerMatch]段的关系一致。

(v20以上版本)[Include]

  • Include=:导入指定的额外配置文件。如果值为mkosi-initrd,那么会使用mkosi-initrd的配置生成Initrd。
    • 特殊值mkosi-initrd表示mkosi-initrd的配置。
      • 这是推荐的行为,因为MKOSI开发组期望构建出的镜像使用与本地环境无关的Initrd。构建过程中会使用镜像内的安装的内核和固件(在安装时均会被MKOSI移动到/usr/lib/modules/目录下)进行打包。
    • 特殊值mkosi-tools表示ToolsTree的配置。
    • 特殊值mkosi-vm表示运行虚拟机时的默认配置。

(v20以上版本)[Config]

额外配置。

  • Profiles=:使用指定的Profile,Profile是mkosi.profiles/目录下保存的一个配置文件或子工作目录,它支持的内容和MKOSI的主工作目录完全一致(除了不能再包含mkosi.profiles/目录),会覆盖主工作目录的对应部分。
    • Profile通常是让用户通过--profile=命令行选项进行自选的,因此很适合表达“同一镜像的不同口味”或“可选功能集”。另外,Profiles=还可以参与[Match]匹配,因此它不仅能“额外引入一段配置”,还会影响条件配置是否生效。
    • 子镜像中也可以创建Profile,但是子镜像中不能设置Profiles=,而且子镜像的Profile必须是工作目录,不可以是配置文件。
    • Profile是继承的:在主镜像上通过Profiles=--profile=启用的Profile,会传递给所有即将被构建的子镜像,无论这些镜像是不是定义了这个Profile。因此,切换Profile的影响范围并不局限于主镜像,而是会参与所有相关子镜像的最终配置计算。
    • Profile会影响缓存:只要某个子镜像的最终生效配置因此发生变化(例如包列表、仓库、脚本等变化),MKOSI就会判定该子镜像的渐进构建缓存失效,并触发这个子镜像清理并重新构建
  • ConfigureScript=:在读取配置文件后(ToolsTree构建之前),在宿主环境中立刻执行的脚本,它会通过标准输入接收JSON格式的配置文件内容,应当通过标准输出输出JSON格式的配置文件内容,用于动态创建或修改构建配置。默认为mkosi.configure
  • MinimumVersion=:要求的最小MKOSI版本号。可以是一个版本号,或是一个以commit:开头的Git Commit Hash。
  • PassEnvironment=:传递的环境变量,空格分隔。
  • Dependencies=:子镜像的依赖关系,子镜像是mkosi.images/目录下保存的一个子工作目录,它支持的配置项和MKOSI的主工作目录相比会少一些。
    • 该配置项对应的命令行选项是--dependency,不是--dependencies
    • 在主(Main)镜像中设置:选择要构建的子镜像子集(及其依赖)。未设置时,默认构建所有子镜像,即相当于Dependencies=*,如果有Dependencies=,那么只构建指定的子镜像。
    • 子镜像中设置:声明构建前提,保证依赖镜像先于本镜像构建。这个声明是双向的——不仅构建时决定顺序,clean时同样决定清理范围。因此,如果多个子镜像共享同一基础镜像,不要在子镜像内声明对共享镜像的Dependencies=,否则--dependency <subimage> clean会导致连带清理共享镜像,破坏缓存。如果你希望明确表明依赖关系,使用PathExists=%O/XXX
    • 子镜像通过其目录确定镜像名,不会受到ImageId=的影响。
    • 要使用子镜像的产物,在主镜像中使用%O占位符。这个占位符不会被延迟展开
    • 这些配置项只影响主镜像和默认工具树,而且不允许在子镜像中进行配置:
      • RepositoryKeyCheck=
      • RepositoryKeyFetch=
      • SourceDateEpoch=
      • CacheOnly=
      • WorkspaceDirectory=
      • PackageCacheDirectory=
      • BuildSources=
      • BuildSourcesEphemeral=
      • ProxyClientCertificate=
      • ProxyClientKey=
      • ProxyExclude=
      • ProxyPeerCertificate=
      • ProxyUrl=
    • 这些配置项同时影响主镜像、默认工具树和所有子镜像,而且不允许在子镜像中进行配置:
      • Architecture=
      • BuildDirectory=
      • CacheDirectory=
      • Distribution=
      • ExtraSearchPaths=
      • Incremental=
      • LocalMirror=
      • Mirror=
      • OutputDirectory=
      • OutputMode=
      • PackageDirectories=
      • Release=
      • RepartOffline=
      • Repositories=
      • SandboxTrees=
      • ToolsTree=
      • ToolsTreeCertificates=
      • UseSubvolumes=
      • SecureBootCertificate=
      • SecureBootCertificateSource=
      • SecureBootKey=
      • SecureBootKeySource=
      • VerityCertificate=
      • VerityCertificateSource=
      • VerityKey=
      • VerityKeySource=
      • SignExpectedPcrCertificate=
      • SignExpectedPcrCertificateSource=
      • SignExpectedPcrKey=
      • SignExpectedPcrKeySource=
      • VolatilePackageDirectories=
      • WithNetwork=
      • WithTests
    • 这些配置项会被传递到子镜像,而且可以在子镜像中进行覆盖配置:
      • Profiles=
      • ImageId=
      • ImageVersion=
      • SectorSize=
      • CacheKey=
      • BuildKey=
      • CompressLevel=
    • 这些配置项不会被传递到子镜像:
      • MinimumVersion=
      • PassEnvironment=
      • ToolsTreeDistribution=
      • ToolsTreeRelease=
      • ToolsTreeProfiles=
      • ToolsTreeMirror=
      • ToolsTreeRepositories=
      • ToolsTreeSandboxTrees=
      • ToolsTreePackages=
      • ToolsTreePackageDirectories=
      • History=
      • 整个[Runtime]
    • 这些配置项实际上只会在MKOSI运行过程中进行检查,因此可以用于动态依赖(例如,使用%O指定输出目录中的产物):
      • BaseTrees=
      • ExtraTrees=
      • Initrds=

[Distribution]

发行版配置。

  • Distribution=:使用的发行版,目前支持fedora,debian,ubuntu,arch,opensuse,mageia,centos,centos_epel,openmandriva,rocky,rocky_epel,alma,alma_epel,gentoo,custom。默认使用主机的发行版。
    • Distribution=custom时,无法自动构建Rootfs,需要自行提供。
  • Release=:使用的版本。默认使用主机的版本。
  • Snapshot=:使用的快照日期,格式为YYYYmmdd
  • Architecture=:使用的架构。默认使用主机的架构。
  • Mirror=:使用的软件源。
    • 在使用EPEL时,如果要为EPEL使用其他软件源,使用$EPEL_MIRROR环境变量。
    • Debian的Debug和Security源无法直接切换软件源,考虑使用mkosi.sandbox,或是构建无Debug和Security仓库的sid|unstable版本。
    • MKOSI内置了一部分软件源,要禁用它们,考虑创建mkosi.sandbox,并在其中包含空的仓库配置文件。
    • 该选项的值在大部分情况下会被写入镜像,作为镜像的默认软件源。但是OpenSUSE构建后不会写入软件源,用户需要自行执行zypper addrepo
    • Fedora Rawhide国内可用的镜像站只有SJTU
  • LocalMirror=:使用本地软件源,这意味着:
    • 在构建前置镜像(如ToolsTree)时不会使用这里的软件源。
    • 在构建主镜像时只会使用这些软件源,会屏蔽掉内置的默认软件源并覆盖Mirror=的值,但是不会写入镜像。
    • 对于Apt,默认情况下只会启用main分区。
  • RepositoryKeyCheck=:检查镜像源密钥。
  • RepositoryKeyFetch=:在安装时自动尝试获取软件源密钥,默认启用,如果禁用,将从宿主机传递软件源密钥,这需要安装对应的*-keyring软件包。
  • Repositories=:启用的Yum源名(例如epel),或是Apt源的分区(例如main)。使用逗号分隔。

如果希望屏蔽掉内置的默认软件源并覆盖Mirror=的值,使用自定义镜像源,建议使用mkosi.sandbox/SandboxTrees=)目录,并在目录中创建空的包管理器对应的软件源文件:

  • etc/apt/sources.list.d/mkosi.sources(注意必须是DEB822格式)
  • etc/yum.repos.d/mkosi.repo
  • etc/pacman.conf
  • etc/apk/repositories
  • etc/zypp/repos.d/mkosi.repo

[Output]

输出方式。

  • Format=:导出格式,mkosi支持非常多种镜像,包括但不仅限于:
    • disk:GPT+UEFI磁盘镜像。使用systemd-repart生成。
    • directory:纯目录形式的容器根文件系统。
    • tar:Tar包。
    • cpio:CPIO包。
    • addon:UKI扩展。
    • uki:UKI镜像。
      • 该格式会导致[Content]段的配置作用于UKI的Initrd中的内容(而不是不存在的根文件系统),通常情况下会希望启用Include=mkosi-initrd以针对Initrd进行裁剪。
      • 需要特别注意,除非MakeInitrd=yes,否则UKI内的systemd无法按照预期的行为进行换根操作。
    • esp:构建一个包含单个ESP的GPT分区表的磁盘镜像,ESP中包含Bootloader(必须包含,必须安装systemd-boot)和UKI(可选包含,如果Bootable=yes且安装了内核包),从而可以直接通过VM启动。
      • 该格式会导致[Content]段的配置作用于UKI的Initrd中的内容(而不是不存在的根文件系统),通常情况下会希望启用Include=mkosi-initrd以针对Initrd进行裁剪。
      • 需要特别注意,除非MakeInitrd=yes,否则UKI内的systemd无法按照预期的行为进行换根操作。
    • oci:OCI容器镜像。镜像的Entrypoint固定为/sbin/init加上KernelCommandLine=的值。
    • sysext|confext|portable:系统扩展镜像。它们的分区格式配置位于Repart Definitions
    • none:当前配置用于分组,或是执行纯构建操作。如果没有mkosi.build脚本的话,会跳过任何构建过程(包括mkosi.prepare脚本,此时不仅不会生成产物,也不会执行实际镜像构建,mkosi.prepare脚本所做的任何操作都是没有意义的)。
      • 需要特别注意:Format=none且没有mkosi.build脚本的镜像,虽然不会真正执行构建,但是仍然会参与MKOSI的依赖解析、clean范围计算和缓存状态判断。但是,这类镜像又永远不会生成渐进缓存,这就导致在进行缓存一致性判断时,它总是会导致Cache Mismatch,一旦它出现在依赖链条上,就会导致所有其他本来可以复用缓存的子镜像全部被判定为需要清理缓存。在把主镜像设为Format=none、并结合--dependency把子镜像当作Build Target使用时,这一点尤其容易导致反直觉的缓存清理行为。
  • ManifestFormat=:生成镜像大纲(保存安装的所有软件包列表)的格式,可选jsonchangelog,默认不生成。
  • Output=:输出文件/目录的前缀,默认为image
    • ImageId + ImageVersion%i_%v)会被用作默认的输出文件名前缀(Output=)。
  • OutputExtension=:输出文件的后缀,会覆盖默认后缀。
  • OutputDirectory=:构建产品的保存目录,默认为工作目录下的image,也可以手动创建mkosi.output/以默认使用该目录。
  • CompressOutput=:输出镜像的压缩方式,请注意,这会导致shellbootvm命令在该镜像上不可用。仅可用于tarcpioukiespoci镜像。可选值有布尔值和xzzstd等压缩算法,以及none。默认使用zstd
    • CompressLevel=:压缩等级。
  • SplitArtifacts=:分开导出的产品列表,使用逗号分隔,可选值有ukikernelinitrdpartitionstarpcrsroothashkernel-modules-initrdos_release
    • 默认值no等价于uki,kernel,initrd
    • yes等价于uki,kernel,initrd,partitions
    • uki会导出UKI。
    • kernelinitrd会导出组成UKI的内核和Initrd。
    • partitions在使用磁盘镜像时,会告诉systemd-repart针对每个分区分别进行导出。默认情况下,导出的分区文件会以输出文件名前缀.分区名命名。
    • tar会额外导出一份构建时产生的根文件系统的Tar包。
  • ImageVersion=:镜像的版本号。
    • 你也可以使用mkosi.version文件指定版本号,此文件还可以是一个可执行的Shell脚本,使用其输出作为版本号。但是请注意,这种情况下脚本的输出必须是稳定的,否则会导致MKOSI的历史记录完全失效
    • mkosi.version文件可以通过mkosi bump命令进行更新,如果存在mkosi.bump脚本,那么还会使用这个脚本的输出更新版本。
    • ImageId + ImageVersion%i_%v)会被用作默认的输出文件名前缀(Output=)。
  • ImageId=:镜像ID,会被写入镜像的os-release文件中。
    • ImageId + ImageVersion%i_%v)会被用作默认的输出文件名前缀(Output=)。
  • RepartDirectories=systemd-repart的分区定义配置文件目录,默认为工作目录下的mkosi.repart/
    • 注意,MKOSI的分区配置文件中的CopyFiles=的源路径会使用镜像中的根文件系统。
  • SectorSize=systemd-repart的块大小。
  • Seed=systemd-repart使用的UUID,也可以使用mkosi.seed文件保存,默认值为random
  • Overlay=:该配置项必须和BaseTrees=一起使用,是否仅仅保留相对于Base Tree发生改变的内容,用于构建系统扩展镜像。
    • 在构建系统扩展镜像时,可以使用该选项结合BaseTree=指定的目录以获取增量部分,即扩展镜像的部分。
  • CleanScripts=:用于构建的最后阶段进行清理的脚本,默认为mkosi.clean
  • OciLabels=:仅用于OCI镜像,配置OCI标签,OCI标签没有明确要求,完全是用户自定义的元数据,大家一般遵循org.opencontainers.image.*作为前缀。
  • OciAnnotations=:仅用于OCI镜像,配置OCI Annotation。默认情况下,MKOSI会自动设置io.systemd.mkosi.version,如果配置了ImageVersion=,那么还会自动设置org.opencontainers.image.version
    • 一些常见的Annotation包括:
    • org.opencontainers.image.created:镜像创建时间。
    • org.opencontainers.image.authors:作者。
    • org.opencontainers.image.url:主页URL。
    • org.opencontainers.image.documentation:文档。
    • org.opencontainers.image.source:源代码。
    • org.opencontainers.image.version:版本号。

[Build]

  • ToolsTree=:在指定目录下搜索构建工具组,也可以使用mkosi.tools/目录保存,这有助于使用其他版本的构建工具组。
    • 默认情况下,在构建不同发行版时会自动使用Tools Tree。
    • 如果值为空,那么任何情况下都不会使用Tools Tree。
    • 如果值为yesdefault,那么会从指定的发行版的软件源中自动获取Tools Tree。在目标发行版的systemd版本低于构建所需的最低要求时,会自动启用。
    • 在当前发行版低于构建所需的最低systemd版本时,该选项必须启用。
    • 在宿主机没有安装构建时必需的工具(如reprepo)时,启用该选项可以避免在宿主机安装软件包。
    • ToolsTreeDistribution=:用于自动获取ToolsTree=的目标发行版。
    • ToolsTreeRelease=:用于自动获取ToolsTree=的目标发行版版本。
    • ToolsTreeMirror=:用于自动获取ToolsTree=的目标发行版软件源。
    • ToolsTreeRepositories=:和Repositories=一样,但是用于构建ToolsTree=
    • ToolsTreeSandboxTrees=:和SandboxTrees=一样,但是用于构建ToolsTree=
    • ToolsTreeProfiles=:在ToolsTree=中启用的Profile,默认启用除了develgui之外的所有Profile,逗号分隔,可选值有:
      • misc:用于脚本的各种杂项工具。
      • package-manager:包管理器及其附属的各种工具。
      • runtime:用于boot镜像时所需的各种工具。
      • gui:用于VM的GUI控制台的工具。
      • devel:用于Debug的各种工具。
    • ToolsTreePackages=:在ToolsTree=中包含的额外软件包。
    • ToolsTreeCertificates=:是否使用Tools Tree中的证书,这包括/etc/pki/etc/ssl/etc/ca-certificates/var/lib/ca-certificates,如果禁用,使用主机上的证书。
    • 你也可以使用mkosi.tools.conf文件或包含若干配置文件的mkosi.tools.conf/目录来对ToolsTree进行细粒度的配置(但不允许配置Distribution=Release=Mirror=。配置文件中接受的配置项和主镜像基本上一致。
  • Incremental=:渐进构建,可选值为布尔值或strict。在执行mkosi.build脚本之前提供镜像缓存,这可以显著提高构建速度。
    • 如果为strict,那么如果缓存镜像不存在则会报错退出。
    • 每个缓存都会保存一个Manifest,在重新缓存时,会对其进行对比并确定缓存是否可用,Manifest保存的元数据包括:
      • Distribution=
      • Release=
      • Mirror=
      • Repositories=
      • Architecture=
      • Snapshot=
      • 包管理器
      • Packages=
      • BuildPackages=
      • PackageDirectories=
      • Overlay=
      • PrepareScripts=,不包括任何其他脚本
  • CacheOnly=:纯缓存模式,可选值有autometadataalwaysnever,如果为always,那么包管理器不会联网,总是只使用本地缓存;如果为metadata,那么包管理器不会更新元数据,但是可以下载软件包;如果为auto,那么仅在不存在本地缓存时更新元数据。
  • SandboxTrees=:接受逗号分隔的,冒号绝对路径对。前者是主机上的路径,后者是MKOSI构建沙箱内的路径,表示在执行构建之前拷贝到构建沙箱的文件。也可以使用mkosi.sandbox/目录进行保存。
  • WorkspaceDirectory=:临时文件的保存目录,默认为$XDG_CACHE_HOME$HOME/.cache/var/tmp
  • CacheDirectory=Incremental=yes时镜像缓存的保存目录,可以直接使用工作目录下的mkosi.cache/(如果存在)。
  • PackageCacheDirectory=:为该镜像特别设置软件包管理器的缓存目录,可以直接使用工作目录下的mkosi.pkgcache/(如果存在),默认情况下使用系统缓存目录/var/cache/~/.cache/
  • BuildDirectory=:用于保存树外构建中间产物的目录,这使得构建的中间产物可以在多次构建时共享,实现增量构建,在构建脚本中可以通过$BUILDDIR访问。默认为mkosi.builddir/
  • UseSubvolumes=:是否在构建过程中使用Btrfs子卷和快照,这可以显著提高Base Tree和Cached Tree的拷贝速度,同时,如果Format=directory,那么最终产物会保持为一个Btrfs子卷,可选值有布尔值或auto
  • RepartOffline=:是否在不使用回环设备的情况下构建磁盘镜像。默认情况下为yes
    • 如果禁用,那么systemd-repart必须具有特权,因此构建只能通过root用户执行。
    • 只有两种情况下必须禁用:
      1. 使用了Btrfs,而且在mkosi.repart/配置中设置了Subvolumes=。因为Btrfs子卷必须通过回环设备创建。
      2. 另一种是以XFS为根文件系统,而且启用了SELinux。因为必须通过回环设备才能确保SELinux扩展属性被写入XFS文件系统。
  • History=:是否保存最新一次构建的历史快找,历史会被保存在.mkosi-private/history/latest.json
  • ExtraSearchPaths=:用于搜索工具的额外目录,会在构建时被添加到$PATH中。
    • 如果你使用了ToolsTree=,那么这个配置项的路径代表着ToolsTree内的路径。你必须在ToolsTree中安装你的额外工具(通过mkosi box命令,例如mkosi box -- meson setup build)。
  • ProxyUrl=:使用的代理地址。
    • ProxyExclude=:不使用代理的主机名。
    • ProxyPeerCertificate=:代理Peer的证书。
    • ProxyClientCertificate=:代理客户端的证书。
    • ProxyCientKey=:代理客户端的Key。
  • WithTests=:设为假,则在执行mkosi.build脚本时,$WITH_TESTS变量为0。
  • WithNetwork=:在执行mkosi.buildmkosi.postinstmkosi.finalize脚本时提供网络。
  • MakeScriptsExecutable=:是否自动将脚本设为可执行,默认值为no

[Content]

镜像配置:

  • Packages=:包含的包,使用逗号或是换行符分隔。会使用该发行版对应的包管理器进行安装。
  • VolatilePackages=:包含的包,但是这些包不受Incremental=影响,不会被缓存。
  • BuildPackages=:在构建时,使用Overlay的方式安装软件包。这样的包不会在镜像中保留,应当用于mkosi.build.chroot脚本。
  • PackageDirectories=:读取指定目录下的额外软件包,并允许在构建时通过Packages=进行安装。也可以使用mkosi.packages/目录保存。
  • VolatilePackageDirectories=:读取指定目录下的额外软件包,并允许在构建时通过Packages=进行安装,这些包不受Incremental=影响,不会被缓存。
  • WithRecommends=:是否安装弱依赖,仅适用于aptdnfzypper。默认禁用。
  • WithDocs=:是否在镜像内保留文档。默认启用。
  • MakeInitrd=:是否在镜像内创建/init(指向/usr/lib/systemd/systemd)和/etc/initrd-release文件。该配置项通常在MKOSI-INITRD内部使用。
  • BaseTrees=:接受逗号分隔的,冒号绝对路径对。前者是主机上的路径,后者是镜像内的路径,表示在获取发行版基本根文件系统之前,将主机文件/目录拷贝到镜像内。这也是Overlay=yes参考的基础文件系统。如果没有指定镜像内的路径,那么默认为/
    • 设置该配置项会导致MKOSI跳过自动构建基本的Base Tree。
    • 注意!如果自行提供Base Tree,那么必须在Base Tree,或者Skeleton Tree,或者mkosi.prepare脚本中提供/usr/lib/os-release
  • SkeletonTrees=:接受逗号分隔的,冒号绝对路径对。前者是主机上的路径,后者是镜像内的路径,表示在已经构建基本根文件系统,开始安装软件包之前,将主机文件/目录拷贝到镜像内。
    • mkosi.skeleton/目录下的文件总是会被拷贝到相同的目标位置下。
  • ExtraTrees=:接受逗号分隔的,冒号绝对路径对。前者是主机上的路径,后者是镜像内的路径,表示在安装软件包之后,将主机文件/目录拷贝到镜像内。
    • mkosi.extra/目录下的文件总是会被拷贝到相同的目标位置下。
  • RemovePackages=在构建构建后删除指定的包
  • RemoveFiles=:在构建后删除指定通配符匹配的文件,路径使用镜像内的根文件系统。
  • CleanPackageMetadata=:是否在构建后清理镜像内包管理器的缓存。默认为auto,表示仅在镜像类型为directorytar时启用。
    • Apt:/var/lib/apt/var/cache/apt/var/lib/dpkg
    • DNF:/var/lib/dnf/var/cache/dnf/var/lib/rpm/usr/lib/sysimage/rpm
    • DNF5:/var/lib/libdnf5/var/cache/libdnf5/var/lib/rpm/usr/lib/sysimage/rpm
    • Zypper:/var/lib/zypp/var/cache/zypp/var/lib/rpm/usr/lib/sysimage/libdnf5/usr/lib/sysimage/rpm
    • Pacman:/var/lib/pacman/var/cache/pacman
    • Apk:/var/lib/apk/var/cache/apk
  • Bootable=:镜像是否可引导(是否安装引导程序),可选值除了布尔值还有auto。默认为auto,即仅在安装了Bootloader软件包时才安装。如果设为yes,那么在Bootloader=设置的内核和Bootloader软件包不存在时,构建会失败。
    • 如果设为yes,镜像内必须安装对应架构的内核和引导程序。
    • 设为no时,如果设置了Format=esp,那么仍然会在ESP中安装引导程序,但是不会在ESP中安装UKI或内核+Initrd文件
    • Bootloader=:使用的引导程序,可选noneukisystemd-bootgrubsystemd-boot-signedgrub-signeduki-signed。默认为systemd-boot
      • 如果Bootable=no,那么该配置项无意义。
      • 如果使用none,那么和Bootable=no的效果大体上一致,但存在细微差异。
        • Bootable=yes+Bootloader=none时总是会检查构建过程中是否安装了内核,没有的话会导致构建失败。
        • Bootable=yes+Bootloader=none时总是会尝试生成Initrd。
        • Bootable=yes+Bootloader=none时总是会自动创建ESP分区。
        • Bootable=yes+Bootloader=none时总是会设置KERNEL_INSTALL_BYPASS=1环境变量。
        • Bootable=yes+Bootloader=none时不会设置BOOT_ROOT=/boot环境变量。
        • Bootable=yes+Bootloader=none时不会为apt设置INITRD=No
        • Bootable=yes+Bootloader=none时如果启用了SecureBoot仍然会进行检查密钥。
      • 如果使用grub,那么镜像中必须安装对应架构的grub软件包,确保存在/usr/lib/grub/usr/share/grub2目录,宿主机(或Tools Tree)中必须存在grub-mkimage程序(通常由grub-common包提供)。MKOSI会将GRUB的EFI程序安装到构建过程中的/efi目录,为GRUB生成配置文件/efi/grub/grub.cfg(除非用户明确提供了mkosi.extra/efi/)。此外,MKOSI还会生成一个/efi/EFI/发行版名称/grub.cfg配置文件,它的内容实际上是加载/efi/grub/grub.cfg
      • 如果使用systemd-boot,那么镜像中必须安装对应架构的systemd-boot软件包,确保存在bootctl程序和systemd-boot的EFI程序。MKOSI会将systemd-boot的EFI程序安装到构建过程中的/efi目录,在不使用UKI的情况下,MKOSI会为systemd-boot生成标准的Type1引导项文件/efi/loader/entries/Token-内核版本.conf
      • 如果使用uki,那么不会在ESP中安装任何其他引导程序,而是直接将UKI安装到构建过程中的/efi目录(准确地说,是/efi/EFI/BOOT/BOOT*.EFI如果启用了Shim,那么是/efi/EFI/BOOT/grub*.EFI),作为EFI Boot Manager的引导项使用。
      • *-signed表示使用对应的Bootloader的已签名版本。MKOSI会认为对应的Bootloader已经签名,在SecureBoot=yes的情况下,不会再对Bootloader进行签名。
    • BiosBootloader=:是否安装BIOS引导,可选nonegrub,如果启用,那么需要创建大小为1MB,类型为21686148-6449-6e6f-744e-656564454649Hah!IdontNeedEFI)的biosboot分区。默认为none
      • 如果Bootable=noFormat=不是disk,那么该配置项无意义。
      • 如果使用grub,那么宿主环境中必须安装i386架构的grub软件包(grub-pc-bingrub-common),确保提供了grub-bios-setupgrub-mkimage命令行工具,特别注意,AArch64架构上没有这个软件包
    • ShimBootLoader=:是否安装Shim,可选noneunsignedsigned。在SecureBoot=yes的情况下,如果为unsigned,那么MKOSI会安装未签名的Shim,并自行对其进行签名,如果为signed,那么MKOSI会安装已签名的Shim。默认为none
      • 如果设为unsigned,那么镜像内必须安装shim-unsigned软件包。
      • 如果设为signed,那么镜像内必须安装shim-signed软件包。
      • 如果设为signed,那么除非Bootloader=明确指定*-signed,否则默认情况下不会使用已签名的Bootloader。
      • 在没有启用SecureBoot=true的情况下,该选项没有意义。
  • KernelModules=:在镜像中包含的内核模块,格式为相对于/usr/lib/modules/内核版本/kernel/的子目录的通配模式(即*?[-])。这对于Initrd特别有用。
    • 默认包含所有内核模块。
    • 匹配所有以.ko.ko.gz.ko.xz.ko.zst结尾的模块文件。
    • -开头的行会被视为反选。
    • 没有以/开头的行会被递归地在所有子目录中匹配(相当于隐式地在行首添加了一个*/)。
    • /结尾的行会被递归匹配其所有的子目录(相当于隐式地在行尾添加了一个*)。
    • 关键词default表示包含MKOSI-INITRD的mkosi.conf中的所有默认配置,参见mkosi.conf。这也是未设置该配置项的默认值。MKOSI-INITRD中的配置是systemd开发组在开发ParticleOS时在不同硬件平台上测试得到的,目前仍在更新,因此可能会有缺失。
    • 关键词host表示包含宿主机当前加载的所有模块。
    • KernelModulesIncludeHost=:是否在镜像中包含宿主机当前加载的所有模块。这等价于KernelModulesInitrdInclude=host
    • 该配置项仅对没有启用Overlay=yes的镜像(也就是说,不是Sysext、Confext和Portable Service的镜像)有意义。
    • 值得参考的通用Initrd模块内容:Ubuntu Core的选择
    • KernelModulesInclude=:(已过时)在镜像中包含的内核模块,格式为相对于/usr/lib/modules/内核版本/kernel目录的,re.search()风格的路径正则表达式。
      • 该配置项的优先级高于KernelModulesExclude=,且在没有设置KernelModulesExclude=的情况下没有意义,因为MKOSI默认情况下总是包含所有的内核模块。
    • KernelModulesExclude=:(已过时)在镜像中排除的内核模块,格式为相对于/usr/lib/modules/内核版本/kernel目录的,re.search()风格的路径正则表达式。
      • 该配置项默认为空。
  • FirmwareFiles=:在镜像中包含的固件,格式为相对于/usr/lib/firmware/目录的通配模式。即使没有任何内核模块依赖这些固件,MKOSI也会强制将其包含到镜像内。
    • FirmwareInclude=:(已过时)在镜像中包含的固件,格式为相对于/usr/lib/firmware/目录的,re.search()风格的路径正则表达式。即使没有任何内核模块依赖这些固件,MKOSI也会强制将其包含到镜像内。
    • 该配置项的优先级高于FirmwareExclude=
    • FirmwareExclude=:(已过时)在镜像中排除的固件,格式为相对于/usr/lib/firmware/目录的,re.search()风格的路径正则表达式。即使有内核模块依赖这些固件,MKOSI也会强制将其排除掉。
    • 不存在FirmwareFilesInitrd=,这是故意的,用户应当通过mkosi.images/子镜像功能实现更细粒度的Initrd自定义。

Initrd配置:

  • Initrds=禁用自动构建的默认Initrd,使用用户提供的指定Initrd,在构建时,MKOSI会取消构建Default Initrd,转而使用这里提供的Initrd。
    • MKOSI会在推测镜像可引导(安装了内核包、Format=disk)时自动构建默认Initrd,将该配置项置为空表示禁用内置的检测逻辑,强制禁用默认Initrd构建。
    • 特殊值default表示构建内置的默认Initrd(Default Initrd)。默认情况下,如果你没有指定Initrds=为其他特定的镜像,会自动触发构建。
    • 你也可以在构建时使用$ARTIFACTDIR/io.mkosi.initrd目录保存流程中构建的Initrd,这里的Initrd会被追加到Default Initrd或用户提供的Initrd中。
    • 如果要在构建时使用其他方式构建Initrd,应该在mkosi.images/目录下创建一个子镜像,并将该配置项设为Initrds=%O/子镜像名称
  • InitrdProfile=:启用的额外Default Initrd配置,默认不启用。
    • lvm:LVM支持。
    • raid:RAID支持。
    • pksc11:PKSC11智能卡支持。
    • plymouth:Plymouth Logo支持。
    • nfs:NFS上的根文件系统支持。
    • 你也可以使用mkosi.initrd.conf文件或包含若干配置文件的mkosi.initrd.conf/目录来对ToolsTree进行细粒度的配置(但不允许配置Distribution=Release=Mirror=。配置文件中接受的配置项和主镜像基本上一致。
  • KernelModulesInitrd=:在构建可引导(Bootable=true安装了内核包)的镜像时,MKOSI会先读取MKOSI-INITRD的配置,构建一个目标发行版的,不包含任何模块的默认Initrd(Default Initrd)(注意了!MKOSI-INITRD的构建配置中没有包含内核模块包,所以不会包含任何内核模块),然后,该配置项用于确定是否需要构建一个额外的,包含特定的当前正在构建的镜像的内核模块的Initrd(Kernel Modules Initrd)(即,根据KernelModulesInitrdInclude=KernelModulesInitrdExclude=配置项构建的Initrd),并将其合并到默认Initrd中。默认启用。
    • 如果你的镜像中没有安装内核包,那么你必须单独构建一个default-initrd子镜像才会触发自动构建Default Initrd,子镜像的构建配置中只需要包含Include=mkosi-initrd,然后在主镜像中使用Initrds=%O/initrd.cpio.zst
    • 在启用了UKI(UnifiedKernelImages!=none)时,禁用该配置项有助于用户根据自己的需要构建出更通用的UKI镜像,并使用Format=addon构建扩展Initrd进行补充。
    • 在用户使用自己提供的Initrd(Initrds=),且自己提供的Initrd中包含内核模块时,也可以考虑禁用该配置项。
    • 在未启用UKI时,该配置项没有意义,因为此时总是会构建默认Initrd和内核模块Initrd,并进行合并。
  • KernelInitrdModules=:在Initrd中包含的模块,格式为相对于/usr/lib/modules/内核版本/kernel/目录的通配模式。
    • 默认包含所有内核模块。
    • 关键词default表示包含MKOSI-INITRD的mkosi.conf中的所有KernelModules=配置,参见mkosi.conf。这也是未设置该配置项的默认值。
    • 关键词host表示包含宿主机当前加载的所有模块。
    • KernelModulesInitrdIncludeHost=:是否在Initrd中包含宿主机当前加载的所有模块。这等价于KernelModulesInitrdInclude=host
    • KernelModulesInitrdInclude=:(已过时)在Initrd中包含的模块,格式为相对于/usr/lib/modules/内核版本/kernel目录的,re.search()风格的路径正则表达式。
      • 该配置项的优先级高于KernelModulesInitrdExclude=,且在没有设置KernelModulesInitrdExclude=的情况下没有意义,因为MKOSI默认情况下总是包含所有的内核模块。
    • KernelModulesInitrdExclude=:(已过时)在Initrd中排除的模块,格式为相对于/usr/lib/modules/内核版本/kernel目录的,re.search()风格的路径正则表达式。
      • 因为MKOSI默认会加载mkosi-initrd的配置(KernelModulesInitrdInclude=default),因此如果希望重设模块,需要先设置KernelModulesInitrdExclude=.*
      • 该配置项默认为空。
  • UnifiedKernelImages=:是否使用UKI而不是独立Initrd,接受的值有noneunsignedsignedauto。默认为auto,即尽可能使用。
    • 如果Bootloader=*-signed,那么除非UnifiedKernelImages=被明确设为unsignednone,构建时会需要在/usr/lib/modules/内核版本/目录下提供一个预构建的、已签名的UKI(例如发行版提供的,包含已签名的UKI的软件包),否则构建会失败。UnifiedKernelImages=unsigned会触发在本地执行ukify并构建UKI,这在设置了Firmware=custom的情况下很有用。
    • 如果UnifiedKernelImages=不为noneauto,那么镜像中必须安装systemd-boot-efi(或其他提供/usr/lib/systemd/boot/efi/*.efi.stub桩程序的软件包)。
    • 默认内核命令行参数逻辑
      • 如果启用了UKI:如果systemd-repart发现了存在哈希值(即,启用了Verity)的根文件系统分区,那么会自动将其哈希值写入UKI的roothash=内核命令行参数中。否则,如果systemd-repart发现了存在哈希值(即,启用了Verity)的/usr/文件系统分区,那么会自动将其哈希值写入UKI的usrhash=内核命令行参数中
      • 如果未启用UKI,且使用systemd-boot:如果systemd-repart发现了存在哈希值(即,启用了Verity)的根文件系统分区,那么会将其哈希值写入BLS引导条目的roothash=内核命令行参数中。否则,如果systemd-repart发现了存在哈希值(即,启用了Verity)的/usr/文件系统分区,那么会将其哈希值写入BLS引导条目的usrhash=内核命令行参数中。否则,如果找到了GPT PARTTYPE为root*的分区,那么会将其PARTUUID写入BLS引导条目的root=PARTUUID=内核命令行参数中。
      • 如果未启用UKI,且使用GRUB:如果systemd-repart发现了存在哈希值(即,启用了Verity)的根文件系统分区,那么会将其哈希值写入grub.cfgroothash=内核命令行参数中。否则,如果systemd-repart发现了存在哈希值(即,启用了Verity)的/usr/文件系统分区,那么会将其哈希值写入grub.cfgusrhash=内核命令行参数中。否则,如果找到了GPT PARTTYPE为root*的分区,那么会将其PARTUUID写入grub.cfgroot=PARTUUID=内核命令行参数中。否则,如果找到了GPT PARTLABEL为usr*的分区,那么会将其PARTUUID写入grub.cfgmount.usr=PARTUUID=内核命令行参数中。
    • UnifiedKernelImageFormat=:UKI文件命名,默认为&e-&k,支持的占位符有:
      • &&&本身。
      • &e:Token。
      • &k:内核版本。
      • &hroothash=usrhash=的值。
      • &c:引导尝试次数。
    • UnifiedKernelImageProfiles=:为构建的UKI添加配置(即.profile段),接受逗号分隔的配置文件。也可以保存在mkosi.uki-profiles/目录下。配置以.conf结尾,只有一个UKIProfile段。请注意,每个UKI配置在使用时都会被追加UKI的基础内核命令行参数。可用内容有:
      • Profile=:配置信息,包括ID(必须)和TITLE
      • Cmdline=:内核命令行参数。
      • SignExpectedPcr=:是否为该配置进行PCR签名。默认启用。
      • 部分UEFI固件实现会限制PE段(Section)数量,因此可能无法接受过多的UKI配置文件。
    • PeAddons=:(已被移除)构建额外的UKI插件,接收逗号分隔的ukify配置文件,额外的PE插件会以配置文件名.addon.efi命名,也可以将ukify配置文件保存在mkosi.pe-addons/目录下。
      • 该配置项现在已经废弃,为MKOSI-ADDON让位。
  • Splash=:是否在Initrd中添加海报。
  • MicrocodeHost=:是否在Initrd中仅仅包含主机的CPU微码。默认禁用。
  • InitrdPackages=:在MKOSI自动构建的默认Initrd中额外安装的软件包。
  • InitrdVolatilePackages=:在MKOSI自动构建的默认Initrd中额外安装的软件包。但是这些包不受Incremental=影响,不会被缓存。
  • KernemModulesInitrd=:是否额外导出独立的模块Initrd,该Initrd只包含UKI中的内核模块部分,默认启用。
  • KernelCommandLine=:封装在UKI或Initrd内的内核命令行参数。
    • 如果检测到使用了Verity的root分区或usr分区,那么会自动填充roothash=分区Hash值usrhash=分区Hash值
    • 内核命令行参数root=PARTUUIDmount.usr=PARTUUID会被自动填充类型为root/usr的分区的UUID。

镜像预设配置,这些配置会通过systemd-firstboot的对应选项传递,还会写入到镜像内的/usr/lib/credstore目录下:

  • MachineID=:设置镜像内的机器ID,默认为random,也可以使用mkosi.machine-id文件保存。
  • Locale=:镜像内的语言。
    • 对于Arch、Debian这类使用locale-gen工具在本地编译语言的发行版来说,如果/etc/locale.gen内配置了语言(通过sed -i '/^# 语言.UTF-8.*/c\语言.UTF-8 UTF-8' /etc/locale.gen),那么在构建时会自动执行locale-gen
  • Keymap=:镜像内的Keymap。
  • Hostname=:设置镜像内的默认主机名。
  • RootShell=:Root用户的默认Shell。
  • RootPassword=:设置镜像内Root用户的默认密码。
    • 你也可以使用mkosi.rootpw文件指定密码,该文件还可以是一个可以执行的Shell脚本,使用其输出作为密码。
  • Autologin=:是否设置Root用户在/dev/pts/0/dev/tty1/dev/hvc0的自动登录。
  • Ssh=:是否在镜像内创建SSH套接字单元以允许通过mkosi ssh连接,使用mkosi genkey创建密钥后才可以使用mkosi ssh。可选值有:
    • auto:如果镜像中存在sshd且不存在systemd-ssh-generator,则创建单元。
    • always:总是创建单元。
    • never:总是不创建单元。
    • runtime:不配置单元,但是在运行时仍然进行SSH密钥的检测。
  • SELinuxRelabel=:是否在镜像内进行Relabel。

为了实现更多功能,还可以在构建镜像的不同过程中执行各种钩子脚本脚本:

  • SyncScripts=:在开始构建镜像之前,在宿主环境中执行的脚本,该脚本有网络连接,用于更新需要在镜像中进行构建的各种源代码,注意该脚本的更新操作不会破坏BuildSourcesEphemeral=,默认为mkosi.sync
  • PrepareScripts=:该脚本会首先在镜像中安装软件包后,以final位置参数被执行一次(执行的操作会被保留在镜像中),然后在创建构建Overlayfs之后以build位置参数再被执行一次(执行的操作不会被保留在镜像中)。该脚本被执行时有网络连接,因此可以用于安装软件包。默认为mkosi.prepare
    • 注意!如果自行提供Base Tree,那么必须在Base Tree,或者Skeleton Tree,或者mkosi.prepare脚本中提供/usr/lib/os-release
  • BuildScripts=:在准备好构建环境后,在宿主环境或镜像中执行的脚本,该脚本可以具有网络连接,取决于WithNetwork=的值。默认为mkosi.build
    • BuildSources=:进行构建时拷贝的代码树目录,接受逗号分隔的,冒号绝对路径对。前者是主机上的路径(默认为.),后者是沙盒内的路径(默认为同名路径),镜像内的路径总是会自动添加/work/src前缀。默认情况下值为.,即将当前工作目录完全拷贝到沙盒的/work/src
    • BuildSourcesEphemeral=:是否不保留对源码树的更改,如果启用,那么除了mkosi.sync以外的所有脚本对BuildSources=的修改都不会保留。如果设为buildcache,那么mkosi.build脚本的修改产生的Overlay会被保留在BuildDirectory=中,其他脚本的修改仍然会被抛弃,默认禁用。
  • PostInstallationScripts=:在配置完Extra Tree后,在宿主环境或镜像中执行的脚本,该脚本可以具有网络连接,取决于WithNetwork=的值,默认为mkosi.postinst
  • FinalizeScripts=:在Overlay的下层文件系统(即Base Tree)已经移除后,在宿主环境或镜像中执行的脚本,该脚本可以具有网络连接,取决于WithNetwork=的值,默认为mkosi.finalize
  • PostOutputScripts=:在输出产品后,在宿主环境或镜像中执行的脚本,该脚本没有网络连接,默认为mkosi.postoutput
  • Environment=:给脚本传递的环境变量。
    • MKOSI_LESS:使用指定的分页器,而非less
    • $MKOSI_DNF:指定使用的dnf
    • EPEL_MIRROR:指定EPEL的镜像站。
    • MKOSI_DNF_DISABLE_PLUGINS:指定dnf禁用的插件。
    • MKOSI_DNF_ENABLE_PLUGINS:指定dnf启用的插件。
  • EnvironmentFile=:从文件给脚本传递环境变量。

[Validation]

  • SecureBoot=:是否启用安全启动,这会影响是否对systemd-boot(或是UKI)进行签名。
    • SecureBootKey=:使用的安全启动PEM密钥。默认使用mkosi.key
    • SecureBootCertificate=:使用的安全启动X509证书。默认使用mkosi.crt
    • SecureBootSignTool=:为PE文件进行安全启动签名的程序,可选systemd-sbsignsbsignauto
  • SecureBootAutoEnroll=:是否自动导入安全启动的Key。
    • 这仅对虚拟机生效,如果希望对物理机生效,在Extra Tree中包含/efi/loader/loader.conf文件,并写入secure-boot-enroll forcesecure-boot-enroll manual
  • Verity=:是否为扩展镜像启用Verity,可选值有signedhashdeferauto或布尔值。
    • 如果设为signedyes,那么必须提供VerityKey=VerityCertificate=,从而让镜像在构建时创建独立的签名分区,否则构建会失败。
    • 如果设为hash,那么MKOSI会启用Verity,但是不会在镜像中创建签名分区。
    • 如果设为defer,那么MKOSI会启用Verity,在镜像中预留出签名分区的空间,但是不实际创建。
    • 如果设为no,那么MKOSI会禁用Verity。
    • 如果设为auto,那么MKOSI会在提供了提供VerityKey=VerityCertificate=时启用Verity并转为signed模式,否则禁用Verity。这是默认值。
    • VerityKey=:用于为Verity设备签名的PEM密钥。默认使用mkosi.key
    • VerityCertificate=:用于为Verity设备签名的X509证书。默认使用mkosi.crt
  • SignExpectedPcr=:使用systemd-measure在UKI中包含PCR(平台配置寄存器)的签名,可选布尔值或auto,如果设为auto(默认值),那么会根据systemd-measure的存在性判断是否包含签名。
    • SignExpectedPcrKey=:签名使用的PEM密钥。默认使用mkosi.key
    • SignExpectedPcrCertificate=:签名使用的X509证书。默认使用mkosi.crt
  • Passphrase=:LUKS加密的密钥文件,必须是600权限位,它应当保存完整的LUKS密钥,以一个换行结尾(即/etc/crypttab文件的格式)。
    • 也可以选择在工作目录下创建mkosi.passphrase文件。
  • Checksum=:指定镜像的校验码。
    • Sign=:是否用gpg给校验码签名。默认为否。
    • OpenPGPTool=:签名使用的OpenPGP实现,默认为gpg,可选值有sqoprsop
    • Key=:给校验码签名的密钥。

[Runtime]

通用:

  • Register=:是否向systemd-machined注册,以允许通过machinectl进行管理,默认启用。
  • Machine=:机器名,用于systemd-machined注册的名称。
  • Credentials=:启动VM/容器时传递的systemd的系统凭据。
  • Ephemeral=:是否将镜像设为暂态,所有操作在关闭之后都会撤销。
  • RuntimeTrees=接受逗号分隔的,冒号绝对路径对。前者是主机上的路径,后者是虚拟机内的路径,如果没有后面的部分,则自动挂载在/root/src目录下。
  • RuntimeSize=:将镜像在启动时扩大到指定大小。
  • RuntimeNetwork=:使用的网络模式,可选值有userinterfacenoneinterface依赖systemd-networkd配置网络接口。默认值为user
  • RuntimeBuildSources=:挂载指定的源代码目录到虚拟机内的/work目录下。
  • RuntimeHome=:是否挂载主机的家目录到虚拟机的/root
  • SshKey=:使用的SSH私钥。
  • SshCertificate=:使用的SSH证书。
  • ForwardJournal=:是否将容器或虚拟机的日志发送到宿主机。
  • SysupdateDirectory=mkosi sysupdate使用的配置目录,默认使用mkosi.sysupdate/
    • MKOSI使用的Sysupdate配置必须设置PathRelativeTo=explicitPath=/才能正常使用sysupdate子命令进行更新,这会使得配置中的Path=使用mkosi.output/(或OutputDirectory=指定的目录)作为根。
    • 这同时也意味着,SplitArtifacts=必须启用以对分区进行更新,而且更新内容(分区镜像文件、EFI程序、Initrd)文件名必须与Sysupdate配置中的MatchPattern=匹配。
  • StorageTargetMode=:是否在serve时使用systemd-storagetm提供镜像服务。可选值有auto或布尔值。如果设为auto,那么会自动检测systemd-storagetm的存在性,以及当前构建产品是否为disk格式,如果都满足时,才会启用。

容器配置:

  • NSpawnSettings=:使用systemd-nspawn启动时,读取的.nspawn单元文件。
    • 默认使用mkosi.nspawn文件。
  • UnitProperties=:传递给NSPawn的Scope属性。

所有虚拟机配置项都仅适用于使用vm启动的虚拟机:

  • VirtualMachineMonitor=:使用的虚拟机超管,可选值有qemuvmspawn。默认值为qemu
  • Console=:虚拟机终端的接入模式。可选值有:interactive交互文本终端,read-only只读的交互文本终端,nativeQemu原生的交互文本终端,gui使用图形界面。
    • gui需要在ToolsTree中额外安装qemu-system-guipipewire。添加QemuArgs=-full-screen可以让Qemu进入全屏模式。
  • RAM=:设置Qemu的内存。默认为2G。
  • KVM=:是否启用KVM加速,默认为auto
  • VSock=:是否启用VSock,默认为auto
  • VSockCID=:VSock的连接ID,可选值有hashauto或手动输入。
  • TPM=:是否启用TPM,默认为auto
  • Removable=:是否以可插拔形式接入根文件系统镜像,默认禁用。
  • Firmware=:使用的Qemu固件,可选uefi-secure-bootuefilinuxlinux-noinitrdauto。默认值为auto,表示按顺序尝试使用uefi-secure-bootlinux
    • Linux=:指定使用的Direct Boot Kernel。
    • FirmwareVariables=:设置UEFI固件中的变量。可选值有:microsoft导入包含Microsoft安全启动证书的固件变量,microsoft-mok导入包含额外安全启动证书的固件变量,custom导入SecureBootCertificate=指定的安全启动证书,或是一个路径。变量文件可以通过virt-fw-vars工具生成。
  • Drives=:添加的驱动器,格式为ID:大小[:目录:选项:文件ID:标记位]。标记位支持的值有:
    • persist:为磁盘启用持久化。
  • QemuArgs:配置空格或换行符分隔的Qemu原生参数。
  • KernelCommandLineExtra=:通过Qemu传递的额外内核命令行参数。
    • 在Qemu中硬编码的额外内核命令行参数为(mkosi/mkosi/qemu.py):
      • rw
      • systemd.wants=network.target
      • module_blacklist=vmw_vmci systemd.tty.term.hvc0=宿主机终端类型 systemd.tty.columns.hvc0=宿主机终端列数 systemd.tty.rows.hvc0=宿主机终端行数
    • 除非明确地手动进行设置,否则存在以下内核命令行参数的默认值:
      • ip=enc0:any ip=enp0s1:any ip=enp0s2:any ip=host0:any ip=none
      • loglevel=4
      • SYSTEMD_SULOGIN_FORCE=1

分区

MKOSI会从mkosi.repart/RepartDirectories=指定的目录中读取systemd-repart的配置文件,并据此对磁盘镜像进行分区。文件命名应当为优先级-分区名.conf,格式为:

1
2
3
4
5
6
7
[Partition]
Type=分区类型
Format=文件系统
# 是否精简化文件系统
Minimize=best|off|guess
SizeMinBytes=最小大小
SizeMaxBytes=最大大小

默认情况下,MKOSI使用以下内置配置:

00-esp.conf

1
2
3
4
5
6
7
[Partition]
Type=esp
Format=vfat
CopyFiles=/boot:/
CopyFiles=/efi:/
SizeMinBytes=512M
SizeMaxBytes=512M
  • 需要注意的是,CopyFiles=CopyBlocks=的源路径的根是构建中的镜像的根(也就是$DESTDIR)。
  • 创建vfat文件系统需要宿主机(或Tools Tree)中存在mcopy程序(通常由mtools包提供)。

05-bios.conf

1
2
3
4
5
6
[Partition]
# UUID of the grub BIOS boot partition which grubs needs on GPT to
# embed itself into.
Type=21686148-6449-6e6f-744e-656564454649
SizeMinBytes=1M
SizeMaxBytes=1M

10-root.conf

1
2
3
4
5
[Partition]
Type=root
Format=<distribution-default-filesystem>
CopyFiles=/
Minimize=guess

资源目录

有以下相关的资源目录:

  • mkosi.conf.d/:额外的MKOSI配置,可以是文件,也可以是子工作目录,子工作目录支持的内容和MKOSI的主工作目录完全一致(必须包含mkosi.conf文件)。
    • 如果使用文件的话,那么文件中的[Match]段仅对该文件生效。
    • 如果使用子工作目录的话,那么子工作目录中的mkosi.conf中的[Match]段对整个子工作目录生效。
  • mkosi.tools.confmkosi.tools.conf/:额外的ToolsTree配置,可以是文件,也可以是包含配置文件的目录。
  • mkosi.local/:额外的本地MKOSI配置,可以是文件,也可以是子工作目录。
  • mkosi.skeleton/mkosi.skeleton.tar:在已经构建基本根文件系统,开始安装软件包之前,将文件/目录拷贝到镜像内。与SkeletonTrees=配置项有关。
  • mkosi.extra/mkosi.extra.tar:在安装软件包之后,将额外的文件/目录拷贝到镜像内。与ExtraTrees=配置项有关。
  • mkosi.sandbox/mkosi.sandbox.tar:在构建时配置包管理器,但是不将文件写入镜像(这个目录之前被称为mkosi.pkgmngr/)。这可以作为在构建时手动配置软件包管理器的手段,如果需要包含到镜像中,应该使用mkosi.skeleton/mkosi.skeleton.tar
  • mkosi.nspawnsystemd-nspawn读取的容器单元文件。与NspawnSettings=配置项有关。
  • mkosi.cache/Incremental=yes时镜像缓存镜像缓存的保存目录。与CacheDirectory=配置项有关。
  • mkosi.builddir/:用于执行树外构建的构建目录。与BuildDirectory=配置项有关。
  • mkosi.rootpw:镜像中Root用户的密码。文件必须具有0600或更低的权限位。与RootPasswordFile=配置项有关。
  • mkosi.passphrase:LUKS加密使用的密码。文件必须具有0600或更低的权限位。与PassphraseFile=配置项有关。
  • mkosi.crtmkosi.key:用于签名(UEFI SecureBoot、Verity等)的X.509证书和PEM私钥。与SecureBootCertificate=SecureBootKey=VerityCertificate=VerityKey=配置项有关。
  • mkosi.output/:用于保存所有的构建产品。与OutputDirectory=配置项有关。
  • mkosi.credentials/:额外的系统凭据。目录中的每个文件名将用作凭据名,文件内容成为凭据值。与Credentials=配置项有关。
  • mkosi.repart/:用于systemd-repart的分区定义文件,这些文件在构建磁盘镜像时传递给systemd-repart。如果存在mkosi.repart/或是使用了RepartDirectories=,那么就不会使用任何默认的分区定义。与RepartDirectories=配置项有关。
  • mkosi.uki-profiles/:额外的UKI配置文件。与UnifiedKernelImageProfiles=配置项有关。
  • mkosi.sync.dmkosi.prepare.dmkosi.build.dmkosi.postinst.dmkosi.finalize.dmkosi.postoutput.dmkosi.clean.d:各种钩子脚本的保存目录,目录中的脚本应当以.sh为后缀,可以添加额外的.chroot后缀以在镜像内执行。
  • mkosi.pe-addons/(已经废弃):额外的PE插件的ukify配置文件。与PeAddons=配置项有关。

钩子脚本

钩子脚本如果在末尾添加了.chroot后缀(例如,mkosi.postinst.chroot),那么会Chroot到镜像中执行,否则会在宿主环境中执行。

注:

  • “在宿主环境中”意味着使用宿主机提供的命令行工具。
  • “在镜像中”意味着使用镜像中提供的命令行工具。

脚本中有以下可用变量(具体可参考__init__.pyrun_*_scripts函数):

变量 含义 configure sync prepare build postinst finalize postoutput clean
$ARCHITECTURE 镜像的架构
$QEMU_ARCHITECTURE Qemu格式的架构,用于通过qemu-system-$QEMU_ARCHITECTURE获取Qemu程序
$DISTRIBUTION 镜像的发行版
$DISTRIBUTION_ARCHITECTURE 发行版格式的架构
$RELEASE 镜像的版本
$PROFILES Profiles=中设置的配置文件,使用逗号分隔
$BUILDROOT 当前正在构建的镜像的根目录,实际上是/buildroot
$CACHED 如果有可用的缓存,为1
$CHROOT_SCRIPT 当前正在执行的脚本相对于镜像根目录的路径,主要用于mkosi-chroot,实际上是/work/build-script
$SRCDIR 启动mkosi的路径,实际上是/work/src
$CHROOT_SRCDIR 执行mkosi-chroot以后,$SRCDIR所在的路径,实际上也是/work/src
$BUILDDIR 树外构建中间文件目录(mkosi.builddir)相对于镜像根目录的路径,实际上是/work/build/$发行版名称,仅在BuildDirectory=mkosi.builddir/存在时,才会被定义
$CHROOT_BUILDDIR 执行mkosi-chroot以后,$BUILDDIR所在的路径,实际上也是/work/build/$发行版名称,仅在BuildDirectory=mkosi.builddir/存在时,才会被定义
$DESTDIR 用于保存mkosi.build构建时生成的产品的目录,实际上是/work/dest,该目录仅在执行mkosi.build时存在,在mkosi.build脚本执行完成后,该目录下的层次结构会被拷贝到镜像的根文件系统并进行覆盖
$CHROOT_DESTDIR 执行mkosi-chroot以后,$DESTDIR所在的路径,实际上也是/work/dest
$OUTPUTDIR 镜像构建产物的保存目录,实际上是/work/out,注意,在mkosi.build脚本中不可见
$CHROOT_OUTPUTDIR 执行mkosi-chroot以后,$OUTPUTDIR所在的路径,实际上也是/work/out
$PACKAGEDIR PackageDirectories=设定的本地软件包目录,实际上是/work/packagesmkosi.build脚本可以将产品软件包放到该目录
$ARTIFACTDIR 实际上是/work/artifacts,类似于$PACKAGEDIR,但是并非用于软件包管理器,而是用于mkosi内部使用的产品,例如手动构建的Initrd,目前接受io.mkosi.microcodeio.mkosi.initrd两个子目录
$WITH_DOCS 是否设置了WithDocs=
$WITH_TESTS 是否设置了WithTests=
$WITH_NETWORK 是否设置了WithNetwork=
$SOURCE_DATE_EPOCH 如果设置了SourceDateEpoch=TIMESTAMP,那么会保存时间戳
$MKOSI_UID 调用mkosi的用户UID
$MKOSI_GID 调用mkosi的用户GID
$MKOSI_CONFIG 当前镜像配置的JSON
$IMAGE_ID ImageId=的值
$IMAGE_VERSION ImageVersion=的值
$MKOSI_DEBUG Debug=的值

此外,以下命令可用:

  • mkosi-chroot:一个封装的chroot,会自动Chroot到镜像内执行命令。
  • 镜像对应的发行版的包管理器可用,你也可以使用mkosi-install|remove|upgrade|reinstall封装工具。
  • git已经自动配置好safe.directory=*
  • useraddgroupadd已经自动配置好--root=$BUILDROOT

执行流程

创建MKOSI构建沙箱:

  1. 解析命令行选项。
  2. 解析配置文件。
  3. 在宿主环境中执行mkosi.configure脚本。
  4. 检查当前用户是不是Root,如果当前用户不是Root,则尝试根据当前用户的SubUID和SubGID隔离用户命名空间。
  5. 隔离挂载命名空间。
  6. 只读化重新挂载各种目录,包括:
    • /usr
    • /etc
    • /opt
    • /srv
    • /boot
    • /efi
    • /media
    • /mnt

开始构建镜像:

  1. 拷贝Sandbox Tree(mkosi.sandbox/)中的文件到构建沙箱中。
  2. 更新软件包元数据。
  3. 在宿主环境中执行Sync脚本(mkosi.sync)。
  4. 拷贝Base Tree中的文件到镜像中。如果配置了Overlay=yes,那么执行Overlay挂载。
  5. 检查并尝试复用缓存镜像。
  6. 拷贝一份包管理器元数据到镜像内。
  7. 拷贝Skeleton Tree(mkosi.skeleton/)中的文件到镜像中。
  8. 安装发行版基本文件(如果设置了BaseTrees=,则会跳过这一步)。
  9. 安装发行版软件包。
  10. 使用final命令行参数在宿主环境或镜像中执行mkosi.prepare脚本(如果Format=none且没有mkosi.build脚本的话,则会跳过)。
  11. 如果存在mkosi.build脚本的话,在镜像的Overlayfs中安装构建所需的软件包(BuildPackages=)。
  12. 如果存在mkosi.build脚本的话,使用build命令行参数,在已经安装了BuildPackages=的Overlayfs中执行mkosi.prepare脚本。
  13. 如果存在对应的配置(Incremental=)的话,缓存该镜像(如果Format=none且没有mkosi.build脚本的话,则会跳过)。
  14. 如果存在mkosi.build脚本的话,在已经安装了BuildPackages=的Overlayfs中执行mkosi.build脚本。
  15. 如果Format=none的话,结束构建。
  16. 拷贝构建脚本的产物(保存在$DESTDIR目录下)到镜像内。
  17. 拷贝Extra Tree(mkosi.extra/)中的文件到镜像内。
  18. 在宿主环境或镜像中执行mkosi.postinst脚本。
  19. 写入配置项中配置的其他杂项文件。例如Ssh=Autologin=MakeInitrd=
  20. 如果配置了Bootable=yes,安装systemd-boot,并按需配置(SecureBoot=)安全启动。
  21. 如果/etc/locale.gen配置了需要编译的语言,执行locale-gen
  22. 如果没有配置Overlay=yes,执行systemd-sysusers
  23. 如果没有配置Overlay=yes,执行systemd-tmpfiles
  24. 如果没有配置Overlay=yes,执行systemctl preset-all
  25. 如果配置了KernelModules=,删除镜像的模块目录/usr/lib/modules/内不需要的模块。如果配置了FirmwareFiles=,删除镜像的模块目录/usr/lib/firmware/内不需要的固件。
  26. 如果发行版没有提供modules.depmodules.dep.binmodules.symbolsmodules.symbols.bin全部四个文件,执行depmod生成它们。
  27. 如果没有配置Overlay=yes,执行systemd-firstboot
  28. 如果没有配置Overlay=yes,执行systemd-hwdb
  29. 如果安装了SELinux Policy的话,执行SELinux Relabel。
  30. 移除指定的软件包和文件(RemovePackages=RemoveFiles=)。
  31. 如果配置了Overlay=yes,对比Overlay的下层文件系统清理同名文件。
  32. 在宿主环境或镜像中执行mkosi.finalize脚本。
  33. 配置内核,如果存在对应的配置(UnifiedKernelImages=)的话,生成UKI镜像。创建Bootloader配置文件。
  34. 如果Format=disk,导出磁盘文件。
  35. 导出产物到mkosi.output/
  36. 在宿主环境中执行mkosi.postoutput脚本。

最佳实践

  1. 要构建磁盘镜像,你需要这些软件包:btrfs-progs apt dosfstools mtools e2fsprogs squashfs-tools gnupg python3 tar xfsprogs zypper sbsigntool[s] [edk2-]ovmf xz[-utils]
  2. 创建工作目录,并使用mkosi.confmkosi.conf.d/*.conf(或mkosi.conf.d/*/mkosi.conf)配置文件以固化构建信息。
  3. 使用mkosi.postinst.chroot脚本在镜像中进行配置。
  4. 使用mkosi.extra向镜像中传递额外文件。
  5. 使用mkosi.build.chroot脚本进行构建,这个脚本对镜像根文件系统的修改不会保留。在脚本中使用$DESTDIR保存构建产物。
  6. 缓存有两种维度:使用mkosi.pkgcache目录进行软件包缓存,使用mkosi.cache目录保存Incremental=的增量构建镜像。需要重建增量缓存时,使用-ff选项。
  7. 务必谨慎使用mkosi clean:它会清理整个依赖链条上的所有镜像,而非仅仅清理--dependency指定的单个目标。反过来说,通过--dependency指定构建目标在构建阶段是可行的,但是在清理时不能依赖clean,你应该直接执行rm
  8. Format=none且没有mkosi.build脚本的镜像,虽然不会真正执行构建,但是仍然会参与MKOSI的依赖解析、clean范围计算和缓存状态判断。但是,这类镜像又永远不会生成渐进缓存,这就导致在进行缓存一致性判断时,它总是会导致Cache Mismatch。一旦它出现在依赖链条上,就会导致所有其他本来可以复用缓存的子镜像全部被判定为需要清理缓存。在把主镜像设为Format=none、并结合--dependency把子镜像当作Build Target使用时,这一点尤其容易导致反直觉的缓存清理行为。
  9. 要测试不同的内核命令行参数的效果,使用KernelCommandLineExtra=而不是重建镜像,以提高效率。
  10. 可以在无特权的情况下构建磁盘镜像,但是容器文件系统仍然需要Root权限。
  11. 在启用SecureBoot=yes时,可以执行mkosi genkey直接生成证书和密钥,也可以自己创建mkosi.keymkosi.crt。你还可以使用ShimBootloader=yes以安装Shim。
  12. 使用BiosBootloader=grub以为BIOS引导安装GRUB。
  13. 除非你需要调用包管理器或useradd等命令,否则你应该使用没有.chroot后缀的钩子脚本。
  14. 使用useradd -m -g $USER --password "$(openssl passwd -stdin -6 <<< '密码明文')"添加一个普通用户。
  15. 在Debian中mkosi qemu不可用,解决方案为执行cp /usr/lib/tmpfiles.d/static-nodes-permissions.conf /etc/tmpfiles.d/static-nodes-permissions.conf,修改/dev/kvm的权限位为0666
  16. 在非特权模式构建时,chown()调用是被禁用的(钩子脚本中没有禁用,要禁用,设置环境变量MKOSI_CHROOT_SUPPRESS_CHOWN=1),要解决该问题,使用SubUID:unshare --map-auto --map-current-user --setuid 0 --setgid 0
  17. 在你正式构建时,使用WithRecommends=no以安装弱依赖。
  18. 在使用dd克隆产品镜像前,执行truncate -s|--size=文件大小 镜像文件.img以调整磁盘镜像大小。
  19. MKOSI可以负责构建Initrd,因此一个没有Initrd构建工具的镜像是可能的。
    • 不过需要注意的是,MKOSI构建的Initrd中会使用systemd作为/init,这与使用MKOSI-INITRD构建的Initrd是一致的。
  20. 使用mkosi.profiles/保存不同的镜像口味,使用mkosi.conf.d/保存不同情况下的镜像配置,对于多种子场景的情况,将情况相对少且固定的子场景放在父目录(例如,输出格式为容器或VM),将情况相对较多且不固定的子场景放在子目录(例如,版本)。
  21. 使用Format=ukiMakeInitrd=no来创建一个USI(Unified System Image,单个UEFI PE形式的完整系统)。
  22. 在构建自定义发行版时,使用mkosi.skeleton/提供最小化的根文件系统,然后使用mkosi.prepare.chroot脚本安装软件包。
  23. 如果希望通过mkosi.configure脚本动态修改配置,使用jq从标准输入获取JSON,然后进行修改:jq -Mc '. += {"配置项": "值"}',打印到标准输出即可。
  24. 要构建源代码,在BuildPackages=中添加构建需要的软件包,然后用BuildSources=提供源代码,在mkosi.prepare脚本中安装动态的构建时依赖(在命令行参数为final时在镜像中安装,在命令行参数为build时在构建Overlayfs中安装),并使用mkosi.build脚本执行构建,将产物保存到$OUTPUTDIR,通过mkosi.postinst脚本安装产物。
  25. 国内具有debian-debug的镜像站为ISCAS,建议使用。
  26. 如果mkosi vm启动时阻塞,考虑几个问题:磁盘大小是否经过调整(RuntimeSize=),GRUB/SDBoot的Timeout是否为无限或比较大的数字,Initrd中是否包含了必要的内核模块。
  27. 如果要使用自定义的内核,只需要在构建时不安装发行版的内核包或移除掉/usr/lib/modules/目录下的所有文件,然后把自己的内核放在镜像的/usr/lib/modules/<版本号>目录下(版本号格式见KERNEL_VERSION_PATTERN)即可。但要注意,/usr/lib/modules/目录下只能有一个子目录,否则MKOSI会无法判断使用哪个内核而导致构建失败
  28. 在特殊情况下,使用kill -s 'SIGTMIN+4' $PID发送信号给Nspawn容器中的systemd以进行强制关机。
  29. 单元依赖的Vendor模式(直接在/usr/lib/systemd/system/multi-user.target.wants/目录下创建软链接)仅适用于没有[Install]段的内部单元声明内部依赖,尽量不要使用,而是使用/usr/lib/systemd/{system,user}-preset/*.preset
  30. Preset机制不支持进行mask,你需要使用mkosi.extrasystemd-tmpfiles/etc/systemd/system/目录下创建指向/dev/null的符号链接并进行覆盖(L+ /etc/systemd/system/服务名.service - - - - /dev/null)。
  31. 在构建Confext镜像(Overlay=yes)时,不要在Preset文件中设置disable *.service,这不会起作用(disable意味着删除/etc/systemd/system/*.target.wants/下的符号链接,删除动作没有办法在Confext镜像中保存),使用Mask,通过mkosi.extrasystemd-tmpfiles也不会被执行)在/etc/systemd/system/目录下创建指向/dev/null的同名符号链接。
    • 增量优于覆盖。尽量在/etc下创建符号链接,而不是覆盖/usr下的Vendor单元文件。
    • 如果单元文件本来就在/etc(虽然通常情况下不会出现这种情况),通过mkosi.extra创建符号链接也不会起作用(如果和Base Tree中的文件名相同,会被MKOSI删掉)。最后的手段是:你可以使用systemd的Drop-in机制,创建/etc/systemd/system/单元.service.d/99-disable.conf,内容加一行ConditionPathExists=!/

对于使用systemd的kernel-install的发行版来说,MKOSI在构建时会自动跳过内核安装前后置Hook的执行,但是,Debian使用自己实现的Hook(位于/etc/kernel/postinst.d//etc/kernel/postrm.d/目录下),因此要跳过,创建脚本mkosi.skeleton/etc/kernel/postinst.d/00disable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh
set -ue

for hook in /etc/kernel/postinst.d/* /etc/kernel/postrm.d/*; do
if [ "$(basename "${hook}")" = '00disabled' ]; then
continue
else
cat << 'EOT' > "${hook}"
#!/bin/sh
set -ue

exit 0
EOT
fi
done

以及脚本mkosi.skeleton/etc/kernel/postrm.d/00disable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh
set -ue

for hook in /etc/kernel/postrm.d/* /etc/kernel/postrm.d/*; do
if [ "$(basename "${hook}")" = '00disabled' ]; then
continue
else
cat << 'EOT' > "${hook}"
#!/bin/sh
set -ue

exit 0
EOT
fi
done

发行版最小化构建

一些发行版的最小化构建软件包如下:

Arch

1
2
3
[Content]
Packages=linux
systemd

Fedora

1
2
3
4
5
6
[Content]
Packages=kernel
systemd
systemd-boot
udev
util-linux

CentOS/Rocky

1
2
3
4
5
[Content]
Packages=kernel
systemd
systemd-boot
udev

Debian

1
2
3
4
5
6
7
[Content]
Packages=linux-image-generic
systemd
systemd-boot
systemd-sysv
udev
dbus

Ubuntu

1
2
3
4
5
6
7
8
9
[Distribution]
Repositories=main,universe

[Content]
Packages=linux-image-generic
systemd
systemd-sysv
udev
dbus

OpenSUSE

1
2
3
4
[Content]
Packages=kernel-default
systemd
udev

实例

构建一个Fedora容器镜像

使用命令行:

1
mkosi -d fedora -p kernel,systemd,systemd-udev,systemd-boot --format directory build

使用配置文件:

1
2
3
4
5
6
7
8
9
# mkosi.conf
[Distribution]
Distribution=fedora

[Output]
Format=directory

[Content]
Packages=kernel,systemd,systemd-udev,systemd-boot

然后执行:

1
mkosi build

构建一个Debian可引导磁盘镜像

使用配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# mkosi.conf
[Distribution]
Distribution=debian

[Output]
Format=disk
Output=debian-12-minimal

[Content]
Bootable=yes
Packages=linux-image-generic
systemd
systemd-boot
systemd-sysv
udev
dbus
WithDocs=yes
CleanPackageMetadata=yes
WithUnifiedKernelImages=none
KernelCommandLine=console=ttyS0
Password=toor

[Validation]
SecureBoot=no

然后执行:

1
mkosi build

构建一个用于编译程序的临时环境

mkosi.conf

1
2
3
4
5
6
7
8
9
10
[Distribution]
Distribution=debian

[Output]
Format=none

[Content]
# 使用BuildPackages安装构建时的软件包
BuildPackages=
build-essential

mkosi.build.chroot

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
set -uex

# 构建脚本

# 源码位于工作目录下
cd "${SRCDIR}/src"
...

# 构建目录使用$BUILDDIR,方便增量构建
meson setup "$BUILDDIR"
meson compile -C "$BUILDDIR"

mkosi.finalize

1
2
3
4
5
#!/bin/bash
set -uex

# 最终导出到$OUTPUTDIR
cp -r "$BUILDDIR"/* "$OUTPUTDIR"

构建Initrd

mkosi.conf

1
2
3
4
5
6
7
[Output]
Format=cpio

[Content]
Packages=systemd
udev
kmod

或是利用现有的mkosi-initrd配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Include]
Include=mkosi-initrd

[Content]
KernelInitrdModules=default
ata/
scsi/
nvme/
usb/
mmc/
firewire/
gpio/
virtio/
md/
mtd/
block/
input/
hid/

构建一个USI(UEFI PE程序形式的完整系统镜像)

具体的精简方式见Build USI

mkosi.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[Distribution]
# 发行版
Distribution=debian

[Content]
# 不创建/etc/initrd-release,这会使systemd不以Initrd模式启动,也就是不会执行换根
# 这实际上是默认值
MakeInitrd=no
# USI不需要安装任何Bootloader
Bootable=no
KernelCommandLine=rw
# 我们不希望交互式初始化,跳过Firstboot
Locale=
Keymap=
Timezone=
RootPassword=
Packages=
base
linux
systemd
linux-firmware
intel-ucode
amd-ucode
iwd
less
nano
# 你可以考虑删除下列文件以释放空间
RemoveFiles=
/usr/include # 所有头文件
/usr/share/pkgconfig # 所有PKGCONFIG文件
/usr/share/locale # 所有Locale文件
/var/cache/* # 缓存
/var/log/* # 日志

[Output]
Format=uki
CompressOutput=zst
ImageId=example-usi
  • 建议禁用systemd-homed.servicesystemd-userdbd.servicesystemd-sysupdate.service

构建一个Stub NetESP(纯网络启动镜像)

这种情况下,UKI必须通过HTTP提供。

mkosi.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Distribution]
# 发行版
Distribution=debian

[Output]
Format=esp
ImageVersion=
Output=镜像名
# UEFI固件要求:磁盘镜像必须是.img后缀
OutputExtension=raw.img

[Content]
# 安装systemd-boot,但是**不安装**内核或UKI
Bootable=no
Packages=
systemd-boot
systemd-boot-efi

mkosi.extra/efi/loader/entries/netesp.conf

1
2
3
4
title NetBoot
architecture 架构
# 从别的地方下载UKI并启动
uki-url http://地址/UKI.efi

mkosi.extra/efi/loader/loader.conf

1
timeout 3

思路:systemd-boot从远程服务器下载带有root=dissect的UKI,UKI中保存systemd-import-generator相关的内核命令行参数,同样从远程拉取Rootfs并启动,例如:

1
ip=dhcp rd.systemd.pull=raw,machine,verify=no,blockdev:rootdisk:http://地址/rootfs.raw root=dissect rd.systemd.mask=systemd-repart.service systemd.mask=systemd-repart.service

构建单独包含NetBoot的用户空间逻辑的UKI(用于UEFI HTTP Boot服务器或直接使用)

mkosi.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[Include]
Include=mkosi-initrd

[Distribution]
# 发行版
Distribution=debian

[Output]
Format=uki
Output=镜像名

[Content]
Bootable=no
# 构建UKI的三要素
Packages=
# 提供systemd-stub
systemd-boot-efi
# 提供importd
systemd-container
# 提供内核
linux-image-generic
# UKI中封装的内核命令行参数
KernelCommandline=
root=dissect
# 以raw镜像为例,你也可以使用tar包或Hermetic /usr挂载
rd.systemd.pull=raw,machine,verify=no,blockdev:rootdisk:http://地址/rootfs.raw
ip=dhcp
# systemd-repart没办法对Tmpfs或Loop设备进行重新分区,总是会失败
rd.systemd.mask=systemd-repart.service
systemd.mask=systemd-repart.service
# UKI内已经包含了内核,Initrd中不需要再包含
# 日后也许可以换成RemovePackages=
RemoveFiles=
/boot/config-*
/boot/System.map-*
/boot/vmlinuz-*

构建一个Pull-Tar NetESP(根文件系统从网络拉取)

mkosi.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[Include]
# 应用Initrd裁剪
Include=mkosi-initrd

[Distribution]
# 发行版
Distribution=debian

[Output]
Format=esp
Output=镜像名

# Content段,实际影响的是UKI内的内容
[Content]
# 构建UKI,需要安装systemd-boot和内核
Bootable=yes
Packages=
systemd-boot
systemd-boot-efi
# 提供importd
systemd-container
# 触发UKI构建
linux-image-generic
# UKI中封装的内核命令行参数
KernelCommandline=
root=dissect
# 设置URL
rd.systemd.pull=raw,machine,verify=no,blockdev:rootdisk:http://地址/rootfs.raw
ip=dhcp
# systemd-repart没办法对Tmpfs或Loop设备进行重新分区,总是会失败
rd.systemd.mask=systemd-repart.service
systemd.mask=systemd-repart.service
# UKI内已经包含了内核,Initrd中不需要再包含
# 日后也许可以换成RemovePackages=
RemoveFiles=
/boot/config-*
/boot/System.map-*
/boot/vmlinuz-*

构建一个Pull-Hermetic-USR-Raw NetESP(根文件系统从网络拉取)

mkosi.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[Include]
# 应用Initrd裁剪
Include=mkosi-initrd

[Distribution]
# 发行版
Distribution=debian

[Output]
# 你也可以使用uki
Format=esp
Output=镜像名

# Content段,实际影响的是UKI内的内容
[Content]
# 构建UKI,需要安装systemd-boot和内核
Bootable=yes
Packages=
systemd-boot
systemd-boot-efi
# 提供importd
systemd-container
# 触发UKI构建
linux-image-generic
# UKI中封装的内核命令行参数
KernelCommandline=
# 设置URL
rd.systemd.pull=raw,machine,verify=no,blockdev:usrdisk:http://URL
mount.usr=/dev/disk/by-loop-ref/usrdisk.raw-part2
root=tmpfs
ip=dhcp
# systemd-repart没办法对Tmpfs或Loop设备进行重新分区,总是会失败
rd.systemd.mask=systemd-repart.service
systemd.mask=systemd-repart.service
# UKI内已经包含了内核,Initrd中不需要再包含
# 日后也许可以换成RemovePackages=
RemoveFiles=
/boot/config-*
/boot/System.map-*
/boot/vmlinuz-*

构建一个系统扩展镜像

mkosi.conf(如果没有转换为Format=disk的需求,你也可以将mkosi.images/base/mkosi.conf的配置合并到这里,少一次拷贝):

1
2
3
4
5
6
7
8
[Output]
# 如果不希望保留基础镜像的话,使用
# Format=none
Format=disk

[Content]
# 会拷贝base子镜像的内容
BaseTrees=%O/base

mkosi.images/base/mkosi.conf(扩展镜像相对的基础镜像):

1
2
3
4
5
6
7
8
9
10
11
12
[Output]
Format=directory
# 该镜像完全是个中间镜像
# 版本号在这里无意义
ImageVersion=

[Content]
# 该配置项在directory格式下实际上没有影响,但是会增加额外的依赖程序检测
Bootable=no
Packages=
systemd
udev
  • 除了文件存在包含关系(因为BaseTrees=%O/base)外,子镜像的配置和父镜像大部分没有继承关系(参考上文)。请务必注意,尤其是有关Initrd的配置

mkosi.images/btrfs-sysext/mkosi.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Assert]
PathExists=%O/base

[Output]
# 或Format=confext
Format=sysext
Overlay=yes

[Content]
Bootable=no
# 注意这里选择base镜像的目录树作为BaseTree
BaseTrees=%O/base
# 只有这个需要改
Packages=btrfs-progs

mkosi.images/btrfs-confext/mkosi.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
[Assert]
PathExists=%O/base

[Output]
ImageVersion=
Format=confext
Overlay=yes

[Content]
Bootable=no
# 注意这里选择base镜像的目录树作为BaseTree
BaseTrees=%O/base
Packages=btrfs-progs

注意:MKOSI在构建时,如果发现镜像列表中有至少一个镜像具有BaseTrees=配置,那么就会导致缓存失效,判定元数据需要同步,进而强制清除列表中所有镜像的缓存mkosi.cache/)。因此在构建扩展镜像时,务必只指定最终的目标镜像(如 mkosi --dependency sysext-xxx build)。否则会导致基座镜像的缓存被误删,导致每次都重新构建。

保留Confext中的额外信息

构建Overlay=yes的镜像时,MKOSI默认不会在最后执行一次systemd-tmpfilespreset。如果你希望在Confext中保留单元依赖和/etc下可能创建的目录,创建mkosi.postinst.chroot,在其中包含:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SYSTEMD_TMPFILES_FORCE_SUBVOL=0 \
systemd-tmpfiles \
--boot \
--create \
--remove \
--prefix=/etc || {
RET="$?"
test "$RET" -eq 65 ||
test "$RET" -eq 73
}

systemctl preset-all

systemctl --global preset-all

构建一个多模态扩展镜像

一个同时包含/etc/usr的扩展镜像,既可以当作Sysext使用,也可以当作Confext使用。

mkosi.conf以及mkosi.images/base/mkosi.conf的内容与上一段完全相同,如果没有转换为Format=disk的需求,你同样也可以将mkosi.images/base/mkosi.conf的配置合并到mkosi.conf,少一次拷贝。

mkosi.images/btrfs/mkosi.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Config]
Dependencies=base

[Output]
ImageVersion=
# 注意格式
Format=disk
Overlay=yes
# 只有这个需要改
ImageId=btrfs

[Content]
Bootable=no
# 注意这里选择base镜像的目录树作为BaseTree
BaseTrees=%O/base
# 只有这个需要改
Packages=btrfs-progs

你还需要自定义磁盘分区格式mkosi.images/btrfs/mkosi.repart/10-root.conf

1
2
3
4
5
6
7
[Partition]
Type=root
Format=erofs
CopyFiles=/usr/
CopyFiles=/opt/
CopyFiles=/etc/
Minimize=best

接着还需要一个自定义脚本mkosi.images/btrfs/mkosi.postinst.chroot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/sh
set -eu

EXT_IMAGE_ID="${IMAGE_ID}"

if [ -f /usr/lib/os-release ]; then
. /usr/lib/os-release
else
ID="_any"
fi

mkdir -p /usr/lib/extension-release.d

cat > "/usr/lib/extension-release.d/extension-release.${EXT_IMAGE_ID}" << EOF
ID=${ID}
SYSEXT_ID=${IMAGE_ID}
SYSEXT_SCOPE=${SYSEXT_SCOPE:-initrd system portable}
ARCHITECTURE=${ARCHITECTURE}
EOF

mkdir -p /etc/extension-release.d

cat > "/etc/extension-release.d/extension-release.${EXT_IMAGE_ID}" << EOF
ID=${ID}
${VERSION_ID:+VERSION_ID="${VERSION_ID}"
}CONFEXT_ID=${IMAGE_ID}
CONFEXT_SCOPE=${CONFEXT_SCOPE:-initrd system portable}
ARCHITECTURE=${ARCHITECTURE}
EOF

构建一个Portable Service

Portable Service不可以是扩展镜像,也不支持Overlay=yes

mkosi.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[Build]
# 如果脚本需要网络的话
# WithNetwork=yes

[Output]
Format=portable

[Content]
# 不安装内核/UKI
Bootable=no
# 你可以考虑删除下列文件以释放空间
RemoveFiles=
/usr/include
/usr/share
/usr/games
/var/cache/*
/boot
/efi
# 使用BuildPackages安装软件包
# 这样就不会有冗余文件
BuildPackages=
gcc
make
# 使用Packages提供最终镜像中需要包含的包
Packages=
bash
coreutils
# 在os-release中生成PORTABLE_PREFIXES
Environment=PORTABLE_PREFIXES=服务名称

mkosi.build

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
set -uex

# 从源码构建代码
make all && \
make install

# 拷贝文件
cp XXX "$DESTDIR/usr/bin/"

# 创建之后需要绑定挂载的目标目录
mkdir -p /xxx

mkosi.extra/usr/lib/systemd/system/XXX.service

1
2
3
4
5
6
7
# 关键点是必须包含StateDirectory等目录,否则程序将无法写入数据目录
# 请注意提前在镜像中创建绑定挂载的目标目录
[Service]
StateDirectory=XXX
ConfigurationDirectory=XXX
RuntimeDirectory=XXX
LogsDirectory=XXX

mkosi.finalize

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
set -uex

# 必须创建以避免Bind Mount失败
# 你不可以使用chroot执行,因为resolv.conf会在chroot内以只读模式挂载:
# https://github.com/systemd/mkosi/blob/main/mkosi/__init__.py
touch "$BUILDROOT/etc/resolv.conf"

cat << 'EOT' >> "$BUILDROOT/usr/lib/os-release"
PORTABLE_PREFIXES=服务名称
EOT

镜像信息

默认情况下,MKOSI构建的镜像无法被systemd-dissect判断为Portable Service镜像,这是因为镜像中的os-release文件中没有包含PORTABLE_PREFIXES=字段,你可以手动配置:

mkosi.conf

1
2
[Content]
Environment=PORTABLE_PREFIXES=服务名称

或者:

mkosi.finalize

1
2
3
4
5
6
#!/bin/bash
set -uex

cat << 'EOT' >> "$BUILDROOT/usr/lib/os-release"
PORTABLE_PREFIXES=服务名称
EOT

可变镜像

默认情况下镜像中会使用erofs,因此镜像是不可变的,你必须使用StateDirectory=等单元配置项将宿主系统中的目录以读写模式绑定挂载到Portable Service的根文件系统中。

如果你希望调整这一行为,创建mkosi.repart/,包含以下文件:

10-root.conf

1
2
3
4
5
[Partition]
Type=root
Format=ext4
CopyFiles=/
Minimize=best
  • 如果你没有设置StateDirectory=等单元配置项,那么写入的数据会被保存在镜像内部。

构建一个不可变操作系统

首先创建一个如下分区的操作系统:

00-esp.conf

1
2
3
4
5
6
7
[Partition]
Type=esp
Format=vfat
CopyFiles=/efi:/
CopyFiles=/boot:/
SizeMinBytes=1G
SizeMaxBytes=1G

10-root-verity-sig.conf

1
2
3
4
5
6
[Partition]
Type=root-verity-sig
Label=%M_%A_verity_sig
Verity=signature
VerityMatchKey=root
SplitName=%t.%U

11-root-verity.conf

1
2
3
4
5
6
7
8
[Partition]
Type=root-verity
Label=%M_%A_verity
Verity=hash
VerityMatchKey=root
SizeMinBytes=300M
SizeMaxBytes=300M
SplitName=%t.%U

12-root.conf

1
2
3
4
5
6
7
8
9
10
[Partition]
Type=root
Format=erofs
Label=%M_%A_root
Verity=data
VerityMatchKey=root
CopyFiles=/
ExcludeFilesTarget=/var/
Minimize=yes
SplitName=%t.%U

除此之外我们还需要构建一个Initrd,Initrd中的systemd-repart.service的启动时间需要进行些许修改:

mkosi.images/initrd/mkosi.conf

1
2
[Include]
Include=mkosi-initrd

mkosi.images/initrd/mkosi.extra/usr/lib/systemd/system/systemd-repart.service.d/sysroot.conf

1
2
3
[Unit]
After=sysroot.mount
ConditionDirectoryNotEmpty=|/sysroot/usr/lib/repart.d

为了进行AB更新,我们还需要在镜像中包含一些预设的分区配置:

mkosi.extra/usr/lib/repart.d/00-esp.conf

1
2
[Partition]
Type=esp

mkosi.extra/usr/lib/repart.d/10-root-verity-sig.conf

1
2
3
[Partition]
Type=root-verity-sig
Label=%M_%A_verity_sig

mkosi.extra/usr/lib/repart.d/11-root-verity.conf

1
2
3
[Partition]
Type=root-verity
Label=%M_%A_verity

mkosi.extra/usr/lib/repart.d/12-root.conf

1
2
3
4
5
[Partition]
Type=root
Label=%M_%A
SizeMinBytes=2G
SizeMaxBytes=2G

mkosi.extra/usr/lib/repart.d/20-root-verity-sig.conf

1
2
3
[Partition]
Type=root-verity-sig
Label=_empty

mkosi.extra/usr/lib/repart.d/21-root-verity.conf

1
2
3
4
5
[Partition]
Type=root-verity
Label=_empty
SizeMinBytes=300M
SizeMaxBytes=300M

mkosi.extra/usr/lib/repart.d/22-root.conf

1
2
3
4
5
[Partition]
Type=root
Label=_empty
SizeMinBytes=2G
SizeMaxBytes=2G

mkosi.extra/usr/lib/repart.d/30-swap.conf

1
2
3
4
5
6
[Partition]
Type=swap
Format=swap
Encrypt=tpm2
SizeMinBytes=4G
SizeMaxBytes=4G

mkosi.extra/usr/lib/repart.d/40-var.conf

1
2
3
4
5
[Partition]
Type=var
Format=ext4
Encrypt=tpm2
SizeMinBytes=2G

因为/etc不可变,所以我们还需要在构建时提供Machine ID:

1
systemd-id128 new > mkosi.machine-id

最后为了未来镜像中的系统更新,我们还需要提供systemd-sysupdate的配置(你需要自行补充[Source]段):

mkosi.extra/usr/lib/sysupdate.d/10-root-verity-sig.transfer

1
2
3
4
5
6
7
8
9
10
[Transfer]
ProtectVersion=%A

[Target]
Type=partition
Path=auto
MatchPattern=%M_@v_verity_sig
MatchPartitionType=root-verity-sig
PartitionFlags=0
ReadOnly=1

mkosi.extra/usr/lib/sysupdate.d/11-root-verity.transfer

1
2
3
4
5
6
7
8
9
10
[Transfer]
ProtectVersion=%A

[Target]
Type=partition
Path=auto
MatchPattern=%M_@v_verity
MatchPartitionType=root-verity
PartitionFlags=0
ReadOnly=1

mkosi.extra/usr/lib/sysupdate.d/12-root.transfer

1
2
3
4
5
6
7
8
9
10
[Transfer]
ProtectVersion=%A

[Target]
Type=partition
Path=auto
MatchPattern=%M_@v
MatchPartitionType=root
PartitionFlags=0
ReadOnly=1

mkosi.extra/usr/lib/sysupdate.d/20-uki.transfer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Transfer]
ProtectVersion=%A

[Target]
Type=regular-file
Path=/EFI/Linux
PathRelativeTo=boot
MatchPattern=%M_@v+@l-@d.efi \
%M_@v+@l.efi \
%M_@v.efi
Mode=0600
TriesLeft=3
TriesDone=0
InstancesMax=2

构建一个不安全的/usr不可变操作系统

由于上文提到的原因(UKI必须使用Verity Hash对/usr/分区进行定位),我们无法让MKOSI在构建镜像时,自动向UKI中填充/usr/分区所在的位置,因此,我们必须手动在mkosi.conf中设置:

1
2
3
4
[Content]
# 要么根文件系统在Initrd中是可写的,要么存在/usr目录,否则会导致挂载失败
# 也可以使用mount.usr=PARTUUID
KernelCommandLine=rw mount.usr=PARTLABEL=%i_%v

systemd-repart配置如下:

00-esp.conf

1
2
3
4
5
6
7
[Partition]
Type=esp
Format=vfat
CopyFiles=/efi:/
CopyFiles=/boot:/
SizeMinBytes=1G
SizeMaxBytes=1G

10-usr.conf

1
2
3
4
5
6
7
8
[Partition]
Type=usr
# 注意这里,和内核命令行参数一致
Label=%M_%A
Format=erofs
CopyFiles=/usr:/
Minimize=yes
SplitName=%t.%U

此外,还需要在镜像内提供一些分区配置:

mkosi.extra/usr/lib/repart.d/00-esp.conf

1
2
[Partition]
Type=esp

mkosi.extra/usr/lib/repart.d/10-usr-verA.conf

1
2
3
4
5
6
[Partition]
Type=usr
Label=%M_%A
SizeMinBytes=5G
SizeMaxBytes=20G
Weight=2000

mkosi.extra/usr/lib/repart.d/20-usr-verB.conf

1
2
3
4
5
6
[Partition]
Type=usr
Label=_empty
SizeMinBytes=5G
SizeMaxBytes=20G
Weight=2000

mkosi.extra/usr/lib/repart.d/30-swap.conf

1
2
3
4
5
[Partition]
Type=swap
Format=swap
SizeMinBytes=4G
SizeMaxBytes=4G

mkosi.extra/usr/lib/repart.d/40-root.conf

1
2
3
4
5
6
7
8
[Partition]
Type=root
Format=btrfs
SizeMinBytes=1G
Weight=20000
Subvolumes=/var
# 这是为了使systemd-journald使用持久模式
MakeDirectories=/var/log/journal

mkosi.extra/usr/lib/repart.d/50-home.conf

1
2
3
4
5
[Partition]
Type=home
Format=btrfs
SizeMinBytes=1G
Weight=40000

我们还需要确保/etc/目录在启动时动态生成,因此需要:

mkosi.extra/usr/lib/tmpfiles.d/etc.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 仅作参考
# This overrides the same file from systemd since we want to symlink everything
# into /etc instead of copying so updates to /usr propagate properly.
L /etc/os-release - - - - ../usr/lib/os-release
# mtab needs to be refreshed on every boot.
L+ /etc/mtab - - - - ../proc/self/mounts
# Contains the default systemd locale
L /etc/locale.conf
L /etc/nsswitch.conf
L /etc/issue
L /etc/profile
# Required by pam_env plugin
L /etc/security
L /etc/bash.bashrc
L /etc/bash.bash_logout
# Canonical location to look for certificates
L /etc/ca-certificates
L /etc/debuginfod
L /etc/ssh
# Canonical location to look for certificates
L /etc/ssl
# Required by pam environment plugin
L /etc/environment
# Contains the archlinux keyring required to build images
L /etc/pacman.d
# Required to generate desktop environment application menus
L /etc/xdg
# Contains default font configuration
L /etc/fonts
# Configuration for man
L /etc/man_db.conf
# Configuration for ldconfig
L /etc/ld.so.conf
L /etc/ld.so.conf.d
# We NEED authselect to correctly login
L /etc/authselect
L /etc/sysconfig

为了确保ldconfig.service/etc/目录生成后再启动,我们还需要:

mkosi.extra/usr/lib/systemd/system/ldconfig.service.d/tmpfiles.conf

1
2
[Unit]
After=systemd-tmpfiles-setup.service

最后,我们需要MKOSI的更新配置:

mkosi.sysupdate/10-usr.transfer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Transfer]
ProtectVersion=%A

[Source]
Type=regular-file
Path=/
PathRelativeTo=explicit
MatchPattern=%M_@v.usr-%a.@u.raw

[Target]
Type=partition
Path=auto
MatchPattern=%M_@v
MatchPartitionType=usr
PartitionFlags=0
ReadOnly=1

mkosi.sysupdate/20-uki.transfer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Transfer]
ProtectVersion=%A

[Source]
Type=regular-file
Path=/
PathRelativeTo=explicit
MatchPattern=%M_@v.efi

[Target]
Type=regular-file
Path=/EFI/Linux
PathRelativeTo=boot
MatchPattern=%M_@v+@l-@d.efi \
%M_@v+@l.efi \
%M_@v.efi
Mode=0444
TriesLeft=3
TriesDone=0
InstancesMax=2

杂记

DEB822

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Types: deb
URIs: http://mirrors.ustc.edu.cn/debian
Suites: testing
Components: main contrib non-free non-free-firmware
Trusted: yes

Types: deb
URIs: http://mirrors.ustc.edu.cn/debian
Suites: testing-updates
Components: main contrib non-free non-free-firmware
Trusted: yes

Types: deb
URIs: http://mirrors.ustc.edu.cn/debian-security
Suites: testing-security
Components: main contrib non-free non-free-firmware
Trusted: yes

Types: deb
URIs: https://fast-mirror.isrc.ac.cn/debian-debug
Suites: testing-debug
Components: main contrib non-free non-free-firmware
Trusted: yes

UserNS

在使用Apparmor的情况下,MKOSI需要开启UserNS支持,执行:

1
2
sudo sysctl -w kernel.apparmor_restrict_unprivileged_unconfined=1
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1

ToolsTree的问题

/usr/share/factory

如果使用了ToolsTree,而且镜像中存在/usr/share/factory,那么ToolsTree中必须也存在/usr/share/factory,否则会发生文件系统只读错误,这是因为ToolsTree的/usr会被只读地绑定挂载到Sandbox中这导致无法在ToolsTree中自动创建/usr/share/factory挂载点

不过,该问题在Commit 09497d0中已被修复。

os-release

在Sandbox中,/etc/os-release来自Tools Tree,/usr/lib/os-release来自宿主环境,这意味着MKOSI期望/etc/os-release是一个指向/usr/lib/os-release的符号链接,如果Tools Tree中不是这样,那么会导致systemd-sysupdate的版本判断完全错乱Debian Sid就有这个问题,因此不要使用Sid作为Tools Tree

内核模块的问题

Issue 3603报告了一个在默认情况下MKOSI构建的镜像的Initrd中不包含任何模块的问题(即KernelInitrdModules=没有设置的默认情况下不是包含所有模块而是不包含任何模块)。

这个问题的主要原因是这个逻辑错误导致在没有设置KernelModulesInitrdExclude=exclude集合)的情况下,如果也没有设置KernelInitrdModules=include集合),那么最终包含的内核模块集合(modules集合)被错误地重设为空(被设为了空的keep集合,而不是保留之前的值)。

gen_required_kernel_modules函数会调用有问题的filter_kernel_modules函数,这个函数只在两个地方出现:

  1. run_depmod函数调用的process_kernel_modules函数中。
  2. build_kernel_modules_initrd函数中。

直觉上这似乎也会影响影响镜像内包含的内核模块(run_depmod函数),但是实际上却没有,这是为什么呢?因为process_kernel_modules函数明确包含了在没有设置KernelModules=KernelModulesExclude=FirmwareFiles=FirmwareExclude=的情况下跳过的逻辑,所以并没有造成任何影响。

loader.conf的位置

根据Issue 3614,如果为ESP分区配置了:

1
2
CopyFiles=/boot
CopyFiles=/efi

那么如果希望在镜像内安装systemd-boot的配置文件loader.conf,则必须放在mkosi.extra/efi/loader/loader.conf。因为多个CopyFiles=字段会按顺序拷贝文件,而systemd-boot在通过bootctl安装到镜像中时,如果/efi/loader/loader.conf不存在,则会自动生成一个默认配置文件,如果用户自定义的配置文件放在mkosi.extra/boot/loader/loader.conf,就会被它覆盖。

“自动安装”元数据丢失

MKOSI支持保留镜像内的软件包元数据(CleanPackageMetadata=no),但是对于aptdnf来说,“自动安装”(Auto-Installed)元数据会被丢失

PR 3682修复。

mkosi.version重新定义

PR 3696重写了MKOSI的历史记录逻辑,现在MKOSI的历史记录中不会保存完整的配置记录,因此一旦配置发生改变,就不会认为镜像已被构建。

这带来的最大影响是现在不可以再使用完全动态生成版本的mkosi.version脚本,否则MKOSI总是会认为镜像未被构建。解决方案是转而使用mkosi.bump

LUKS加密

Passphrase=mkosi.passphrase创建的密钥会用于mkosi.repart中设置了Encrypt=key-file的分区。

APK

部分发行版尚未打包APK,可以下载静态编译版本并放在PATH路径下:apk-tools-static

Archlinux ARM构建失败

Archlinux ARM的内核路径不符合标准,因此需要额外的处理才能正常构建,创建mkosi.postinst.chroot

1
2
3
4
5
#!/bin/bash
set -ue

[ -f /boot/Image ] &&
mv /boot/Image "/boot/vmlinuz-$(ls -1 /usr/lib/modules | head -n 1)"

UKI Profile

UKI Profile在设计上可以支持额外文件,但是这不会再实现了Lennart最终决定在systemd-boot实现在加载UKI时Overlay式的扩展

UNAME到OCI架构格式的转换

1
2
3
4
5
6
7
8
9
10
UNAME_TO_OCI = {
"x86_64": "amd64",
"amd64": "amd64",
"aarch64": "arm64",
"arm64": "arm64",
"armv7l": "arm",
"s390x": "s390x",
"ppc64le": "ppc64le",
"riscv64": "riscv64",
}