Shipping a 150 KB Mac App With No Xcode Project, No Dependencies, No Electron
How ChargeBeep goes from one main.swift file to a drag-to-Applications .dmg with a single swiftc call and a 60-line bash script — and where code signing actually bites.
- Swift
- macOS
- Distribution
The whole app is one file
ChargeBeep is a commercial menu-bar app I sell on Gumroad. It has a settings window,
a sound picker, login-item autostart, power management, and a battery readout in the
menu bar. It is also a single main.swift file of about 730 lines, with no Xcode
project, no .xcodeproj, no Swift Package Manager manifest, and no third-party code.
That’s not minimalism for its own sake — it’s what the app actually needs. A menu-bar utility that talks to IOKit and AppKit doesn’t need a build system with a dependency graph. It needs a compiler. So the “build” is literally:
swiftc -O main.swift -o ChargeBeep
One command, a ~150 KB binary. For comparison, the same app in Electron would ship an entire Chromium runtime — tens of megabytes — to draw a menu-bar icon and play a sound.
Hand-rolling the .app bundle
A macOS app isn’t a binary, it’s a bundle — a directory with a specific layout. You
don’t need Xcode to make one; you need mkdir and a plist. The build script assembles
it by hand:
ChargeBeep.app/
Contents/
MacOS/ChargeBeep # the swiftc output
Resources/
microwave-done.wav # built-in sound
AppIcon.icns
Sounds/ # extra bundled sounds, auto-discovered at runtime
Info.plist
The Info.plist is generated inline in the script. Two keys matter most for a
menu-bar app:
LSUIElement=true— this is what makes the app run with no Dock icon and no menu bar of its own; it lives only as a status item. Without it you’d get a bouncing Dock icon for a utility that has no window most of the time.LSMinimumSystemVersion=11.0— sets the floor so the login-item and power APIs are guaranteed present.
The extra sounds are a nice detail: the app enumerates Resources/Sounds/ at runtime
and populates the picker from whatever .wav/.aiff/.caf files are there, so adding
a sound to the product is “drop a file in the folder and rebuild” — no code change.
Packaging the .dmg
Distribution is a disk image with a shortcut to /Applications, so users get the
familiar drag-to-install window:
mkdir -p "$STAGE"
cp -R "ChargeBeep.app" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
hdiutil create -volname "ChargeBeep" -srcfolder "$STAGE" \
-ov -format UDZO "ChargeBeep.dmg"
The ln -s /Applications is the whole “drag the app onto the Applications folder”
convention — it’s just a symlink sitting next to the app inside the image.
Where signing actually bites
This is the part nobody warns you about until Gatekeeper does. The build does an ad-hoc signature:
codesign --force --deep --sign - "ChargeBeep.app"
The - means “sign with no identity” — it satisfies the code must be signed at all
requirement on Apple Silicon, but it is not a Developer ID signature and it is
not notarized. The practical consequences, which you have to be honest with
customers about:
- On first launch the user must right-click → Open once, because Gatekeeper blocks double-click launch of un-notarized apps. After that first approval it launches normally.
- For a frictionless “just double-click it” experience you need a paid Apple Developer
account, a Developer ID certificate, and to run the app through
notarytoolfor notarization. That’s a real cost-and-process decision, not a code one.
For a low-price indie utility, ad-hoc signing plus a clear “right-click → Open the first time” instruction is a legitimate trade-off — you’re trading a one-time click of friction for skipping the whole Apple Developer Program overhead. Just don’t pretend the friction isn’t there; put the instruction on the download page.
Why do it this way
The temptation with any Mac app is to open Xcode and let it generate a project, a
scheme, an asset catalog, and a dependency manifest. For something this size, all of
that is scaffolding you now have to maintain around 730 lines of actual program. A
main.swift and a bash script means the entire buildable, sellable product is two
files a person can read start to finish in an afternoon — and that legibility is worth
more than any convenience Xcode was offering.