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": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 ./.; }];
|
||||||
|
|
|
@ -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,48 +607,92 @@ 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
|
||||||
|
|
||||||
# Build the generation and run the activate script. Note, we
|
case $profileAction in
|
||||||
# specify an output link so that it is treated as a GC root. This
|
set|noop)
|
||||||
# prevents an unfortunately timed GC from removing the generation
|
# Build the generation and run the activate script. Note, we
|
||||||
# before activation completes.
|
# specify an output link so that it is treated as a GC root. This
|
||||||
generation="$WORK_DIR/generation"
|
# prevents an unfortunately timed GC from removing the generation
|
||||||
|
# before activation completes.
|
||||||
|
generation="$WORK_DIR/generation"
|
||||||
|
|
||||||
setFlakeAttribute
|
setFlakeAttribute
|
||||||
if [[ -v FLAKE_CONFIG_URI ]]; then
|
if [[ -v FLAKE_CONFIG_URI ]]; then
|
||||||
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 \
|
fi
|
||||||
&& "$generation/activate" || return
|
;;
|
||||||
|
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
|
fi
|
||||||
|
|
||||||
presentNews
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -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" ''
|
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/*"
|
|
||||||
|
|
||||||
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.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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
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