先來一段緊箍咒:nvm、fvm、gvm、sdkman、fnm、n、g、rvm、jenv、phpbrew、rustup、swiftenv、pyenv、rbenv...
這些都是用來解決編程語言多版本管理的工具,如果你是個程序員肯定認識或是用過幾個,但是剛接觸編程的小白,就會有些撓頭了。
啥是編程語言版本管理工具?它們有什麼用呢?
舉個例子,用 Java 的開發者可能會遇見的問題,公司的項目是萬年不變 JDK 8,但個人項目用的是最新的 JDK 21。這種情況下,在一臺電腦上開發公司和個人項目的時候,就需要切換一下當前開發環境對應的 JDK 版本,否則項目跑不起來。編程語言版本管理工具就是用來切換/管理編程語言不同版本的工具,比如 Java 語言對應的工具是 jenv
。
每一種編程語言都有一個對應的版本管理工具,對於多語言開發者來說就需要安裝、配置、學習各種版本管理工具,記憶不同工具的使用命令,這和緊箍咒無異。那咋辦啊?
莫慌,今天 HelloGitHub 帶來的是一款跨平臺版本、支持多語言的版本管理工具——vfox,讓你無憂應對多編程語言、不同版本的開發環境。該項目由國人(99 年的小夥)開發,更貼合國內開發者的使用習慣。
GitHub 地址:https://github.com/version-fox/vfox
接下來,讓我們一起走近 vfox 瞭解它的功能、上手使用、技術原理和強大的插件系統吧!
一、介紹
vfox 是一個類 nvm、fvm、sdkman、asdf 的版本管理工具,具有跨平臺、通用、易拓展的特性:
- 簡單:安裝簡單,一套命令管理所有語言
- 跨平臺:支持 Windows、Linux、macOS
- 人性化:換項目時自動切換到對應編程語言、支持自動補全
- 擴展性:容易上手的插件系統,添加冷門的編程語言
- 作用域:支持 Global、Project、Session 三種作用域
質疑聲:同類型的項目挺多的啊,不能一個國人開發、開源就來求 Star 吧?
下面,我們就來和在 GitHub 上有 20k Star 的同類型工具 asdf PK 一下,看看 vfox 是不是重複造輪子,到底能不能打!
二、對比 asdf
這裏主要從操作系統兼容性、性能和插件換源三個方面進行對比。
2.1 兼容性
兼容性 | Windows | Linux | macOS |
---|---|---|---|
asdf | ❌ | ✅ | ✅ |
vfox | ✅ | ✅ | ✅ |
首先,asdf 是用 shell 腳本實現的工具,所以並不支持原生 Windows 環境。而 vfox 是用 Go + Lua 實現的,因此天生支持 Windows 和其他操作系統。
2.2 性能
上圖是對兩個工具最核心的切換版本功能進行基準測試的結果,很容易就能得出結論:vfox 比 asdf 快 5 倍。
速度 | 平均 | 最快 | 最慢 |
---|---|---|---|
asdf | 158.7 ms | 154 ms | 168.4 ms |
vfox | 28.1ms | 27.1 ms | 32.3 ms |
技術解析:asdf 執行切換版本的速度之所以較慢,主要是由於其墊片機制。簡單來說,當你嘗試運行如 node 這樣的命令時,asdf 會首先查找對應的墊片,然後根據 .tool-versions
文件或全局設置來確定使用哪個版本的 node 。這個查找和確定版本的過程會消耗一定的時間,從而影響了命令的執行速度。
相比之下,vfox 則採用了直接操作環境變量的方式來管理版本,它會直接設置和切換環境變量,從而避免了查找和確定版本的過程。因此,在執行速度上要比使用墊片機制的 asdf 快得多。
雖然 asdf 很強,但是它對 Windows 原生無能爲力。雖然 vfox 很新,但在性能和跨平臺方面做得更好。
2.3 插件換源
大多數時候,我們會被網絡問題而困擾,所以切換下載源的操作是必不可少的。
下面以切換 Node.js 源爲例,對比 asdf 和 vfox 在換源時的區別。
asdf 是通過 asdf-vm/asdf-nodejs
插件實現了對於 Node.js 的支持,但該插件是需要手動預定義一個環境變量來修改下載源,多語言換源還需要設置多個不同的環境變量。
- 優點:可以靈活切換任何鏡像源
- 缺點:需要手動設置,操作不友好
vfox 選擇了另一種方法,即一個鏡像源對應一個插件。
$ vfox add nodejs/nodejs # 使用官方下載源
$ vfox add nodejs/npmmirror # 使用 npmmirror 鏡像
$ vfox add python/python # 官方下載源
$ vfox add python/npmmirror
雖然這樣會使倉庫的插件變多,但使用起來降低了負擔,也沒有亂七八糟的環境變量需要配置,對用戶非常友好!
三、上手
說了這麼多,還沒上手玩一下簡直忍不了。
3.1. 安裝
Windows 用戶只需要下載安裝器進行安裝即可,Linux 用戶可以使用 APT 或 YUM 來快速安裝,macOS 用戶可以使用 Homebrew 安裝。更詳細的安裝方式可查看文檔
$ brew tap version-fox/tap
$ brew install vfox
安裝完成之後,需要將 vfox 掛載到你的 shell 中,從下麪條目中選擇一條適合你 shell 的。
echo 'eval "$(vfox activate bash)"' >> ~/.bashrc
echo 'eval "$(vfox activate zsh)"' >> ~/.zshrc
echo 'vfox activate fish | source' >> ~/.config/fish/config.fish
# 對於 Powershell 用戶,將下面行添加到你的 $PROFILE 文件中
Invoke-Expression "$(vfox activate pwsh)"
3.2 使用
安裝好了,但你還做不了任何事情,因爲 vfox 是使用插件作爲擴展,按需安裝。
不知道應該添加哪些插件,可以用
vfox available
命令查看所有可用插件
所以你還需要安裝插件,以 Node.js 爲例,爲了獲得更好的體驗,我們添加 npmmirror 鏡像源插件:vfox add nodejs/npmmirror
。
在插件成功安裝之後,你就可以玩起來了!
- 安裝指定版本:
vfox install nodejs@<version>
- 安裝最新版本:
vfox install nodejs@latest
- 切換版本:
vfox use nodejs[@<version>]
文字表達遠不如圖片來的更直觀,我們直接上效果圖。
四、技術原理
vfox 支持 Global、Session、Project 三種作用域,這三種作用域能夠滿足我們日常開發所需的場景。
作用域 | 命令 | 說明 |
---|---|---|
Global | vfox use -g <sdk-name> |
全局範圍有效 |
Session | vfox use -s <sdk-name> |
當前 shell 會話有效 |
Project | vfox use -p <sdk-name> |
當前項目下有效 |
那麼你對它們的實現原理感興趣嗎?咱們廢話不多說,直接看原理圖!
vfox 是基於 shell 的 hook 機制實現的,hook 機制簡單來說就是每當我們執行完命令之後,shell 都會調用一下你配置的鉤子函數(hook),即 vfox env <shell-name>
命令,我們後面解釋這個命令是幹什麼的。
說回到作用域上來,vofox 是通過 .tool-versions
文件來記錄每個 SDK 對應的版本號信息。對於三種作用域,會分別在不同的地方創建 .tool-versions
文件,用於記錄作用域內所需要的 SDK 版本信息。
Global
->$HOME/.version-fox/.tool-versions
Project
->當前項目目錄
Session
->$HOME/.version-fox/tmp/<shell-pid>/.tool-versions
代碼如下:
func newSdkManagerWithSource(sources ...RecordSource) *Manager {
meta, err := newPathMeta()
if err != nil {
panic("Init path meta error")
}
var paths []string
for _, source := range sources {
// 根據不同的作用域選擇性加載不同位置的.tool-versions文件
switch source {
case GlobalRecordSource:
paths = append(paths, meta.ConfigPath)
case ProjectRecordSource:
// 當前目錄
curDir, err := os.Getwd()
if err != nil {
panic("Get current dir error")
}
paths = append(paths, curDir)
case SessionRecordSource:
// Shell會話臨時目錄
paths = append(paths, meta.CurTmpPath)
}
}
// env.Record是用來專門操作.tool-versions文件的, 增刪改查
var record env.Record
if len(paths) == 0 {
record = env.EmptyRecord
} else if len(paths) == 1 {
r, err := env.NewRecord(paths[0])
if err != nil {
panic(err)
}
record = r
} else {
r, err := env.NewRecord(paths[0], paths[1:]...)
if err != nil {
panic(err)
}
record = r
}
// SdkManager是用來專門管理Sdk的組件, 到這裏Manager就可以通過Record來獲取和修改Sdk版本信息咯
return newSdkManager(record, meta)
}
上面提到,最核心的其實是 hook 機制調用的 vfox env <shell-name>
命令,那它到底幹了件什麼事情呢?
func envCmd(ctx *cli.Context) error {
...
// 拿到對應shell的組件
s := shell.NewShell(shellName)
if s == nil {
return fmt.Errorf("unknow target shell %s", shellName)
}
// 上面提到的加載.tool-versions信息到Manager中
manager := internal.NewSdkManagerWithSource(internal.SessionRecordSource, internal.ProjectRecordSource)
defer manager.Close()
// 獲取需要配置的環境變量信息
envKeys, err := manager.EnvKeys()
if err != nil {
return err
}
// 將環境變量信息, 翻譯成符合對應shell的命令
exportStr := s.Export(envKeys)
fmt.Println(exportStr)
return nil
}
}
func (m *Manager) EnvKeys() (env.Envs, error) {
shellEnvs := make(env.Envs)
var paths []string
// 這裏就是前面說的, Record包含了所有的版本信息, 只需要取出來即可
for k, v := range m.Record.Export() {
if lookupSdk, err := m.LookupSdk(k); err == nil {
if keys, err := lookupSdk.EnvKeys(Version(v)); err == nil {
for key, value := range keys {
if key == "PATH" {
paths = append(paths, *value)
} else {
shellEnvs[key] = value
}
}
}
}
}
...
return shellEnvs, nil
}
沒看懂代碼沒關係,用一句話概括這段代碼的功能:將 .tool-versions
記錄的 SDK 版本信息,翻譯成具體 shell 可執行的命令,其實核心技術就這麼樸實無華。
五、插件系統
插件系統是 vfox 的核心,它賦予 vfox 無限的可能性,不僅僅侷限於單一的 SDK。通過插件系統,vfox 能夠靈活地適應任何 SDK 的需求,無論是現有的還是未來可能出現的。
更重要的是,插件系統使用 Lua 作爲插件的開發語言,內置了一些常用模塊,如 http
、json
、html
、file
等,這使得插件系統不僅功能強大,而且易於開發和自定義。用戶可以根據自己的需求,輕鬆編寫和定製自己的腳本,從而實現更多的功能。
口說無憑,我們直接寫一個簡單的插件來體驗一下,以寫一個 Windows 環境下可用的 Python 插件爲例。
5.1 插件模板結構
在開工之前,我們首先需要了解一下插件結構是什麼樣子,以及都提供了哪些鉤子函數供我們實現。
--- 內置全局變量: 操作系統和架構類型
OS_TYPE = ""
ARCH_TYPE = ""
--- 描述當前插件的基本信息, 插件名稱、版本、最低運行時版本等信息
PLUGIN = {
name = "xxx",
author = "xxx",
version = "0.0.1",
description = "xxx",
updateUrl = "https://localhost/xxx.lua",
minRuntimeVersion = "0.2.3",
}
--- 1.預安裝鉤子函數。vfox 會根據提供的元信息, 幫你提前下載好所需的文件(如果是壓縮包,會幫你解壓)放到指定目錄。
function PLUGIN:PreInstall(ctx)
return {
version = "0.1.1",
sha256 = "xxx", --- 可選
sha1 = "xxx", --- 可選
url = "文件地址"
}
end
--- 2.後置鉤子函數。這裏主要是做一些額外操作, 例如編譯源碼。
function PLUGIN:PostInstall(ctx)
end
--- 3.可用鉤子函數。 告訴 vfox 當前插件都有哪些可用版本。
function PLUGIN:Available(ctx)
end
--- 4.環境信息鉤子函數。 告訴 vfox 當前SDK所需要配置的環境變量有哪些。
function PLUGIN:EnvKeys(ctx)
end
總共就 4 個鉤子函數,是不是非常簡單。
5.2 Python 插件實現
OK,萬事俱備那我們正式開始實現 Python 插件咯~
--- vfox 提供的庫
local http = require("http") --- 發起 http 請求
local html = require("html") --- 解析 html
OS_TYPE = ""
ARCH_TYPE = ""
--- python 下載源地址信息
local PYTHON_URL = "https://www.python.org/ftp/python/"
local DOWNLOAD_SOURCE = {
--- ...
EXE = "https://www.python.org/ftp/python/%s/python-%s%s.exe",
SOURCE = "https://www.python.org/ftp/python/%s/Python-%s.tar.xz"
}
PLUGIN = {
name = "python",
author = "aooohan",
version = "0.0.1",
minRuntimeVersion = "0.2.3",
}
function PLUGIN:PreInstall(ctx)
--- 拿到用戶輸入版本號, 解析成具體版本號
local version = ctx.version
if version == "latest" then
version = self:Available({})[1].version
end
if OS_TYPE == "windows" then
local url, filename = checkAvailableReleaseForWindows(version)
return {
version = version,
url = url,
note = filename
}
else
--- 非 Windows 環境實現, 略
end
end
function checkAvailableReleaseForWindows(version)
--- 處理架構類型, 同一架構的不同名稱
local archType = ARCH_TYPE
if ARCH_TYPE == "386" then
archType = ""
else
archType = "-" .. archType
end
--- 檢查是否存在 exe 安裝器, 當然 Python 還提供了其他安裝器, 例如 msi、web-installer 等
local url = DOWNLOAD_SOURCE.EXE:format(version, version, archType)
local resp, err = http.head({
url = url
})
if err ~= nil or resp.status_code ~= 200 then
error("No available installer found for current version")
end
return url, "python-" .. version .. archType .. ".exe"
end
--- vfox 會在 PreInstall 執行完之後, 執行當前鉤子函數.
function PLUGIN:PostInstall(ctx)
if OS_TYPE == "windows" then
return windowsCompile(ctx)
else
--- 略
end
end
function windowsCompile(ctx)
local sdkInfo = ctx.sdkInfo['python']
--- vfox 分配的安裝路徑
local path = sdkInfo.path
local filename = sdkInfo.note
--- exe 安裝器路徑
local qInstallFile = path .. "\\" .. filename
local qInstallPath = path
--- 執行安裝器
local exitCode = os.execute(qInstallFile .. ' /quiet InstallAllUsers=0 PrependPath=0 TargetDir=' .. qInstallPath)
if exitCode ~= 0 then
error("error installing python")
end
--- 清理安裝器
os.remove(qInstallFile)
end
--- 告訴 vfox 可用版本
function PLUGIN:Available(ctx)
return parseVersion()
end
function parseVersion()
--- 這裏就是解析對應的 html 頁面, 通過正則匹配具體版本號了
local resp, err = http.get({
url = PYTHON_URL
})
if err ~= nil or resp.status_code ~= 200 then
error("paring release info failed." .. err)
end
local result = {}
--- 解析 html 略
return result
end
--- 配置環境變量, 主要是 PATH, 但是注意 Windows 和 Unix-like 路徑不一致, 所以要區分
function PLUGIN:EnvKeys(ctx)
local mainPath = ctx.path
if OS_TYPE == "windows" then
return {
{
key = "PATH",
value = mainPath
}
}
else
return {
{
key = "PATH",
value = mainPath .. "/bin"
}
}
end
end
至此,我們就完成了一個 Windows 環境下可用的 Python 插件啦~🎉
當然,這只是爲了方便演示如何自己實現插件,vfox 目前已經提供了完善的 Python 插件,可以通過 vfox add python/npmmirror
命令直接安裝使用哦。
vfox 目前已支持 12 種插件,還在努力豐富中💪💪💪
- Python ✅ ->
python/npmmirror
- Nodejs ✅ ->
nodejs/npmmirror
- Java ✅ ->
java/adoptium-jdk
- Golang ✅ ->
golang/golang
- Dart ✅ ->
dart/dart
- Flutter ✅ ->
flutter/flutter-cn
- .Net ✅ ->
dotnet/dotnet
- Deno ✅ ->
deno/deno
- Zig ✅ ->
zig/zig
- Maven ✅ ->
maven/maven
- Graalvm ✅ ->
java/graalvm
- Kotlin ✅ ->
kotlin/kotlin
- Ruby ⌛️
- PHP ⌛️
六、結束
我的初衷是不管什麼語言,只要是需要版本管理,只需要一個工具就能簡單高效的完成。所以我創建了 vfox,它是一款專注於多語言、多版本管理的生態工具,目標只有一個:讓所有的編程語言版本管理變得簡單易用。無論你是 JavaScript、Java 還是 Python 的開發者,vfox 都能爲你提供一站式的解決方案。
我們的願景是創建一個適合國人使用的、簡單易用的多語言、多版本管理工具。我們相信,只有真正理解開發者的需求,才能創造出真正有價值的工具。vfox 就是這樣的工具,它是爲了解決開發者在日常工作中遇到的版本管理問題而生。
GitHub 地址:https://github.com/version-fox/vfox
最後,感謝 HelloGitHub 提供的機會,讓我能向更多人介紹 vfox。作爲一個開源項目的創作者,我深感開源的力量。它不僅僅是代碼的共享,更是知識和經驗的共享。希望 vfox 能成爲我們溝通的橋樑,歡迎各種形式的反饋和建議,讓我們一起變強!