news 2026/9/14 0:13:26

用 Nix 构建 systemd Portable Services 镜像:pkgs.portableService 完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Nix 构建 systemd Portable Services 镜像:pkgs.portableService 完整指南

用 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-releasePORTABLE_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目录。实现上它们与根文件系统骨架一起作为closureInforootPaths参与闭包收集(实现源码)。
  • 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-releaselib.generators.toKeyValue生成,包含PORTABLE_ID=pnamePORTABLE_PRETTY_NAME=descriptionHOME_URL=homepageID=nixosPRETTY_NAME=NixOSBUILD_ID=rolling等键值(实现源码),null 值通过filterNull自动剔除;
  • 将各 unit 复制到/etc/systemd/system/
  • 根据symlinks逐条创建符号链接。

第二阶段:拼装 squashfs 镜像。最终 derivation 以pname = "${pname}-img"命名,使用pkgs.closureInfo收集rootFsScaffoldcontents的完整 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.servicedemo.socket两个 unit:service 通过Requires=demo.socketAfter=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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 0:07:29

机载雷达STAP原理与MATLAB实战:空时自适应处理杂波抑制全解析

简介:面向本科、硕士阶段雷达信号处理教研学习的一套MATLAB实现,聚焦机载雷达时空自适应处理(STAP)核心算法,兼顾理论演示、仿真复现与工程实践。资源包含完整运行结果与可视化脚本,可直接在MATLAB 2019a中…

作者头像 李华
网站建设 2026/9/14 0:00:27

Excel密码恢复工具Passper功能详解与实战技巧

1. Passper for Excel工具核心功能解析Passper for Excel是一款专注于Excel文件密码恢复和限制解除的专业工具,最新发布的v3.8.0版本在密码破解效率和成功率方面都有显著提升。这款工具主要解决两类常见问题:一是忘记Excel文件打开密码的情况&#xff0c…

作者头像 李华
网站建设 2026/9/13 23:58:04

计算机死机的时候,它在干什么?

今日, 花费些许分钟, 跟诸位分享一个极具趣味, 且能够增长知识的问题, 那便是: 当电脑出现死机状况的时候, 究竟正在做些什么?电脑死机,应该每个接触计算机的小伙伴都经历过吧。尤其是在早些年, 那时电脑配置不像现在这般高, 要是多开启几个重量级的应用程序, 死机…

作者头像 李华