macOS / Linux 装了一堆包管理器?用 upkeep 一条命令全部更新

7 阅读8分钟

开发环境用得越久,包管理器通常越多。

系统层有 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

它会自动识别当前操作系统,检查机器上已经安装的工具,执行对应更新,并在最后统一汇总完成、跳过和失败状态。

没有安装的普通工具会安全跳过,不会因为缺少某个包管理器而让整个任务失败。单个更新步骤失败时,后面的步骤仍会继续执行,最终再通过非零退出码报告失败。

这意味着一次执行就能回答三个问题:

  1. 哪些工具完成了更新?
  2. 哪些工具因为未安装或当前环境不适用而跳过?
  3. 哪些步骤失败,需要单独处理?

upkeep 能更新什么

upkeep 同时处理平台专属系统包和跨平台开发工具。

分类macOSLinux
系统软件包Homebrew formula 与 caskDNF 软件包
Node.js 全局包npm、pnpm、Bunnpm、pnpm、Bun
Python CLIpipx、uv 及 uv toolspipx、uv 及 uv tools
Python 包当前 python3 中可安全更新的包当前已激活虚拟环境中的顶层包
Rustrustup、Cargo 全局工具rustup、Cargo 全局工具
RubyRubyGems 用户目录中的 gemRubyGems 用户目录中的 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 或贡献更多安全的更新策略。

项目地址:github.com/guanshan/up…

如果这个工具减少了维护本地开发环境的时间,欢迎点一个 Star,让更多需要统一更新入口的开发者看到它。