nix and home-manager, 最好的系统环境管理工具

前言

由 chatgpt 生成…

在当今的技术领域,配置管理变得越来越重要。对于开发者和技术爱好者来说,保持一致的开发环境和工具配置是至关重要的。然而,传统的配置文件管理方式可能会导致配置分散、混乱和难以维护的问题。

幸运的是,有一个强大的工具可以帮助我们解决这些问题,那就是home-manager。Home-manager是一个基于Nix的工具,旨在帮助用户统一管理他们的配置文件,并通过声明性的方式进行配置。

本文将介绍如何安装home-manager以及使用它来管理一些常见的配置文件,例如zsh、bash和vim。我们将探索如何使用home-manager的内置功能和插件来简化配置文件的管理,并介绍如何定制和贡献新的配置。

接下来的内容中, 主要围绕两个工具的使用.

  • home-manager 声明式, 函数式, 面向终态的用户配置管理方案
  • 通过 yadm 完成将配置文件同步到 github

安装 home-manager

传统 nix installer

假设当前系统处于初始化状态, 这里首先更新 channel , 接着安装最新的 home-manager.

官方安装教程 https://nix-community.github.io/home-manager/index.html#sec-install-standalone
可选的方式还有通过 flake 加载.

1
2
3
4
nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager 
nix-channel --update

nix-shell '<home-manager>' -A install

安装完毕, home-manager 会自动生成一个最小化配置在 ~/.config/home-manager/home.nix 这个文件中. 这里 home-manager 的可以告一段落

官方文档中会提及修改 .profile 等操作. 这部分会在 program 段落中进行.

通过 flake - standalone 安装

通过 flake 安装 home-manager 需要对 flake 有一定了解, 否则安装过程会比较曲折.
我个人强烈建议通过传统方式了解 home-manager 如何使用,再迁移到 flake. 虽然流程长了点, 但可以减少被 flake 折磨的风险.

通过 flake 方式引入 home-manager , 就需要注意 system 参数, 按照自己的需求, 配置好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
description = "flake: home-manager configuration works both for darwin module or standalone";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-23.11-darwin";
home-manager.url = "github:nix-community/home-manager/release-23.11";
flake-utils.url = "github:numtide/flake-utils";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};

outputs =
inputs@{ self
, nix-darwin
, nixpkgs
, home-manager
, flake-utils
, ...
}:
let
systems = [ "x86_64-darwin" ];
users = [ "user1" "user2" ];

formatters = flake-utils.lib.eachSystem systems (system: {
formatter = nixpkgs.legacyPackages.${system}.nixpkgs-fmt;
});

homePackages = flake-utils.lib.eachSystem systems (system:

let
pkgs = nixpkgs.legacyPackages.${system};
in
{

packages.homeConfigurations = nixpkgs.lib.genAttrs users (name: home-manager.lib.homeManagerConfiguration {
#pkgs = (nixpkgs.legacyPackages.${system}.extend (import ./overlays/default.nix));
inherit pkgs;
# Specify your home configuration modules here, for example,
# the path to your home.nix.
modules = [ ./home-configuration.nix ];
# Optionally use extraSpecialArgs
# to pass through arguments to home.nix

extraSpecialArgs = {
#arguments pass into home.nix
username = name;
homeDirectory = "/home/${name}";
};
});
});
in
{
inherit (formatters) formatter;
inherit (homePackages) packages;
};
}

flake.nix 会引入 home.nix , 也就是传统方式下用的 home.nix, 一份配置也可以做到支持多种方式使用.

注意, users 和 systems 需要根据实际需求改成自己的值, 我手里有不少机器共用着一份配置, 只要把需要的 system 和 user 写上就行了.
还有一些更加复杂的情况, 比如自定义 home 路径等, 这里就不展开了.

测试的构建命令 USER=user1 nix run 'home-manager' -- --flake . build

如果构建成功, 可以在 result 目录下看到生成的 home 配置

最终通过 nix run 'home-manager' -- --flake . switch

完整例子请参考 https://github.com/hitsmaxft/mixed-darwin-homemanager-example

通过 yadm 将配置文件同步到 github

在通过 nix-shell 载入 yadm 和相关软件, 通过执行 nix-shell -p pkg1 pkg2 , 可以快速得到临时环境

假设我们的 dotfiles 将上传到 $GIT_USER 这个用户名下的 dotfiles 仓库.
那么 github 全路径为 [email protected]:${GIT_USER}/dotfiles.git

1
2
3
4
5
yadm init
yadm add ~/.config/home-manager/home.nix
yadm commit -m 'add home.nix'
yadm remote add origin [email protected]:${GIT_USER}/dotfiles.git

如果仓库中没有任何内容, 这是应该直接推送到 master 分支, (或者 main )

1
yadm push -u origin master

假设你已经有现成的 dotfiles, 可以直接执行 yadm clone [email protected]:${GIT_USER}/dotfiles.git 一步初始化

home.program: 声明式管理shell 环境

home-manager 可以用于替代 stowhomebrew , 通过在 ~/.nix-profile/bin 下加入软链等, 方便地管理当前环境中的各种工具.

对于经常需要修改开发环境的开发者

home.programs 内置了大量 常用 shell 命令的配置, 通过这些配置项, 一方面自动化往当前 home.nix 中增加 nix package, 同时还能生成对应的配置文件, 替代手动管理配置文件.

解决了配置文件分散在各个目录和文件下的脏乱差现场, 现在配置文件都托管给了 nix 通过 home-manager 进行集中化管理, 同时
社区中大量已经贡献进来的 programs 配置项不仅可以直接使用, 后面会介绍如何通过 import 定制已有的 program.

当然可以也新增自己的 program 实现, 最后贡献给 home-manager 社区.

接下来章节中, 首先介绍一些常见场景下的 home-manager 使用案例

  • zsh
  • bash
  • vim

热身开始, 通过 nix 管理 zsh 配置文件

过去配置 zsh, 一般是通过修改 .zshrc .zprofile.zshenv 这些文件, 如果需要同步到不同机器, 可以借助 yadm 同步到 github .

随着 shell 的配置项的代码量不断膨胀,

接下来将介绍如何通过 home-managerprograms.zsh 配置项, 声明式管理 zsh 的所有配置文件.

以下是一个 zsh 的配置实例, 内置了一部分常用的 zsh 特性.

  • 内置 oh-my-zsh , 启用 vi-modejump 插件
  • .zshenv 加入 nix 环境变量 (这就是前面安装 nix 环境中跳过的部分)
    • envExtra
  • .zshrc 加入自定义选项
    • initExtraFirst 这部分脚本会在 .zshrc 最前面, 先于 oh-my-zsh 和其他脚本
    • initExtra 这部分脚本优先级低于其他的
    • initExtraBeforeCompInit 这部分脚本将在 zsh插件 加载之前执行

具体实现见 https://github.com/nix-community/home-manager/blob/master/modules/programs/zsh.nix
zshrc 配置项大致的优先级为 initExtraFirst > initExtraBeforeCompInit > plugsin > ohmyzsh > initExtra

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{pkg, ...} : 
{
home.programs = {
zsh = {
enable = true;
oh-my-zsh = {
enable = true;
plugins = [
"vi-mode"
"jump"
]

};

envExtra = ''
if [ -e ''${HOME}/.nix-profile/etc/profile.d/nix.sh ]; then . ''$HOME/.nix-profile/etc/profile.d/nix.sh; fi # added by Nix installer

'';
initExtraFirst = ''
'';
initExtra = ''
'';


};
};
}

管理 bash 配置

接下来是一个 bash 的配置实例.

给 bash 内置了补全功能和 starship (类似 p10k 的 zsh 提示符主题, rust 实现, 需要预先在 packages 中安装)

home-manager 中内置了 starship , 可以通过 programs.starship 快速配置. 这里只是提供一个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
home.packages = [
starship
];
programs = {
bash = {
enable = true;
enableCompletion = true;
initExtra = ''
eval "$(starship init bash)"
'';
};
};
}

.bashrc 中, 可以看到最终效果为:

1
2
3
4
5
6
if [[ ! -v BASH_COMPLETION_VERSINFO ]]; then
. "/nix/store/*******-bash-completion-2.11/etc/profile.d/bash_completion.sh"
fi

# init starship prompt
eval "$(starship init bash)"

通过 import 管理自定义 program

这里提供一个我自己的例子. aigc 是一个自定的 program , 会自动根据配置, 选择不同的 llm-cli 命令,生成对应的git-ai-commit 指令.

options.programs.ai-commit 定义了配置的选项
config 则是根据选项, 修改了 home.packages 实现动态添加命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
## modules/aigc.nix
{ config, pkgs, lib, ... }:
let
cfg = config.programs.ai-commit;
in
{
meta.maintainers = [ lib.maintainers.histmaxft ];

options.programs.ai-commit = {
enable = lib.mkEnableOption "ai-commit";

command = lib.mkOption {
default = "gemini-cli";
type = lib.types.str;
};

limit = lib.mkOption {
default = 10000;
type = lib.types.int;
};
system = lib.mkOption {
default = "generate a detail, clean and one-line commit message for key changes in following git commit diff, don't use markdown syntax, reply you final commit message only";
type = lib.types.str;
};
};
config = lib.mkIf cfg.enable
{
home = {
packages = [
(pkgs.writeTextFile {
name = "git-ai-commit";
executable = true;
destination = "/bin/git-ai-commit";
text = ''#!${pkgs.zsh}/bin/zsh
set -euo pipefail
special_arg="--system"
arg_list=()
command="${cfg.command}"
for arg in "$@"; do
if [[ "$arg" == "--system" ]]; then
shift
shift
continue
fi
if [[ "$arg" == "--gemini" ]]; then
shift
command="gemini-cli"
continue
fi
arg_list+=("$arg")
done
TEXT="$(git diff -U1 --cached)" || exit -1
SYSTEM_PROMPT="''${SYSTEM_PROMPT:-"${cfg.system}"}"
PROMPT="''${PROMPT-"summary following git diff"}\nGIT DIFF:\n$(printf "%q" "$TEXT" ))"

git commit -m "$(''${command} --limit ${builtins.toString cfg.limit} --system "''${SYSTEM_PROMPT}" "''${PROMPT}")" "''${arg_list[@]}"

'';
checkPhase = ''
${pkgs.stdenv.shellDryRun} "$target"
'';
meta.mainProgram = "git-ai-commit";
})
];
};
};
}

将 aigc.nix 放在了 modules 配置目录下.
并且通过配置启用

1
2
3
home.imports = [ ./modules/aigc.nix ];
home.programs.aigc.enable=true;
home.programs.aigc.command=pkgs.gemini-cli;

其实除了自定义 program, 还有一种用法增强现有的 programs, 通过 module 实现模块化管理

这里举一个给 sd 增加一个 settings 的自定义选项.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#~/.config/home-manager/modules/xx.nix
{ config, lib, ... }:
let
cfg = config.programs.sd;
in {
options.programs.sd.settings = {
enable = lib.mkEnableOption "xxx";

settings = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.str;
example = lib.literalExpression ''
{
XXX_LOADED = "yes";
}
'';
description = lib.mdDoc
"XXX config";
};
};
config = lib.mkIf cfg.enable {
home = {
sessionVariables = cfg.settings;
};
};
}

接下来需要在 home.nix 中通过 import 导入 xxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# home.nix
{}:
{
imports = [ ./modules/xxx.nix ];

programs = {
sd = {
enable = true;
settings = {
XXX_LOADED = "yes";
};
};

};
}

通过 overlay 增加自定义函数和应用包

这里举一个例子, 往 <nixpkgs> 中新增 vim-darwin 包.

  • 关闭 GUI
  • 开启 darwin 特性, vim 和 mac 剪贴板交互需要开启该编译选项
  • 内置两个基础插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ~/.config/home-manager/overlays/default.nix

final: prev:
let
pkgs = prev;
in
{
vim-darwin = pkgs.vim-full.customize {
vimrcConfig.packages.default = {
gui = true;
darwin = true;
start = [
pkgs.vimPlugins.vim-nix
pkgs.vimPlugins.vim-plug
];
};
};
}

完成上面的基础 overlays 脚本, 接下来需要在 home-manager 中引用.

1
2
3
4
5
6
7
8
9
#  ~/.config/home-manager/home.nix

{pkgs, ...} : {

nixpkgs.overlays = [
(import ./overlays/default.nix)
];

}

再次运行 home-manager switch 完成 overlays 加载

最后, 再次修改 home.nixhome.pacakges 完成 vim-darwin 的安装

1
2
3
4
5
6
7
8
9
10
11
12
13

{pkgs, ...} : {

nixpkgs.overlays = [
(import ./overlays/default.nix)
];

home.packages = [
vim-darwin
]

}

总结

就此, 本文总结了大部分常见的 home-manager 配置管理技巧, 更多内容可以参考官方文档 https://nix-community.github.io/home-manager/index.html

相对于手动管理配置文件, 采用 home-manager 有以下几个优点

  • 声明式配置 - 使用 home-manager,可以使用 Nix 表达式以声明性的方式定义配置。这样可以实现版本控制、可复现性和易分享的配置。
  • 集中化管理 - home-manager 提供了一种集中管理您的 shell 环境配置的方法,而不是将配置文件分散在不同的目录和文件中。这样可以更容易地维护和更新配置。
  • 模块化, 可复用 - home-manager 以模块化的方式处理配置,允许您根据需要启用或禁用特定的组件或程序。它还提供了许多预配置的程序,可以直接使用或根据需求进行自定义。
  • 支持版本管理 - 由于 home-manager 的配置存储在像 Git 这样的版本控制系统中,您可以轻松跟踪更改、还原到以前的版本,并与他人合作。这在管理 shell 配置时提供了额外的安全性和灵活性