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

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/Concept... -- Dan Knapp "An infallible method of conciliating a tiger is to allow oneself to be devoured." (Konrad Adenauer)
participants (1)
-
Dan Knapp