Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

app_bundler

Packages an already-built executable into a real macOS AppName.app bundle — the same shape Xcode produces, built by hand instead. Point it at any compiled binary (Spinel or otherwise) and it gives you the directory tree, Info.plist, optional icon, optional Terminal-hosted mode for text CLIs, and optional codesigning that Finder, Spotlight, and the Dock all expect from a real app.

Built with, and requires, Spinel (spin). See DEVELOPMENT_PLAN.md for the full design record.

Build

spin build

Compiles both bin/*.rb targets into build/bin/: app_bundler itself, and terminal_launcher, a small stub binary --terminal mode copies into bundles it creates (see Terminal-hosted mode below for why that's a separate compiled binary rather than a written-out shell script). Keep both together — spin install (which copies bin/* to ~/.local/bin) does this for you automatically.

Usage

app_bundler --executable PATH --name NAME --identifier ID [options]
flag required meaning
--executable=PATH yes the built executable to wrap
--name=NAME yes app display name; produces NAME.app
--identifier=ID yes reverse-DNS CFBundleIdentifier, e.g. com.you.app
--output=DIR directory to write NAME.app into (default: .)
--version=VERSION CFBundleVersion / CFBundleShortVersionString (default: 1.0.0)
--icon=PATH an .icns file, a .iconset directory, or a source image (PNG etc.)
--terminal wrap a text-mode CLI so it opens in Terminal.app instead of running headless
--sign=IDENTITY codesign the bundle; - for ad-hoc
--copyright=TEXT NSHumanReadableCopyright
--min-os-version=VER LSMinimumSystemVersion (default: this machine's major OS version)
--force overwrite an existing NAME.app
-h, --help show usage

Worked example: bundling an sdl_ui demo

sdl_ui is a sibling project in this workspace with several SDL-based GUI demo binaries under bin/*_demo.rb. Bundling one of its built demos end to end:

cd ../sdl_ui
spin build widgets_demo

cd ../app_bundler
./build/bin/app_bundler \
  --executable=../sdl_ui/build/bin/widgets_demo \
  --name="Widgets Demo" \
  --identifier=com.example.widgetsdemo \
  --sign=-

open "Widgets Demo.app"

This produces Widgets Demo.app with a real Info.plist (NSHighResolutionCapable set, so it renders at full sharpness on Retina displays), an ad-hoc code signature, and CFBundleExecutable pointing straight at widgets_demo — it's already a GUI app, so no Terminal shim is needed. open launches it windowed, with a Dock icon, and no flashing terminal — confirmed via mdls -name kMDItemKind reporting Application and the process running under its own name, not as a child of Terminal.

Terminal-hosted mode

Some executables are text-mode CLIs that expect a real TTY — Finder gives launched apps none by default. --terminal handles this by pointing CFBundleExecutable at a small stub (terminal_launcher, built alongside app_bundler itself — see Build) instead of the real binary. At launch, the stub resolves its own bundle-relative location, then asks Terminal.app to run the real binary (shipped alongside it in Contents/MacOS/ under its own name) via AppleScript's do script.

Why a compiled stub and not a written-out #!/bin/sh shim (the obvious-looking alternative): confirmed by hand that it doesn't work. A bundle whose CFBundleExecutable is a plain shell script fails to launch via open/Finder outright — even ad-hoc codesigned — while the identical bundle with a real Mach-O executable in that slot launches fine. LaunchServices' launch validation requires a genuine executable format there, not a script. terminal_launcher sidesteps this by being itself a tiny compiled Spinel program, the same trick tools like Platypus use for their own stub.

First launch prompts for permission. The first time a newly built --terminal app tries to control Terminal.app, macOS shows a one-time "App wants to control Terminal" Automation permission dialog — this is standard OS privacy behavior for any app that scripts another app, not something app_bundler can (or should) suppress. Click Allow once; subsequent launches are silent.

Code signing

--sign=IDENTITY runs codesign --force --deep --sign IDENTITY on the finished bundle. --sign=- (a literal hyphen) requests ad-hoc signing — enough to clear Gatekeeper's harshest local-run block, and what every example above uses. It is not a substitute for a real Developer ID signature: ad-hoc-signed bundles won't survive redistribution or notarization. Omit --sign entirely (the default) for local use that doesn't need it.

Icons

--icon accepts three input shapes:

  • An .icns file — copied straight into Contents/Resources/.
  • An Apple .iconset directory (the multi-resolution PNG folder convention) — converted via iconutil -c icns.
  • A single source image (PNG etc.) — resized into a full iconset via sips at each of the 10 required sizes/scales, then converted the same way.

Omit --icon and Finder shows the generic app icon; no placeholder is shipped by default.

Testing

spin test

test/bundle_basic.rb exercises the bundle-creation path directly (skipping ARGV/OptionParser): directory tree, binary copy + permissions, Info.plist contents, --force overwrite behavior, and the missing-executable error path — against a throwaway fixture executable (the tool doesn't care what's inside it, so a placeholder file is enough for this kind of test).

Icon conversion (iconutil/sips), codesigning, and the Terminal launcher were verified by hand against real inputs rather than in the snapshot suite, since they shell out to macOS system tools and, in the Terminal case, involve real GUI/Automation-permission state that isn't practical to assert against in a headless test run.

About

Packages a compiled Spinel (or any) binary into a real macOS .app bundle

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages