├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── MDM ├── Jamf-lastrun-EA.sh ├── ReadMe.md ├── patchomator JAMF.pdf └── patchomator jamf pro execution script.sh ├── README.md ├── images ├── patch-o-mater-icon.png ├── patch-o-mater-large.png ├── patchomator-banner.png └── progress-dialog.png ├── patchomator.example.plist └── patchomator.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: option8 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bbprojectd 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Charles Mangin macnerd@n-able.com 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MDM/Jamf-lastrun-EA.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Contributed by Michael Zukrow ("@Michael Z" on Macadmins Slack) 4 | 5 | 6 | logPATH="/private/var/log/Patchomator.log" 7 | lastLine=$( tail -n 1 $logPATH | cut -d" " -f 1,2 ) 8 | echo "last line is " + $lastLine 9 | if [ $lastLine = "Patchomator finished:" ]; 10 | then 11 | lastRun=$( tail -n 1 $logPATH | cut -d" " -f3 ) 12 | echo "$lastRun" 13 | else 14 | echo "error" 15 | fi 16 | 17 | exit 0 -------------------------------------------------------------------------------- /MDM/ReadMe.md: -------------------------------------------------------------------------------- 1 | # Contributed MDM Utilities 2 | 3 | ## Jamf-lastrun-EA 4 | ### Contributed by Michael Zukrow ("@Michael Z" on Macadmins Slack) 5 | 6 | Add this Extension Attributes to JAMF to get date of last run from your end points 7 | You can use this to create smart groups to scope actions 8 | for example you could offer Installomator as a self service policy and as an auto run policy 9 | If a user runs the self service it would update the EA and smart group so the auto run doesn't occur in window 10 | allowing more flexibility to keep end points updated and give users a better experience 11 | 12 | ## patchomator jamf pro execution script 13 | ### Contributed by Jordy Thery 14 | 15 | This script will check if Installomator and Patchomator are found locally in their respecitve folders. 16 | If found, it will execute Patchomator silently (--yes) to install (--install) updates if found. 17 | 18 | Jamf Pro script parameters 19 | $4 = ignored labels (space separated) 20 | $5 = required labels (space separated). 21 | 22 | This could be adapted to replace triggers at the end of the script by more Jamf Pro script parameters to have more control. 23 | -------------------------------------------------------------------------------- /MDM/patchomator JAMF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mac-Nerd/patchomator/ee4ec87d4eedaa4b1029df9d596561f9b4405e84/MDM/patchomator JAMF.pdf -------------------------------------------------------------------------------- /MDM/patchomator jamf pro execution script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # 3 | # This script will check if Installomator and Patchomator are found locally in their respecitve folders. 4 | # If found, it will execute Patchomator silently (--yes) to install (--install) updates if found. 5 | 6 | # Jamf Pro script parameters 7 | # $4 = ignored labels (space separated) 8 | # $5 = required labels (space separated). 9 | 10 | # This could be adapted to replace triggers at the end of the script by more Jamf Pro script parameters to have more control. 11 | 12 | InstallomatorPath="/usr/local/Installomator/Installomator.sh" 13 | PatchomatorPath="/usr/local/Installomator/patchomator.sh" 14 | # 15 | # Check if Installomator is found locally. 16 | # 17 | if [ ! -f $InstallomatorPath ] 18 | then 19 | echo "Installomator is not installed. Exiting." 20 | exit 1 21 | fi 22 | echo "Installomator is installed. Continuing." 23 | 24 | # 25 | # Check if Patchomator is found locally. 26 | # 27 | if [ ! -f $PatchomatorPath ] 28 | then 29 | echo "Patchomator is not installed. Exiting." 30 | exit 2 31 | fi 32 | echo "Patchomator is installed. Continuing." 33 | 34 | # 35 | # Run Patchomator silently with optional exclusions and requirements. 36 | # This uses Jamf Pro script parameter 4 for ignored labels and parameter 5 for required labels. 37 | # 38 | echo "Running \"patchomator.sh --yes --install --ignored --required\"" 39 | zsh "$PatchomatorPath" --yes --install --ignored "$4" --required "$5" 40 | echo "Patchomator script has finished. Exiting." 41 | exit 0 42 | 43 | # 44 | # End of file. 45 | # 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Patchomator icon and text](https://github.com/Mac-Nerd/patchomator/blob/1.1/images/patchomator-banner.png?raw=true) 2 | 3 | # Patchomator 4 | A management script for Installomator. Work in progress. 5 | 6 | ## What does it do? 7 | Installomator uses small scripts designed to check if a specified app is installed, and if so, what version. If the latest version is not already installed, Installomator proceeds to download and install the necessary update. Each of these script fragments is a "label" used to call Installomator to install or update a particular app. 8 | 9 | Patchomator extends Installomator into a more general purpose patching tool. The Patchomator script processes all of the available labels, and uses them to determine which apps are already installed on the Mac. This list is passed to Installomator to proceed with updating any that are out of date, and optionally saved in a configuration file to speed up subsequent runs. 10 | 11 | Large portions of Patchomator code came directly from Installomator: 12 | https://github.com/Installomator/Installomator 13 | 14 | _Installomator is Copyright 2020 Armin Briegel, Scripting OS X_ 15 | 16 | 17 | ## Installation 18 | 19 | Download the latest PKG installer from the [Releases page](https://github.com/Mac-Nerd/patchomator/releases). 20 | 21 | Or you can download or clone this repo, copy or move `patchomator.sh` to the same location as Installomator, and set it executable. 22 | 23 | ``` 24 | curl -LO https://github.com/Mac-Nerd/patchomator/raw/main/patchomator.sh 25 | chmod a+x patchomator.sh 26 | sudo mv patchomator.sh /usr/local/Installomator/ 27 | ``` 28 | 29 | 30 | ## Usage 31 | 32 | ### Command line 33 | 34 | `./patchomator.sh` 35 | *Dry Run.* Without the `--install` option, Patchomator will run interactively, and search the system for applications that can be upgraded by Installomator. The user will be prompted to yes/no when duplicate or ambiguous app names are found. 36 | 37 | `--yes` 38 | The `--yes` option will accept the default or first choice at each prompt. Frustratingly, this is sometimes "No". 39 | 40 | When finished, the script will output its findings but not install anything. The configuration won't be saved unless `--write` is specified. 41 | 42 | `--write` 43 | Runs as normal, and creates or updates a configuration file at the default plist path `/Library/Application Support/Patchomator/patchomator.plist`. 44 | 45 | `./patchomator.sh --read` 46 | *Read Config.* 47 | Displays the current configuration, based on an existing configuration file at the default plist path `/Library/Application Support/Patchomator/patchomator.plist`. 48 | 49 | `./patchomator.sh --install [ --ignored "label1 label2" --required "label3 label4" ]` 50 | 51 | *Install mode.* Scans the system for installed apps and matches them to Installomator labels. Launches Installomator to update any that are not current. If an existing configuration file is found, it will be used to skip the discovery step. *Test before use.* 52 | 53 | 54 | *Additional switches.* 55 | `--ignored "list of labels to ignore"` 56 | `--required "list of labels to require"` 57 | Optional lists of ignored and/or required labels can be added to fine-tune the installation operation. See the section "*Ignored and Required Labels*" for more details. 58 | 59 | `--everywhere` Allows for searching the entire filesystem for applications during discovery. By default, apps installed in /Applications, /usr/local and /Library are discovered. 60 | 61 | `--config "path to config file"` Override the default configuration file location for `--read --write` or `--install` options. 62 | 63 | `--pathtoinstallomator "path to Installomator.sh"` Overrides the default Installomator path for `--install` option. 64 | 65 | `--options "option1=value option2=value ..."` Command line options to pass to Installomator during installation mode. Multiple command line option should be separated by spaces, and inside quotes. For more information, see the [Installomator Wiki](https://github.com/Installomator/Installomator/wiki/Configuration-and-Variables) 66 | 67 | `-s` | `--skipverify` Skips the signature verification step for discovered apps. *Does not skip verifying on installation.* 68 | 69 | `-q` | `--quiet` *Quiet mode*. Minimal output. 70 | 71 | `-v` | `--verbose` *Verbose mode*. Logs more information to stdout. Overrides -q 72 | 73 | `-h` | `--help` Show usage message and exits. 74 | 75 | 76 | When run, Patchomator will prompt you to install Installomator, if it doesn't already exist at the default path or the one specified with `-p [InstallomatorPATH]`. Patchomator will happily run without Installomator, but won't actually install any updates by itself. 77 | 78 | If the Installomator label files are not present, or are older than 30 days, they will be downloaded from the latest Installomator release on GitHub and put in a directory called "fragments" in the same directory as patchomator.sh 79 | 80 | ### Configuration 81 | 82 | When written using the `--write` option, the file `patchomator.plist` contains a list of the applications found on the system, and the corresponding Installomator labels which can be used to install or update each. By default, Patchomator will look for its configuration file in `/Library/Preferences/Patchomator` but the full path can be overridden with the `-c` or `--config` command line switch. 83 | 84 | The current state of the configuration can be read with the following command 85 | 86 | ``` 87 | defaults read /Library/Application\ Support/Patchomator/patchomator.plist 88 | 89 | { 90 | "/Applications/1Password 7.app" = 1password7; 91 | "/Applications/BBEdit.app" = bbedit; 92 | "/Applications/Discord.app" = discord; 93 | "/Applications/Dropbox.app" = dropbox; 94 | "/Applications/Suspicious Package.app" = suspiciouspackage; 95 | "/Applications/Utilities/DEPNotify.app" = depnotify; 96 | "/Applications/VLC.app" = vlc; 97 | IgnoredLabels = ( 98 | googlechrome, 99 | firefox, 100 | zoom 101 | ); 102 | RequiredLabels = ( 103 | depnotify, 104 | gotomeeting 105 | ); 106 | } 107 | 108 | ``` 109 | 110 | or by running Patchomator.sh with the `-r` or `--read` command line switch. 111 | 112 | This will also display two lists of Installomator labels, marked `IgnoredLabels` and `RequiredLabels`. See the next section for details. 113 | 114 | ### MDM instructions 115 | 116 | More detail coming soon. For now, have a look at [the MDM folder](https://github.com/Mac-Nerd/patchomator/tree/main/MDM) for a starting point. 117 | 118 | If you currently use Patchomator with an MDM, please [open an issue](https://github.com/Mac-Nerd/patchomator/issues) and let me know if you have any questions, or want to share your setup. 119 | 120 | 121 | ### Ignored and Required Labels 122 | 123 | By default, if Patchomator detects an application is installed that corresponds to a known label, it will be added to the configuration and updated on subsequent runs. However, there are some apps that are best left alone - either to be updated via some other mechanism, or to be kept at a specific stable version. There are also apps that will match to multiple labels. For example, "Firefox.app" can be installed by any of the following labels: firefox_da, firefox_intl, firefox, firefoxdeveloperedition, firefoxesr_intl, firefoxesr, firefoxpkg_intl, firefoxpkg. To prevent an update clobbering an installed app, or grabbing the wrong version of an ambiguous one, labels can be set to "ignored". 124 | 125 | Specific labels can be ignored in two ways. First, the `IgnoredLabels` array can be added to an existing `patchomator.plist` with the following command 126 | 127 | ```defaults write /path/to/patchomator.plist IgnoredLabels -array label1 label2 label3``` 128 | 129 | _Note: This will replace any existing `IgnoredLabels` array that already exists in the plist._ 130 | 131 | Alternately, you may ignore labels at runtime by listing them on the command line with the `--ignored` command line switch. The list of labels follows `--ignored` as a quoted string, separated by spaces. 132 | 133 | ```patchomator.sh --ignored "googlechrome googlechromeenterprise zoomclient zoomgov"``` 134 | 135 | Like ignored labels, you can also specify required labels. These are useful for apps that you want to be certain are consistently installed on every system, and reinstalled if they have been moved or uninstalled. 136 | 137 | The `RequiredLabels` array works the same way in `patchomator.plist` as `IgnoredLabels` 138 | 139 | ```defaults write /path/to/patchomator.plist RequiredLabels -array label1 label2 label3``` 140 | 141 | and as a one-time switch on the command line with `--required` 142 | 143 | ```patchomator.sh --required "googlechromepkg zoom"``` 144 | 145 | 146 | ## Swift Dialog 147 | 148 | As of 1.1, Patchomator will display progress and other messages via [Swift Dialog](https://github.com/swiftDialog/swiftDialog), if the system has it installed. 149 | 150 | ![Patchomator dialog example](https://github.com/Mac-Nerd/patchomator/blob/1.1/images/progress-dialog.png?raw=true) 151 | 152 | You can suppress these dialogs with the `--quiet` command line switch. 153 | 154 | Currently, the dialogs cannot be customized. If you would like to be able to, please [open an issue](https://github.com/Mac-Nerd/patchomator/issues) and let me know. 155 | 156 | 157 | ## Patching with Patchomator 158 | 159 | ### Run discovery 160 | 161 | ``` 162 | % sudo /usr/local/Installomator/patchomator.sh 163 | 164 | Package labels not present at /usr/local/Installomator/fragments. Attempting to download from https://github.com/installomator/ 165 | Downloading https://api.github.com/repos/Installomator/Installomator/tarball/v10.3 to /usr/local/Installomator/installomator.latest.tar.gz 166 | Extracting /usr/local/Installomator/installomator.latest.tar.gz into /usr/local/Installomator 167 | Processing label 1password7. 168 | Found 1Password 7.app version 7.9.10 169 | Processing label 1password8. 170 | Processing label 1passwordcli. 171 | Processing label 4kvideodownloader. 172 | Processing label 8x8. 173 | [...] 174 | Processing label bbedit. 175 | Found BBEdit.app version 14.6.5 176 | Processing label bbeditpkg. 177 | Found BBEdit.app version 14.6.5 178 | /Applications/BBEdit.app already linked to label bbedit. 179 | Replace label bbedit with bbeditpkg? [y/N] y 180 | Replacing. 181 | Processing label betterdisplay. 182 | Processing label bettertouchtool. 183 | [...] 184 | Processing label googlechrome. 185 | Found Google Chrome.app version 112.0.5615.49 186 | Processing label googlechromeenterprise. 187 | Found Google Chrome.app version 112.0.5615.49 188 | /Applications/Google Chrome.app already linked to label googlechrome. 189 | Replace label googlechrome with googlechromeenterprise? [y/N] n 190 | Skipping. 191 | Processing label googlechromepkg. 192 | Found Google Chrome.app version 112.0.5615.49 193 | /Applications/Google Chrome.app already linked to label googlechrome. 194 | Replace label googlechrome with googlechromepkg? [y/N] y 195 | Replacing. 196 | [...] 197 | Completed with 13 errors. 198 | 199 | 200 | Currently configured labels: 201 | obs 202 | handbrake 203 | suspiciouspackage 204 | dropbox 205 | apparency 206 | discord 207 | blender 208 | bbeditpkg 209 | lulu 210 | depnotify 211 | thunderbird 212 | tunnelblick 213 | gotomeeting 214 | 1password7 215 | visualstudiocode 216 | brave 217 | vlc 218 | zoom 219 | utm 220 | signal 221 | knockknock 222 | hancock 223 | googlechromepkg 224 | 225 | Ignored Labels: 226 | 227 | Required Labels: 228 | ``` 229 | 230 | ### Write configuration 231 | 232 | ``` % sudo /usr/local/Installomator/patchomator.sh --write 233 | 234 | No config file at /Library/Application Support/Patchomator/patchomator.plist. Creating one now. 235 | 236 | File Doesn't Exist, Will Create: /Library/Application Support/Patchomator/patchomator.plist 237 | Initializing Plist... 238 | Package labels installed. Last updated 0 days ago. 239 | Processing label 1password7. 240 | Found 1Password 7.app version 7.9.10 241 | Processing label 1password8. 242 | [...] 243 | 244 | Completed with 13 errors. 245 | 246 | 247 | Currently configured labels: 248 | /Applications/1Password 7.app 1password7 249 | /Applications/Apparency.app apparency 250 | /Applications/BBEdit.app bbeditpkg 251 | /Applications/Brave Browser.app brave 252 | /Applications/Discord.app discord 253 | /Applications/Dropbox.app dropbox 254 | /Applications/GoToMeeting.app gotomeeting 255 | /Applications/Google Chrome.app googlechromepkg 256 | /Applications/Hancock.app hancock 257 | /Applications/HandBrake.app handbrake 258 | /Applications/KnockKnock.app knockknock 259 | /Applications/LuLu.app lulu 260 | /Applications/OBS.app obs 261 | /Applications/Signal.app signal 262 | /Applications/Suspicious Package.app suspiciouspackage 263 | /Applications/Thunderbird.app thunderbird 264 | /Applications/Tunnelblick.app tunnelblick 265 | /Applications/UTM.app utm 266 | /Applications/Utilities/DEPNotify.app depnotify 267 | /Applications/VLC.app vlc 268 | /Applications/Visual Studio Code.app visualstudiocode 269 | /Applications/blender.app blender 270 | /Applications/zoom.us.app zoom 271 | IgnoredLabels 272 | 273 | RequiredLabels 274 | ``` 275 | 276 | ### Install and update 277 | 278 | ``` 279 | % sudo /usr/local/Installomator/patchomator.sh --install 280 | 281 | [ERROR] Installomator was not found at /usr/local/Installomator/Installomator.sh 282 | Patchomator can still discover apps on the system and create a configuration for later use, but will not be able to install or update anything without Installomator. 283 | Download and install Installomator now? [y/N] y 284 | 285 | installer: Package name is 286 | installer: Upgrading at base path / 287 | installer: Preparing for installation…..... 288 | installer: Preparing the disk…..... 289 | installer: Preparing …..... 290 | installer: Waiting for other installations to complete…..... 291 | installer: Configuring the installation…..... 292 | installer: 293 | # 294 | installer: Validating packages…..... 295 | # 296 | installer: Running installer actions… 297 | installer: 298 | installer: Finishing the Installation…..... 299 | installer: 300 | # 301 | installer: The software was successfully installed...... 302 | installer: The upgrade was successful. 303 | Existing config found at /Library/Application Support/Patchomator/patchomator.plist. 304 | Passing 25 labels to Installomator. 305 | Installing 1password7... 306 | 2023-04-26 16:46:41 : INFO : 1password7 : setting variable from argument BLOCKING_PROCESS_ACTION=tell_user 307 | 2023-04-26 16:46:41 : INFO : 1password7 : setting variable from argument NOTIFY=success 308 | 2023-04-26 16:46:41 : REQ : 1password7 : ################## Start Installomator v. 10.3, date 2023-02-10 309 | 2023-04-26 16:46:41 : INFO : 1password7 : ################## Version: 10.3 310 | [...] 311 | 2023-04-26 16:46:42 : REQ : 1password7 : ################## End Installomator, exit code 0 312 | 313 | ``` 314 | 315 | ## Help! It's not working! 316 | 317 | Sorry about that. If you're willing and able to help test, please report any problems by [opening an issue](https://github.com/Mac-Nerd/patchomator/issues). And if you can see where I've messed something up, I'm open to pull requests. 318 | 319 | 320 | -------------------------------------------------------------------------------- /images/patch-o-mater-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mac-Nerd/patchomator/ee4ec87d4eedaa4b1029df9d596561f9b4405e84/images/patch-o-mater-icon.png -------------------------------------------------------------------------------- /images/patch-o-mater-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mac-Nerd/patchomator/ee4ec87d4eedaa4b1029df9d596561f9b4405e84/images/patch-o-mater-large.png -------------------------------------------------------------------------------- /images/patchomator-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mac-Nerd/patchomator/ee4ec87d4eedaa4b1029df9d596561f9b4405e84/images/patchomator-banner.png -------------------------------------------------------------------------------- /images/progress-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mac-Nerd/patchomator/ee4ec87d4eedaa4b1029df9d596561f9b4405e84/images/progress-dialog.png -------------------------------------------------------------------------------- /patchomator.example.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | /Applications/1Password 7.app 6 | 1password7 7 | /Applications/BBEdit.app 8 | bbedit 9 | /Applications/Discord.app 10 | discord 11 | /Applications/Dropbox.app 12 | dropbox 13 | /Applications/Suspicious Package.app 14 | suspiciouspackage 15 | /Applications/Utilities/DEPNotify.app 16 | depnotify 17 | /Applications/VLC.app 18 | vlc 19 | IgnoredLabels 20 | 21 | googlechrome 22 | firefox 23 | zoom 24 | 25 | RequiredLabels 26 | 27 | depnotify 28 | gotomeeting 29 | 30 | 31 | -------------------------------------------------------------------------------- /patchomator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Version: 2025.06.09 - 1.1.3b3 4 | # "April Foolish" 5 | 6 | # Gigantic Thanks to: 7 | # rondelltron 8 | # Skinflint 9 | 10 | # Big Thanks to: 11 | # Adam Codega 12 | # @tlark 13 | # @mickl089 14 | # Shad Hass 15 | # Derek McKenzie 16 | # Armin Briegel 17 | # Jordy Thery 18 | # Trevor Sysock 19 | # Michael Zukrow 20 | # Sjur Lohne 21 | # Max Roy 22 | 23 | # To Fix: 24 | 25 | 26 | # To Do: 27 | # Add MDM optimized Non-interactive Mode --mdm "MDMName" 28 | # apps installed in other weird locations should be identifiable by their pkg receipt. 29 | 30 | # Recent Changes/Fixes: 31 | # Consistent messages for exiting and logging 32 | # Set maximum rolled logs to 5 by default. Configured via backupLogsMax 33 | # Roll logs if greater than 1MB by default. Configured via logSizeMax in bytes 34 | # Use appCustomVersion from label file for a check 35 | # Detect Swift Dialog 36 | # remove extra spaces, and use requiredLabelsList 37 | # 1.1.2 Installomator 10.8 version check 38 | # Only search for apps in /Applications by default, optionally --everywhere 39 | # Passing installomator options with spaces in. 40 | # Automatically ignore labels that conflict with required ones 41 | # Swift Dialog support 42 | # labels with dashes. Seriously. 43 | # Added logging to /var/log/Patchomator.log 44 | # Interactive mode overhaul, automatically adding skipped labels as ignored 45 | # 1.1 Ignored labels from CLI added into preferences on --write 46 | # [speed] --skip-verify to skip the step of verifying discovered apps. Does *not* skip the verification on install. 47 | # [speed] Defer verification step until discovery is complete. Parallelize as much as possible. 48 | # Offers to install Installomator update, but requires user intervention. 49 | # On --write, add any found label to the config, even if the latest version is installed 50 | # Messaging for missing config file on --write 51 | # Respects --installomatoroptions setting for ignoring App Store apps (or not) 52 | 53 | # Older: 54 | # Add --ignored "all" option to skip discovery all together 55 | # Add --installomatoroptions to pass options to installomator 56 | # Turn off pretty printed formatting for --quiet 57 | # Monterey fix for working path 58 | # Major overhaul based on MacAdmins #patchomator feedback 59 | # 7 days -> 30 days 60 | # Added required/excluded keys in preference file 61 | # system-level config file for running via sudo, or deploying via MDM 62 | # git and Xcode tools are optional now. Did you know GitHub has a pretty decent API? 63 | # No longer requires root for normal operation. (thanks, @tlark) 64 | # Downloads XCode Command Line Tools to provide git (Thanks Adam Codega) 65 | # Install package/github release 66 | # add back installomator install steps 67 | # use release version of installomator, not dev. (Thanks Adam Codega) 68 | # selfupdate when labels are older than 7 days 69 | # parse label name, expectedTeamID, packageID 70 | # match to codesign -dvvv of *.app 71 | # packageID to Identifier 72 | # expectedTeamID to TeamIdentifier 73 | # added quiet mode, noninteractive mode 74 | # choose between labels that install the same app (firefox, etc) 75 | # - offer user selection 76 | # - pick the first match (noninteractive mode) 77 | # on duplicate labels, skip subsequent verification 78 | # on -I, parse generated config, pipe to Installomator to install updates 79 | # Installomator requires root 80 | 81 | # NGD: 82 | # self-update switch branches from release to latest source 83 | 84 | 85 | 86 | if [ -z "${ZSH_VERSION}" ]; then 87 | >&2 echo "[ERROR] This script is only compatible with Z shell (/bin/zsh). Re-run with" 88 | echo "\t zsh patchomator.sh" 89 | exit 1 90 | fi 91 | 92 | # Environment checks 93 | 94 | OSVERSION=$(defaults read /System/Library/CoreServices/SystemVersion ProductVersion | awk '{print $1}') 95 | OSMAJOR=$(echo "${OSVERSION}" | cut -d . -f1) 96 | OSMINOR=$(echo "${OSVERSION}" | cut -d . -f2) 97 | 98 | 99 | if [[ $OSMAJOR -lt 11 ]] && [[ $OSMINOR -lt 13 ]] 100 | then 101 | echo "[ERROR] Patchomator requires MacOS 10.13 or higher." 102 | exit 1 103 | fi 104 | 105 | 106 | # Check your privilege 107 | if [ $(whoami) = "root" ] 108 | then 109 | IAMROOT=true 110 | else 111 | IAMROOT=false 112 | fi 113 | 114 | 115 | # log levels from Installomator/fragments/arguments.sh 116 | 117 | if [[ $DEBUG -ne 0 ]]; then 118 | LOGGING=DEBUG 119 | elif [[ -z $LOGGING ]]; then 120 | LOGGING=INFO 121 | datadogLoggingLevel=INFO 122 | fi 123 | 124 | logPATH="/private/var/log/Patchomator.log" 125 | backupLogsMax=5 126 | logSizeMax=$((1024 * 1024)) # 1 MB in bytes 127 | 128 | declare -A levels=(DEBUG 0 INFO 1 WARN 2 ERROR 3 REQ 4) 129 | declare -A configArray=() 130 | 131 | declare -A InstallomatorOptions=() 132 | 133 | declare -A foundLabelsArray=() 134 | declare -A ignoredLabelsArray=() 135 | declare -A requiredLabelsArray=() 136 | 137 | 138 | 139 | # default paths 140 | export PATH=/usr/bin:/bin:/usr/sbin:/sbin 141 | 142 | InstallomatorPATH=("/usr/local/Installomator/Installomator.sh") 143 | defaultConfigfile=("/Library/Application Support/Patchomator/patchomator.plist") 144 | managedConfigfile=("/Library/Managed Preferences/com.mac-nerd.patchomator.plist") 145 | #patchomatorPath=$(dirname $(realpath $0)) # default install at /usr/local/Installomator/ 146 | 147 | # "realpath" doesn't exist on Monterey. 148 | patchomatorPath="/usr/local/Installomator/" 149 | 150 | fragmentsPATH=("$patchomatorPath/fragments") 151 | 152 | # Pretty print, ignored if no terminal (eg, running via MDM) 153 | BOLD=$(tput bold 2>/dev/null) 154 | RESET=$(tput sgr0 2>/dev/null) 155 | RED=$(tput setaf 1 2>/dev/null) 156 | YELLOW=$(tput setaf 3 2>/dev/null) 157 | 158 | [[ -f /usr/local/bin/dialog ]] && DialogPATH="/var/tmp/dialog.log" || DialogPATH="/dev/null" 159 | 160 | ####################################### 161 | # Functions 162 | 163 | usage() { 164 | echo "\n${BOLD}Usage:${RESET}" 165 | echo "\tpatchomator.sh [ -ryqvIh -c configfile -p InstallomatorPATH ]\n" 166 | echo "${BOLD}Default:${RESET}" 167 | echo "\tScans the system for installed apps and matches them to Installomator labels." 168 | echo "\t${BOLD}--ignored \"space-separated list of labels to ignore\"" 169 | echo "\t${BOLD}--required \"space-separated list of labels to require\"" 170 | echo "\t${BOLD}-w | --write \t${RESET} Write Config. Creates a new config file or refreshes an existing one." 171 | echo "\t${BOLD}-r | --read \t${RESET} Read Config. Parses and displays an existing config file. \n\tDefault path ${YELLOW}$defaultConfigfile${RESET}" 172 | echo "\t${BOLD}-c | --config \"path to config file\" \t${RESET} Overrides default configuration file location." 173 | echo "\t${BOLD}-e | --everywhere\t${RESET} Search the entire filesystem for matching apps." 174 | echo "\t${BOLD}-y | --yes \t${RESET} Non-interactive mode. Accepts the default (usually nondestructive) choice at each prompt. Use with caution." 175 | echo "\t${BOLD}-q | --quiet \t${RESET} Quiet mode. Minimal output." 176 | echo "\t${BOLD}-v | --verbose \t${RESET} Verbose mode. Logs more information to stdout. Overrides ${BOLD}--quiet${RESET}" 177 | echo "\t${BOLD}-s | --skipverify \t${RESET} Skips the signature verification step for discovered apps. ${BOLD}Does not skip verifying on installation.${RESET}" 178 | echo "\t${BOLD}-I | --install \t${RESET} Install mode. This parses an existing configuration and sends the commands to Installomator to update. ${BOLD}Requires sudo${RESET}" 179 | echo "\t${BOLD}-p | --pathtoinstallomator \"path to Installomator.sh\"${RESET}\n\tDefault Installomator Path ${YELLOW}/usr/local/Installomator/Installomator.sh${RESET}" 180 | echo "\t${BOLD}-o | --options \"option1=value option2=value ...\"${RESET}\n\tCommand line options passed through to Installomator.${RESET}" 181 | echo "\t${BOLD}-h | --help \t${RESET} Show this text and exit.\n" 182 | echo "${YELLOW}See readme for more options and examples: ${BOLD}https://github.com/mac-nerd/Patchomator${RESET}" 183 | exit 0 184 | } 185 | 186 | caffexit () { 187 | kill "$caffeinatepid" 188 | echo "quit:" >> $DialogPATH 189 | finishAndExit $1 190 | } 191 | 192 | finishAndExit () { 193 | echo "Patchomator finished: $(date '+%F %H:%M:%S')" | tee -a "$logPATH" 194 | exit $1 195 | } 196 | 197 | makepath() { # creates the full path to a file, but not the file itself 198 | mkdir -p "$(sed 's/\(.*\)\/.*/\1/' <<< $1)" # && touch $1 199 | } 200 | 201 | notice() { # verbose mode 202 | if [[ ${#verbose} -eq 1 ]]; then 203 | echo "${YELLOW}[NOTICE]${RESET} $1" | tee -a "$logPATH" 204 | fi 205 | } 206 | 207 | infoOut() { # normal messages 208 | if (( ! ${#quietmode} )); then 209 | echo "$1" | tee -a "$logPATH" 210 | echo "progresstext: $1" >> $DialogPATH 211 | fi 212 | } 213 | 214 | error() { # bad, but recoverable 215 | echo "${BOLD}[ERROR]${RESET} $1" | tee -a "$logPATH" 216 | let errorCount++ 217 | } 218 | 219 | fatal() { # something bad happened. 220 | echo "\n${BOLD}${RED}[FATAL ERROR]${RESET} $1\n\n" | tee -a "$logPATH" 221 | echo "quit:" >> $DialogPATH 222 | 223 | finishAndExit 1 224 | } 225 | 226 | # --read 227 | # --write 228 | displayConfig() { 229 | echo "\n${BOLD}Currently configured labels:${RESET}" 230 | 231 | # if a config file was created, show it at the end. 232 | if [[ -f $defaultConfigfile ]] 233 | then 234 | column -t -s "=;\"\"" <<< $(defaults read "$defaultConfigfile" | tr -d "{}()\"") 235 | else 236 | # if no config was saved, show the results of the discovery process 237 | for discoveredItem in $configArray 238 | do 239 | echo $discoveredItem 240 | done 241 | 242 | echo "\n${BOLD}Ignored Labels:${RESET}" 243 | for ignoredItem in $ignoredLabelsList 244 | do 245 | echo $ignoredItem 246 | done 247 | 248 | echo "\n${BOLD}Required Labels:${RESET}" 249 | for requiredItem in $requiredLabelsList 250 | do 251 | echo $requiredItem 252 | done 253 | 254 | fi 255 | 256 | echo "quit:" >> $DialogPATH 257 | 258 | finishAndExit 0 259 | } 260 | 261 | checkInstallomator() { 262 | 263 | infoOut "Checking Installomator version." 264 | # check for existence of Installomator to enable installation of updates 265 | notice "Looking for Installomator.sh at ${YELLOW}$InstallomatorPATH ${RESET}" 266 | 267 | InstalledVersion="$($InstallomatorPATH version | tail -1)" 268 | LatestVersion="$(versionFromGit Installomator Installomator)" 269 | 270 | notice "Latest Version: $LatestVersion - Installed Version: $InstalledVersion" 271 | 272 | if [[ "$InstalledVersion" -ne "$LatestVersion" ]] 273 | then 274 | error "Installomator was found, but is out of date. You can update it by running \n\t${YELLOW}sudo $InstallomatorPATH installomator ${RESET}" 275 | 276 | if (( ${#noninteractive} )) 277 | then 278 | notice "Running in non-interactive mode. Skipping Installomator update." 279 | else 280 | OfferToInstall 281 | fi 282 | fi 283 | 284 | if ! [[ -f $InstallomatorPATH ]] 285 | then 286 | error "Installomator was not found at ${YELLOW}$InstallomatorPATH ${RESET}" 287 | 288 | LatestInstallomator=$(curl --silent --fail "https://api.github.com/repos/Installomator/Installomator/releases/latest" | awk -F '"' "/browser_download_url/ && /pkg\"/ { print \$4; exit }") 289 | 290 | if (( ${#noninteractive} )) 291 | then 292 | notice "Running in non-interactive mode. Skipping Installomator install." 293 | else 294 | OfferToInstall 295 | fi 296 | 297 | 298 | else 299 | if [ $(echo $InstalledVersion | cut -d . -f 1) -lt 10 ] 300 | then 301 | fatal "Installomator is installed, but is out of date. Versions prior to 10.0 function unpredictably with Patchomator.\nYou can probably update it by running \n\t${YELLOW}sudo $InstallomatorPATH installomator ${RESET}" 302 | fi 303 | fi 304 | 305 | } 306 | 307 | 308 | # --install 309 | OfferToInstall() { 310 | #Check your privilege 311 | if $IAMROOT 312 | then 313 | echo -n "Patchomator can still discover apps on the system and create a configuration for later use, but will not be able to install or update anything without Installomator. \ 314 | \n${BOLD}Download and install Installomator now? ${YELLOW}[y/N]${RESET} " 315 | 316 | read DownloadFromGithub 317 | 318 | if [[ $DownloadFromGithub =~ '[Yy]' ]] 319 | then 320 | installInstallomator 321 | else 322 | echo "${BOLD}Continuing without Installomator.${RESET}" 323 | # disable installs 324 | if [[ $installmode == true ]] 325 | then 326 | fatal "Patchomator cannot install or update apps without the latest Installomator. If you would like to continue, either re-run Patchomator without ${YELLOW}--install${RESET}, or install Installomator from this URL:\ 327 | \n\t ${YELLOW}https://github.com/Installomator/Installomator${RESET}" 328 | fi 329 | fi 330 | else 331 | fatal "Specify a different path with \"${YELLOW}-p [path to Installomator]${RESET}\" or download and install it from here:\ 332 | \n\t ${YELLOW}https://github.com/Installomator/Installomator${RESET}\ 333 | \n\nThis script can also attempt to install Installomator for you. Re-run patchomator with ${YELLOW}sudo${RESET} or without ${YELLOW}--install${RESET}" 334 | fi 335 | } 336 | 337 | installInstallomator() { 338 | # Get the URL of the latest PKG From the Installomator GitHub repo 339 | # no need for git, if there's an API 340 | PKGurl=$(curl --silent --fail "https://api.github.com/repos/Installomator/Installomator/releases/latest" | awk -F '"' "/browser_download_url/ && /pkg\"/ { print \$4; exit }") 341 | 342 | # Expected Team ID of the downloaded PKG 343 | expectedTeamID="JME5BW3F3R" 344 | 345 | tempDirectory=$( mktemp -d ) 346 | notice "Created working directory '$tempDirectory'" 347 | 348 | # Download the installer package 349 | notice "Downloading Installomator package" 350 | curl --location --silent "$PKGurl" -o "$tempDirectory/Installomator.pkg" || fatal "Download failed." 351 | 352 | # Verify the download 353 | teamID=$(spctl -a -vv -t install "$tempDirectory/Installomator.pkg" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()') 354 | notice "Team ID of downloaded package: $teamID" 355 | 356 | # Install the package, only if Team ID validates 357 | if [ "$expectedTeamID" = "$teamID" ] 358 | then 359 | notice "Package verified. Installing package Installomator.pkg" 360 | installer -pkg "$tempDirectory/Installomator.pkg" -target / -verbose || fatal "Installation failed. See /var/log/installer.log for details." 361 | else 362 | fatal "Package verification failed. TeamID does not match." 363 | fi 364 | 365 | # Remove the temporary working directory when done 366 | notice "Deleting working directory '$tempDirectory' and its contents" 367 | rm -Rf "$tempDirectory" 368 | 369 | } 370 | 371 | 372 | checkLabels() { 373 | 374 | infoOut "Checking for latest labels." 375 | notice "Looking for labels in ${fragmentsPATH}/labels/" 376 | 377 | # use curl to get the labels - who needs git? 378 | if [[ ! -d "$fragmentsPATH" ]] 379 | then 380 | if [[ -w "$patchomatorPath" ]] 381 | then 382 | infoOut "Package labels not present at $fragmentsPATH. Attempting to download from https://github.com/installomator/" 383 | downloadLatestLabels 384 | else 385 | fatal "Package labels not present and $patchomatorPath is not writable. Re-run patchomator with sudo to download and install them." 386 | fi 387 | 388 | else 389 | labelsAge=$((($(date +%s) - $(stat -t %s -f %m -- "$fragmentsPATH/labels")) / 86400)) 390 | 391 | if [[ $labelsAge -gt 30 ]] 392 | then 393 | if [[ -w "$patchomatorPath" ]] 394 | then 395 | error "Package labels are out of date. Last updated ${labelsAge} days ago. Attempting to download from https://github.com/installomator/" 396 | downloadLatestLabels 397 | else 398 | fatal "Package labels are out of date. Last updated ${labelsAge} days ago. Re-run patchomator with sudo to update them." 399 | 400 | fi 401 | 402 | else 403 | infoOut "Package labels installed. Last updated ${labelsAge} days ago." 404 | fi 405 | fi 406 | 407 | } 408 | 409 | dialogProgress() { 410 | echo "message: $1" >> $DialogPATH 411 | echo "progress: reset" >> $DialogPATH 412 | } 413 | 414 | dialogPercent() { # steps / max 415 | echo "progress: $((100*$1/$2))" >> $DialogPATH 416 | } 417 | dialogReset() { 418 | echo "progress: reset" >> $DialogPATH 419 | } 420 | 421 | rollLogs() { 422 | notice "Rolling over logs. Max logs is $backupLogsMax." 423 | for (( i=backupLogsMax; i>=1; i-- )); do 424 | prevLog=$((i-1)) 425 | if [[ $prevLog -eq 0 ]]; then 426 | srcLog="$logPATH" 427 | else 428 | srcLog="$logPATH.$prev" 429 | fi 430 | destLog="$logPATH.$i" 431 | 432 | if [[ -f "$srcLog" ]]; then 433 | mv -f "$srcLog" "$destLog" 434 | fi 435 | done 436 | touch "$logPATH" 2> /dev/null && chmod a+rw "$logPATH" || error "$logPATH not writable." 437 | } 438 | 439 | downloadLatestLabels() { 440 | 441 | dialogProgress "Downloading latest labels." 442 | 443 | dialogPercent 1 5 444 | # gets the latest release version tarball. 445 | latestURL=$(curl -sSL -o - "https://api.github.com/repos/Installomator/Installomator/releases/latest" | grep tarball_url | awk '{gsub(/[",]/,"")}{print $2}') # remove quotes and comma from the returned string 446 | #eg "https://api.github.com/repos/Installomator/Installomator/tarball/v10.3" 447 | 448 | 449 | tarPath="$patchomatorPath/installomator.latest.tar.gz" 450 | 451 | notice "Downloading ${latestURL} to ${tarPath}" 452 | dialogPercent 2 5 453 | 454 | curl -sSL -o "$tarPath" "$latestURL" || fatal "Unable to download. Check ${patchomatorPath} is writable or re-run as root." 455 | 456 | dialogPercent 3 5 457 | 458 | notice "Extracting ${tarPath} into ${patchomatorPath}" 459 | tar -xz --include='*/fragments/*' -f "$tarPath" --strip-components 1 -C "$patchomatorPath" || fatal "Unable to extract ${tarPath}. Corrupt or incomplete download?" 460 | touch "${fragmentsPATH}/labels/" 461 | dialogPercent 5 5 462 | 463 | } 464 | 465 | # --install 466 | doInstallations() { 467 | 468 | infoOut "Performing installations." 469 | 470 | # No sleeping 471 | /usr/bin/caffeinate -d -i -m -u & 472 | caffeinatepid=$! 473 | 474 | # Count errors 475 | errorCount=0 476 | 477 | InstallomatorOptionsString="" 478 | 479 | if [[ -n "$OptionsString" ]]; then 480 | InstallomatorOptionsString+="$OptionsString" 481 | else 482 | # convert InstallomatorOptions array to string 483 | for key value in ${(kv)InstallomatorOptions}; do 484 | InstallomatorOptionsString+=" $key=\"$value\"" 485 | done 486 | fi 487 | 488 | installedLabels=0 489 | dialogProgress "Installing $numLabels items." 490 | 491 | for label in $queuedLabelsArray 492 | do 493 | let installedLabels++ 494 | dialogPercent $installedLabels $numLabels 495 | 496 | infoOut "Installing ${label}..." 497 | ${InstallomatorPATH} ${label} ${InstallomatorOptionsString} 498 | if [ $? != 0 ]; then 499 | error "Error installing ${label}. Exit code $?" 500 | fi 501 | done 502 | 503 | infoOut "Errors: $errorCount" 504 | caffexit $errorCount 505 | 506 | } 507 | 508 | 509 | FindAppFromLabel() { 510 | # appname label_name packageID 511 | label_name=$1 512 | installLocation="" 513 | applist="" 514 | 515 | notice "Label: $label_name" 516 | 517 | if [ -z "$appName" ]; then 518 | # when not given derive from name 519 | appName="$name.app" 520 | fi 521 | 522 | # if the appversion is already set, there is an appCustomVersion function defined 523 | # check the funtion to see if it uses defaults read for an Info.plist for the app 524 | # if that exists, we can parse the file path from the function 525 | 526 | if [[ -n "$appversion" ]]; then 527 | if echo "$appCustomVersion" | grep -q 'Contents/Info\.plist'; then 528 | installLocation=$(echo "$appCustomVersion" | sed -n 's|.*defaults read *"\{0,1\}\([^"]\{1,\}\)/Contents/Info.plist.*|\1|p') 529 | if [[ -z "$installLocation" ]]; then 530 | installLocation=$(awk ' 531 | /defaults read/ { 532 | path = $0 533 | gsub(/.*read "/, "", path) 534 | sub(/\/Contents\/Info.plist.*/, "", path) 535 | print path 536 | exit 537 | }' <<< "$appCustomVersion") 538 | fi 539 | if [[ -d "$installLocation" ]]; then 540 | notice "Found: ${installLocation}" 541 | applist="$installLocation" 542 | fi 543 | fi 544 | fi 545 | 546 | # shortcut: pkgs contains a version number, if it's installed then we don't have to search the HD for the file 547 | # still need to confirm it's installed, tho. Receipts can be unreliable. 548 | 549 | if [[ -n "$packageID" ]] && [[ -z "$applist" ]]; then 550 | notice "Searching system for $packageID" 551 | appversion="$(pkgutil --pkg-info-plist ${packageID} 2>/dev/null | grep -A 1 pkg-version | tail -1 | sed -E 's/.*>([0-9.]*)<.*/\1/g')" 552 | if [[ -n "$appversion" ]]; then 553 | notice "--- found packageID $packageID version $appversion installed" 554 | installLocation="$(pkgutil --pkg-info-plist ${packageID} 2>/dev/null | grep -A 1 install-location | tail -1 | sed -E 's/.*>(.*)<.*/\1/g')" 555 | for ext in .app .plugin .prefPane .framework .kext; do 556 | if [ -d "/${installLocation}${ext}" ]; then 557 | notice "Found: /${installLocation}${ext}" 558 | applist="/${installLocation}${ext}" 559 | break 560 | fi 561 | done 562 | fi 563 | fi 564 | 565 | # get app in /Applications, or /Applications/Utilities, or find using Spotlight if not already found 566 | 567 | if [[ -z "$applist" ]]; then 568 | notice "Searching system for $appName" 569 | if [[ -d "/Applications/$appName" ]]; then 570 | applist="/Applications/$appName" 571 | elif [[ -d "/Applications/Utilities/$appName" ]]; then 572 | applist="/Applications/Utilities/$appName" 573 | else 574 | if (( ${#everywhere} )); then 575 | applist=$(mdfind "kMDItemFSName == '$appName' && kMDItemContentType == 'com.apple.application-bundle'" -0 ) 576 | else 577 | applist=$(mdfind -onlyin "/Applications/" -onlyin "/usr/local/" -onlyin "/Library/" "kMDItemFSName == '$appName' && kMDItemContentType == 'com.apple.application-bundle'" -0 ) 578 | fi 579 | # can't install things in /System/Applications, and probably shouldn't look in /Users 580 | # apps installed in other weird locations should be identifiable by their pkg receipt. 581 | # random files named *.app were potentially coming up in the list. Now it has to be an actual app bundle 582 | fi 583 | fi 584 | 585 | appPathArray=( ${(0)applist} ) 586 | 587 | if [[ ${#appPathArray} -gt 0 ]] 588 | then 589 | 590 | filteredAppPaths=( ${(M)appPathArray:#${targetDir}*} ) 591 | 592 | if [[ ${#filteredAppPaths} -eq 1 ]] 593 | then 594 | installedAppPath=$filteredAppPaths[1] 595 | 596 | [[ -n "$appversion" ]] || appversion=$(defaults read "$installedAppPath/Contents/Info.plist" "$versionKey" 2> /dev/null) 597 | 598 | infoOut "Found $name version $appversion" 599 | 600 | notice "Label: $label_name" 601 | notice "--- found app at $installedAppPath" 602 | 603 | # Is current app from App Store 604 | # AND is IGNORE_APP_STORE_APPS=yes? 605 | 606 | if [[ -d "$installedAppPath"/Contents/_MASReceipt ]] && [[ $InstallomatorOptions[IGNORE_APP_STORE_APPS] =~ [YyEeSs1] ]] 607 | then 608 | notice "$appName is from App Store. Ignoring." 609 | notice "Use the Installomator option \"IGNORE_APP_STORE_APPS=no\" to replace." 610 | 611 | else 612 | 613 | foundLabelsArray[$label_name]="$installedAppPath" 614 | 615 | fi 616 | fi 617 | 618 | fi 619 | } 620 | 621 | 622 | verifyApp() { 623 | foundLabel="$1" 624 | appPath="$2" 625 | 626 | notice "--- Processing Label $foundLabel at $appPath" 627 | 628 | 629 | if [[ -n "$configArray[$appPath]" ]] 630 | then 631 | infoOut "$appPath already verified." 632 | else 633 | if [[ $skipVerify == false ]] 634 | then 635 | 636 | infoOut "Verifying: $appPath" 637 | 638 | # verify with spctl 639 | appVerify=$(spctl -a -vv "$appPath" 2>&1 ) 640 | appVerifyStatus=$(echo $?) 641 | 642 | # If there is no usable signature and the app type is .plugin, then try another method 643 | # Found useful for JRE since Oracle does not sign JRE Plugin, but does sign in bin 644 | 645 | if [[ "$appVerify" == *"no usable signature" ]] && [[ "$appPath" == *".plugin" ]]; then 646 | teamIdentifiers="$(find "$appPath/Contents/Home/Bin" -type f -exec codesign -dv {} 2>&1 \; | grep TeamIdentifier | sort -u)" 647 | if [[ -n "$teamIdentifiers" ]]; then 648 | idCount=$(printf "%s\n" "$teamIdentifiers" | wc -l | tr -d ' ') 649 | if ((idCount > 1)); then 650 | error "Error verifying $appPath" 651 | notice "Team IDs do not match: expected: $expectedTeamID, found multiple IDs in plugin Home/Bin directory" 652 | return 653 | fi 654 | teamID="${teamIdentifiers#*=}" 655 | else 656 | error "Error verifying $appPath" 657 | notice "Team IDs do not match: expected: $expectedTeamID, found no IDs in plugin Home/Bin directory" 658 | return 659 | fi 660 | else 661 | if [[ $appVerifyStatus -ne 0 ]]; then 662 | error "Error verifying $appPath: Returned $appVerifyStatus" 663 | return 664 | fi 665 | teamID=$(echo $appVerify | awk '/origin=/ {print $NF }' | tr -d '()' ) 666 | fi 667 | 668 | if [ "$expectedTeamID" != "$teamID" ]; then 669 | error "Error verifying $appPath" 670 | notice "Team IDs do not match: expected: $expectedTeamID, found $teamID" 671 | return 672 | fi 673 | 674 | fi 675 | 676 | infoOut "Checking version: $appPath" 677 | # run the commands in current_label to check for the new version string 678 | newversion=$(zsh << SCRIPT_EOF 679 | declare -A levels=(DEBUG 0 INFO 1 WARN 2 ERROR 3 REQ 4) 680 | currentUser=$currentUser 681 | source "$fragmentsPATH/functions.sh" 682 | ${current_label} 683 | echo "\$appNewVersion" 684 | SCRIPT_EOF 685 | ) 686 | infoOut "-- $newversion" 687 | 688 | fi 689 | # build array of labels for the config and/or installation 690 | 691 | # push label to array 692 | # if in write config mode, writes to plist. Otherwise to an array. 693 | if [[ -n "$configArray[$appPath]" ]] 694 | then 695 | exists="$configArray[$appPath]" 696 | 697 | infoOut "${appPath} already linked to label ${exists}." 698 | if (( ${#noninteractive} )) 699 | then 700 | infoOut "\t${BOLD}Skipping.${RESET}" 701 | return 702 | else 703 | echo -n "${BOLD}Replace label ${exists} with $foundLabel? ${YELLOW}[y/N]${RESET} " 704 | read replaceLabel 705 | 706 | if [[ $replaceLabel =~ '[Yy]' ]] 707 | then 708 | infoOut "\t${BOLD}Replacing.${RESET}" 709 | configArray[$appPath]=$foundLabel 710 | 711 | # Remove duplicate label already in queue: 712 | labelsList=$(echo "$labelsList" | sed s/"$exists "//) 713 | 714 | # add replaced label to Ignored list 715 | ignoredLabelsArray["$exists"]=1 716 | 717 | if (( ${#writeconfig} )) 718 | then 719 | /usr/libexec/PlistBuddy -c "set \":${appPath}\" ${foundLabel}" "$defaultConfigfile" 720 | /usr/libexec/PlistBuddy -c "add \":IgnoredLabels:\" string \"${exists}\"" $defaultConfigfile 721 | fi 722 | 723 | else 724 | infoOut "\t${BOLD}Skipping.${RESET}" 725 | # add skipped label to Ignored list 726 | if (( ${#writeconfig} )) 727 | then 728 | /usr/libexec/PlistBuddy -c "add \":IgnoredLabels:\" string \"${foundLabel}\"" $defaultConfigfile 729 | fi 730 | return 731 | fi 732 | fi 733 | else 734 | configArray[$appPath]=$foundLabel 735 | if (( ${#writeconfig} )) 736 | then 737 | /usr/libexec/PlistBuddy -c "add \":${appPath}\" string ${foundLabel}" "$defaultConfigfile" 738 | fi 739 | fi 740 | 741 | appversion="$(pkgutil --pkg-info-plist ${packageID} 2>/dev/null | grep -A 1 pkg-version | tail -1 | sed -E 's/.*>([0-9.]*)<.*/\1/g')" 742 | [[ -n "$appversion" ]] || appversion=$(defaults read "$appPath/Contents/Info.plist" "$versionKey" 2>/dev/null) 743 | 744 | notice "--- Installed version: ${appversion}" 745 | 746 | [[ -n "$newversion" ]] && notice "--- Newest version: ${newversion}" 747 | 748 | if [[ "$appversion" == "$newversion" ]] 749 | then 750 | notice "--- Latest version installed." 751 | else 752 | queueLabel 753 | fi 754 | } 755 | 756 | 757 | 758 | # --install 759 | queueLabel() { 760 | # add to queue if in install mode 761 | if [[ $installmode == true ]] 762 | then 763 | notice "Queueing $label_name" 764 | 765 | labelsList+="$label_name " 766 | # echo "$labelsList" 767 | fi 768 | } 769 | 770 | 771 | ####################################### 772 | # You're probably wondering why I've called you all here... 773 | 774 | 775 | # Command line options 776 | 777 | #zparseopts -D -E -F -K -- \ 778 | zparseopts -D -E -F -K -- \ 779 | -help+=showhelp h+=showhelp \ 780 | -install=installmode I=installmode \ 781 | -quiet=quietmode q=quietmode \ 782 | -yes=noninteractive y=noninteractive \ 783 | -verbose=verbose v=verbose \ 784 | -read=readconfig r=readconfig \ 785 | -write=writeconfig w=writeconfig \ 786 | -config:=configfile c:=configfile \ 787 | -skipverify=skipVerify s=skipVerify \ 788 | -pathtoinstallomator:=InstallomatorPATH p:=InstallomatorPATH \ 789 | -ignored:=ignoredLabels \ 790 | -required:=requiredLabels \ 791 | -mdm:=MDMName m:=MDMName \ 792 | -everywhere=everywhere e=everywhere \ 793 | -options:=CLIOptions o:=CLIOptions \ 794 | || fatal "Bad command line option. See patchomator.sh --help" 795 | 796 | # -h --help 797 | # -I --install 798 | # -q --quiet 799 | # -y --yes 800 | # -v --verbose 801 | # -r --read 802 | # -w --write 803 | # -c --config 804 | # -s --skip-verify 805 | # -p --pathtoinstallomator 806 | # -m --mdm [one of jamf, mosyleb, mosylem, addigy, microsoft, ws1, other ] Any other Mac MDM solutions worth mentioning? 807 | # -e --everywhere 808 | # -o --options "list of installomator options to pass through" 809 | 810 | 811 | 812 | 813 | # Show usage 814 | # -h --help 815 | if (( ${#showhelp} )) 816 | then 817 | usage 818 | fi 819 | 820 | notice "Verbose Mode enabled." # and if it's not? This won't echo. 821 | 822 | if [[ ${#configfile} -eq 0 ]] && [[ -f $managedConfigfile ]] 823 | then 824 | defaultConfigfile=$managedConfigfile 825 | elif [[ ${#configfile} -gt 0 ]] 826 | then 827 | defaultConfigfile=$configfile[-1] # either provided on the command line, or default path 828 | fi 829 | 830 | 831 | # prevent patchomator modify the content of the managed config 832 | if [[ $defaultConfigfile == $managedConfigfile ]] && (( ${#writeconfig} )) 833 | then 834 | fatal "You should not manualy overwrite ${YELLOW}$managedConfigfile${RESET}" 835 | fi 836 | 837 | # check if config file is writeable if writeconfig is true 838 | if [[ ! -w $defaultConfigfile ]] && (( ${#writeconfig} )) 839 | then 840 | fatal "Configuration file ${YELLOW}$defaultConfigfile${RESET} is not writeable. Try again with ${YELLOW}sudo${RESET}" 841 | fi 842 | 843 | InstallomatorPATH=$InstallomatorPATH[-1] # either provided on the command line, or default /usr/local/Installomator 844 | 845 | MDMName=$MDMName[-1] #[one of jamf, mosyleb, mosylem, addigy, microsoft, ws1, other ] 846 | 847 | # --mdm 848 | # Assumes certain settings when an MDM is declared: 849 | # - Installomator options: 850 | # - logo 851 | # - ? 852 | # --install 853 | # --quiet 854 | # --yes 855 | 856 | 857 | ### Default Installomator Options: 858 | 859 | InstallomatorOptions=(\ 860 | [NOTIFY]=success \ 861 | [PROMPT_TIMEOUT]=86400 \ 862 | [BLOCKING_PROCESS_ACTION]=tell_user \ 863 | [LOGO]=appstore \ 864 | [IGNORE_APP_STORE_APPS]="no" \ 865 | [SYSTEMOWNER]=0 \ 866 | [REOPEN]="yes" \ 867 | [INTERRUPT_DND]="yes" \ 868 | [NOTIFY_DIALOG]=1 \ 869 | [LOGGING]="INFO" \ 870 | ) 871 | 872 | # Parse command line --options 873 | OptionsString=$CLIOptions[-1] 874 | 875 | # split on spaces, then on = 876 | # AddOptions=$(echo "$OptionsString" | awk -v OFS="\n" '{$1=$1}1' | awk -v FS="=" '{print "InstallomatorOptions+=\(["$1"]="$2"\)"}') 877 | 878 | # Add them to the InstallomatorOptions array 879 | # eval "$AddOptions" 880 | 881 | # Additional optional settings by MDM 882 | # if [ "$MDMName" ] 883 | # then 884 | # quietmode[1]=true 885 | # # installmode=true 886 | # noninteractive[1]=true 887 | # fi 888 | # 889 | # if [ "$MDMName" ] 890 | # then 891 | # # set logos for known MDM vendors 892 | # if [ "$MDMName" != "other" ] 893 | # then 894 | # InstallomatorOptions[LOGO]="$MDMName" 895 | # fi 896 | # fi 897 | 898 | ## Starting up. Need to log options, etc 899 | 900 | ## check log is writable and rollover if over size 901 | if [[ -w "$logPATH" ]] then 902 | # #exists and writable check size 903 | fileSize=$(stat -f%z "$logPATH" 2>/dev/null) 904 | if (( fileSize > logSizeMax )); then 905 | rollLogs 906 | fi 907 | elif [[ ! -f "$logPATH" ]] then 908 | # #doesn't exist 909 | touch "$logPATH" 2> /dev/null && chmod a+rw "$logPATH" || error "$logPATH not writable." 910 | fi 911 | 912 | echo "Patchomator starting: $(date '+%F %H:%M:%S')" | tee -a "$logPATH" 913 | 914 | notice "Option Count ${#InstallomatorOptions[@]}" 915 | notice "Installomator Options:" 916 | 917 | for key value in ${(kv)InstallomatorOptions}; do 918 | notice " - $key=\"$value\"" 919 | done 920 | 921 | # ReadConfig mode - read existing plist and display in pretty columns 922 | # skips discovery and all the rest 923 | # --read 924 | if [[ ${#readconfig} -eq 1 ]] 925 | then 926 | 927 | notice "Reading Config" 928 | 929 | if ! [[ -f $defaultConfigfile ]] 930 | then 931 | fatal "No config file at $defaultConfigfile. Run patchomator again with ${YELLOW}--write${RESET} to create one now.\n" 932 | else 933 | displayConfig 934 | fi 935 | 936 | fi 937 | 938 | ## initiate swiftdialog if we're doing more than just reading config. 939 | 940 | if (( ! ${#quietmode} )); then 941 | [[ -f /usr/local/bin/dialog ]] && /usr/local/bin/dialog -t "Patchomator Progress" -m "Starting Patchomator." --style mini --icon "/usr/local/Installomator/patch-o-mater-icon.png" -o --progress 100 --button1text "..." & sleep .1 942 | fi 943 | 944 | if [[ -f $defaultConfigfile ]] && (( ! ${#writeconfig} )) 945 | then 946 | infoOut "Reading existing configuration for ignored/required labels" 947 | 948 | # parse the config for existing ignored/required labels 949 | ignoredLabelsFromConfig=($(defaults read "$defaultConfigfile" IgnoredLabels | awk '{printf "%s ",$NF}' | tr -c -d "[:alnum:][:space:][\-_]" | tr -s "[:space:]")) 950 | 951 | requiredLabelsFromConfig=($(defaults read "$defaultConfigfile" RequiredLabels | awk '{printf "%s ",$NF}' | tr -c -d "[:alnum:][:space:][\-_]" | tr -s "[:space:]")) 952 | 953 | for ignoredLabel in $ignoredLabelsFromConfig 954 | do 955 | if [[ -f "${fragmentsPATH}/labels/${ignoredLabel}.sh" ]] 956 | then 957 | ignoredLabelsArray["$ignoredLabel"]=1 958 | # echo $ignoredLabelsArray["$ignoredLabel"] 959 | notice "Ignoring $ignoredLabel" 960 | fi 961 | done 962 | 963 | for requiredLabel in $requiredLabelsFromConfig 964 | do 965 | if [[ -f "${fragmentsPATH}/labels/${requiredLabel}.sh" ]] 966 | then 967 | requiredLabelsArray["$requiredLabel"]=1 968 | notice "Requiring $requiredLabel" 969 | fi 970 | done 971 | 972 | fi 973 | 974 | 975 | # --install 976 | # some functions act differently based on install vs discovery/read/write 977 | if (( ${#installmode} )) 978 | then 979 | installmode=true 980 | skipDiscovery=true 981 | if (( ${#writeconfig} )); then 982 | infoOut "Writing config and discovery are disabled when installing." 983 | writeconfig="" 984 | fi 985 | else 986 | installmode=false 987 | skipDiscovery=false 988 | 989 | # can't do discovery without the labels files. 990 | checkLabels 991 | 992 | # speed up the discovery phase. 993 | if (( ${#skipVerify} )) 994 | then 995 | skipVerify=true 996 | else 997 | skipVerify=false 998 | fi 999 | 1000 | fi 1001 | 1002 | 1003 | # Create Config file if none already exists 1004 | if ! [[ -f $defaultConfigfile ]] # no existing config 1005 | then 1006 | if [[ -d $defaultConfigfile ]] # common mistake, select a directory, not a filename 1007 | then 1008 | fatal "Please specify a file name for the configuration, not a directory.\n\tExample: ${YELLOW}patchomator --write --config \"/etc/patchomator.plist\"" 1009 | fi 1010 | 1011 | if [[ -d "$(dirname $defaultConfigfile)" ]] # directory exists 1012 | then 1013 | if [[ -w "$(dirname $defaultConfigfile)" ]] #directory is writable 1014 | then 1015 | infoOut "No existing config file at $defaultConfigfile. Creating one now." 1016 | else 1017 | # exists, but not writable 1018 | fatal "$(dirname $defaultConfigfile) exists, but is not writable. Re-run patchomator with sudo to create the config file there, or use a writable path with\n\t ${YELLOW}--config \"path to config file\"${RESET}" 1019 | fi 1020 | else # directory doesn't exist 1021 | infoOut "The path to $defaultConfigfile does not exist. Making path and creating file now." 1022 | makepath "$defaultConfigfile" 1023 | fi 1024 | 1025 | # creates a blank plist 1026 | plutil -create xml1 "$defaultConfigfile" || fatal "Unable to create $defaultConfigfile. Re-run patchomator with sudo to create the config file there, or use a writable path with\n\t ${YELLOW}--config \"path to config file\"${RESET}" 1027 | 1028 | # add sections for label arrays 1029 | /usr/libexec/PlistBuddy -c 'add ":IgnoredLabels" array' "${defaultConfigfile}" 1030 | /usr/libexec/PlistBuddy -c 'add ":RequiredLabels" array' "${defaultConfigfile}" 1031 | fi 1032 | 1033 | # Clear config to write 1034 | if (( ${#writeconfig} )) 1035 | then 1036 | notice "Writing Config" 1037 | 1038 | if ! [[ -w $defaultConfigfile ]] 1039 | then 1040 | fatal "$defaultConfigfile is not writable. Re-run patchomator with sudo, or use a writable path with\n\t ${YELLOW}--config \"path to config file\"${RESET}" 1041 | fi 1042 | 1043 | infoOut "Refreshing $defaultConfigfile" 1044 | 1045 | # empty the existing plist 1046 | /usr/libexec/PlistBuddy -c "clear dict" "${defaultConfigfile}" &>/dev/null 1047 | 1048 | # add sections for label arrays 1049 | /usr/libexec/PlistBuddy -c 'add ":IgnoredLabels" array' "${defaultConfigfile}" 1050 | /usr/libexec/PlistBuddy -c 'add ":RequiredLabels" array' "${defaultConfigfile}" 1051 | 1052 | fi 1053 | 1054 | 1055 | # MOAR Functions! miscellaneous pieces referenced in the occasional label 1056 | # Needs to confirm that labels exist first. 1057 | source "$fragmentsPATH/functions.sh" 1058 | 1059 | # can't install without the 'mator 1060 | # can't check version without the functions. 1061 | checkInstallomator 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | if [[ $installmode == true ]] 1068 | then 1069 | 1070 | # Check your privilege 1071 | if ! $IAMROOT 1072 | then 1073 | fatal "Install mode must be run with root/sudo privileges. Re-run Patchomator with\n\t ${YELLOW}sudo zsh patchomator.sh --install${RESET}" 1074 | fi 1075 | 1076 | fi 1077 | 1078 | 1079 | # --required 1080 | if [[ -n "$requiredLabels" ]] 1081 | then 1082 | 1083 | requiredLabelsList=("${(@s/ /)requiredLabels[-1]}") 1084 | notice "Required labels: $requiredLabelsList" 1085 | 1086 | for requiredLabel in $requiredLabelsList 1087 | do 1088 | if [[ -f "${fragmentsPATH}/labels/${requiredLabel}.sh" ]] 1089 | then 1090 | notice "[CLI] Requiring ${requiredLabel}." 1091 | 1092 | if (( ${#writeconfig} )) 1093 | then 1094 | /usr/libexec/PlistBuddy -c "add \":RequiredLabels:\" string \"${requiredLabel}\"" $defaultConfigfile 1095 | fi 1096 | 1097 | if [[ $installmode == true ]] 1098 | then 1099 | label_name=$requiredLabel 1100 | queueLabel # add to installer queue 1101 | fi 1102 | requiredLabelsArray[$requiredLabel]=1 1103 | 1104 | else 1105 | error "No such label ${requiredLabel}" 1106 | fi 1107 | 1108 | done 1109 | 1110 | fi 1111 | 1112 | # --ignored 1113 | if [[ -n "$ignoredLabels" ]] 1114 | then 1115 | 1116 | ignoredLabelsList=("${(@s/ /)ignoredLabels[-1]}") 1117 | 1118 | if [[ "$(echo $ignoredLabelsList | tr '[:upper:]' '[:lower:]')" == "all" ]] # ALL All all aLl etc. 1119 | then 1120 | 1121 | notice "[CLI] Ignored=all. Skipping discovery." 1122 | skipDiscovery=true 1123 | 1124 | else 1125 | notice "[CLI] Ignoring labels: $ignoredLabelsList" 1126 | 1127 | for ignoredLabel in $ignoredLabelsList 1128 | do 1129 | 1130 | # echo "+++ $ignoredLabel" 1131 | 1132 | if [[ -f "${fragmentsPATH}/labels/${ignoredLabel}.sh" ]] 1133 | then 1134 | 1135 | if [[ ${#writeconfig} -eq 1 ]] 1136 | then 1137 | /usr/libexec/PlistBuddy -c "add \":IgnoredLabels:\" string \"${ignoredLabel}\"" $defaultConfigfile 1138 | fi 1139 | 1140 | ignoredLabelsArray["$ignoredLabel"]=1 1141 | 1142 | else 1143 | error "No such label ${ignoredLabel}" 1144 | fi 1145 | 1146 | done 1147 | fi 1148 | 1149 | fi 1150 | 1151 | 1152 | 1153 | # discovery mode 1154 | # the main attraction. 1155 | 1156 | 1157 | # DISCOVERY PHASE 1158 | 1159 | # get current user 1160 | currentUser=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }') 1161 | 1162 | uid=$(id -u "$currentUser") 1163 | 1164 | notice "Current User: $currentUser (UID $uid)" 1165 | 1166 | # start of label pattern 1167 | label_re='^([a-z0-9\_-]*)(\))$' 1168 | #label_re='^([a-z0-9\_-]*)(\)|\|\\)$' 1169 | 1170 | # ignore comments 1171 | comment_re='^\#$' 1172 | 1173 | # end of label pattern 1174 | endlabel_re='^;;' 1175 | 1176 | targetDir="/" 1177 | versionKey="CFBundleShortVersionString" 1178 | 1179 | IFS=$'\n' 1180 | in_label=0 1181 | current_label="" 1182 | 1183 | ### MAIN EVENT 1184 | 1185 | # for each .sh file in fragments/labels/ strip out the switch/case lines and any comments. 1186 | # get app name, label name, packageID 1187 | 1188 | 1189 | if [[ $skipDiscovery != true ]] 1190 | then 1191 | 1192 | numFragments=$(ls "$fragmentsPATH"/labels/*.sh | wc -l | xargs) 1193 | processedFragments=0 1194 | 1195 | dialogProgress "Processing $numFragments labels" 1196 | 1197 | for labelFragment in "$fragmentsPATH"/labels/*.sh; do 1198 | 1199 | let processedFragments++ 1200 | 1201 | dialogPercent $processedFragments $numFragments 1202 | 1203 | labelFile=$(basename -- "$labelFragment") 1204 | labelFile=${labelFile%.*} 1205 | 1206 | 1207 | while read -r labelInFile 1208 | do 1209 | 1210 | if [[ $ignoredLabelsArray["$labelInFile"] -eq 1 ]] 1211 | then 1212 | notice "Ignoring labels in $labelFile." 1213 | continue 2 # we're done here. Move along. 1214 | fi 1215 | 1216 | done < <(grep -E '^([a-z0-9\_-]*)(\)|\|\\)$' "$labelFragment" | sed -e 's/[\|\\\)]//g' ) 1217 | 1218 | # clear for next iteration 1219 | expectedTeamID="" 1220 | packageID="" 1221 | name="" 1222 | appName="" 1223 | current_label="" 1224 | versionKey="" 1225 | appCustomVersion="" 1226 | appversion="" 1227 | 1228 | 1229 | ## for discovery phase, use grep: '^([a-z0-9\_-]*)(\)|\|\\)$' 1230 | ## labelFragment contains n label_names 1231 | ## easier than parsing line by line 1232 | 1233 | 1234 | # set variables 1235 | 1236 | eval $(grep -E -m1 '^\s*expectedTeamID' "$labelFragment") 2>/dev/null 1237 | 1238 | if [[ -z $expectedTeamID ]] 1239 | then 1240 | infoOut "Error in $labelFile. No Team ID." 1241 | continue 1242 | fi 1243 | 1244 | eval $(grep -E -m1 '^\s*name=' "$labelFragment") 2>/dev/null 1245 | eval $(grep -E -m1 '^\s*packageID' "$labelFragment") 2>/dev/null 1246 | eval $(grep -E -m1 '^\s*versionKey' "$labelFragment") 2>/dev/null 1247 | versionKey="${versionKey:-CFBundleShortVersionString}" 1248 | 1249 | if grep -q '^\s*appCustomVersion\s*()' "$labelFragment" 1250 | then 1251 | appCustomVersion=$(grep -E -m1 '^\s*appCustomVersion' "$labelFragment" | sed -E 's/^.*\(\)[[:space:]]*\{[[:space:]]*(.*)[[:space:]]*\}/\1/') 1252 | if [[ -z "$appCustomVersion" ]] || [[ "$appCustomVersion" == *"{"$ ]] 1253 | then 1254 | appCustomVersion=$(awk ' 1255 | /^[[:space:]]*appCustomVersion[[:space:]]*\(\)[[:space:]]*\{/ { inside=1; next } 1256 | inside { 1257 | if ($0 ~ /^[[:space:]]*\}/) { inside=0; exit } 1258 | print 1259 | }' "$labelFragment") 1260 | fi 1261 | if [[ ! "$appCustomVersion" =~ ^[[:space:]]*strings ]]; then 1262 | appversion=$(eval "$appCustomVersion" 2>/dev/null) 1263 | fi 1264 | fi 1265 | 1266 | infoOut "Processing label $labelFile." 1267 | FindAppFromLabel "$labelFile" 1268 | done 1269 | else 1270 | # read existing config. One label per line. Send labels to Installomator for updates. 1271 | infoOut "Existing config found at $defaultConfigfile." 1272 | 1273 | labelsFromConfig=($(defaults read "$defaultConfigfile" | grep -e ';$' | awk '{printf "%s ",$NF}' | tr -c -d "[:alnum:][:space:][\-_]" | tr -s "[:space:]")) 1274 | 1275 | ignoredLabelsFromConfig=($(defaults read "$defaultConfigfile" IgnoredLabels | awk '{printf "%s ",$NF}' | tr -c -d "[:alnum:][:space:][\-_]" | tr -s "[:space:]")) 1276 | 1277 | requiredLabelsFromConfig=($(defaults read "$defaultConfigfile" RequiredLabels | awk '{printf "%s ",$NF}' | tr -c -d "[:alnum:][:space:][\-_]" | tr -s "[:space:]")) 1278 | 1279 | ignoredLabelsList+=($ignoredLabelsFromConfig) 1280 | requiredLabelsList+=($requiredLabelsFromConfig) 1281 | 1282 | labelsList+=($labelsFromConfig $requiredLabelsList) 1283 | 1284 | # # deduplicate ignored labels and remove extra spacing 1285 | ignoredLabelsList=($(tr ' ' '\n' <<< "${ignoredLabelsList[@]}" | sort -u | tr '\n' ' ')) 1286 | ignoredLabelsList="${ignoredLabelsList## }" 1287 | ignoredLabelsList="${ignoredLabelsList%% }" 1288 | ignoredLabelsList="${ignoredLabelsList//[[:space:]]+/ }" 1289 | 1290 | # # deduplicate required labels and remove extra spacing 1291 | requiredLabelsList=($(tr ' ' '\n' <<< "${requiredLabelsList[@]}" | sort -u | tr '\n' ' ')) 1292 | requiredLabelsList="${requiredLabelsList## }" 1293 | requiredLabelsList="${requiredLabelsList%% }" 1294 | requiredLabelsList="${requiredLabelsList//[[:space:]]+/ }" 1295 | 1296 | # # deduplicate labels list and remove extra spacing 1297 | labelsList=($(tr ' ' '\n' <<< "${labelsList[@]}" | sort -u | tr '\n' ' ')) 1298 | labelsList="${labelsList## }" 1299 | labelsList="${labelsList%% }" 1300 | labelsList="${labelsList//[[:space:]]+/ }" 1301 | 1302 | # # remove ignored labels 1303 | labelsList=${labelsList:|ignoredLabelsList} 1304 | 1305 | notice "Labels to install: $labelsList" 1306 | notice "Ignoring labels: $ignoredLabelsList" 1307 | notice "Required labels: $requiredLabelsList" 1308 | 1309 | fi 1310 | # end discovery 1311 | 1312 | 1313 | totalFoundLabels=${#foundLabelsArray} 1314 | processedLabels=0 1315 | 1316 | dialogProgress "Processing $totalFoundLabels discovered labels" 1317 | 1318 | # for each app found, check version and verify 1319 | for foundLabel appPath in ${(kv)foundLabelsArray}; 1320 | do 1321 | 1322 | let processedLabels++ 1323 | 1324 | dialogPercent $processedLabels $totalFoundLabels 1325 | 1326 | if [[ $ignoredLabelsArray["$foundLabel"] -ne 1 ]] 1327 | then 1328 | 1329 | # echo "$foundLabel == $appPath" 1330 | labelFragment="${fragmentsPATH}/labels/${foundLabel}.sh" 1331 | 1332 | # read the label as a sub-script 1333 | exec 3< "${labelFragment}" 1334 | 1335 | while read -r -u 3 line; do 1336 | 1337 | # strip spaces and tabs 1338 | scrubbedLine="$(echo $line | sed -E -e 's/^( |\t)*//g' -e 's/^\s*#.*$//')" 1339 | 1340 | if [[ -n $scrubbedLine ]]; then 1341 | 1342 | if [[ $in_label -eq 0 && "$scrubbedLine" =~ $label_re ]]; then 1343 | 1344 | label_name=${match[1]} 1345 | in_label=1 1346 | continue # skips to the next iteration 1347 | fi 1348 | 1349 | if [[ $in_label -eq 1 && "$scrubbedLine" =~ $endlabel_re ]]; then 1350 | # label complete. A valid label includes a Team ID. If we have one, we can check for installed 1351 | [[ -n $expectedTeamID ]] && verifyApp "$foundLabel" "$appPath" 1352 | 1353 | in_label=0 1354 | packageID="" 1355 | name="" 1356 | appName="" 1357 | expectedTeamID="" 1358 | current_label="" 1359 | appNewVersion="" 1360 | versionKey="CFBundleShortVersionString" 1361 | 1362 | continue # skips to the next iteration 1363 | fi 1364 | 1365 | if [[ $in_label -eq 1 ]]; then 1366 | [[ -z $current_label ]] && current_label=$line || current_label=$current_label$'\n'$line 1367 | 1368 | case $scrubbedLine in 1369 | 1370 | 'name='*|'packageID'*|'expectedTeamID'*) 1371 | eval "$scrubbedLine" 1372 | ;; 1373 | 1374 | esac 1375 | fi 1376 | fi 1377 | done 1378 | fi 1379 | done 1380 | 1381 | 1382 | # install mode. Requires root and Installomator, checks for existing config. 1383 | # --install 1384 | 1385 | if [[ $installmode == true ]] 1386 | then 1387 | 1388 | IFS=' ' 1389 | 1390 | queuedLabelsArray=("${(@s/ /)labelsList}") 1391 | numLabels=${#queuedLabelsArray[@]} 1392 | 1393 | if [[ $numLabels > 0 ]] 1394 | then 1395 | infoOut "Passing $numLabels labels to Installomator: $queuedLabelsArray" 1396 | doInstallations 1397 | else 1398 | infoOut "Nothing to do." # inbox zero 1399 | fi 1400 | 1401 | echo "quit:" >> $DialogPATH 1402 | 1403 | finishAndExit 0 1404 | 1405 | fi 1406 | 1407 | # end install mode 1408 | 1409 | if [ "$errorCount" -gt 0 ] 1410 | then 1411 | infoOut "${BOLD}Completed with $errorCount errors.${RESET}\n" 1412 | else 1413 | infoOut "${BOLD}Done.${RESET}\n" 1414 | fi 1415 | 1416 | if (( ! (${#quietmode} && ${#writeconfig}) )); then 1417 | displayConfig 1418 | fi 1419 | 1420 | finishAndExit 0 1421 | 1422 | #### That's a wrap. Don't forget to tip your server. You don't have to go home, but you can't stay here. 1423 | --------------------------------------------------------------------------------