├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------