Wrapping a macOS Component Package into a Distribution Package
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
- Save the script below as a Python file (e.g.,
wrap_component_pkg.py). - Make the script executable by running
chmod +x wrap_component_pkg.pyin the Terminal. - Run the script with the component package as an argument:
./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.
#!/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.