build-cross-platform-packages
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBuild 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: or native macOS:
convert icon.jpg -resize 512x512 icon.pngsips -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: (referenced in Info.plist without extension)
docs/AppIcon.png - Tip: macOS automatically handles Retina @2x icons when using .icns format
- 尺寸: 最小512x512 PNG,推荐使用透明背景
- 生成: 使用Ideogram (https://ideogram.ai) 或AI生成工具获取专业质量图标
- 转换: 或原生macOS命令:
convert icon.jpg -resize 512x512 icon.pngsips -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 - 放置位置: (在Info.plist中引用时无需扩展名)
docs/AppIcon.png - 提示: 使用.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 iconWhy 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
undefinedbash
undefinedCreate 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
undefinedCreate 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 1Root Cause: Background processes (Spotlight indexing, mdworker, Finder) hold file handles on the mounted DMG volume even after AppleScript completes. This prevents from succeeding, which blocks the final step.
hdiutil detachhdiutil convertWhy 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 detachhdiutil convert为何间歇性发生: 竞争条件的时机取决于:
- CI执行期间的系统负载
- Spotlight索引速度
- DMG中的文件数量
- macOS运行器状态
解决方案: 逐步升级的多策略卸载(温和→强制):
bash
undefinedCreate 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 5Historical 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
undefinedtoml
undefinedCargo.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"] # 确保资产包含在包中
undefinedbuild.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 , embedding the icon into the compiled .exe before the MSI installer packages it.
cargo buildrust
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在期间运行,在MSI安装程序打包前将图标嵌入编译后的.exe文件。
cargo buildCreate Multi-Resolution ICO
创建多分辨率ICO
bash
undefinedbash
undefinedImageMagick 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
( -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色以兼容
( -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: packages each workspace member independently, but tries to reach outside the package directory during build. The packaged .crate file doesn't include parent directory assets.
cargo packageinclude_bytes!("../../assets/")问题原因: 会独立打包每个工作区成员,但在构建时尝试访问包目录外的内容。打包后的.crate文件不包含父目录资产。
cargo packageinclude_bytes!("../../assets/")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.pngrust
// 在cargo package时失败,因为../../超出了包边界
let logo = include_bytes!("../../assets/logo.png");错误:
couldn't read src/../../assets/logo.pngSolution: Copy to Package (Recommended)
解决方案:复制到包中(推荐)
bash
undefinedbash
undefinedCopy 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
undefinedcp assets/logo.png yourapp-gui/assets/
```rust
// 更新代码以使用本地路径(包内)
let logo = include_bytes!("../assets/logo.png"); // ../assets相对于src/目录toml
undefinedCargo.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
undefinedbash
undefinedList 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 theskill.robust-dependency-installation
创建结果: 专业的Windows安装程序,添加开始菜单快捷方式、配置PATH并干净地处理升级。
另请参阅: 如需在MSI中捆绑外部依赖项(FFmpeg、ExifTool、Tesseract等)并使用统一的检测/执行代码路径,请查看技能。robust-dependency-installation
WiX v6 Setup
WiX v6配置
powershell
dotnet tool install --global wix # Microsoft's official installer frameworkpowershell
dotnet tool install --global wix # 微软官方安装程序框架Key WiX v6 Syntax Changes
WiX v6关键语法变化
| v3/v4 | v6 | Reason |
|---|---|---|
| | Clearer 64-bit intent |
| | Condition is an attribute now |
| v3/v4 | v6 | 原因 |
|---|---|---|
| | 明确64位意图 |
| | 条件现在是属性 |
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: or https://www.uuidgenerator.net/
uuidgenWhy 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后切勿更改:使用或https://www.uuidgenerator.net/
uuidgenUpgradeCode的重要性: Windows使用它检测现有安装。更改它会创建一个独立的产品,不会升级旧版本。
Build MSI
构建MSI
bash
wix build yourapp.wxs -o YourApp.msiVersion handling: Update in .wxs for each release. MajorUpgrade handles uninstalling the old version automatically.
Version="1.0.0"bash
wix build yourapp.wxs -o YourApp.msi版本处理: 每次发布时更新.wxs中的。MajorUpgrade会自动处理旧版本的卸载。
Version="1.0.0"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 and in ALL files:
your@email.commaintainer@example.com- - Maintainer
debian/control - - Upstream-Contact
debian/copyright - - All entries
debian/changelog
bash
undefined替换所有文件中的和:
your@email.commaintainer@example.com- - 维护者
debian/control - - 上游联系人
debian/copyright - - 所有条目
debian/changelog
bash
undefinedFind 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.pngIcon paths: Use package directory (), not workspace root.
yourapp-gui/assets/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 +0000yourapp (1.0.0-1) unstable; urgency=medium
* Initial release
-- Your Name <your@email.com> Mon, 01 Jan 2024 12:00:00 +0000Linux Build Dependencies (Rust + Tesseract/Leptonica)
Linux构建依赖(Rust + Tesseract/Leptonica)
bash
sudo apt-get install -y pkg-config libleptonica-dev libtesseract-dev libclang-dev clangPKG_CONFIG_PATH Fix (Ubuntu/Debian multiarch):
bash
export PKG_CONFIG_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
cargo build --releaseGitHub 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 --releasebash
sudo apt-get install -y pkg-config libleptonica-dev libtesseract-dev libclang-dev clangPKG_CONFIG_PATH修复(Ubuntu/Debian多架构):
bash
export PKG_CONFIG_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH"
cargo build --releaseGitHub 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 --releaseBuild DEB
构建DEB
bash
dpkg-buildpackage -us -uc -b
lintian ../yourapp_1.0.0-1_amd64.debbash
dpkg-buildpackage -us -uc -b
lintian ../yourapp_1.0.0-1_amd64.debGitHub 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-*.txtTip: Use in artifact names to prevent collisions when uploading from parallel jobs.
matrix.targetyaml
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.targetSLSA Attestations
SLSA证明
yaml
undefinedyaml
undefinedAdd 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 yournameShows: 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 pushStep 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 () with scope, not (can't trigger cross-repo workflows).
HOMEBREW_DISPATCH_TOKENrepoGITHUB_TOKEN步骤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}\"}}"⚠️ 重要: 使用具有权限的个人访问令牌(),而非(无法触发跨仓库工作流)。
repoHOMEBREW_DISPATCH_TOKENGITHUB_TOKENComplete Automation
完整自动化
Single command release:
bash
cargo release patch --executeAutomation chain:
- cargo-release → version bump, crates.io publish, tag push
- Tag push → GitHub Actions (release creation, platform builds)
- Builds complete → Checksums, SLSA attestations uploaded
- repository_dispatch → Homebrew formula update
单命令发布:
bash
cargo release patch --execute自动化链:
- cargo-release → 版本号更新、crates.io发布、标签推送
- 标签推送 → GitHub Actions(发布创建、平台构建)
- 构建完成 → 上传校验和、SLSA证明
- repository_dispatch → Homebrew公式更新
⚠️ cargo-release Tag Naming (Universal Limitation)
⚠️ cargo-release标签命名(通用限制)
Issue: cargo-release tags workspace members as (not ), which doesn't match standard GitHub Actions triggers.
package-v0.6.14v0.6.14v*.*.*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 , workflow doesn't trigger even though tags were pushed.
cargo release patch --executeVerification:
bash
git tag --sort=-version:refname | head -5问题: cargo-release会将工作区成员标记为(而非),这与标准的GitHub Actions触发模式不匹配。
package-v0.6.14v0.6.14v*.*.*这是cargo-release的有意行为 - 它设计用于处理具有独立版本化包的单体仓库。对于单版本工作区,你必须手动创建简单标签。
症状: 执行后,即使推送了标签,工作流也不会触发。
cargo release patch --execute验证:
bash
git tag --sort=-version:refname | head -5Shows: 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`标签:
```bashCreate 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 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"
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 5gh 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