Wrapping a macOS Component Package into a Distribution Package

Michael Page
macOSpackagepkgMDMScriptIT AdministrationMacAdmins

The Challenge with Component Packages

When deploying software to macOS devices, you'll often encounter two types of installer packages: component packages and distribution packages. While they both have the .pkg extension, they are structured differently.

A component package contains the files for a single piece of software. A distribution package, on the other hand, is a container that can hold multiple component packages and includes a Distribution file that controls the installation process.

Some Mobile Device Management (MDM) solutions, like Jamf School, only accept distribution packages. This can be a problem if you have a component package that you need to deploy.

The Solution: A Conversion Script

To solve this problem, I've written a Python script that reliably wraps a component package inside a new distribution package. It works by using Apple's built-in productbuild command-line tool in a two-step process.

First, the script calls productbuild --synthesize. This command inspects the original component package and automatically generates a Distribution file. This file is an XML blueprint that tells the macOS installer all the necessary details about the package, such as its identifier and version. Using --synthesize is the official and most reliable way to create this blueprint, as it guarantees the format is correct.

Second, the script calls productbuild --distribution. This command takes the newly generated blueprint and the original component package and builds the final, wrapped distribution package that is ready for deployment.

How to Use the Script

  1. Save the script below as a Python file (e.g., wrap_component_pkg.py).
  2. Make the script executable by running chmod +x wrap_component_pkg.py in the Terminal.
  3. Run the script with the component package as an argument:
bash
./wrap_component_pkg.py /path/to/your/component.pkg

The script will create a new distribution package in the same directory as the original, with "-Distribution" appended to the file name (e.g., component-Distribution.pkg).

The Script

Here is the Python script to perform the conversion.

python
#!/usr/bin/env python3
import sys
import subprocess
import tempfile
from pathlib import Path

def wrap_component(component_pkg_path):
    """
    Wraps a component package in a distribution package.
    """
    component_pkg = Path(component_pkg_path).resolve()
    
    if not component_pkg.exists():
        print(f"Error: {component_pkg} does not exist.", file=sys.stderr)
        sys.exit(1)
    
    if not component_pkg.suffix == '.pkg':
        print(f"Warning: {component_pkg} doesn't have a .pkg extension.", file=sys.stderr)
    
    # Create a temporary directory to work in
    with tempfile.TemporaryDirectory(prefix='wrap_dist_pkg_') as temp_dir:
        work_dir = Path(temp_dir)
        distribution_xml_path = work_dir / "distribution.xml"
        
        # 1. Synthesize a distribution file from the component package
        print("Synthesizing distribution file...")
        try:
            subprocess.run(
                [
                    "productbuild",
                    "--synthesize",
                    "--package",
                    str(component_pkg),
                    str(distribution_xml_path),
                ],
                check=True,
                capture_output=True,
                text=True,
            )
        except subprocess.CalledProcessError as e:
            print(f"Error synthesizing distribution: {e.stderr}", file=sys.stderr)
            sys.exit(1)
            
        # 2. Build the new distribution package
        out_pkg = component_pkg.with_name(f"{component_pkg.stem}-Distribution.pkg")
        print(f"Building distribution package: {out_pkg}")
        
        try:
            subprocess.run(
                [
                    "productbuild",
                    "--distribution",
                    str(distribution_xml_path),
                    "--package-path",
                    str(component_pkg.parent),
                    str(out_pkg),
                ],
                check=True,
                capture_output=True,
                text=True,
            )
            print(f"Done.\nCreated: {out_pkg}")
        except subprocess.CalledProcessError as e:
            print(f"Error running productbuild: {e.stderr}", file=sys.stderr)
            sys.exit(1)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <component.pkg>", file=sys.stderr)
        sys.exit(1)
    wrap_component(sys.argv[1])

Verifying the Result

After creating the distribution package, it's a good practice to verify that it behaves identically to the original component package. A fantastic tool for this is Suspicious Package.

You can use it to inspect both the original .pkg and the newly generated -Distribution.pkg. When you do, check the following to ensure the integrity of the package:

  • Payload: The files and directories to be installed should be identical.
  • Scripts: Any pre-install or post-install scripts should be the same.
  • Receipt: The package identifier and version should match what was extracted from the original package.

This quick check confirms that your new distribution package is a faithful wrapper around the original and will install the same components on the target system.