Tauri macOS 发布后用户说「还是老版本」:三处版本号必须同步改
发完新包,用户装好 dmg 打开 app,截图一发过来——「关于」里还是 0.1.0,新功能一个都看不到。第一反应是不是代码没打进去?打开构建日志,新代码明明在里面。那到底哪里出了问题?
这是一个 Tauri macOS 发布流程里非常隐蔽的坑:版本号没动,dmg 文件名没变,Finder 拖拽安装悄悄失败——整个过程没有任何报错信号。
现象
- 打了新代码,构建成功,产出 dmg 文件;
- 用户下载安装,打开 app,「关于」窗口里版本号与上次完全一样;
- 新功能不见踪影,用户反馈「装了还是老版本」;
- 开发侧重新检查代码,逻辑没问题,构建产物也包含新代码。
根因
Tauri 的版本号不是单一来源。它分散在三个文件里,打包时三者必须同步:
| 文件 | 字段 |
|------|------|
| package.json | "version" |
| src-tauri/tauri.conf.json | "version" |
| src-tauri/Cargo.toml | [package] 下的 version |
只要有一处没改(常见情形是忘了 Cargo.toml),bundler 输出的 dmg 文件名就和上次一样,比如仍叫 MyApp_0.1.0_aarch64.dmg。
文件名不变带来第二层问题:用户把新 .app 拖进 /Applications 时,Finder 检测到「同名 app 已存在」,弹出「保留两者 / 替换 / 停止」提示。用户可能点了「保留两者」,也可能这次根本没弹(之前已经处理过),导致旧 app 原封不动地留在原位继续被打开。
没有版本号变化 = 没有视觉信号 = 用户和开发者都不知道装的是哪一个。
只改 package.json 和 tauri.conf.json 不够。Tauri 打包时 dmg 的文件名来自 Cargo.toml 中的 version,只要这里没动,输出产物的名字与上次完全相同,Finder 的替换弹窗行为不可预测,旧 app 极有可能存活。
排查:先确认安装的到底是不是新构建
比较安装目录里的二进制 mtime 和构建产物的 mtime:
APP="/Applications/MyApp.app"
# 查看已安装版本
/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$APP/Contents/Info.plist"
stat -f "%Sm %N" "$APP/Contents/MacOS/"*
# 查看构建产物
stat -f "%Sm %N" src-tauri/target/aarch64-apple-darwin/release/bundle/macos/MyApp.app/Contents/MacOS/*如果 /Applications 里的二进制 mtime 比构建产物还早,结论就一个:新 dmg 从来没有覆盖掉旧 app。
正解
第一步:三处一起改版本号
# package.json
"version": "0.2.0"
# src-tauri/tauri.conf.json
"version": "0.2.0"
# src-tauri/Cargo.toml
[package]
version = "0.2.0"
改完 Cargo.toml 后,下次构建 Cargo.lock 会自动更新,把它一并纳入同一个 chore(release): bump 0.1.0 → 0.2.0 commit。
第二步:重新构建,产出新名字的 dmg
现在 bundler 输出 MyApp_0.2.0_aarch64.dmg,「关于」弹窗显示 0.2.0,用户有了明确的版本信号。
第三步:用 ditto 强制覆盖用户机器上的旧 app
不要让用户再次用 Finder 拖拽,直接用 ditto 安装,它会保留 notarization 签名信息且不附加 quarantine xattr:
killall MyApp 2>/dev/null || true
rm -rf "/Applications/MyApp.app"
ditto "src-tauri/target/aarch64-apple-darwin/release/bundle/macos/MyApp.app" \
"/Applications/MyApp.app"
# 验证
/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" \
"/Applications/MyApp.app/Contents/Info.plist"
spctl --assess --type execute --verbose=2 "/Applications/MyApp.app"
# 预期输出:source=Notarized Developer IDpackage.json + tauri.conf.json + Cargo.toml 三处版本号同时 bump,重新构建得到新名字的 dmg;用 ditto 而非 Finder 拖拽覆盖安装,避免交互式替换弹窗导致旧 app 存活。
如果项目启用了自动更新(update.json + minisign 流程),版本号卡在旧值意味着更新服务器永远看不到「有新版本」,自动更新永远不会触发。同样需要三处同步改。
一句话外卖
Tauri 版本号是三文件 SSOT,任何一处没动,dmg 就和上次同名,Finder 拖拽安装不可靠,旧 app 静默存活——每次发版都要机械地三处一起改。