换一台机器,从装好系统到"能干活",有人两小时,有人两天。差别不在手速,而在多少东西停在脑子里:哪个插件要装、哪条 alias 写过、哪个项目要哪个 JDK、证书放在哪个目录。这些没写下来的东西,在旧机器上没有成本,换机那天一次性收账。
点文件管理的目标不是"备份配置",而是把"这台机器能用"变成一件可执行、可核对、可重复的事。
1. 判据:几步、几个判断点
| 环节 | 可自动化的部分 | 只能人工判断的部分 |
|---|---|---|
| 系统与包管理 | 装包清单、版本钉住 | 该装哪个版本、要不要用系统包 |
| Shell 与编辑器 | 配置文件、插件清单 | 冲突键位怎么取舍 |
| 语言运行时 | 版本管理器与版本文件 | 项目用哪个版本 |
| 密钥与凭证 | 拉取脚本、keyring 读取 | 哪些权限、给哪台机器 |
| 项目仓库 | clone 清单 | 哪些是当前在做的 |
判据只有两个数:从裸机到能干活的总步数,和其中需要人做决定的步数。第二个数更重要:总步数可以靠多写脚本压下去,判断点数只能靠把判断前置(写进默认值)压下去。
─── 恢复计时:每段单独记,别只记总数
$ t0=$(date +%s)
$ ./bootstrap.sh --stage base // 装包、装版本管理器
$ echo "base: $(( $(date +%s) - t0 ))s"
$ ./bootstrap.sh --stage shell // 链接点文件、装插件
$ ./bootstrap.sh --stage projects // clone 清单
$ echo "total: $(( $(date +%s) - t0 ))s"
典型量级:脚本覆盖的段落以分钟计,人工判断段落以十分钟计。恢复时间由判断点数决定,不由命令数决定。
2. 三种方式与它们的成本结构
| 方式 | 初装成本 | 日常变更成本 | 坏掉时在调什么 | 最坏情况 |
|---|---|---|---|---|
| 手工拷贝 | 零 | 每台机器各改一遍 | 没有抽象层可调,只能人肉 diff | 两台机器悄悄分叉 |
| 符号链接仓库 | 低 | 改一次即生效 | 链接指向、权限、路径 | 改的是 A,跑的是 B |
| 声明式配置 | 中到高 | 改源文件再 apply | 工具本身的模板与条件逻辑 | 抽象层坏了,得先学抽象层 |
第三种不是"更先进",是把成本从变更时挪到初装时。它的收益随机器数和管理项数增长;机器只有一台时,它收不回成本。
─── 符号链接仓库的最小骨架:先跑通,再谈抽象
$ ls -A ~/dotfiles
bashrc gitconfig ssh_config nvim/ bin/
$ for f in bashrc gitconfig; do
> ln -sfn ~/dotfiles/$f ~/.$f
> done
$ ls -l ~/.bashrc
lrwxrwxrwx 1 u u 22 Nov 9 10:02 /home/u/.bashrc -> /home/u/dotfiles/bashrc
链接仓库有两个隐藏成本。一是工具写入配置时会打断链接:编辑器或安装器把符号链接替换成实体文件,之后改仓库不再生效。二是目录级链接的边界:链接整个目录时,工具生成的缓存文件也会进仓库。
3. 进不进版本控制
| 内容 | 进仓库 | 理由与替代 |
|---|---|---|
| Shell、编辑器、终端配置 | 是 | 纯文本,跨机器一致 |
| 包与插件清单(含版本) | 是 | 恢复的实质是版本,不是文件 |
| 密钥、token、证书私钥 | 否 | 运行时注入;仓库一旦被克隆就撤不回 |
| 机器特有配置(主机名、绝对路径、GPU 开关) | 条件化后进 | 用模板变量表达,不复制两份 |
| 本地状态(历史、缓存、会话、本地库) | 否 | 属于使用痕迹,不属于环境定义 |
| 一次性脚本与临时配置 | 否 | 会变成没人敢删的考古层 |
判断标准一句话:这份文件描述"环境应该是什么样"就进;描述"我用过什么"就不进。
─── 凭证不进仓库:从环境或 keyring 读,缺失时静默跳过
$ git config --global credential.helper "cache --timeout=3600"
// macOS: security find-generic-password -s api -a me -w
// Linux: secret-tool lookup service api account me
// 通用: pass show api/token
─── 提交前的最小守卫:宁可误报,不要漏报
$ git -C ~/dotfiles diff --cached \
> | grep -nEi 'BEGIN (RSA|OPENSSH) PRIVATE KEY|api[_-]?key|secret'
$ git -C ~/dotfiles ls-files | grep -Ei '\.(pem|key|p12|kdbx)$'
$ git -C ~/dotfiles status --short --ignored | head
守卫只能挡住想到的形态,不构成保证。真正的边界是:凭证从不经过仓库,不是"凭证进过仓库但被拦住了"。前者每次多一步注入,后者的密钥会长期留在历史里。
4. 机器差异的表达
| 差异维度 | 典型形态 | 表达方式 | 成本 |
|---|---|---|---|
| 主机名与角色 | 笔记本 vs 服务器 | 运行时分支 | 分支不可见,读配置时看不到有哪些取值 |
| 绝对路径 | 家目录、数据盘、缓存盘 | 模板变量 | 变量名散落,改一处漏一处 |
| 包名与版本 | openjdk-17-jdk vs brew install openjdk@17 |
按平台分文件 | 每平台一份清单,必然分叉 |
| OS 与架构 | macOS / Linux / WSL | 条件包含 | 组合数增长快于机器数 |
| 资源规模 | CPU、内存、GPU | 运行时探测,不写死 | 探测逻辑本身要维护 |
条件化是成本转移:不写分支就要复制两份文件,写分支就要维护分支逻辑。取舍经验是:分支只允许出现在入口文件这一处,不要散落到每个配置文件;每个分支必须有注释说明"什么情况走这里",否则三个月后没人敢删。
─── 条件化集中在入口:分支可见、可枚举
dot_zshrc
dot_gitconfig
dot_config/nvim/private_dot_config // private = 600 权限
dot_zshrc.darwin // 只在 macOS 应用
dot_zshrc.linux // 只在 Linux 应用
--- dotbot 风格:清单即事实,apply 幂等
- link:
~/.zshrc: zshrc
~/.gitconfig: gitconfig
- shell:
- [git submodule update --init --recursive, 安装插件]
- command: ./scripts/platform-hook.sh
stdout: false
stderr: true
声明式的收益是幂等:apply 十次和一次结果相同,所以能写进自动化。成本是调试抽象层:模板语法、条件函数、权限修饰符各有各的规则,报错通常指向工具内部而不是你的配置。
5. 演练与最小集
没演练过的恢复流程等于一份没验证的备份。演练的判据不是"跑完了",而是中途没有查过任何聊天记录、截图、旧机器。
| 层级 | 内容 | 恢复预算 | 缺了会怎样 |
|---|---|---|---|
| 必需 | Shell、编辑器、语言版本管理器、git 身份、SSH | 30 分钟内 | 完全无法开始工作 |
| 常用 | 插件、别名、终端主题、常用 CLI | 1 小时内 | 能干活,但每步都别扭 |
| 可选 | 主题微调、玩具项目、实验性工具 | 有空再说 | 无感 |
─── 演练的最小形态:空环境里跑,不看旧机器
$ docker run --rm -it -v ~/dotfiles:/dotfiles ubuntu:24.04 bash
$ apt-get update && apt-get install -y git curl
$ cd /dotfiles && ./bootstrap.sh --stage all
$ exec zsh // 提示符、别名、补全是否都在
$ git config user.name && ssh -T [email protected] // 身份与密钥
演练频率按配置改动频率定,不按日历定:每次改入口脚本,就在空环境跑一遍。小而频繁的验证比一年一次的大演练有效,因为后者要重建的记忆量太大,人会自动省略细节。
跨平台分叉的成本不会消失,只能选择放在哪:三份文件(改三处)或条件分支(改一处但维护逻辑)。第三种选择不存在。
6. 度量与取舍
| 度量 | 采集方式 | 用途 | 反噬 |
|---|---|---|---|
| 裸机恢复步数 | 数 bootstrap 的阶段数 | 判断是否该再加抽象 | 步数少但每步都要判断时,数字骗人 |
| 人工判断步数 | 演练时记录"停下来想"的次数 | 真正的瓶颈指标 | 需要诚实记录,最容易漏记 |
| 配置漂移量 | 实际配置与仓库的 diff 行数 | 发现"只在那台机器上改过" | 只管文本,管不到工具内部状态 |
| 恢复耗时 | 分段计时 | 定位最贵的一段 | 受网络影响,波动大 |
"可重建"与"稳定"是一对张力:频繁改配置会破坏肌肉记忆,也让恢复脚本长期未验证。常见失衡是把配置当项目开发:每周重构一次,工作时间被侵蚀。合理的节奏是:配置改动集中在少数窗口做,做完立刻演练,其余时间冻结。
7. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 改了仓库文件,机器上没变 | 链接被工具替换成实体文件 | ls -l 看类型,必要时重跑链接 |
| 新机器某条 alias 不生效 | 条件分支没命中(主机名或 OS 判错) | 在入口处打印实际命中的分支名 |
| 提示符正常但补全失效 | 插件子模块未初始化 | 在清单里显式写 submodule update |
| apply 报模板错误 | 工具版本与新语法不匹配 | 钉住工具版本,在 CI 里跑一次 apply |
| WSL 下换行错乱 | 文件被 CRLF 污染 | 加 .gitattributes 强制 LF |
| 凭证本机可用、新机不可用 | 凭证在本地 keyring,没进注入流程 | 把凭证来源写进必需层 |
8. 边界
| 场景 | 为什么收益低 |
|---|---|
| 长期只用一台机器 | 恢复成本从不发生,投入只增加维护 |
| 工作流极简单(浏览器加一个编辑器) | 需要管理的东西少于写脚本的成本 |
| 公司强制统一环境(镜像下发、无 root) | 能改的只剩用户级配置,抽象层无处落地 |
| 团队已有环境即代码(devcontainer、Nix) | 个人仓库与之重叠,且更容易分叉 |
| 机器是临时的一次性环境 | 建仓成本高于重配成本 |
触发建仓的信号很明确:换机、加第二台机器、或因环境问题浪费过一整天。任一出现,手工拷贝就开始有真实成本。
点文件仓库的价值不在文件数量,而在那条链能不能在陌生机器上一次跑通。跑不通的仓库比没有仓库更危险——它让人相信"我有备份"。
9. 自测
- 你的恢复流程里,需要人工判断的步数大概是多少?卡在哪一步?
- 符号链接仓库最常见的失效方式是什么,怎么在几秒内看出来?
- 为什么包与插件清单的版本,比配置文件本身更值得钉住?
- 条件化配置应该集中在哪里,散落会造成什么后果?
- 什么信号出现时,建点文件仓库才真正开始划算?
- 你的恢复演练多久没跑了,上次跑的时候有没有查旧机器的记录?