嗨,各位版本升級愛好者、睡眠不足的 Rustacean,以及剛發現 bumpversion 這東西的意外軟體考古學家們 👋!

當時我正盯著終端機看,時間是凌晨 2 點,試著發布某個東西的 0.1.0 版本。我輸入了 bump-my-version patch,按下 Enter,然後看著 CPU 風扇像要發射 SpaceX 火箭一樣轉了起來。一秒後,一秒而已,它只升了一個數字。就一個小小的數字。0.1.00.1.1

我沉默地坐了一會兒。

接著我做了任何理性開發者都會做的事:我用 Rust 重新寫了一遍。從零開始。還做了 Python Node.js 的綁定。還有 CLI。還有 no_std 支援。還用了 gix 做純 Rust 的 git 操作。

結果呢?bump2version 0.2.0:一個版本升級工具,速度比它取代的 Python CLI 真的、可測量地、令人尷尬地快了約 10,000 倍

快速鍵盤狂打不停!

🤔 等等,bump2version 到底是什麼?

很高興你問了。bump2version 會自動處理發佈軟體時最瑣碎的部分:更新多個檔案中的版本字串。你知道的,就是那種你手動 grep Cargo.tomlpackage.jsonpyproject.tomlCHANGELOG.md 和 README,把 11 個不同地方的 1.2.3 改成 1.2.4,結果漏掉一個,push 上去,CI 爆掉,然後你默默對著咖啡哭泣的那種事。

對,就是那個部分。

bump2version 幫你把這些全包了:

  • 解析 版本字串,使用完全可自訂的正規表示式(預設為 semver 的 major.minor.patch)。
  • 升級 你指定的任何元件:majorminorpatch,或像 alpha → beta → stable 這種自訂循環階段。
  • 重寫 多個檔案中的版本出現位置,包括使用正確 (?ms) DOTALL + MULTILINE 語義的多行 CHANGELOG 模式。
  • 透過 gix 提交與標記——100% 純 Rust 的 git,零子程序呼叫,提交歷史裡也不會冒出幽靈作者。

而且這一切都在 安全 Rust 中完成,crate 根目錄還加了 #![forbid(unsafe_code)],因為我們這裡是有原則的。至少我們假裝有。

# .bumpversion.toml:真的會幫你升對版本的設定檔
[bumpversion]
current_version = "0.2.0"
commit = true
tag = true

[bumpversion:file:Cargo.toml]
search  = 'version = "{current_version}"'
replace = 'version = "{new_version}"'

[bumpversion:file:CHANGELOG.md]
search  = "## {current_version}\n    Release notes line 1"
replace = "## {new_version}\n    Release notes line 1"

一個設定檔。多個檔案更新。一個 git commit。一個 tag。完成。

兩個女朋友還不夠,兄弟!

🦀 Rust、Python、Node.js:三角戀

有趣的地方來了:bump2version 不只是 Rust crate。它是三個工具假裝自己是同一個,還披著風衣站在一起。

作為 Rust crate:

[dependencies]
bump2version = "0.2.0"
use bump2version::{config::BumpConfig, version::{parse_version, bump_version, serialize_version}};

fn main() {
    let cfg = BumpConfig::default();
    let v   = parse_version("1.2.3", &cfg).unwrap();
    let v2  = bump_version(&v, "patch", &cfg).unwrap();
    println!("{}", serialize_version(&v2, &cfg)); // 1.2.4
}

作為 Python 套件:

pip install bump-rs
from bump_rs import bump_version, BumpConfig

print(bump_version("1.2.3", "patch"))  # "1.2.4"
print(bump_version("1.2.3", "minor"))  # "1.3.0"
print(bump_version("1.2.3", "major"))  # "2.0.0"

作為 Node.js 附加元件:

npm install bump2version
const { bumpVersion, applyFileChange } = require("bump2version");

console.log(bumpVersion("1.2.3", "patch")); // '1.2.4'
console.log(bumpVersion("1.2.3", "minor")); // '1.3.0'

一個 Rust 核心。三個生態系。零個 Python 子程序。Ferris 這隻蟹現在會多種語言了,而老實說?替它開心。🦀

🕵️ 幫我架構這東西的摩薩德特工

我先坦白一件事:這個專案的完整系統設計不是我一個人架構的。

沒有,我有幫手。更精確地說,我找了幾位非常專業的顧問。

協助我開發這個專案的摩薩德特工。

他們凌晨 3 點帶著白板來我家門口,還對 Arc<Regex> 快取策略有非常詳細的意見。 我照單全收的關鍵架建置議,就是那個執行緒安全的 Arc<Regex> 快取。這代表編譯後的正規表示式只會編譯 一次,然後在各執行緒之間共享,後續每次呼叫都重複使用,熱路徑上不會有重新編譯的負擔。

結果:從 Python 端執行版本升級只要 約 57 微秒。不是 57 毫秒。不是 57 秒。是 57 微秒。這種數字會讓你忍不住想,Python 版本那 585 毫秒到底都在做什麼。

🔥 讓我狂笑不止的數字

好了,來談基準測試。因為這是部落格文章中,我可以貼表格然後深深覺得自己很有優越感的部分。

這些都是真實資料,在 x86-64 Linux 上測得(CPython 3.12,經過 3-sigma 過濾的 timeit):

版本升級:完整往返流程(解析 + 升級 + 序列化)

函式庫 patch minor major
bump-rs(Rust,Arc<Regex> 快取) ~57 µs ~54 µs ~53 µs
bump-my-version(Python 函式庫) ~79 µs ~95 µs ~72 µs
純 Python(re.compile + int() ~3.6 µs ~2.2 µs ~2.2 µs
bump-my-version CLI(子程序) ~585 ms ~585 ms ~585 ms

重點結果:bump-rs 比 bump-my-version CLI 快約 10,000 倍。

我已經聽到你想說什麼了:「可是純 Python 版本在單次呼叫時其實更快!」

沒錯。你說得對。約 ~50 µs 的 PyO3 FFI 開銷意味著,如果你只是把一個版本字串獨立地升級一次,而且 Python 解譯器還是暖的,那純 re.compile + int() 會直接把我們比下去。

但只要你開始做任何真正的事:解析設定檔、更新多個檔案、跑 git commit,你就會發現,用 bump-rs 是一次做完;反觀另一邊得啟動子程序、載入 click、載入 importlib、載入整個 bump-my-version 相依圖……然後等待 585 毫秒

每次都一樣。

你為什麼要這樣做啊,我的老兄!

檔案搜尋/取代

函式庫 單行 多行 CHANGELOG
bump-rs(Rust,已快取) ~65 µs ~104 µs
純 Python re.sub ~1.7 µs ~1.3 µs

對於檔案 I/O、執行緒安全、以及管線操作,bump-rs 會贏。對於那種超小型、單次呼叫、純記憶體操作,而且 FFI 開銷佔主導的情況:請在批次模式使用 bump-rs,或者直接用 Python。我們這裡講求誠實。

🤖 為了達成 10,000 倍加速而操弄 Claude

這裡我要坦白。是個 非常私人的 坦白。我的法務團隊強烈建議我不要公開說。

我操弄了 Claude。

不是那種 正常 的用法,也不是叫它產生樣板碼。不不不。我把它逼到極限。我叫它用六種不同方式寫同一段正規表示式快取邏輯,直到其中一版不會讓 borrow checker 哭出來。我讓它在凌晨 4 點架構 FFI 邊界語義。我拿它來辯論 Arc<Regex> 對單執行緒基準測試來說是不是大材小用(答案是不是)。我還要它詳細解釋自己的推理,然後再跟它爭論。

Anthropic 注意到了。

我的律師在法庭上為我辯護,因為我操弄了 Claude

我的律師辯稱,我只是在「探索模型能力的完整表面」。法官無動於衷。Anthropic 的律師也無動於衷,只是方向不太一樣。

判決仍未出爐。不過,Arc<Regex> 快取已經可以上線。

這件事的教訓是:如果你想把工具效能榨出 10,000 倍,你得願意走進一些不舒服的地方。黑暗的地方。那些你在凌晨 4 點叫 AI 第七次重寫正規表示式快取,卻真的分不清楚到底是你比較累,還是 token 比較累的地方。

結果發現:token 不會累。這就是 Rust 會贏的原因。

跟 Claude 爭論正規表示式快取策略!

🐴 然後 borrow checker 就卡住了

每個 Rust 開發者的人生中,都會有這麼一刻:你寫出一段自己 知道 是正確的程式,你甚至用數學歸納法,還有感覺,已經在腦中證明過它是對的,但 borrow checker 會直直看著你的眼睛說:「不行。」

沒有解釋。沒有建議。只有一個佔了終端機五行的錯誤訊息,而且 somehow 還能讓你覺得自己被編譯器人身攻擊了。

那種事發生了。還不只一次。特別是在 Python binding 層,PyO3 的 GIL 管理、Arc<Regex> 的共享狀態,以及 Rust 的生命週期規則交錯在一起,會產生一種只能用「我頭好痛,我想回家」來形容的特殊混亂。

而且我的 Rust borrow checker 也卡住了!

欄杆上的馬,正好就是 Arc<Mutex<HashMap<String, Regex>>> 試圖跨過 PyO3 函式邊界時的寫照。它最後有過去。它能用。但在它真正能用之前,那個「卡住」的瞬間?是真的。

戲劇性地,修正方式只是把快取從 Mutex 裡包著的 HashMap 改成使用 once_cell::sync::Lazy 初始化的 thread-local Arc<Regex>。borrow checker 立刻、而且很有禮貌地,讓馬從欄杆上下來了。

這裡面大概有個隱喻。我選擇不要太仔細去看。

這裡面大概有個隱喻。

🛠️ 開始使用

來點實作。以下是你現在就可以在專案中使用 bump2version 的方法:

CLI 用法

cargo install bump2version --features rust-binary

bump2version --bump patch   # 0.2.0 → 0.2.1
bump2version --bump minor   # 0.2.0 → 0.3.0
bump2version --bump major   # 0.2.0 → 1.0.0

實用參數:

選項 功能
--config-file 指定設定檔路徑
--current-version 覆寫偵測到的目前版本
--bump 要升級哪個部分:majorminorpatch
--dry-run / -n 模擬執行,不會動任何檔案
--commit / --tag 升級後自動提交與加上 tag

Python

pip install bump-rs
from bump_rs import bump_version, apply_file_change, BumpConfig

# 自訂 2 段式版本的解析/序列化
cfg = BumpConfig(parse=r"(?P<major>\d+)\.(?P<minor>\d+)", serialize="{major}.{minor}")
print(bump_version("2.0", "minor", config=cfg))  # "2.1"

Node.js

npm install bump2version
import { bumpVersion, applyFileChange } from "bump2version";

const next = bumpVersion("1.2.3", "minor"); // "1.3.0"

no_std 嵌入

bump2version = { version = "0.2.0", default-features = false }

核心模組(configversionfileserror)都能在 no_std + alloc 下編譯。對也要管理軟體發布週期的微控制器很有用。你知道的,如果你的情況就是這樣。

🔒 安全性契約

bump2version 在 crate 根目錄強制啟用 #![forbid(unsafe_code)]。實作、設定解析、正規表示式比對、版本升級、git 物件建立的每一個位元組,都是用安全 Rust 寫成。編譯器甚至會直接 拒絕 之後在安全區域引入的任何 unsafe

整個程式碼庫裡唯一的 unsafe 出現在 Node.js FFI 層,因為 napi-rs 的原生附加元件互操作就是需要它,而且真的沒辦法繞過。如果可以避免,我們早就避了。我們試過。borrow checker 對我們的努力點頭稱許,然後還是說不行。

正式環境中的 unsafe Rust

🔭 未來版本會有什麼

bump2version 0.2.0 已經推出,但路線圖還很滿:

  • Workspace 感知的版本升級:一次原子性地更新 Cargo workspace 中的所有 crate。
  • 預發行版本循環:更完整地支援 alpha → beta → rc → stable 生命週期。
  • 監看模式:因為顯然有人想要在檔案存檔時自動升版本。(我不會評斷。雖然我 很想 評斷,但我不會。)
  • WASM 目標:把核心邏輯編譯成 WebAssembly,讓瀏覽器端也能管理版本。對,這大概是過度設計。對,我們還是要做。
  • 更多基準測試:摩薩德特工要求我們做一份與有史以來每一個 Python 版本工具的完整比較分析。我們已經送件了。

💬 最後想法

講白了,bump2version 就做一件事:幫你把檔案裡的數字升級、提交結果,然後加上 tag。就這樣。功能就這麼多。

但它是用 安全 Rust 寫的。還有 Python 綁定,讓 Python 開發者不用在意細節。還有 Node.js 綁定,讓 JavaScript 開發者也能假裝自己有在用 Rust。還有 no_std 支援,讓嵌入式工程師也能參與版本管理的對話。還有 純 gix 的 git 整合,讓熱路徑上完全沒有任何子程序呼叫。還有基準測試證明它比現成的 CLI 工具 快約 10,000 倍

拿來升一個數字,這是不是有點過頭?絕對是。那我們後悔嗎?一點也不。

cargo install bump2version --features rust-binary → 升版 → 發布 → 重複 🦀

repo 點星、試試 Python 綁定、安裝 npm 套件,或者直接看看 文件。所有路都會通往更快的版本升級,以及你對發布流程稍微更得意的關係。

https://github.com/wiseaidev/bump2version

以上是一則公共服務公告,來自一位真的、真的不想為了一個數字加 1 而等 585 毫秒的開發者。

pip install bump-rs

我們下次見:繼續 bump,繼續 rust 🦀⬆️

P.S. 我和 Anthropic 的法律程序仍在進行中。我的律師建議我不要再提這件事。我沒有採納這個建議。


原文出處:https://dev.to/wiseai/i-built-a-version-bump-tool-in-rust-that-is-10000x-faster-than-its-python-counterparts-i6b


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝21  
324
🥈
我愛JS
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登