开发环境用得越久,包管理器通常越多。
系统层有 Homebrew 或 DNF,Node.js 生态里有 npm、pnpm 和 Bun,Python 工具可能来自 pipx 或 uv,此外还有 rustup、Cargo、RubyGems。每套工具都有自己的更新命令、参数和安全边界。
一次完整更新,往往意味着在终端里依次执行这些命令:
brew update && brew upgrade
npm update --global
pnpm update --global --latest
bun update --global --latest
pipx upgrade-all
uv tool upgrade --all
rustup update
cargo-install-update --all
gem update
真正麻烦的并不是记住某一条命令,而是下面这些问题:
- 当前机器到底安装了哪些工具?
- macOS 和 Linux 应该执行哪些不同的系统更新?
- Python 环境受 PEP 668 保护时,怎样避免破坏系统环境?
- 某一步失败后,后面的更新是否还能继续?
- 私有 npm 包如何使用独立 registry,又不把内部配置写进公开脚本?
- 多开两个终端时,如何避免两个更新任务同时运行?
为了解决这些日常问题,我开源了 upkeep:一个面向 macOS 和 Linux 的本地包管理器一键更新器。
GitHub:github.com/guanshan/up…
一条命令更新本地开发工具
upkeep 的核心入口只有一条命令:
make update
它会自动识别当前操作系统,检查机器上已经安装的工具,执行对应更新,并在最后统一汇总完成、跳过和失败状态。
没有安装的普通工具会安全跳过,不会因为缺少某个包管理器而让整个任务失败。单个更新步骤失败时,后面的步骤仍会继续执行,最终再通过非零退出码报告失败。
这意味着一次执行就能回答三个问题:
- 哪些工具完成了更新?
- 哪些工具因为未安装或当前环境不适用而跳过?
- 哪些步骤失败,需要单独处理?
upkeep 能更新什么
upkeep 同时处理平台专属系统包和跨平台开发工具。
| 分类 | macOS | Linux |
|---|---|---|
| 系统软件包 | Homebrew formula 与 cask | DNF 软件包 |
| Node.js 全局包 | npm、pnpm、Bun | npm、pnpm、Bun |
| Python CLI | pipx、uv 及 uv tools | pipx、uv 及 uv tools |
| Python 包 | 当前 python3 中可安全更新的包 | 当前已激活虚拟环境中的顶层包 |
| Rust | rustup、Cargo 全局工具 | rustup、Cargo 全局工具 |
| Ruby | RubyGems 用户目录中的 gem | RubyGems 用户目录中的 gem |
其中有几个行为值得单独说明。
npm、pnpm 与 Bun
npm 会枚举当前全局包并执行更新。即使 npm ls 因 extraneous 或 invalid 依赖树返回非零状态,只要输出仍可用,upkeep 就会继续处理。
pnpm 会先在禁网、禁交互提示的条件下探测全局目录。这样可以避免未激活的 Corepack shim 在更新过程中突然触发下载或等待输入。
Bun 没有全局清单时会安全跳过,不会把「没有内容可更新」误判为任务失败。
pipx 与 uv
pipx 只处理其管理的命令行工具。macOS 缺少 pipx 时,可以通过 Homebrew 安装;Linux 默认只更新已经存在的 pipx,不主动修改系统 Python。
uv 会更新自身及其管理的工具。通过 GitHub API 自更新受限时,upkeep 会回退到 uv 官方安装脚本;如果 uv 由 Homebrew 或其他外部包管理器安装,则把自身更新交还给对应包管理器,只更新 uv tools。
Rust、Cargo 与 RubyGems
检测到 rustup 时执行工具链更新。Cargo 全局工具依赖已经安装的 cargo-update,缺少时只给出提示,不会为了完成一次更新而临时编译新的辅助工具。
RubyGems 只更新用户目录中的 gem,不使用 sudo,也不会触碰系统级 RubyGems 安装。
Python 更新为什么要区分平台
本地更新脚本最容易出问题的地方之一是 Python。
直接执行全局 pip install --upgrade 看起来省事,但可能遇到系统 Python、Homebrew Python、虚拟环境、用户 site 和 PEP 668 等多种情况。upkeep 因此采用了偏保守的策略。
macOS
upkeep 检查当前 python3 的过期包,优先使用可写的安装目录,其次使用用户 site。
如果检测到 PEP 668 保护,或者找不到明确且安全的可写目标,该步骤会跳过。脚本不会加入 --break-system-packages,也不会回退到 sudo pip。
Linux
Linux 只更新当前已经激活的虚拟环境,并且只处理顶层、非 editable 的过期包。更新结束后还会执行 pip check。
没有激活虚拟环境时,系统 pip 不会被调用。
这套策略的目标不是「尽可能多地升级」,而是「只更新能够确认安全边界的内容」。
默认保守,但保留私有 npm 包能力
公开仓库不应该硬编码公司域名、私有 registry 或内部包名。upkeep 将这些内容放在仓库外的可选配置中。
默认配置路径是:
~/.config/upkeep/config.sh
也可以通过 UPKEEP_CONFIG 指定其他文件。仓库提供了中性示例:
PRIVATE_NPM_REGISTRY='https://registry.example.com/npm'
PRIVATE_NPM_PACKAGES=(
'@acme/cli'
'@acme/strict-cli|--engine-strict'
)
每一项可以只写包名,也可以通过 | 附加额外的 npm install 参数。
显式配置的私有包会始终安装或更新,并在 registry 非空时走指定 registry。其余全局 npm 包继续使用 npm 当前配置的公开 registry。
没有私有配置时,upkeep 不会触发任何私有包安装,所有枚举到的 npm 全局包都按普通公开包处理。
更新之前,先做一次只读体检
在新机器或工具大版本升级后,可以先运行:
make doctor
doctor 不会修改任何状态,只检查真实环境中的关键条件,包括:
- 操作系统与 Bash 版本;
- 并发锁路径和残留状态;
- Homebrew 或 DNF 是否存在;
- npm registry 与 pnpm Corepack shim 状态;
- Python、pip、PEP 668、pipx 和 uv 安装方式;
- rustup、Cargo、cargo-update 与 RubyGems 用户目录。
自动化测试可以证明脚本符合预期,但无法覆盖每台机器上的真实工具版本。doctor 用来补上这一层环境检查。
把安全边界写进脚本
一键更新工具如果没有明确边界,很容易从「省时间」变成「修环境」。upkeep 对高风险操作做了主动限制:
- 不修改任何项目依赖或 lockfile;
- 不执行
npm audit fix --force; - 不执行 DNF autoremove 或 Homebrew cleanup;
- 不绕过 PEP 668;
- 不批量更新 Linux 系统 Python;
- 不使用 sudo 更新 Python 包或 RubyGems;
- sudo 只用于经过校验的 DNF 绝对路径;
- 检测到
NODE_TLS_REJECT_UNAUTHORIZED=0时,仅在当前更新进程中恢复 TLS 校验; - 单个步骤失败后继续执行,最后统一汇总并返回非零状态。
脚本还会使用并发锁,避免同一用户同时运行多个更新任务。
Linux 优先使用 flock,进程退出后由内核自动释放;没有 flock 的 macOS 使用原子 mkdir 锁,并处理 PID 陈旧锁与信号清理。
顺手清理 Docker 构建缓存
仓库还提供了一个独立的 Docker 清理入口:
make clean-docker
它会清理未使用的构建缓存和悬空镜像,并在操作前后输出 docker system df。
该命令不会删除有标签的镜像、容器、卷或网络,也不会执行范围更大的 docker system prune。
Bash 3.2 兼容与双平台 CI
macOS 系统自带的 Bash 版本较旧,因此 upkeep 以 Bash 3.2 作为最低兼容版本。
代码不使用关联数组、mapfile 或其他 Bash 4+ 特性,空数组在 set -u 下也使用兼容写法。
仓库目前包含 57 项基于 mock 命令的自动化测试,覆盖:
- Darwin 与 Linux 平台分流;
- Homebrew、DNF 和 sudo 边界;
- npm 私有配置、空配置与枚举失败;
- pnpm Corepack、Bun、pipx、uv、Cargo 与 RubyGems;
- Python 虚拟环境、PEP 668 和可写目录策略;
- 并发锁、陈旧锁、失败汇总与危险环境变量;
- Docker 清理范围和失败处理。
GitHub Actions 会在 Ubuntu 和 macOS 14 上运行完整测试。macOS 任务直接覆盖 Bash 3.2,避免「Linux 上测试通过,macOS 自带 Bash 无法运行」的问题。
三步开始使用
git clone https://github.com/guanshan/upkeep.git
cd upkeep
# 先进行只读体检
make doctor
# 执行全部更新
make update
查看更新范围和安全边界:
make update-help
运行测试:
make test
upkeep 只有 Bash 和 Make 两个基础依赖,不需要常驻进程、数据库或额外运行时。
适合哪些开发环境
upkeep 比较适合以下情况:
- 同时使用 macOS 与基于 DNF 的 Linux;
- 本地安装了多个语言生态的全局工具;
- 希望定期更新工具,但不想维护一长串命令;
- 重视 Python、sudo、自动清理和并发执行的安全边界;
- 需要在公开默认行为之外配置少量私有 npm CLI。
它不是项目依赖更新器,也不替代 Renovate、Dependabot、Ansible 或系统配置管理工具。upkeep 只负责当前开发机上的包管理器与全局工具更新。
最后
本地开发工具通常不会一次性变复杂,而是在长期使用中逐渐增加。upkeep 把分散的更新命令统一到一个入口,同时尽量保留各个平台和工具原本的安全边界。
仓库采用 MIT License,欢迎试用、提交 Issue 或贡献更多安全的更新策略。
如果这个工具减少了维护本地开发环境的时间,欢迎点一个 Star,让更多需要统一更新入口的开发者看到它。