~ruther/nix-tmpactivator

2bad14e6e82ef12c6a89ecc0ea609404a4cb3b9c — Rutherther 11 months ago
initial commit
A  => default.nix +17 -0
@@ 1,17 @@
{ pkgs ? import <nixpkgs> {} }:

let
  inherit (pkgs) lib;
  tmpLib = import ./lib { inherit pkgs; inherit (pkgs) lib; };
in pkgs.lib.evalModules {
  specialArgs = {
    inherit tmpLib pkgs;
    utils = import "${pkgs.path}/nixos/lib/utils.nix" { inherit lib pkgs; config = { systemd = { package = "systemd"; globalEnvironment = {}; }; }; };
  };
  modules = [
    ./modules/tmp-files.nix
    ./modules/home.nix
    ./modules/systemd.nix
    ./modules/config.nix
  ];
}

A  => flake.lock +26 -0
@@ 1,26 @@
{
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1713032949,
        "narHash": "sha256-WZR0/LpLkSsajw9uFwUCEWBA9QtWWRRBNScnvwjJHCM=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "44f5a5f39c795cf7a2281529e732b0dcd9427140",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "repo": "nixpkgs",
        "type": "github"
      }
    },
    "root": {
      "inputs": {
        "nixpkgs": "nixpkgs"
      }
    }
  },
  "root": "root",
  "version": 7
}

A  => flake.nix +24 -0
@@ 1,24 @@
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

  outputs = { self, nixpkgs }: let
    lib = nixpkgs.lib;
  in {

    tmpActivatorModules = {
      tmpActivator = import ./modules;
    };

    lib = {
      mkTmpActivator = { pkgs, modules, specialArgs ? {} }: lib.evalModules {
        specialArgs = specialArgs // {
          inherit pkgs;
          tmpLib = import ./lib { inherit pkgs; inherit (pkgs) lib; };
        };
        modules = [
          self.tmpActivatorModules.tmpActivator
        ] ++ modules;
      };
    };
  };
}

A  => lib/default.nix +95 -0
@@ 1,95 @@
{ pkgs, lib, ... }:

rec {
  escapeTmpFileContents = contents: builtins.replaceStrings ["\n"] ["\\n"] contents;
  mkTmpFile = { type ? "f", target, mode ? "0700", user, group, contents }: "${type} \"${target}\" ${mode} ${user} ${group} - ${escapeTmpFileContents contents}";
  mkRmTmpFile = { type ? "f", target }: "${if type == "d" then "R" else "r"} ${target}";

  tmpFileType = lib.types.submodule ({ config, ... }: {
    options = {
      enable = lib.mkOption {
        type = lib.types.bool;
        default = true;
      };

      type = lib.mkOption { type = lib.types.str; default = "f"; };
      mode = lib.mkOption { type = lib.types.str; default = "0400"; };
      user = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; };
      group = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; };

      executable = lib.mkOption {
        type = lib.types.bool;
        default = false;
      };

      target = lib.mkOption {
        type = lib.types.str;
      };
      source = lib.mkOption {
        type = lib.types.nullOr lib.types.path;
        default = null;
      };
      text = lib.mkOption {
        type = lib.types.nullOr lib.types.lines;
        default = null;
      };
    };
  });

  homeFileType = lib.types.attrsOf (lib.types.submodule ({ config, name, ... }: {
    options = {
      enable = lib.mkOption {
        type = lib.types.bool;
        default = true;
      };

      type = lib.mkOption { type = lib.types.str; default = "f"; };
      mode = lib.mkOption { type = lib.types.str; default = "0400"; };
      user = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; };
      group = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; };

      executable = lib.mkOption {
        type = lib.types.bool;
        default = false;
      };

      target = lib.mkOption {
        type = lib.types.str;
      };
      source = lib.mkOption {
        type = lib.types.nullOr lib.types.path;
        default = null;
      };
      text = lib.mkOption {
        type = lib.types.nullOr lib.types.lines;
        default = null;
      };
    };

    config = {
      target = lib.mkDefault name;
      source = lib.mkIf (config.text != null) (lib.mkDefault (pkgs.writeTextFile {
        inherit (config) text;
        executable = config.executable == true;
        name = storeFileName config.target;
      }));
    };
  }));

  # Figures out a valid Nix store name for the given path.
  storeFileName = path:
    let
      # All characters that are considered safe. Note "-" is not
      # included to avoid "-" followed by digit being interpreted as a
      # version.
      safeChars = [ "+" "." "_" "?" "=" ] ++ lib.lowerChars ++ lib.upperChars
        ++ lib.stringToCharacters "0123456789";

      empties = l: lib.genList (x: "") (lib.length l);

      unsafeInName =
        lib.stringToCharacters (lib.replaceStrings safeChars (empties safeChars) path);

      safeName = lib.replaceStrings unsafeInName (empties unsafeInName) path;
    in "home_" + safeName;
}

A  => modules/default.nix +14 -0
@@ 1,14 @@
{ lib, config, pkgs, ... }:

{
  imports = [
    ./tmpfiles.nix
    ./home.nix
    ./systemd.nix
  ];

  _module.args = {
    utils = import "${pkgs.path}/nixos/lib/utils.nix" { inherit lib config pkgs; };
    tmpLib = import ../lib { inherit pkgs; inherit (pkgs) lib; };
  };
}

A  => modules/home.nix +135 -0
@@ 1,135 @@
{ pkgs, tmpLib, lib, config, ... }:

let
  homeFiles = lib.filterAttrs (name: conf: conf.enable) config.home.file;

  sourceStorePath = file:
    let
      sourcePath = toString file.source;
      sourceName = tmpLib.storeFileName (baseNameOf sourcePath);
    in
      if builtins.hasContext sourcePath
      then file.source
      else builtins.path { path = file.source; name = sourceName; };
in {
  options = {
    home = {
      user = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = null;
      };

      group = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = null;
      };

      homeDirectory = lib.mkOption {
        type = lib.types.str;
        default = "/home/${config.home.user}";
      };

      file = lib.mkOption {
        type = tmpLib.homeFileType;
        default = {};
      };

      homeFilesPackage = lib.mkOption {
        type = lib.types.package;
      };
    };
  };

  config = lib.mkIf (config.home.user != null) {
    tmpfiles.defaultUser = config.home.user;
    tmpfiles.defaultGroup = config.home.group;

    tmpfiles.files = lib.attrValues (lib.mapAttrs (name: conf: {
      type = "L+";
      mode = "-";
      user = "-";
      group = "-";
      text = "${config.home.homeFilesPackage}/${name}";
      target = "${config.home.homeDirectory}/${name}";
    }) homeFiles);

    home.homeFilesPackage = pkgs.runCommandLocal "home-files" {
      nativeBuildInputs = [ pkgs.xorg.lndir ];
    }
    (''
        mkdir -p $out

        # Needed in case /nix is a symbolic link.
        realOut="$(realpath -m "$out")"

        function insertFile() {
          local source="$1"
          local relTarget="$2"
          local executable="$3"
          local recursive="$4"

          # If the target already exists then we have a collision. Note, this
          # should not happen due to the assertion found in the 'files' module.
          # We therefore simply log the conflict and otherwise ignore it, mainly
          # to make the `files-target-config` test work as expected.
          if [[ -e "$realOut/$relTarget" ]]; then
            echo "File conflict for file '$relTarget'" >&2
            return
          fi

          # Figure out the real absolute path to the target.
          local target
          target="$(realpath -m "$realOut/$relTarget")"

          # Target path must be within $HOME.
          if [[ ! $target == $realOut* ]] ; then
            echo "Error installing file '$relTarget' outside \$HOME" >&2
            exit 1
          fi

          mkdir -p "$(dirname "$target")"
          if [[ -d $source ]]; then
            if [[ $recursive ]]; then
              mkdir -p "$target"
              lndir -silent "$source" "$target"
            else
              ln -s "$source" "$target"
            fi
          else
            [[ -x $source ]] && isExecutable=1 || isExecutable=""

            # Link the file into the home file directory if possible,
            # i.e., if the executable bit of the source is the same we
            # expect for the target. Otherwise, we copy the file and
            # set the executable bit to the expected value.
            if [[ $executable == inherit || $isExecutable == $executable ]]; then
              ln -s "$source" "$target"
            else
              cp "$source" "$target"

              if [[ $executable == inherit ]]; then
                # Don't change file mode if it should match the source.
                :
              elif [[ $executable ]]; then
                chmod +x "$target"
              else
                chmod -x "$target"
              fi
            fi
          fi
        }
      '' + lib.concatStrings (
        lib.mapAttrsToList (n: v: ''
          insertFile ${
            lib.escapeShellArgs [
              (sourceStorePath v)
              n
              (if v.executable == null
               then "inherit"
               else toString v.executable)
               "false"
            ]}
        '') homeFiles
      ));
  };
}

A  => modules/systemd.nix +92 -0
@@ 1,92 @@
{ config, lib, utils, ... }:

let
  inherit (utils) systemdUtils;

  inherit
    (systemdUtils.lib)
    makeUnit
    generateUnits
    targetToUnit
    serviceToUnit
    sliceToUnit
    socketToUnit
    timerToUnit
    pathToUnit;

  cfg = config.systemd;

in {
  options = {
    systemd = {
      units = lib.mkOption {
        default = {};
        type = systemdUtils.types.units;
      };
      services = lib.mkOption {
        default = {};
        type = systemdUtils.types.services;
      };
      slices = lib.mkOption {
        default = {};
        type = systemdUtils.types.slices;
      };
      paths = lib.mkOption {
        default = {};
        type = systemdUtils.types.paths;
      };
      sockets = lib.mkOption {
        default = {};
        type = systemdUtils.types.sockets;
      };
      targets = lib.mkOption {
        default = {};
        type = systemdUtils.types.targets;
      };
      timers = lib.mkOption {
        default = {};
        type = systemdUtils.types.timers;
      };

      unitsPackage = lib.mkOption {
        type = lib.types.package;
      };

      package = lib.mkOption {
        default = "systemd";
      };
      globalEnvironment = lib.mkOption {
        default = {};
      };
    };

  };

  config = {
    systemd.units = with lib;
         mapAttrs' (n: v: nameValuePair "${n}.path"    (pathToUnit    n v)) cfg.paths
      // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
      // mapAttrs' (n: v: nameValuePair "${n}.slice"   (sliceToUnit   n v)) cfg.slices
      // mapAttrs' (n: v: nameValuePair "${n}.socket"  (socketToUnit  n v)) cfg.sockets
      // mapAttrs' (n: v: nameValuePair "${n}.target"  (targetToUnit  n v)) cfg.targets
      // mapAttrs' (n: v: nameValuePair "${n}.timer"   (timerToUnit   n v)) cfg.timers;

    systemd.unitsPackage = generateUnits {
      type = "user";
      inherit (cfg) units;
      upstreamUnits = [];
      upstreamWants = [];
      package = "systemd";
      packages = [];
    };

    # home.file.".config/systemd/user".source = generateUnits {
    #   type = "user";
    #   inherit (cfg) units;
    # };
    #
    home.file = lib.mapAttrs' (name: conf: (lib.nameValuePair ".config/systemd/user/${name}" {
      source = "${config.systemd.unitsPackage}/${name}";
    })) config.systemd.units;
  };
}

A  => modules/tmpfiles.nix +101 -0
@@ 1,101 @@
{ config, tmpLib, pkgs, lib, ... }:

let
  inherit (tmpLib) mkTmpFile;

  tmpFiles = lib.lists.filter (file: file.enable) config.tmpfiles.files;
in {
  options = {
    tmpfiles = {
      defaultUser = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
      };
      defaultGroup = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
      };

      files = lib.mkOption {
        type = lib.types.listOf tmpLib.tmpFileType;
        default = [];
        description = ''
          The files to configure.
        '';
      };

      configurationFileLines = lib.mkOption {
        type = lib.types.listOf lib.types.str;
      };

      configurationFile = lib.mkOption {
        type = lib.types.str;
      };

      removalConfigurationFile = lib.mkOption {
        type = lib.types.str;
      };

      configurationPackage = lib.mkOption {
        type = lib.types.package;
        description = ''
          This package contains the tmpfiles configuration package
        '';
      };

      activationPackage = lib.mkOption {
        type = lib.types.package;
        description = ''
          This package contains a script for activation of the tmp files using `systemd-tmpfiles`
        '';
      };
    };
  };

  config = {
    # assertions = builtins.map
    #   (file: {
    #     assertion = file.source == null || file.text == null;
    #     message = "Either text or source can be set, not both.";
    #   })
    # tmpFiles;
    #

    tmpfiles.configurationFileLines = builtins.map (file: (tmpLib.mkTmpFile ({
      type = file.type;
      target = file.target;
      mode = file.mode;
      user = if file.user == null then config.tmpfiles.defaultUser else file.user;
      group = if file.group == null then config.tmpfiles.defaultGroup else file.group;
      contents = if file.source != null then file.source else file.text;
    }))) tmpFiles;

    tmpfiles.configurationFile = pkgs.lib.concatStringsSep "\n" config.tmpfiles.configurationFileLines;

    tmpfiles.removalConfigurationFile = lib.concatStrings (builtins.map (file: tmpLib.mkRmTmpFile ({
      type = if file.type == "d" then "R" else "r";
      target = file.target;
    }) + "\n") tmpFiles);

    tmpfiles.configurationPackage = pkgs.runCommand "tmpfiles-configuration" {
      configuration = config.tmpfiles.configurationFile;
      removal = config.tmpfiles.removalConfigurationFile;
    } ''
      mkdir -p $out/share/tmpfiles $out/lib/tmpfiles.d
      echo -n "$configuration" > "$out/lib/tmpfiles.d/tmpfiles.conf"
      echo -n "$removal" > "$out/share/tmpfiles/rm-tmpfiles.conf"
    '';

    tmpfiles.activationPackage = pkgs.symlinkJoin {
      name = "tmpfiles-activation";

      paths = [
        (pkgs.writeShellScriptBin "activate" ''
          systemd-tmpfiles --create "${config.tmpfiles.configurationPackage}/lib/tmpfiles.d/tmpfiles.conf"
        '')
        (pkgs.writeShellScriptBin "deactivate" ''
          systemd-tmpfiles --remove "${config.tmpfiles.configurationPackage}/share/tmpfiles/rm-tmpfiles.conf"
        '')
        config.tmpfiles.configurationPackage
      ];
    };
  };
}

Do not follow this link