1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-12-21 08:21:12 +01:00
home-manager/modules/files.nix
2025-12-16 18:49:58 +01:00

264 lines
8.1 KiB
Nix

{
pkgs,
config,
lib,
putter,
...
}:
let
cfg = lib.filterAttrs (n: f: f.enable) config.home.file;
homeDirectory = config.home.homeDirectory;
fileType =
(import lib/file-type.nix {
inherit homeDirectory lib pkgs;
}).fileType;
sourceStorePath =
file:
let
sourcePath = toString file.source;
sourceName = config.lib.strings.storeFileName (baseNameOf sourcePath);
in
if builtins.hasContext sourcePath then
file.source
else
builtins.path {
path = file.source;
name = sourceName;
};
putterStatePath = "${config.xdg.stateHome}/home-manager/putter-state.json";
in
{
options = {
home.file = lib.mkOption {
description = "Attribute set of files to link into the user home.";
default = { };
type = fileType "home.file" "{env}`HOME`" homeDirectory;
};
home-files = lib.mkOption {
type = lib.types.package;
internal = true;
description = "Package to contain all home files";
};
home.internal = {
filePutterConfig = lib.mkOption {
type = lib.types.package;
internal = true;
description = "Putter configuration.";
};
};
};
config = {
assertions = [
(
let
dups = lib.attrNames (
lib.filterAttrs (n: v: v > 1) (
lib.foldAttrs (acc: v: acc + v) 0 (lib.mapAttrsToList (n: v: { ${v.target} = 1; }) cfg)
)
);
dupsStr = lib.concatStringsSep ", " dups;
in
{
assertion = dups == [ ];
message = ''
Conflicting managed target files: ${dupsStr}
This may happen, for example, if you have a configuration similar to
home.file = {
conflict1 = { source = ./foo.nix; target = "baz"; };
conflict2 = { source = ./bar.nix; target = "baz"; };
}'';
}
)
];
# Using this function it is possible to make `home.file` create a
# symlink to a path outside the Nix store. For example, a Home Manager
# configuration containing
#
# `home.file."foo".source = config.lib.file.mkOutOfStoreSymlink ./bar;`
#
# would upon activation create a symlink `~/foo` that points to the
# absolute path of the `bar` file relative the configuration file.
lib.file.mkOutOfStoreSymlink =
path:
let
pathStr = toString path;
name = lib.hm.strings.storeFileName (baseNameOf pathStr);
in
pkgs.runCommandLocal name { } ''ln -s ${lib.escapeShellArg pathStr} $out'';
# This verifies that the links we are about to create will not
# overwrite an existing file.
home.activation.checkLinkTargets = lib.hm.dag.entryBefore [ "writeBoundary" ] ''
${lib.getExe putter} check -v \
--state-file "${putterStatePath}" \
${config.home.internal.filePutterConfig}
'';
home.activation.linkGeneration = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
${lib.getExe putter} apply $VERBOSE_ARG -v ''${DRY_RUN:+--dry-run} \
--state-file "${putterStatePath}" \
${config.home.internal.filePutterConfig}
'';
home.activation.checkFilesChanged = lib.hm.dag.entryBefore [ "linkGeneration" ] (
let
homeDirArg = lib.escapeShellArg homeDirectory;
in
''
function _cmp() {
if [[ -d $1 && -d $2 ]]; then
diff -rq "$1" "$2" &> /dev/null
else
cmp --quiet "$1" "$2"
fi
}
declare -A changedFiles
''
+ lib.concatMapStrings (
v:
let
sourceArg = lib.escapeShellArg (sourceStorePath v);
targetArg = lib.escapeShellArg v.target;
in
''
_cmp ${sourceArg} ${homeDirArg}/${targetArg} \
&& changedFiles[${targetArg}]=0 \
|| changedFiles[${targetArg}]=1
''
) (lib.filter (v: v.onChange != "") (lib.attrValues cfg))
+ ''
unset -f _cmp
''
);
home.activation.onFilesChange = lib.hm.dag.entryAfter [ "linkGeneration" ] (
lib.concatMapStrings (v: ''
if (( ''${changedFiles[${lib.escapeShellArg v.target}]} == 1 )); then
if [[ -v DRY_RUN || -v VERBOSE ]]; then
echo "Running onChange hook for" ${lib.escapeShellArg v.target}
fi
if [[ ! -v DRY_RUN ]]; then
${v.onChange}
fi
fi
'') (lib.filter (v: v.onChange != "") (lib.attrValues cfg))
);
home.internal.filePutterConfig =
let
putter = import ./lib/putter.nix { inherit lib; };
manifest = putter.mkPutterManifest {
inherit putterStatePath;
sourceBaseDirectory = config.home-files;
targetBaseDirectory = config.home.homeDirectory;
fileEntries = attrValues cfg;
};
in
pkgs.writeText "hm-putter.json" manifest;
# Symlink directories and files that have the right execute bit.
# Copy files that need their execute bit changed.
home-files =
pkgs.runCommandLocal "home-manager-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"
local ignorelinks="$5"
# 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"
if [[ $ignorelinks ]]; then
lndir -silent -ignorelinks "$source" "$target"
else
lndir -silent "$source" "$target"
fi
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)
v.target
(if v.executable == null then "inherit" else toString v.executable)
(toString v.recursive)
(toString v.ignorelinks)
]
}
'') cfg
)
);
};
}