A GitHub Action to automatically bundle Rust binaries for distribution on macOS, Windows, and Linux.
- 🪟 Windows: Creates
.ziparchives with icons - 🐧 Linux: Creates
.tar.gzarchives with.desktopfiles - 🍎 macOS: Creates
.appbundles with proper structure and icons - 🎨 Metadata & Icons: Automatically extracts metadata using
cargo metadataand embeds icons - 🔐 Code Signing: Supports macOS code signing
- 📦 Flexible: Include additional files and folders in your bundles
- 🎯 Simple: Easy integration with your existing workflows
With auto-discovery, you can use the action with minimal configuration:
- name: Bundle Application
uses: julcst/rust-bundler@v1The action will automatically:
- Discover
Cargo.tomlin the working directory to extract package metadata usingcargo metadata - Use the package name as the binary name
- Auto-discover the binary path from cargo metadata
- Look for
icon.pngin common locations (optional, works without it) - Detect the platform and create the appropriate bundle
Note: Metadata and icons are always optional. The action works fine without them, creating minimal bundles containing just the binary and any included files.
Point to your project root and let auto-discovery do the rest:
- name: Bundle Application
uses: julcst/rust-bundler@v1
with:
working-directory: examples/my-app # Your project root
include-files: 'assets/ README.md LICENSE'The binary will be auto-discovered using cargo metadata to determine the target directory and profile.
If auto-discovery doesn't work or you don't have Cargo.toml:
- name: Bundle Application
uses: julcst/rust-bundler@v1
with:
binary-name: your-app-nameYou can also specify everything explicitly:
- name: Bundle Application
uses: julcst/rust-bundler@v1
with:
binary-name: your-app-name
icon-path: 'icon.png'
cargo-toml-path: 'Cargo.toml'Build and bundle with a custom profile or target triple:
- name: Build with custom profile
run: cargo build --profile production # Requires [profile.production] in Cargo.toml
- name: Bundle Application
uses: julcst/rust-bundler@v1
with:
profile: productionNote: The bundler supports Cargo's historical profile naming where dev and test profiles use the debug directory, and release and bench profiles use the release directory. Custom profiles use their own directory name.
Or with a specific target triple for cross-compilation:
- name: Build for specific target
run: cargo build --release --target aarch64-unknown-linux-gnu
- name: Bundle Application
uses: julcst/rust-bundler@v1
with:
target: aarch64-unknown-linux-gnuThe action automatically detects the platform and creates the appropriate bundle in the dist/ directory with embedded metadata and icons.
name: Release
on:
push:
tags:
- 'v*'
env:
CARGO_TERM_COLOR: always
SCCACHE_GHA_ENABLED: true
RUSTC_WRAPPER: sccache
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup sccache (optional)
uses: mozilla-actions/sccache-action@v0.0.9
continue-on-error: true
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --release
- name: Bundle
uses: julcst/rust-bundler@v1
with:
binary-name: myapp
icon-path: 'icon.png'
cargo-toml-path: 'Cargo.toml'
- name: Upload Bundle
uses: actions/upload-artifact@v4
with:
name: myapp-${{ matrix.os }}
path: dist/*name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: macos-latest
target: x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Setup sccache (optional)
uses: mozilla-actions/sccache-action@v0.0.9
continue-on-error: true
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --release --target ${{ matrix.target }}
env:
RUSTC_WRAPPER: sccache
- name: Bundle
uses: julcst/rust-bundler@v1
with:
binary-name: myapp
icon-path: 'icon.png'
cargo-toml-path: 'Cargo.toml'
include-files: 'README.md LICENSE'
target: ${{ matrix.target }}
- name: Upload to Release
uses: softprops/action-gh-release@v2
with:
files: dist/*For signed macOS releases, add certificate import before bundling:
- name: Import Code Signing Certificate
if: runner.os == 'macOS'
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: Bundle and Sign
if: runner.os == 'macOS'
uses: julcst/rust-bundler@v1
with:
binary-name: myapp
macos-sign: true
macos-sign-identity: "Developer ID Application: Your Name (TEAM_ID)"
macos-bundle-id: com.example.myapp
icon-path: 'icon.png'
cargo-toml-path: 'Cargo.toml'| Input | Description | Required | Default |
|---|---|---|---|
binary-name |
Name of the binary to bundle (without extension). Auto-discovered from Cargo.toml if not provided. | No | Auto-discovered |
include-files |
Space-separated list of files or folders to include | No | "" |
output-name |
Name of the output bundle (without extension) | No | Same as binary-name |
working-directory |
Working directory (project root). Binary auto-discovered using cargo metadata. | No | . |
icon-path |
Path to icon file (PNG format, will be converted per platform). Auto-discovered if not provided. | No | Auto-discovered |
cargo-toml-path |
Path to Cargo.toml for extracting metadata. Auto-discovered if not provided. | No | Auto-discovered |
profile |
Build profile to use (e.g., release, debug, dev, or custom profile name). Supports historical naming: dev/test map to debug, release/bench map to release. | No | release |
target |
Target triple for cross-compilation (e.g., x86_64-unknown-linux-gnu). Optional. | No | "" |
macos-sign |
Enable macOS code signing | No | false |
macos-sign-identity |
macOS code signing identity | No | "" |
macos-bundle-id |
macOS bundle identifier | No | com.example.{binary-name} |
macos-app-name |
macOS application display name | No | Same as binary-name |
| Output | Description |
|---|---|
bundle-path |
Path to the created bundle |
bundle-name |
Name of the created bundle file |
myapp-windows.zip
├── myapp.exe # Binary (with embedded metadata/icon if build.rs used)
├── README.md # Additional files (if included)
└── LICENSE
myapp-linux.tar.gz
├── myapp
├── myapp.png # Icon (if icon-path provided)
├── myapp.desktop # Desktop entry (if cargo metadata available)
├── README.md # Additional files (if included)
└── LICENSE
myapp-macos.app/
└── Contents/
├── Info.plist # With metadata from cargo metadata
├── MacOS/
│ ├── myapp
│ ├── README.md # Additional files (if included)
│ └── LICENSE
└── Resources/
└── AppIcon.icns # Converted icon (if icon-path provided)
The bundler can automatically extract metadata using cargo metadata command and embed icons into your application bundles. Both metadata and icons are completely optional - the action works fine without them, creating minimal bundles.
The action automatically discovers common files when available:
Cargo.toml: Only searched in the working directory root:
Cargo.toml(in working directory)
Icon file (optional): Searched in the following locations:
icon.png(current directory)./icon.pngassets/icon.pngresources/icon.png../icon.png../assets/icon.png
Binary location: Automatically determined from cargo metadata output and build parameters:
- Uses the
target_directoryfield from cargo metadata - Uses the
profileinput (defaults torelease) to determine the profile directory- Historical profile names are handled:
devandtestmap todebugdirectory,releaseandbenchmap toreleasedirectory - Custom profiles use their own directory name
- Historical profile names are handled:
- Optionally uses the
targetinput for cross-compilation (e.g.,x86_64-unknown-linux-gnu) - Search path pattern:
{target_directory}/{target}/{profile_directory}/(or{target_directory}/{profile_directory}/when no target specified) - Falls back to manual search if cargo metadata is unavailable
Binary name: Automatically extracted from the binary target in Cargo.toml using cargo metadata.
If these files are not found, the action continues normally and creates a minimal bundle.
When Cargo.toml is found in the working directory, the bundler uses cargo metadata to extract:
- Package Name: Used for display name and binary name (if not provided)
- Version: Embedded in platform-specific metadata
- Description: Used in
.desktopfiles (Linux) andInfo.plist(macOS) - Authors: Used in metadata where applicable
- Target Directory: Used to locate the compiled binary
- Binary Name: Extracted from binary targets
If Cargo.toml is not found or cargo metadata fails, bundles are created without metadata.
When an icon is found (auto-discovered or explicitly provided), PNG format recommended:
- Linux: Copied as-is alongside the
.desktopfile - macOS: Automatically converted to
.icnsformat usingsipsandiconutil(falls back to PNG if tools unavailable) - Windows: Automatically converted to
.icoformat using ImageMagick oricotool(falls back to PNG if tools unavailable)
If no icon is found, bundles are created without icons.
On Windows, metadata and icons should be embedded at build time using a build script. This is the recommended approach as it embeds resources directly into the executable before bundling.
Setup:
-
Add
winresandico-builderto yourCargo.tomlbuild dependencies:[build-dependencies] winres = "0.1" ico-builder = "0.1"
-
Create a
build.rsfile in your project root:fn main() { #[cfg(target_os = "windows")] { use std::path::Path; // Automatically generate icon.ico from icon.png let png_path = Path::new("icon.png"); let ico_path = Path::new("icon.ico"); if png_path.exists() && !ico_path.exists() { if let Err(e) = ico_builder::IcoBuilder::default() .add_source_file(png_path) .build_file(ico_path) { println!("cargo:warning=Failed to generate icon.ico: {}", e); } } // Embed icon and metadata let mut res = winres::WindowsResource::new(); if ico_path.exists() { res.set_icon("icon.ico"); } res.compile().unwrap(); } }
-
The
winrescrate automatically reads metadata from yourCargo.toml(name, version, description, authors). -
Place your
icon.pngfile in the project root. The build script will automatically convert it to.icoformat.
Note: With this approach, the executable will already have embedded metadata and icons when bundled. No manual icon conversion is required.
To control which files are included in your source distribution (and available during builds), use the include field in Cargo.toml:
[package]
name = "myapp"
include = [
"src/**/*",
"Cargo.toml",
"Cargo.lock",
"icon.ico",
"build.rs",
"README.md",
"LICENSE",
]This is preferred over using the action's include-files parameter for files needed during the build process. The include-files parameter is best used for runtime assets that should be bundled with the final application.
The generated .desktop file follows the Desktop Entry Specification. Install it to ~/.local/share/applications/ or /usr/share/applications/ for desktop integration.
Icon conversion requires macOS-specific tools (sips and iconutil), which are included with macOS. When these tools are not available (e.g., on non-macOS runners), the original PNG will be included in the Resources directory as a fallback.
All macOS .app bundles are automatically signed with an ad-hoc signature (using codesign --sign -) to prevent "damaged" errors on modern macOS and Apple Silicon. This allows the app to run locally without full code signing. For distribution, use the macos-sign option with a proper Developer ID certificate.
Runtime dependencies:
cargo- Required for extracting metadata viacargo metadatajq- Required for parsing JSON output from cargo metadata- The binary must be built before running this action (e.g., with
cargo build --release)
Platform-specific:
- For macOS code signing, proper certificates must be imported into the keychain first
- For Windows ZIP creation on non-Windows runners, the
ziputility must be available - For optimal icon conversion:
- macOS:
sipsandiconutil(included with macOS, falls back to PNG if unavailable) - Linux: No conversion needed (uses PNG directly)
- macOS:
Metadata extraction:
The bundler automatically includes files from your Cargo.toml metadata:
readmefield - Automatically includes README file in bundleslicense-filefield - Automatically includes LICENSE file in bundles- Package metadata (name, version, description, authors) - Used for platform-specific metadata files
If you get "Binary not found" error:
- Verify the binary name matches exactly (without .exe extension, even on Windows)
- Check that
working-directorypoints to the correct location - Ensure
cargo build --releasecompleted successfully
If you get permission errors when running the binary from the bundle:
- The action automatically sets executable permissions for Linux tar.gz files
- For macOS, the binary in the .app bundle is also marked as executable
If code signing fails:
- Ensure certificates are properly imported into the keychain
- Use the correct signing identity format: "Developer ID Application: Your Name (YOUR_TEAM_ID)"
- Replace "Your Name" with your developer name
- Replace "YOUR_TEAM_ID" with your Apple Team ID (10 characters)
- Check that the bundle ID is in reverse domain notation (e.g., com.example.app)
- Verify the certificate is valid and not expired
If macOS reports the app is damaged or can't be opened:
- The bundler automatically applies ad-hoc code signing to prevent this issue
- If you still see this error after downloading, run:
xattr -cr path/to/app.appto remove quarantine attributes - For distribution to other users, use proper code signing with
macos-sign: trueand a Developer ID certificate
If additional files are missing from the bundle:
- File paths in
include-filesshould be relative to the repository root - Use space-separated list:
"file1.txt file2.txt folder/" - Check that the files exist before the bundling step runs
This action is ready to be published to the GitHub Marketplace:
- Create a release with a tag (e.g.,
v1.0.0) - The action will automatically appear in the GitHub Marketplace
- Users can reference it with:
julcst/rust-bundler@v1
We follow semantic versioning:
- Major versions:
v1,v2(breaking changes) - Minor versions:
v1.1,v1.2(new features, backward compatible) - Patch versions:
v1.0.1,v1.0.2(bug fixes)
Users can pin to specific versions or use major version tags that always point to the latest compatible version.
The repository includes a working example in the examples/ directory:
A simple Rust application that demonstrates bundling assets with your application. The example reads and displays text from a bundled assets/hello.txt file, showing how to include additional files and folders in your bundles.
See examples/hello-world/README.md for details.
This example is automatically tested on Linux, Windows, and macOS via CI to ensure the bundler works correctly across all platforms.
See CONTRIBUTING.md for details on how to contribute to this project.
MIT