返回博客

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 原封不动地留在原位继续被打开。

没有版本号变化 = 没有视觉信号 = 用户和开发者都不知道装的是哪一个。

坑:忘改 Cargo.toml,dmg 文件名不变,旧 app 静默存活

只改 package.jsontauri.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 ID
正解:三文件同步 + ditto 强制覆盖

package.json + tauri.conf.json + Cargo.toml 三处版本号同时 bump,重新构建得到新名字的 dmg;用 ditto 而非 Finder 拖拽覆盖安装,避免交互式替换弹窗导致旧 app 存活。

附:auto-updater 场景同样受影响

如果项目启用了自动更新(update.json + minisign 流程),版本号卡在旧值意味着更新服务器永远看不到「有新版本」,自动更新永远不会触发。同样需要三处同步改。

一句话外卖

Tauri 版本号是三文件 SSOT,任何一处没动,dmg 就和上次同名,Finder 拖拽安装不可靠,旧 app 静默存活——每次发版都要机械地三处一起改。