Installation Q & A
Q
I used to be able to define preflight, preinstall and preupgrade scripts with bundle packages. Why is it not possible for flat packages?
A
Well, it's still possible but since the old script hierarchy is gone, you will need to add a bit of extra work.

Scripts reality

With bundle packages, you can use the following scripts:
Script NameRun IfRun When
preflightRun every time.Run before the payload has been installed and before the other scripts.
preinstallRun if the package is installed for the first time on the system.Run before the payload is installed.
preupgradeRun if the package has already been installed on the system.Run before the payload is installed.
postinstallRun if the package is installed for the first time on the system.Run after the payload has been installed.
preupgradeRun if the package has already been installed on the system.Run after the payload has been installed.
postflightRun every time.Run after the payload has been installed and after the other scripts.
Installer.app runs either the preinstall/postinstall or preupgrade/postupgrade scripts depending on the presence of the package in the receipts of the system.

With flat packages, you can only use the following scripts:
Script NameRun ifRun when
preinstallRun every time.Run before the payload has been installed.
postinstallRun every time.Run after the payload has been installed.

So, basically, the preflight and postflight script have respectively become the preinstall and postinstall scripts. And there's apparently no way to run a specific script depending on the fact the payload is being installed for the first time or being upgraded.

Actually, there is a way. It just requires us to detect ourself when the package is being installed for the first time or being upgraded. And then we can decide to launch a specific upgrade or install script from the preinstall or postinstall script of the flat package.

Figuring out whether a package has already been installed or not

Note: To avoid confusions between the flat package and bundle package preinstall/postinstall scripts, we will use pre-installation and post-installation to describe the preinstall and postinstall scripts for flat package.

Apple provides on every system a command line tool named pkgutil(1) which shall be used when you want to get information about packages in the receipts data base.

The --pkgs option of this tool lists all the packages whose identifier matches a regular expression:

  • If a package has already been installed, the identifier should be in the list.
  • If it has not been installed yet, it will not be in the list.
If we want to know whether the package "com.mycompany.pkg.mypackage" has been installed, we can run the following script:

The --volume option of this tool lets us specify which volume should be inspected.

/usr/sbin/pkgutil --volume / --pkgs=com.mycompany.pkg.mypackage

If the flat package has not been installed yet on the startup volume, the output will be empty.

Writing custom pre-installation and post-installation scripts

So we can use the solution described above in the pre-installation script of a flat package to either run a preinstall or preupgrade script as we could with bundle packages.

Note: For the --volume argument, we will use the fact that the target volume of an installation is passed as $3 (which we will surround with double-quotes to deal with volumes that have spaces in their names).

#!/bin/sh

foundPackage=`/usr/sbin/pkgutil --volume "$3" --pkgs=com.mycompany.pkg.mypackage`

if test -n "$foundPackage"; then

    # It's an upgrade

    /bin/sh ./preupgrade.sh

else

    # It's a clean install

    /bin/sh ./preinstall.sh

fi

exit 0
pre-installation.sh

and the post-installation script would then look like:
#!/bin/sh

foundPackage=`/usr/sbin/pkgutil --volume "$3" --pkgs=com.mycompany.pkg.mypackage`

if test -n "$foundPackage"; then

    # It's an upgrade

    /bin/sh ./postupgrade.sh

else

    # It's a clean install

    /bin/sh ./postinstall.sh

fi

exit 0
post-installation.sh

Then we just need to add the preinstall.sh, preupgrade.sh, postinstalll.sh, postupgrade.sh scripts to the Resources of the flat package.

Mission accomplished.

Improving the scripts

Well, the mission is not completely accomplished.

There are 2 issues with this solution:

  • Issue #1:

    This is a small one. If we want to re-use this solution in other packages or we need to change the identifier of the package for whatever reason, we will need to edit the pre-installation.sh and post-installation.sh scripts too.

    There's a solution for that. When the pre-installation and post-installation scripts are run by the installation engine, some environment variables specifically related to the installation process are set. In this case, we are glad to notice that the INSTALL_PKG_SESSION_ID environment variable is set to the identifier of the flat package being currently installed.

    So, we just need to replace com.mycompany.pkg.mypackage in the pre-installation.sh and post-installation.sh scripts with $INSTALL_PKG_SESSION_ID.

  • Issue #2:

    This is a major one when your package needs to work on Mac OS X v10.5.x.

    Let's say you are installing your package for the first time and you have used the solution described above.

    Here's what happens when you install your package on Mac OS X v10.6 or later:

    1. The /usr/sbin/pkgutil --pkgs=com.mycompany.pkg.mypackage command is invoked from within the pre-installation.sh script and it returns nothing. So you can decide that this is a clean install
    2. The pyaload gets installed.
    3. The /usr/sbin/pkgutil --pkgs=com.mycompany.pkg.mypackage command is invoked again, this time from within the post-installation.sh script. Again, it returns nothing. So you can decide that this is a clean install.

    Here's what happens you install when your package on Mac OS X v10.5.x:

    1. The /usr/sbin/pkgutil --pkgs=com.mycompany.pkg.mypackage command is invoked from within the pre-installation.sh script and it returns nothing. So you can decide that this is a clean install.
    2. The pyaload gets installed.
    3. The /usr/sbin/pkgutil --pkgs=com.mycompany.pkg.mypackage command is invoked again, this time from within the post-installation.sh script. It will return the identifier of the package. So you will consider that the installation is an upgraded. Wrong.

    What happens is that on Mac OS X v10.5.x, the receipts data base is upgraded after the payload has been installed. While on Mac OS X v10.6 and later, the receipts data base is upgraded at least after the post-installation script is run.

    Here's a solution to this (you can probably come up with other solutions):

    • From the pre-installation.sh script, if the installation is run on Mac OS X v10.5.x, we will write a cookie on disk to indicate whether it's a fresh install or an upgrade.
    • From the post-installation.sh script, if the installation is run on Mac OS X v10.5.x, we will check the cookie to be able to figure out whether it's actually a fresh install or an upgrade.

    Tricks we use:

    • To detect the installation is run on Mac OS X v10.5.x, we do not check the OS version. We check for the existence of the SHARED_INSTALLER_TEMP env var. It exists only on Mac OS X v10.6 and later.
    • Since the pre-installation.sh and post-installation.sh scripts can be run as root, we would like to write the cookie to a secure location (and not one that can be pre-determinated easily). For that, we will use the INSTALLER_TEMP env var that provides a random temporary folder.

Once we have fixed these 2 issues, the scripts look like this:

#!/bin/sh

foundPackage=`/usr/sbin/pkgutil --volume "$3" --pkgs=$INSTALL_PKG_SESSION_ID`

if test -n "$foundPackage"; then

    # It's an upgrade

    /bin/sh ./preupgrade.sh

else

    # It's a clean install

    ## For Mac OS X 10.5 ##

    if [ -z "$SHARED_INSTALLER_TEMP" ]; then

        /bin/echo "INSTALL" > $INSTALLER_TEMP/.install.$INSTALL_PKG_SESSION_ID

    fi

    ## End For Mac OS X 10.5 ##

    /bin/sh ./preinstall.sh

fi

exit 0
pre-installation.sh

and
#!/bin/sh

foundPackage=`/usr/sbin/pkgutil --volume "$3" --pkgs=$INSTALL_PKG_SESSION_ID`

## For Mac OS X 10.5 ##

if [ -z "$SHARED_INSTALLER_TEMP" ]; then

    if [ -f $INSTALLER_TEMP/.install.$INSTALL_PKG_SESSION_ID ]; then

        foundPackage=""

        /bin/rm $INSTALLER_TEMP/.install.$INSTALL_PKG_SESSION_ID

    fi
fi

## End For Mac OS X 10.5 ##

if test -n "$foundPackage"; then

    # It's an upgrade

    /bin/sh ./postupgrade.sh

else

    # It's a clean install

    /bin/sh ./postinstall.sh

fi

exit 0
post-installation.sh

This solution has been successfully tested on Mac OS X 10.5.7, 10.5.8, 10.6.8, 10.7.5, 10.8.2 and 10.8.3.

Sample Project

Revision History
06/26/13Added the --volume "$3" arguments as suggested by Paul Cook and Per Olofsson.

Site Map

Copyright 2013 Stéphane Sudre. All rights reserved.