Marking macOS component packages available based on hardware platform type
Tags: macos, packaging, tutorial
Published: 2024-05-10 21:35:32

_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](https://scriptingosx.com/) 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](https://python.org) installer. 

![python.org customizable package example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wmfqdp14crff4axfeb3k.png)

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
<?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: 

![package with both options enabled and selected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0gpjf989s9vsa0dnv7q4.png)

That's not what we want. Let's explore our options. Apple documents all the options available [here](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html). 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 to `false`, 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 is `true`.

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](https://developer.apple.com/documentation/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. Use `man 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. 

```sh
❯ 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
<?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>
```

![Packages showing proper enabled item but both selected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y6foimfys8q71jfz9c5q.png)



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.

```log
====================================================================
 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](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html#//apple_ref/doc/uid/TP40005370-CH100-SW10). 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:


![Packages with proper enabled item and proper selected item](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/auoxw0x10t9zeudwd2fk.png)



```log
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.