{ pkgs, lib, stdenv }:
/*
  Create a systemd portable service image
  https://systemd.io/PORTABLE_SERVICES/

  Example:
  pkgs.portableService {
    pname = "demo";
    version = "1.0";
    units = [ demo-service demo-socket ];
  }
*/
{
  # The name and version of the portable service. The resulting image will be
  # created in result/$pname_$version.raw
  pname
, version

  # Units is a list of derivations for systemd unit files. Those files will be
  # copied to /etc/systemd/system in the resulting image. Note that the unit
  # names must be prefixed with the name of the portable service.
, units

  # Basic info about the portable service image, used for the generated
  # /etc/os-release
, description ? null
, homepage ? null

  # A list of attribute sets {object, symlink}. Symlinks will be created
  # in the root filesystem of the image to objects in the nix store.
, symlinks ? [ ]

  # A list of additional derivations to be included in the image as-is.
, contents ? [ ]

  # mksquashfs options
, squashfsTools ? pkgs.squashfsTools
, squash-compression ? "xz -Xdict-size 100%"
, squash-block-size ? "1M"
}:

let
  filterNull = lib.filterAttrs (_: v: v != null);
  envFileGenerator = lib.generators.toKeyValue { };

  rootFsScaffold =
    let
      os-release-params = {
        PORTABLE_ID = pname;
        PORTABLE_PRETTY_NAME = description;
        HOME_URL = homepage;
        ID = "nixos";
        PRETTY_NAME = "NixOS";
        BUILD_ID = "rolling";
      };
      os-release = pkgs.writeText "os-release"
        (envFileGenerator (filterNull os-release-params));

    in
    stdenv.mkDerivation {
      pname = "root-fs-scaffold";
      inherit version;

      buildCommand = ''
        # scaffold a file system layout
        mkdir -p $out/etc/systemd/system $out/proc $out/sys $out/dev $out/run \
                 $out/tmp $out/var/tmp $out/var/lib $out/var/cache $out/var/log

        # empty files to mount over with host's version
        touch $out/etc/resolv.conf $out/etc/machine-id

        # required for portable services
        cp ${os-release} $out/etc/os-release
      ''
      # units **must** be copied to /etc/systemd/system/
      + (lib.concatMapStringsSep "\n" (u: "cp ${u} $out/etc/systemd/system/${u.name};") units)
      + (lib.concatMapStringsSep "\n"
        ({ object, symlink }: ''
          mkdir -p $(dirname $out/${symlink});
          ln -s ${object} $out/${symlink};
        '')
        symlinks)
      ;
    };
in

assert lib.assertMsg (lib.all (u: lib.hasPrefix pname u.name) units) "Unit names must be prefixed with the service name";

stdenv.mkDerivation {
  pname = "${pname}-img";
  inherit version;

  nativeBuildInputs = [ squashfsTools ];
  closureInfo = pkgs.closureInfo { rootPaths = [ rootFsScaffold ] ++ contents; };

  buildCommand = ''
    mkdir -p nix/store
    for i in $(< $closureInfo/store-paths); do
      cp -a "$i" "''${i:1}"
    done

    mkdir -p $out
    # the '.raw' suffix is mandatory by the portable service spec
    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}
  '';
}
