Cross-Platform Rust: The Real Packaging Challenges Behind ToolGrid
When I started building ToolGrid, a privacy-first developer toolkit in Rust, I thought the hardest part would be the application logic. I was wrong. The real challenge emerged when I tried to package this sophisticated GUI application across different platforms.
The Promise vs. Reality
Rust sells itself on cross-platform compilation, and for command-line tools, it largely delivers. But when you’re building a desktop application with a custom GUI framework, the story gets complicated quickly.
ToolGrid is built using the Freya GUI framework—a Skia-based, Rust-native GUI system. It provides offline utilities like JSON formatting, JWT decoding, Base64 tools, and more, all without sending data to external servers.
Linux: The “Easy” Platform That Isn’t
Debian Packages with cargo-deb
On paper, Linux packaging should be straightforward. The Rust ecosystem provides cargo-deb, which handles most of the heavy lifting. However, getting the configuration right requires careful attention to detail.
Here is the exact [package.metadata.deb] configuration from my Cargo.toml:
[package.metadata.deb]
maintainer = "Raj <[email protected]>"
copyright = "2024, Raj"
extended-description = """\
A developer utility application built with Rust and Freya.
Features include JSON formatter, JWT decoder, Base64 tools, and more.
100% Local. Zero Tracking."""
assets = [
[
"target/release/Toolgrid",
"usr/bin/",
"755",
],
[
"assets/images/app-icon.png",
"usr/share/icons/hicolor/512x512/apps/toolgrid.png",
"644",
],
[
"assets/desktop/toolgrid.desktop",
"usr/share/applications/",
"644",
],
]
You might be wondering about the numbers like "755" and "644" in the assets array. These are Unix file permissions in octal notation, and they are critical for security and functionality:
755(Executable): Used for the binary. The7gives full permissions to the owner (root), while the5s allow everyone else to read and execute the file. If you accidentally used644here, users would get a “Permission denied” error when trying to run the app.644(Read-Only): Used for static assets like the icon and.desktopfile. This allows everyone to read the files (so the OS can display the icon), but prevents non-root users from modifying them.
This configuration is the glue that pieces everything together. Here is how it works:
- The Binary:
target/release/Toolgridis copied to/usr/bin/, making it globally executable asToolgrid. - The Entry Point: The
assets/desktop/toolgrid.desktopfile (actual content shown below) is copied to/usr/share/applications/. This is the critical step. Linux desktop environments (GNOME, KDE, etc.) monitor this specific directory. By placing the file here, the system “discovers” your application. - The Icon: The icon is placed in
/usr/share/icons/..., which the.desktopfile references.
Crucially, the connection relies on the content of the .desktop file matching the filesystem layout defined in Cargo.toml. Take a look at assets/desktop/toolgrid.desktop:
[Desktop Entry]
Name=Toolgrid
Comment=Developer Utilities
Exec=Toolgrid
Icon=toolgrid
Terminal=false
Type=Application
Categories=Development;Utility;
Keywords=JSON;JWT;Base64;Rust;Freya;
StartupWMClass=toolgrid
Notice the line Exec=Toolgrid. Because Cargo.toml placed my binary in /usr/bin/ (which is in the system $PATH), this simple command is all the desktop environment needs to launch the application. The Icon=toolgrid line works similarly, resolving to the image I placed in /usr/share/icons/.
While .deb packages are great for Debian and Ubuntu based distributions, they don’t solve the problem for the rest of the Linux ecosystem. For that, I turned to Flatpak.
Flatpak: The Universal Solution
The Flatpak Configuration (Manifest)
Flatpak uses a YAML file (often called a “manifest”) to tell the builder what to do. Here is my complete, “simple” configuration that takes my pre-built binary and packages it:
app-id: com.brahaspati.toolgrid
runtime: org.freedesktop.Sdk
runtime-version: '24.08'
sdk: org.freedesktop.Sdk
command: toolgrid
finish-args:
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --device=dri
- --share=network
- --filesystem=host
modules:
- name: toolgrid
buildsystem: simple
build-commands:
- install -D target/release/Toolgrid /app/bin/toolgrid
- install -D Toolgrid/assets/desktop/toolgrid.desktop /app/share/applications/com.brahaspati.toolgrid.desktop
- sed -i 's/Exec=Toolgrid/Exec=toolgrid/' /app/share/applications/com.brahaspati.toolgrid.desktop
- sed -i 's/Icon=toolgrid/Icon=com.brahaspati.toolgrid/' /app/share/applications/com.brahaspati.toolgrid.desktop
- install -D Toolgrid/assets/images/app-icon-512.png /app/share/icons/hicolor/512x512/apps/com.brahaspati.toolgrid.png
sources:
- type: dir
path: ../
Breaking It Down for Humans
If you’ve never looked at a Flatpak manifest before, here is what is happening:
- The Identity (
app-id):com.brahaspati.toolgrid. This is the unique name of your app. It must match your.desktopfile name and icon filename for everything to link up correctly. - The Base (
runtime&sdk): I am identifying which platform to run on (org.freedesktop.Sdk). Think of this as the “operating system” inside the container. - The Sandbox Rules (
finish-args): This is where I poke holes in the container security:--socket=wayland/--socket=fallback-x11: “Let me show a window on the screen.”--share=network: “Let me talk to the internet.”--filesystem=host: “Let me see the user’s files.” (Critical for a tool that formats files!)
- The Workhorse (
modules): This is the recipe for installing the app. Since I usedbuildsystem: simple, it acts like a shell script:- Install Binary: Copy
Toolgrid.exe(from Linux build) to/app/bin/. - Install Desktop File: Copy the
.desktopfile to where Linux expects it. - Fix Paths (
sed): This is a pro tip. I usesedto rename theExeccommand andIconname inside the.desktopfile to match theapp-id. This ensures the specific Flatpak environment launches it correctly. - Install Icon: Place the icon where the system can find it.
- Install Binary: Copy
The Build Workflow
Here is the complete step-by-step process. You don’t need a PhD in Linux internals to do this, just follow the recipe:
Step 1. Build the Rust Binary First, I needed to compile the actual application. On Debian/Ubuntu, installing the dependencies is the first step:
sudo apt-get install libssl-dev pkg-config libgtk-3-dev build-essential
cargo build --release
Step 2. Prepare Flatpak Environment Install the builder and the runtime environment defined in my YAML (version 24.08):
sudo apt install flatpak-builder
flatpak install flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08
Step 3. Build and Install Locally
I use flatpak-builder to build the app and install it into your local repository for testing:
# Clean build
flatpak-builder build-dir com.brahaspati.toolgrid.yml --force-clean
# Install locally (with --force-clean to ensure a fresh state)
flatpak-builder --user --install build-dir com.brahaspati.toolgrid.yml --force-clean
Step 4. Create Distributable Bundle
Once it’s working locally, you can create a single-file .flatpak bundle to share with others:
flatpak build-bundle ~/.local/share/flatpak/repo toolgrid.flatpak com.brahaspati.toolgrid
Step 5. Run and Verify Check if it’s installed and run it:
flatpak ps
flatpak run com.brahaspati.toolgrid
That’s it. You have effectively containerized your Rust application.
Windows: When Tooling Betrays You
The WiX Version Conflict
Windows packaging was supposed to be simple with cargo wix. The reality? A nightmare of tooling incompatibility.
The standard cargo wix tool is designed for WiX v3, but modern Windows development machines often install WiX v6. These aren’t just different versions—they’re fundamentally different tools:
- WiX v3: Uses
candle.exeandlight.exewith XML schema v3. - WiX v6: Uses
wix.exewith XML schema v4/v6.
When I tried to use cargo wix, it simply failed to find the expected binaries.
The Manual Build Workflow
Since the automated tools failed me, I established a robust manual workflow. Here is how you can replicate it:
Step 1. Install the Tooling First, you need the actual build tools. Download and install the WiX Toolset v4/v5 (or v6 depending on availability) from the official GitHub releases.
Step 2. Prepare the Assets
The installer needs 2 critical files in your wix/ or assets/ directory:
app-icon.ico: A true.icofile for the Taskbar and Control Panel.LICENSE.rtf: A Rich Text Format license file. The WiX UI requires this format for the “License Agreement” screen; it cannot read Markdown or plain text.
Here is the exact RTF content I used for the MIT license:
{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\*\generator Riched20 10.0.19041}\viewkind4\uc1
\pard\sa200\sl276\slmult1\f0\fs22\lang9 MIT License\par
\par
Copyright (c) 2024 Raj\par
\par
Permission is hereby granted, free of charge, to any person obtaining a copy\par
of this software and associated documentation files (the "Software"), to deal\par
in the Software without restriction, including without limitation the rights\par
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\par
copies of the Software, and to permit persons to whom the Software is\par
furnished to do so, subject to the following conditions:\par
\par
The above copyright notice and this permission notice shall be included in all\par
copies or substantial portions of the Software.\par
\par
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\par
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\par
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\par
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\par
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\par
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\par
SOFTWARE.\par
}
Step 3. Automate the Build
Instead of remembering complex wix.exe commands, I created a build_installer.cmd script to handle the heavy lifting:
- Compiles the Rust binary in release mode.
- Copies the License and assets to the correct staging folders.
- Invokes
wix buildwith the correct flags and extensions.
@echo off
setlocal
echo [1/3] Building Release Binary...
cargo build --release
if %errorlevel% neq 0 (
echo Cargo build failed!
exit /b %errorlevel%
)
echo [2/3] Preparing Assets...
if not exist "wix\LICENSE.rtf" (
copy LICENSE.rtf wix\LICENSE.rtf
)
if not exist "target\wix" (
mkdir target\wix
)
echo [3/3] Generating MSI Installer...
:: Update path to WiX if necessary
set "WIX_PATH=C:\Program Files\WiX Toolset v6.0\bin\wix.exe"
"%WIX_PATH%" build wix\main.wxs ^
-o target\wix\Toolgrid.msi ^
-d Version=0.1.0 ^
-d CargoTargetBinDir=..\target\release ^
-arch x64 ^
-ext WixToolset.UI.wixext
if %errorlevel% neq 0 (
echo MSI generation failed!
exit /b %errorlevel%
)
echo.
echo ==========================================
echo Installer created successfully!
echo Location: target\wix\Toolgrid.msi
echo ==========================================
pause
The WiX Configuration
The wix/main.wxs file is the heart of the installer. It defines the directory structure, shortcuts, and features. Here is a glimpse into the structure (stripped of some standard boilerplate) that makes the installer tick:
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package Name="Toolgrid" UpgradeCode="4CCC1EA1-B679-4023-8D7F-E6BBAC045078" Manufacturer="Raj" Language="1033" Codepage="1252" Version="$(var.Version)" InstallerVersion="450">
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="A newer version of [ProductName] is already installed. Setup will now exit." />
<Media Id="1" Cabinet="media1.cab" EmbedCab="yes" DiskPrompt="CD-ROM #1" />
<Property Id="DiskPrompt" Value="Toolgrid Installation" />
<Feature Id="Binaries" Title="Application" Description="Installs all binaries and the license." Level="1" ConfigurableDirectory="APPLICATIONFOLDER" AllowAdvertise="no" Display="expand" AllowAbsent="no">
<ComponentRef Id="License" />
<ComponentRef Id="binary0" />
<Feature Id="Environment" Title="PATH Environment Variable" Description="Add the install location to PATH." Level="1">
<ComponentRef Id="Path" />
</Feature>
<ComponentRef Id="ApplicationShortcut" />
</Feature>
<!-- UI and Icon Configuration -->
<Icon Id='ProductICO' SourceFile='assets\images\app-icon.ico'/>
<Property Id='ARPPRODUCTICON' Value='ProductICO' />
<UI>
<ui:WixUI Id="WixUI_FeatureTree" />
</UI>
<WixVariable Id="WixUILicenseRtf" Value="LICENSE.rtf" />
<!-- Directory Structure -->
<StandardDirectory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="Toolgrid">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="Toolgrid"
Description="A fast, privacy-first developer toolkit."
Target="[#exe0]"
WorkingDirectory="Bin"
Icon="ProductICO"/>
<RemoveFolder Id="CleanUpShortCut" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\Raj\Toolgrid" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</Directory>
</StandardDirectory>
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
<Directory Id="APPLICATIONFOLDER" Name="Toolgrid">
<Component Id="License">
<File Id="LicenseFile" DiskId="1" Source="LICENSE.rtf" KeyPath="yes" />
</Component>
<Directory Id="Bin" Name="bin">
<Component Id="Path" Guid="77B79582-E577-49E0-9118-8A4621D41535" KeyPath="yes">
<Environment Id="PATH" Name="PATH" Value="[Bin]" Permanent="no" Part="last" Action="set" System="yes" />
</Component>
<Component Id="binary0">
<File Id="exe0" Name="Toolgrid.exe" DiskId="1" Source="$(var.CargoTargetBinDir)\Toolgrid.exe" KeyPath="yes" />
</Component>
</Directory>
</Directory>
</Directory>
</Package>
</Wix>
This XML file handles the magic of creating a “real” Windows application—shortcuts, PATH variables, and uninstaller entries.
However, getting the code to compile and run was only half the battle. Asset management became its own cross-platform puzzle. Linux demanded PNG icons and .desktop files in specific system directories, while Windows refused to look at anything other than .ico files and required specific Registry keys for the “Add/Remove Programs” list. It wasn’t just about writing code; it was about speaking the native dialect of every operating system I touched.
What I Learned
This journey taught me that cross-platform support in Rust is a spectrum. While the language itself is incredibly portable, the ecosystem around GUI packaging is still catching up. I often found myself stuck between outdated tools and bleeding-edge incompatibilities—like the WiX version mismatch—forcing me to build my own bridges.
Ultimately, I learned that there is no substitute for “getting your hands dirty.” Custom build scripts became inevitable, documentation became my lifeline, and testing on real hardware became non-negotiable. You cannot simply ship code; you must ship a tailored experience for each OS, speaking its language and respecting its conventions.
Conclusion
Cross-platform Rust development is both more powerful and more complex than the marketing suggests. It demands a willingness to engage with the unique constraints of each operating system. The tooling will eventually catch up, but until then, manual intervention and custom scripting are part of the price you pay for native performance.
But looking at ToolGrid running seamlessly on a Linux desktop and a Windows laptop—secure, fast, and private—the effort payoff is clear. With enough persistence (and perhaps a few shell scripts), you can indeed build and ship sophisticated cross-platform applications.
Just don’t expect it to be as simple as cargo build --release.
Have you faced similar cross-platform packaging challenges with Rust? I’d love to hear about your experiences and solutions.