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 OSX legacy packaging 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¶
Flat packages are xar
archives (see the xar
man page).
To see the contents of a flat package, the most convenient command is
pkgutil --expand
, as in:
$ 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:
$ pkgutil --flatten an_output_dir my.pkg
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
andpostinstall
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¶
$ mkdir pkg_examples
$ cd pkg_examples
$ mkdir scripts
scripts/preinstall
#!/bin/sh
echo "Running preinstall" > /tmp/my_preinstall.log
exit 0 # all good
scripts/postinstall
#!/bin/sh
echo "Running postinstall" > /tmp/my_postinstall.log
exit 0 # all good
The scripts need to be executable:
$ 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:
$ pkgbuild --nopayload --scripts scripts --identifier my.fake.pkg my_package.pkg
pkgbuild: Adding top-level preinstall script
pkgbuild: Adding top-level postinstall script
pkgbuild: Wrote package to my_package.pkg
Install the package:
$ sudo installer -pkg my_package.pkg -target /
installer: Package name is my_package
installer: Installing at base path /
installer: The install was successful.
Check the scripts ran by looking for output:
$ cat /tmp/my_preinstall.log
Running preinstall
$ cat /tmp/my_postinstall.log
Running postinstall
Exit code other than zero causes the installer to give an error message:
scripts/postinstall
#!/bin/sh
exit 1 # not so good
$ pkgbuild --nopayload --scripts scripts --identifier my.fake.pkg my_package.pkg
pkgbuild: Adding top-level preinstall script
pkgbuild: Adding top-level postinstall script
pkgbuild: Wrote package to my_package.pkg
$ sudo installer -pkg my_package.pkg -target /
installer: Package name is my_package
installer: Installing at base path /
installer: The install failed (The Installer encountered an error that caused the installation to fail. Contact the software manufacturer for assistance.)
Fixed if the script is not run:
$ rm scripts/postinstall
$ pkgbuild --nopayload --scripts scripts --identifier my.fake.pkg my_package.pkg
pkgbuild: Adding top-level preinstall script
pkgbuild: Wrote package to my_package.pkg
$ sudo installer -pkg my_package.pkg -target /
installer: Package name is my_package
installer: Installing at base path /
installer: The install was successful.
There are some useful environment variables available to the
preinstall
, postinstall
scripts:
$ # Get the default environment when not in preinstall
$ sudo env > /tmp/my_sudo_envs.log
scripts/preinstall
#!/bin/sh
env > /tmp/my_preinstall_envs.log
echo "Positional arguments" $@ >> /tmp/my_preinstall_envs.log
exit 0
$ chmod u+x scripts/preinstall
$ pkgbuild --nopayload --scripts scripts --identifier my.fake.pkg my_package.pkg
pkgbuild: Adding top-level preinstall script
pkgbuild: Wrote package to my_package.pkg
$ sudo installer -pkg my_package.pkg -target /
installer: Package name is my_package
installer: Installing at base path /
installer: The install was successful.
Here are the new environment variables inserted by the installer:
$ diff /tmp/my_sudo_envs.log /tmp/my_preinstall_envs.log --old-line-format="" --unchanged-line-format=""
INSTALLER_TEMP=/private/tmp/PKInstallSandbox.ihgwFN/tmp
DSTVOLUME=/
TMPDIR=/private/tmp/PKInstallSandbox.ihgwFN/tmp
DSTROOT=/
USER=root
SCRIPT_NAME=preinstall
SHARED_INSTALLER_TEMP=/var/folders/zz/zyxvpxvq6csfxvn_n0000000000000/C/PKInstallSandboxManager-shared-tmp
INSTALLER_SECURE_TEMP=/var/folders/zz/zyxvpxvq6csfxvn_n0000000000000/C/PKInstallSandboxManager/52D185A5-181E-41F8-832A-7C5E31B4A3E4.activeSandbox/136FEC37-93BE-4C71-A891-6A5E63455E02
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/libexec
PWD=/private/tmp/PKInstallSandbox.ihgwFN/Scripts/my.fake.pkg.CiIISN
INSTALL_PKG_SESSION_ID=my.fake.pkg
PACKAGE_PATH=/Users/mb312/dev_trees/docosx/pkg_examples/my_package.pkg
SHLVL=1
COMMAND_LINE_INSTALL=1
_=/usr/bin/env
Positional arguments /Users/mb312/dev_trees/docosx/pkg_examples/my_package.pkg / / /
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:
$ 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 topkgbuild
. This analyzes a given directoryroot-path
taking this directory to represent the root/
directory of the target system. The installer will copy each directory atroot-path
to the target system at the same relative filesystem location. - By individual bundle component, using
--component
flag topkgbuild
. Specify directories to copy, and give their output locations separately using the--install-location
flag.
Destination root¶
$ 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
my_root
└── tmp
├── dir1
│ └── file1
└── dir2
└── file2
3 directories, 2 files
$ pkgbuild --root my_root --identifier my.fake.pkg my_package.pkg
pkgbuild: Inferring bundle components from contents of my_root
pkgbuild: Wrote package to my_package.pkg
Do the install:
$ sudo installer -pkg my_package.pkg -target /
installer: Package name is my_package
installer: Installing at base path /
installer: The install was successful.
$ cat /tmp/dir1/file1
Interesting info
$ cat /tmp/dir2/file2
Compelling story
This install did write a package receipt:
$ pkgutil --pkgs / | grep my.fake.pkg
my.fake.pkg
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:
$ echo "int main(int argc, char** argv) { }" > aprogram.c
$ clang -g aprogram.c -o aprogram
$ tree aprogram.dSYM
aprogram.dSYM
└── Contents
├── Info.plist
└── Resources
└── DWARF
└── aprogram
3 directories, 2 files
Build, install the package:
$ pkgbuild --component aprogram.dSYM --install-location /tmp my_package.pkg
pkgbuild: Adding component at /Users/mb312/dev_trees/docosx/pkg_examples/aprogram.dSYM
pkgbuild: Wrote package to my_package.pkg
$ sudo installer -pkg my_package.pkg -target /
installer: Package name is my_package
installer: Upgrading at base path /
installer: The upgrade was successful.
The installer has written the expected output files:
$ tree /tmp/aprogram.dSYM
/tmp/aprogram.dSYM
└── Contents
├── Info.plist
└── Resources
└── DWARF
└── aprogram
3 directories, 2 files
You can add more than one bundle to a single component installer.
Here I make another bundle with a different name:
$ echo "int main(int argc, char** argv) { }" > aprogram2.c
$ clang -g aprogram2.c -o aprogram2
$ tree aprogram2.dSYM
aprogram2.dSYM
└── Contents
├── Info.plist
└── Resources
└── DWARF
└── aprogram2
3 directories, 2 files
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.
$ pkgbuild --component aprogram.dSYM --install-location /tmp \
$ --component aprogram2.dSYM --install-location /tmp \
$ --identifier my.fake.pkg \
$ my_package.pkg
pkgbuild: Adding component at /Users/mb312/dev_trees/docosx/pkg_examples/aprogram.dSYM
pkgbuild: Adding component at /Users/mb312/dev_trees/docosx/pkg_examples/aprogram2.dSYM
pkgbuild: Wrote package to my_package.pkg
$ sudo installer -pkg my_package.pkg -target /
installer: Package name is my_package
installer: Upgrading at base path /
installer: The upgrade was successful.
$ tree /tmp/aprogram.dSYM
/tmp/aprogram.dSYM
└── Contents
├── Info.plist
└── Resources
└── DWARF
└── aprogram
3 directories, 2 files
$ tree /tmp/aprogram2.dSYM
/tmp/aprogram2.dSYM
└── Contents
├── Info.plist
└── Resources
└── DWARF
└── aprogram2
3 directories, 2 files
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 Destination root).
$ productbuild --root my_root my_archive.pkg
productbuild: Wrote product to 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 Explicit component(s)) and / or; - package paths(s) with one or more uses of the
--package
flag.
Build product archive with two component bundles:
$ productbuild --component aprogram.dSYM /tmp --component aprogram2.dSYM /tmp my_archive.pkg
productbuild: Adding component at /Users/mb312/dev_trees/docosx/pkg_examples/aprogram.dSYM
productbuild: Adding component at /Users/mb312/dev_trees/docosx/pkg_examples/aprogram2.dSYM
productbuild: Wrote product to my_archive.pkg
Make bundles into component packages:
$ pkgbuild --component aprogram.dSYM --install-location /tmp aprogram.pkg
pkgbuild: Adding component at /Users/mb312/dev_trees/docosx/pkg_examples/aprogram.dSYM
pkgbuild: Wrote package to aprogram.pkg
$ pkgbuild --component aprogram2.dSYM --install-location /tmp aprogram2.pkg
pkgbuild: Adding component at /Users/mb312/dev_trees/docosx/pkg_examples/aprogram2.dSYM
pkgbuild: Wrote package to aprogram2.pkg
Build product archives from the component packages:
$ productbuild --package aprogram.pkg --package aprogram2.pkg my_archive.pkg
productbuild: Wrote product to 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:
$ productbuild --synthesize --component aprogram.dSYM --component aprogram2.dSYM Distribution-1
productbuild: Adding component at /Users/mb312/dev_trees/docosx/pkg_examples/aprogram.dSYM
productbuild: Inferred install-location of /Users/mb312/dev_trees/docosx/pkg_examples
productbuild: Adding component at /Users/mb312/dev_trees/docosx/pkg_examples/aprogram2.dSYM
productbuild: Inferred install-location of /Users/mb312/dev_trees/docosx/pkg_examples
productbuild: Wrote synthesized distribution to Distribution-1
Analyze two component packages to give a Distribution
file:
$ productbuild --synthesize --package aprogram.pkg --package aprogram2.pkg Distribution-2
productbuild: Wrote synthesized distribution to Distribution-2
$ cat Distribution-2
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<installer-gui-script minSpecVersion="1">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
<options customize="never" require-scripts="false"/>
<choices-outline>
<line choice="default">
<line choice="com.apple.xcode.dsym.aprogram"/>
<line choice="com.apple.xcode.dsym.aprogram2"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="com.apple.xcode.dsym.aprogram" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram" version="1.0.0" onConclusion="none">aprogram.pkg</pkg-ref>
<choice id="com.apple.xcode.dsym.aprogram2" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram2" version="1.0.0" onConclusion="none">aprogram2.pkg</pkg-ref>
</installer-gui-script>
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.
$ productbuild --distribution Distribution-2 my_archive.pkg
productbuild: Wrote product to my_archive.pkg
Customizing the Distribution
file¶
See: distribution definition file and installer Javascript reference.
Adding system and volume checks¶
distro_check.xml
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<installer-gui-script minSpecVersion="1">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
<choices-outline>
<line choice="default">
<line choice="com.apple.xcode.dsym.aprogram"/>
<line choice="com.apple.xcode.dsym.aprogram2"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="com.apple.xcode.dsym.aprogram" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram" version="1.0.0" onConclusion="none">aprogram.pkg</pkg-ref>
<choice id="com.apple.xcode.dsym.aprogram2" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram2" version="1.0.0" onConclusion="none">aprogram2.pkg</pkg-ref>
<installation-check script="installation_check()"/>
<volume-check script="volume_check()"/>
<script><![CDATA[
function installation_check () {
// Check 64 bit
if (! system.sysctl('hw.cpu64bit_capable')) {
// need to set error type
my.result.type = "Fatal"
my.result.message = "Installer needs 64 bit"
return false
}
return true
}
function volume_check () {
// check installed file exists
log_file = my.target.mountpoint + 'tmp/my_postinstall.log'
if (! system.files.fileExistsAtPath(log_file)) {
// need to set error type
my.result.type = "Fatal"
my.result.message = "No my_postinstall file on volume"
return false
}
return true
}
]]></script>
</installer-gui-script>
$ productbuild --distribution distro_check.xml my_archive.pkg
productbuild: Wrote product to my_archive.pkg
$ sudo installer -pkg my_archive.pkg -target /
installer: Package name is
installer: Upgrading at base path /
installer: The upgrade was successful.
Now make the volume check fail:
$ sudo rm /tmp/my_postinstall.log
$ sudo installer -pkg my_archive.pkg -target /
installer: Cannot install on volume / because it is disabled.
installer: No my_postinstall file on volume
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.
distro_debug.xml
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<installer-gui-script minSpecVersion="1">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
<choices-outline>
<line choice="default">
<line choice="com.apple.xcode.dsym.aprogram"/>
<line choice="com.apple.xcode.dsym.aprogram2"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="com.apple.xcode.dsym.aprogram" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram" version="1.0.0" onConclusion="none">aprogram.pkg</pkg-ref>
<choice id="com.apple.xcode.dsym.aprogram2" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram2" version="1.0.0" onConclusion="none">aprogram2.pkg</pkg-ref>
<installation-check script="installation_check()"/>
<script><![CDATA[
function installation_check () {
// Some debug prints
system.log('my: ' + system.propertiesOf(my))
system.log('system: ' + system.propertiesOf(system))
system.log('system.applications: ' +
system.propertiesOf(system.applications))
return true
}
]]></script>
</installer-gui-script>
$ productbuild --distribution distro_debug.xml my_archive.pkg
productbuild: Wrote product to my_archive.pkg
Use the -dumplog
flag to the installer to see the log.
$ sudo installer -dumplog -pkg my_archive.pkg -target /
installer: Package name is
installer: Upgrading at base path /
installer: The upgrade was successful.
Mar 24 15:18:04 shanghai installer[67824] <Debug>: JS: my: choice,target,search,product,result
Mar 24 15:18:04 shanghai installer[67824] <Debug>: JS: system: supportedSpecVersion,bundles,openCL,defaults,search,ioregistry,env,files,hasWindowServer,users,applications,version,localizedStandardStringWithFormat,gestalt,localizedStringWithFormat,run,sysctl,propertiesOf,log,numericalCompare,runOnce,compareVersions,localizedString,localizedStandardString
Mar 24 15:18:04 shanghai installer[67824] <Debug>: JS: system.applications: fromIdentifier,all,fromPID
Mar 24 15:18:05 shanghai installer[67824] <Debug>: Product archive /Users/mb312/dev_trees/docosx/pkg_examples/my_archive.pkg trustLevel=100
Mar 24 15:18:06 shanghai installer[67824] <Debug>: -[IFDInstallController(Private) _buildInstallPlan]: location = file://localhost
Mar 24 15:18:06 shanghai installer[67824] <Debug>: -[IFDInstallController(Private) _buildInstallPlan]: file://localhost/Users/mb312/dev_trees/docosx/pkg_examples/my_archive.pkg#aprogram.pkg
Mar 24 15:18:06 shanghai installer[67824] <Debug>: -[IFDInstallController(Private) _buildInstallPlan]: file://localhost/Users/mb312/dev_trees/docosx/pkg_examples/my_archive.pkg#aprogram2.pkg
Mar 24 15:18:06 shanghai installer[67824] <Debug>: Set authorization level to root for session
Mar 24 15:18:06 shanghai installer[67824] <Debug>: Will use PK session
Mar 24 15:18:06 shanghai installer[67824] <Info>: Starting installation:
Mar 24 15:18:06 shanghai installer[67824] <Notice>: Configuring volume "SSD"
Mar 24 15:18:06 shanghai installer[67824] <Info>: Preparing disk for local booted install.
Mar 24 15:18:06 shanghai installer[67824] <Notice>: Free space on "SSD": 8.93 GB (8929431552 bytes).
Mar 24 15:18:06 shanghai installer[67824] <Notice>: Create temporary directory "/var/folders/zz/zyxvpxvq6csfxvn_n0000000000000/T//Install.67824ePvO0B"
Mar 24 15:18:06 shanghai installer[67824] <Notice>: IFPKInstallElement (2 packages)
Mar 24 15:18:06 shanghai installer[67824] <Debug>: Using authorization level of root for IFPKInstallElement
Mar 24 15:18:06 shanghai installer[67824] <Notice>: PackageKit: Enqueuing install with boosting
Mar 24 15:18:07 shanghai installer[67824] <Notice>: Running install actions
Mar 24 15:18:07 shanghai installer[67824] <Notice>: Removing temporary directory "/var/folders/zz/zyxvpxvq6csfxvn_n0000000000000/T//Install.67824ePvO0B"
Mar 24 15:18:07 shanghai installer[67824] <Notice>: Finalize disk "SSD"
Mar 24 15:18:07 shanghai installer[67824] <Notice>: Notifying system of updated components
Mar 24 15:18:07 shanghai installer[67824] <Notice>:
Mar 24 15:18:07 shanghai installer[67824] <Notice>: **** Summary Information ****
Mar 24 15:18:07 shanghai installer[67824] <Notice>: Operation Elapsed time
Mar 24 15:18:07 shanghai installer[67824] <Notice>: -----------------------------
Mar 24 15:18:07 shanghai installer[67824] <Notice>: disk 0.02 seconds
Mar 24 15:18:07 shanghai installer[67824] <Notice>: script 0.00 seconds
Mar 24 15:18:07 shanghai installer[67824] <Notice>: zero 0.01 seconds
Mar 24 15:18:07 shanghai installer[67824] <Notice>: install 1.03 seconds
Mar 24 15:18:07 shanghai installer[67824] <Notice>: -total- 1.06 seconds
Mar 24 15:18:07 shanghai installer[67824] <Notice>:
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:
<options allow-external-scripts="yes"/>
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:
distro_system_run.xml
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<installer-gui-script minSpecVersion="1">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
<options allow-external-scripts="yes"/>
<choices-outline>
<line choice="default">
<line choice="com.apple.xcode.dsym.aprogram"/>
<line choice="com.apple.xcode.dsym.aprogram2"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="com.apple.xcode.dsym.aprogram" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram" version="1.0.0" onConclusion="none">aprogram.pkg</pkg-ref>
<choice id="com.apple.xcode.dsym.aprogram2" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram2" version="1.0.0" onConclusion="none">aprogram2.pkg</pkg-ref>
<installation-check script="installation_check()"/>
<script><![CDATA[
function installation_check () {
// Run some sh code
exit_code = system.run('/bin/sh', '-c',
'echo "Command ran" > /tmp/my_system_run.log; exit 1')
if (exit_code != 0) {
// need to set error type
my.result.type = "Fatal"
my.result.message = "Test function failed by design"
return false
}
return true
}
]]></script>
</installer-gui-script>
$ productbuild --distribution distro_system_run.xml my_archive.pkg
productbuild: Wrote product to my_archive.pkg
$ sudo installer -pkg my_archive.pkg -target /
installer: Error - Test function failed by design
Check the script ran:
$ cat /tmp/my_system_run.log
Command ran
Here’s an example of a custom script:
distro_custom_run.xml
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<installer-gui-script minSpecVersion="1">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
<options allow-external-scripts="yes"/>
<choices-outline>
<line choice="default">
<line choice="com.apple.xcode.dsym.aprogram"/>
<line choice="com.apple.xcode.dsym.aprogram2"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="com.apple.xcode.dsym.aprogram" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram" version="1.0.0" onConclusion="none">aprogram.pkg</pkg-ref>
<choice id="com.apple.xcode.dsym.aprogram2" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram2" version="1.0.0" onConclusion="none">aprogram2.pkg</pkg-ref>
<installation-check script="installation_check()"/>
<script><![CDATA[
function installation_check () {
// Run a custom script
exit_code = system.run('my_test_cmd.sh', 'args', 'I', 'passed');
if (exit_code != 0) {
// need to set error type
my.result.type = "Fatal"
my.result.message = "Test function failed by design"
return false
}
return true
}
]]></script>
</installer-gui-script>
Make a directory to contain the test script:
$ mkdir archive_scripts
Use test script to look for any interesting environment variables available to the custom script:
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:
$ chmod u+x archive_scripts/my_test_cmd.sh
Build the product archive with --scripts
flag:
$ productbuild --distribution distro_custom_run.xml --scripts archive_scripts my_archive.pkg
productbuild: Wrote product to my_archive.pkg
$ sudo installer -pkg my_archive.pkg -target /
installer: Error - Test function failed by design
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:
$ diff /tmp/my_sudo_envs.log /tmp/my_custom_envs.log --old-line-format="" --unchanged-line-format=""
SHELL=/bin/bash
USER=root
SUDO_USER=mb312
SUDO_UID=501
__CF_USER_TEXT_ENCODING=0x0:0:0
USERNAME=root
MAIL=/var/mail/root
PWD=/private/var/folders/zz/zyxvpxvq6csfxvn_n0000000000000/T/com.apple.install.PGDGIdg6
SHLVL=1
SUDO_COMMAND=/usr/sbin/installer -pkg my_archive.pkg -target /
COMMAND_LINE_INSTALL=1
DISPLAY=/tmp/launch-bIbO3P/org.macosforge.xquartz:0
_=/usr/bin/env
Positional arguments args I passed
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 (
<welcome ... />
element) - Readme (
<readme ... />
) - License (
<license ... />
) - Conclusion (
<conclusion ... />
)
To do this, you specify the filename containing the custom text, and the format of the file. For example:
<welcome file="welcome.html" mime-type="text/html"/>
The file should be in the Resources/<language>.lproj
directory of the
installer .pkg
, where <language>
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
.
$ mkdir -p my_resources/English.lproj
my_resources/English.lproj/welcome.html
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1">
<title>Install my_archive </title>
</head>
<body>
<font face="Helvetica">
<b>Welcome to the <i>my_archive</i> installer</b>
<p>
This installer will install <i>my_archive</i> on your computer.
</font>
</ul>
</body>
</html>
distro_welcome.xml
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<installer-gui-script minSpecVersion="1">
<welcome file="welcome.html" mime-type="text/html"/>
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
<choices-outline>
<line choice="default">
<line choice="com.apple.xcode.dsym.aprogram"/>
<line choice="com.apple.xcode.dsym.aprogram2"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="com.apple.xcode.dsym.aprogram" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram" version="1.0.0" onConclusion="none">aprogram.pkg</pkg-ref>
<choice id="com.apple.xcode.dsym.aprogram2" visible="false">
<pkg-ref id="com.apple.xcode.dsym.aprogram2"/>
</choice>
<pkg-ref id="com.apple.xcode.dsym.aprogram2" version="1.0.0" onConclusion="none">aprogram2.pkg</pkg-ref>
</installer-gui-script>
We use the --resources
flag to add the resources directory:
$ productbuild --distribution distro_welcome.xml --resources my_resources my_archive.pkg
productbuild: Wrote product to 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
.