Installer Package Scripting Installer Package Scripting Making your deployments easier, one !# at a time Before we get started, there’s two things I’d like to mention. The first is that, all of the sides, speakers’ notes and the demos are available for download and I’ll be providing a link at the end of the talk. I tend to be one of those folks who can’t keep up with the speaker and take notes at the same time, so for those folks in the same situation, no need to take notes. Everything I’m covering is going to be available for download. You should: •Know the basics of writing scripts. •Know the basics of building an installer package. To set some expectations management for this session, my assumption is that folks here already know the basics of how to write scripts and build an installer package. We’re going to be discussing how to build on that foundation to solve problems. Now, installer packages are pretty great. They’re one of Apple’s two recommended ways to install software, with the other method being dragging and dropping a self-contained application into place. Installer Package Advantages •Permissions Control •File location control •May not require logged-in user •Can run scripts as part of the installation process I personally believe installer packages are the superior installation method. You can control the permissions on the installed files. You can control where the installed files are placed. When properly built, a package can be installed with nobody logged in. Last, but not least, you can run scripts as part of the installation process. Another advantage, outside of the package itself, is that every Mac management system I’ve run into yet has the ability to install an installer package. For folks who have to support multiple management systems, this common ability means you can build one package and be confident that it will deploy the same regardless of the management system used to install it. Using Scripts However, the real magic of using installer packages happens when you start using scripts as part of the installation process. JavaScript Ruby PHP PerlShell Python Many More! What languages can you use? Well, if a client Mac supports a language, the Installer tool should be able to support running scripts written in that language on that client. #!/bin/bash Probably the most common form is going to be shell scripting using the bash shell, though, so that’s what I’m going to focus on. Bundle vs. Flat From there, your scripting options are going to depend on whether you’re using a bundle-style package or a flat package. Bundle-style package scripts NAME HOW USED preflight Preflight scripts are run before files are being installed. If the script does not return an exit status of 0, Installer will cancel the installation. preinstall Preinstall scripts are run before files are being installed and after the preflight script if one is defined. This script is run only if the component is being installed for the first time. If the script does not return an exit status of 0, Installer will cancel the installation. preupgrade Preupgrade scripts are run before files are being installed and after the preflight script if one is defined. This script is run only if the component has been previously installed. If the script does not return 0, Installer will cancel the installation. postinstall Postinstall scripts are run after files have been installed and before the postfligt script if one is defined. This script is run only if the component is being installed for the first time. If the script does not return 0, Installer will declare the installation failed. postupgrade Postupgrade scripts are run after files have been installed and before the postfligt script if one is defined. This script is run only if the component has been previously installed. If the script does not return an exit status of 0, Installer will declare the installation failed. postflight Postflight scripts are run after files have been installed. If the script does not return an exit status of 0, Installer will declare the installation failed. Bundle-style installer packages are created as a Mac OS X bundle that contains scripts and a description of the package requirements and behavior. As part of creating a bundle-style package, you can define scripts that will be executed before or after the installation. Normally, there are six types of scripts can be used in a bundle-style package. Bundle-style package scripts NAME HOW USED preflight Preflight scripts are run before files are being installed. If the script does not return an exit status of 0, Installer will cancel the installation. postflight Postflight scripts are run after files have been installed. If the script does not return an exit status of 0, Installer will declare the installation failed. However, most of the time you’ll be using these two types. Preflight scripts get run before files are installed and postflight scripts run after files are installed. The other types of scripts only apply in specific defined circumstances which may not apply to your package. Bundle-style ⇢ Flat package Category Mapping BUNDLE-STYLE SCRIPT NAME FLAT SCRIPT NAME preflight = preinstall postflight = postinstall When Apple created the flat package format, they simplified the script options and made only two script options available. The bundle-style post/preinstall and pre/postupdate script options were removed. Apple retained the concept of pre- and postflight scripts, but Apple renamed those script categories to preinstall and postinstall to make their functions more clear. Scripts must return an exit status of zero A commonality between scripts used in bundle-style and flat packages are that the scripts must return an exit status of zero or else Installer will report failure. #!/bin/bash if [[ -f "/path/to/file" ]]; then /usr/sbin/do_something "/path/to/file" fi exit 0 Scripts must return an exit status of zero If you're building a shell script for an installer package, one way to ensure that the script returns a status of zero is by adding "exit 0" to the end of your script. VARIABLE WHAT’S REFERENCED $0 = returns the path to the script $1 = returns the path to the package $2 = returns the target location (for example: /Applications) $3 = returns the target volume (for example: / Volumes/Macintosh HD) Installer Script Variables When building a script for use with an installer package, it's helpful to know that the Installer application can automatically pass along information to the script using variables. The ones shown on the screen are for shell scripts. Using Installer Script Variables #!/bin/bash # Detects if /Users is present. If /Users is present, # the chflags command will unhide it if [[ -d "$3/Users" ]]; then chflags nohidden "$3/Users" fi # Detects if /Users/Shared is present. If /Users/Shared is present, # the chflags command will unhide it if [[ -d "$3/Users/Shared" ]]; then chflags nohidden "$3/Users/Shared" fi exit 0 Here’s an example of using the dollar sign three variable in an installer script. In this case, we’re able to take advantage of the Installer telling us which drive the package is being installed on to have our script run actions on the targeted drive. One thing that’s important to know is the closest set of variables to the script is going to win. If your system management tool assigns dollar sign 3 to something different than Installer does, the Installer-assigned meaning will be used by the script inside the package. So, big deal right? I can use scripts as part of installer packages. Great. Why have a session about this? •Fix other installation scripts •Install software which uses a third-party installer •Deploy custom configurations •Run scripts without installing files There are lots of cool ways to use installer scripts. You can use them to fix problems in other people’s installer scripts, install software which doesn’t use any of Apple’s recommended installation methods, deploy configuration files for other installers, and you can even build an installer package which is only a delivery mechanism for scripts. The Luggage Iceberg Packages Free Packaging Tools munkipkg For your packaging needs, there are a number of free tools available. I personally use Stéphane Sudre’s Iceberg and Packages for when I need to manually create a package, but try them all out and use the one that works best for you. Iceberg builds bundle-style installer packages, while Packages builds flat packages. If you need to use source control for your packaging, I recommend using either Munkipkg or The Luggage. Both are free open source tools which allow packages to be built in a consistent and repeatable way using source files and scripts. In the case of the The Luggage, this was a tool originally created by Joe Block, who had written a tool when he worked at Google which used makefiles to generate installers. This allowed the other members of his group to easily review installer package changes before they were put into production. When Joe left Google, he wanted to have a similar tool available, so he wrote and open-sourced The Luggage. Jamf’s Composer is also an available tool for creating packages, and it includes a handy feature for taking "before" and "after" snapshots of your system, where you make the initial snapshot, install your software, then take a second snapshot once the installation is finished. Composer will then generate a list of the files and directories that changed and use those changes to generate a package. The main reason it’s not up on this slide is that it isn’t free. It is available for purchase from Jamf and is also included as part of Jamf’s Jamf Pro Suite. Free Packaging Tools https://youtu.be/BI10WWrgG2A AutoPkg To further automate your packaging, there is another open source tool called AutoPkg. AutoPkg is hugely useful because it’s designed to automate the tasks one would normally perform manually to prepare third-party software for deployment. For those not familiar with AutoPkg, there was a great introductory talk by Anthony Reimer from the University of Calgary this past summer at the Penn State MacAdmins Conference. The talk was posted to YouTube and the link is available at the bottom of the screen. AutoPkg • Downloading an application and/or updates for it, usually via a web browser • Extracting them from a multitude of archive formats • Adding site-specific configuration • Adding sane versioning information • "Fixing" poorly-written installer scripts • Saving these modifications back to a compressed disk image or installer package • Importing these into a software distribution system like Munki, Jamf Pro, FileWave, etc. • Customizing the associated metadata for such a system with site-specific data, post-installation scripts, version info or other metadata While this talk is going to focus on manually building packages to solve specific problems using installer scripts, I want to mention AutoPkg because it often allows you to solve the same problems in a consistent and repeatable fashion, with the added bonus of also automating the download of the applications you need to package. In many cases, I will solve a packaging problem first by manually building and testing a package. Then I will write a recipe file for AutoPkg to automate the building of that same package. For those not familiar with AutoPkg and how it works, a recipe is an XML file which describes a sequence of tasks for AutoPkg to run. These tasks can include downloading some piece of software, building an installer package for that software, and then importing the newly- created installer package into your Mac software management tool. To help illustrate this, I'll be discussing some examples later of how I first solved a problem with manual packaging then later turned it into an AutoPkg recipe. Fixing Other Installation Scripts To give some practical examples, let’s look at how a vendor’s installer package didn’t cover all the possible deployment scenarios and how you can use your own script to fix it. For this scenario, we’ll be working with the Citrix Workspace installer. The Workspace installer package provided by Citrix runs both a preinstall and postinstall script. LOG_FILE_PATH="$HOME/Library/Logs/ReceiverInstall.log" As part of the postinstall script, one of the script variables references HOME. HOME is a variable which is set at login to be the path name for the user’s home directory. Citrix’s working assumption is that someone will be logged in when Citrix Workspace is installed. Scripts must return an exit status of zero However, there will be times when nobody is logged in. In these cases, an error will be generated and the script will return an exit status other than zero. One thing to remember is that installation scripts must return an exit status of zero or else Installer will report failure. How to fix this? By avoiding the error condition. We can do this by writing a script that does three tasks: 1. Verifying that the Citrix-built installer is available at a location defined by the script. 2. If the installer is present, making sure HOME always returns a valid result by resetting HOME to be a value controlled by the script. 3. Running the Citrix-built installer while using the updated HOME value. First step is verifying that the installer is available at the script’s defined location. Next step is trying make sure we match the Citrix installer’s assumption if at all possible. We do that by first figuring out if there’s a logged-in user. After that, figure out the location of the logged-in user’s home folder. Next step is setting the HOME value to match the path of the logged-in user’s home folder. This matches the default behavior of the HOME variable, so it matches the assumptions Citrix made when building their installer. Here’s where we set HOME to always return a value even in those conditions where the Citrix-built installer would error. If a logged-in user can’t be determined, HOME is set to use the var root directory, which is the home directory of the root user on macOS. Last step, the script runs the Citrix-built installer. Now that the script is setting the HOME value to be valid in all conditions, the Citrix-built installer should not error. + Let’s put all of this together into a package. To do this, I’ll be using the script I just described as a postinstall script, a copy of the the latest Citrix Workspace installer and the Packages app to create the package. First step is to open the Packages app and set up a raw package project. Next, we name it and define where the Packages project file will be stored. In this case, I’m naming the package as Citrix Workspace and storing the project files in a Citrix Workspace directory in my home folder. In the Project pane, the main thing I’m checking here is the name to make sure it’s right. As long as that’s correct, there’s no need to change anything here from the defaults. In the Settings pane, we want to require the admin password for installation and on successful installation, we don’t want to do anything else. In the Payload pane, we’re not changing anything from the defaults. In the scripts pane, we need to add the Citrix-built installer under additional resources and add the postinstall script to the post- installation section. “These resources can be used by the pre and post-installation scripts” The reason why we add the Citrix installer to the additional resources section is because Packages will store those resources in the same location as it stores the pre and post install scripts. This allows our script to be able to access the Citrix-built Workspaces installer. @ Packages File Edit View 2sentation rarchy Project Build Window Help Citrix Workspace Project Settings Payload } Scripts | Comments Once the package is built, you can test it by deploying it onto a Mac while that Mac is at the login screen with nobody logged-in. It should install successfully where previously you would have received an error. https://github.com/autopkg/rtrouton-recipes/blob/master/CitrixWorkspace/ CitrixWorkspace.pkg.recipe For those who want to automate this process using AutoPkg, I have written a Citrix Workspace package recipe to create a package just like the one we’ve been talking about. It’s available on GitHub via the address shown on the screen and uses the same postinstall script that I’ve described. Install software which uses a third-party installer There are also vendors who prefer to use installers which don’t use any of Apple’s supported methods. These can be challenging, but you can still package these if you can run the provided installer from the command line. For this scenario, we’ll be working with the Adobe Creative Cloud Desktop app installer. Adobe goes their own way most of the time when it comes to installers and the Creative Cloud Desktop app for macOS is a good example. It uses an Adobe-developed installer and in no way leverages either of Apple’s supported installation methods. /path/to/Install.app/Contents/MacOS/Install --mode=silent That said, Adobe did include a way to run a silent install from the command line. @ Finder File Edit View Go Window Help HQ = With this information, we can build a script which does two tasks: 1. Identify the directory that the script is running from. The Adobe installer will be placed in the same directory. 2. Running the Adobe installer using the silent install mode. First step is identifying the script’s location. Second is running the installer, using the location information to provide the path to the installer. + Now that we have that, let’s put all of this together into a package. To do this, I’ll be using the script I just described as a postinstall script, a copy of the the latest Adobe Creative Cloud Desktop installer, the installer’s support directories and the Packages app to create the package. First step is to open the Packages app and set up a raw package project. Next, we name it and define where the Packages project file will be stored. In this case, I’m naming the package as Adobe Creative Cloud Desktop Installer and storing the project files in a Adobe Creative Cloud Desktop Installer directory in my home folder. In the Project pane, the main thing I’m checking here is the name to make sure it’s right. As long as that’s correct, there’s no need to change anything here from the defaults. In the Settings pane, we want to require the admin password for installation and on successful installation, we don’t want to do anything else. In the Payload pane, we’re not changing anything from the defaults. In the scripts pane, we need to add the Adobe installer and its support directories under additional resources and add the postinstall script to the post-installation section. As mentioned during the Citrix Workspace installer, placing things in additional resources means they can be referenced by pre and post installation scripts. Packages File Edit View Presentat Hierarchy Project Build Window Help Project Adobe Creative Cloud Desktop Installer Settings Payload a Comments Choose. Choose. Once the package is built, you can test it by deploying it onto a Mac that doesn’t have the Adobe Creative Cloud Desktop app installed and verify that the desktop app installs correctly. https://github.com/autopkg/rtrouton-recipes/blob/master/AdobeCreativeCloud/ AdobeCreativeCloudInstaller.pkg.recipe For those who want to automate this process using AutoPkg, I have written a Adobe Creative Cloud Desktop app package recipe to create a package just like the one we’ve been talking about. It’s available on GitHub via the address shown on the screen and uses the same postinstall script that I’ve described. Deploy Custom Configurations You can also use scripts to help deploy a custom software configuration. In many cases, vendors will support placing a configuration file in the same directory as their installer. An example of this is F5 Network’s VPN client. When the F5 installer detects a file with a certain name in the same directory as the installer, it uses the contents of that file to install the VPN configuration along with the software. Filename: config_tmp.f5c The file in question is an XML document configured and named as shown on the screen. You normally shouldn’t have to worry about creating this file, as your VPN admin should be able to provide it to you. A check for a file with the specified name is included as part of a postinstall script included with the F5 VPN installer. The script assumes that the installer and configuration file are located in the same directory. If the configuration file is found, the installer copies it into Library Application Support F5Networks so that the VPN software can detect and use it. Once we have both the configuration file and the VPN installer, we can write a script that does three tasks: 1. Verifying that the configuration file is available at a location defined by the script. 2. Verifying that the VPN installer is available at the same location. 3. Running the VPN installer and verifying it ran successfully. + Now that we have that, let’s put all of this together into a package. To do this, I’ll be using the script I just described as a postinstall script, a copy of the the latest F5 VPN installer, the configuration file and the Packages app to create the package. The process is going to be pretty much identical to our previous examples. First step is to open the Packages app and set up a raw package project. Next, we name it and define where the Packages project file will be stored. In this case, I’m naming the package as F5 VPN Installer and storing the project files in a F5 VPN Installer directory in my home folder. In the Project pane, the main thing I’m checking here is the name to make sure it’s right. As long as that’s correct, there’s no need to change anything here from the defaults. In the Settings pane, we want to require the admin password for installation and on successful installation, we don’t want to do anything else. In the Payload pane, we’re not changing anything from the defaults. In the scripts pane, we need to add the F5 installer and the configuration file under additional resources and add the postinstall script to the post-installation section. As mentioned during the previous examples, placing things in additional resources means they can be referenced by pre and post installation scripts. @ Packages File Edit View Presentation Hierarchy Project Build Window Help ma F5 VPN Installer Project Settings Payload Comments Choose. Name Once the package is built, you can test it by deploying it onto a Mac that doesn’t have the F5 VPN installed and verify that the VPN installs correctly and is configured with the desired setup. Run scripts without installing files The final area I want to talk about is one of my favorites, where you can use an installer package as a delivery mechanism for scripts. No files get installed in this case because the only thing in the installer package is the script. Payload-free packages Payload-free packages is Apple’s term to describe installer packages that install no files and which have been built only to run scripts. As with other installer packages, there are two kinds of payload-free installer packages, bundle-style and flat. Building payload-free packages with pkgbuild http://www.manpagez.com/man/1/pkgbuild/ Apple has built support into its command line pkgbuild tool, which is used to build flat installer packages. Building payload-free packages with pkgbuild pkgbuild --identifier com.identifier.here \ --nopayload \ --scripts /path/to/scripts \ /path/to/package_name_here.pkg The no payload option tells pkgbuild that the package being built will contain only scripts Let’s take a look at how this works by turning the following script into a payload-free package. We’ll start by saving the script inside a directory named scripts as an executable file named postinstall. Not postinstall dot sh, just postinstall. The name must be right or Installer won’t recognize it. The reason for the scripts directory is that pkgbuild's scripts option is set to look for a directory with scripts inside, rather than specifying the scripts themselves. pkgbuild --identifier com.company.sysdiagnose \ --nopayload \ --scripts /path/to/scripts \ /path/to/sysdiagnose_gathering.pkg Once the scripts directory is set up and the postinstall script saved inside it, running the following command with root privileges will build a payload-free package and store it in the designated location. Once the package is built, you can hand it off to whatever or whoever needs it and they’ll be able to execute the script without knowing anything more than how to install a package. Hang on, where's the version number? pkgbuild --identifier com.identifier.here \ --nopayload \ --scripts /path/to/scripts \ /path/to/package_name_here.pkg For those familiar with pkgbuild, you may think I've left something out - The version number. Actually, I didn't leave it out. When you're building payload-free packages using the nopayload option, the version number isn't required. How come? Payload-free flat packages may not leave installer receipts When building a payload-free package with Apple’s pkgbuild tool using the nopayload flag, no receipt is left behind. No receipt, no need for a version number. Payload-free flat packages may not leave installer receipts When I filed a bug on this, Apple said that this was intended behavior for payload-free packages built using pkgbuild's nopayload option. In Apple’s opinion, payload-free packages are a convenient bag for scripts. Any other type of package will leave behind a receipt, including a bundle-style payload-free package. Why is this important? If your systems management tools relies on receipts to tell whether a payload-free package has been installed, a payload-free package that leaves no receipt behind means that your management tool won’t be able to tell that it’s been installed. This may result in the payload-free package and its associated script being run repeatedly on your managed machine. pkgbuild --identifier com.identifier.here \ --root /path/to/empty_directory \ --scripts /path/to/scripts --version 1.0 \ /path/to/package_name_here.pkg Making payload-free flat packages which leave installer receipts However, you can make a package with pkgbuild that, while not technically payload-free, will act just like one. The key is to create an empty directory and set pkgbuild’s –root option to look there for files. pkgbuild’s –root option is used to tell pkgbuild which files to package, but since there will be no files in an empty directory, the package will install no files on the destination Mac. However, it will leave behind a receipt. @ Finder File Edit View Go Window Help £Oe04%8D §) Seecooee https://github.com/rtrouton/payload-free_package_printer_generator Payload-Free Package Printer Generator Another tool I've developed is a script used to build payload-free packages, where the package sets up a printer with a desired configuration. • -n: Name of the print queue. May not contain spaces, tabs, # or / characters. (required) • -l: The physical location of the printer. Examples may include Reception Desk, Librarian's Office or Second Floor, Room 2C456 (optional) • -d: The printer name which is displayed in the Printers & Scanners pane of System Preferences, as well as in the print dialogue boxes. (required) • -a: The IP or DNS address of the printer. Protocol must be specified as part of the address (for example, use lpd://ip.address.goes.here or lpd://dns.address.goes.here for LPR printing.) (required) • -p: Name of the driver file in /Library/Printers/PPDs/Contents/Resources/. This must use the full path to the drive (starting with /Library). (required) • -1: Specify first printer option. (optional) • -2: Specify second printer option. (optional) • -3: Specify third printer option. (optional) • -4: Specify fourth printer option. (optional) • -5: Specify fifth printer option. (optional) • -6: Specify sixth printer option. (optional) • -7: Specify seventh printer option. (optional) • -8: Specify eighth printer option. (optional) • -9: Specify ninth printer option. (optional) • -c: Name of the Apple Developer ID Installer certificate being used to sign the payload- free package. Certificate name should be formatted like Developer ID Installer: Your Name or Developer ID Installer: Your Name (F487797D). (optional) Payload-Free Package Printer Generator This script has a number of options, including an option to sign the payload-free package using an Apple Developer certificate. Signing the package would enable the package to be posted somewhere for download and get past Gatekeeper. •-n: ReceptionDeskBrotherLaserPrinter •-l: Reception Desk •-d: Reception Desk Brother Laser Printer •-a: lpd://192.168.1.121 •-p: "/Library/Printers/PPDs/Contents/ Resources/Brother DCP-L2540DW series CUPS.gz" While this tool has a lot of options, you may only need to use a few of them to set up a printer. Let's take a look at how this works using only these options. /path/to/payload-free_package_printer_generator.sh \ -n ReceptionDeskBrotherLaserPrinter \ -l "Reception Desk" \ -d "Reception Desk Brother Laser Printer" \ -a lpd://192.168.1.121 \ -p "/Library/Printers/PPDs/Contents/Resources/Brother DCP-L2540DW series CUPS.gz" Here's how the command to run the script would look. I have it broken up into separate lines for clarity, normally this would all be on one line. @ Finder File Edit View Go Window Help Pili. gL Oe204S8O (|) sef0o0ee8 The build process covered up the Terminal window, but here's the output you should see. @ Finder File Edit View Go Window Help Sie) ¥ 3.000880 = eccooee pkgutil Suspicious Package Pacifist Examining Scripts Now that you’ve seen how you can use scripts, how can you check out existing installer package scripts to see what they’re doing? There’s a few tools that can help you out. There’s Apple’s pkgutil command line tool, Pacifist and Suspicious Package. pkgutil and Suspicious Package are both free while Pacifist is shareware with a free trial period. Since both are free with no strings attached, let’s look at pkgutil and Suspicious Package. Expanding packages with pkgutil pkgutil --expand \ /path/to/package_name_here.pkg \ /path/to/directory_goes_here pkgutil doesn’t directly examine scripts for you, but it expands the contents of an installer package into a directory. From there, you can examine the scripts included with the package. pkgutil doesn’t directly examine scripts for you, but it expands the contents of an installer package into a directory. From there, you can examine the scripts included with the package. Suspicious Package Suspicious Package is a phenomenally useful tool, for reasons that will be shown in a second. It’ll check out an installer package for you, show you its contents and even warn you if there’s problems. @ Finder File Edit View Go Window Help LO@eO4tBS © /BtlCooee © Understanding Payload-Free Packages: https:// derflounder.wordpress.com/2014/06/01/understanding- payload-free-packages/ Creating payload-free packages with pkgbuild: https:// derflounder.wordpress.com/2012/08/15/creating-payload-free- packages-with-pkgbuild/ Apple Developer Software Delivery Legacy Guide: http:// tinyurl.com/hze8pr8 Flat Package Format - The missing documentation: http:// s.sudre.free.fr/Stuff/Ivanhoe/FLAT.html Useful Links Useful Links Preparing EndNote X8 for deployment using AutoPkg: https:// derflounder.wordpress.com/2016/11/15/preparing-endnote-x8- for-deployment-using-autopkg/ Creating a DNAStar Lasergene 13.x installer: https:// derflounder.wordpress.com/2016/03/17/creating-a-dnastar- lasergene-13-x-installer/ Deploying a pre-configured Junos Pulse VPN client on OS X: https://derflounder.wordpress.com/2015/03/13/deploying-a- pre-configured-junos-pulse-vpn-client-on-os-x/ Repackaging the LabVIEW 2013 Pro installer: https:// derflounder.wordpress.com/2013/12/06/repackaging-the- labview-2013-pro-installer/ Pacifist: https://charlessoft.com Suspicious Package: https://mothersruin.com/software/ SuspiciousPackage/ Iceberg: http://s.sudre.free.fr/Software/Iceberg.html Packages: http://s.sudre.free.fr/Software/Packages/ about.html Useful Links The Luggage: http://luggage.apesseekingknowledge.net MunkiPkg: https://github.com/munki/munki-pkg Jamf Composer: https://www.jamf.com/products/jamf- composer/ Payload-Free Package Creator: https://github.com/rtrouton/ Payload-Free-Package-Creator Useful Links Payload-Free Package Printer Generator: https://github.com/ rtrouton/payload-free_package_printer_generator Downloads PDF available from the following link: https://tinyurl.com/PSUMacAdmins2019PDF Keynote slides available from the following link: https://tinyurl.com/PSUMacAdmins2019Keynote