···11+{ pkgs, lib, stdenv }:
22+/*
33+ Create a systemd portable service image
44+ https://systemd.io/PORTABLE_SERVICES/
55+66+ Example:
77+ pkgs.portableService {
88+ pname = "demo";
99+ version = "1.0";
1010+ units = [ demo-service demo-socket ];
1111+ }
1212+*/
1313+{
1414+ # The name and version of the portable service. The resulting image will be
1515+ # created in result/$pname_$version.raw
1616+ pname
1717+, version
1818+1919+ # Units is a list of derivations for systemd unit files. Those files will be
2020+ # copied to /etc/systemd/system in the resulting image. Note that the unit
2121+ # names must be prefixed with the name of the portable service.
2222+, units
2323+2424+ # Basic info about the portable service image, used for the generated
2525+ # /etc/os-release
2626+, description ? null
2727+, homepage ? null
2828+2929+ # A list of attribute sets {object, symlink}. Symlinks will be created
3030+ # in the root filesystem of the image to objects in the nix store.
3131+, symlinks ? [ ]
3232+3333+ # A list of additional derivations to be included in the image as-is.
3434+, contents ? [ ]
3535+3636+ # mksquashfs options
3737+, squashfsTools ? pkgs.squashfsTools
3838+, squash-compression ? "xz -Xdict-size 100%"
3939+, squash-block-size ? "1M"
4040+}:
4141+4242+let
4343+ filterNull = lib.filterAttrs (_: v: v != null);
4444+ envFileGenerator = lib.generators.toKeyValue { };
4545+4646+ rootFsScaffold =
4747+ let
4848+ os-release-params = {
4949+ PORTABLE_ID = pname;
5050+ PORTABLE_PRETTY_NAME = description;
5151+ HOME_URL = homepage;
5252+ ID = "nixos";
5353+ PRETTY_NAME = "NixOS";
5454+ BUILD_ID = "rolling";
5555+ };
5656+ os-release = pkgs.writeText "os-release"
5757+ (envFileGenerator (filterNull os-release-params));
5858+5959+ in
6060+ stdenv.mkDerivation {
6161+ pname = "root-fs-scaffold";
6262+ inherit version;
6363+6464+ buildCommand = ''
6565+ # scaffold a file system layout
6666+ mkdir -p $out/etc/systemd/system $out/proc $out/sys $out/dev $out/run \
6767+ $out/tmp $out/var/tmp $out/var/lib $out/var/cache $out/var/log
6868+6969+ # empty files to mount over with host's version
7070+ touch $out/etc/resolv.conf $out/etc/machine-id
7171+7272+ # required for portable services
7373+ cp ${os-release} $out/etc/os-release
7474+ ''
7575+ # units **must** be copied to /etc/systemd/system/
7676+ + (lib.concatMapStringsSep "\n" (u: "cp ${u} $out/etc/systemd/system/${u.name};") units)
7777+ + (lib.concatMapStringsSep "\n"
7878+ ({ object, symlink }: ''
7979+ mkdir -p $(dirname $out/${symlink});
8080+ ln -s ${object} $out/${symlink};
8181+ '')
8282+ symlinks)
8383+ ;
8484+ };
8585+in
8686+8787+assert lib.assertMsg (lib.all (u: lib.hasPrefix pname u.name) units) "Unit names must be prefixed with the service name";
8888+8989+stdenv.mkDerivation {
9090+ pname = "${pname}-img";
9191+ inherit version;
9292+9393+ nativeBuildInputs = [ squashfsTools ];
9494+ closureInfo = pkgs.closureInfo { rootPaths = [ rootFsScaffold ] ++ contents; };
9595+9696+ buildCommand = ''
9797+ mkdir -p nix/store
9898+ for i in $(< $closureInfo/store-paths); do
9999+ cp -a "$i" "''${i:1}"
100100+ done
101101+102102+ mkdir -p $out
103103+ # the '.raw' suffix is mandatory by the portable service spec
104104+ mksquashfs nix ${rootFsScaffold}/* $out/"${pname}_${version}.raw" \
105105+ -quiet -noappend \
106106+ -exit-on-error \
107107+ -keep-as-directory \
108108+ -all-root -root-mode 755 \
109109+ -b ${squash-block-size} -comp ${squash-compression}
110110+ '';
111111+}