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
+ ];
+ };
+ };
+}