用 Nix 构建 systemd Portable Services 镜像:pkgs.portableService 完整指南
【免费下载链接】nixpkgsNix Packages collection & NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs
pkgs.portableService是 Nixpkgs 提供的一个构建函数,它把软件包及其全部依赖打包成一个只读、不可变的squashfs原始磁盘镜像(.raw文件),该镜像完全符合 systemd Portable Services 为主体,结合 实现源码 与 NixOS 集成测试,讲解全部输入参数、完整构建示例以及底层实现原理,读完你可以独立用 Nix 构建、装载并调试自己的 Portable Service 镜像。
什么是 Portable Services,为什么用 Nix 构建
Portable Services 是 systemd 239(2018-06-22 发布)起引入的一种服务分发机制。与容器不同,Portable Service 镜像内的文件系统结构遵循规范约定(如/usr/lib/systemd/system、/etc/systemd/system等目录),被装载(attach)后其 unit 文件直接暴露给宿主 systemd,由宿主直接管理进程,无需独立的容器运行时。镜像以.raw后缀的磁盘镜像形式存在,具备只读、不可变特性,天然适合分发与审计。
Nix 构建 Portable Service 的独特优势在于:构建产物天然可复现。Nixpkgs 通过closureInfo机制收集目标包及其所有依赖的完整 store 闭包,一次性写入镜像,避免手工拼装依赖时常见的遗漏与冲突;同时 Nix 的 store 路径哈希确保相同输入必然产出相同镜像。
需要注意的是,本文讨论的是 Nixpkgs 自带的pkgs.portableService构建函数(官方手册将其归类于 镜像构建助手),它专门产出面向 systemdportablectl的镜像,区别于通用的磁盘镜像制作工具。
构建产物形态
portableService生成的镜像会出现在 Nix store 中,文件扩展名为.raw,这是 Portable Services 规范强制要求的后缀。镜像内部包含规范要求的文件系统骨架,以及传给portableService的包和它们的全部依赖。
从 实现源码 可以看出,最终的 derivation 名为${pname}-img,产出文件遵循$pname_$version.raw的命名模板,该模板同样被 Portable Services 规范支持。例如构建 hello 服务的镜像后,会在/nix/store/...-hello-img-2.12.1/hello_2.12.1.raw得到产物。
输入参数详解
portableService接受单个包含以下属性的参数集合:
必选参数
pname(String):Portable Service 的名称,镜像将按$pname_$version.raw模板命名。在源码中它还承担双重职责——用作生成的/etc/os-release中PORTABLE_ID的值,并要求所有 unit 文件名必须以它为前缀(见下文units与 命名约束)。version(String):Portable Service 的版本,参与镜像文件名模板$pname_$version.raw。units(List of Attribute Set):systemd unit 文件 derivation 的列表。每个 derivation 必须且只能产出一个文件,文件名必须以pname开头、以 unit 类型后缀结尾(如.service、.socket、.timer等)。这些 unit 会被复制到镜像内的/etc/systemd/system/目录——实现中通过lib.concatMapStringsSep "\n" (u: "cp ${u} $out/etc/systemd/system/${u.name};") units逐条复制,u.name正是 derivation 输出文件的名字,这也是该命名约束的底层原因(实现源码)。
可选参数
description(String 或 Null,默认null):若指定,其值会作为PORTABLE_PRETTY_NAME写入镜像内的/etc/os-release文件,供任何人检查镜像时获取更多信息。homepage(String 或 Null,默认null):若指定,其值会作为HOME_URL写入镜像内的/etc/os-release文件。symlinks(List of Attribute Set,默认[]):元素格式为{ object, symlink }。对列表中的每一项,portableService会在镜像根文件系统中symlink指定的路径(相对镜像根)创建一个指向object的符号链接。object所依赖的所有包及其依赖会被自动复制进镜像。这适用于那些假设某些文件全局存在的应用(例如/etc/ssl或/bin/bash)。contents(List of Attribute Set,默认[]):附加 derivation 列表,这些 derivation 会被原样放入镜像内的/nix/store目录。实现上它们与根文件系统骨架一起作为closureInfo的rootPaths参与闭包收集(实现源码)。squashfsTools(Attribute Set,默认pkgs.squashfs-tools):覆盖内部使用的mksquashfs(1)提供者。squash-compression(String,默认"xz -Xdict-size 100%"):作为压缩选项传给内部调用的mksquashfs(1)。squash-block-size(String,默认"1M"):作为块大小选项传给内部调用的mksquashfs(1)。
实现中这些压缩参数最终被拼接到mksquashfs命令的-b ${squash-block-size} -comp ${squash-compression}部分(实现源码),覆盖它们即可按需调整镜像大小与压缩比。
镜像内部结构:从源码看生成过程
portableService的实现分两个阶段(完整逻辑见 pkgs/build-support/portable-service/default.nix):
第一阶段:构建根文件系统骨架(rootFsScaffold)。这是一个独立的stdenv.mkDerivation,负责:
- 创建
/etc/systemd/system、/proc、/sys、/dev、/run、/tmp、/var/tmp、/var/lib、/var/cache、/var/log等规范要求的目录; - 创建空的
/etc/resolv.conf与/etc/machine-id(装载时由宿主版本覆盖挂载); - 将生成的
/etc/os-release复制进骨架——os-release由lib.generators.toKeyValue生成,包含PORTABLE_ID=pname、PORTABLE_PRETTY_NAME=description、HOME_URL=homepage、ID=nixos、PRETTY_NAME=NixOS、BUILD_ID=rolling等键值(实现源码),null 值通过filterNull自动剔除; - 将各 unit 复制到
/etc/systemd/system/; - 根据
symlinks逐条创建符号链接。
第二阶段:拼装 squashfs 镜像。最终 derivation 以pname = "${pname}-img"命名,使用pkgs.closureInfo收集rootFsScaffold与contents的完整 store 闭包,将闭包内每个 store 路径以cp -a方式放入nix/store目录,随后执行:
SOURCE_DATE_EPOCH=0 mksquashfs nix ${rootFsScaffold}/* $out/"${pname}_${version}.raw" \ -quiet -noappend \ -exit-on-error \ -keep-as-directory \ -all-root -root-mode 755 \ -b ${squash-block-size} -comp ${squash-compression}值得注意的工程细节:SOURCE_DATE_EPOCH=0被显式设置以保证镜像构建可复现(Nixpkgs issue #390696 相关);-keep-as-directory将 store 目录与根骨架平级压入镜像根;-all-root -root-mode 755统一镜像内文件属主。
示例一:构建并装载 hello 服务的 Portable Service 镜像
以下示例构建一个包含hello包及其 service unit 的 Portable Service 镜像:
{ lib, writeText, portableService, hello, }: let hello-service = writeText "hello.service" '' [Unit] Description=Hello world service [Service] Type=oneshot ExecStart=${lib.getExe hello} ''; in portableService { pname = "hello"; inherit (hello) version; units = [ hello-service ]; }注意writeText "hello.service"的输出文件名恰好以pname = "hello"开头、以.service结尾,满足 命名约束;如果不满足,构建会直接抛错 "Unit names must be prefixed with the service name"。
构建产物并通过portablectl(1)装载、启动与卸载的完整流程如下:
$ nix-build (some output removed for clarity) /nix/store/8c20z1vh7z8w8dwagl8w87b45dn5k6iq-hello-img-2.12.1 $ portablectl attach /nix/store/8c20z1vh7z8w8dwagl8w87b45dn5k6iq-hello-img-2.12.1/hello_2.12.1.raw Created directory /etc/systemd/system.attached. Created directory /etc/systemd/system.attached/hello.service.d. Written /etc/systemd/system.attached/hello.service.d/20-portable.conf. Created symlink /etc/systemd/system.attached/hello.service.d/10-profile.conf → /usr/lib/systemd/portable/profile/default/service.conf. Copied /etc/systemd/system.attached/hello.service. Created symlink /etc/portables/hello_2.12.1.raw → /nix/store/8c20z1vh7z8w8dwagl8w87b45dn5k6iq-hello-img-2.12.1/hello_2.12.1.raw. $ systemctl start hello $ journalctl -u hello Feb 28 22:39:16 hostname systemd[1]: Starting Hello world service... Feb 28 22:39:16 hostname hello[102887]: Hello, world! Feb 28 22:39:16 hostname systemd[1]: hello.service: Deactivated successfully. Feb 28 22:39:16 hostname systemd[1]: Finished Hello world service. $ portablectl detach hello_2.12.1 Removed /etc/systemd/system.attached/hello.service. Removed /etc/systemd/system.attached/hello.service.d/10-profile.conf. Removed /etc/systemd/system.attached/hello.service.d/20-portable.conf. Removed /etc/systemd/system.attached/hello.service.d. Removed /etc/portables/hello_2.12.1.raw. Removed /etc/systemd/system.attached.从portablectl的输出可以看到装载的关键动作:unit 被复制到/etc/systemd/system.attached/、镜像被软链到/etc/portables/、并套用了 default 的 portable profile 配置。这套流程与 NixOS 集成测试 中验证的行为一致——测试使用portablectl attach --now --runtime装载demo_1.0.raw,等待demo.service进入运行态后再detach并断言其已停止。
示例二:通过 symlinks 使文件全局可用
有些服务假设某些文件或目录全局存在,例如默认信任全部 SSL 证书的服务会期望证书位于特定位置。此时必须在调用portableService时指定symlinks属性。以下示例在示例一的基础上,将/etc/ssl在镜像内全局暴露(仅为演示目的——hello本身并不使用/etc/ssl):
{ lib, writeText, portableService, hello, cacert, }: let hello-service = writeText "hello.service" '' [Unit] Description=Hello world service [Service] Type=oneshot ExecStart=${lib.getExe hello} ''; in portableService { pname = "hello"; inherit (hello) version; units = [ hello-service ]; symlinks = [ { object = "${cacert}/etc/ssl"; symlink = "/etc/ssl"; } ]; }实现层面,每个{ object, symlink }元素对应执行mkdir -p $(dirname $out/${symlink}); ln -s ${object} $out/${symlink};(实现源码),并且由于object会被纳入最终镜像的闭包收集,其依赖会自动随镜像分发,无需手动逐一列举。
多 unit 组合:socket 激活场景
units天然支持多个 unit 的组合。在 NixOS 集成测试 中,demo服务就同时携带了demo.service与demo.socket两个 unit:service 通过Requires=demo.socket、After=demo.socket声明对 socket 的依赖,socket 则监听/run/demo.sock。这种多 unit 结构是 Portable Service 支持 socket 激活、timer 定时触发等 systemd 经典机制的典型用法,所有 unit 只需满足"以pname为前缀 + 正确类型后缀"的命名规则即可一同打进镜像。
使用前提与限制
- 宿主机 systemd 版本:Portable Services 需要 systemd 239 及以上(2018-06-22 发布),请在目标系统上用
systemctl --version确认。 - 宿主机非 NixOS 亦可:这正是该方案的典型价值——镜像自包含所有依赖,可在众多现代 Linux 发行版上通过
portablectl直接使用。 - unit 命名强约束:unit 文件名必须以
pname为前缀,否则构建阶段assert会直接失败(实现源码)。 - 镜像只读不可变:运行时状态应写入
/var、/run、/tmp等由宿主挂载覆盖的目录,/etc/resolv.conf与/etc/machine-id在镜像内预留为空文件以接受宿主版本。 - 可复现性:实现通过
SOURCE_DATE_EPOCH=0固定时间戳保证镜像可复现构建。
参考资源
- 官方手册原文:doc/build-helpers/images/portableservice.section.md
- 实现源码:pkgs/build-support/portable-service/default.nix
- 顶层注册(
pkgs.portableService = callPackage ../build-support/portable-service { }):pkgs/top-level/all-packages.nix - NixOS 集成测试(含 socket 激活场景与 attach/detach 验证):nixos/tests/systemd-portabled.nix
【免费下载链接】nixpkgsNix Packages collection & NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考