Replace home-managertitle

2024-05-21date
You may really don't need it
description

    home-manager is a basic system for managing a user environment using Nix. It allows setting declarative user envs.

    For a nixos user includes me, home-manager may already introduced at the same time as the config created, for managing user-spec configs. But it has really poor maintainence and code quality, and it does produced few bugs several months I use it. And I do considered removing it but seems there's no a better solution exist.

    Things changed when I saw this post. TLDR:

    • Use nix profile mechanism instead of home.packages. User-spec package management solved.
    • Use nix run and nix app (flake) mechanism for deploying user-spec software configs. This could be conveniently migrate from existing home-manager profile.

    The blog above doesn't gave a detail implementation for referencing, which we'll discuss here.

    Before start,

    Follow this post you could finally get:

    • remove home-manager
    • User specific software config switch timecost reducing, significantly.
      • use home-manager as nixos module will require priviledge when trying change user settings, and consume same eval time as entire nixos config, which about 20s. Time cost reduced to ~4s after this.
    • Better maintainence for config. If ur powerful enough.

    What we'll lost

    • No more home-manager. Things may not be better.
    • Difficulties for calling OS config option in user specific config. In home-manager with nixosModule installation (standalone install has no this function as well btw) you generally do this by calling osConfig.
    • slate-symlink cleaning. Which could be easily solved by writting a simple script.

    Prerequisites

    • A Nix Flake, with a working nixosConfiguration.
    • (optional) A home-manager config (either standalone or nixosModule installation)
    • Want, to get rid of the home-manager.
    • About 3 hours or days.
    "

    Difference between user-specific packages/config and global's: they were stored under user home.

    "

    You can always start from install user specific package

    Collect Configs (Optional)

    If you already have a deployed home-manager configuration, which linked things to your $HOME/.config directory, you may want to use this script to collect them all.

    import os
    import shutil
    
    def find_and_copy_symlinks(source_dir, target_dir):
        """
        Recursively find all symbolic links in source_dir and copy their real files to target_dir,
        preserving the original directory structure.
    
        Args:
            source_dir (str): The directory to search for symbolic links.
            target_dir (str): The directory where the real files will be copied.
        """
        for root, dirs, files in os.walk(source_dir):
            for name in files + dirs:
                path = os.path.join(root, name)
                if os.path.islink(path):
                    real_path = os.path.realpath(path)
                    relative_path = os.path.relpath(path, source_dir)
                    target_path = os.path.join(target_dir, relative_path)
    
                    # Create directories in target path
                    os.makedirs(os.path.dirname(target_path), exist_ok=True)
    
                    # Copy the real file to the target path
                    if os.path.isfile(real_path):
                        shutil.copy2(real_path, target_path)
                    elif os.path.isdir(real_path):
                        shutil.copytree(real_path, target_path, dirs_exist_ok=True)
    
                    print(f"Copied {real_path} to {target_path}")
    
    if __name__ == "__main__":
        source_directory = f'{os.environ["HOME"]}/.config'  # Replace with the source directory
        target_directory = "./target"  # Replace with the target directory
        find_and_copy_symlinks(source_directory, target_directory)

    This will collect all symlink real paths into the ./target directory. Please note that you will still need to manually replace the /nix/store/xxhash-name strings in each file.

    Here’s an example of what it collected for me:

    ~/Src/hmlink/target
    > tree
    .
    ├── alacritty
    │   └── alacritty.toml
    ├── aria2
    │   └── aria2.conf
    ├── atuin
    │   └── config.toml
    ├── bspwm
    .....<snip>

    Mapping target path & real file

    We need to automatically build an attrset from the tree above. e.g.

    {
      "/home/user/.config/alacritty/alacritty.toml" = abs_path_of_dir/alacritty/alacritty.toml;
      # ...<snip>
    }

    for further manipulating. To implement this a recursive read and update could be introduced:

    code

    { lib, pkgs, ... }@args:
    # https://gist.github.com/thalesmg/ae5dc3c5359aed78a33243add14a887d
    let
      configPlace = "~/.config";
    
      inherit (builtins) readDir foldl' attrNames;
      inherit (lib.attrsets) filterAttrs setAttrByPath recursiveUpdate;
      inherit (lib) removeSuffix;
      inherit (pkgs) writeText;
    
      listRecursive = pathStr: listRecursive' { } pathStr;
      listRecursive' =
        acc: pathStr:
        let
          toPath = s: path + "/${s}";
          path = ./. + pathStr;
          contents = readDir path;
          dirs = filterAttrs (k: v: v == "directory") contents;
          files = filterAttrs (k: v: v == "regular" && k != "default.nix") contents;
          dirs' = foldl' (acc: d: recursiveUpdate acc (listRecursive (pathStr + "/" + d))) { } (
            attrNames dirs
          );
          files' = foldl' (
            acc: f:
            recursiveUpdate acc (
              setAttrByPath [ "${configPlace}${pathStr}/${(removeSuffix ".nix" f)}" ] (
                if lib.hasSuffix ".nix" f then
                  (writeText (removeSuffix ".nix" f) (import (toPath f) args))
                else
                  (toPath f)
              )
    
            )
          ) { } (attrNames files);
        in
        recursiveUpdate dirs' files';
    in
    listRecursive ""

    This will structure the attribute set according to the directory structure. It also provides a mechanism for using variables, such as pkgs.hello, in the configuration. To do this, simply add the suffix .nix to the file name and wrap the file content with a Nix string.

    e.g.

    # example.nix
    
    {pkgs, ...}:
    ''
    foobar ${pkgs.hello}
    ''

    will evaluated to:

    foobar /nix/store/xxxxxxxxxxxxxxxxxxxxxx-hello
    

    Make a new dir in whatever place of your config repo. Place default.nix in and you could copy target/*, which above mentioned, or your $HOME/.config if u don't use home-manager, into this directory. See structure or maybe a more elegant way.

    This will read the entire dir and output a text:

    mkdir -p /home/user/.config/alacritty; ln -sf ./alacritty/alacritty.toml /home/user/.config/alacritty/alacritty.toml
    mkdir ...<snip>
    

    Finally we need a nix app for deploying these.

    {
    # flake.nix toplevel
    # may require edit, each-system config has automatically append by flake-parts in my case.
    apps.default =
      let homeCfgAttr = (import ./home { inherit pkgs lib; });
          parent =
             let
               inherit (inputs.nixpkgs.lib)
                 concatStringsSep
                 reverseList
                 splitString
                 drop
                 ;
             in
             p: concatStringsSep "/" (reverseList (drop 1 (reverseList (splitString "/" p))));
    in
    {
      type = "app";
      program = pkgs.writeScriptBin "link-home" (
        toString (
          lib.concatStringsSep "\n" (
            lib.foldlAttrs (
              acc: n: v:
              acc ++ lib.singleton "mkdir -p ${lib.parent n}; ln -sf ${v} ${n}"
            ) [ ] homeCfgAttr
          )
        )
      );
    };
    
    }

    While config changed just run nix run and all will be set.

    Further more, to impl deeper ingegration with nixos module, benefiet from the { dest = source; } structure, we can easily:

    { lib, pkgs, ... }@args:
    { lib, pkgs, user, ... }@args:
    let
      configPlace = "~/.config";
      configPlace = "/home/${user}/.config";
    
      inherit (builtins) readDir foldl' attrNames;
      inherit (lib.attrsets) filterAttrs setAttrByPath recursiveUpdate;
      inherit (lib) removeSuffix;
      inherit (pkgs) writeText;
    
      listRecursive = pathStr: listRecursive' { } pathStr;

    and adding a nixos module, everything just works:

    {
      lib,
      pkgs,
      user,
      ...
    }:
    let
      homeCfgAttr = (import ../pathToAboveFile { inherit pkgs lib user; });
    in
    {
      systemd.tmpfiles.rules = lib.foldlAttrs (
        acc: n: v:
        acc ++ lib.singleton "L+ ${n} - - - - ${v}"
      ) [ ] homeCfgAttr;
    }

    More easily. Add following to flake toplevel

    {
    
    # flake.nix
      packages.default = pkgs.symlinkJoin {
        name = "user-pkgs";
        paths =
          import ./userPkgs.nix { inherit pkgs; }
          # avoid being gc
          ++ (lib.singleton (
            map (
              path:
              let
                source = homeCfgAttr.${path};
              in
              pkgs.writeTextDir path source
            ) (builtins.attrNames homeCfgAttr)
          ));
      };
    
    }

    and userPkgs.nix

    # userPkgs.nix
    {pkgs, ...}:
      with pkgs; [ hello cowsay ]

    Trace by git.

    while first deploy, run nix profile install .#userPkgs.

    while changed, run nix profile upgrade <index> which index seen from nix profile list

    NOTICE that if using root on tmpfs, ~/.nix-profile may not exsit at startup if home on tmpfs, which causes packages not loaded to env. Adding a link for solving this.

    {
      systemd.tmpfiles.rules = [
        "L /home/${user}/.nix-profile - - - - /home/${user}/.local/state/nix/profiles/profile"
      ];
    }

    You may need to pay attention to the behavior when garbage collection (GC) is executed, as discussed in the mentioned article.

    ©2018-2025 Secirian | CC BY-SA 4.0