mirror of
https://github.com/nix-community/home-manager.git
synced 2024-12-14 11:57:55 +00:00
Experiments
This is a completely experimental proof-of-concept type setup of - using Putter for file management, - removing profile management from activation script, and - add `home-manager test` command, which activates the configuration but does not create a new profile generation.
This commit is contained in:
parent
e84811035d
commit
8237e3f60f
7 changed files with 440 additions and 285 deletions
226
flake.lock
226
flake.lock
|
@ -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": 1705677747,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
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, ... }:
|
||||
outputs = { self, nixpkgs, putter, ... }:
|
||||
{
|
||||
nixosModules = rec {
|
||||
home-manager = import ./nixos;
|
||||
|
@ -76,7 +77,10 @@
|
|||
in lib.throwIf (used != [ ]) msg v;
|
||||
|
||||
in throwForRemovedArgs (import ./modules {
|
||||
inherit pkgs lib check extraSpecialArgs;
|
||||
inherit pkgs lib check;
|
||||
extraSpecialArgs = extraSpecialArgs // {
|
||||
putter = putter.packages.${pkgs.system}.default;
|
||||
};
|
||||
configuration = { ... }: {
|
||||
imports = modules
|
||||
++ [{ programs.home-manager.path = toString ./.; }];
|
||||
|
|
|
@ -421,7 +421,7 @@ EOF
|
|||
# Specify the source of Home Manager and Nixpkgs.
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager";
|
||||
url = "git+file:///home/rycee/devel/home-manager";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
@ -452,7 +452,7 @@ EOF
|
|||
_i "Creating initial Home Manager generation..."
|
||||
echo
|
||||
|
||||
if doSwitch; then
|
||||
if doSwitch --switch; then
|
||||
# translators: The "%s" specifier will be replaced by a file path.
|
||||
_i $'All done! The home-manager tool should now be installed and you can edit\n\n %s\n\nto configure Home Manager. Run \'man home-configuration.nix\' to\nsee all available options.' \
|
||||
"$confFile"
|
||||
|
@ -607,48 +607,92 @@ function doBuild() {
|
|||
}
|
||||
|
||||
function doSwitch() {
|
||||
# How we should handle the Home Manager profile before running the
|
||||
# activation. Can be
|
||||
#
|
||||
# - "noop" : build a new configuration but do not update the profile.
|
||||
#
|
||||
# - "set" : build a new configuration and set it as the current generation.
|
||||
#
|
||||
# - "rollback" : rollback to the previous generation.
|
||||
#
|
||||
# Here "leave" is the default for the test action and "set" is the default
|
||||
# for switch.
|
||||
local profileAction
|
||||
|
||||
while (( $# > 0 )); do
|
||||
local opt="$1"
|
||||
shift
|
||||
|
||||
case $opt in
|
||||
--switch)
|
||||
profileAction='set'
|
||||
;;
|
||||
--test)
|
||||
profileAction='noop'
|
||||
;;
|
||||
--rollback)
|
||||
profileAction='rollback'
|
||||
;;
|
||||
*)
|
||||
_i 'Unknown argument %s' "$opt"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
setHomeManagerPathVariables
|
||||
setWorkDir
|
||||
|
||||
local generation
|
||||
|
||||
# Build the generation and run the activate script. Note, we
|
||||
# specify an output link so that it is treated as a GC root. This
|
||||
# prevents an unfortunately timed GC from removing the generation
|
||||
# before activation completes.
|
||||
generation="$WORK_DIR/generation"
|
||||
case $profileAction in
|
||||
set|noop)
|
||||
# Build the generation and run the activate script. Note, we
|
||||
# specify an output link so that it is treated as a GC root. This
|
||||
# prevents an unfortunately timed GC from removing the generation
|
||||
# before activation completes.
|
||||
generation="$WORK_DIR/generation"
|
||||
|
||||
setFlakeAttribute
|
||||
if [[ -v FLAKE_CONFIG_URI ]]; then
|
||||
doBuildFlake \
|
||||
"$FLAKE_CONFIG_URI.activationPackage" \
|
||||
--out-link "$generation" \
|
||||
${PRINT_BUILD_LOGS+--print-build-logs} \
|
||||
&& "$generation/activate" || return
|
||||
else
|
||||
doBuildAttr \
|
||||
--out-link "$generation" \
|
||||
--attr activationPackage \
|
||||
&& "$generation/activate" || return
|
||||
setFlakeAttribute
|
||||
if [[ -v FLAKE_CONFIG_URI ]]; then
|
||||
doBuildFlake \
|
||||
"$FLAKE_CONFIG_URI.activationPackage" \
|
||||
--out-link "$generation" \
|
||||
${PRINT_BUILD_LOGS+--print-build-logs}
|
||||
else
|
||||
doBuildAttr \
|
||||
--out-link "$generation" \
|
||||
--attr activationPackage
|
||||
fi
|
||||
;;
|
||||
rollback)
|
||||
generation="$HM_PROFILE_DIR/home-manager"
|
||||
;;
|
||||
esac
|
||||
|
||||
# TODO: Support nix profile.
|
||||
case $profileAction in
|
||||
set)
|
||||
nix-env --profile "$HM_PROFILE_DIR/home-manager" --set "$generation"
|
||||
;;
|
||||
rollback)
|
||||
nix-env --profile "$HM_PROFILE_DIR/home-manager" --rollback
|
||||
;;
|
||||
esac
|
||||
|
||||
"$generation/activate" || return
|
||||
|
||||
if [[ $profileAction == 'set' || $profileAction == 'noop' ]]; then
|
||||
presentNews
|
||||
fi
|
||||
|
||||
presentNews
|
||||
}
|
||||
|
||||
function doListGens() {
|
||||
setHomeManagerPathVariables
|
||||
|
||||
# Whether to colorize the generations output.
|
||||
local color="never"
|
||||
if [[ ! -v NO_COLOR && -t 1 ]]; then
|
||||
color="always"
|
||||
fi
|
||||
|
||||
pushd "$HM_PROFILE_DIR" > /dev/null
|
||||
# shellcheck disable=2012
|
||||
ls --color=$color -gG --time-style=long-iso --sort time home-manager-*-link \
|
||||
| cut -d' ' -f 4- \
|
||||
| sed -E 's/home-manager-([[:digit:]]*)-link/: id \1/'
|
||||
popd > /dev/null
|
||||
# TODO: Support nix profile.
|
||||
nix-env --profile "$HM_PROFILE_DIR/home-manager" --list-generations
|
||||
}
|
||||
|
||||
# Removes linked generations. Takes as arguments identifiers of
|
||||
|
@ -928,7 +972,7 @@ while [[ $# -gt 0 ]]; do
|
|||
opt="$1"
|
||||
shift
|
||||
case $opt in
|
||||
build|init|instantiate|option|edit|expire-generations|generations|help|news|packages|remove-generations|switch|uninstall)
|
||||
build|init|instantiate|option|edit|expire-generations|generations|help|news|packages|remove-generations|rollback|switch|test|uninstall)
|
||||
COMMAND="$opt"
|
||||
;;
|
||||
-A)
|
||||
|
@ -983,6 +1027,17 @@ while [[ $# -gt 0 ]]; do
|
|||
-n|--dry-run)
|
||||
export DRY_RUN=1
|
||||
;;
|
||||
--rollback)
|
||||
case $COMMAND in
|
||||
switch)
|
||||
COMMAND_ARGS+=("$opt")
|
||||
;;
|
||||
*)
|
||||
_iError '--rollback can only be used with %s' "switch"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--option|--arg|--argstr)
|
||||
PASSTHROUGH_OPTS+=("$opt" "$1" "$2")
|
||||
shift 2
|
||||
|
@ -1038,7 +1093,10 @@ case $COMMAND in
|
|||
doInstantiate
|
||||
;;
|
||||
switch)
|
||||
doSwitch
|
||||
doSwitch --switch "${COMMAND_ARGS[@]}"
|
||||
;;
|
||||
test)
|
||||
doSwitch --test
|
||||
;;
|
||||
generations)
|
||||
doListGens
|
||||
|
@ -1046,6 +1104,9 @@ case $COMMAND in
|
|||
remove-generations)
|
||||
doRmGenerations "${COMMAND_ARGS[@]}"
|
||||
;;
|
||||
rollback)
|
||||
doRollback
|
||||
;;
|
||||
expire-generations)
|
||||
if [[ ${#COMMAND_ARGS[@]} != 1 ]]; then
|
||||
_i 'expire-generations expects one argument, got %d.' "${#COMMAND_ARGS[@]}" >&2
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{ pkgs, config, lib, ... }:
|
||||
{ pkgs, config, lib, putter, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
|
@ -21,6 +21,8 @@ let
|
|||
then file.source
|
||||
else builtins.path { path = file.source; name = sourceName; };
|
||||
|
||||
putterStatePath = "${config.xdg.stateHome}/home-manager/putter-state.json";
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
|
@ -31,6 +33,14 @@ in
|
|||
type = fileType "home.file" "{env}`HOME`" homeDirectory;
|
||||
};
|
||||
|
||||
home.internal = {
|
||||
filePutterConfig = mkOption {
|
||||
type = types.package;
|
||||
internal = true;
|
||||
description = "Putter configuration.";
|
||||
};
|
||||
};
|
||||
|
||||
home-files = mkOption {
|
||||
type = types.package;
|
||||
internal = true;
|
||||
|
@ -70,221 +80,17 @@ in
|
|||
|
||||
# This verifies that the links we are about to create will not
|
||||
# overwrite an existing file.
|
||||
home.activation.checkLinkTargets = hm.dag.entryBefore ["writeBoundary"] (
|
||||
let
|
||||
# Paths that should be forcibly overwritten by Home Manager.
|
||||
# Caveat emptor!
|
||||
forcedPaths =
|
||||
concatMapStringsSep " " (p: ''"$HOME"/${escapeShellArg p}'')
|
||||
(mapAttrsToList (n: v: v.target)
|
||||
(filterAttrs (n: v: v.force) cfg));
|
||||
home.activation.checkLinkTargets = hm.dag.entryBefore ["writeBoundary"] ''
|
||||
${getExe putter} check -v \
|
||||
--state-file "${putterStatePath}" \
|
||||
${config.home.internal.filePutterConfig}
|
||||
'';
|
||||
|
||||
check = pkgs.writeText "check" ''
|
||||
${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 ${escapeShellArg builtins.storeDir})/*-home-manager-files/*"
|
||||
|
||||
forcedPaths=(${forcedPaths})
|
||||
|
||||
newGenFiles="$1"
|
||||
shift
|
||||
for sourcePath in "$@" ; do
|
||||
relativePath="''${sourcePath#$newGenFiles/}"
|
||||
targetPath="$HOME/$relativePath"
|
||||
|
||||
forced=""
|
||||
for forcedPath in "''${forcedPaths[@]}"; do
|
||||
if [[ $targetPath == $forcedPath* ]]; then
|
||||
forced="yeah"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n $forced ]]; then
|
||||
verboseEcho "Skipping collision check for $targetPath"
|
||||
elif [[ -e "$targetPath" \
|
||||
&& ! "$(readlink "$targetPath")" == $homeFilePattern ]] ; then
|
||||
# The target file already exists and it isn't a symlink owned by Home Manager.
|
||||
if cmp -s "$sourcePath" "$targetPath"; then
|
||||
# First compare the files' content. If they're equal, we're fine.
|
||||
warnEcho "Existing file '$targetPath' is in the way of '$sourcePath', will be skipped since they are the same"
|
||||
elif [[ ! -L "$targetPath" && -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then
|
||||
# Next, try to move the file to a backup location if configured and possible
|
||||
backup="$targetPath.$HOME_MANAGER_BACKUP_EXT"
|
||||
if [[ -e "$backup" ]]; then
|
||||
errorEcho "Existing file '$backup' would be clobbered by backing up '$targetPath'"
|
||||
collision=1
|
||||
else
|
||||
warnEcho "Existing file '$targetPath' is in the way of '$sourcePath', will be moved to '$backup'"
|
||||
fi
|
||||
else
|
||||
# Fail if nothing else works
|
||||
errorEcho "Existing file '$targetPath' is in the way of '$sourcePath'"
|
||||
collision=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -v collision ]] ; then
|
||||
errorEcho "Please move the above files and try again or use 'home-manager switch -b backup' to back up existing files automatically."
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
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. Switch over the Home Manager gcroot and current profile
|
||||
# links.
|
||||
#
|
||||
# 3. 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 = 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" && -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then
|
||||
# The target exists, back it up
|
||||
backup="$targetPath.$HOME_MANAGER_BACKUP_EXT"
|
||||
run mv $VERBOSE_ARG "$targetPath" "$backup" || errorEcho "Moving '$targetPath' failed!"
|
||||
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 ${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
|
||||
|
||||
if [[ ! -v oldGenPath || "$oldGenPath" != "$newGenPath" ]] ; then
|
||||
_i "Creating profile generation %s" $newGenNum
|
||||
if [[ -e "$genProfilePath"/manifest.json ]] ; then
|
||||
# Remove all packages from "$genProfilePath"
|
||||
# `nix profile remove '.*' --profile "$genProfilePath"` was not working, so here is a workaround:
|
||||
nix profile list --profile "$genProfilePath" \
|
||||
| cut -d ' ' -f 4 \
|
||||
| xargs -rt $DRY_RUN_CMD nix profile remove $VERBOSE_ARG --profile "$genProfilePath"
|
||||
run nix profile install $VERBOSE_ARG --profile "$genProfilePath" "$newGenPath"
|
||||
else
|
||||
run nix-env $VERBOSE_ARG --profile "$genProfilePath" --set "$newGenPath"
|
||||
fi
|
||||
|
||||
run --silence nix-store --realise "$newGenPath" --add-root "$newGenGcPath"
|
||||
if [[ -e "$legacyGenGcPath" ]]; then
|
||||
run rm $VERBOSE_ARG "$legacyGenGcPath"
|
||||
fi
|
||||
else
|
||||
_i "No change so reusing latest profile generation %s" "$oldGenNum"
|
||||
fi
|
||||
|
||||
linkNewGen
|
||||
''
|
||||
);
|
||||
home.activation.linkGeneration = hm.dag.entryAfter ["writeBoundary"] ''
|
||||
${getExe putter} apply $VERBOSE_ARG -v ''${DRY_RUN:+--dry-run} \
|
||||
--state-file "${putterStatePath}" \
|
||||
${config.home.internal.filePutterConfig}
|
||||
'';
|
||||
|
||||
home.activation.checkFilesChanged = hm.dag.entryBefore ["linkGeneration"] (
|
||||
let
|
||||
|
@ -325,6 +131,16 @@ in
|
|||
'') (filter (v: v.onChange != "") (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
|
||||
|
|
|
@ -570,9 +570,13 @@ in
|
|||
|
||||
home.packages = [ config.home.sessionVariablesPackage ];
|
||||
|
||||
# A dummy entry acting as a boundary between the activation
|
||||
# script's "check" and the "write" phases.
|
||||
home.activation.writeBoundary = hm.dag.entryAnywhere "";
|
||||
# The entry acting as a boundary between the activation script's "check" and
|
||||
# the "write" phases. This is where we commit to attempting to actually
|
||||
# activate the configuration. We do this by creating a GC root for the new
|
||||
# generation so that we guard against it disappearing before we complete.
|
||||
home.activation.writeBoundary = hm.dag.entryAnywhere ''
|
||||
run --silence nix-store --realise "$newGenPath" --add-root "$newGenGcPath"
|
||||
'';
|
||||
|
||||
# Install packages to the user environment.
|
||||
#
|
||||
|
@ -706,6 +710,11 @@ in
|
|||
fi
|
||||
|
||||
${activationCmds}
|
||||
|
||||
# Create the "current generation" GC root and remove the temporary
|
||||
# "activation in-progress" GC root.
|
||||
run --silence nix-store --realise "$newGenPath" --add-root "$currentGenGcPath"
|
||||
run rm $VERBOSE_ARG "$newGenGcPath"
|
||||
'';
|
||||
in
|
||||
pkgs.runCommand
|
||||
|
@ -726,6 +735,7 @@ in
|
|||
substituteInPlace $out/activate \
|
||||
--subst-var-by GENERATION_DIR $out
|
||||
|
||||
ln -s ${config.home.internal.filePutterConfig} $out/putter.json
|
||||
ln -s ${config.home-files} $out/home-files
|
||||
ln -s ${cfg.path} $out/home-path
|
||||
|
||||
|
|
|
@ -59,34 +59,13 @@ function setupVars() {
|
|||
declare -gr hmDataPath="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager"
|
||||
declare -gr genProfilePath="$profilesDir/home-manager"
|
||||
declare -gr newGenPath="@GENERATION_DIR@";
|
||||
declare -gr newGenGcPath="$hmGcrootsDir/current-home"
|
||||
declare -gr newGenGcPath="$hmGcrootsDir/new-home"
|
||||
declare -gr currentGenGcPath="$hmGcrootsDir/current-home"
|
||||
declare -gr legacyGenGcPath="$globalGcrootsDir/current-home"
|
||||
|
||||
declare greatestGenNum
|
||||
greatestGenNum=$( \
|
||||
nix-env --list-generations --profile "$genProfilePath" \
|
||||
| tail -1 \
|
||||
| sed -E 's/ *([[:digit:]]+) .*/\1/')
|
||||
|
||||
if [[ -n $greatestGenNum ]] ; then
|
||||
declare -gr oldGenNum=$greatestGenNum
|
||||
declare -gr newGenNum=$((oldGenNum + 1))
|
||||
else
|
||||
declare -gr newGenNum=1
|
||||
fi
|
||||
|
||||
if [[ -e $genProfilePath ]] ; then
|
||||
if [[ -e $currentGenGcPath ]] ; then
|
||||
declare -g oldGenPath
|
||||
oldGenPath="$(readlink -e "$genProfilePath")"
|
||||
fi
|
||||
|
||||
_iVerbose "Sanity checking oldGenNum and oldGenPath"
|
||||
if [[ -v oldGenNum && ! -v oldGenPath
|
||||
|| ! -v oldGenNum && -v oldGenPath ]]; then
|
||||
_i $'The previous generation number and path are in conflict! These\nmust be either both empty or both set but are now set to\n\n \'%s\' and \'%s\'\n\nIf you don\'t mind losing previous profile generations then\nthe easiest solution is probably to run\n\n rm %s/home-manager*\n rm %s/current-home\n\nand trying home-manager switch again. Good luck!' \
|
||||
"${oldGenNum:-}" "${oldGenPath:-}" \
|
||||
"$profilesDir" "$hmGcrootsDir"
|
||||
exit 1
|
||||
oldGenPath="$(readlink -e "$currentGenGcPath")"
|
||||
fi
|
||||
}
|
||||
|
||||
|
@ -181,15 +160,13 @@ if [[ -v VERBOSE ]]; then
|
|||
fi
|
||||
|
||||
_iVerbose "Activation variables:"
|
||||
if [[ -v oldGenNum ]] ; then
|
||||
verboseEcho " oldGenNum=$oldGenNum"
|
||||
if [[ -v oldGenPath ]] ; then
|
||||
verboseEcho " oldGenPath=$oldGenPath"
|
||||
else
|
||||
verboseEcho " oldGenNum undefined (first run?)"
|
||||
verboseEcho " oldGenPath undefined (first run?)"
|
||||
fi
|
||||
verboseEcho " newGenPath=$newGenPath"
|
||||
verboseEcho " newGenNum=$newGenNum"
|
||||
verboseEcho " genProfilePath=$genProfilePath"
|
||||
verboseEcho " newGenGcPath=$newGenGcPath"
|
||||
verboseEcho " currentGenGcPath=$currentGenGcPath"
|
||||
verboseEcho " legacyGenGcPath=$legacyGenGcPath"
|
||||
|
|
63
modules/lib/putter.nix
Normal file
63
modules/lib/putter.nix
Normal file
|
@ -0,0 +1,63 @@
|
|||
# Contains some handy functions for generating Putter file manifests.
|
||||
|
||||
{ lib }:
|
||||
|
||||
let
|
||||
|
||||
inherit (lib)
|
||||
concatMap concatLists mapAttrsToList hasPrefix removePrefix 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;
|
||||
}
|
Loading…
Reference in a new issue