Packaging a Desktop App
Ship a runnable binary.
Packaging a Desktop App is a free Learn Rust Coding lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Learn Rust Coding learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
From Debug to Release
Development uses cargo run, which builds an unoptimized debug binary. For distribution you want a release build: cargo build --release.
Release builds enable optimizations and strip debug overhead, producing a far smaller, faster binary under target/release/.
cargo build --release
# binary at target/release/my_appShrinking the Binary
egui apps can be large because they statically link a GPU backend. Trim size with a release profile in Cargo.toml.
Enabling link-time optimization, a single codegen unit, panic-abort, and stripping symbols can cut megabytes off the final executable.
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = trueHiding the Console on Windows
On Windows a GUI app should not spawn a console window. Add the windows_subsystem attribute at the crate root, gated to release so you still see logs while developing.
Without this, double-clicking the executable flashes a black terminal alongside your window.
#![cfg_attr(
not(debug_assertions),
windows_subsystem = "windows"
)]
fn main() -> eframe::Result<()> { /* ... */ }Setting a Window Icon
A polished app sets its own window icon. Load PNG bytes at startup, decode them to RGBA, and pass an IconData through NativeOptions.
Embed the image with include_bytes! so the icon ships inside the binary rather than as a loose file.
let icon = eframe::icon_data::from_png_bytes(
include_bytes!("../assets/icon.png")
).unwrap();
let opts = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_icon(icon),
..Default::default()
};Configuring the Viewport
The ViewportBuilder controls the native window: initial size, minimum size, title, resizability, and decorations.
Set sensible defaults so your app opens at a usable size on first launch instead of a tiny or oversized window.
let viewport = egui::ViewportBuilder::default()
.with_inner_size([900.0, 600.0])
.with_min_inner_size([400.0, 300.0])
.with_title("My App");
let opts = eframe::NativeOptions { viewport, ..Default::default() };Embedding Assets
For a single-file distributable, embed fonts, images, and config directly in the binary with include_bytes! and include_str!.
This avoids shipping a folder of resources and prevents the app from breaking when run from a different working directory.
let font = egui::FontData::from_static(
include_bytes!("../assets/Inter.ttf")
);
// register font in ctx.set_fonts(...)macOS App Bundles
On macOS a bare binary is not a clickable app. You need a .app bundle with an Info.plist and an .icns icon.
The cargo-bundle tool automates this, reading metadata from a [package.metadata.bundle] section in Cargo.toml.
[package.metadata.bundle]
name = "My App"
identifier = "com.example.myapp"
icon = ["assets/icon.icns"]
# then: cargo bundle --releaseCode Signing and Notarization
Distributing on macOS to other machines requires signing with a Developer ID certificate and notarizing with Apple, or Gatekeeper blocks the app.
Use codesign to sign the bundle, then submit it with notarytool and staple the ticket. Windows similarly benefits from an Authenticode signature to avoid SmartScreen warnings.
codesign --deep --force --options runtime \
--sign "Developer ID Application: Name (TEAMID)" \
MyApp.appCross-Platform Builds
Each OS needs a native build because eframe links platform GPU and windowing libraries. The reliable approach is to build on each target OS, often via CI runners.
Tools like cross help for Linux targets, but true cross-compilation of GUI binaries to macOS or Windows from Linux is fragile — prefer matrix CI builds.
# GitHub Actions matrix
# runs-on: [ubuntu-latest, macos-latest, windows-latest]
cargo build --releaseThe Web Target
egui also compiles to WebAssembly. Build with the wasm32-unknown-unknown target and bundle using trunk, which produces an HTML/JS/WASM set you can host statically.
The same App code runs in the browser via eframe::WebRunner, mounted onto a canvas element.
rustup target add wasm32-unknown-unknown
trunk build --release
# outputs dist/ ready to hostDistribution Checklist
Before shipping: tune the release profile for size, hide the Windows console, embed an icon and assets, bundle for macOS, and sign for both macOS and Windows.
Test the final artifact on a clean machine — not your dev box — to catch missing system libraries or unsigned-binary warnings real users would hit.
Quick Check
Why add the windows_subsystem attribute?
Recap
Ship with cargo build --release, then optimize the release profile for size and hide the Windows console. Set a window icon and viewport, and embed assets with include_bytes! for a self-contained binary.
On macOS bundle with cargo-bundle and sign plus notarize; build per-OS via CI for cross-platform coverage, and target WebAssembly with trunk. Always test the final artifact on a clean machine. That completes the egui course.
Frequently asked questions
Is the “Packaging a Desktop App” lesson free?
Yes — the full text of “Packaging a Desktop App” is free to read here on the web, and the Learn Rust Coding course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Learn Rust Coding course, upgrade to CoddyKit PRO.
What will I learn in “Packaging a Desktop App”?
Ship a runnable binary. You practise Learn Rust Coding with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Learn Rust Coding?
No prior experience is required. Learn Rust Coding on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Packaging a Desktop App” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Learn Rust Coding lesson?
Yes. Every Learn Rust Coding lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Immediate-Mode UI Basics
- Widgets and Layout
- Managing App State
- Packaging a Desktop App