Cover image by SimonWaldherr, CC BY-SA 4.0 https://creativecommons.org/licenses/by-sa/4.0, via Wikimedia Commons
On macOS, you can create two different installer package types:
- Flat/Component Packages
- Distribution Packages
Flat packages are the most common used packages, but distribution packages are more robust and can contain multiple flat packages. That's enough detail for this article but if you want to know more Armin Briegel of ScriptingOSX has a great book covering a lot of the details of these package types. I highly recommend picking up a copy for reference. One of the benefits of Distribution packages is that you can include components as a choice for folks to install specific pieces. An example is provided below, courtesy of the Python.org installer.
Despite being three years after the Apple Silicon revolution, some components or software can not be made universal yet (or the vendor has specifically chosen not to provide universal binaries). A common workaround for this is the vendor provides a flat package, where the post-install script actually installs the proper package based on the hardware platform. This is fine and does work, but hides the logic behind an installation script. This also makes managing a distribution package much messier as the packages aren't declared in the Distribution.xml file.
Let's look at a basic Distribution.xml for my "Test Package" that includes an ARM and X86 component packages. This package defines two "choices" where the user can choose to install the X86 or ARM variant. For non-technical users, this could lead to confusion where they may install the wrong item. (For reference, the distribution will have the customize option set to always
to demonstrate the choices. I recommend for enterprise packages, marking this to never
to prevent this UI from showing and always respecting your default choices)
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<installer-gui-script minSpecVersion="2">
<title>Test Package</title>
<options customize="always" require-scripts="false" rootVolumeOnly="true" hostArchitectures="arm64,x86_64"/>
<choices-outline>
<line choice="arm"/>
<line choice="x86"/>
</choices-outline>
<choice id="arm" title="ARM">
<pkg-ref id="com.acme.pkg.test_arm">test_arm-1.0.pkg</pkg-ref>
</choice>
<choice id="x86" title="x86">
<pkg-ref id="com.acme.pkg.test_x86">test_x86-1.0.pkg</pkg-ref>
</choice>
</installer-gui-script>
In the Installer UI, this presents as such:
That's not what we want. Let's explore our options. Apple documents all the options available here. Reading more, we want to use the enabled
attribute for the choice:
Optional. Specifies whether the user can select or deselect this option in the Installer customization pane. If
false
, or a JavaScript expression that evaluates tofalse
, the choice is dimmed so the user cannot select or deselect it. Re-evaluated as the user interacts with the choice tree, so a choice can be enabled or disabled based on the state of other choices. Default value istrue
.
So we can drive it via JavaScript... wait...we can use Javascript in an Installer package??!? Yup. Apple has a variant of JavaScript they call Installer JS. This allows you to use a limited set of JavaScript properties to enhance your package choices. Let's explore some items of Installer JS to see what our options are.
Following Apple's documentation we can see there's a class for System
with a function called sysctl
. It has this wonderful note:
If you are using this function to screen for hardware features at install-time, consider using the “Pre-install Requirements Property List” setting of
productbuild
instead. Useman productbuild
in the macOS Terminal application to see a list of supported requirement keys.
This would be great...but distribution packages can not contain other distribution packages where we could use the Pre-install markers to limit the packages to architectures. Dang that's out of the way...but we can still keep going. The sysctl
function matches exactly how sysctl
works in Terminal or scripting.
When we run sysctl -n hw.machine
we can get our expected architecture arm64
...but wait node can lead to unexpected results if ran under Rosetta! We can however rely on our CPU name to tell us definitively what arch we're running on.
❯ sysctl -n hw.machine
arm64
❯ arch -x86_64 sysctl -n hw.machine
x86_64
❯ sysctl -n machdep.cpu.brand_string
Apple M1 Pro
❯ arch -x86_64 sysctl -n machdep.cpu.brand_string
Apple M1 Pro
Great! We've identified a way to track what hardware we're running and a potential way to reference it in the Distribution.xml file. JavaScript has a string includes
function to check for specific substrings. Let's put it all together. We need a script
element to contain our script so we can call it per choice. We also need to remember to invert one of the options so that enabled
is false
! When calling the script function, we can just reference the function name in the attribute value:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
https://developer.apple.com/library/mac/documentation/DeveloperTools/Reference/DistributionDefinitionRef/
https://developer.apple.com/library/mac/documentation/DeveloperTools/Reference/InstallerJavaScriptRef/
-->
<installer-gui-script minSpecVersion="2">
<title>Test Package</title>
<options customize="always" require-scripts="false" rootVolumeOnly="true" hostArchitectures="arm64,x86_64"/>
<script>
<![CDATA[
function is_arm() {
if(system.sysctl("machdep.cpu.brand_string").includes("Apple")) {
return true;
}
return false;
}
]]>
</script>
<choices-outline>
<line choice="arm"/>
<line choice="x86"/>
</choices-outline>
<choice id="arm" title="ARM" enabled="is_arm()">
<pkg-ref id="com.acme.pkg.test_arm">test_arm-1.0.pkg</pkg-ref>
</choice>
<choice id="x86" title="x86" enabled="! is_arm()">
<pkg-ref id="com.acme.pkg.test_x86">test_x86-1.0.pkg</pkg-ref>
</choice>
</installer-gui-script>
Success....wait a second. The package still shows as selected but enabled??? This is unexpected. Maybe a UI problem? If we hit CMD+L
we can open up the install log for this installer. Switching to Show All Logs mode, then hitting next, we can see it will install both. This is not what we want.
====================================================================
MacLaptop Installer[98327]: User picked Custom Install
MacLaptop Installer[98327]: Choices selected for installation:
MacLaptop Installer[98327]: Install: "Test Package"
MacLaptop Installer[98327]: Install: "ARM"
MacLaptop Installer[98327]: test-meta.pkg#test_arm-1.0.pkg : com.acme.pkg.test_arm : 1.0
MacLaptop Installer[98327]: Install: "x86"
MacLaptop Installer[98327]: test-meta.pkg#test_x86-1.0.pkg : com.acme.pkg.test_x86 : 1.0
====================================================================
We can adjust this further to get our results. We can also solve this in multiple ways. You can duplicate the enabled
item for selected
which will unselect the item for the specific arch. You can also change it from enabled
to selected
and add the start_enabled="false"
attribute. You can modify the behavior entirely using the choice
attributes on the docs. For most enterprise installations, this won't even be visible. I recommend going the enabled
and selected
route to ensure the package functions as expected with no customization. Like so:
MacLaptop Installer[1808]: ================================================================================
MacLaptop Installer[1808]: User picked Custom Install
MacLaptop Installer[1808]: Choices selected for installation:
MacLaptop Installer[1808]: Install: "Test Package"
MacLaptop Installer[1808]: Install: "ARM"
MacLaptop Installer[1808]: test-meta.pkg#test_arm-1.0.pkg : com.acme.pkg.test_arm : 1.0
MacLaptop Installer[1808]: ================================================================================
I hope this can help those in need of dual packages for the same non-universal capable item.
Top comments (0)