2017-10-05 19:38:46 +00:00
|
|
|
|
{ pkgs, config, lib, ... }:
|
|
|
|
|
|
|
|
|
|
with lib;
|
|
|
|
|
|
|
|
|
|
let
|
|
|
|
|
|
2023-01-25 21:58:35 +00:00
|
|
|
|
cfg = filterAttrs (n: f: f.enable) config.home.file;
|
2017-11-06 09:28:42 +00:00
|
|
|
|
|
2017-10-05 22:19:03 +00:00
|
|
|
|
homeDirectory = config.home.homeDirectory;
|
2017-10-05 19:38:46 +00:00
|
|
|
|
|
2017-11-06 09:28:54 +00:00
|
|
|
|
fileType = (import lib/file-type.nix {
|
|
|
|
|
inherit homeDirectory lib pkgs;
|
|
|
|
|
}).fileType;
|
2017-11-06 09:28:42 +00:00
|
|
|
|
|
2019-01-16 01:14:14 +00:00
|
|
|
|
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; };
|
|
|
|
|
|
2017-10-05 19:38:46 +00:00
|
|
|
|
in
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
options = {
|
|
|
|
|
home.file = mkOption {
|
2023-07-01 23:45:18 +00:00
|
|
|
|
description = "Attribute set of files to link into the user home.";
|
2017-10-05 19:38:46 +00:00
|
|
|
|
default = {};
|
2023-06-30 05:22:38 +00:00
|
|
|
|
type = fileType "home.file" "{env}`HOME`" homeDirectory;
|
2017-10-05 19:38:46 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
home-files = mkOption {
|
|
|
|
|
type = types.package;
|
|
|
|
|
internal = true;
|
2023-07-01 23:45:18 +00:00
|
|
|
|
description = "Package to contain all home files";
|
2017-10-05 19:38:46 +00:00
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
config = {
|
2021-06-27 14:42:45 +00:00
|
|
|
|
assertions = [(
|
|
|
|
|
let
|
|
|
|
|
dups =
|
|
|
|
|
attrNames
|
|
|
|
|
(filterAttrs (n: v: v > 1)
|
|
|
|
|
(foldAttrs (acc: v: acc + v) 0
|
|
|
|
|
(mapAttrsToList (n: v: { ${v.target} = 1; }) cfg)));
|
|
|
|
|
dupsStr = 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"; };
|
|
|
|
|
}'';
|
|
|
|
|
})
|
|
|
|
|
];
|
|
|
|
|
|
2020-05-01 23:06:41 +00:00
|
|
|
|
lib.file.mkOutOfStoreSymlink = path:
|
|
|
|
|
let
|
|
|
|
|
pathStr = toString path;
|
|
|
|
|
name = hm.strings.storeFileName (baseNameOf pathStr);
|
|
|
|
|
in
|
|
|
|
|
pkgs.runCommandLocal name {} ''ln -s ${escapeShellArg pathStr} $out'';
|
|
|
|
|
|
2017-10-05 19:38:46 +00:00
|
|
|
|
# This verifies that the links we are about to create will not
|
|
|
|
|
# overwrite an existing file.
|
2020-01-16 22:41:14 +00:00
|
|
|
|
home.activation.checkLinkTargets = hm.dag.entryBefore ["writeBoundary"] (
|
2017-10-05 19:38:46 +00:00
|
|
|
|
let
|
2020-03-17 21:27:29 +00:00
|
|
|
|
# Paths that should be forcibly overwritten by Home Manager.
|
|
|
|
|
# Caveat emptor!
|
|
|
|
|
forcedPaths =
|
2019-10-29 02:29:12 +00:00
|
|
|
|
concatMapStringsSep " " (p: ''"$HOME"/${escapeShellArg p}'')
|
2020-03-17 21:27:29 +00:00
|
|
|
|
(mapAttrsToList (n: v: v.target)
|
|
|
|
|
(filterAttrs (n: v: v.force) cfg));
|
|
|
|
|
|
2024-04-05 13:05:00 +00:00
|
|
|
|
storeDir = escapeShellArg builtins.storeDir;
|
|
|
|
|
|
|
|
|
|
check = pkgs.substituteAll {
|
|
|
|
|
src = ./files/check-link-targets.sh;
|
|
|
|
|
|
|
|
|
|
inherit (config.lib.bash) initHomeManagerLib;
|
|
|
|
|
inherit forcedPaths storeDir;
|
|
|
|
|
};
|
2017-10-05 19:38:46 +00:00
|
|
|
|
in
|
|
|
|
|
''
|
|
|
|
|
function checkNewGenCollision() {
|
|
|
|
|
local newGenFiles
|
|
|
|
|
newGenFiles="$(readlink -e "$newGenPath/home-files")"
|
2019-04-30 12:52:44 +00:00
|
|
|
|
find "$newGenFiles" \( -type f -or -type l \) \
|
2019-04-27 11:48:57 +00:00
|
|
|
|
-exec bash ${check} "$newGenFiles" {} +
|
2017-10-05 19:38:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
checkNewGenCollision || exit 1
|
|
|
|
|
''
|
|
|
|
|
);
|
|
|
|
|
|
2017-11-15 17:12:40 +00:00
|
|
|
|
# This activation script will
|
|
|
|
|
#
|
|
|
|
|
# 1. Remove files from the old generation that are not in the new
|
|
|
|
|
# generation.
|
|
|
|
|
#
|
2024-01-24 21:48:51 +00:00
|
|
|
|
# 2. Symlink files from the new generation into $HOME.
|
2017-11-15 17:12:40 +00:00
|
|
|
|
#
|
|
|
|
|
# This order is needed to ensure that we always know which links
|
|
|
|
|
# belong to which generation. Specifically, if we're moving from
|
|
|
|
|
# generation A to generation B having sets of home file links FA
|
|
|
|
|
# and FB, respectively then cleaning before linking produces state
|
|
|
|
|
# transitions similar to
|
|
|
|
|
#
|
|
|
|
|
# FA → FA ∩ FB → (FA ∩ FB) ∪ FB = FB
|
|
|
|
|
#
|
|
|
|
|
# and a failure during the intermediate state FA ∩ FB will not
|
|
|
|
|
# result in lost links because this set of links are in both the
|
|
|
|
|
# source and target generation.
|
2020-01-16 22:41:14 +00:00
|
|
|
|
home.activation.linkGeneration = hm.dag.entryAfter ["writeBoundary"] (
|
2017-10-05 19:38:46 +00:00
|
|
|
|
let
|
2021-06-27 21:31:20 +00:00
|
|
|
|
link = pkgs.writeShellScript "link" ''
|
2024-01-13 22:15:00 +00:00
|
|
|
|
${config.lib.bash.initHomeManagerLib}
|
|
|
|
|
|
2017-10-05 19:38:46 +00:00
|
|
|
|
newGenFiles="$1"
|
|
|
|
|
shift
|
|
|
|
|
for sourcePath in "$@" ; do
|
|
|
|
|
relativePath="''${sourcePath#$newGenFiles/}"
|
|
|
|
|
targetPath="$HOME/$relativePath"
|
2019-04-17 19:15:38 +00:00
|
|
|
|
if [[ -e "$targetPath" && ! -L "$targetPath" && -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then
|
2020-05-19 12:44:44 +00:00
|
|
|
|
# The target exists, back it up
|
2019-04-17 19:15:38 +00:00
|
|
|
|
backup="$targetPath.$HOME_MANAGER_BACKUP_EXT"
|
2024-01-13 22:15:00 +00:00
|
|
|
|
run mv $VERBOSE_ARG "$targetPath" "$backup" || errorEcho "Moving '$targetPath' failed!"
|
2019-04-17 19:15:38 +00:00
|
|
|
|
fi
|
2020-05-19 12:44:44 +00:00
|
|
|
|
|
2021-10-30 07:30:41 +00:00
|
|
|
|
if [[ -e "$targetPath" && ! -L "$targetPath" ]] && cmp -s "$sourcePath" "$targetPath" ; then
|
2020-05-19 12:44:44 +00:00
|
|
|
|
# The target exists but is identical – don't do anything.
|
2024-01-23 21:59:26 +00:00
|
|
|
|
verboseEcho "Skipping '$targetPath' as it is identical to '$sourcePath'"
|
2020-05-19 12:44:44 +00:00
|
|
|
|
else
|
|
|
|
|
# Place that symlink, --force
|
2023-01-09 12:00:21 +00:00
|
|
|
|
# This can still fail if the target is a directory, in which case we bail out.
|
2024-01-13 22:15:00 +00:00
|
|
|
|
run mkdir -p $VERBOSE_ARG "$(dirname "$targetPath")"
|
|
|
|
|
run ln -Tsf $VERBOSE_ARG "$sourcePath" "$targetPath" || exit 1
|
2020-05-19 12:44:44 +00:00
|
|
|
|
fi
|
2017-10-05 19:38:46 +00:00
|
|
|
|
done
|
|
|
|
|
'';
|
|
|
|
|
|
2021-06-27 21:31:20 +00:00
|
|
|
|
cleanup = pkgs.writeShellScript "cleanup" ''
|
2021-04-29 23:29:39 +00:00
|
|
|
|
${config.lib.bash.initHomeManagerLib}
|
2017-10-05 19:38:46 +00:00
|
|
|
|
|
2019-11-25 09:54:27 +00:00
|
|
|
|
# A symbolic link whose target path matches this pattern will be
|
|
|
|
|
# considered part of a Home Manager generation.
|
2019-10-29 02:29:12 +00:00
|
|
|
|
homeFilePattern="$(readlink -e ${escapeShellArg builtins.storeDir})/*-home-manager-files/*"
|
2019-11-25 09:54:27 +00:00
|
|
|
|
|
2017-10-05 19:38:46 +00:00
|
|
|
|
newGenFiles="$1"
|
2017-11-11 13:16:45 +00:00
|
|
|
|
shift 1
|
|
|
|
|
for relativePath in "$@" ; do
|
2017-10-05 19:38:46 +00:00
|
|
|
|
targetPath="$HOME/$relativePath"
|
|
|
|
|
if [[ -e "$newGenFiles/$relativePath" ]] ; then
|
2024-01-23 21:59:26 +00:00
|
|
|
|
verboseEcho "Checking $targetPath: exists"
|
2019-11-25 09:54:27 +00:00
|
|
|
|
elif [[ ! "$(readlink "$targetPath")" == $homeFilePattern ]] ; then
|
2020-05-17 01:35:05 +00:00
|
|
|
|
warnEcho "Path '$targetPath' does not link into a Home Manager generation. Skipping delete."
|
2017-10-05 19:38:46 +00:00
|
|
|
|
else
|
2024-01-23 21:59:26 +00:00
|
|
|
|
verboseEcho "Checking $targetPath: gone (deleting)"
|
2024-01-13 22:15:00 +00:00
|
|
|
|
run rm $VERBOSE_ARG "$targetPath"
|
2017-10-05 19:38:46 +00:00
|
|
|
|
|
|
|
|
|
# Recursively delete empty parent directories.
|
|
|
|
|
targetDir="$(dirname "$relativePath")"
|
|
|
|
|
if [[ "$targetDir" != "." ]] ; then
|
|
|
|
|
pushd "$HOME" > /dev/null
|
|
|
|
|
|
|
|
|
|
# Call rmdir with a relative path excluding $HOME.
|
|
|
|
|
# Otherwise, it might try to delete $HOME and exit
|
|
|
|
|
# with a permission error.
|
2024-01-13 22:15:00 +00:00
|
|
|
|
run rmdir $VERBOSE_ARG \
|
2017-10-05 19:38:46 +00:00
|
|
|
|
-p --ignore-fail-on-non-empty \
|
|
|
|
|
"$targetDir"
|
|
|
|
|
|
|
|
|
|
popd > /dev/null
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
'';
|
|
|
|
|
in
|
|
|
|
|
''
|
|
|
|
|
function linkNewGen() {
|
2021-04-29 23:29:39 +00:00
|
|
|
|
_i "Creating home file links in %s" "$HOME"
|
2017-11-15 17:31:04 +00:00
|
|
|
|
|
2017-10-05 19:38:46 +00:00
|
|
|
|
local newGenFiles
|
|
|
|
|
newGenFiles="$(readlink -e "$newGenPath/home-files")"
|
2019-04-30 12:52:44 +00:00
|
|
|
|
find "$newGenFiles" \( -type f -or -type l \) \
|
2019-04-27 11:48:57 +00:00
|
|
|
|
-exec bash ${link} "$newGenFiles" {} +
|
2017-10-05 19:38:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cleanOldGen() {
|
2022-03-26 14:04:19 +00:00
|
|
|
|
if [[ ! -v oldGenPath || ! -e "$oldGenPath/home-files" ]] ; then
|
2017-10-05 19:38:46 +00:00
|
|
|
|
return
|
|
|
|
|
fi
|
|
|
|
|
|
2021-04-29 23:29:39 +00:00
|
|
|
|
_i "Cleaning up orphan links from %s" "$HOME"
|
2017-10-05 19:38:46 +00:00
|
|
|
|
|
|
|
|
|
local newGenFiles oldGenFiles
|
|
|
|
|
newGenFiles="$(readlink -e "$newGenPath/home-files")"
|
|
|
|
|
oldGenFiles="$(readlink -e "$oldGenPath/home-files")"
|
2017-11-11 13:16:45 +00:00
|
|
|
|
|
|
|
|
|
# Apply the cleanup script on each leaf in the old
|
|
|
|
|
# generation. The find command below will print the
|
|
|
|
|
# relative path of the entry.
|
|
|
|
|
find "$oldGenFiles" '(' -type f -or -type l ')' -printf '%P\0' \
|
|
|
|
|
| xargs -0 bash ${cleanup} "$newGenFiles"
|
2017-10-05 19:38:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-15 17:12:40 +00:00
|
|
|
|
cleanOldGen
|
2017-10-05 19:38:46 +00:00
|
|
|
|
linkNewGen
|
|
|
|
|
''
|
|
|
|
|
);
|
|
|
|
|
|
2020-01-16 22:41:14 +00:00
|
|
|
|
home.activation.checkFilesChanged = hm.dag.entryBefore ["linkGeneration"] (
|
2021-06-27 21:31:20 +00:00
|
|
|
|
let
|
|
|
|
|
homeDirArg = escapeShellArg homeDirectory;
|
|
|
|
|
in ''
|
2021-05-20 20:12:45 +00:00
|
|
|
|
function _cmp() {
|
|
|
|
|
if [[ -d $1 && -d $2 ]]; then
|
|
|
|
|
diff -rq "$1" "$2" &> /dev/null
|
|
|
|
|
else
|
|
|
|
|
cmp --quiet "$1" "$2"
|
|
|
|
|
fi
|
|
|
|
|
}
|
2018-02-16 23:32:29 +00:00
|
|
|
|
declare -A changedFiles
|
2021-06-27 21:31:20 +00:00
|
|
|
|
'' + concatMapStrings (v:
|
|
|
|
|
let
|
|
|
|
|
sourceArg = escapeShellArg (sourceStorePath v);
|
|
|
|
|
targetArg = escapeShellArg v.target;
|
|
|
|
|
in ''
|
|
|
|
|
_cmp ${sourceArg} ${homeDirArg}/${targetArg} \
|
|
|
|
|
&& changedFiles[${targetArg}]=0 \
|
|
|
|
|
|| changedFiles[${targetArg}]=1
|
|
|
|
|
'') (filter (v: v.onChange != "") (attrValues cfg))
|
2021-05-20 20:12:45 +00:00
|
|
|
|
+ ''
|
|
|
|
|
unset -f _cmp
|
|
|
|
|
''
|
2018-02-16 23:32:29 +00:00
|
|
|
|
);
|
|
|
|
|
|
2020-01-16 22:41:14 +00:00
|
|
|
|
home.activation.onFilesChange = hm.dag.entryAfter ["linkGeneration"] (
|
2018-02-16 23:32:29 +00:00
|
|
|
|
concatMapStrings (v: ''
|
2021-07-31 22:54:47 +00:00
|
|
|
|
if (( ''${changedFiles[${escapeShellArg v.target}]} == 1 )); then
|
|
|
|
|
if [[ -v DRY_RUN || -v VERBOSE ]]; then
|
|
|
|
|
echo "Running onChange hook for" ${escapeShellArg v.target}
|
|
|
|
|
fi
|
|
|
|
|
if [[ ! -v DRY_RUN ]]; then
|
|
|
|
|
${v.onChange}
|
|
|
|
|
fi
|
2018-02-16 23:32:29 +00:00
|
|
|
|
fi
|
|
|
|
|
'') (filter (v: v.onChange != "") (attrValues cfg))
|
|
|
|
|
);
|
|
|
|
|
|
2019-03-18 00:25:14 +00:00
|
|
|
|
# Symlink directories and files that have the right execute bit.
|
|
|
|
|
# Copy files that need their execute bit changed.
|
2021-06-27 21:31:20 +00:00
|
|
|
|
home-files = pkgs.runCommandLocal
|
2019-03-18 00:25:14 +00:00
|
|
|
|
"home-manager-files"
|
|
|
|
|
{
|
2020-06-05 05:26:03 +00:00
|
|
|
|
nativeBuildInputs = [ pkgs.xorg.lndir ];
|
2019-03-18 00:25:14 +00:00
|
|
|
|
}
|
|
|
|
|
(''
|
2017-11-06 09:28:50 +00:00
|
|
|
|
mkdir -p $out
|
|
|
|
|
|
2019-11-22 09:54:29 +00:00
|
|
|
|
# Needed in case /nix is a symbolic link.
|
|
|
|
|
realOut="$(realpath -m "$out")"
|
|
|
|
|
|
2017-11-06 09:28:50 +00:00
|
|
|
|
function insertFile() {
|
|
|
|
|
local source="$1"
|
|
|
|
|
local relTarget="$2"
|
|
|
|
|
local executable="$3"
|
2017-12-11 14:14:45 +00:00
|
|
|
|
local recursive="$4"
|
2017-11-06 09:28:50 +00:00
|
|
|
|
|
2021-06-27 14:42:45 +00:00
|
|
|
|
# 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
|
|
|
|
|
|
2017-11-06 09:28:50 +00:00
|
|
|
|
# Figure out the real absolute path to the target.
|
|
|
|
|
local target
|
2019-11-22 09:54:29 +00:00
|
|
|
|
target="$(realpath -m "$realOut/$relTarget")"
|
2017-11-06 09:28:50 +00:00
|
|
|
|
|
|
|
|
|
# Target path must be within $HOME.
|
2019-11-22 09:54:29 +00:00
|
|
|
|
if [[ ! $target == $realOut* ]] ; then
|
2017-11-06 09:28:50 +00:00
|
|
|
|
echo "Error installing file '$relTarget' outside \$HOME" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2017-10-05 19:38:46 +00:00
|
|
|
|
|
2017-11-06 09:28:50 +00:00
|
|
|
|
mkdir -p "$(dirname "$target")"
|
|
|
|
|
if [[ -d $source ]]; then
|
2017-11-06 17:00:25 +00:00
|
|
|
|
if [[ $recursive ]]; then
|
|
|
|
|
mkdir -p "$target"
|
|
|
|
|
lndir -silent "$source" "$target"
|
|
|
|
|
else
|
|
|
|
|
ln -s "$source" "$target"
|
|
|
|
|
fi
|
2017-11-06 09:28:50 +00:00
|
|
|
|
else
|
|
|
|
|
[[ -x $source ]] && isExecutable=1 || isExecutable=""
|
2017-11-11 23:49:36 +00:00
|
|
|
|
|
|
|
|
|
# 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.
|
2017-12-02 21:52:35 +00:00
|
|
|
|
if [[ $executable == inherit || $isExecutable == $executable ]]; then
|
2017-11-06 09:28:50 +00:00
|
|
|
|
ln -s "$source" "$target"
|
|
|
|
|
else
|
|
|
|
|
cp "$source" "$target"
|
2017-11-11 23:49:36 +00:00
|
|
|
|
|
|
|
|
|
if [[ $executable == inherit ]]; then
|
|
|
|
|
# Don't change file mode if it should match the source.
|
|
|
|
|
:
|
|
|
|
|
elif [[ $executable ]]; then
|
2017-11-06 09:28:50 +00:00
|
|
|
|
chmod +x "$target"
|
2017-10-05 19:38:46 +00:00
|
|
|
|
else
|
2017-11-06 09:28:50 +00:00
|
|
|
|
chmod -x "$target"
|
2017-10-05 19:38:46 +00:00
|
|
|
|
fi
|
2017-11-06 09:28:50 +00:00
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
'' + concatStrings (
|
|
|
|
|
mapAttrsToList (n: v: ''
|
2020-08-29 16:22:03 +00:00
|
|
|
|
insertFile ${
|
|
|
|
|
escapeShellArgs [
|
|
|
|
|
(sourceStorePath v)
|
|
|
|
|
v.target
|
|
|
|
|
(if v.executable == null
|
|
|
|
|
then "inherit"
|
|
|
|
|
else toString v.executable)
|
|
|
|
|
(toString v.recursive)
|
|
|
|
|
]}
|
2017-11-06 09:28:50 +00:00
|
|
|
|
'') cfg
|
2019-03-18 00:25:14 +00:00
|
|
|
|
));
|
2017-10-05 19:38:46 +00:00
|
|
|
|
};
|
|
|
|
|
}
|