mirror of
https://github.com/nix-community/home-manager.git
synced 2026-06-05 21:02:51 +00:00
home-manager: optionally use putter for file management
The Putter tool is a Rust implementation of the file management component of Home Manager. It takes a JSON manifest that indicates how files should be symlinked and when the manifest is applied, does its best to ensure that the file system reflect the manifest. Putter offers some additional features that Home Manager does not expose today, such as file copying (potentially recursive) and support for overriding files within a recursive tree (e.g., inside a symlinked tree one could ensure that a specific file is copies with specific permissions). This is considered highly experimental at the moment and to use Putter one must set a hidden option.
This commit is contained in:
@@ -55,6 +55,8 @@ let
|
||||
name = sourceName;
|
||||
};
|
||||
|
||||
putterStatePath = "${config.xdg.stateHome}/home-manager/putter-state.json";
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
@@ -88,11 +90,44 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
home.fileActivator = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
enum [
|
||||
"legacy"
|
||||
"putter"
|
||||
];
|
||||
default = "legacy";
|
||||
example = "putter";
|
||||
visible = false;
|
||||
description = ''
|
||||
The tooling to use to place files during activation.
|
||||
|
||||
The legacy option (currently the default) is the built-in tooling that
|
||||
is very robust, but is limited in future potential.
|
||||
|
||||
The putter option is a new external tool that may replace the legacy
|
||||
alternative in the future. It is not as hardened as the legacy
|
||||
alternative but will allow future features such as file copying.
|
||||
|
||||
This option should be considered experimental and is therefore hidden
|
||||
from documentation at this time.
|
||||
'';
|
||||
};
|
||||
|
||||
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 = {
|
||||
@@ -149,21 +184,29 @@ in
|
||||
|
||||
storeDir = lib.escapeShellArg builtins.storeDir;
|
||||
|
||||
check = pkgs.replaceVars ./files/check-link-targets.sh {
|
||||
legacyCheckScript = 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
|
||||
''
|
||||
legacyCheckLinkTargets = ''
|
||||
function checkNewGenCollision() {
|
||||
local newGenFiles
|
||||
newGenFiles="$(readlink -e "$newGenPath/home-files")"
|
||||
find "$newGenFiles" \( -type f -or -type l \) \
|
||||
-exec bash ${legacyCheckScript} "$newGenFiles" {} +
|
||||
}
|
||||
|
||||
checkNewGenCollision || exit 1
|
||||
'';
|
||||
|
||||
putterCheckLinkTargets = ''
|
||||
${lib.getExe pkgs.putter} check $VERBOSE_ARG \
|
||||
--state-file "${putterStatePath}" \
|
||||
${config.home.internal.filePutterConfig}
|
||||
'';
|
||||
in
|
||||
if config.home.fileActivator == "putter" then putterCheckLinkTargets else legacyCheckLinkTargets
|
||||
);
|
||||
|
||||
# This activation script will
|
||||
@@ -186,7 +229,9 @@ in
|
||||
# source and target generation.
|
||||
home.activation.linkGeneration = lib.hm.dag.entryAfter [ "writeBoundary" ] (
|
||||
let
|
||||
link = pkgs.writeShellScript "link" ''
|
||||
storeDir = lib.escapeShellArg builtins.storeDir;
|
||||
|
||||
legacyLink = pkgs.writeShellScript "link" ''
|
||||
${config.lib.bash.initHomeManagerLib}
|
||||
|
||||
newGenFiles="$1"
|
||||
@@ -220,12 +265,12 @@ in
|
||||
done
|
||||
'';
|
||||
|
||||
cleanup = pkgs.writeShellScript "cleanup" ''
|
||||
legacyCleanup = 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/*"
|
||||
homeFilePattern="$(readlink -e ${storeDir})/*-home-manager-files/*"
|
||||
|
||||
newGenFiles="$1"
|
||||
shift 1
|
||||
@@ -256,38 +301,81 @@ in
|
||||
fi
|
||||
done
|
||||
'';
|
||||
|
||||
# If Putter is not enabled, then generate a fake state file to allow
|
||||
# switching to Putter in the future.
|
||||
putterCompatState =
|
||||
let
|
||||
putter = import ./lib/putter.nix { inherit lib; };
|
||||
manifest = putter.mkPutterCompatState {
|
||||
sourceBaseDirectory = config.home-files;
|
||||
targetBaseDirectory = config.home.homeDirectory;
|
||||
fileEntries = cfg;
|
||||
};
|
||||
in
|
||||
pkgs.writeText "hm-putter-state.json" manifest;
|
||||
|
||||
# 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.
|
||||
legacyLinkGeneration = ''
|
||||
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 ${legacyLink} "$newGenFiles" {} +
|
||||
|
||||
# Copy in the Putter compatible state file. This is to allow a later
|
||||
# switchover to Putter.
|
||||
run install -Dp -m600 $VERBOSE_ARG ${putterCompatState} ${putterStatePath}
|
||||
}
|
||||
|
||||
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 ${legacyCleanup} "$newGenFiles"
|
||||
}
|
||||
|
||||
cleanOldGen
|
||||
linkNewGen
|
||||
'';
|
||||
|
||||
putterLinkGeneration = ''
|
||||
${lib.getExe pkgs.putter} apply $VERBOSE_ARG ''${DRY_RUN:+--dry-run} \
|
||||
--state-file "${putterStatePath}" \
|
||||
${config.home.internal.filePutterConfig}
|
||||
'';
|
||||
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
|
||||
''
|
||||
if config.home.fileActivator == "putter" then putterLinkGeneration else legacyLinkGeneration
|
||||
);
|
||||
|
||||
home.activation.checkFilesChanged = lib.hm.dag.entryBefore [ "linkGeneration" ] (
|
||||
@@ -334,6 +422,18 @@ in
|
||||
'') (lib.filter (v: v.onChange != "") 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 = 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 =
|
||||
|
||||
@@ -912,6 +912,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"
|
||||
|
||||
82
modules/lib/putter.nix
Normal file
82
modules/lib/putter.nix
Normal file
@@ -0,0 +1,82 @@
|
||||
# Contains some handy functions for generating Putter file manifests.
|
||||
|
||||
{ lib }:
|
||||
|
||||
let
|
||||
|
||||
inherit (lib)
|
||||
filter
|
||||
hasPrefix
|
||||
optionalAttrs
|
||||
;
|
||||
|
||||
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
|
||||
# Create a Putter entry for the given file.
|
||||
mkEntry =
|
||||
f:
|
||||
{
|
||||
source = "${sourceBaseDirectory}/${f.target}";
|
||||
# source = "${f.source}";
|
||||
target = (if hasPrefix "/" f.target then "" else "${targetBaseDirectory}/") + f.target;
|
||||
}
|
||||
// optionalAttrs f.force {
|
||||
collision.resolution = "force";
|
||||
}
|
||||
// optionalAttrs f.recursive {
|
||||
action.type = "recursive_symlink";
|
||||
};
|
||||
|
||||
putterJson = {
|
||||
version = "1";
|
||||
state = putterStatePath;
|
||||
files = map mkEntry (filter (f: f.enable) fileEntries);
|
||||
};
|
||||
|
||||
putterJsonText = builtins.toJSON putterJson;
|
||||
in
|
||||
putterJsonText;
|
||||
|
||||
# Create a putter state file to allow compatibility between legacy and putter
|
||||
# managed files.
|
||||
#
|
||||
# Note, the interface of this function is not considered stable, it may change
|
||||
# as the needs of Home Manager change.
|
||||
mkPutterCompatState =
|
||||
{
|
||||
sourceBaseDirectory,
|
||||
targetBaseDirectory,
|
||||
fileEntries,
|
||||
}:
|
||||
let
|
||||
mkEntry = f: {
|
||||
name = (if hasPrefix "/" f.target then "" else "${targetBaseDirectory}/") + f.target;
|
||||
value = {
|
||||
source = "${sourceBaseDirectory}/${f.target}";
|
||||
modified = {
|
||||
secs_since_epoch = 0;
|
||||
nanos_since_epoch = 0;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
stateJsonText = builtins.toJSON {
|
||||
version = "1";
|
||||
files = lib.listToAttrs (map mkEntry (filter (f: f.enable) fileEntries));
|
||||
};
|
||||
in
|
||||
stateJsonText;
|
||||
}
|
||||
Reference in New Issue
Block a user