build-cross-platform-packages

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Build Cross-Platform Packages

构建跨平台安装包

Build professional distributable packages for macOS, Windows, and Linux for Rust GUI applications.
Quick reference for common tasks:
  • macOS DMG with app bundle and CLI tools included
  • Windows MSI with Start Menu shortcuts and PATH configuration
  • Linux DEB with desktop integration and dependency management
  • Automated GitHub Actions workflows for all platforms
  • SLSA attestations for supply chain security
  • Homebrew formula auto-updates via repository_dispatch
为Rust GUI应用构建适用于macOS、Windows和Linux的专业可分发安装包。
常见任务快速参考:
  • 包含应用包和CLI工具的macOS DMG
  • 带开始菜单快捷方式和PATH配置的Windows MSI
  • 集成桌面环境并管理依赖的Linux DEB
  • 适用于所有平台的自动化GitHub Actions工作流
  • 供应链安全的SLSA证明
  • 通过repository_dispatch实现Homebrew公式自动更新

macOS DMG Package

macOS DMG安装包

What this creates: Professional drag-to-install DMG with app bundle containing both GUI and CLI binaries.
创建结果: 专业的拖拽式安装DMG,包含同时带有GUI和CLI二进制文件的应用包。

Icon Requirements

图标要求

  • Size: 512x512 PNG minimum, transparent background recommended
  • Generate: Use Ideogram (https://ideogram.ai) or AI generator for professional quality
  • Convert:
    convert icon.jpg -resize 512x512 icon.png
    or native macOS:
    sips -z 512 512 icon.png
  • Convert to ICNS:
    mkdir AppIcon.iconset && sips -z 512 512 icon.png --out AppIcon.iconset/icon_512x512.png && iconutil -c icns AppIcon.iconset
  • Place:
    docs/AppIcon.png
    (referenced in Info.plist without extension)
  • Tip: macOS automatically handles Retina @2x icons when using .icns format
  • 尺寸: 最小512x512 PNG,推荐使用透明背景
  • 生成: 使用Ideogram (https://ideogram.ai) 或AI生成工具获取专业质量图标
  • 转换:
    convert icon.jpg -resize 512x512 icon.png
    或原生macOS命令:
    sips -z 512 512 icon.png
  • 转换为ICNS格式:
    mkdir AppIcon.iconset && sips -z 512 512 icon.png --out AppIcon.iconset/icon_512x512.png && iconutil -c icns AppIcon.iconset
  • 放置位置:
    docs/AppIcon.png
    (在Info.plist中引用时无需扩展名)
  • 提示: 使用.icns格式时,macOS会自动处理Retina @2x图标

App Bundle Structure

应用包结构

YourApp.app/Contents/
├── Info.plist           # Application metadata
├── MacOS/
│   ├── your-gui-binary  # Main executable (launches GUI)
│   └── bin/
│       └── your-cli-binary  # CLI tool (accessed via PATH)
└── Resources/
    └── AppIcon.icns     # Application icon
Why this structure?
  • GUI launches when app is double-clicked
  • CLI accessible system-wide when app is in /Applications
  • Single DMG distributes both tools
YourApp.app/Contents/
├── Info.plist           # 应用元数据
├── MacOS/
│   ├── your-gui-binary  # 主可执行文件(启动GUI)
│   └── bin/
│       └── your-cli-binary  # CLI工具(可通过PATH访问)
└── Resources/
    └── AppIcon.icns     # 应用图标
为何采用此结构?
  • 双击应用时启动GUI
  • 应用安装到/Applications后,CLI可在系统范围内访问
  • 单个DMG同时分发两种工具

Info.plist Template

Info.plist模板

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleName</key><string>YourApp</string>
    <key>CFBundleDisplayName</key><string>Your App</string>
    <key>CFBundleIdentifier</key><string>com.yourcompany.yourapp</string>
    <key>CFBundleVersion</key><string>1.0.0</string>
    <key>CFBundleShortVersionString</key><string>1.0.0</string>
    <key>CFBundleExecutable</key><string>your-gui-binary</string>
    <key>CFBundleIconFile</key><string>AppIcon.icns</string>
    <key>CFBundlePackageType</key><string>APPL</string>
    <key>LSMinimumSystemVersion</key><string>10.13</string>
    <key>NSHighResolutionCapable</key><true/>
</dict>
</plist>
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleName</key><string>YourApp</string>
    <key>CFBundleDisplayName</key><string>Your App</string>
    <key>CFBundleIdentifier</key><string>com.yourcompany.yourapp</string>
    <key>CFBundleVersion</key><string>1.0.0</string>
    <key>CFBundleShortVersionString</key><string>1.0.0</string>
    <key>CFBundleExecutable</key><string>your-gui-binary</string>
    <key>CFBundleIconFile</key><string>AppIcon.icns</string>
    <key>CFBundlePackageType</key><string>APPL</string>
    <key>LSMinimumSystemVersion</key><string>10.13</string>
    <key>NSHighResolutionCapable</key><true/>
</dict>
</plist>

Create DMG

创建DMG

bash
undefined
bash
undefined

Create directory structure

创建目录结构

mkdir -p dmg-temp/YourApp.app/Contents/{MacOS,Resources,MacOS/bin}
mkdir -p dmg-temp/YourApp.app/Contents/{MacOS,Resources,MacOS/bin}

Copy binaries

复制二进制文件

cp target/release/your-gui dmg-temp/YourApp.app/Contents/MacOS/ cp target/release/your-cli dmg-temp/YourApp.app/Contents/MacOS/bin/ chmod +x dmg-temp/YourApp.app/Contents/MacOS/*
cp target/release/your-gui dmg-temp/YourApp.app/Contents/MacOS/ cp target/release/your-cli dmg-temp/YourApp.app/Contents/MacOS/bin/ chmod +x dmg-temp/YourApp.app/Contents/MacOS/*

Copy metadata and resources

复制元数据和资源

cp Info.plist dmg-temp/YourApp.app/Contents/ cp AppIcon.icns dmg-temp/YourApp.app/Contents/Resources/
cp Info.plist dmg-temp/YourApp.app/Contents/ cp AppIcon.icns dmg-temp/YourApp.app/Contents/Resources/

Create drag-to-install link

创建拖拽到应用程序文件夹的链接

ln -s /Applications dmg-temp/Applications
ln -s /Applications dmg-temp/Applications

Build DMG (UDZO = compressed)

构建DMG(UDZO = 压缩格式)

hdiutil create -volname "YourApp" -srcfolder dmg-temp -ov -format UDZO YourApp.dmg
hdiutil create -volname "YourApp" -srcfolder dmg-temp -ov -format UDZO YourApp.dmg

Cleanup

清理临时文件

rm -rf dmg-temp

**Result**: Users drag YourApp.app to Applications, instantly getting both GUI and CLI.
rm -rf dmg-temp

**结果**: 用户将YourApp.app拖拽到Applications文件夹,即可同时获得GUI和CLI工具。

Advanced DMG with Custom Appearance

自定义外观的高级DMG

For professional DMGs with custom window layout and background:
bash
undefined
如需带有自定义窗口布局和背景的专业DMG:
bash
undefined

Create temporary read-write DMG

创建临时可读写DMG

hdiutil create -volname "YourApp" -srcfolder dmg-temp -ov -format UDRW temp.dmg
hdiutil create -volname "YourApp" -srcfolder dmg-temp -ov -format UDRW temp.dmg

Mount it

挂载DMG

device=$(hdiutil attach -readwrite -noverify -noautoopen "temp.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}')
device=$(hdiutil attach -readwrite -noverify -noautoopen "temp.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}')

Customize appearance with AppleScript

使用AppleScript自定义外观

echo ' tell application "Finder" tell disk "YourApp" open set current view of container window to icon view set toolbar visible of container window to false set the bounds of container window to {100, 100, 700, 500} set position of item "YourApp.app" of container window to {150, 200} set position of item "Applications" of container window to {450, 200} delay 2 end tell end tell ' | osascript
echo ' tell application "Finder" tell disk "YourApp" open set current view of container window to icon view set toolbar visible of container window to false set the bounds of container window to {100, 100, 700, 500} set position of item "YourApp.app" of container window to {150, 200} set position of item "Applications" of container window to {450, 200} delay 2 end tell end tell ' | osascript

Finalize (with race condition handling)

完成制作(处理竞争条件)

chmod -Rf go-w /Volumes/YourApp sync
chmod -Rf go-w /Volumes/YourApp sync

IMPORTANT: Wait for Finder to release the volume

重要:等待Finder释放卷

sleep 5
sleep 5

Retry loop handles "Resource busy" race condition

重试循环处理“资源忙”竞争条件

for i in {1..5}; do if hdiutil detach ${device} 2>/dev/null; then break fi echo "Detach attempt $i failed, retrying..." sleep 2 done
for i in {1..5}; do if hdiutil detach ${device} 2>/dev/null; then break fi echo "Detach attempt $i failed, retrying..." sleep 2 done

Force detach if still mounted

如果仍挂载则强制卸载

hdiutil detach ${device} -force || true
hdiutil detach ${device} -force || true

Convert to compressed read-only DMG

转换为压缩只读DMG

hdiutil convert temp.dmg -format UDZO -imagekey zlib-level=9 -o YourApp.dmg rm -f temp.dmg

**Why the retry logic?**
- macOS Finder may still be accessing the mounted DMG even after AppleScript completes
- This causes `hdiutil: couldn't eject "disk6" - Resource busy` errors
- The retry loop with delays solves this race condition
- Affects both Intel and Apple Silicon builds in GitHub Actions
- Without this fix, DMG creation randomly fails with exit code 16

**Result**: Users drag YourApp.app to Applications, instantly getting both GUI and CLI.
hdiutil convert temp.dmg -format UDZO -imagekey zlib-level=9 -o YourApp.dmg rm -f temp.dmg

**为何需要重试逻辑?**
- 即使AppleScript执行完成,macOS Finder可能仍在访问挂载的DMG
- 这会导致`hdiutil: couldn't eject "disk6" - Resource busy`错误
- 带延迟的重试循环可解决此竞争条件
- 影响GitHub Actions中的Intel和Apple Silicon构建
- 若无此修复,DMG创建会随机失败,退出码为16

**结果**: 用户将YourApp.app拖拽到Applications文件夹,即可同时获得GUI和CLI工具。

DMG Creation Troubleshooting: "Resource Busy" Errors

DMG创建故障排除:“资源忙”错误

Problem: DMG creation randomly fails in GitHub Actions with:
hdiutil: couldn't eject "disk2" - Resource busy
hdiutil: convert failed - Resource temporarily unavailable
Error: Process completed with exit code 1
Root Cause: Background processes (Spotlight indexing, mdworker, Finder) hold file handles on the mounted DMG volume even after AppleScript completes. This prevents
hdiutil detach
from succeeding, which blocks the final
hdiutil convert
step.
Why It's Intermittent: The race condition timing varies based on:
  • System load during CI execution
  • Spotlight indexing speed
  • Number of files in the DMG
  • macOS runner state
Solution: Multi-strategy unmount with progressive escalation (gentle → force):
bash
undefined
问题: 在GitHub Actions中,DMG创建随机失败,报错:
hdiutil: couldn't eject "disk2" - Resource busy
hdiutil: convert failed - Resource temporarily unavailable
Error: Process completed with exit code 1
根本原因: 后台进程(Spotlight索引、mdworker、Finder)在AppleScript完成后仍持有挂载DMG卷的文件句柄。这会阻止
hdiutil detach
成功执行,进而阻塞最终的
hdiutil convert
步骤。
为何间歇性发生: 竞争条件的时机取决于:
  • CI执行期间的系统负载
  • Spotlight索引速度
  • DMG中的文件数量
  • macOS运行器状态
解决方案: 逐步升级的多策略卸载(温和→强制):
bash
undefined

Create temporary read-write DMG

创建临时可读写DMG

hdiutil create -volname "YourApp" -srcfolder dmg-temp -ov -format UDRW temp.dmg
hdiutil create -volname "YourApp" -srcfolder dmg-temp -ov -format UDRW temp.dmg

Mount it

挂载DMG

device=$(hdiutil attach -readwrite -noverify -noautoopen "temp.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}')
device=$(hdiutil attach -readwrite -noverify -noautoopen "temp.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}')

Customize appearance with AppleScript (your existing code here)

使用AppleScript自定义外观(现有代码)

echo '...' | osascript
echo '...' | osascript

Finalize DMG

完成DMG制作

chmod -Rf go-w /Volumes/YourApp sync
chmod -Rf go-w /Volumes/YourApp sync

ROBUST UNMOUNT: Progressive escalation with verification

可靠卸载:逐步升级并验证

echo "Attempting to unmount ${device}..."
echo "Attempting to unmount ${device}..."

Strategy 1: Try gentle unmount with retries

策略1:尝试温和卸载并重试

for i in {1..3}; do if hdiutil detach ${device} 2>/dev/null; then echo "Successfully detached on attempt $i" break fi echo "Detach attempt $i failed, waiting..." sleep 3 done
for i in {1..3}; do if hdiutil detach ${device} 2>/dev/null; then echo "Successfully detached on attempt $i" break fi echo "Detach attempt $i failed, waiting..." sleep 3 done

Strategy 2: Check if still mounted and use diskutil

策略2:检查是否仍挂载并使用diskutil

if diskutil info ${device} >/dev/null 2>&1; then echo "Device still mounted, using diskutil unmount force..." diskutil unmountDisk force ${device} || true sleep 2 fi
if diskutil info ${device} >/dev/null 2>&1; then echo "Device still mounted, using diskutil unmount force..." diskutil unmountDisk force ${device} || true sleep 2 fi

Strategy 3: Final force detach

策略3:最终强制卸载

if diskutil info ${device} >/dev/null 2>&1; then echo "Device STILL mounted, using hdiutil detach -force..." hdiutil detach ${device} -force || true sleep 3 fi
if diskutil info ${device} >/dev/null 2>&1; then echo "Device STILL mounted, using hdiutil detach -force..." hdiutil detach ${device} -force || true sleep 3 fi

Verify device is unmounted

验证设备是否已卸载

if diskutil info ${device} >/dev/null 2>&1; then echo "WARNING: Device may still be mounted, checking for blocking processes..." lsof | grep ${device} || true fi
if diskutil info ${device} >/dev/null 2>&1; then echo "WARNING: Device may still be mounted, checking for blocking processes..." lsof | grep ${device} || true fi

Extra sync and wait for filesystem

额外同步并等待文件系统

sync sleep 5
sync sleep 5

Remove existing output DMG if it exists

如果存在则删除现有输出DMG

rm -f YourApp.dmg
rm -f YourApp.dmg

Convert to compressed read-only DMG

转换为压缩只读DMG

echo "Converting temp.dmg to final DMG..." hdiutil convert temp.dmg -format UDZO -imagekey zlib-level=9 -o YourApp.dmg
echo "Converting temp.dmg to final DMG..." hdiutil convert temp.dmg -format UDZO -imagekey zlib-level=9 -o YourApp.dmg

Clean up temp files

清理临时文件

rm -f temp.dmg

**Why This Works**:
1. **Progressive escalation**: Starts with gentle unmount, escalates to force only if needed
2. **Verification between steps**: Uses `diskutil info` to check mount status before each strategy
3. **Multiple tools**: Combines `hdiutil detach` and `diskutil unmountDisk` (different unmount mechanisms)
4. **Diagnostic output**: Shows `lsof` output if unmount fails to help debug persistent issues
5. **Multiple sync calls**: Ensures filesystem writes complete before attempting unmount
6. **Longer delays**: Gives background processes time to release file handles

**Implementation Location**: `.github/workflows/release.yml` in the "Create DMG installer (macOS only)" step, lines ~328-377.

**Applies To**: Both Intel (x86_64-apple-darwin) and Apple Silicon (aarch64-apple-darwin) DMG builds.

**Testing**: After implementing this fix, DMG creation should succeed consistently even under system load. Monitor with:
```bash
gh run watch
gh run list --workflow=release.yml --limit 5
Historical Context: This fix addresses random DMG creation failures due to the race condition.
rm -f temp.dmg

**为何此方案有效**:
1. **逐步升级**: 从温和卸载开始,仅在必要时升级到强制卸载
2. **步骤间验证**: 在每个策略前使用`diskutil info`检查挂载状态
3. **多工具结合**: 结合`hdiutil detach`和`diskutil unmountDisk`(不同的卸载机制)
4. **诊断输出**: 如果卸载失败,显示`lsof`输出以帮助调试持续存在的问题
5. **多次同步调用**: 确保文件系统写入完成后再尝试卸载
6. **更长延迟**: 给后台进程足够时间释放文件句柄

**实现位置**: `.github/workflows/release.yml`中的“Create DMG installer (macOS only)”步骤,约第328-377行。

**适用范围**: Intel(x86_64-apple-darwin)和Apple Silicon(aarch64-apple-darwin)DMG构建。

**测试**: 实施此修复后,即使在系统负载下,DMG创建也应持续成功。使用以下命令监控:
```bash
gh run watch
gh run list --workflow=release.yml --limit 5
历史背景: 此修复解决了由竞争条件导致的DMG创建随机失败问题。

Windows Icon Embedding

Windows图标嵌入

What this does: Embeds your application icon directly in the .exe file so it appears in Task Manager, file explorer, and shortcuts. This happens at compile time via build.rs.
功能: 将应用图标直接嵌入.exe文件,使其在任务管理器、文件资源管理器和快捷方式中显示。此操作通过build.rs在编译时完成。

Setup winres

配置winres

toml
undefined
toml
undefined

Cargo.toml - Only needed for Windows builds

Cargo.toml - 仅Windows构建需要

[target.'cfg(windows)'.build-dependencies] winres = "0.1" # Embeds Windows resources (icon, version info)
[package] include = ["src//*", "assets//*", "Cargo.toml", "build.rs"] # Ensure assets included in package
undefined
[target.'cfg(windows)'.build-dependencies] winres = "0.1" # 嵌入Windows资源(图标、版本信息)
[package] include = ["src//*", "assets//*", "Cargo.toml", "build.rs"] # 确保资产包含在包中
undefined

build.rs

build.rs

rust
fn main() {
    #[cfg(windows)]  // Only run on Windows builds
    {
        let mut res = winres::WindowsResource::new();
        res.set_icon("assets/app.ico");  // Path to multi-resolution ICO file
        res.set("ProductName", "YourApp");  // Shown in Task Manager
        res.set("FileDescription", "Description");  // Shown in file properties
        res.set("LegalCopyright", "Copyright (c) 2025");
        res.compile().expect("Failed to compile Windows resources");
    }
}
Tip: The build.rs runs during
cargo build
, embedding the icon into the compiled .exe before the MSI installer packages it.
rust
fn main() {
    #[cfg(windows)]  // 仅在Windows构建时运行
    {
        let mut res = winres::WindowsResource::new();
        res.set_icon("assets/app.ico");  // 多分辨率ICO文件路径
        res.set("ProductName", "YourApp");  // 任务管理器中显示的名称
        res.set("FileDescription", "Description");  // 文件属性中显示的描述
        res.set("LegalCopyright", "Copyright (c) 2025");
        res.compile().expect("Failed to compile Windows resources");
    }
}
提示: build.rs在
cargo build
期间运行,在MSI安装程序打包前将图标嵌入编译后的.exe文件。

Create Multi-Resolution ICO

创建多分辨率ICO

bash
undefined
bash
undefined

ImageMagick converts PNG to ICO with multiple resolutions

ImageMagick将PNG转换为多分辨率ICO

Windows automatically picks the right size for each context

Windows会根据不同场景自动选择合适的尺寸

magick convert app.png
( -clone 0 -resize 256x256 ) \ # Large icons (file explorer) ( -clone 0 -resize 128x128 ) \ # Medium icons ( -clone 0 -resize 64x64 ) \ # Small icons ( -clone 0 -resize 48x48 ) \ # Taskbar ( -clone 0 -resize 32x32 ) \ # Title bar ( -clone 0 -resize 16x16 ) \ # System tray -delete 0 -colors 256 app.ico # Reduce to 256 colors for compatibility

**Why multi-resolution?** Windows picks different icon sizes depending on context (taskbar, file explorer, system tray). Single-size ICOs look pixelated when scaled.
magick convert app.png
( -clone 0 -resize 256x256 ) \ # 大图标(文件资源管理器) ( -clone 0 -resize 128x128 ) \ # 中等图标 ( -clone 0 -resize 64x64 ) \ # 小图标 ( -clone 0 -resize 48x48 ) \ # 任务栏图标 ( -clone 0 -resize 32x32 ) \ # 标题栏图标 ( -clone 0 -resize 16x16 ) \ # 系统托盘图标 -delete 0 -colors 256 app.ico # 减少到256色以兼容

**为何需要多分辨率?** Windows会根据不同场景(任务栏、文件资源管理器、系统托盘)选择不同的图标尺寸。单尺寸ICO缩放后会出现像素化。

Cargo Workspace Assets

Cargo工作区资产

Why this issue occurs:
cargo package
packages each workspace member independently, but
include_bytes!("../../assets/")
tries to reach outside the package directory during build. The packaged .crate file doesn't include parent directory assets.
问题原因:
cargo package
会独立打包每个工作区成员,但
include_bytes!("../../assets/")
在构建时尝试访问包目录外的内容。打包后的.crate文件不包含父目录资产。

Problem: include_bytes!() with Workspace Assets

问题:使用工作区资产的include_bytes!()

rust
// Fails during cargo package because ../../ escapes the package boundary
let logo = include_bytes!("../../assets/logo.png");
Error:
couldn't read src/../../assets/logo.png
rust
// 在cargo package时失败,因为../../超出了包边界
let logo = include_bytes!("../../assets/logo.png");
错误:
couldn't read src/../../assets/logo.png

Solution: Copy to Package (Recommended)

解决方案:复制到包中(推荐)

bash
undefined
bash
undefined

Copy assets into the package that needs them

将资产复制到需要它的包中

cp assets/logo.png yourapp-gui/assets/

```rust
// Update code to use local path (within package)
let logo = include_bytes!("../assets/logo.png");  // ../assets relative to src/
toml
undefined
cp assets/logo.png yourapp-gui/assets/

```rust
// 更新代码以使用本地路径(包内)
let logo = include_bytes!("../assets/logo.png");  // ../assets相对于src/目录
toml
undefined

Cargo.toml - Tell cargo to include assets in the packaged .crate

Cargo.toml - 告知cargo将资产包含在打包的.crate中

include = ["src//*", "assets//*", "Cargo.toml", "build.rs"]

**Why this works**: Each package becomes self-contained with its own assets, allowing `cargo package` and `cargo publish` to succeed.
include = ["src//*", "assets//*", "Cargo.toml", "build.rs"]

**为何有效**: 每个包都成为包含自身资产的独立单元,使`cargo package`和`cargo publish`能够成功执行。

Verify Before Publishing

发布前验证

bash
undefined
bash
undefined

List files that will be included in the package

列出将包含在包中的文件

cargo package -p package-name --list | grep assets
cargo package -p package-name --list | grep assets

Inspect the actual .crate archive

检查实际的.crate归档文件

tar -tzf target/package/package-name-*.crate | grep assets

**Expected output**: Should show `assets/logo.png` in the package contents.
tar -tzf target/package/package-name-*.crate | grep assets

**预期输出**: 应在包内容中显示`assets/logo.png`。

Windows MSI Installer

Windows MSI安装程序

What this creates: Professional Windows installer that adds Start Menu shortcuts, configures PATH, and handles upgrades cleanly.
See also: For bundling external dependencies (FFmpeg, ExifTool, Tesseract, etc.) in your MSI with unified detection/execution code paths, see the
robust-dependency-installation
skill.
创建结果: 专业的Windows安装程序,添加开始菜单快捷方式、配置PATH并干净地处理升级。
另请参阅: 如需在MSI中捆绑外部依赖项(FFmpeg、ExifTool、Tesseract等)并使用统一的检测/执行代码路径,请查看
robust-dependency-installation
技能。

WiX v6 Setup

WiX v6配置

powershell
dotnet tool install --global wix  # Microsoft's official installer framework
powershell
dotnet tool install --global wix  # 微软官方安装程序框架

Key WiX v6 Syntax Changes

WiX v6关键语法变化

v3/v4v6Reason
<Directory Id="ProgramFilesFolder">
<StandardDirectory Id="ProgramFiles64Folder">
Clearer 64-bit intent
<Custom>NOT Installed</Custom>
<Custom Condition="NOT Installed" />
Condition is an attribute now
v3/v4v6原因
<Directory Id="ProgramFilesFolder">
<StandardDirectory Id="ProgramFiles64Folder">
明确64位意图
<Custom>NOT Installed</Custom>
<Custom Condition="NOT Installed" />
条件现在是属性

MSI Template (yourapp.wxs)

MSI模板(yourapp.wxs)

xml
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
  <!-- Package metadata -->
  <Package Name="YourApp" Version="1.0.0" Manufacturer="Your Company"
           UpgradeCode="GUID-HERE" Language="1033">  <!-- UpgradeCode: NEVER CHANGE! Used to detect previous installations -->
    <MajorUpgrade DowngradeErrorMessage="Newer version installed." />  <!-- Auto-uninstall old version -->
    <MediaTemplate EmbeddingCompressionLevel="high" />  <!-- Compress to reduce file size -->

    <!-- What to install -->
    <Feature Id="ProductFeature" Title="YourApp" Level="1">
      <ComponentGroupRef Id="ProductComponents" />
    </Feature>

    <!-- Where to install -->
    <StandardDirectory Id="ProgramFiles6432Folder">  <!-- C:\Program Files\ -->
      <Directory Id="INSTALLFOLDER" Name="YourApp">  <!-- C:\Program Files\YourApp\ -->
        <Component Id="MainExecutable" Bitness="always64">
          <File Id="GUI" Source="target\release\yourapp-gui.exe" KeyPath="yes">
            <Shortcut Id="StartMenu" Directory="ProgramMenuFolder" Name="YourApp" />  <!-- Start Menu shortcut -->
          </File>
          <File Id="CLI" Source="target\release\yourapp.exe" />  <!-- CLI tool -->
        </Component>
        <Component Id="PathEnvironment" Bitness="always64">
          <!-- Add install dir to system PATH so CLI is accessible from any terminal -->
          <Environment Id="PATH" Name="PATH" Value="[INSTALLFOLDER]"
                       Permanent="no" Part="last" Action="set" System="yes" />
        </Component>
      </Directory>
    </StandardDirectory>
  </Package>

  <!-- Component registry -->
  <Fragment>
    <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
      <ComponentRef Id="MainExecutable" />
      <ComponentRef Id="PathEnvironment" />
    </ComponentGroup>
  </Fragment>
</Wix>
Critical: Generate UpgradeCode once and NEVER change it:
uuidgen
or https://www.uuidgenerator.net/
Why UpgradeCode matters: Windows uses it to detect existing installations. Changing it creates a separate product that won't upgrade the old one.
xml
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
  <!-- 包元数据 -->
  <Package Name="YourApp" Version="1.0.0" Manufacturer="Your Company"
           UpgradeCode="GUID-HERE" Language="1033">  <!-- UpgradeCode:切勿更改!用于检测之前的安装 -->
    <MajorUpgrade DowngradeErrorMessage="Newer version installed." />  <!-- 自动卸载旧版本 -->
    <MediaTemplate EmbeddingCompressionLevel="high" />  <!-- 压缩以减小文件大小 -->

    <!-- 要安装的内容 -->
    <Feature Id="ProductFeature" Title="YourApp" Level="1">
      <ComponentGroupRef Id="ProductComponents" />
    </Feature>

    <!-- 安装位置 -->
    <StandardDirectory Id="ProgramFiles6432Folder">  <!-- C:\Program Files\ -->
      <Directory Id="INSTALLFOLDER" Name="YourApp">  <!-- C:\Program Files\YourApp\ -->
        <Component Id="MainExecutable" Bitness="always64">
          <File Id="GUI" Source="target\release\yourapp-gui.exe" KeyPath="yes">
            <Shortcut Id="StartMenu" Directory="ProgramMenuFolder" Name="YourApp" />  <!-- 开始菜单快捷方式 -->
          </File>
          <File Id="CLI" Source="target\release\yourapp.exe" />  <!-- CLI工具 -->
        </Component>
        <Component Id="PathEnvironment" Bitness="always64">
          <!-- 将安装目录添加到系统PATH,使CLI可从任何终端访问 -->
          <Environment Id="PATH" Name="PATH" Value="[INSTALLFOLDER]"
                       Permanent="no" Part="last" Action="set" System="yes" />
        </Component>
      </Directory>
    </StandardDirectory>
  </Package>

  <!-- 组件注册表 -->
  <Fragment>
    <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
      <ComponentRef Id="MainExecutable" />
      <ComponentRef Id="PathEnvironment" />
    </ComponentGroup>
  </Fragment>
</Wix>
关键: 生成一次UpgradeCode后切勿更改:使用
uuidgen
https://www.uuidgenerator.net/
UpgradeCode的重要性: Windows使用它检测现有安装。更改它会创建一个独立的产品,不会升级旧版本。

Build MSI

构建MSI

bash
wix build yourapp.wxs -o YourApp.msi
Version handling: Update
Version="1.0.0"
in .wxs for each release. MajorUpgrade handles uninstalling the old version automatically.
bash
wix build yourapp.wxs -o YourApp.msi
版本处理: 每次发布时更新.wxs中的
Version="1.0.0"
。MajorUpgrade会自动处理旧版本的卸载。

Linux DEB Package

Linux DEB包

Debian Directory

Debian目录结构

debian/
├── control          # Package metadata
├── rules            # Build script
├── changelog        # Version history
├── copyright        # License
├── compat           # Debian level (13)
├── yourapp.desktop  # Desktop entry
└── source/format    # "3.0 (quilt)"
debian/
├── control          # 包元数据
├── rules            # 构建脚本
├── changelog        # 版本历史
├── copyright        # 许可证
├── compat           # Debian兼容级别(13)
├── yourapp.desktop  # 桌面入口
└── source/format    # "3.0 (quilt)"

⚠️ Common Mistake: Placeholder Emails

⚠️ 常见错误:占位符邮箱

Replace
your@email.com
and
maintainer@example.com
in ALL files:
  • debian/control
    - Maintainer
  • debian/copyright
    - Upstream-Contact
  • debian/changelog
    - All entries
bash
undefined
替换所有文件中的
your@email.com
maintainer@example.com
:
  • debian/control
    - 维护者
  • debian/copyright
    - 上游联系人
  • debian/changelog
    - 所有条目
bash
undefined

Find placeholders

查找占位符

grep -r "example.com" debian/
grep -r "example.com" debian/

Replace (adjust pattern)

替换(调整匹配模式)

sed -i 's/your@email.com/youractual@email.com/g' debian/{control,copyright,changelog}

**Best practice**: Match email in `Cargo.toml` authors.
sed -i 's/your@email.com/youractual@email.com/g' debian/{control,copyright,changelog}

**最佳实践**: 与`Cargo.toml`作者中的邮箱保持一致。

debian/control

debian/control

Source: yourapp
Section: utils
Priority: optional
Maintainer: Your Name <your@email.com>
Build-Depends: debhelper-compat (= 13), cargo, rustc, pkg-config, libleptonica-dev, libtesseract-dev
Standards-Version: 4.6.0
Homepage: https://github.com/yourname/yourapp

Package: yourapp
Architecture: amd64
Depends: ${shlibs:Depends}, ${misc:Depends}, libimage-exiftool-perl, tesseract-ocr, ffmpeg
Description: Short description
 Long description.
Source: yourapp
Section: utils
Priority: optional
Maintainer: Your Name <your@email.com>
Build-Depends: debhelper-compat (= 13), cargo, rustc, pkg-config, libleptonica-dev, libtesseract-dev
Standards-Version: 4.6.0
Homepage: https://github.com/yourname/yourapp

Package: yourapp
Architecture: amd64
Depends: ${shlibs:Depends}, ${misc:Depends}, libimage-exiftool-perl, tesseract-ocr, ffmpeg
Description: Short description
 Long description.

debian/rules

debian/rules

makefile
#!/usr/bin/make -f
export DEB_BUILD_MAINT_OPTIONS = hardening=+all  # Enable security hardening (ASLR, stack protection, etc.)

%:
	dh $@  # Use debhelper to handle most packaging tasks

override_dh_auto_build:
	cargo build --release --workspace  # Build all workspace members

override_dh_auto_install:
	# Install binaries to /usr/bin with executable permissions
	install -D -m 755 target/release/yourapp-gui debian/yourapp/usr/bin/yourapp-gui
	install -D -m 755 target/release/yourapp debian/yourapp/usr/bin/yourapp

	# Install desktop entry for application menu integration
	install -D -m 644 debian/yourapp.desktop debian/yourapp/usr/share/applications/yourapp.desktop

	# Install icon to BOTH locations for maximum compatibility:
	# - /usr/share/pixmaps/ (legacy, used by older desktop environments)
	# - /usr/share/icons/hicolor/512x512/apps/ (freedesktop standard, preferred)
	install -D -m 644 yourapp-gui/assets/yourapp.png debian/yourapp/usr/share/pixmaps/yourapp.png
	install -D -m 644 yourapp-gui/assets/yourapp.png debian/yourapp/usr/share/icons/hicolor/512x512/apps/yourapp.png
Icon paths: Use package directory (
yourapp-gui/assets/
), not workspace root.
makefile
#!/usr/bin/make -f
export DEB_BUILD_MAINT_OPTIONS = hardening=+all  # 启用安全加固(ASLR、栈保护等)

%:
	dh $@  # 使用debhelper处理大多数打包任务

override_dh_auto_build:
	cargo build --release --workspace  # 构建所有工作区成员

override_dh_auto_install:
	# 将二进制文件安装到/usr/bin并设置可执行权限
	install -D -m 755 target/release/yourapp-gui debian/yourapp/usr/bin/yourapp-gui
	install -D -m 755 target/release/yourapp debian/yourapp/usr/bin/yourapp

	# 安装桌面入口以集成到应用程序菜单
	install -D -m 644 debian/yourapp.desktop debian/yourapp/usr/share/applications/yourapp.desktop

	# 将图标安装到两个位置以获得最大兼容性:
	# - /usr/share/pixmaps/(旧版,用于较旧的桌面环境)
	# - /usr/share/icons/hicolor/512x512/apps/(freedesktop标准,推荐)
	install -D -m 644 yourapp-gui/assets/yourapp.png debian/yourapp/usr/share/pixmaps/yourapp.png
	install -D -m 644 yourapp-gui/assets/yourapp.png debian/yourapp/usr/share/icons/hicolor/512x512/apps/yourapp.png
图标路径: 使用包目录(
yourapp-gui/assets/
),而非工作区根目录。

debian/yourapp.desktop

debian/yourapp.desktop

ini
[Desktop Entry]
Type=Application
Name=YourApp
Comment=Description
Exec=/usr/bin/yourapp-gui
Icon=yourapp
Terminal=false
Categories=Utility;
ini
[Desktop Entry]
Type=Application
Name=YourApp
Comment=Description
Exec=/usr/bin/yourapp-gui
Icon=yourapp
Terminal=false
Categories=Utility;

debian/changelog

debian/changelog

yourapp (1.0.0-1) unstable; urgency=medium

  * Initial release

 -- Your Name <your@email.com>  Mon, 01 Jan 2024 12:00:00 +0000
yourapp (1.0.0-1) unstable; urgency=medium

  * Initial release

 -- Your Name <your@email.com>  Mon, 01 Jan 2024 12:00:00 +0000

Linux Build Dependencies (Rust + Tesseract/Leptonica)

Linux构建依赖(Rust + Tesseract/Leptonica)

bash
sudo apt-get install -y pkg-config libleptonica-dev libtesseract-dev libclang-dev clang
PKG_CONFIG_PATH Fix (Ubuntu/Debian multiarch):
bash
export PKG_CONFIG_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
cargo build --release
GitHub Actions:
yaml
- run: sudo apt-get install -y pkg-config libleptonica-dev libtesseract-dev libclang-dev clang
- run: |
    export PKG_CONFIG_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
    cargo build --release
bash
sudo apt-get install -y pkg-config libleptonica-dev libtesseract-dev libclang-dev clang
PKG_CONFIG_PATH修复(Ubuntu/Debian多架构):
bash
export PKG_CONFIG_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
cargo build --release
GitHub Actions:
yaml
- run: sudo apt-get install -y pkg-config libleptonica-dev libtesseract-dev libclang-dev clang
- run: |
    export PKG_CONFIG_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
    cargo build --release

Build DEB

构建DEB

bash
dpkg-buildpackage -us -uc -b
lintian ../yourapp_1.0.0-1_amd64.deb
bash
dpkg-buildpackage -us -uc -b
lintian ../yourapp_1.0.0-1_amd64.deb

GitHub Actions: Automated Release

GitHub Actions:自动化发布

What this does: Automatically builds installers for all platforms when you push a git tag, creates a GitHub release, and generates checksums + attestations.
功能: 当你推送git标签时,自动为所有平台构建安装程序,创建GitHub发布,并生成校验和+证明。

Auto-Trigger on Tag Push

标签推送自动触发

yaml
name: Release
on:
  push:
    tags: ['v*.*.*']  # Triggers when you push v1.0.0, v0.5.1, etc.
  workflow_dispatch:  # Manual trigger fallback if needed
    inputs:
      tag: {required: true, type: string}

jobs:
  create-release:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get_version.outputs.version }}  # Share version with build jobs
    steps:
      - uses: actions/checkout@v4
      - id: get_version
        run: |
          # Extract version from tag (v1.0.0 → 1.0.0)
          if [ "${{ github.event_name }}" = "push" ]; then
            echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
          else
            echo "version=${{ inputs.tag }}" >> $GITHUB_OUTPUT
          fi
      - run: gh release create ${{ steps.get_version.outputs.version }} --draft  # Create draft release
        env: {GH_TOKEN: ${{ github.token }}}

  build:
    needs: create-release  # Wait for release to be created
    strategy:
      matrix:  # Build all platforms in parallel
        include:
          - {os: macos-latest, target: x86_64-apple-darwin, archive: dmg}    # macOS Intel
          - {os: macos-latest, target: aarch64-apple-darwin, archive: dmg}   # macOS Apple Silicon
          - {os: windows-latest, target: x86_64-pc-windows-msvc, archive: msi}  # Windows
          - {os: ubuntu-latest, target: x86_64-unknown-linux-gnu, archive: deb}  # Linux
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with: {targets: ${{ matrix.target }}}

      # Platform-specific builds happen here (DMG, MSI, DEB creation)

      # Generate checksums with unique filenames to avoid artifact collisions
      - run: sha256sum * | tee checksums-${{ matrix.target }}.txt
      - uses: actions/upload-artifact@v4
        with:
          name: yourapp-${{ matrix.target }}  # Unique artifact name per platform
          path: |
            *.dmg
            *.msi
            ../*.deb
            checksums-*.txt
Tip: Use
matrix.target
in artifact names to prevent collisions when uploading from parallel jobs.
yaml
name: Release
on:
  push:
    tags: ['v*.*.*']  # 推送v1.0.0、v0.5.1等标签时触发
  workflow_dispatch:  # 手动触发回退(如有需要)
    inputs:
      tag: {required: true, type: string}

jobs:
  create-release:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get_version.outputs.version }}  # 与构建作业共享版本号
    steps:
      - uses: actions/checkout@v4
      - id: get_version
        run: |
          # 从标签中提取版本号(v1.0.0 → 1.0.0)
          if [ "${{ github.event_name }}" = "push" ]; then
            echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
          else
            echo "version=${{ inputs.tag }}" >> $GITHUB_OUTPUT
          fi
      - run: gh release create ${{ steps.get_version.outputs.version }} --draft  # 创建草稿发布
        env: {GH_TOKEN: ${{ github.token }}}

  build:
    needs: create-release  # 等待发布创建完成
    strategy:
      matrix:  # 并行构建所有平台
        include:
          - {os: macos-latest, target: x86_64-apple-darwin, archive: dmg}    # macOS Intel
          - {os: macos-latest, target: aarch64-apple-darwin, archive: dmg}   # macOS Apple Silicon
          - {os: windows-latest, target: x86_64-pc-windows-msvc, archive: msi}  # Windows
          - {os: ubuntu-latest, target: x86_64-unknown-linux-gnu, archive: deb}  # Linux
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with: {targets: ${{ matrix.target }}}

      # 平台特定构建在此处进行(DMG、MSI、DEB创建)

      # 生成带有唯一文件名的校验和以避免工件冲突
      - run: sha256sum * | tee checksums-${{ matrix.target }}.txt
      - uses: actions/upload-artifact@v4
        with:
          name: yourapp-${{ matrix.target }}  # 每个平台使用唯一的工件名称
          path: |
            *.dmg
            *.msi
            ../*.deb
            checksums-*.txt
提示: 在工件名称中使用
matrix.target
以避免并行作业上传时发生冲突。

SLSA Attestations

SLSA证明

yaml
undefined
yaml
undefined

Add supply chain security attestations to prove build provenance

添加供应链安全证明以验证构建来源

  • uses: actions/attest-build-provenance@v1 with: {subject-path: 'yourapp.${{ matrix.archive }}'}

**What this adds**: Cryptographic proof that the artifact was built by this repo's workflow. Users can verify authenticity:
```bash
gh attestation verify yourapp.dmg --owner yourname
Shows: Exact commit SHA, workflow run, and timestamp that built the artifact.
  • uses: actions/attest-build-provenance@v1 with: {subject-path: 'yourapp.${{ matrix.archive }}'}

**添加的内容**: 加密证明工件是由本仓库的工作流构建的。用户可以验证真实性:
```bash
gh attestation verify yourapp.dmg --owner yourname
显示内容: 构建工件的精确提交SHA、工作流运行和时间戳。

Homebrew Auto-Update

Homebrew自动更新

Cross-Repo Trigger (repository_dispatch)

跨仓库触发(repository_dispatch)

Step 1: Target workflow (homebrew-yourapp)
yaml
on:
  workflow_dispatch:
    inputs: {version: {required: true, type: string}}
  repository_dispatch:
    types: [update-formulae]

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - id: get-version
        run: |
          if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
            echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT
          else
            echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
          fi
      - uses: actions/checkout@v4
      - run: gh release download "v${{ steps.get-version.outputs.version }}" --repo yourname/yourapp --pattern "checksums.txt"
        env: {GH_TOKEN: ${{ github.token }}}
      - id: checksums
        run: |
          echo "arm64_sha=$(grep aarch64-apple-darwin checksums.txt | awk '{print $1}')" >> $GITHUB_OUTPUT
          echo "x86_64_sha=$(grep x86_64-apple-darwin checksums.txt | awk '{print $1}')" >> $GITHUB_OUTPUT
      - run: |
          sed -i "s/version \".*\"/version \"${{ steps.get-version.outputs.version }}\"/" Formula/yourapp.rb
          # Update URLs and SHAs...
      - run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/ Casks/
          git commit -m "chore: update to v${{ steps.get-version.outputs.version }}"
          git push
Step 2: Send dispatch from main repo
yaml
update-homebrew:
  needs: [create-release, build, checksums]
  runs-on: ubuntu-latest
  if: ${{ !contains(needs.create-release.outputs.version, '-') }}  # Skip pre-releases
  steps:
    - run: |
        VERSION=${{ needs.create-release.outputs.version }}
        curl -L -X POST \
          -H "Authorization: Bearer ${{ secrets.HOMEBREW_DISPATCH_TOKEN }}" \
          https://api.github.com/repos/yourname/homebrew-yourapp/dispatches \
          -d "{\"event_type\":\"update-formulae\",\"client_payload\":{\"version\":\"${VERSION#v}\"}}"
⚠️ Important: Use Personal Access Token (
HOMEBREW_DISPATCH_TOKEN
) with
repo
scope, not
GITHUB_TOKEN
(can't trigger cross-repo workflows).
步骤1: 目标工作流(homebrew-yourapp)
yaml
on:
  workflow_dispatch:
    inputs: {version: {required: true, type: string}}
  repository_dispatch:
    types: [update-formulae]

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - id: get-version
        run: |
          if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
            echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT
          else
            echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
          fi
      - uses: actions/checkout@v4
      - run: gh release download "v${{ steps.get-version.outputs.version }}" --repo yourname/yourapp --pattern "checksums.txt"
        env: {GH_TOKEN: ${{ github.token }}}
      - id: checksums
        run: |
          echo "arm64_sha=$(grep aarch64-apple-darwin checksums.txt | awk '{print $1}')" >> $GITHUB_OUTPUT
          echo "x86_64_sha=$(grep x86_64-apple-darwin checksums.txt | awk '{print $1}')" >> $GITHUB_OUTPUT
      - run: |
          sed -i "s/version \".*\"/version \"${{ steps.get-version.outputs.version }}\"/" Formula/yourapp.rb
          # 更新URL和SHA...
      - run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/ Casks/
          git commit -m "chore: update to v${{ steps.get-version.outputs.version }}"
          git push
步骤2: 从主仓库发送触发请求
yaml
update-homebrew:
  needs: [create-release, build, checksums]
  runs-on: ubuntu-latest
  if: ${{ !contains(needs.create-release.outputs.version, '-') }}  # 跳过预发布版本
  steps:
    - run: |
        VERSION=${{ needs.create-release.outputs.version }}
        curl -L -X POST \
          -H "Authorization: Bearer ${{ secrets.HOMEBREW_DISPATCH_TOKEN }}" \
          https://api.github.com/repos/yourname/homebrew-yourapp/dispatches \
          -d "{\"event_type\":\"update-formulae\",\"client_payload\":{\"version\":\"${VERSION#v}\"}}"
⚠️ 重要: 使用具有
repo
权限的个人访问令牌(
HOMEBREW_DISPATCH_TOKEN
),而非
GITHUB_TOKEN
(无法触发跨仓库工作流)。

Complete Automation

完整自动化

Single command release:
bash
cargo release patch --execute
Automation chain:
  1. cargo-release → version bump, crates.io publish, tag push
  2. Tag push → GitHub Actions (release creation, platform builds)
  3. Builds complete → Checksums, SLSA attestations uploaded
  4. repository_dispatch → Homebrew formula update
单命令发布:
bash
cargo release patch --execute
自动化链:
  1. cargo-release → 版本号更新、crates.io发布、标签推送
  2. 标签推送 → GitHub Actions(发布创建、平台构建)
  3. 构建完成 → 上传校验和、SLSA证明
  4. repository_dispatch → Homebrew公式更新

⚠️ cargo-release Tag Naming (Universal Limitation)

⚠️ cargo-release标签命名(通用限制)

Issue: cargo-release tags workspace members as
package-v0.6.14
(not
v0.6.14
), which doesn't match standard
v*.*.*
GitHub Actions triggers.
This is intentional cargo-release behavior - it's designed to handle monorepos with independently versioned packages. For single-version workspaces, you must manually create the simple tag.
Symptom: After
cargo release patch --execute
, workflow doesn't trigger even though tags were pushed.
Verification:
bash
git tag --sort=-version:refname | head -5
问题: cargo-release会将工作区成员标记为
package-v0.6.14
(而非
v0.6.14
),这与标准的
v*.*.*
GitHub Actions触发模式不匹配。
这是cargo-release的有意行为 - 它设计用于处理具有独立版本化包的单体仓库。对于单版本工作区,你必须手动创建简单标签。
症状: 执行
cargo release patch --execute
后,即使推送了标签,工作流也不会触发。
验证:
bash
git tag --sort=-version:refname | head -5

Shows: yourapp-v0.6.14, yourapp-gui-v0.6.14 (wrong)

显示: yourapp-v0.6.14, yourapp-gui-v0.6.14(错误)

Should show: v0.6.14 (correct)

应显示: v0.6.14(正确)


**Workaround**: Manually create and push the simple `vX.Y.Z` tag:
```bash

**解决方法**: 手动创建并推送简单的`vX.Y.Z`标签:
```bash

Create simple tag (disable GPG signing if needed)

创建简单标签(如需禁用GPG签名)

GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0='tag.gpgSign' GIT_CONFIG_VALUE_0='false'
git tag -a v0.6.14 -m "Release v0.6.14 - Description here"
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0='tag.gpgSign' GIT_CONFIG_VALUE_0='false'
git tag -a v0.6.14 -m "Release v0.6.14 - Description here"

Push the tag

推送标签

git push origin v0.6.14 --no-verify
git push origin v0.6.14 --no-verify

Verify workflow triggered

验证工作流是否触发

gh run list --workflow=release.yml --limit 1

**Root cause**: cargo-release uses package names in tags for workspace members. The GitHub Actions workflow triggers on `v*.*.*` pattern which doesn't match `package-v*.*.*`.

**Alternative**: Configure `release.toml` to customize tag format (not yet tested).

**Checklist**:
- [ ] `on: push: tags: ['v*.*.*']` in release workflow
- [ ] Version extraction handles both `push` and `workflow_dispatch`
- [ ] Homebrew workflow listens for `repository_dispatch`
- [ ] Main workflow sends dispatch after successful build
- [ ] `release.toml` configured
- [ ] **After cargo-release, manually create `v*.*.*` tag**

**Monitor**:
```bash
gh run watch
gh run list --limit 5
gh run list --workflow=release.yml --limit 1

**根本原因**: cargo-release会为工作区成员的标签添加包名。GitHub Actions工作流触发模式为`v*.*.*`,与`package-v*.*.*`不匹配。

**替代方案**: 配置`release.toml`自定义标签格式(尚未测试)。

**检查清单**:
- [ ] 发布工作流中包含`on: push: tags: ['v*.*.*']`
- [ ] 版本提取处理`push`和`workflow_dispatch`两种情况
- [ ] Homebrew工作流监听`repository_dispatch`
- [ ] 主工作流在构建成功后发送触发请求
- [ ] 已配置`release.toml`
- [ ] **执行cargo-release后,手动创建`v*.*.*`标签**

**监控**:
```bash
gh run watch
gh run list --limit 5