A recent diversion had me busy for a fair amount of time trying to generate a .app bundle for MacOS-X for a program written in SBCL. There were some interesting challenges, so I felt it might be useful to someone if I share what I learned.
The mac has two ‘styles’ of executable programs: regular UNIX style executable (-r-xr-xr-x style) and an “app bundle” type. The bundle is a directory with a .app extension and a collection of subdirectories and files. App bundles contain the executable, some metadata and (optionally) a collection of libraries, Framework bundles and other resources.
Once we’ve saved the the executable imagine in sbcl as a UNIX style file using sb-ext:save-lisp-and-die with :executable t and :top-level set to your “main” function, we can start piecing together the .app structure. Inside of the .app, the executable will be copied to <myprogram>.app/Contents/MacOS/<myprogram>. Additionally, a PkgInfo file and Info.plist file will need to be generated and copied into <myprogram>.app/Contents/, with an optional .icns file in <myprogram>.app/Contents/Resources/. Any .dylib or .framework libraries would be copied to <myprogram>.app/Contents/Frameworks.
Everything is pretty straight forward so far, but there is a gotcha in there. Apps can be moved, yet SBCL records the absolute path to the libraries on save-lisp-and-die. When the libraries are initially loaded using load-shared-object, you need to give each library “:dont-save t” and manually load at program initialization. Since I use CFFI, I altered my copy to automatically set the flag no matter what, then do # darwin(cffi:load-foreign-library #P”Contents/Frameworks/<mylib>.dylib”). Now we’ve solved the issue of being able to move the .app bundle from an SBCL image.
Now for another gotcha. The finder leaves the current working directory as / when a bundle is launched, so those relative paths we cffi:load-foreign-library’d fail. Before any library loading happens, we need to find where we want to set the cwd to and make it happen. sb-sys has a function os-get-runtime-executable-path that will return the full path of the UNIX executable, so some string manipulation can help us cwd to the .app directory. I have a constant executable-name so I can do # darwin(sb-posix:chdir (let ((binname (sb-ext:os-get-runtime-executable-path t))) (subseq binname 0 (- (length binname) (length (concatenate ‘string “Contents/MacOS/” executable-name )))))).
To automate everything, I made a shell script that rm’s the app dir, executes sbcl to compile the app and call save-lisp-and-die, then copy everything into the .app structure as necessary. Data files get copied to <myprogram>.app/Contents/Resources/. Sweet, a working .app that has an icon and can be double-clicked from the finder to start it. For icing on the cake, my script calls “hdiutil create <myprogram>.dmg -srcfolder <myprogram.app> -ov” to generate a compressed .dmg for distribution.