Before AutoPkg’s 1.0 release in November 2016, it took at least three processors to create a simple pkg recipe that installs an app into /Applications: PkgRootCreator to create the folder structure, Copier to copy the app into place, and PkgCreator to create the actual installer package.
With the release of AutoPkg 1.0, the AppPkgCreator processor allowed simplifying those steps into one for many recipes. Not only does this make a large number of AutoPkg recipes much simpler and easier for administrators to understand, but it also streamlines autopkg audit
checks.
PkgCreator to AppPkgCreator script
To help encourage administrators to use the AppPkgCreator processor, I’ve written a Python script that does all the work of converting recipes for you.
Here’s the source of the script; see below for usage information and tips.
#!/usr/local/autopkg/python | |
# encoding: utf-8 | |
# PkgCreator to AppPkgCreator | |
# Copyright 2019-2021 Elliot Jordan | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
"""PkgCreator_to_AppPkgCreator.py | |
Script for converting compatible AutoPkg "pkg" type recipes from using | |
PkgRootCreator-Copier-PkgCreator to using AppPkgCreator. | |
""" | |
import argparse | |
import os | |
import plistlib | |
from distutils.version import LooseVersion | |
from xml.parsers.expat import ExpatError | |
def build_argument_parser(): | |
"""Build and return the argument parser.""" | |
parser = argparse.ArgumentParser( | |
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter | |
) | |
parser.add_argument("repo_path", help="Path to search for AutoPkg recipes.") | |
parser.add_argument( | |
"--auto", | |
action="store_true", | |
default=False, | |
help="Automatically apply suggested changes to recipes. Only recommended " | |
"for repos you manage or are submitting a pull request to. (Applying " | |
"changes to repos added using `autopkg repo-add` may result in failure " | |
"to update the repo in the future.)", | |
) | |
parser.add_argument( | |
"-v", | |
"--verbose", | |
action="count", | |
default=0, | |
help="Print additional output useful for " | |
"troubleshooting. (Can be specified multiple times.)", | |
) | |
return parser | |
def evaluate(recipe, recipe_path, args): | |
"""Perform conversion to AppPkgCreator.""" | |
if args.verbose > 0: | |
print("Evaluating %s..." % (recipe_path)) | |
skip_marker = "Cannot use AppPkgCreator" | |
if skip_marker in recipe.get("Description", "") + recipe.get("Comment", ""): | |
if args.verbose > 1: | |
print( | |
" Skipping: A comment indicates we can't use AppPkgCreator for this recipe." | |
) | |
return | |
if "Process" not in recipe: | |
if args.verbose > 1: | |
print(" Skipping: This recipe has no process") | |
return | |
process = recipe.get("Process") | |
if len(process) < 3: | |
if args.verbose > 1: | |
print( | |
" Skipping: This recipe doesn't use PkgRootCreator-Copier-PkgCreator." | |
) | |
return | |
if "AppPkgCreator" in [x["Processor"] for x in process]: | |
if args.verbose > 1: | |
print(" Skipping: This recipe already uses AppPkgCreator.") | |
return | |
old_procs = {"PkgRootCreator": -1, "Copier": -1, "PkgCreator": -1} | |
for old_proc in old_procs: | |
for idx, processor in enumerate([x["Processor"] for x in process]): | |
if processor == old_proc: | |
old_procs[old_proc] = idx | |
indexes = ( | |
old_procs["PkgRootCreator"], | |
old_procs["Copier"], | |
old_procs["PkgCreator"], | |
) | |
if sorted(indexes) != list(range(min(indexes), max(indexes) + 1)): | |
if args.verbose > 1: | |
print( | |
" Skipping: PkgRootCreator-Copier-PkgCreator not found in sequential order." | |
) | |
return | |
pkgrootcreator_model_args = { | |
"pkgdirs": {"Applications": "0775"}, | |
"pkgroot": "%RECIPE_CACHE_DIR%/%NAME%", | |
} | |
if process[indexes[0]]["Arguments"] != pkgrootcreator_model_args: | |
if args.verbose > 1: | |
print(" Skipping: PkgRootCreator arguments are non-standard.") | |
return | |
copier_source_path = os.path.split( | |
recipe["Process"][indexes[1]]["Arguments"]["source_path"] | |
) | |
if copier_source_path[0] != "%pathname%" or not copier_source_path[1].endswith( | |
".app" | |
): | |
if args.verbose > 1: | |
print(" Skipping: Copier arguments are non-standard.") | |
# TODO: Might be able to use Copier source_path as the AppPkgCreator app_path in some cases. | |
return | |
if "scripts" in process[indexes[2]]["Arguments"]["pkg_request"]: | |
if args.verbose > 1: | |
print( | |
" Skipping: PkgCreator includes scripts, which AppPkgCreator does not support." | |
) | |
return | |
if not args.auto: | |
print("✨ Recipe %s is eligible for AppPkgCreator." % recipe_path) | |
else: | |
print("✨ Recipe %s is eligible for AppPkgCreator. Converting..." % recipe_path) | |
# Remove PkgRootCreator | |
del recipe["Process"][indexes[0]] | |
# Remove Copier, which is now at the PkgRootCreator index. | |
del recipe["Process"][indexes[0]] | |
# Remove PkgCreator, which is now at the PkgRootCreator index. | |
del recipe["Process"][indexes[0]] | |
# Insert AppPkgCreator. | |
recipe["Process"].insert(indexes[0], {"Processor": "AppPkgCreator"}) | |
# Update minimum AutoPkg version. | |
if recipe.get("MinimumVersion", "0") < LooseVersion("1.0.0"): | |
recipe["MinimumVersion"] = "1.0.0" | |
with open(recipe_path, "wb") as openfile: | |
plistlib.dump(recipe, openfile) | |
def main(): | |
"""Main process.""" | |
# Parse command line arguments. | |
argparser = build_argument_parser() | |
args = argparser.parse_args() | |
# Extensions to include in converting. | |
target_file_exts = (".pkg.recipe",) | |
# Gather list of eligible files. | |
for root, dirs, files in os.walk(os.path.expanduser(args.repo_path)): | |
dirs[:] = [d for d in dirs if not d.startswith(".")] | |
for idx, filename in enumerate(files): | |
if filename.endswith(target_file_exts): | |
recipe_path = os.path.join(root, filename) | |
try: | |
with open(recipe_path, "rb") as openfile: | |
recipe = plistlib.load(openfile) | |
evaluate(recipe, recipe_path, args) | |
except ExpatError: | |
print("[ERROR] Unable to read %s", recipe_path) | |
continue | |
if __name__ == "__main__": | |
main() |
Usage
As with many command line tools, running /path/to/PkgCreator_to_AppPkgCreator.py --help
will produce usage information.
% ./PkgCreator_to_AppPkgCreator.py --help
usage: PkgCreator_to_AppPkgCreator.py [-h] [--auto] [-v] repo_path
PkgCreator_to_AppPkgCreator.py
Script for converting compatible AutoPkg "pkg" type recipes from using
PkgRootCreator-Copier-PkgCreator to using AppPkgCreator.
positional arguments:
repo_path Path to search for AutoPkg recipes.
optional arguments:
-h, --help show this help message and exit
--auto Automatically apply suggested changes to recipes. Only
recommended for repos you manage or are submitting a pull
request to. (Applying changes to repos added using `autopkg
repo-add` may result in failure to update the repo in the
future.)
-v, --verbose Print additional output useful for troubleshooting. (Can be
specified multiple times.)
Automatically apply changes
To automatically update eligible recipes in your AutoPkg recipe repository to use the AppPkgCreator processor, run this command (substituting the appropriate paths):
/path/to/PkgCreator_to_AppPkgCreator.py --auto /path/to/your-recipes
You’ll see output that looks like this:
✨ Recipe ./Foo/FooPro.pkg.recipe is eligible for AppPkgCreator. Converting...
✨ Recipe ./Bar/BarApp.pkg.recipe is eligible for AppPkgCreator. Converting...
✨ Recipe ./Baz/BazSuite.pkg.recipe is eligible for AppPkgCreator. Converting...
Review the changes made by the script, and run each modified recipe to make sure it works as expected. Once the modified recipes have been tested successfully, you can commit and push the changes.
Reverting
If a converted recipe doesn’t work for any reason, you can revert the changes using Git:
cd /path/to/your-recipes
git checkout -- path/to/recipe
Marking Exemptions
You might have reasons not to switch to AppPkgCreator in certain recipes. For example, if you think a recipe may require adding a preinstall/postinstall script in the future.
To ignore specific recipes when running this script, add the phrase Cannot use AppPkgCreator
somewhere in the Comment or Description of the recipe. (Example here.)
Manual Changes
In many cases, you may also be able to remove PlistReader, Versioner, or AppDmgVersioner processors, since AppPkgCreator itself includes versioning (using the CFBundleShortVersionString
key). These changes are not done automatically by the script, so you’ll need to make them manually if needed.
Thank you!
I periodically review AutoPkg recipes written by others, and I’m always grateful when a recipe author finds a way to simplify and streamline a recipe I use. Often, those improvements inspire me to write bulk automation like this, which I hope will enable and encourage the positive cycle of community refinement to continue.