################# OSX flat packages ################# OSX flat packages are single installer files with ``.pkg`` file extensions. They appear to have originated with OSX 10.5. You can't install these files on OSX < 10.5. The previous packaging formats were *bundles* rather than single files: bundle A structured directory hierarchy that stores files in a way that facilitates their retrieval (see glossary in the `software delivery legacy guide`_) See :doc:`legacy_package_redux` for details. As far as I know there is no Apple document describing the flat package format. You may find these other pages useful as background: * A `MacTech flat package article`_; * An introduction to the `flat package format`_; * A guide on `unpacking flat packages`_ manually; * A comprehensive `stackoverflow package building answer`_. ******************************* About the examples on this page ******************************* I wrote this page in reStructuredText (reST_) for Sphinx_, with some custom Sphinx extensions. The extensions pick up the code fragments in this page and run them on my laptop as part of the build process for the web pages. They also write out the example files listed on this page. The combination means that, at the time I last built the website, I confirmed that the code ran on my laptop, and gave the output you see here. ************************ Flat packages in general ************************ .. runblock:: :hide: rm -rf pkg_examples mkdir pkg_examples sudo pkgutil --forget my.fake.pkg sudo pkgutil --forget com.apple.xcode.dsym.aprogram sudo pkgutil --forget com.apple.xcode.dsym.aprogram2 sudo rm -rf /tmp/my_*.log /tmp/dir1 /tmp/dir2 /tmp/aprogram*.dSYM Flat packages are ``xar`` archives (see the ``xar`` man page). .. pkgrun:: :hide: pkgbuild --nopayload --identifier my.pkg my.pkg To see the contents of a flat package, the most convenient command is ``pkgutil --expand``, as in: .. pkgrun:: pkgutil --expand my.pkg an_output_dir This will unpack product archive (meta-packages) and their component packages, and allows you to futz with the directory contents in ``an_output_dir`` before reconstituting the package with: .. pkgrun:: pkgutil --flatten an_output_dir my.pkg .. runblock:: :hide: rm -rf pkg_examples On OSX, ``tar`` will unpack xar archives, and will automatically detect that the archive is in xar format with ``tar xf my.pkg``, if you really want to do the unpacking without ``pkgutil``. *********************** Different package types *********************** Flat packages can be of type: * *product archive* |--| a meta-package format containing one or more *component packages* with an XML file specifying the behavior of the installer, including system and volume requirement checks giving informative error messages. See the ``productbuild`` man page for more detail. * *component package* |--| an installer to install one component. Generally included as part of a *product archive* but can be used as an installer in its own right. A component package installer cannot control the behavior of the installer, but can abort the install or raise a post-install error via ``preinstall`` and ``postinstall`` scripts. ******************* Component installer ******************* A component installer goes through these stages: #. Runs ``preinstall`` script if present. An exit code other than zero aborts the installation. #. Writes payload to one or more locations on the target volume #. Runs ``postinstall`` script if present. An exit code other then zero gives an error message. We start with some component packages that only have scripts and no payload, to show how they work. Component installer scripts =========================== .. runblock:: mkdir pkg_examples cd pkg_examples mkdir scripts .. pkgwrite:: :language: bash # file: scripts/preinstall #!/bin/sh echo "Running preinstall" > /tmp/my_preinstall.log exit 0 # all good .. pkgwrite:: :language: bash # file: scripts/postinstall #!/bin/sh echo "Running postinstall" > /tmp/my_postinstall.log exit 0 # all good The scripts need to be executable: .. pkgrun:: chmod u+x scripts/* Each package needs a package *identifier* to identify it to the database of installed packages on the target system. You can list the recorded installed packages on target ``/`` with:: pkgutil --pkgs / Build the package with a fake identifier: .. pkgrun:: pkgbuild --nopayload --scripts scripts --identifier my.fake.pkg my_package.pkg Install the package: .. pkgrun:: sudo installer -pkg my_package.pkg -target / Check the scripts ran by looking for output: .. pkgrun:: cat /tmp/my_preinstall.log .. pkgrun:: cat /tmp/my_postinstall.log Exit code other than zero causes the installer to give an error message: .. pkgwrite:: :language: bash # file: scripts/postinstall #!/bin/sh exit 1 # not so good .. pkgrun:: pkgbuild --nopayload --scripts scripts --identifier my.fake.pkg my_package.pkg .. pkgrun:: :allow-fail: sudo installer -pkg my_package.pkg -target / Fixed if the script is not run: .. pkgrun:: rm scripts/postinstall pkgbuild --nopayload --scripts scripts --identifier my.fake.pkg my_package.pkg .. pkgrun:: sudo installer -pkg my_package.pkg -target / There are some useful environment variables available to the ``preinstall``, ``postinstall`` scripts: .. pkgrun:: # Get the default environment when not in preinstall sudo env > /tmp/my_sudo_envs.log .. pkgwrite:: :language: bash # file: scripts/preinstall #!/bin/sh env > /tmp/my_preinstall_envs.log echo "Positional arguments" $@ >> /tmp/my_preinstall_envs.log exit 0 .. pkgrun:: chmod u+x scripts/preinstall pkgbuild --nopayload --scripts scripts --identifier my.fake.pkg my_package.pkg .. pkgrun:: sudo installer -pkg my_package.pkg -target / Here are the new environment variables inserted by the installer: .. runblock:: :allow-fail: diff /tmp/my_sudo_envs.log /tmp/my_preinstall_envs.log --old-line-format="" --unchanged-line-format="" These appear to be a superset of the environment variables listed for the old bundle installers at `install operations`_. No payload, no receipt ---------------------- These "scripts only" installers have no payload, and write no package receipt to the package database: .. runblock:: :allow-fail: pkgutil --pkgs / | grep my.fake.pkg Payload ======= A flat-package component installer can install one or more bundles to given output locations on the target filesystem. There are two ways of specifying payload with target location: * Using a *destination root*, using ``--root`` flag to ``pkgbuild``. This analyzes a given directory ``root-path`` taking this directory to represent the root ``/`` directory of the target system. The installer will copy each directory at ``root-path`` to the target system at the same relative filesystem location. * By individual bundle *component*, using ``--component`` flag to ``pkgbuild``. Specify directories to copy, and give their output locations separately using the ``--install-location`` flag. .. _destination-root: Destination root ---------------- .. pkgrun:: mkdir -p my_root/tmp/dir1 my_root/tmp/dir2 echo "Interesting info" > my_root/tmp/dir1/file1 echo "Compelling story" > my_root/tmp/dir2/file2 tree -a my_root .. pkgrun:: pkgbuild --root my_root --identifier my.fake.pkg my_package.pkg .. pkgrun:: :hide: sudo rm -rf /tmp/dir1 /tmp/dir2 my_file Do the install: .. pkgrun:: sudo installer -pkg my_package.pkg -target / .. pkgrun:: cat /tmp/dir1/file1 .. pkgrun:: cat /tmp/dir2/file2 This install did write a package receipt: .. pkgrun:: pkgutil --pkgs / | grep my.fake.pkg .. _explicit-component: Explicit component(s) --------------------- We point to a bundle to install and (usually) specify the install location. In this case the bundle must be a properly formed bundle of some sort. I'll make a debug symbols bundle by doing a tiny compilation with clang: .. pkgrun:: echo "int main(int argc, char** argv) { }" > aprogram.c clang -g aprogram.c -o aprogram tree aprogram.dSYM .. pkgrun:: :hide: sudo rm -rf /tmp/aprogram.dSYM /tmp/aprogram2.dSYM Build, install the package: .. pkgrun:: pkgbuild --component aprogram.dSYM --install-location /tmp my_package.pkg .. pkgrun:: sudo installer -pkg my_package.pkg -target / The installer has written the expected output files: .. pkgrun:: tree /tmp/aprogram.dSYM You can add more than one bundle to a single component installer. Here I make another bundle with a different name: .. pkgrun:: echo "int main(int argc, char** argv) { }" > aprogram2.c clang -g aprogram2.c -o aprogram2 tree aprogram2.dSYM When you add more than one component, you need to give a package identifier, because ``pkgbuild`` will not know which component to get an identifier from. .. pkgrun:: pkgbuild --component aprogram.dSYM --install-location /tmp \ --component aprogram2.dSYM --install-location /tmp \ --identifier my.fake.pkg \ my_package.pkg .. pkgrun:: sudo installer -pkg my_package.pkg -target / .. pkgrun:: tree /tmp/aprogram.dSYM .. pkgrun:: tree /tmp/aprogram2.dSYM *************** Product archive *************** Component packages give a very basic default install. Product archives allow you to wrap one or more component installs with an install configuration that includes: * custom welcome, readme, license and conclusion screens; * custom system requirement and volume requirement checks with informative error messages using Javascript code; * ability to customize choices of component packages to install using Javascript code. An XML file named ``Distribution`` specifies these options (see `distribution definition file`_). Building product archives with ``productarchive`` ================================================= ``productarchive`` has five modes of action: Archive for "in-app" content ---------------------------- This appears to be something to do with the App Store. Use the ``--content`` flag for this one. Archive from destination root ----------------------------- Build an archive from a directory where the paths of the files relative to the ``--root`` directory give the output install location on the target volume (see :ref:`destination-root`). .. pkgrun:: productbuild --root my_root my_archive.pkg Archive from component bundle(s) or package(s) ---------------------------------------------- Build a product archive from one or more components by passing: * bundle path(s) with one or more uses of the ``--component`` flag (see :ref:`explicit-component`) and / or; * package paths(s) with one or more uses of the ``--package`` flag. Build product archive with two component bundles: .. pkgrun:: productbuild --component aprogram.dSYM /tmp --component aprogram2.dSYM /tmp my_archive.pkg Make bundles into component packages: .. pkgrun:: pkgbuild --component aprogram.dSYM --install-location /tmp aprogram.pkg .. pkgrun:: pkgbuild --component aprogram2.dSYM --install-location /tmp aprogram2.pkg Build product archives from the component packages: .. pkgrun:: productbuild --package aprogram.pkg --package aprogram2.pkg my_archive.pkg Build a ``Distribution`` file from component bundle(s) / packages(s) -------------------------------------------------------------------- This builds a template ``Distribution`` XML file by analyzing one or more components in the form of bundles or ``.pkg`` files. Use the ``--synthesize`` flag. Analyze two component bundles to give a ``Distribution`` file: .. pkgrun:: productbuild --synthesize --component aprogram.dSYM --component aprogram2.dSYM Distribution-1 Analyze two component packages to give a ``Distribution`` file: .. pkgrun:: productbuild --synthesize --package aprogram.pkg --package aprogram2.pkg Distribution-2 .. pkgrun:: cat Distribution-2 Archive from ``Distribution`` file ---------------------------------- Build a product archive from a pre-existing ``Distribution`` file, using the ``--distribution`` flag. The ``Distribution`` file refers to pre-existing component ``.pkg`` files. If these are not in the current directory, you can give paths to find ``.pkg`` files with the ``--package-path`` flag. .. pkgrun:: productbuild --distribution Distribution-2 my_archive.pkg ************************************* Customizing the ``Distribution`` file ************************************* See: `distribution definition file`_ and `installer Javascript reference`_. Adding system and volume checks =============================== .. pkgwrite:: :language: xml # file: distro_check.xml aprogram.pkg aprogram2.pkg .. pkgrun:: productbuild --distribution distro_check.xml my_archive.pkg .. pkgrun:: sudo installer -pkg my_archive.pkg -target / Now make the volume check fail: .. pkgrun:: :allow-fail: sudo rm /tmp/my_postinstall.log sudo installer -pkg my_archive.pkg -target / The volume check gets run on every candidate volume; each volume that passes the check is eligible for the install. Debugging ``Distribution`` Javascript with ``system.log`` ========================================================= Getting the Javascript right can be tricky because it is running in a highly specific environment. ``system.log()`` can help. .. pkgwrite:: :language: xml # file: distro_debug.xml aprogram.pkg aprogram2.pkg .. pkgrun:: productbuild --distribution distro_debug.xml my_archive.pkg Use the ``-dumplog`` flag to the installer to see the log. .. pkgrun:: sudo installer -dumplog -pkg my_archive.pkg -target / Adding custom script checks =========================== You can add custom executable scripts or binaries to run via the Javascript functions such as the volume check and the system check. You first have to enable the ``allow-external-scripts`` option in the Distribution file XML:: This will cause the installer GUI to ask if you want to allow an external script to check if the software can be installed. You can then call external programs via the Javascript ``system.run()`` function. The programs you call with ``system.run`` either need to be on the system PATH that the installer uses, or provided in the installer, usually via the ``--scripts`` flag to ``productbuild``. Here's an example of a command already on the path: .. pkgwrite:: :language: xml # file: distro_system_run.xml aprogram.pkg aprogram2.pkg .. pkgrun:: productbuild --distribution distro_system_run.xml my_archive.pkg .. pkgrun:: :allow-fail: sudo installer -pkg my_archive.pkg -target / Check the script ran: .. pkgrun:: cat /tmp/my_system_run.log Here's an example of a custom script: .. pkgwrite:: :language: xml # file: distro_custom_run.xml aprogram.pkg aprogram2.pkg Make a directory to contain the test script: .. pkgrun:: mkdir archive_scripts Use test script to look for any interesting environment variables available to the custom script: .. pkgwrite:: :language: bash # file: archive_scripts/my_test_cmd.sh #!/bin/sh env > /tmp/my_custom_envs.log echo "Positional arguments" $@ >> /tmp/my_custom_envs.log exit 2 The script must be executable: .. pkgrun:: chmod u+x archive_scripts/my_test_cmd.sh Build the product archive with ``--scripts`` flag: .. pkgrun:: productbuild --distribution distro_custom_run.xml --scripts archive_scripts my_archive.pkg .. pkgrun:: :allow-fail: sudo installer -pkg my_archive.pkg -target / Check the environment variables available to the custom script. We already have ``/tmp/my_sudo_envs.log`` with the standard environment variables available as root, so look for the difference: .. pkgrun:: :allow-fail: diff /tmp/my_sudo_envs.log /tmp/my_custom_envs.log --old-line-format="" --unchanged-line-format="" There do not seem to be any environment variables to tell us where the installer ``.pkg`` file is. Here we can work out which volume the installer is installing to with the ``SUDO_COMMAND`` variable, but this will only work for command line installs - the installer GUI does not set this variable. Customizing the installer pages =============================== See `distribution definition file`_ for details. You can customize these pages: * Welcome (```` element) * Readme (````) * License (````) * Conclusion (````) To do this, you specify the filename containing the custom text, and the format of the file. For example:: The file should be in the ``Resources/.lproj`` directory of the installer ``.pkg``, where ```` is a language like "English", "French" or "Spanish", or shorthand for these such as "en", "fr", "es". You can provide a ``Resources`` directory with the ``--resources`` flag to ``productbuild``. .. pkgrun:: mkdir -p my_resources/English.lproj .. pkgwrite:: :language: html # file: my_resources/English.lproj/welcome.html Install my_archive Welcome to the my_archive installer

This installer will install my_archive on your computer. .. pkgwrite:: :language: xml # file: distro_welcome.xml aprogram.pkg aprogram2.pkg We use the ``--resources`` flag to add the resources directory: .. pkgrun:: productbuild --distribution distro_welcome.xml --resources my_resources my_archive.pkg The welcome won't show up in the command line install, so you need to run this installer via the GUI to see the new text. I used an HTML file here, and that works as expected, for me at least. There are many installers that use ``.rtf`` files instead of HTML, but I heard a rumor that ``productbuild`` does not deal with these correctly. If you want to use rich text format files with ``productbuild``, you might have to do some post-processing of your installer with |--| ``pkgutil --expand my_archive.pkg out_dir``; (futz); ``pkgutil --flatten out_dir my_archive.pkg``. .. include:: links_names.inc