diff --git a/flake.lock b/flake.lock index c4ba38570..1008bac93 100644 --- a/flake.lock +++ b/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" } } }, diff --git a/flake.nix b/flake.nix index d047c7bf5..4a1028e75 100644 --- a/flake.nix +++ b/flake.nix @@ -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 ./.; }]; diff --git a/home-manager/home-manager b/home-manager/home-manager index eb69530a8..c3afa248a 100644 --- a/home-manager/home-manager +++ b/home-manager/home-manager @@ -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 diff --git a/modules/files.nix b/modules/files.nix index 327cf1bbd..6b03f035d 100644 --- a/modules/files.nix +++ b/modules/files.nix @@ -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 diff --git a/modules/home-environment.nix b/modules/home-environment.nix index 27a71e457..5b08e26b7 100644 --- a/modules/home-environment.nix +++ b/modules/home-environment.nix @@ -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 diff --git a/modules/lib-bash/activation-init.sh b/modules/lib-bash/activation-init.sh index 9a38e7c5a..143f0b975 100755 --- a/modules/lib-bash/activation-init.sh +++ b/modules/lib-bash/activation-init.sh @@ -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" diff --git a/modules/lib/putter.nix b/modules/lib/putter.nix new file mode 100644 index 000000000..5654f5929 --- /dev/null +++ b/modules/lib/putter.nix @@ -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; +}