Build Simple Packages with Scripts

In a past post, I described how path_helper works. As an example, I mentioned the installer for Python 3 which runs a postinstall script that locates and modifies the current user’s shell profile file to add the Python 3 binary directory to the PATH.

Not only is modifying a user’s file a horrible practice, but it will not achieve the desired purpose when the user installing the package is ultimately not the user using the system. This setup happens fairly frequently in managed deployment workflows.

As described in the earlier post, macOS will add the contents of files in /etc/paths.d/ to all users’ PATHs. So, all we have to do is create a file with the path to the Python 3 binary directory in /etc/paths.d/. A perfect task for a simple installer package.

The steps to create such an installer are simple:

$ mkdir -p Python3PathPkg/payload
$ cd Python3PathPkg
$ echo "/Library/Frameworks/Python.framework/Versions/3.7/bin" > payload/Python-3.7
$ pkgbuild --root payload --install-location /private/etc/paths.d --version 3.7  --identifier com.example.Python3.path Python3Path-3.7.pkg
pkgbuild: Inferring bundle components from contents of payload
pkgbuild: Wrote package to Python3Path-3.7.pkg

This is not so hard. However, since the path to binary contains the major and minor version number, you will have to create a new version when Python 3 updates to 3.8 (and 3.9, etc…).

So, it makes sense to script the process. With a package this simple, you can create everything required to build the package (i.e. the payload folder with contents) from the script in a temporary directory and then discard it after building.

You can find my script at this Github repository.

Note: when you modify the PATH with path_helper, your additions will be appended. The Python 3 installer prepends to the PATH. This might lead to slightly different behavior, as the Python 3 behavior overrides any system binaries. If you want to prepend for every user, you have to modify the /etc/paths file.

There are a few other simple installers where this approach makes sense. I also made a script that builds a package to create the .AppleSetupDone file in /var/db to suppress showing the setup assistant at first boot. Since I was planning to use this with the startosinstall --installpackage option, this script builds a product archive, rather than a flat component package.

You could create this package once and hold on to it whenever you need it again, but I seem to keep losing the pkg files. The script allows you to easily re-build the package in a different format or sign it when necessary. Also, dealing with the invisible file is a bit easier when you just create them on demand.

The last example creates a single invisible file .RunLanguageChooserToo, also in /var/db/. This will show an additional dialog before the Setup Assistant to choose the system language. MacAdmins might want to have this dialog for the obvious reason, but it also allows for a useful hack. When you invoke the Terminal at the Language Chooser with ctrl-option-command-T it will have root privileges, which allows some interesting workflows.

With this package the creation of the flag file happens too late to actually show the chooser. So I added the necessary PackageInfo flags to make the installer require a restart. Note that startosinstall will only respect this flag with a Mojave installer, not with High Sierra.

These three scripts can be used as templates for many similar use cases. As your needs get more complex, you should move to pkgbuild scripts with a payload folder, munkipkg, or Whitebox Packages.

You can learn about the details of inspecting and building packages in my book: “Packaging for Apple Administrators”