一个 Rust 写的版本管理器:把 Node、Go、Java、Python 收进同一个目录

chihqiang chihqiang 2026-11-11 约 10 分钟 3730 字

一个项目跑 Node 18,另一个项目要 Node 22;Go、Java、Python 各自还有一套版本管理器。换台电脑,或者接手一个老项目,光是“把版本对齐”就要折腾半天。

lvm 想解决的就是这件事:用 Rust 写成的单个二进制,统一管理 Node.js、Go、Java、Python、Dart、Flutter、Kotlin、Rust 八种运行时,支持全局和项目级切换,所有版本都放在自己的目录里,不往系统目录里塞东西。

安装:一条命令

macOS 或 Linux 下可以直接执行:

1
curl -fsSL https://raw.githubusercontent.com/chihqiang/lvm/main/install.sh | bash

脚本会识别操作系统和 CPU 架构,下载对应的二进制并安装到 /usr/local/bin/lvm。也可以从 Releases 手动下载,放进 PATH 即可。

首次使用,建议把这两行加进 ~/.bashrc 或 ~/.zshrc:

1
2
eval "$(lvm env)"
eval "$(lvm hook)"

lvm env 会设置 LVM_HOME、把 ~/.lvm/bin 加入 PATH,并为已经激活的语言输出 GOPATH、JAVA_HOME、FLUTTER_HOME 等变量。lvm hook 则负责进入目录时自动切换版本。

先拿 Node.js 试一遍

命令基本和常见的版本管理器一致:

1
2
3
4
5
6
7
8
9
10
11
# 查看可安装版本
lvm list-remote node
lvm list-remote node --lts

# 安装最新 22.x,或指定精确版本
lvm install node 22
lvm install node 20.0.0

# 切换并查看
lvm use node 22
lvm current node

lvm install 装完默认会把该版本设为默认版本;如果不希望这样,可以加 --no-default。lvm use 在目标版本还没安装时会自动下载,不想联网就加 --skip-install。

版本号支持完整 semver、部分版本(22、1.22)、LTS 代号(--lts=iron)和 lts/-1 这类 LTS 偏移写法。想回到系统自己装的版本,用:

1
lvm use node system

真正省事的是 .lvmrc

单语言项目用 .nvmrc 就够了,但一个仓库里同时有前端、后端和脚本时,每个语言配一个文件很麻烦。lvm 的做法是在项目根目录放一个 .lvmrc:

1
2
3
node=22.0.0
go=1.22.3
python=3.12

然后不带参数执行:

1
lvm install

它会把文件里声明的版本一次装好。多个语言时会并行安装,比挨个执行快一些。

切换也一样,进入项目目录后:

1
lvm use

查找顺序是 .lvmrc → 各语言自己的 RC 文件(Node 为 .nvmrc)→ 默认别名 → 最新版。也就是说,从 nvm 或 fnm 迁过来时,原来的 .nvmrc 不用删;只有 .lvmrc 不存在时才会回退去读它。

如果希望切换后顺手把版本写进 .lvmrc,加 --save 或 -w:

1
2
lvm install node 22 --save
lvm use go 1.22 --save

写入时会保留原有注释和空行,不会把整个文件重排。

自动切换、隔离和清理

把 eval "$(lvm hook)" 加进 shell 配置后,进入包含 .lvmrc 或 .nvmrc 的目录就会自动切换。bash 用 PROMPT_COMMAND,zsh 用 chpwd,fish 监听 PWD 变化,PowerShell 则覆写 prompt。

这里有个细节做得比较克制:hook 调用的是 lvm use --no-default --skip-install。也就是说,进入目录不会悄悄改你的默认版本,也不会因为版本没装就突然开始下载。缺版本时它不会自动补,需要你自己执行 lvm install。

版本隔离也比较彻底:

  • Node.js 的 npm install -g 装进对应版本目录;
  • Go 的 GOPATH 指向当前版本的 packages,go install 的二进制不与其他版本共享;
  • Dart、Flutter 的 PUB_CACHE 按版本隔离;
  • Java、Kotlin、Rust 等通过 current 符号链接切换。

好处是升级 Node 不会让旧项目的全局 CLI 突然坏掉;代价是切换版本后,全局包需要重新安装。lvm 为此提供了两个 Node.js 专用能力:

1
2
# 从旧版本迁移全局包
lvm install node 22 --reinstall-packages-from=20.14.0

以及在 ~/.lvm/default-packages 中列出每次安装后都要装的包:

1
2
3
4
# 每行一个,支持 # 注释
pnpm@8.15.9
typescript
eslint

旧版本攒多了,可以清理:

1
lvm prune node --keep 3

它只保留最新的 N 个版本,并跳过当前使用和默认版本。

国内网络下的镜像配置

每种语言都留了镜像环境变量。比如:

1
2
3
4
5
# Node.js
export LVM_NODE_MIRROR=https://npmmirror.com/mirrors/node

# Go
export LVM_GO_MIRROR=https://golang.google.cn/dl

Java、Python、Dart、Flutter、Kotlin、Rust 也都有对应的 LVM_*_MIRROR。数据目录默认是 ~/.lvm,如果设置了 XDG_DATA_HOME,则使用 $XDG_DATA_HOME/lvm。

下载过的安装包会留在缓存目录,加上 --offline 就只从缓存安装,适合内网或弱网环境。

实现上有几个值得看的小设计

lvm 的代码量不大,但几个工程细节处理得比较认真。

用 trait 收敛语言差异。 每种语言实现 Language trait,必须提供 install 和 latest_version;目录结构、二进制名、版本前缀、bin 子目录都有默认实现,个别语言再覆盖。新增语言不需要改命令解析,注册进 LanguageRegistry 就能用。像 Rust 这种把工具链拆在 rustc/、cargo/ 子目录里的布局,就通过覆盖 is_installed 和 post_install 来处理。

安装和切换都尽量做到“要么成功,要么保持原状”。 版本先解压到临时目录,完成后再 rename 到正式目录;切换时先在旁边创建临时符号链接,再用 rename 替换。Unix 上这两步都是原子操作,不会出现“装到一半”的版本被当成可用版本。Windows 的符号链接替换要弱一些,代码里单独做了兼容。

解压时防路径穿越。 zip 和 tar.gz 都会去掉顶层目录,并拒绝绝对路径和包含 .. 的条目,避免压缩包把文件写到版本目录之外。

校验失败会重下,拿不到校验信息则告警跳过。 官方源提供校验和时会做 SHA256 比对,缓存文件校验不过就删掉重下。不过如果镜像没有提供校验信息,lvm 只打印警告然后继续安装,这一点在使用第三方镜像时需要留意。

多语言安装并行执行。 .lvmrc 里声明多个语言时,安装任务会并发跑;失败信息会汇总后一起返回,不会只报第一个错误。

CI 也覆盖了 Linux、macOS、Windows 三个平台,执行 fmt、clippy、构建和测试。

采用前需要知道的边界

lvm 目前还是 0.1.x,适合个人开发环境和对工具链要求不那么苛刻的团队,生产环境的关键构建链路建议先做验证。

几个实际使用中的注意点:

  • 自动切换不负责安装。 hook 带了 --skip-install,进入新项目发现版本缺失时,需要手动执行 lvm install。
  • Windows 的自动 hook 是受限的。 默认会提示不支持,只有显式指定 --shell powershell 才会生成对应脚本。
  • 全局包按版本隔离。 这不是 bug,而是设计选择;频繁切换版本的话,提前维护 default-packages 会省事很多。
  • 镜像的校验完整度取决于镜像本身。 如果很在意供应链安全,优先使用提供 SHA256 的官方源或可信镜像。

最后

如果你只写 Node,nvm 或 fnm 已经够用;lvm 的价值主要在“多语言 + 项目级锁定 + 统一目录”这三件事同时成立的时候。一个二进制、一个 .lvmrc、一次 lvm install,至少能让新机器上的环境准备少几个步骤。

项目地址:github.com/chihqiang/lvm

分享: Twitter Facebook
chihqiang

chihqiang

代码与生活的点滴记录