[cabal-devel] Support for .app bundles on Mac OS X

Dan Knapp dankna at gmail.com
Sat May 28 16:05:43 CEST 2011


  This is to summarize a conversation I had with dcoutts on IRC, for
everyone's benefit.

  On the Mac, both libraries and executables can be packaged in "bundles",
which are special directory structures that are treated as files by the
graphical file manager.  We already have a ticket, #583, proposing that we
automate the creation of these bundles; I've been working on it.  At this
point I have a working prototype (see link to darcs repository, below) that
runs using the hooks, and am simply working on integrating it with Cabal
"for real".

  I'm aware that my prototype will need some changes to be accepted; in
particular, I rebelled against the directory and filepath packages and wrote
my own filesystem-portability layer, mostly out of frustration with
System.Directory's recursive-delete function, which currently traverses
symbolic links (very dangerous!) since it doesn't know about them.  That
will have to be changed, and also my discussion with Duncan brought up some
issues with the interface that will need reworking.

  Now, bundles.  There are two cases, .framework bundles which are for
dynamically-linked shared libraries, and .app bundles which are for
executables.  In the spirit of avoiding feature creep, for now I'm just
supporting .app bundles, but with an eye towards supporting .framework ones
later; I think that's most of what people want, anyhow.

  Incidentally, a bundle can include other bundles, which will be linked
with it magically at runtime.  This is (among Mac fans) a very nice thing to
be able to do, as it simplifies dependency distribution no end.  The existing
package cabal-macosx pulls in fgl (functional graph library) and MissingH to
do some graph-theoretical stuff to chase dependencies and automatically
include them in the bundle.  I note that it would actually be better to only
include things explicitly requested, as there may be licensing implications,
and take the simpler stance that if you want to include frameworks in your
bundle you can, uh, implement that feature yourself. :)

  Now, what are the prerequisites of building a bundle?  To what extent can it
be done generically, without the package author providing Mac-specific
information?  Well, the major obstacle to that is the Info.plist file, which
is XML in a schema defined by Apple that is basically an attribute/value tree.
That file contains various metadata.  For detailed documentation on what it
holds, go to the developer.apple.com link below and scroll down to "The
Information Property List File", but I'll summarize here.

  There's a ton of stuff - a reverse-DNS-name along the lines of
com.dankna.niftyprogram which is used to identify the program; a filename
pointing to an icon file within the bundle; copyright string; version string;
what OS version is required; names of Objective-C classes and .nib files to be
instantiated.  There's a list of document types the application recognizes,
including both what its role is with regard to each (editor/reader/whatever)
and how they can be identified (file extension; legacy four-character code;
UTI, such as public.text or com.dankna.niftyprogram.document).  There's a
list of UTIs the application exports, which is separate from the list of
document types but used by it.  There's also a list of UTIs it imports.

  Because there's so much information, and because Apple may extend the set of
information included at any time, I believe what makes sense to store in the
.cabal is a path to an Info.plist file to be included into the bundle.  The
other approach would be to map all this information onto fields to go directly
into the .cabal, and then generate the Info.plist from that.  I'm willing to
do the work to come up with that mapping and parse it all out, if people feel
I should.  It would have a certain elegance to it, as it would keep all
metadata in only one place.  Since Windows and Linux don't use UTIs, basically
none of the information would be applicable to them.

  One idea that came up was to provide tools to generate an initial Info.plist
file for the user to edit further.  I don't think this would be useful; Apple
already provides such tools, and it's not really practical to develop for the
Mac without actually having a Mac to test on.

  Now, what else goes into a bundle?  So far we've covered the compiled code
itself, and the Info.plist metadata.  There's also the Resources directory
(located at NiftyProgram.app/Contents/Resources/), which contains, in general,
arbitrary files such as icons, sounds, graphics, and localized text.  But
/in particular/ it contains .nib files, which are a binary serialization of
GUI objects (and often non-GUI objects as well), and which are compiled by
Apple's "ibtool" command-line program from .xib files, which are XML, although
far too verbose to edit by hand.

  After some discussion with Duncan, he has convinced me that the right thing
to do is to list everything that belongs in the Resources directory in the
data-files: field, but recognize .xibs by their extension and run ibtool on
them instead of copying them directly.

  Now, I would like to take a moment to note at this point that there is
absolutely, positively no way to run a Mac GUI program without a .app bundle,
and if there were, it would be unsupported and prone to causing the window
manager to crash.  So it makes no sense whatsoever to try to run "in place"
out of the source tree without copying things into a .app bundle.  The
implication is that it makes no sense to include nibs/xibs in the installed
data, unless we are building a bundle.  So what I plan to do is this:

  There will be an additional boolean, determined at configure-time, that
tells us whether we're building a bundle or not.  That boolean will be
testable with if-blocks in the .cabal file, using the syntax "if bundle".
When building in appropriate circumnstances (more on that in a sec), it will
default to true; otherwise to false; in either case it will be settable with
configure-time flags --*-bundle and --no-*-bundle, where * is the name of
the executable section, or --bundles and --no-bundles to set it for all
executable sections at once.

  There is a difficult balance here because on most platforms, the choice of
data-dir: is made by the person who builds the package, which is usually a
different person from the one who authors it in the first place.  But when
building a bundle, there is only one possible choice, so we straddle the
author/builder line a bit.

  Now, how do we default it?  Well, first off, if we're not on a Mac, default
to false (no bundle).  If we're on a Mac, /and/ the package description is
such that if we used true as the value we would have a bundle-info: field in
the Executable section we are considering at the moment, then default to true.
Otherwise, default to false.  The intent of this is that if the author has
provided the information we require to build a bundle, we want to build it
by default; otherwise, we don't.  The bundle-info: field is the one mandatory
field to build a bundle, so it's appropriate to discriminate based on it.

  I envision that an app which is meant to build /only/ as a bundle would look
like this:

Executable NiftyApp
  if bundle
    bundle-info: Mac/Info.plist
    main-is: Mac/main.m
    other-modules: FrontEnd.Mac.Utilities
    includes: Mac/AppDelegate.h
    c-sources: Mac/AppDelegate.m
    data-dir: Mac/Resources
    data-files: Application.icns,
                Document.icns,
                MainMenu.xib
    frameworks: Cocoa
  other-modules: Network.Jabber,
                 Network.SASL
  build-depends: base >= 4.1 && < 5,
                 bytestring >= 0.9.1.4 && < 1,
                 utf8-string >= 0.3.6 && < 1,
                 direct-sqlite >= 1.1 && < 2

  Notice how this puts main-is: inside the conditional, so that the package
description is invalid if the condition is false.  This is how we express the
"only as a bundle" constraint.  In contrast, an app which can also build a
command-line version would look like this:

Executable NiftyApp
  if bundle
    bundle-info: Mac/Info.plist
    main-is: Mac/main.m
    includes: Mac/AppDelegate.h
    c-sources: Mac/AppDelegate.m
    data-dir: Mac/Resources
    data-files: Application.icns,
                Document.icns,
                MainMenu.xib
    frameworks: Cocoa
  else
    main-is: FrontEnd.Terminal.hs
  other-modules: Network.Jabber,
                 Network.SASL
  build-depends: base >= 4.1 && < 5,
                 bytestring >= 0.9.1.4 && < 1,
                 utf8-string >= 0.3.6 && < 1,
                 direct-sqlite >= 1.1 && < 2

  This provides an alternate main-is: value for building without a bundle,
so that it's valid both ways.  Notice the placement of the frameworks: flag;
in the first example, it's just for consistency, but in the second example it
actually makes a difference (not one which would keep the program from
running, but it's still good to only link against libraries we're actually
using).

  Also, it will be an error for a bundle-info: field to be present when the
bundle flag is false; that way, the approach I've used in these two examples
is more discoverable, since putting in bundle-info: without a conditional will
lead to a helpful message.

  So much for the proposal; on to implementation.  I recommend looking at
my code in the prototype (the cabal-app link, below, in particular the
function buildApp at the bottom of App.hs, linked separately for your
convenience) to see what the steps are.  Briefly, we start by removing any
stale copy of the bundle that's around; we can't in general know whether the
app itself has written to its own contents since we created it, so we have to
get rid of it completely to be sure we don't have metastability that would
lead to inconsistent ability to build.  Then we create the directory
hierarchy; write out a tiny file .app/PkgInfo that has to be there but can be
autogenerated; copy the info plist (not to be confused with PkgInfo) to
.app/Contents/Info.plist; compile .xibs to .nibs with output going to
.app/Contents/Resources/; and copy other data files to
.app/Contents/Resources/.

  So a full directory tree might look like:

    NiftyApp.app/
    NiftyApp.app/PkgInfo
    NiftyApp.app/Contents/
    NiftyApp.app/Contents/Info.plist
    NiftyApp.app/Contents/MacOS/
    NiftyApp.app/Contents/MacOS/NiftyApp
    NiftyApp.app/Contents/Resources/
    NiftyApp.app/Contents/Resources/Application.icns
    NiftyApp.app/Contents/Resources/Document.icns
    NiftyApp.app/Contents/Resources/MainMenu.nib

  Duncan spoke of wanting to partition this into "some simple build system
extensions (like knowing what to do with compiling .xib files) and then a
separate deployment phase".  He didn't mean deployment phase as in "cabal
install" but rather something new happening within but towards the end of
"cabal build".  Even though that's how my prototype does it, I think that
because of reusing --datadir and data-files:, in the real version a partition
is only possible to a very limited extent:  We can, and will, make the bundle
flag imply that --datadir is set to go within the bundle rather than in
/usr/local/share/ or wherever, but that's not sufficient, since installing
data files when building as a bundle is part of the build phase and not the
install phase (it's impossible to run the .app without copying the files into
their final places within it, so we haven't really built at all unless we do
that).

  Notice, by the way, that this whole setup conflates the author/builder
distinction a bit.  Normally (without bundles), the builder gets to pick
install paths.  Here, the author is picking them, and the builder only gets
to choose whether to accept those choices (build as a bundle) or not (build
as a non-bundle, if the author has supported that, which of course isn't
possible for GUI apps).  The reason this is an acceptable restriction is that
the final .app bundle is relocatable simply by moving it to anywhere the user
wants it, so although the builder is restricted in his initial choices, he
has more freedom ultimately.

  So - that's what I'm working on.  Since the prototype is already working,
I expect to have the real thing working in a day or two, and then hopefully
we can get it added.  Exciting stuff!


See:
  http://hackage.haskell.org/trac/hackage/ticket/583
  http://dankna.com/software/darcs/cabal-app/
  http://dankna.com/software/darcs/cabal-app/Distribution/App.hs
  http://developer.apple.com/library/mac/#documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html%23//apple_ref/doc/uid/10000123i-CH101-SW1


-- 
Dan Knapp
"An infallible method of conciliating a tiger is to allow oneself to
be devoured." (Konrad Adenauer)



More information about the cabal-devel mailing list