openrig:用代码管理开发环境,实现跨平台一键重建

📅 发布时间:2026/10/14 23:02:41
openrig:用代码管理开发环境,实现跨平台一键重建
如果你和我一样每年都要在至少三台机器上重建开发环境那你大概率经历过这种场面打开一台新电脑先装 Homebrew再装 apt花一个下午把.zshrc里的 alias 一个个扒回来然后发现 tmux 配色不对、Neovim 插件全红、Git 用户信息忘了配。这种事情我重复了五年直到有一天决定不再重来。于是有了 openrig一个专门管理“工作台配置”的开源方案。rig 在极客圈里一般指“装备”或“工作台”有人把整台游戏电脑叫 gaming rig有人把录音设备叫 recording rig。openrig 的意思很直白把我这套开发工作台的配置完全打开让任何一台新机器都能一键重建。它不只是把点文件dotfiles同步一下而是一个带安装脚本、模块开关、模板变量和自检机制的完整配置体系。后面我会把目录结构、核心脚本、实测踩坑全部拆开讲适合正在维护自己 dotfiles 的开发者也适合刚入门想建立一套规范化终端环境的新手。1. 为什么我会开源一套叫 openrig 的开发环境1.1 痛点的起点配置散落没有一个人说得清我的处境可能跟很多人类似办公用 macOS家里用 Linux还有一台常年在跑的云服务器。三台机器的系统不同、包管理器不同、预装工具也不同。过去我的做法是“哪台缺什么就装什么”结果就是每台机器的配置都不一样.zshrc越改越长alias 经常在这台有、那台没有。真正让我崩溃的是一次系统重装。我当时自信地把 Home 目录备份了但恢复后发现.config里各个程序的自定义设置根本没有被统一管理Neovim 的插件依赖、tmux 的会话恢复脚本、Git 的全局 ignore全都要手动找回来。折腾了两天最后依然有几个旧工具链的小癖好没复原。那时候我就意识到一个开发环境如果不以代码形式管理本质上就是不可复现的黑历史。1.2 openrig 的定位比 dotfiles 多走一步dotfiles 这个词大家应该不陌生把.zshrc、.tmux.conf、.gitconfig放到一个 Git 仓库里到新机器 clone 下来再软链接过去。它能解决“配置文件丢失”的问题但它不管“依赖装没装”“版本对不对”“符号链接建没建成功”。openrig 在我的定义里是“dotfiles 的超集”。它除了放配置文件还包含引导脚本bootstrap.sh在新机器上自动识别系统并安装基础依赖模块化目录结构每个工具一个模块可单独开关统一的安装与刷新命令不用再记零散的ln -s操作模板变量系统把机器相关的差异用户名、工作目录、Git 身份单独抽出来自检脚本安装后告诉你哪些配置还没生效。一句话dotfiles 是“把配置存下来”openrig 是“把整套环境长出来”。1.3 为什么不直接用整机镜像或容器我曾认真考虑过用 Docker 或整机镜像来解决问题但实际试下来不划算。下面是我对比过的几条路线各有各的适用场景不过对我来说 openrig 是最合适的。方案恢复速度维护成本适用场景整机镜像 / 磁盘克隆最快高镜像体量大跨硬件容易出问题同型号机器批量部署远程容器 / 云 IDE快中但本地终端、输入法、GUI 工具链体验差项目隔离、多人协作纯 dotfiles 仓库慢低但只管配置文件不管依赖单机备份openrig 这类配置系统较快中脚本本身需要维护多平台个人开发环境、团队新机初始化容器适合跑服务但不太适合当日常开发环境。编辑器要装插件终端要连系统剪贴板输入法要跟桌面环境配合这些在容器里都要额外打洞。整机镜像则太重换一块硬盘都可能触发一堆驱动问题。配置系统的最大优势是轻和透明每一行配置都能 review每次变更都进 Git 历史出了问题可以随时回滚。1.4 什么人适合用这套思路如果你满足下面任意一条我建议你也照着 openrig 的思路整理一套自己的配置系统有两台以上设备且系统不完全相同每半年至少重装一次系统、换一次电脑或者入职新公司需要配新机受够了某个工具在某台机器上“就是跟别的不一样”想给团队做一套统一的新人开发环境模板。我不保证 openrig 本身能让所有环境秒级一致但至少能让你告别“人肉 beancount 配置”的日子。下面进入正题看看这套方案到底怎么搭。2. 目录结构每个模块就是一个可以独立开关的积木2.1 模块化约定install.sh config 目录openrig 的核心是模块。每个模块负责一个工具或一组工具彼此之间尽量不耦合。我定的约定很简单一个模块就是一个子目录目录名就是模块名模块下必须有install.sh负责安装和生成配置如果这个工具需要配置文件就放在config/目录里安装完成后由管理脚本软链到用户目录。这样做的好处是当你只想在新机器上装 Neovim 相关配置不想要我喜欢的 tmux 布局时可以直接用--only neovim参数跳过其他模块。模块是离散的开关由外层脚本控制而不是在安装脚本里写一堆 if 判断整个工具链。2.2 顶层结构一览我的仓库结构如下你可以直接作为参考模板openrig/ ├── bootstrap.sh ├── manager.sh ├── vars/ │ ├── env.sh │ └── secrets.age ├── modules/ │ ├── zsh/ │ │ ├── install.sh │ │ └── config/ │ │ ├── .zshrc │ │ └── aliases.zsh │ ├── tmux/ │ │ ├── install.sh │ │ └── config/.tmux.conf │ ├── neovim/ │ │ ├── install.sh │ │ └── config/ │ ├── git/ │ │ ├── install.sh │ │ └── config/ │ ├── tools/ │ │ ├── install.sh │ │ └── config/ │ └── ... ├── templates/ │ └── gitconfig.tmpl └── README.mdbootstrap.sh是入口在全新机器上最先执行manager.sh是日常维护工具负责创建软链接、调用模块安装脚本、刷新配置vars/env.sh放机器相关的变量比如USER_NAME、WORK_DIRtemplates/存放需要动态生成的配置文件模板。2.3 符号链接的生成规则配置文件的生效方式我采用“先备份后软链”的策略而不是直接复制。原因很简单如果你在新机器上改了.zshrc你希望改动回到 Git 仓库里以便管理那就必须让编辑器打开的是同一个文件。软链接是符合直觉的方案。manager.sh的核心逻辑大致是这样遍历模块下所有config/*文件对每个文件检查用户目录下是否已有同名文件如果已有且不是指向当前仓库的链接就先重命名为.bak再创建链接如果已经是指向当前仓库的链接就跳过执行每个模块的install.sh传入--verbose或--dry-run选项。这么一套流程保证了脚本可以重复执行而不产生副作用也就是常说的幂等。我第一次跑完之后又跑了一遍没有任何报错也不会把已有的配置备份文件再次覆盖。2.4 模板变量把“机器差异”和“个人偏好”分开同一个配置在不同机器上往往只有几个变量不同Git 的 user.name、某些工具的下载目录、代理端口等。如果全部写死在配置里仓库就无法在不同电脑间通用。我的做法是利用一个简单的模板替换templates/gitconfig.tmpl里写{{USER_NAME}}这样的占位符install.sh在生成配置时读取vars/env.sh里的变量并替换。我并不为了这个功能引入 Ruby 的 ERB 或 Python 的 Jinja2因为这些工具本身可能还没装好用 Shell 的sed做简单替换就够了。等到有一天模板复杂到 sed 搞不定再迁移到更重的工具也不迟。模板这套机制帮我解决了最头疼的 Git 身份问题。办公机器上我想让提交记录显示公司邮箱家里的机器想显示个人邮箱这个差异现在只在vars/env.sh里体现配置文件模板完全不用改。2.5 模块盘点我实际维护了哪些模块模块名作用亮点zshShell 配置、别名、历史、补全手动维护插件清单启动速度稳定tmux终端复用、会话持久化自定义前缀键、复制模式接入系统剪贴板neovim编辑器配置、插件管理使用 lazy.nvim插件版本锁定在 lock 文件里git全局配置、模板、常用 alias区分工作/个人 Git 身份tools常用命令行工具集合用函数统一管理不同系统的安装命令sshSSH 客户端配置、known_hosts 管理密钥明文不入库只放配置引用每个模块都要保证能独立安装、独立卸载。模块之间如果需要依赖我会在install.sh开头显式声明比如 neovim 模块依赖 tools 模块里的git和curl脚本启动时会先检查它们是否存在。这套依赖关系写得越清楚排错就越简单。3. 核心实现从零搭起一套可复用的工作台配置3.1 bootstrap.sh一条命令完成新机器初始化新机器最痛苦的部分是从零开始。openrig 的bootstrap.sh解决的就是“拿到一台新机clone 仓库后跑什么”。脚本的主力逻辑可以简化成下面这段#!/usr/bin/env bash set -euo pipefail DRY_RUN${DRY_RUN:-0} VERBOSE${VERBOSE:-0} # 检测操作系统与包管理器 detect_os() { case $(uname -s) in Darwin) OSmacos; PKG_MGRbrew ;; Linux) OSlinux; detect_linux_pkgmgr ;; *) echo unsupported OS; exit 1 ;; esac } # 执行命令前的 dry-run 钩子 run() { if [ $DRY_RUN -eq 1 ]; then echo DRY-RUN: $* else $ fi } # 安装一个包支持多个包管理器 install_pkg() { local pkg$1 case $PKG_MGR in brew) run brew install $pkg ;; apt) run sudo apt-get install -y $pkg ;; pacman) run sudo pacman -S --noconfirm $pkg ;; esac } detect_os echo Detected: $OS / $PKG_MGR install_pkg git install_pkg curl # 继续调用 manager.sh 处理模块 bash $(dirname $0)/manager.sh $这里set -euo pipefail是三件套保险-e让脚本在出错时立刻退出避免带着错误继续跑-u让未定义的变量直接报错防止$PKG_MGR拼错导致静默失败-o pipefail让管道中任何一环出错都能被捕获。dry-run 是我强烈建议每个人保留的功能。你可以在DRY_RUN1的情况下看到这台新机器将被安装的每一个包、执行的每一条命令而不是在 root 权限下直接开始装一堆软件。如果哪一步不对劲改完再正式跑能避免一边安装一边发现脚本逻辑错了然后手动清理的尴尬。3.2 跨平台包安装的抽象函数openrig 要用的核心工具其实不多Git、curl、zsh、tmux、ripgrep、fzf、fd、lazygit 这些。但它们在 macOS 上要用brew install在 Debian 系上要用apt-get install在 Arch 系上要用pacman -S。如果每个模块都写一遍包管理器分支仓库里会到处是 if。所以我把包安装收敛成一个公共函数install_pkg放在bootstrap.sh里。所有模块只写“我要装什么”不关心底层的包管理细节。new 模块加入时也不用去关心“用户是 Ubuntu 还是 Fedora”。Fedora 我额外加了一个分支因为dnf的用法和其他几个不太一样。包管理器抽象这层做得越薄越好别把“解析依赖”“安装编译选项”这种逻辑也塞进去否则这个函数会变成技术债的重灾区。3.3 zsh tmux neovim 三件套的配合这三个工具是我日常开发的主力它们不是孤立的配置也需要互相配合。zsh 模块里我没有用 Oh My Zsh原因不是它不好而是它启动有点重。我直接手动维护.zshrc引入必要的插件补全和提示并把 alias 单独拆到aliases.zsh里。zsh 的加载顺序我按照“环境变量 - 插件 - alias - 函数”排列避免出现 alias 定义时的依赖问题。tmux 模块里我改了前缀键从默认的C-b改成C-a这样更顺手。我加上了鼠标支持和所见即所得的复制模式让它能直接读取系统剪贴板。最关键的是开启了会话持久化配合tmux-resurrect和tmux-continuum重启机器后能恢复到之前的窗口布局。neovim 模块用的是 lazy.nvim 做插件管理。它有个 lock 文件会把每个插件的 commit 锁住避免某一天某个插件更新后整个编辑器崩掉。我的init.lua里有一个非常小的核心片段local lazypath vim.fn.stdpath(data) .. /lazy/lazy.nvim if not vim.loop.fs_stat(lazypath) then vim.fn.system({ git, clone, https://github.com/folke/lazy.nvim.git, lazypath, }) end vim.opt.rtp:prepend(lazypath) require(lazy).setup(plugins)第一次打开 nvim 时它会自动拉取 lazy.nvim然后根据plugins/目录下的配置加载所有插件。新机器装完 openrig 后我第一次打开 neovim 只需要等几秒下载插件之后就是熟悉的编辑界面。这套组合把“新环境适应成本”降到了非常低。3.4 Git 与 SSH 的配置模板Git 配置里最让我受益的是一个叫 conditional include 的机制。它允许我按目录自动切换不同的 Git 身份所有放在~/work/下的仓库自动使用公司邮箱所有个人目录下的仓库使用个人邮箱。配置文件模板大致长这样[user] name {{USER_NAME}} email {{USER_EMAIL}} [includeIf gitdir:~/work/] path ~/.gitconfig.work这个方案不需要每次手动改 remote URL也不用在本地仓库里写 user.email。配合 openrig 的模板变量{{USER_NAME}}和{{USER_EMAIL}}会在安装时被替换成当前机器的实际值非常干净。SSH 模块我比较保守私钥永远不进仓库。仓库里只放~/.ssh/config的模板包含 Host 别名、登录用户、端口等。私钥和敏感凭证单独使用系统钥匙串或 age 加密文件管理。团队场景下我甚至不建议把公钥放进仓库因为公钥也是敏感信息的一部分轮换起来同样麻烦。4. 上线之后踩过的真实坑openrig 看着简单真正跑起来之后遇到的坑一点也不少。下面这几个都是我实际调试过的每一个都曾经让我怀疑脚本是不是又被环境“偷偷改了设定”。4.1 同样一条命令macOS 和 Linux 行为完全不一样最开始我的manager.sh里用sed -i改模板文件在 Linux 上跑得挺好到 macOS 上直接报sed: -i may not be used with stdin。原因很经典macOS 自带的 BSD sed 和 GNU sed 的-i参数写法不同。排查过程花了我半个小时最后解决方案是在脚本里加一个检测函数如果检测到是 macOS就用gsed替代。类似的问题还出现在find命令的参数、readlink -f、date格式化这些地方。所以后来我给 openrig 定了一条铁律涉及到跨平台差异的命令一律用小函数包一层不允许直接裸写在模块里。4.2 包管理器不能硬编码我第一版脚本里写死了apt-get因为我在 Ubuntu 上开发。后来放到一台 Arch 机器上跑直接失败。那次的错误信息很蠢它告诉我apt-get: command not found但脚本依然尝试继续执行直到后面一个依赖缺失才整体崩掉。从那以后我把包管理器抽象成了函数并且坚持在bootstrap.sh最前面检测系统。另外我意识到sudo的使用也要小心。在 macOS 上brew一般不需要 sudo在 Arch 上pacman必须用 sudo两个系统上的权限模型不同。不在抽象层处理这个问题用户就会被莫名其妙的权限错误卡住。4.3 “~” 在配置文件里不一定等于 HOME这是个很隐蔽的坑。在 zsh 里~会被展开成$HOME但在某些 tmux 启动的执行环境中~的展开依赖 SHELL 变量如果 SHELL 没有正确设置~/.config就真的成了一个叫“~”的目录。我在做软链时用了ln -s源路径写的是~/.zshrc结果发现有一部分机器上符号链接目标变成了/home/user/~/.zshrc直接失效。排查之后我把所有脚本内部的路径统一改写为${HOME}/.zshrc。这是给后来人的第一个建议脚本里永远别用~只用$HOME。4.4 环境变量串台tmux 和 zsh 互相同步tmux 启动时会向所有会话推送环境变量。我的 zsh 模块里配置了一些工具链的 PATH如果同时在 tmux 里开了好几个窗格一个窗格里更新的PATH可能会被其他窗格继承导致某个工具时灵时不灵。我的解决办法是不在全局.zshrc里塞项目相关的环境变量而是让每个项目目录维护自己的.envrc配合 direnv 在进入目录时按需加载。全局 zsh 只保留工具链本身的东西比如 autoload 和通用 alias。这样环境变量不会互相污染出问题的范围也被限制在单个项目里。4.5 差点把密钥提交进仓库有一次为了测试 SSH 模块我把一段真实私钥内容临时放进了某个模块的配置文件里同时还在新建分支和提交。如果不是我设置了pre-commit钩子去扫描BEGIN OPENSSH PRIVATE KEY字样那次提交就会带着私钥推到远端。从那以后 openrig 里加了两个安全机制一个是.gitignore里强制忽略密钥文件和.env类文件另一个是 Git 钩子在提交前扫描常见机密关键字。如果你维护类似的仓库我强烈建议至少做到这两条。密钥一旦推上去即使马上删除提交第三方也可能已经克隆到了。4.6 工具升级太快配置反而先把自己升级坏了Neovim 的不同版本对插件 API 的支持差异很大。有一段时间我升级到 nightly 版本结果一个自动补全插件直接报错我又没有锁定版本回滚非常痛苦。后来我在 neovim 模块里增加了版本锁定机制指定一个经过测试的稳定版本而不是默认安装 nightly。zsh 和 tmux 也类似我会在vars/env.sh里记录工具的最低版本要求安装时做一次版本比对。这套机制不能说百分百免疫升级问题但至少把意外换挡的概率降下来不少。5. 把 openrig 从个人项目扩展成团队级工作台5.1 团队标准与个人偏好的隔离如果你想把 openrig 的这套思路应用到团队最容易踩的雷就是“统一一切”。任何团队里都会有这样的场景标准化的 Git 别名大家愿意接受但 vim 和 emacs 的争论根本不该由配置文件来解决。我在团队版里做了一层覆盖机制基础模块提供团队公共配置比如代码风格提交前缀、通用 commit 模板、CI 工具链个人覆盖模块放在另一个目录由每个人自己维护。团队模块更新的历史记录要尽量清晰让人一看就知道“为什么这么改”。5.2 新人机器的无人值守部署团队场景里让一个新人在第一天就跑起来价值非常高。我的做法是把 openrig 的部署命令继续简化成一条远程命令先让系统里安装 Git再 clone 仓库最后执行bootstrap.sh。这里必须留个心眼无论脚本多完善都要让新人先跑DRY_RUN1模式看一遍将要执行的命令确认没有不可理解的操作再正式安装。我不建议做成完全的无人值守因为一些交互式配置仍然需要人工确认。智能一点的方向是让脚本自己检测当前系统状态输出一份“待办清单”减少新人的学习成本。5.3 我每三个月做一次“从零重建演练”这个习惯来自一次惨痛经历我的 macOS 升级失败不得不恢复数据结果发现保存的配置文件里有一条只有我自己才看得懂的怪异注释却不知道它原本是在解决什么问题。从那以后我每三个月会在虚拟机或一台闲置机器上跑一次完整的从零重建流程。模拟场景包括新系统、无任何工具链、只有一个空目录和 Git 权限。这样才能确保bootstrap.sh不是在特定环境下的偶然成功而是真正可复现。每次演练几乎都能发现一个被遗漏的依赖这个投入非常值。5.4 未来的扩展方向openrig 现在已经不局限于终端配置文件了我在往“开发环境巡检”的方向扩展。比如在安装完成后生成一份巡检报告列出尚未生效的配置、需要手动导入的密钥、版本过低的工具。这个巡检功能本质上就是把安装期间的状态检查从调试期保持到日常使用期。另外一个方向是集成语言工具链的版本管理。以前每个模块里可能都要写“安装 Python 3.12”“安装 Node 20”现在我更倾向于在 openrig 元层面统一管理运行时版本避免不同项目之间因为工具链版本打架。这是一个还在演化中的功能等稳定了我再单独写一篇文章聊聊细节。如果你也想动手整理自己的 openrig我的建议是步子别迈太大。先把最常用的三到五个工具的配置收进来跑通从零部署的流程再逐步加入更多模块。这套东西真正值钱的地方不在脚本本身而在你每一次踩坑后写下的注释、每一次环境变化后及时更新的维护习惯。把开发环境当作代码来管理它就会像代码一样给你一个可回滚、可审计、可交付的确定性。