Compare commits

...

1 commit

Author SHA1 Message Date
Robert Helgesson 8237e3f60f
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.
2024-01-24 22:24:32 +01:00
7 changed files with 440 additions and 285 deletions

View file

@ -1,5 +1,105 @@
{ {
"nodes": { "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": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1705677747, "lastModified": 1705677747,
@ -16,9 +116,133 @@
"type": "github" "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": { "root": {
"inputs": { "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,8 +2,9 @@
description = "Home Manager for Nix"; description = "Home Manager for Nix";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 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 { nixosModules = rec {
home-manager = import ./nixos; home-manager = import ./nixos;
@ -76,7 +77,10 @@
in lib.throwIf (used != [ ]) msg v; in lib.throwIf (used != [ ]) msg v;
in throwForRemovedArgs (import ./modules { in throwForRemovedArgs (import ./modules {
inherit pkgs lib check extraSpecialArgs; inherit pkgs lib check;
extraSpecialArgs = extraSpecialArgs // {
putter = putter.packages.${pkgs.system}.default;
};
configuration = { ... }: { configuration = { ... }: {
imports = modules imports = modules
++ [{ programs.home-manager.path = toString ./.; }]; ++ [{ programs.home-manager.path = toString ./.; }];

View file

@ -421,7 +421,7 @@ EOF
# Specify the source of Home Manager and Nixpkgs. # Specify the source of Home Manager and Nixpkgs.
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
home-manager = { home-manager = {
url = "github:nix-community/home-manager"; url = "git+file:///home/rycee/devel/home-manager";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
@ -452,7 +452,7 @@ EOF
_i "Creating initial Home Manager generation..." _i "Creating initial Home Manager generation..."
echo echo
if doSwitch; then if doSwitch --switch; then
# translators: The "%s" specifier will be replaced by a file path. # 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.' \ _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" "$confFile"
@ -607,10 +607,47 @@ function doBuild() {
} }
function doSwitch() { 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 setWorkDir
local generation local generation
case $profileAction in
set|noop)
# Build the generation and run the activate script. Note, we # Build the generation and run the activate script. Note, we
# specify an output link so that it is treated as a GC root. This # specify an output link so that it is treated as a GC root. This
# prevents an unfortunately timed GC from removing the generation # prevents an unfortunately timed GC from removing the generation
@ -622,33 +659,40 @@ function doSwitch() {
doBuildFlake \ doBuildFlake \
"$FLAKE_CONFIG_URI.activationPackage" \ "$FLAKE_CONFIG_URI.activationPackage" \
--out-link "$generation" \ --out-link "$generation" \
${PRINT_BUILD_LOGS+--print-build-logs} \ ${PRINT_BUILD_LOGS+--print-build-logs}
&& "$generation/activate" || return
else else
doBuildAttr \ doBuildAttr \
--out-link "$generation" \ --out-link "$generation" \
--attr activationPackage \ --attr activationPackage
&& "$generation/activate" || return
fi 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 presentNews
fi
} }
function doListGens() { function doListGens() {
setHomeManagerPathVariables setHomeManagerPathVariables
# Whether to colorize the generations output. # TODO: Support nix profile.
local color="never" nix-env --profile "$HM_PROFILE_DIR/home-manager" --list-generations
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
} }
# Removes linked generations. Takes as arguments identifiers of # Removes linked generations. Takes as arguments identifiers of
@ -928,7 +972,7 @@ while [[ $# -gt 0 ]]; do
opt="$1" opt="$1"
shift shift
case $opt in 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" COMMAND="$opt"
;; ;;
-A) -A)
@ -983,6 +1027,17 @@ while [[ $# -gt 0 ]]; do
-n|--dry-run) -n|--dry-run)
export DRY_RUN=1 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) --option|--arg|--argstr)
PASSTHROUGH_OPTS+=("$opt" "$1" "$2") PASSTHROUGH_OPTS+=("$opt" "$1" "$2")
shift 2 shift 2
@ -1038,7 +1093,10 @@ case $COMMAND in
doInstantiate doInstantiate
;; ;;
switch) switch)
doSwitch doSwitch --switch "${COMMAND_ARGS[@]}"
;;
test)
doSwitch --test
;; ;;
generations) generations)
doListGens doListGens
@ -1046,6 +1104,9 @@ case $COMMAND in
remove-generations) remove-generations)
doRmGenerations "${COMMAND_ARGS[@]}" doRmGenerations "${COMMAND_ARGS[@]}"
;; ;;
rollback)
doRollback
;;
expire-generations) expire-generations)
if [[ ${#COMMAND_ARGS[@]} != 1 ]]; then if [[ ${#COMMAND_ARGS[@]} != 1 ]]; then
_i 'expire-generations expects one argument, got %d.' "${#COMMAND_ARGS[@]}" >&2 _i 'expire-generations expects one argument, got %d.' "${#COMMAND_ARGS[@]}" >&2

View file

@ -1,4 +1,4 @@
{ pkgs, config, lib, ... }: { pkgs, config, lib, putter, ... }:
with lib; with lib;
@ -21,6 +21,8 @@ let
then file.source then file.source
else builtins.path { path = file.source; name = sourceName; }; else builtins.path { path = file.source; name = sourceName; };
putterStatePath = "${config.xdg.stateHome}/home-manager/putter-state.json";
in in
{ {
@ -31,6 +33,14 @@ in
type = fileType "home.file" "{env}`HOME`" homeDirectory; type = fileType "home.file" "{env}`HOME`" homeDirectory;
}; };
home.internal = {
filePutterConfig = mkOption {
type = types.package;
internal = true;
description = "Putter configuration.";
};
};
home-files = mkOption { home-files = mkOption {
type = types.package; type = types.package;
internal = true; internal = true;
@ -70,221 +80,17 @@ in
# This verifies that the links we are about to create will not # This verifies that the links we are about to create will not
# overwrite an existing file. # overwrite an existing file.
home.activation.checkLinkTargets = hm.dag.entryBefore ["writeBoundary"] ( home.activation.checkLinkTargets = hm.dag.entryBefore ["writeBoundary"] ''
let ${getExe putter} check -v \
# Paths that should be forcibly overwritten by Home Manager. --state-file "${putterStatePath}" \
# Caveat emptor! ${config.home.internal.filePutterConfig}
forcedPaths =
concatMapStringsSep " " (p: ''"$HOME"/${escapeShellArg p}'')
(mapAttrsToList (n: v: v.target)
(filterAttrs (n: v: v.force) cfg));
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" '' home.activation.linkGeneration = hm.dag.entryAfter ["writeBoundary"] ''
${config.lib.bash.initHomeManagerLib} ${getExe putter} apply $VERBOSE_ARG -v ''${DRY_RUN:+--dry-run} \
--state-file "${putterStatePath}" \
# A symbolic link whose target path matches this pattern will be ${config.home.internal.filePutterConfig}
# 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.checkFilesChanged = hm.dag.entryBefore ["linkGeneration"] ( home.activation.checkFilesChanged = hm.dag.entryBefore ["linkGeneration"] (
let let
@ -325,6 +131,16 @@ in
'') (filter (v: v.onChange != "") (attrValues cfg)) '') (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. # Symlink directories and files that have the right execute bit.
# Copy files that need their execute bit changed. # Copy files that need their execute bit changed.
home-files = pkgs.runCommandLocal home-files = pkgs.runCommandLocal

View file

@ -570,9 +570,13 @@ in
home.packages = [ config.home.sessionVariablesPackage ]; home.packages = [ config.home.sessionVariablesPackage ];
# A dummy entry acting as a boundary between the activation # The entry acting as a boundary between the activation script's "check" and
# script's "check" and the "write" phases. # the "write" phases. This is where we commit to attempting to actually
home.activation.writeBoundary = hm.dag.entryAnywhere ""; # 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. # Install packages to the user environment.
# #
@ -706,6 +710,11 @@ in
fi fi
${activationCmds} ${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 in
pkgs.runCommand pkgs.runCommand
@ -726,6 +735,7 @@ in
substituteInPlace $out/activate \ substituteInPlace $out/activate \
--subst-var-by GENERATION_DIR $out --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 ${config.home-files} $out/home-files
ln -s ${cfg.path} $out/home-path ln -s ${cfg.path} $out/home-path

View file

@ -59,34 +59,13 @@ function setupVars() {
declare -gr hmDataPath="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager" declare -gr hmDataPath="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager"
declare -gr genProfilePath="$profilesDir/home-manager" declare -gr genProfilePath="$profilesDir/home-manager"
declare -gr newGenPath="@GENERATION_DIR@"; 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 -gr legacyGenGcPath="$globalGcrootsDir/current-home"
declare greatestGenNum if [[ -e $currentGenGcPath ]] ; then
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
declare -g oldGenPath declare -g oldGenPath
oldGenPath="$(readlink -e "$genProfilePath")" oldGenPath="$(readlink -e "$currentGenGcPath")"
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
fi fi
} }
@ -181,15 +160,13 @@ if [[ -v VERBOSE ]]; then
fi fi
_iVerbose "Activation variables:" _iVerbose "Activation variables:"
if [[ -v oldGenNum ]] ; then if [[ -v oldGenPath ]] ; then
verboseEcho " oldGenNum=$oldGenNum"
verboseEcho " oldGenPath=$oldGenPath" verboseEcho " oldGenPath=$oldGenPath"
else else
verboseEcho " oldGenNum undefined (first run?)"
verboseEcho " oldGenPath undefined (first run?)" verboseEcho " oldGenPath undefined (first run?)"
fi fi
verboseEcho " newGenPath=$newGenPath" verboseEcho " newGenPath=$newGenPath"
verboseEcho " newGenNum=$newGenNum"
verboseEcho " genProfilePath=$genProfilePath" verboseEcho " genProfilePath=$genProfilePath"
verboseEcho " newGenGcPath=$newGenGcPath" verboseEcho " newGenGcPath=$newGenGcPath"
verboseEcho " currentGenGcPath=$currentGenGcPath"
verboseEcho " legacyGenGcPath=$legacyGenGcPath" verboseEcho " legacyGenGcPath=$legacyGenGcPath"

63
modules/lib/putter.nix Normal file
View 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;
}