Jump to content

Developing macOS Applications in Rust

From JOHNWICK

For training AI models, I use an Ubuntu server with 16 cores alongside a MacBook Pro M4 Max. During training, I frequently monitor CPU and memory usage to ensure all cores are utilized efficiently and to estimate memory consumption. However, I missed having on macOS a system monitor that provides a visualization similar to what I’m used to on Ubuntu. So, I built one — a simple System Monitor application that displays CPU and memory usage.

Although it’s easy to develop such a Rust-based application, I wanted to go a step further and create a fully native macOS app, complete with an icon, bundle metadata, and Launchpad integration. Fortunately, this only takes a few additional steps once your Rust application is working.

Step 1: Install cargo-bundle cargo install cargo-bundle This tool packages your Rust application into a proper .app bundle for macOS.

Step 2: Prepare an App Icon Create or download a 1024×1024 icon. In my case, I generated one using ChatGPT. Save it as

Step 3: Add Bundle Metadata to Cargo.toml [package.metadata.bundle] name = "SysMonitor" identifier = "xx.yyy.sysmonitor" icon = ["assets/SysMonitor.icns"]

  1. resources = ["assets/"]

Here you define the app name, bundle identifier, and icon set.
Optional: you can later add a resources section for assets.

Step 4: Generate the .icns Icon Set Use the following shell script (make_icns.sh) to convert your 1024×1024 PNG into an Apple .icns bundle.

  1. !/usr/bin/env bash

set -euo pipefail

  1. make_icns.sh
  2. Usage: ./make_icns.sh <input-image> <output-dir>
  3. Example: ./make_icns.sh SysMonitor_1024.png ./assets

if $# -ne 2 ; then

 echo "Usage: $0 <input-image> <output-dir>"
 exit 1

fi

INPUT="$1" OUTDIR="$2"

  1. ---- Preflight checks -------------------------------------------------------

if ! -f "$INPUT" ; then

 echo "Error: input file not found: $INPUT" >&2
 exit 1

fi

if ! command -v sips >/dev/null 2>&1; then

 echo "Error: 'sips' not found (macOS only). Install Xcode command line tools." >&2
 exit 1

fi

if ! command -v iconutil >/dev/null 2>&1; then

 echo "Error: 'iconutil' not found. Install Xcode or its command line tools." >&2
 exit 1

fi

mkdir -p "$OUTDIR"

  1. Derive names

BASENAME="$(basename "$INPUT")" STEM="${BASENAME%.*}" ICONSET_DIR="$OUTDIR/${STEM}.iconset" ICNS_PATH="$OUTDIR/$STEM.icns"

  1. ---- Validate input dimensions ---------------------------------------------
  2. We want at least 1024x1024 for best results.

WIDTH=$(sips -g pixelWidth "$INPUT" 2>/dev/null | awk '/pixelWidth:/ {print $2}') HEIGHT=$(sips -g pixelHeight "$INPUT" 2>/dev/null | awk '/pixelHeight:/ {print $2}')

if [[ -z "${WIDTH:-}" || -z "${HEIGHT:-}" ]]; then

 echo "Error: couldn't read image dimensions from: $INPUT" >&2
 exit 1

fi

if "$WIDTH" -ne "$HEIGHT" ; then

 echo "Error: input image must be square. Got ${WIDTH}x${HEIGHT}." >&2
 exit 1

fi

if "$WIDTH" -lt 1024 ; then

 echo "Error: input image must be at least 1024x1024. Got ${WIDTH}x${HEIGHT}." >&2
 exit 1

fi

  1. Create a 1024x1024 working copy (downscale if larger, keep quality)

TMPDIR="$(mktemp -d)" trap 'rm -rf "$TMPDIR"' EXIT WORK_1024="$TMPDIR/icon_1024.png" sips -s format png -z 1024 1024 "$INPUT" --out "$WORK_1024" >/dev/null

  1. ---- Build iconset ----------------------------------------------------------

rm -rf "$ICONSET_DIR" mkdir -p "$ICONSET_DIR"

sips -z 16 16 "$WORK_1024" --out "$ICONSET_DIR/icon_16x16.png" >/dev/null sips -z 32 32 "$WORK_1024" --out "$ICONSET_DIR/[email protected]" >/dev/null sips -z 32 32 "$WORK_1024" --out "$ICONSET_DIR/icon_32x32.png" >/dev/null sips -z 64 64 "$WORK_1024" --out "$ICONSET_DIR/[email protected]" >/dev/null sips -z 128 128 "$WORK_1024" --out "$ICONSET_DIR/icon_128x128.png" >/dev/null sips -z 256 256 "$WORK_1024" --out "$ICONSET_DIR/[email protected]" >/dev/null sips -z 256 256 "$WORK_1024" --out "$ICONSET_DIR/icon_256x256.png" >/dev/null sips -z 512 512 "$WORK_1024" --out "$ICONSET_DIR/[email protected]" >/dev/null sips -z 512 512 "$WORK_1024" --out "$ICONSET_DIR/icon_512x512.png" >/dev/null cp "$WORK_1024" "$ICONSET_DIR/[email protected]" # 1024x1024

  1. ---- Create .icns -----------------------------------------------------------

iconutil -c icns "$ICONSET_DIR" -o "$ICNS_PATH"

echo "Iconset: $ICONSET_DIR" echo "ICNS: $ICNS_PATH" echo

Then, generate the icon set: ./icons/mkicon.sh ./icons/SysMonitor.png ./assets

Step 5: Build the App Bundle cargo bundle --release APP=./target/release/bundle/osx/SysMonitor.app

Step 6: Test the Application open $APP

Step 7: Troubleshooting

If you run into issues, here are some quick diagnostics:

  1. Quick way to edit/check the plist

/usr/libexec/PlistBuddy -c "Print" $APP/Contents/Info.plist

  1. Check that the .icns is inside the bundle

ls -l $APP/Contents/Resources

  1. Verify Info.plist points to the icon

/usr/libexec/PlistBuddy -c "Print :CFBundleIconFile" $APP/Contents/Info.plist

  1. If it prints AppIcon.icns, it's good.

If needed, extract the .icns to inspect it:

  1. Validate the .icns itself (most common cause)

mkdir -p tmp/SM.iconset iconutil -c iconset $APP/Contents/Resources/SysMonitor.icns -o tmp/SM.iconset ls -1 tmp/SM.iconset

Step 8: Install the App You can copy the app bundle to either your system or user Applications folder: cp -R target/release/bundle/osx/SysMonitor.app /Applications/

  1. or

cp -R target/release/bundle/osx/SysMonitor.app ~/Applications/

Now it appears in Launchpad, Spotlight search, and you can pin it to the Dock.

Step 9: Adding Resources If your app uses additional resources (icons, images, fonts, configuration files, etc.), add them in Cargo.toml: [package.metadata.bundle] ... resources = ["assets", "images/**/*.png", "data/config_defaults.json"]

These files will be copied into your bundle at
SysMonitor.app/Contents/Resources/...

Typical examples include:

  • Images, icons, cursors
  • Fonts (for custom UI frameworks like Iced)
  • Shaders, CSV/JSON, templates, localization files
  • Default configuration files
  • LICENSE, README, etc.

All resources are read-only inside the bundle. Accessing Resources at Runtime (Rust) When running from the bundle, resources live under .../Contents/Resources.
When running with cargo run, they’re in your project directory.

A small helper function can make this transparent: use std::{

   env,
   path::{Path, PathBuf},

};

  1. [allow(dead_code)]

fn resource_path(rel: impl AsRef<Path>) -> PathBuf {

   // Path to the running executable
   let exe = env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
   // If we’re inside My.app/Contents/MacOS, hop to Contents/Resources
   if let Some(macos_dir) = exe.parent() {
       if macos_dir.ends_with("MacOS") {
           if let Some(contents_dir) = macos_dir.parent() {
               let res = contents_dir.join("Resources");
               return res.join(rel.as_ref());
           }
       }
   }
   // Fallback for `cargo run`: use repo-relative path
   PathBuf::from(rel.as_ref())

}

Usage example: let icon_path = resource_path("assets/SysMonitor.icns"); let out=format!("IconPath: {:?}", icon_path); let _ =std::fs::write("sys_monitor/output.txt", out);

When you run the bundle: open $APP

  1. in output.txt:
  2. IconPath: "xxxxx/target/release/bundle/osx/SysMonitor.app/Contents/Resources/assets/SysMonitor.icns"

And when you run via Cargo: cargo run --release

  1. in output.txt:
  2. IconPath: "assets/SysMonitor.icns"

Notes on Writable Files Only include read-only assets inside the .app bundle.
For writable files like logs or user settings, use a per-user directory such as: ~/Library/Application Support/SysMonitor/ You can conveniently locate this path via the directories crate.

Summary Building a native macOS app in Rust is straightforward with cargo-bundle.
Once your application logic is ready, adding a proper icon, bundle metadata, and resource management takes just a few steps — resulting in a polished, fully integrated macOS experience.