1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-12-21 00:11:11 +01:00

WIP home-manager: use putter for file management

This commit is contained in:
Robert Helgesson 2023-12-04 20:09:16 +01:00
parent 09de9577d4
commit 02c8ce9f92
No known key found for this signature in database
GPG key ID: 96E745BD17AA17ED
5 changed files with 342 additions and 150 deletions

226
flake.lock generated
View file

@ -1,5 +1,105 @@
{
"nodes": {
"crane": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": [
"putter",
"flake-utils"
],
"nixpkgs": [
"putter",
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1697588719,
"narHash": "sha256-n9ALgm3S+ygpzjesBkB9qutEtM4dtIkhn8WnstCPOew=",
"owner": "ipetkov",
"repo": "crane",
"rev": "da6b58e270d339a78a6e95728012ec2eea879612",
"type": "github"
},
"original": {
"owner": "ipetkov",
"ref": "v0.14.3",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696267196,
"narHash": "sha256-AAQ/2sD+0D18bb8hKuEEVpHUYD1GmO2Uh/taFamn6XQ=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "4f910c9827911b1ec2bf26b5a062cd09f8d89f85",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"putter",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1765472234,
@ -16,9 +116,133 @@
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1685801374,
"narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c37ca420157f4abc31e26f436c1145f8951ff373",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1699094435,
"narHash": "sha256-YLZ5/KKZ1PyLrm2MO8UxRe4H3M0/oaYqNhSlq6FDeeA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9d5d25bbfe8c0297ebe85324addcb5020ed1a454",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-utils": [
"putter",
"flake-utils"
],
"gitignore": "gitignore",
"nixpkgs": [
"putter",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1698852633,
"narHash": "sha256-Hsc/cCHud8ZXLvmm8pxrXpuaPEeNaaUttaCvtdX/Wug=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "dec10399e5b56aa95fcd530e0338be72ad6462a0",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"putter": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1704013409,
"narHash": "sha256-v7CTHSKcD6vnIwXRPav+3XETf+uNJz3G+RUF/SHZ+vE=",
"ref": "refs/heads/master",
"rev": "4d773d3aa9feca3af4578dc62cc6f91ebb16b002",
"revCount": 33,
"type": "git",
"url": "file:///home/rycee/devel/putter"
},
"original": {
"type": "git",
"url": "file:///home/rycee/devel/putter"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"putter": "putter"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"putter",
"crane",
"flake-utils"
],
"nixpkgs": [
"putter",
"crane",
"nixpkgs"
]
},
"locked": {
"lastModified": 1696299134,
"narHash": "sha256-RS77cAa0N+Sfj5EmKbm5IdncNXaBCE1BSSQvUE8exvo=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "611ccdceed92b4d94ae75328148d84ee4a5b462d",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},

View file

@ -2,11 +2,13 @@
description = "Home Manager for Nix";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.putter.url = "git+file:///home/rycee/devel/putter";
outputs =
{
self,
nixpkgs,
putter,
...
}:
{

View file

@ -2,6 +2,7 @@
pkgs,
config,
lib,
putter,
...
}:
@ -30,6 +31,8 @@ let
name = sourceName;
};
putterStatePath = "${config.xdg.stateHome}/home-manager/putter-state.json";
in
{
@ -45,6 +48,14 @@ in
internal = true;
description = "Package to contain all home files";
};
home.internal = {
filePutterConfig = lib.mkOption {
type = lib.types.package;
internal = true;
description = "Putter configuration.";
};
};
};
config = {
@ -91,156 +102,17 @@ in
# This verifies that the links we are about to create will not
# overwrite an existing file.
home.activation.checkLinkTargets = lib.hm.dag.entryBefore [ "writeBoundary" ] (
let
# Paths that should be forcibly overwritten by Home Manager.
# Caveat emptor!
forcedPaths = lib.concatMapStringsSep " " (p: ''"$HOME"/${lib.escapeShellArg p}'') (
lib.mapAttrsToList (n: v: v.target) (lib.filterAttrs (n: v: v.force) cfg)
);
home.activation.checkLinkTargets = lib.hm.dag.entryBefore [ "writeBoundary" ] ''
${lib.getExe putter} check -v \
--state-file "${putterStatePath}" \
${config.home.internal.filePutterConfig}
'';
storeDir = lib.escapeShellArg builtins.storeDir;
check = pkgs.replaceVars ./files/check-link-targets.sh {
inherit (config.lib.bash) initHomeManagerLib;
inherit forcedPaths storeDir;
};
in
''
function checkNewGenCollision() {
local newGenFiles
newGenFiles="$(readlink -e "$newGenPath/home-files")"
find "$newGenFiles" \( -type f -or -type l \) \
-exec bash ${check} "$newGenFiles" {} +
}
checkNewGenCollision || exit 1
''
);
# This activation script will
#
# 1. Remove files from the old generation that are not in the new
# generation.
#
# 2. Symlink files from the new generation into $HOME.
#
# 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.
home.activation.linkGeneration = lib.hm.dag.entryAfter [ "writeBoundary" ] (
let
link = pkgs.writeShellScript "link" ''
${config.lib.bash.initHomeManagerLib}
newGenFiles="$1"
shift
for sourcePath in "$@" ; do
relativePath="''${sourcePath#$newGenFiles/}"
targetPath="$HOME/$relativePath"
if [[ -e "$targetPath" && ! -L "$targetPath" ]] ; then
if [[ -n "$HOME_MANAGER_BACKUP_COMMAND" ]] ; then
verboseEcho "Running $HOME_MANAGER_BACKUP_COMMAND $targetPath."
run $HOME_MANAGER_BACKUP_COMMAND "$targetPath" || errorEcho "Running `$HOME_MANAGER_BACKUP_COMMAND` on '$targetPath' failed."
elif [[ -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then
# The target exists, back it up
backup="$targetPath.$HOME_MANAGER_BACKUP_EXT"
if [[ -e "$backup" && -n "$HOME_MANAGER_BACKUP_OVERWRITE" ]]; then
run rm $VERBOSE_ARG "$backup"
fi
run mv $VERBOSE_ARG "$targetPath" "$backup" || errorEcho "Moving '$targetPath' failed!"
fi
fi
if [[ -e "$targetPath" && ! -L "$targetPath" ]] && cmp -s "$sourcePath" "$targetPath" ; then
# The target exists but is identical don't do anything.
verboseEcho "Skipping '$targetPath' as it is identical to '$sourcePath'"
else
# Place that symlink, --force
# This can still fail if the target is a directory, in which case we bail out.
run mkdir -p $VERBOSE_ARG "$(dirname "$targetPath")"
run ln -Tsf $VERBOSE_ARG "$sourcePath" "$targetPath" || exit 1
fi
done
'';
cleanup = pkgs.writeShellScript "cleanup" ''
${config.lib.bash.initHomeManagerLib}
# A symbolic link whose target path matches this pattern will be
# considered part of a Home Manager generation.
homeFilePattern="$(readlink -e ${lib.escapeShellArg builtins.storeDir})/*-home-manager-files/*"
newGenFiles="$1"
shift 1
for relativePath in "$@" ; do
targetPath="$HOME/$relativePath"
if [[ -e "$newGenFiles/$relativePath" ]] ; then
verboseEcho "Checking $targetPath: exists"
elif [[ ! "$(readlink "$targetPath")" == $homeFilePattern ]] ; then
warnEcho "Path '$targetPath' does not link into a Home Manager generation. Skipping delete."
else
verboseEcho "Checking $targetPath: gone (deleting)"
run rm $VERBOSE_ARG "$targetPath"
# 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.
run rmdir $VERBOSE_ARG \
-p --ignore-fail-on-non-empty \
"$targetDir"
popd > /dev/null
fi
fi
done
'';
in
''
function linkNewGen() {
_i "Creating home file links in %s" "$HOME"
local newGenFiles
newGenFiles="$(readlink -e "$newGenPath/home-files")"
find "$newGenFiles" \( -type f -or -type l \) \
-exec bash ${link} "$newGenFiles" {} +
}
function cleanOldGen() {
if [[ ! -v oldGenPath || ! -e "$oldGenPath/home-files" ]] ; then
return
fi
_i "Cleaning up orphan links from %s" "$HOME"
local newGenFiles oldGenFiles
newGenFiles="$(readlink -e "$newGenPath/home-files")"
oldGenFiles="$(readlink -e "$oldGenPath/home-files")"
# 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"
}
cleanOldGen
linkNewGen
''
);
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
@ -286,6 +158,18 @@ in
'') (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 =

View file

@ -906,6 +906,7 @@ in
--subst-var-by GENERATION_DIR $out
ln -s ${config.home-files} $out/home-files
ln -s ${config.home.internal.filePutterConfig} $out/putter.json
ln -s ${cfg.path} $out/home-path
cp "$extraDependenciesPath" "$out/extra-dependencies"

81
modules/lib/putter.nix Normal file
View file

@ -0,0 +1,81 @@
# Contains some handy functions for generating Putter file manifests.
{ lib }:
let
inherit (lib)
concatMap
concatLists
mapAttrsToList
hasPrefix
filter
;
in
{
# Converts a Home Manager style list of file specifications into a Putter
# configuration.
#
# Note, the interface of this function is not considered stable, it may change
# as the needs of Home Manager change.
mkPutterManifest =
{
putterStatePath,
sourceBaseDirectory,
targetBaseDirectory,
fileEntries,
}:
let
# Convert a directory to a Putter configuration. Basically, this will
# create a file entry for each file in the directory. Any sub-directories
# will be handled recursively.
mkDirEntry =
f:
concatLists (
mapAttrsToList (
n: v:
let
f' = f // {
source = "${f.source}/${n}";
target = "${f.target}/${n}";
};
in
mkEntriesForType f' v
) (builtins.readDir f.source)
);
mkEntriesForType =
f: t:
if t == "regular" || t == "symlink" then
mkFileEntry f
else if t == "directory" then
mkDirEntry f
else
throw "unexpected file type ${t}";
# Create a file entry for the given file.
mkFileEntry = f: [
{
collision.resolution = if f.force then "force" else "abort";
action.type = "symlink";
source = "${sourceBaseDirectory}/${f.target}";
target = (if hasPrefix "/" f.target then "" else "${targetBaseDirectory}/") + f.target;
}
];
# Given a Home Manager file entry, produce a list of Putter entries. For
# recursive HM file entries, we recursively traverse the source directory
# and generate a Putter entry for each file we encounter.
mkEntries = f: if f.recursive then mkEntriesForType f "directory" else mkFileEntry f;
putterJson = {
version = "1";
state = putterStatePath;
files = concatMap mkEntries (filter (f: f.enable) fileEntries);
};
putterJsonText = builtins.toJSON putterJson;
in
putterJsonText;
}