Bundling

Once you have your application running, you'll want to create an application bundle that you (or your users) can drag to a convenient place and launch by double-clicking on it in Finder. If you can design the application so that it doesn't need files outside of the bundle, users will be able to install it by simply dragging it from a disk image that they download to whatever folder they like.

Almost wherever they like: Gtk+ applications typically depend heavily on environment variables, so the app bundle will have a shell script to set everything up (called a "launcher script"). The catch is that shell scripts tend to view spaces as delimiters between arguments, so paths with spaces cause trouble unless particular care is taken in writing the launcher script.

There are alternatives: Info.plist keys is perhaps the most straightforward as long as you need only to set variables with no scripting. You can also write your app to load the environment from another file or from the defaults system using GSettings. Python programs substitute a custom C program that links libpython, executes a python script to set up the environment, and then runs the bundled python program.

Gtk-OSX provides a tool, gtk-mac-bundler, to build the application bundle for you. You can clone the latest code with git from Gnome Gitlab.

What follows is a basic procedure for getting and using gtk-mac-bundler. More details may be found in the README provided with the source and in the comments in the example files (found in the examples folder).

How to use gtk-mac-bundler

Clone the repository:

$ git clone https://gitlab.gnome.org/GNOME/gtk-mac-bundler.git
$ cd gtk-mac-bundler
$ make install

This will install gtk-mac-bundler in ~/.local/bin (the same place as the gtk-osx install script puts jhbuild).

Next, make a directory to hold your bundle configuration files (we'll call this the "bundle directory") and copy the contents of ~/gtk-mac-bundler/examples to it.

Bundle File

Copy or rename gtk-demo.bundle to something more like yourappname.bundle. We'll call this file the "bundle file".

Now, edit the bundle file. This file tells gtk-mac-bundler what files are required to run your application, and enables it to find all the dependencies which need to be included in your application bundle. There are a lot of comments in the file itself, and more explanation is available in ~/gtk-mac-bundler/README.

Gtk-mac-bundler can find dependencies which are linked at build-time, using the shell command "otool -L". It cannot find dependencies which are loaded with dlopen (a.k.a g_module_load), so you must include those explicitly in the bundle file with a <binary> tag. (Don't use a <data> tag or gtk-mac-bundler won't be able to find the shared libraries that the module depends on.)

Icon file

You will need to create an icon for your application. OSX doesn't use regular image files for icons. Instead, the .icns file format is used, which contains icons of up to four (five in Snow Leopard) standard sizes. You'll want to build one of these .icns files for your app and point to it with both Info.plist and a <data> entry in your bundle file. Icon Composer (/Developer/Applications/Utilities/Icon Composer.app) is the program to use for this for XCode before 4.4; see High Resolution Guidelines for details about how to create a .icns file with XCode 4.4 or later. If your application already has graphics files in the appropriate resolutions you can just drag them into the appropriate boxes in Icon Composer and save. You don't need to have all of the sizes filled for the icons to work (this is particularly true of the 512x512 icon size first provided in Snow Leopard).

Info.plist

Next, edit Info.plist to refer correctly to your application and the application icon you created. This file is used by Mac OS X when actually launching your application. The basic Info.plist entries are pretty obvious, full documentation is at Apple Documentation).

Recent experimentation has revealed that unless it is an executable binary, the file that CFBundleExecutable names is ignored and launchd will attempt to open a file having the same name as the bundle: If the bundle is Foo.app, the launcher script must be Foo or foo.

Gtk-mac-bundler will use CFBundleName to name the bundle if it's present; this allows CFBundleExecutable to be lowercase and CFBundleName to be capitalized if your program has built-in scripts or references which need to call the launcher script and need it to be lower-cased as is normal for Unix commands. Otherwise it uses the CFBundleExecutable key to name both the bundle and the launcher script.

launcher.sh

A template launcher.sh is included in the example. Either add to this any other shell commands you need to get your application ready to run, or call other shell scripts from it. If you do use other shell scripts, be sure include them in the bundle file. If you're using Info.plist to set up the environment or your program sets up its environment internally you don't need launcher.sh; just comment out or delete the <launcher-script> element from the bundle file.

Run the bundler

Assuming that your bundle description file is called yourappname.bundle, you should now be ready to run gtk-mac-bundler using the the following steps:

$ jhbuild shell
$ export PATH=$PREFIX/bin:~/.local/bin:$PATH
$ gtk-mac-bundler yourappname.bundle

Gtk-mac-bundler copies all of the files you indicated in your bundle file, and also pulls in the dependencies it can find, and it adjusts the install paths to reflect the new locations. Note that some libraries and applications (dbus and Gconf are particularly common) hard-code paths during build and you may find that you need to keep at least some of your installation directory in place. You can finesse this problem by installing the required files (often they're text configuration files) into the bundle and having the launcher script check for and if not present or correct create a symlink from the bundle Resources directory (e.g., Yourappname.app/Contents/Resources) to the installation directory. If you do this, you probably want to set up your prefix when you build to somewhere outside of your home directory. Apple recommends /Library for application data and both /opt and /usr/local are traditional in Unix and Linux. Do be careful with both of these, as it's common for autotools-based programs to build in /usr/local by default, and MacPorts uses /opt/local for its own purposes.

Gtk-mac-bundler reads the Info.plist to determine the app bundle's name, and puts it in the folder indicated by the <destination> tag; in the examples, this is ~/Desktop.

Bundling Python apps

Gtk-mac-bundler supports python apps using both the old, deprecated, no-longer-maintained https://developer.gnome.org/pygtk/stable/ PyGTK library and the current PyGObject https://wiki.gnome.org/Projects/GObjectIntrospection GObject-Introspection-based means of building Gtk applications in Python.

PyGTK programs are a bit trickier to bundle, because a python program isn't a mach-o binary, so it can't go in the <main-binary> element. At the moment you can make libpygtk.dylib the main-binary. Add your python modules to your bundle with <data> tags and add their location to $PYTHONPATH in your launcher script.

There is an example which bundles pygtk-demo. Look at examples/pygtk-demo.bundle, examples/Info-pygtk-demo.plist, examples/pygtk-demo.sh (the launcher script), and examples/pygtk-demo (a wrapper python program which does the python part of setting up and calling pygtk-demo.py). Yes, the last one could be better named.

PyGObject programs can be packaged the same way as PyGTK ones, you just need to add the typelibs as binaries and the gir files as data to your bundle. But you'll get better results using the python-launcher. Compile examples/python-launcher.c while inside your jhbuild shell and name the output to match your program name-launcher, placing it in your project directory (the same one that has your bundle file). This will become the executable. Copy examples/gtk_launcher.py to your project directory and modify it to suit your program's needs. Add the following elements to your bundle file:

  <main-binary>
     ${prefix}/bin/gtk-launcher
  </main-binary>

  <data dest="${bundle}/Contents/Resources">
    ${project}/gtk_launcher.py
  </data>

Changing the names of the files as appropriate. Gir files need special handling because meson-built libraries have no path in the Gir so add this element:

  <gir>
    ${prefix}/share/gir-1.0/*.gir
  </gir>

Don't explicitly add the typelibs, gtk-mac-bundler will rebuild them after processing the Gir files.

Gtk-mac-bundler isn't aware of Python module dependencies (as opposed to, for example, modulefinder), so any modules which aren't installed somewhere in $PREFIX/lib/pythonx.x/ must be manually added to your bundle file in <data> tags.

Code Signing

Since MacOSX 10.8 "Mountain Lion" Macs have used Gatekeeper to control what sorts of applications can be installed. The default behavior is that on the first run of a downloaded application it will refuse to open with a double-click in Finder unless the app has been signed by a trusted developer. Again by default, "trusted" means registered with Apple and using Apple-issued certificates to do the signing. All of this can be overridden by knowledgeable users, but bundlers may wish to avoid making their users apply those overrides. Read the Code Signing Guide for details on the process and how to obtain certificates. One note: Unless you enjoy authenticating, install your certificates in the Login keychain rather than the System one that Apple recommends.

Once you've obtained your certificates and made your bundle, you can sign it with:

codesign -s "certificate name" /Path/to/Foo.app/Contents/MacOS/*

Codesign is available on all versions of MacOS from 10.5 "Leopard" on, but for a signed app to be notarized by Apple (see below), you must run codesign on a mac running 10.15 or later. Also, you will need to sign all nested binaries. You can do that with the --deep argument to codesign, but Apple recommends signing each individually. gtk-mac-bundler will do that for you if you set $APPLICATION_CERT=<certificate name> in your environment. Since gtk-mac-bundler needs to be run in a jhbuild shell, you can easily do this from .jhbuildrc-custom.

As originally designed gtk-mac-bundler uses a shell script launcher to set up the environment before launching the executable. This causes trouble for code-signing on Mac OS X 10.11.3 and later; on previous versions it does work even though Apple has long recommended not having script files in Contents/MacOS and not signing script files. As of August 2016 gtk-mac-bundler includes an example C program, examples/python-launcher.c, which initializes a bundled python library and passes control to a python script indicated in the bundle file. The python script can set almost all of the necessary environment variables; an example is provided as examples/gtk_launcher.py.

"Almost all" because it does no good to set DYLD_LIBRARY_PATH, DYLD_FALLBACK_PATH, or LD_LIBRARY_PATH if your program relies on dlopen() path resolution. dlopen() is a call into the dynamic loader and that was initialized including reading the environment, at program launch. Consequently all dlopen() calls need to use rpaths beginning with @executable_path/../Resources; the link editor created those rpaths with $PREFIX at build time so they need to be converted. gtk-mac-bundler has always transformed all files indicated with <binary> elements and found by running otool -L on those files. Since September 2016 it also transforms the paths in Gir and Typelib files indicated in a <gir> tag so that bound interpreted languages like Python will find libraries linked via gobject-introspection. Any other binding mechanism will require modification outside of gtk-mac-bundler to ensure that its paths can be found without setting DYLD_LIBRARY_PATH.

Notarizing

Beginning with MacOS 10.14.5 for newly-registered developers and with 10.15 betas for everyone, Apple requires that application bundles be notarized. That document is Xcode-oriented; another page provides some rather skeletal command line instructions. Some notes follow.

Notarizing requires Xcode 10, first provided for MacOS 10.14, to codesign and submit the application to Apple for notarizing. That doesn't mean that you have to build your app on MacOS 10.14, just that you need a MacOS 10.14 or newer system to sign and submit it. Apple's documentation says that you need to link against at least MacOS.10.9.sdk in order for your app to be notarized and I have successfully had notarized an app built on MacOS 10.9 with Xcode 6.3 and codesigned on MacOS 10.14 with Xcode 10. Although Apple's documentation says that versions as old as 10.12 can be used for signing I tested that and the notarization was rejected for not having the hardened runtime specified.

Codesign's --deep option doesn't do a good enough job of finding everything that needs to be signed, and gtk-mac-bundler codesigning routine doesn't apply all of the required flags so that it can continue to work on older MacOS versions. So once you've bundled your app you need to re-sign it as follows:

find My.app -name *.dylib -exec codesign -f --timestamp -i org.mine.My.app -s "My Developer Cert" {} \;
find My.app -name *.so -exec codesign -f --timestamp -i org.mine.My.app -s "My Developer Cert" {} \;
codesign -f --timestamp --options runtime -i org.mine.My.app -s "My Developer Cert" My.app

If your bundle includes additional executables that it calls either with fork/exec or by shelling out you'll need to sign each one using a similar command line before signing your app. For example, if your app uses the popular graphviz graphing package by shelling out to its dot program, you'd sign that in addition to My.app with

codesign -f --timestamp --options runtime -i org.mine.My.app -s "My Developer Cert" My.app/Contents/MacOS/dot

Be sure to test your signed bundle now on a 10.14 or newer MacOS: The hardened runtime may impose restrictions that cause it to fail. Exercise all plugins and auxiliary executables as well. If it writes bytecode into the bundle make a copy first so that you don't mess up the signature's checksums. If there are failures you may need to relax the runtime hardening with #Entitlements.

To submit your app to Apple for notarization you need to archive it the same way that you would for distribution. Apple accepts UIDL disk images (aka .dmg files made with Disk Utility) or Zip files. If you use a disk image sign it first:

codesign -i org.mine.My.app -s "My Developer Cert" My-2.3.4.dmg

Before you can submit to Apple you'll need to get a special "per application" password following https://support.apple.com/en-us/HT204397 Apple's instructions.

Submit the archive to Apple with

xcrun altool --notarize-app --primary-bundle-id org.mine.My.app.2.3.4-1 -u "me" -p "per-app-password" My-2.3.4.dmg

The primary bundle id is just a string to help you associate Apple's result email with which submission it goes with. Any string will do. Mind, xcrun altool will provide a UUID when it finishes uploading and that will be in the mail too, so the primary bundle id is a bit redundant, but it's required.

You can check the status of your notarization with

xcrun altool --notarization-info UUID -u "me" -p "per-app-password"

at any time, but Apple will send you an email telling you whether your app passed or failed. If it failed use xcrun altool --notarization-info to get the URL to the error log; you can open it in your browser or fetch it with curl as you prefer. You may find Apple's troubleshooting page helpful in diagnosing the errors.

Once Apple approves your app there's one last step: "Stapling" it.

If you submitted a signed .dmg just staple it directly:

xcrun stapler staple My-2.3.4.dmg

If you submitted a Zip you need to staple the app

xcrun stapler staple My.app

and put it in a new Zip for distribution.

Entitlements

Sometimes your application needs to loosen the chains of a hardened runtime a little in order to work correctly. Apple provides some particular exceptions called entitlements for that purpose. For example, if your program uses an interpreter like Python it will need to put its interpretation results in memory, and not being a native Apple program it probably doesn't know how to sign that memory. That means that it needs the com.apple.security.cs.allow-unsigned-executable-memory entitlement. To create that you need to make a plist file outside of your app bundle and point codesign at it when signing executables. Note that all executables in the bundle need the same entitlements, but shared libraries and loadable modules do not. Here's a sample plist:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
  </dict>
</plist>

and the modified codesign statement to use it:

codesign -f --timestamp --options runtime --entitlements "/path/to/entitlements.plist" -i org.mine.My.app -s "My Developer Cert" My.app

You may find on the web examples of entitlements.plist that include entitlements that are intended not to be enabled marked with <false/> instead of <true/>. xcrun altool rejected my submissions with that form as having an invalid signature so I recommend including only the entitlements actually being requested.

Support

Bugs and other issues should be reported on the gtk-mac-bundler issue tracker; propose updates and fixes as merge requests at gtk-mac-bundler merge requests. Support questions, general suggestions, and so on can be posted to discussion area at the maintainer's Github repo.

Projects/GTK/OSX/Bundling (last edited 2023-05-22 15:11:21 by jralls)