├── LICENSE.md
├── README.md
├── SuppressSetupAssistant
├── .gitignore
├── build-info.plist
└── payload
│ ├── Library
│ └── Receipts
│ │ └── .SetupRegComplete
│ └── private
│ └── var
│ └── db
│ └── .AppleSetupDone
├── TurnOffBluetooth
├── .gitignore
├── build-info.plist
└── scripts
│ └── postinstall
├── munki_kickstart
├── .gitignore
├── build-info.json
└── payload
│ └── Users
│ └── Shared
│ └── .com.googlecode.munki.checkandinstallatstartup
├── munkipkg
└── requirements.txt
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Licensed under the Apache License, Version 2.0 (the "License");
2 | you may not use this source code except in compliance with the License.
3 | You may obtain a copy of the License at
4 |
5 | https://www.apache.org/licenses/LICENSE-2.0
6 |
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # munkipkg
2 |
3 | ## Introduction
4 |
5 | munkipkg is a tool for building packages in a consistent, repeatable manner from source files and scripts in a project directory.
6 |
7 | While you can use munkipkg to generate packages for use with Munki (https://www.munki.org/munki/), the packages munkipkg builds are just normal Apple installer packages usable anywhere you can use Apple installer packages.
8 |
9 | Files, scripts, and metadata are stored in a way that is easy to track and manage using a version control system like git.
10 |
11 | **autopkg** (https://github.com/autopkg/autopkg) is another tool that has some overlap here. It's definitely possible to use autopkg to build packages from files and scripts on your local disk. See https://managingosx.wordpress.com/2015/07/30/using-autopkg-for-general-purpose-packaging/ and https://github.com/gregneagle/autopkg-packaging-demo for examples on how to do this.
12 |
13 | So why consider using munkipkg? It's simple and self-contained, with no external dependencies. It can use JSON or YAML for its build settings file/data, instead of Makefile syntax or XML plists. It does not install a root-level system daemon as does autopkg. It can easily build distribution-style packages and can sign them. Finally, munkipkg can import existing packages.
14 |
15 | ## macOS and Python notes
16 |
17 | munkipkg requires Python. It also uses several command-line tools available on macOS. There is no support for running these on Windows or Linux.
18 |
19 | In macOS 12.3, Apple removed the Python 2.7 install. Out-of-the-box, there is no Python installed. You'll need to provide your own Python3 to use munkipkg.
20 |
21 | Some options for providing an appropriate Python:
22 |
23 | 1) If you also use Munki, use Munki's bundled Python. You could make a symlink at /usr/local/bin/python3 pointing to `/usr/local/munki/munki-python` (this assumes `/usr/local/bin` is in your `PATH`, which it is by default. You could create symlink in any writable directory in your `PATH` if it differs)
24 | 2) Install Python from https://www.python.org. You might still need to create a symlink somewhere so that `/usr/bin/env python3` executes the Python you installed.
25 | 3) Install Apple's Python 3 by running `/usr/bin/python3` and accepting the prompt to install Python (if Xcode or the Xcode Command Line Tools are not already present).
26 | 4) There are other ways to install Python, including Homebrew (https://brew.sh), macadmins-python (https://github.com/macadmins/python), relocatable-python tool (https://github.com/gregneagle/relocatable-python), etc.
27 |
28 | If you don't want to create a symlink or alter your PATH so that `/usr/bin/env python3` executes an appropriate Python for munkipkg, you can just call munkipkg _from_ the Python of your choice, eg: `/path/to/your/python3 /path/to/munkipkg [options]`
29 |
30 | ## Basic operation
31 |
32 | munkipkg builds flat packages using Apple's `pkgbuild` and `productbuild` tools.
33 |
34 | ### Package project directories
35 |
36 | munkipkg builds packages from a "package project directory". At its simplest, a package project directory is a directory containing a "payload" directory, which itself contains the files to be packaged. More typically, the directory also contains a "build-info.plist" file containing specific settings for the build. The package project directory may also contain a "scripts" directory containing any scripts (and, optionally, additional files used by the scripts) to be included in the package.
37 |
38 | ### Package project directory layout
39 | ```
40 | project_dir/
41 | build-info.plist
42 | payload/
43 | scripts/
44 | ```
45 |
46 | ### Creating a new project
47 |
48 | munkipkg can create an empty package project directory for you:
49 |
50 | `munkipkg --create Foo`
51 |
52 | ...will create a new package project directory named "Foo" in the current working directory, complete with a starter build-info.plist, empty payload and scripts directories, and a .gitignore file to cause git to ignore the build/ directory that is created when a project is built.
53 |
54 | Once you have a project directory, you simply copy the files you wish to package into the payload directory, and add a preinstall and/or postinstall script to the scripts directory. You may also wish to edit the build-info.plist.
55 |
56 | ### Importing an existing package
57 |
58 | Another way to create a package project is to import an existing package:
59 |
60 | `munkipkg --import /path/to/foo.pkg Foo`
61 |
62 | ...will create a new package project directory named "Foo" in the current working directory, with payload, scripts and build-info extracted from foo.pkg.
63 | Complex or non-standard packages may not be extracted with 100% fidelity, and not all package formats are supported. Specifically, metapackages are not supported, and distribution packages containing multiple sub-packages are not supported. In these cases, consider importing the individual sub-packages.
64 |
65 | ### Building a package
66 |
67 | This is the central task of munkipkg.
68 |
69 | `munkipkg path/to/package_project_directory`
70 |
71 | Causes munkipkg to build the package defined in package_project_directory. The built package is created in a build/ directory inside the project directory.
72 |
73 | ### build-info
74 |
75 | Build options are stored in a file at the root of the package project. XML plist and JSON formats are supported. YAML is supported if you also install the Python PyYAML module. A build-info file is not strictly required, and a build will use default values if this file is missing.
76 |
77 | XML plist is the default and preferred format. It can represent all the needed macOS data structures. JSON and YAML are also supported, but there is no guarantee that these formats will support future features of munkipkg. (Translation: use XML plist format unless it really, really bothers you; in that case use JSON or YAML but don't come crying to me if you can't use shiny new features with your JSON or YAML files. And please don't ask for help _formatting_ your JSON or YAML!)
78 |
79 | #### build-info.plist
80 |
81 | This must be in XML (text) format. Binary plists and "old-style-ASCII"-formatted plists are not supported. For a new project created with `munkipkg --create Foo`, the build-info.plist looks like this:
82 |
83 | ```xml
84 |
85 |
86 |
87 |
88 | distribution_style
89 |
90 | identifier
91 | com.github.munki.pkg.Foo
92 | install_location
93 | /
94 | name
95 | Foo-${version}.pkg
96 | ownership
97 | recommended
98 | postinstall_action
99 | none
100 | suppress_bundle_relocation
101 |
102 | version
103 | 1.0
104 |
105 |
106 | ```
107 |
108 | #### build-info.json
109 |
110 | Alternately, you may specify build-info in JSON format. A new project created with `munkipkg --create --json Foo` would have this build-info.json file:
111 |
112 | ```json
113 | {
114 | "postinstall_action": "none",
115 | "suppress_bundle_relocation": true,
116 | "name": "Foo-${version}.pkg",
117 | "distribution_style": false,
118 | "preserve_xattr": false,
119 | "install_location": "/",
120 | "version": "1.0",
121 | "ownership": "recommended",
122 | "identifier": "com.github.munki.pkg.Foo"
123 | }
124 | ```
125 |
126 | If both build-info.plist and build-info.json are present, the plist file will be used; the json file will be ignored.
127 |
128 | #### build-info.yaml
129 |
130 | As a third alternative, you may specify build-info in YAML format, if you've installed the Python YAML module (PyYAML). A new project created with `munkipkg --create --yaml Foo` would have this build-info.yaml file:
131 |
132 | ```yaml
133 | distribution_style: false
134 | identifier: com.github.munki.pkg.Foo
135 | install_location: /
136 | name: Foo-${version}.pkg
137 | ownership: recommended
138 | postinstall_action: none
139 | preserve_xattr: false
140 | suppress_bundle_relocation: true
141 | version: '1.0'
142 | ```
143 |
144 | If both build-info.plist and build-info.yaml are present, the plist file will be used; the yaml file will be ignored.
145 |
146 | ##### JSON and YAML formatting note
147 |
148 | Note in the JSON and YAML examples that the version "number" is wrapped in quotes. This is important -- XML plists have explicit type tags and the correct type for a version "number" is `string`. JSON and YAML infer a value's type based on formatting. Without quotes wrapping the value, `1.0` would be interpreted as a floating point number, and not a string, potentially causing an error at build time. This issue might affect future build-info keys supported by `munkipkg`, so take care.
149 |
150 | #### build-info keys
151 |
152 | **distribution_style**
153 | Boolean: true or false. Defaults to false. If present and true, package built will be a "distribution-style" package.
154 |
155 | **identifier**
156 | String containing the package identifier. If this is missing, one is constructed using the name of the package project directory.
157 |
158 | **install_location**
159 | String. Path to the intended install location of the payload on the target disk. Defaults to "/".
160 |
161 | **name**
162 | String containing the package name. If this is missing, one is constructed using the name of the package project directory.
163 |
164 | By default, the package name is suffixed with the version number using `${version}`. This suffix can be removed if desired, or it can be specified manually.
165 |
166 | JSON Example:
167 |
168 | ```json
169 | "name": "munki_kickstart-${version}.pkg"
170 | "name": "munki_kickstart.pkg"
171 | "name": "munki_kickstart-1.0.pkg"
172 | ```
173 |
174 | **ownership**
175 | String. One of "recommended", "preserve", or "preserve-other". Defaults to "recommended". See the man page for `pkgbuild` for a description of the ownership options.
176 |
177 | **postinstall_action**
178 | String. One of "none", "logout", or "restart". Defaults to "none".
179 |
180 | **preserve_xattr**
181 | Boolean: true or false. Defaults to false. Setting this to true would preserve extended attributes, like codesigned flat files (e.g. script files), amongst other xattr's such as the apple quarantine warning (com.apple.quarantine).
182 |
183 | **product id**
184 | Optional. String. Sets the value of the "product id" attribute in a distribution-style package's Distribution file. If this is not defined, the value for `identifier` (the package identifier) will be used instead.
185 |
186 | **suppress\_bundle\_relocation**
187 | Boolean: true or false. Defaults to true. If present and false, bundle relocation will be allowed, which causes the Installer to update bundles found in locations other than their default location. For deploying software in a managed environment, this is rarely what you want.
188 |
189 | **version**
190 | A string representation of the version number. Defaults to "1.0".
191 |
192 | The value of this key is referenced in the default package name using `${version}`. (See the **name** key details above.)
193 |
194 | **signing_info**
195 | Dictionary of signing options. See below.
196 |
197 | **notarization_info**
198 | Dictionary of notarization options. See below.
199 |
200 | #### build-info keys supported by macOS 12+
201 |
202 | **compression**
203 | String. One of "latest" or "legacy". When creating pkg files on macOS 12 or higher, using "latest" in conjunction with a `min-os-version` of `10.10` (or higher) will result in increased compression of pkg content.
204 |
205 | **min-os-version**
206 | String. Numeric representation of the target OS's MAJOR.MINOR versions. Eg "10.5", "10.10", "12.0", etc
207 |
208 | **large-payload**
209 | Boolean. If `large-payload` is set to `true` the `--large-payload` option will be used when building the package. This option requires that `min-os-version` be set to "12.0" or higher. Defaults to False
210 |
211 | ### Build directory
212 |
213 | `munkipkg` creates its packages inside the build directory. A build directory is created within the project directory if one doesn't exist at build time.
214 |
215 | ### Scripts directory
216 |
217 | The scripts folder contains scripts to be included as part of the package.
218 |
219 | munkipkg makes use of `pkgbuild`. Therefore the "main" scripts must be named either "preinstall" or "postinstall" (with no extensions) and must have their execute bit set. Other scripts can be called by the preinstall or postinstall scripts, but only those two scripts will be automatically called during package installation.
220 |
221 | ### Payload directory
222 |
223 | The payload folder contains the files to be installed. These files must have the intended directory structure. Files at the top-level of the payload folder will be installed at the root of the target volume. If you wanted to install files 'foo' and 'bar' in /usr/local/bin of the target volume, your payload folder would look like this:
224 |
225 | ```
226 | payload/
227 | usr/
228 | local/
229 | bin/
230 | foo
231 | bar
232 | ```
233 |
234 | ### Payload-free packages
235 |
236 | You can use this tool to build payload-free packages in two variants.
237 |
238 | If there is no payload folder at all, `pkgbuild` is called with the `--nopayload` option. The resulting package will not leave a receipt when installed.
239 |
240 | If the payload folder exists, but is empty, you'll get a "pseudo-payload-free" package. No files will be installed, but a receipt will be left. This is often the more useful option if you need to track if the package has been installed on machines you manage.
241 |
242 | ### Package signing
243 |
244 | You may sign packages as part of the build process by adding a signing\_info dictionary to the build\_info.plist:
245 |
246 | ```xml
247 | signing_info
248 |
249 | identity
250 | Signing Identity Common Name
251 | keychain
252 | /path/to/SpecialKeychain
253 | additional_cert_names
254 |
255 | Intermediate CA Common Name 1
256 | Intermediate CA Common Name 2
257 |
258 | timestamp
259 |
260 |
261 | ```
262 |
263 | or, in JSON format in a build-info.json file:
264 |
265 | ```json
266 | "signing_info": {
267 | "identity": "Signing Identity Common Name",
268 | "keychain": "/path/to/SpecialKeychain",
269 | "additional_cert_names": ["Intermediate CA Common Name 1",
270 | "Intermediate CA Common Name 2"],
271 | "timestamp": true,
272 | }
273 | ```
274 |
275 | The only required key/value in the signing_info dictionary is 'identity'.
276 |
277 | See the **SIGNED PACKAGES** section of the man page for `pkgbuild` or the **SIGNED PRODUCT ARCHIVES** section of the man page for `productbuild` for more information on the signing options.
278 |
279 |
280 | ### Package notarization
281 |
282 | **Important notes**:
283 |
284 | - Please read the [Customizing the Notarization Workflow](https://developer.apple.com/documentation/security/notarizing_your_app_before_distribution/customizing_the_notarization_workflow) web page before you start notarizing your packages.
285 | - Xcode 13 (or newer) is **required**. If you have more than one version of Xcode installed on your Mac, be sure to use the xcode-select utility to choose the appropriate version: `sudo xcode-select -s /path/to/Xcode13.app`.
286 | - Unproxied network access to the Apple infrastructure (Usually `17.0.0.0/8` network) is required.
287 | - Notarization tool tries to notarize not only the package but also the package payload. All code in the payload (including but not limited to app bundles, frameworks, kernel extensions) needs to be properly signed with the hardened runtime restrictions in order to be notarized. Please read Apple Developer documentation for more information.
288 |
289 | You may notarize **SIGNED PACKAGES** as part of the build process by adding a `notarization_info` dictionary to the build\_info.plist:
290 |
291 | ```xml
292 | notarization_info
293 |
294 | apple_id
295 | john.appleseed@apple.com
296 | password
297 | @keychain:AC_PASSWORD
298 | team_id
299 | ABCDEF12345
300 | asc_provider
301 | JohnAppleseed1XXXXXX8
302 | staple_timeout
303 | 600
304 |
305 | ```
306 |
307 | or, in JSON format in a build-info.json file:
308 |
309 | ```json
310 | "notarization_info": {
311 | "username": "john.appleseed@apple.com",
312 | "password": "@keychain:AC_PASSWORD",
313 | "asc_provider": "JohnAppleseed1XXXXXX8",
314 | "stapler_timeout": 600
315 | }
316 | ```
317 |
318 | Keys/values of the `notarization_info` dictionary:
319 |
320 | | Key | Type | Required | Description |
321 | | ----------------- | ------- | -------- | ----------- |
322 | | apple_id | String | (see authentication) | Login email address of your developer Apple ID |
323 | | team_id | String | (see authentication) | The team identifier for the Developer Team, usually 10 alphanumeric characters |
324 | | password | String | (see authentication) | 2FA app specific password. |
325 | | keychain_profile | String | (see authentication) | App Store Connect API key issuer ID. |
326 | | asc_provider | String | No | Only needed when a user account is associated with multiple providers |
327 | | primary_bundle_id | String | No | Defaults to `identifier`. Whether specified or not underscore characters are always automatically converted to hyphens since Apple notary service does not like underscores |
328 | | staple_timeout | Integer | No | See paragraph bellow |
329 |
330 | **Authentication**
331 |
332 | To notarize the package you have to use Apple ID with access to App Store Connect. There are two possible authentication methods: App-specific password and keychain profile. Either `apple_id`+`team_id`+`password` or `keychain_profile` keys(s) **must** be specified in the `notarization_info` dictionary. If you specify both `password` based takes precedence.
333 |
334 | **Using the password**
335 |
336 | For information about the password and saving it to the login keychain see the web page [Customizing the Notarization Workflow](https://developer.apple.com/documentation/security/notarizing_your_app_before_distribution/customizing_the_notarization_workflow).
337 |
338 | If you configure `munkipkg` to use the password from the login keychain user is going to be prompted to allow access to the password. You can authorize this once clicking *Allow* or permanently clicking *Always Allow*.
339 |
340 | **How to Setup Your Keychain for Notarization with `notarytool`**
341 |
342 | Dependency: `notarytool` is bundled with Xcode, so you need to have the latest version of Xcode installed and the command line tools.
343 |
344 | Run:
345 | `/Applications/Xcode.app/Contents/Developer/usr/bin/notarytool store-credentials`
346 |
347 | It will ask you for a profile name, use: `notarization_credentials` as that is what all our pkginfo files will have as the `keychain_profile` key in the munkipkg project json file, as such:
348 |
349 | Skip the next question about App Store API.
350 |
351 | 1. It will move to ask you for a Developer Apple ID email
352 | 2. The password here is a unique app-specific password created in appleid.apple.com from the same Developer ID account.
353 | 3. Enter your Team ID from the developer certificate.
354 |
355 | All your munkipkg json project files will need that notarization info added as such:
356 |
357 | ```json
358 | {
359 | "postinstall_action": "none",
360 | "suppress_bundle_relocation": true,
361 | "name": "PackageName.pkg",
362 | "distribution_style": true,
363 | "install_location": "/path/to/payload/location/",
364 | "version": "14.0",
365 | "ownership": "recommended",
366 | "identifier": "com.domain.PackageName",
367 | "signing_info": {
368 | "identity": "Developer ID Installer: Company Name (Team ID)",
369 | "keychain": "/path/to/certificate/signing.keychain",
370 | "timestamp": true
371 | },
372 | "notarization_info": {
373 | "keychain_profile": "notarization_credentials"
374 | }
375 | }
376 | ```
377 |
378 | `munkipkg` will now call the `keychain_profile` from the json to run as the credentials for the notarization.
379 |
380 | **Creating the API key**
381 |
382 | 1. Log into [App Store Connect](https://appstoreconnect.apple.com) using developer Apple ID with access to API keys.
383 | 2. Go to Users and Access -> Keys.
384 | 3. Click + button to create a new key.
385 | 4. Name the key and select proper access - Developer.
386 | 5. Download the API key and save it to one of the following directories `./private_keys`, `~/private_keys`, `~/.private_keys`. Filename format is `AuthKey_.p8`. Use `` part when configuring `api_key` option.
387 | 6. Note the *Issuer ID* at the top of the web page. It must be provided using `api_issuer` option.
388 |
389 | **About stapling**
390 |
391 | `munkipkg` basically does following:
392 |
393 | 1. Uploads the package to Apple notary service using `xcrun notarytool submit --output-format plist build/munki_kickstart.pkg --apple-id "john.appleseed@apple.com" --team-id ABCDEF12345 --password "@keychain:AC_PASSWORD"`
394 | 2. Checks periodically state of notarization process using `xcrun notarytool info --output-format plist --apple-id "john.appleseed@apple.com" --team-id ABCDEF12345 --password "@keychain:AC_PASSWORD"`
395 | 3. If notarization was successful `munkipkg` staples the package using `xcrun stapler staple munki_kickstart.pkg`
396 |
397 | There is a time delay between successful upload of a signed package to the notary service and notarization result from the service.
398 | `munkipkg` checks multiple times if notarization process is done. There is sleep period between each try. Sleep period starts at 5 seconds and increases by increments of 5 (5s, 10s, 10s, etc.).
399 | With `staple_timeout` parameter you can specify timeout in seconds (**default: 300 seconds**) after which `munkipkg` gives up.
400 |
401 | ### Additional options
402 |
403 | `--create`
404 | Creates a new empty template package project. See [**Creating a new project**](#creating-a-new-project).
405 |
406 | `--import`
407 | `munkipkg --import /path/to/flat.pkg /path/to/project_dir`
408 |
409 | This option will import an existing package and convert it into a package project. project_dir must not exist; it will be created. build-info will be in plist format, add the --json option to output in JSON format instead. (IE: `munkipkg --json --import /path/to/flat.pkg /path/to/project_dir`) Not all package formats are supported.
410 |
411 | `--export-bom-info`
412 | This option causes munkipkg to export bom info from the built package to a file named "Bom.txt" in the root of the package project directory. Since git does not normally track ownership, group, or mode of tracked files, and since the "ownership" option to `pkgbuild` can also result in different owner and group of files included in the package payload, exporting this info into a text file allows you to track this metadata in git (or other version control) as well.
413 |
414 | `--skip-notarization`
415 | Use this option to skip the whole notarization process when notarization is specified in the build-info.
416 |
417 | `--skip-stapling`
418 | Use this option to skip only the stapling part of the notarization process when notarization is specified in the build-info.
419 |
420 | `--sync`
421 | This option causes munkipkg to read the Bom.txt file, and use its information to create any missing empty directories and to set the permissions on files and directories. See [**Important git notes**](#important-git-notes) below.
422 |
423 | `--quiet`
424 | Causes munkipkg to suppress normal output messages. Errors will still be printed to stderr.
425 |
426 | `--help`, `--version`
427 | Prints help message and tool version, respectively.
428 |
429 | ## Important git notes
430 |
431 | Git was designed to track source code. Its focus is tracking changes in the contents of files. It's not a perfect fit for tracking the parts making up a package. Specifically, git doesn't track owner or group of files or directories, and does not track any mode bits except for the execute bit for the owner. Git also does not track empty directories.
432 |
433 | This could be a problem if you want to store package project directories in git and `git clone` them; the clone operation will fail to replicate empty directories in the package project and will fail to set the correct mode for files and directories. (Owner and group are less of an issue if you use ownership=recommended for your `pkgbuild` options.)
434 |
435 | The solution to this problem is the Bom.txt file, which lists all the files and directories in the package, along with their mode, owner and group.
436 |
437 | This file (Bom.txt) can be tracked by git.
438 |
439 | You can create this file when building package by adding the `--export-bom-info` option. After the package is built, the Bom is extracted and `lsbom` is used to read its contents, which are written to "Bom.txt" at the root of the package project directory.
440 |
441 | A recommended workflow would be to build a project with `--export-bom-info` and add the Bom.txt file to the next git commit in order to preserve the data that git does not normally track.
442 |
443 | After doing a `git clone` or `git pull` operation, you can then use `munkipkg --sync project_name` to cause munkipkg to read the Bom.txt file and use the info within to create any missing directories and to set file and directory modes to those recorded in the bom.
444 |
445 | This workflow is not ideal, as it requires you to remember two new manual steps (`munkipkg --export` before doing a git commit and `munkipkg --sync` after doing a `git clone` or `git pull`) but is necessary to preserve data that git otherwise ignores.
446 |
--------------------------------------------------------------------------------
/SuppressSetupAssistant/.gitignore:
--------------------------------------------------------------------------------
1 | # .DS_Store files!
2 | .DS_Store
3 |
4 | # our build directory
5 | build/
6 |
--------------------------------------------------------------------------------
/SuppressSetupAssistant/build-info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | distribution_style
6 |
7 | identifier
8 | com.github.munki.pkg.SuppressSetupAssistant
9 | install_location
10 | /
11 | name
12 | SuppressSetupAssistant.pkg
13 | ownership
14 | recommended
15 | postinstall_action
16 | none
17 | suppress_bundle_relocation
18 |
19 | version
20 | 1.0
21 |
22 |
23 |
--------------------------------------------------------------------------------
/SuppressSetupAssistant/payload/Library/Receipts/.SetupRegComplete:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/munki/munki-pkg/96cffb4eac9207c1130404ec1fee8f4777fa38fd/SuppressSetupAssistant/payload/Library/Receipts/.SetupRegComplete
--------------------------------------------------------------------------------
/SuppressSetupAssistant/payload/private/var/db/.AppleSetupDone:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/munki/munki-pkg/96cffb4eac9207c1130404ec1fee8f4777fa38fd/SuppressSetupAssistant/payload/private/var/db/.AppleSetupDone
--------------------------------------------------------------------------------
/TurnOffBluetooth/.gitignore:
--------------------------------------------------------------------------------
1 | # .DS_Store files!
2 | .DS_Store
3 |
4 | # our build directory
5 | build/
6 |
--------------------------------------------------------------------------------
/TurnOffBluetooth/build-info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | distribution_style
6 |
7 | identifier
8 | com.github.munki.pkg.TurnOffBluetooth
9 | install_location
10 | /
11 | name
12 | TurnOffBluetooth.pkg
13 | ownership
14 | recommended
15 | postinstall_action
16 | none
17 | suppress_bundle_relocation
18 |
19 | version
20 | 1.0
21 |
22 |
23 |
--------------------------------------------------------------------------------
/TurnOffBluetooth/scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # turn off Bluetooth
4 | if [ "$3" == "/" ]; then
5 | TARGETVOL=""
6 | else
7 | TARGETVOL="$3"
8 | fi
9 |
10 | BLUETOOTHPREFS="$TARGETVOL/Library/Preferences/com.apple.Bluetooth"
11 | /usr/bin/defaults write "$BLUETOOTHPREFS" ControllerPowerState -int 0
12 |
13 | if [ "$3" == "/" ]; then
14 | # we're installing on the startup disk, so
15 | # restart bluetooth daemon to pick up our changes
16 | /usr/bin/killall -HUP blued
17 | fi
18 |
--------------------------------------------------------------------------------
/munki_kickstart/.gitignore:
--------------------------------------------------------------------------------
1 | # .DS_Store files!
2 | .DS_Store
3 |
4 | # our build directory
5 | build/
6 |
--------------------------------------------------------------------------------
/munki_kickstart/build-info.json:
--------------------------------------------------------------------------------
1 | {
2 | "ownership": "recommended",
3 | "suppress_bundle_relocation": true,
4 | "identifier": "com.github.munki.pkg.munki_kickstart",
5 | "postinstall_action": "none",
6 | "distribution_style": true,
7 | "version": "1.0",
8 | "name": "munki_kickstart.pkg",
9 | "install_location": "/"
10 | }
11 |
--------------------------------------------------------------------------------
/munki_kickstart/payload/Users/Shared/.com.googlecode.munki.checkandinstallatstartup:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/munki/munki-pkg/96cffb4eac9207c1130404ec1fee8f4777fa38fd/munki_kickstart/payload/Users/Shared/.com.googlecode.munki.checkandinstallatstartup
--------------------------------------------------------------------------------
/munkipkg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # encoding: utf-8
3 | """
4 | munkipkg
5 |
6 | A tool for making packages from projects that can be easily managed in a
7 | version control system like git.
8 |
9 | """
10 | # Copyright 2015-2022 Greg Neagle.
11 | #
12 | # Licensed under the Apache License, Version 2.0 (the "License");
13 | # you may not use this file except in compliance with the License.
14 | # You may obtain a copy of the License at
15 | #
16 | # http://www.apache.org/licenses/LICENSE-2.0
17 | #
18 | # Unless required by applicable law or agreed to in writing, software
19 | # distributed under the License is distributed on an "AS IS" BASIS,
20 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 | # See the License for the specific language governing permissions and
22 | # limitations under the License.
23 |
24 | from __future__ import absolute_import, print_function
25 |
26 | import glob
27 | import json
28 | import optparse
29 | import os
30 | import plistlib
31 | import shutil
32 | import stat
33 | import subprocess
34 | import sys
35 | import tempfile
36 | import time
37 | from xml.dom import minidom
38 | from xml.parsers.expat import ExpatError
39 |
40 | try:
41 | import yaml
42 | YAML_INSTALLED = True
43 | except ImportError:
44 | YAML_INSTALLED = False
45 |
46 | from xml.dom import minidom
47 | from xml.parsers.expat import ExpatError
48 |
49 | VERSION = "1.0"
50 | DITTO = "/usr/bin/ditto"
51 | LSBOM = "/usr/bin/lsbom"
52 | PKGBUILD = "/usr/bin/pkgbuild"
53 | PKGUTIL = "/usr/sbin/pkgutil"
54 | PRODUCTBUILD = "/usr/bin/productbuild"
55 | XCRUN = "/usr/bin/xcrun"
56 |
57 | GITIGNORE_DEFAULT = """# .DS_Store files!
58 | .DS_Store
59 |
60 | # our build directory
61 | build/
62 | """
63 |
64 | BUILD_INFO_FILE = "build-info"
65 | REQUIREMENTS_PLIST = "product-requirements.plist"
66 | BOM_TEXT_FILE = "Bom.txt"
67 |
68 | STAPLE_TIMEOUT = 300
69 | STAPLE_SLEEP = 5
70 |
71 |
72 | class MunkiPkgError(Exception):
73 | '''Base Exception for errors in this domain'''
74 | pass
75 |
76 |
77 | class BuildError(MunkiPkgError):
78 | '''Exception for build errors'''
79 | pass
80 |
81 |
82 | class PkgImportError(MunkiPkgError):
83 | '''Exception for pkg import errors'''
84 | pass
85 |
86 |
87 | def readPlistFromString(data):
88 | '''Wrapper for the differences between Python 2 and Python 3's plistlib'''
89 | try:
90 | return plistlib.loads(data)
91 | except AttributeError:
92 | # plistlib module doesn't have a load function (as in Python 2)
93 | return plistlib.readPlistFromString(data)
94 |
95 |
96 | def readPlist(filepath):
97 | '''Wrapper for the differences between Python 2 and Python 3's plistlib'''
98 | try:
99 | with open(filepath, "rb") as fileobj:
100 | return plistlib.load(fileobj)
101 | except AttributeError:
102 | # plistlib module doesn't have a load function (as in Python 2)
103 | return plistlib.readPlist(filepath)
104 |
105 |
106 | def writePlist(plist, filepath):
107 | '''Wrapper for the differences between Python 2 and Python 3's plistlib'''
108 | try:
109 | with open(filepath, "wb") as fileobj:
110 | plistlib.dump(plist, fileobj)
111 | except AttributeError:
112 | # plistlib module doesn't have a dump function (as in Python 2)
113 | plistlib.writePlist(plist, filepath)
114 |
115 |
116 | def unlink_if_possible(pathname):
117 | '''Attempt to remove pathname but don't raise an execption if it fails'''
118 | try:
119 | os.unlink(pathname)
120 | except OSError as err:
121 | print("WARNING: could not remove %s: %s" % (pathname, err),
122 | file=sys.stderr)
123 |
124 |
125 | def display(message, quiet=False, toolname=None):
126 | '''Print message to stdout unless quiet is True'''
127 | if not quiet:
128 | if not toolname:
129 | toolname = os.path.basename(sys.argv[0])
130 | print(("%s: %s" % (toolname, message)))
131 |
132 |
133 | def run_subprocess(cmd):
134 | '''Runs cmd with Popen'''
135 | proc = subprocess.Popen(
136 | cmd,
137 | shell=False,
138 | universal_newlines=True,
139 | bufsize=1,
140 | stdin=subprocess.PIPE,
141 | stdout=subprocess.PIPE,
142 | stderr=subprocess.PIPE,
143 | )
144 |
145 | proc_stdout, proc_stderr = proc.communicate()
146 | retcode = proc.returncode
147 | return (retcode, proc_stdout, proc_stderr)
148 |
149 |
150 | def validate_build_info_keys(build_info, file_path):
151 | '''Validates the data read from build_info.(plist|json|yaml|yml)'''
152 | valid_values = {
153 | 'compression': ['legacy', 'latest'],
154 | 'ownership': ['recommended', 'preserve', 'preserve-other'],
155 | 'postinstall_action': ['none', 'logout', 'restart'],
156 | 'suppress_bundle_relocation': [True, False],
157 | 'distribution_style': [True, False],
158 | 'preserve_xattr': [True, False],
159 | }
160 | for key in valid_values:
161 | if key in build_info:
162 | if build_info[key] not in valid_values[key]:
163 | print("ERROR: %s key '%s' has illegal value: %s"
164 | % (file_path, key, repr(build_info[key])),
165 | file=sys.stderr)
166 | print('ERROR: Legal values are: %s' % valid_values[key],
167 | file=sys.stderr)
168 | return False
169 | return True
170 |
171 |
172 | def read_build_info(path):
173 | '''Reads and validates data in the build_info'''
174 | build_info = None
175 | exception_list = (ExpatError, ValueError)
176 | if YAML_INSTALLED:
177 | exception_list = (ExpatError, ValueError, yaml.scanner.ScannerError)
178 | try:
179 | if path.endswith('.json'):
180 | with open(path, 'r') as openfile:
181 | build_info = json.load(openfile)
182 | elif path.endswith(('.yaml', '.yml')):
183 | with open(path, 'r') as openfile:
184 | build_info = yaml.load(openfile, Loader=yaml.FullLoader)
185 | elif path.endswith('.plist'):
186 | build_info = readPlist(path)
187 | except exception_list as err:
188 | raise BuildError("%s is not a valid %s file: %s"
189 | % (path, path.split('.')[-1], str(err)))
190 | validate_build_info_keys(build_info, path)
191 | if '${version}' in build_info['name']:
192 | build_info['name'] = build_info['name'].replace(
193 | '${version}',
194 | str(build_info['version'])
195 | )
196 |
197 | return build_info
198 |
199 |
200 | def make_component_property_list(build_info, options):
201 | """Use pkgbuild --analyze to build a component property list; then
202 | turn off package relocation, Return path to the resulting plist."""
203 | component_plist = os.path.join(build_info['tmpdir'], 'component.plist')
204 | cmd = [PKGBUILD]
205 | if options.quiet:
206 | cmd.append('--quiet')
207 | cmd.extend(["--analyze", "--root", build_info['payload'], component_plist])
208 | try:
209 | returncode = subprocess.call(cmd)
210 | except OSError as err:
211 | raise BuildError(
212 | "pkgbuild execution failed with error code %d: %s"
213 | % (err.errno, err.strerror))
214 | if returncode:
215 | raise BuildError("pkgbuild failed with exit code %d" % returncode)
216 | try:
217 | plist = readPlist(component_plist)
218 | except ExpatError as err:
219 | raise BuildError("Couldn't read %s" % component_plist)
220 | # plist is an array of dicts, iterate through
221 | for bundle in plist:
222 | if bundle.get("BundleIsRelocatable"):
223 | bundle["BundleIsRelocatable"] = False
224 | display('Turning off bundle relocation for %s'
225 | % bundle['RootRelativeBundlePath'], options.quiet)
226 | try:
227 | writePlist(plist, component_plist)
228 | except BaseException as err:
229 | raise BuildError("Couldn't write %s" % component_plist)
230 | return component_plist
231 |
232 |
233 | def make_pkginfo(build_info, options):
234 | '''Creates a stub PackageInfo file for use with pkgbuild'''
235 | if build_info['postinstall_action'] != 'none' and not options.quiet:
236 | display("Setting postinstall-action to %s"
237 | % build_info['postinstall_action'], options.quiet)
238 | pkginfo_path = os.path.join(build_info['tmpdir'], 'PackageInfo')
239 | pkginfo_text = (
240 | ''
241 | ''
242 | % (build_info['postinstall_action'],
243 | str(build_info['preserve_xattr']).lower())
244 | )
245 | try:
246 | fileobj = open(pkginfo_path, mode='w')
247 | fileobj.write(pkginfo_text)
248 | fileobj.close()
249 | return pkginfo_path
250 | except (OSError, IOError) as err:
251 | raise BuildError('Couldn\'t create PackageInfo file: %s' % err)
252 |
253 |
254 | def default_build_info(project_dir):
255 | '''Return dict with default build info values'''
256 | info = {}
257 | info['ownership'] = "recommended"
258 | info['suppress_bundle_relocation'] = True
259 | info['postinstall_action'] = 'none'
260 | info['preserve_xattr'] = False
261 | basename = os.path.basename(project_dir.rstrip('/')).replace(" ", "")
262 | info['name'] = basename + '-${version}.pkg'
263 | info['identifier'] = "com.github.munki.pkg." + basename
264 | info['install_location'] = '/'
265 | info['version'] = "1.0"
266 | info['distribution_style'] = False
267 | return info
268 |
269 |
270 | def get_build_info(project_dir, options):
271 | '''Return dict with build info'''
272 | info = default_build_info(project_dir)
273 | info['project_dir'] = project_dir
274 | # override default values with values from BUILD_INFO_PLIST
275 | supported_keys = [
276 | 'compression',
277 | 'name',
278 | 'identifier',
279 | 'version',
280 | 'ownership',
281 | 'install_location',
282 | 'min-os-version',
283 | 'large-payload',
284 | 'postinstall_action',
285 | 'preserve_xattr',
286 | 'suppress_bundle_relocation',
287 | 'distribution_style',
288 | 'signing_info',
289 | 'notarization_info',
290 | ]
291 | build_file = os.path.join(project_dir, BUILD_INFO_FILE)
292 | file_type = None
293 | if not options.yaml and not options.json:
294 | file_types = ['plist', 'json', 'yaml', 'yml']
295 | for ext in file_types:
296 | if os.path.exists(build_file + '.' + ext):
297 | if file_type is None:
298 | file_type = ext
299 | else:
300 | raise MunkiPkgError(
301 | "ERROR: Multiple build-info files found!")
302 | else:
303 | file_type = (
304 | 'yaml' if options.yaml else 'json' if options.json else 'plist')
305 |
306 | file_info = None
307 | if file_type and os.path.exists(build_file + '.' + file_type):
308 | file_info = read_build_info(build_file + '.' + file_type)
309 |
310 | if file_info:
311 | for key in supported_keys:
312 | if key in file_info:
313 | info[key] = file_info[key]
314 | else:
315 | raise MunkiPkgError('ERROR: No build-info file found!')
316 |
317 | return info
318 |
319 |
320 | def non_recommended_permissions_in_bom(project_dir):
321 | '''Analyzes Bom.txt to determine if there are any items with owner/group
322 | other than 0/0, which implies we should handle ownership differently'''
323 |
324 | bom_list_file = os.path.join(project_dir, BOM_TEXT_FILE)
325 | if not os.path.exists(bom_list_file):
326 | return False
327 | try:
328 | with open(bom_list_file) as fileref:
329 | while True:
330 | item = fileref.readline()
331 | if not item:
332 | break
333 | if item == '\n':
334 | # shouldn't be any empty lines in Bom.txt, but...
335 | continue
336 | parts = item.rstrip('\n').split('\t')
337 | user_group = parts[2]
338 | if user_group != '0/0':
339 | return True
340 | return False
341 | except (OSError, ValueError) as err:
342 | print('ERROR: %s' % err, file=sys.stderr)
343 | return False
344 |
345 |
346 | def sync_from_bom_info(project_dir, options):
347 | '''Uses Bom.txt to apply modes to files in payload dir and create any
348 | missing empty directories, since git does not track these.'''
349 |
350 | # possible to-do: preflight check: if there are files missing
351 | # (and not just directories), or there are extra files or directories,
352 | # bail without making any changes
353 |
354 | # possible to-do: a refinement of the above preflight check
355 | # -- also check file checksums
356 |
357 | bom_list_file = os.path.join(project_dir, BOM_TEXT_FILE)
358 | payload_dir = os.path.join(project_dir, 'payload')
359 | try:
360 | build_info = get_build_info(project_dir, options)
361 | except MunkiPkgError:
362 | build_info = default_build_info(project_dir)
363 | running_as_root = (os.geteuid() == 0)
364 | if not os.path.exists(bom_list_file):
365 | print((
366 | "ERROR: Can't sync with bom info: no %s found in project directory."
367 | % BOM_TEXT_FILE), file=sys.stderr)
368 | return -1
369 | if build_info['ownership'] != 'recommended' and not running_as_root:
370 | print((
371 | "\nWARNING: build-info ownership: %s might require using "
372 | "sudo to properly sync owner and group for payload files.\n"
373 | % build_info['ownership']), file=sys.stderr)
374 |
375 | returncode = 0
376 | changes_made = 0
377 |
378 | try:
379 | with open(bom_list_file) as fileref:
380 | while True:
381 | item = fileref.readline()
382 | if not item:
383 | break
384 | if item == '\n':
385 | # shouldn't be any empty lines in Bom.txt, but...
386 | continue
387 | parts = item.rstrip('\n').split('\t')
388 | path = parts[0]
389 | if path.startswith('./'):
390 | path = path[2:]
391 | full_mode = parts[1]
392 | user_group = parts[2].partition('/')
393 | desired_user = int(user_group[0])
394 | desired_group = int(user_group[2])
395 | desired_mode = int(full_mode[-4:], 8)
396 | payload_path = os.path.join(payload_dir, path)
397 | basename = os.path.basename(path)
398 | if basename.startswith('._'):
399 | otherfile = os.path.join(
400 | os.path.dirname(path), basename[2:])
401 | print((
402 | 'WARNING: file %s contains extended attributes or a '
403 | 'resource fork for %s. git and pkgbuild may not '
404 | 'properly preserve extended attributes.'
405 | % (path, otherfile)), file=sys.stderr)
406 | continue
407 | if os.path.lexists(payload_path):
408 | # file exists, check permission bits and adjust if needed
409 | current_mode = stat.S_IMODE(os.lstat(payload_path).st_mode)
410 | if current_mode != desired_mode:
411 | display("Changing mode of %s to %s"
412 | % (payload_path, oct(desired_mode)),
413 | options.quiet)
414 | os.lchmod(payload_path, desired_mode)
415 | changes_made += 1
416 | elif full_mode.startswith('4'):
417 | # file doesn't exist and it's a directory; re-create it
418 | display("Creating %s with mode %s"
419 | % (payload_path, oct(desired_mode)),
420 | options.quiet)
421 | os.mkdir(payload_path, desired_mode)
422 | changes_made += 1
423 | continue
424 | else:
425 | # missing file. This is a problem.
426 | print("ERROR: File %s is missing in payload"
427 | % payload_path, file=sys.stderr)
428 | returncode = -1
429 | break
430 | if running_as_root:
431 | # we can sync owner and group as well
432 | current_user = os.lstat(payload_path).st_uid
433 | current_group = os.lstat(payload_path).st_gid
434 | if (current_user != desired_user or
435 | current_group != desired_group):
436 | display("Changing user/group of %s to %s/%s"
437 | % (payload_path, desired_user, desired_group),
438 | options.quiet)
439 | os.lchown(payload_path, desired_user, desired_group)
440 | changes_made += 1
441 |
442 | except (OSError, ValueError) as err:
443 | print('ERROR: %s' % err, file=sys.stderr)
444 | return -1
445 |
446 | if returncode == 0 and not options.quiet:
447 | if changes_made:
448 | display("Sync successful.")
449 | else:
450 | display("Sync successful: no changes needed.")
451 | return returncode
452 |
453 |
454 | def add_project_subdirs(build_info):
455 | '''Adds and validates project subdirs to build_info'''
456 | # validate payload and scripts dirs
457 | project_dir = build_info['project_dir']
458 | payload_dir = os.path.join(project_dir, 'payload')
459 | scripts_dir = os.path.join(project_dir, 'scripts')
460 | if not os.path.isdir(payload_dir):
461 | payload_dir = None
462 | if not os.path.isdir(scripts_dir):
463 | scripts_dir = None
464 | elif os.listdir(scripts_dir) in [[], ['.DS_Store']]:
465 | # scripts dir is empty; don't include it as part of build
466 | scripts_dir = None
467 | if not payload_dir and not scripts_dir:
468 | raise BuildError(
469 | "%s does not contain a payload folder or a scripts folder."
470 | % project_dir)
471 |
472 | # make sure build directory exists
473 | build_dir = os.path.join(project_dir, 'build')
474 | if not os.path.exists(build_dir):
475 | os.mkdir(build_dir)
476 | elif not os.path.isdir(build_dir):
477 | raise BuildError("%s is not a directory." % build_dir)
478 |
479 | build_info['payload'] = payload_dir
480 | build_info['scripts'] = scripts_dir
481 | build_info['build_dir'] = build_dir
482 | build_info['tmpdir'] = tempfile.mkdtemp()
483 |
484 |
485 | def write_build_info(build_info, project_dir, options):
486 | '''writes out our build-info file in preferred format'''
487 | try:
488 | if options.json:
489 | build_info_json = os.path.join(
490 | project_dir, "%s.json" % BUILD_INFO_FILE)
491 | with open(build_info_json, 'w') as json_file:
492 | json.dump(
493 | build_info, json_file, ensure_ascii=True,
494 | indent=4, separators=(',', ': '))
495 | elif options.yaml:
496 | build_info_yaml = os.path.join(
497 | project_dir, "%s.yaml" % BUILD_INFO_FILE)
498 | with open(build_info_yaml, 'w') as yaml_file:
499 | yaml_file.write(
500 | yaml.dump(build_info, default_flow_style=False)
501 | )
502 | else:
503 | build_info_plist = os.path.join(
504 | project_dir, "%s.plist" % BUILD_INFO_FILE)
505 | writePlist(build_info, build_info_plist)
506 | except OSError as err:
507 | raise MunkiPkgError(err)
508 |
509 |
510 | def create_default_gitignore(project_dir):
511 | '''Create default .gitignore file for new projects'''
512 | gitignore_file = os.path.join(project_dir, '.gitignore')
513 | fileobj = open(gitignore_file, "w")
514 | fileobj.write(GITIGNORE_DEFAULT)
515 | fileobj.close()
516 |
517 |
518 | def create_template_project(project_dir, options):
519 | '''Create an empty pkg project directory with default settings'''
520 | if os.path.exists(project_dir):
521 | if not options.force:
522 | print((
523 | "ERROR: %s already exists! "
524 | "Use --force to convert it to a project directory."
525 | % project_dir), file=sys.stderr)
526 | return -1
527 | payload_dir = os.path.join(project_dir, 'payload')
528 | scripts_dir = os.path.join(project_dir, 'scripts')
529 | build_dir = os.path.join(project_dir, 'build')
530 | try:
531 | if not os.path.exists(project_dir):
532 | os.mkdir(project_dir)
533 | os.mkdir(payload_dir)
534 | os.mkdir(scripts_dir)
535 | os.mkdir(build_dir)
536 | build_info = default_build_info(project_dir)
537 | write_build_info(build_info, project_dir, options)
538 | create_default_gitignore(project_dir)
539 | display(
540 | "Created new package project at %s" % project_dir, options.quiet)
541 | except (OSError, MunkiPkgError) as err:
542 | print('ERROR: %s' % err, file=sys.stderr)
543 | return -1
544 | return 0
545 |
546 |
547 | def export_bom(bomfile, project_dir):
548 | '''Exports bom to text format. Returns returncode from lsbom'''
549 | destination = os.path.join(project_dir, BOM_TEXT_FILE)
550 | try:
551 | with open(destination, mode='w') as fileobj:
552 | cmd = [LSBOM, bomfile]
553 | proc = subprocess.Popen(cmd, stdout=fileobj, stderr=subprocess.PIPE)
554 | _, stderr = proc.communicate()
555 | if proc.returncode:
556 | raise MunkiPkgError(stderr)
557 | except OSError as err:
558 | raise MunkiPkgError(err)
559 |
560 |
561 | def export_bom_info(build_info, options):
562 | '''Extract the bom file from the built package and export its info to the
563 | project directory'''
564 | pkg_path = os.path.join(build_info['build_dir'], build_info['name'])
565 | cmd = [PKGUTIL, '--bom', pkg_path]
566 | display("Extracting bom file from %s" % pkg_path, options.quiet)
567 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
568 | (stdout, stderr) = proc.communicate()
569 | if proc.returncode:
570 | raise BuildError(stderr.strip())
571 |
572 | bomfile = stdout.strip()
573 | destination = os.path.join(build_info['project_dir'], BOM_TEXT_FILE)
574 | display("Exporting bom info to %s" % destination, options.quiet)
575 | export_bom(bomfile, build_info['project_dir'])
576 | unlink_if_possible(bomfile)
577 |
578 |
579 | def add_signing_options_to_cmd(cmd, build_info, options):
580 | '''If build_info contains signing options, add them to the cmd'''
581 | if 'signing_info' in build_info:
582 | display("Adding package signing info to command", options.quiet)
583 | signing_info = build_info['signing_info']
584 | if 'identity' in signing_info:
585 | cmd.extend(['--sign', signing_info['identity']])
586 | else:
587 | raise BuildError('Missing identity in signing info!')
588 | if 'keychain' in signing_info:
589 | cmd.extend(['--keychain', signing_info['keychain']])
590 | if 'additional_cert_names' in signing_info:
591 | additional_cert_names = signing_info['additional_cert_names']
592 | # convert single string to list
593 | try:
594 | text_types = basestring # pylint: disable=basestring-builtin
595 | except NameError:
596 | text_types = str
597 | if isinstance(additional_cert_names, text_types):
598 | additional_cert_names = [additional_cert_names]
599 | for cert_name in additional_cert_names:
600 | cmd.extend(['--cert', cert_name])
601 | if 'timestamp' in signing_info:
602 | if signing_info['timestamp']:
603 | cmd.extend(['--timestamp'])
604 | else:
605 | cmd.extend(['--timestamp=none'])
606 |
607 |
608 | def build_pkg(build_info, options):
609 | '''Use pkgbuild tool to build our package'''
610 | cmd = [PKGBUILD,
611 | '--ownership', build_info['ownership'],
612 | '--identifier', build_info['identifier'],
613 | '--version', str(build_info['version']),
614 | '--info', build_info['pkginfo_path']]
615 | if build_info['payload']:
616 | cmd.extend(['--root', build_info['payload']])
617 | if build_info.get('install_location'):
618 | cmd.extend(['--install-location', build_info['install_location']])
619 | else:
620 | cmd.extend(['--nopayload'])
621 | if 'compression' in build_info:
622 | cmd.extend(['--compression', build_info['compression']])
623 | if build_info['component_plist']:
624 | cmd.extend(['--component-plist', build_info['component_plist']])
625 | if 'min-os-version' in build_info:
626 | cmd.extend(['--min-os-version', build_info['min-os-version']])
627 | if 'large-payload' in build_info:
628 | if build_info['large-payload']:
629 | cmd.append('--large-payload')
630 | if build_info['scripts']:
631 | cmd.extend(['--scripts', build_info['scripts']])
632 | if options.quiet:
633 | cmd.append('--quiet')
634 | if not build_info.get('distribution_style'):
635 | add_signing_options_to_cmd(cmd, build_info, options)
636 | cmd.append(os.path.join(build_info['build_dir'], build_info['name']))
637 | retcode = subprocess.call(cmd)
638 | if retcode:
639 | raise BuildError("Package creation failed.")
640 |
641 |
642 | def build_distribution_pkg(build_info, options):
643 | '''Converts component pkg to dist pkg'''
644 | pkginputname = os.path.join(build_info['build_dir'], build_info['name'])
645 | distoutputname = os.path.join(
646 | build_info['build_dir'], 'Dist-' + build_info['name'])
647 | if os.path.exists(distoutputname):
648 | retcode = subprocess.call(["/bin/rm", "-rf", distoutputname])
649 | if retcode:
650 | raise BuildError(
651 | 'Error removing existing %s: %s' % (distoutputname, retcode))
652 |
653 | cmd = [PRODUCTBUILD]
654 | if options.quiet:
655 | cmd.append('--quiet')
656 | add_signing_options_to_cmd(cmd, build_info, options)
657 | # if there is a PRE-INSTALL REQUIREMENTS PROPERTY LIST, use it
658 | requirements_plist = os.path.join(
659 | build_info['project_dir'], REQUIREMENTS_PLIST)
660 | if os.path.exists(requirements_plist):
661 | cmd.extend(['--product', requirements_plist])
662 | # if build_info contains a product id use that for product id, otherwise
663 | # use package identifier
664 | product_id = build_info.get('product id', build_info['identifier'])
665 | cmd.extend(['--identifier', product_id, '--version', str(build_info['version'])])
666 | # add the input and output package paths
667 | cmd.extend(['--package', pkginputname, distoutputname])
668 |
669 | retcode = subprocess.call(cmd)
670 | if retcode:
671 | raise BuildError("Distribution package creation failed.")
672 | try:
673 | display("Removing component package %s" % pkginputname, options.quiet)
674 | os.unlink(pkginputname)
675 | display("Renaming distribution package %s to %s"
676 | % (distoutputname, pkginputname), options.quiet)
677 | os.rename(distoutputname, pkginputname)
678 | except OSError as err:
679 | raise BuildError(err)
680 |
681 |
682 | def get_primary_bundle_id(build_info):
683 | '''Gets primary bundle id for notarization'''
684 | primary_bundle_id = build_info['notarization_info'].get(
685 | 'primary_bundle_id',
686 | build_info['identifier'],
687 | )
688 |
689 | # Apple notary service does not like underscores
690 | primary_bundle_id = primary_bundle_id.replace('_', '-')
691 |
692 | return primary_bundle_id
693 |
694 |
695 | def add_authentication_options(cmd, build_info):
696 | '''Add --password or --apiKey + --apiIssuer options to the command'''
697 | if (
698 | 'apple_id' in build_info['notarization_info'] and
699 | 'team_id' in build_info['notarization_info'] and
700 | 'password' in build_info['notarization_info']
701 | ):
702 | cmd.extend(
703 | [
704 | '--apple-id', build_info['notarization_info']['apple_id'],
705 | '--team-id', build_info['notarization_info']['team_id'],
706 | '--password', build_info['notarization_info']['password']
707 | ]
708 | )
709 | elif (
710 | 'keychain_profile' in build_info['notarization_info']
711 | ):
712 | cmd.extend(
713 | [
714 | '--keychain-profile',
715 | build_info['notarization_info']['keychain_profile']
716 | ]
717 | )
718 | else:
719 | raise MunkiPkgError(
720 | "apple_id + team_id + password or keychain_profile "
721 | "must be specified in notarization_info."
722 | )
723 |
724 |
725 | def upload_to_notary(build_info, options):
726 | '''Use xcrun notarytool to upload the package to Apple notary service'''
727 |
728 | display("Uploading package to Apple notary service", options.quiet)
729 | cmd = [
730 | XCRUN,
731 | 'notarytool',
732 | 'submit',
733 | '--output-format',
734 | 'plist',
735 | os.path.join(build_info['build_dir'], build_info['name']),
736 | ]
737 | add_authentication_options(cmd, build_info)
738 |
739 | retcode, proc_stdout, proc_stderr = run_subprocess(cmd)
740 | if retcode:
741 | print("notarytool: " + proc_stderr, file=sys.stderr)
742 | raise MunkiPkgError("Notarization upload failed.")
743 |
744 | if proc_stdout.startswith('Generated JWT'):
745 | proc_stdout = proc_stdout.split('\n',1)[1]
746 | try:
747 | output = readPlistFromString(proc_stdout.encode("UTF-8"))
748 | except ExpatError:
749 | print("notarytool: " + proc_stderr, file=sys.stderr)
750 | raise MunkiPkgError("Notarization upload failed.")
751 |
752 | try:
753 | request_id = output['id']
754 | display("id " + request_id, options.quiet, "notarytool")
755 | display(output['message'], options.quiet, "notarytool")
756 | except KeyError:
757 | raise MunkiPkgError("Unexpected output from notarytool")
758 |
759 | return request_id
760 |
761 |
762 | def get_notarization_state(request_id, build_info, options):
763 | '''Checks for result of notarization process'''
764 | state = {}
765 | cmd = [
766 | XCRUN,
767 | 'notarytool',
768 | 'info',
769 | request_id,
770 | '--output-format',
771 | 'plist',
772 | ]
773 | add_authentication_options(cmd, build_info)
774 |
775 | retcode, proc_stdout, proc_stderr = run_subprocess(cmd)
776 | if retcode:
777 | print("notarytool: " + proc_stderr, file=sys.stderr)
778 | raise MunkiPkgError("Notarization check failed.")
779 |
780 | if proc_stdout.startswith('Generated JWT'):
781 | proc_stdout = proc_stdout.split('\n',1)[1]
782 |
783 | try:
784 | output = readPlistFromString(proc_stdout.encode("UTF-8"))
785 | except ExpatError:
786 | print(proc_stderr, file=sys.stderr)
787 | raise MunkiPkgError("Notarization check failed.")
788 | if 'message' not in output:
789 | print("notarytool: " + output.get('success-message', 'Unexpected response'))
790 | print("notarytool: DEBUG output follows")
791 | print(output)
792 | state['status'] = 'Unknown'
793 | else:
794 | state['id'] = output.get('id', '')
795 | state['status'] = output.get('status', 'Unknown')
796 | state['message'] = output.get('message', '')
797 | return state
798 |
799 |
800 | def notarization_done(state, sleep_time, options):
801 | '''Evaluates whether notarization is still in progress'''
802 | if state['status'] == 'Accepted':
803 | display("Notarization successful. {}".format(state['message']), options.quiet)
804 | return True
805 | elif state['status'] in ['In Progress', 'Unknown']:
806 | display(
807 | "Notarization state: {}. Trying again in {} seconds".format(
808 | state['status'],
809 | sleep_time,
810 | ),
811 | options.quiet,
812 | )
813 | return False
814 | else:
815 | display(
816 | "Notarization unsuccessful:\n"
817 | "\tStatus(id={}): {}\n"
818 | "\tStatus Message: {}".format(
819 | state['id'], state['status'], state['message']
820 | ),
821 | options.quiet,
822 | )
823 | raise MunkiPkgError("Notarization failed")
824 |
825 |
826 | def wait_for_notarization(request_id, build_info, options):
827 | '''Checks notarization state until it is done or we exceed the timeout value'''
828 | display("Getting notarization state", options.quiet)
829 | timeout = build_info['notarization_info'].get('staple_timeout', STAPLE_TIMEOUT)
830 | counter = 0
831 | sleep_time = STAPLE_SLEEP
832 |
833 | while counter < timeout:
834 | time.sleep(sleep_time)
835 | counter += sleep_time
836 | sleep_time += STAPLE_SLEEP
837 |
838 | state = get_notarization_state(request_id, build_info, options)
839 |
840 | if notarization_done(state, sleep_time, options):
841 | return True
842 |
843 | print(
844 | "munkipkg: Timeout EXCEEDED when waiting for the notarization to complete. "
845 | "You can manually staple the package later if notarization is successful.",
846 | file=sys.stderr,
847 | )
848 | return False
849 |
850 |
851 | def staple(build_info, options):
852 | '''Use xcrun staple to add a staple to our package'''
853 | display("Stapling package", options.quiet)
854 | cmd = [
855 | XCRUN,
856 | 'stapler',
857 | 'staple',
858 | os.path.join(build_info['build_dir'], build_info['name']),
859 | ]
860 | retcode, proc_stdout, proc_stderr = run_subprocess(cmd)
861 |
862 | if retcode:
863 | print("stapler: FAILURE " + proc_stderr, file=sys.stderr)
864 | raise MunkiPkgError("Stapling failed")
865 | else:
866 | display("The staple and validate action worked!", options.quiet)
867 |
868 |
869 | def build(project_dir, options):
870 | '''Build our package'''
871 |
872 | build_info = {}
873 | try:
874 | try:
875 | build_info = get_build_info(project_dir, options)
876 | except MunkiPkgError as err:
877 | print(str(err), file=sys.stderr)
878 | exit(-1)
879 |
880 | if build_info['ownership'] in ['preserve', 'preserve-other']:
881 | if os.geteuid() != 0:
882 | print("\nWARNING: build-info ownership: %s might require "
883 | "using sudo to build this package.\n"
884 | % build_info['ownership'], file=sys.stderr)
885 |
886 | add_project_subdirs(build_info)
887 |
888 | build_info['component_plist'] = None
889 | # analyze root and turn off bundle relocation
890 | if build_info['payload'] and build_info['suppress_bundle_relocation']:
891 | build_info['component_plist'] = make_component_property_list(
892 | build_info, options)
893 |
894 | # make a stub PkgInfo file
895 | build_info['pkginfo_path'] = make_pkginfo(build_info, options)
896 |
897 | # remove any pre-existing pkg at the outputname path
898 | outputname = os.path.join(build_info['build_dir'], build_info['name'])
899 | if os.path.exists(outputname):
900 | retcode = subprocess.call(["/bin/rm", "-rf", outputname])
901 | if retcode:
902 | raise BuildError("Could not remove existing %s" % outputname)
903 |
904 | if build_info['scripts']:
905 | # remove .DS_Store file from the scripts folder
906 | if os.path.exists(os.path.join(build_info['scripts'], ".DS_Store")):
907 | display("Removing .DS_Store file from the scripts folder",
908 | options.quiet)
909 | os.remove(os.path.join(build_info['scripts'], ".DS_Store"))
910 |
911 | # make scripts executable
912 | for pkgscript in ("preinstall", "postinstall"):
913 | scriptpath = os.path.join(build_info['scripts'], pkgscript)
914 | if (os.path.exists(scriptpath) and
915 | (os.stat(scriptpath).st_mode & 0o500) != 0o500):
916 | display("Making %s script executable" % pkgscript,
917 | options.quiet)
918 | os.chmod(scriptpath, 0o755)
919 |
920 | # build the pkg
921 | build_pkg(build_info, options)
922 |
923 | # export bom info if requested
924 | if options.export_bom_info:
925 | export_bom_info(build_info, options)
926 |
927 | # convert pkg to distribution-style if requested
928 | if build_info['distribution_style']:
929 | build_distribution_pkg(build_info, options)
930 |
931 | # notarize the pkg
932 | if 'notarization_info' in build_info and not options.skip_notarization:
933 | try:
934 | request_id = upload_to_notary(build_info, options)
935 | if not options.skip_stapling and wait_for_notarization(
936 | request_id, build_info, options
937 | ):
938 | staple(build_info, options)
939 | except MunkiPkgError as err:
940 | print("ERROR: %s" % err, file=sys.stderr)
941 | return -1
942 |
943 | # cleanup temp dir
944 | _ = subprocess.call(["/bin/rm", "-rf", build_info['tmpdir']])
945 | return 0
946 |
947 | except BuildError as err:
948 | print('ERROR: %s' % err, file=sys.stderr)
949 | if build_info.get('tmpdir'):
950 | # cleanup temp dir
951 | _ = subprocess.call(["/bin/rm", "-rf", build_info['tmpdir']])
952 | return -1
953 |
954 |
955 | def get_pkginfo_attr(pkginfo_dom, attribute_name):
956 | """Returns value for attribute_name from PackageInfo dom"""
957 | pkgrefs = pkginfo_dom.getElementsByTagName('pkg-info')
958 | if pkgrefs:
959 | for ref in pkgrefs:
960 | keys = list(ref.attributes.keys())
961 | if attribute_name in keys:
962 | return ref.attributes[attribute_name].value
963 | return None
964 |
965 |
966 | def expand_payload(project_dir):
967 | '''expand Payload if present'''
968 | payload_file = os.path.join(project_dir, 'Payload')
969 | payload_archive = os.path.join(project_dir, 'Payload.cpio.gz')
970 | payload_dir = os.path.join(project_dir, 'payload')
971 | if os.path.exists(payload_file):
972 | try:
973 | os.rename(payload_file, payload_archive)
974 | os.mkdir(payload_dir)
975 | except OSError as err:
976 | raise PkgImportError(err)
977 | cmd = [DITTO, '-x', payload_archive, payload_dir]
978 | retcode = subprocess.call(cmd)
979 | if retcode:
980 | raise PkgImportError("Ditto failed to expand Payload")
981 | unlink_if_possible(payload_archive)
982 |
983 |
984 | def convert_packageinfo(pkg_path, project_dir, options):
985 | '''parse PackageInfo file and create build-info file'''
986 | package_info_file = os.path.join(project_dir, 'PackageInfo')
987 |
988 | pkginfo = minidom.parse(package_info_file)
989 | build_info = {}
990 |
991 | build_info['identifier'] = get_pkginfo_attr(pkginfo, 'identifier') or ''
992 | build_info['version'] = get_pkginfo_attr(pkginfo, 'version') or '1.0'
993 | build_info['install_location'] = get_pkginfo_attr(
994 | pkginfo, 'install-location') or '/'
995 | build_info['postinstall_action'] = get_pkginfo_attr(
996 | pkginfo, 'postinstall-action') or 'none'
997 | build_info['preserve_xattr'] = get_pkginfo_attr(
998 | pkginfo, 'preserve-xattr') == "true"
999 | build_info['name'] = os.path.basename(pkg_path)
1000 | if non_recommended_permissions_in_bom(project_dir):
1001 | build_info['ownership'] = 'preserve'
1002 |
1003 | distribution_file = os.path.join(project_dir, 'Distribution')
1004 | build_info['distribution_style'] = os.path.exists(distribution_file)
1005 |
1006 | write_build_info(build_info, project_dir, options)
1007 | unlink_if_possible(package_info_file)
1008 |
1009 |
1010 | def convert_info_plist(pkg_path, project_dir, options):
1011 | '''Read bundle pkg Info.plist and create build-info file'''
1012 | info_plist = os.path.join(pkg_path, 'Contents/Info.plist')
1013 | info = readPlist(info_plist)
1014 | build_info = {}
1015 |
1016 | build_info['identifier'] = info.get('CFBundleIdentifier', '')
1017 | build_info['version'] = (info.get('CFBundleShortVersionString') or
1018 | info.get('CFBundleVersion') or '1.0')
1019 | build_info['install_location'] = info.get('IFPkgFlagDefaultLocation') or '/'
1020 | build_info['postinstall_action'] = 'none'
1021 | if (info.get('IFPkgFlagRestartAction') in
1022 | ['RequiredRestart', 'RecommendedRestart']):
1023 | build_info['postinstall_action'] = 'restart'
1024 | if (info.get('IFPkgFlagRestartAction') in
1025 | ['RequiredLogout', 'RecommendedLogout']):
1026 | build_info['postinstall_action'] = 'logout'
1027 | build_info['name'] = os.path.basename(pkg_path)
1028 | if non_recommended_permissions_in_bom(project_dir):
1029 | build_info['ownership'] = 'preserve'
1030 | write_build_info(build_info, project_dir, options)
1031 |
1032 |
1033 | def handle_distribution_pkg(project_dir):
1034 | '''If the expanded pkg is a distribution pkg, handle this case'''
1035 | distribution_file = os.path.join(project_dir, 'Distribution')
1036 | if os.path.exists(distribution_file):
1037 | # we have a Distribution-style pkg here
1038 | # look for a _single_ *.pkg dir
1039 | pkgs_pattern = os.path.join(project_dir, '*.pkg')
1040 | pkgs = glob.glob(pkgs_pattern)
1041 | if len(pkgs) != 1:
1042 | raise PkgImportError(
1043 | "Distribution packages to be imported must contain exactly "
1044 | "one component package! Found: %s" % pkgs)
1045 | # move items of interest
1046 | pkg = pkgs[0]
1047 | for item in ['Bom', 'PackageInfo', 'Payload', 'Scripts']:
1048 | source_path = os.path.join(pkg, item)
1049 | if os.path.exists(source_path):
1050 | dest_path = os.path.join(project_dir, item)
1051 | try:
1052 | os.rename(source_path, dest_path)
1053 | except OSError as err:
1054 | raise PkgImportError(err)
1055 | try:
1056 | os.rmdir(pkg)
1057 | except OSError:
1058 | # we don't really care
1059 | pass
1060 |
1061 |
1062 | def script_names(kind='all'):
1063 | '''Return a list of known script names for bundle-style packages.
1064 | If kind is 'pre' or 'post', return just the pre or post names'''
1065 | pre_script_names = ['preflight', 'preinstall', 'preupgrade']
1066 | post_script_names = ['postflight', 'postinstall', 'postupgrade']
1067 | if kind == 'pre':
1068 | return pre_script_names
1069 | if kind == 'post':
1070 | return post_script_names
1071 | return pre_script_names + post_script_names
1072 |
1073 |
1074 | def copy_bundle_pkg_scripts(pkg_path, project_dir, options):
1075 | '''Copies scripts and other items to project scripts directory'''
1076 | # if any of the known script names are in the Resources folder,
1077 | # copy things that aren't *.lproj and package_version from
1078 | # Resources to project_dir/scripts
1079 |
1080 | resources_dir = os.path.join(pkg_path, 'Contents/Resources')
1081 | resources_items = os.listdir(resources_dir)
1082 |
1083 | # does resources_dir contain any of the known script_names?
1084 | if set(resources_items).intersection(set(script_names())):
1085 | scripts_dir = os.path.join(project_dir, 'scripts')
1086 | os.mkdir(scripts_dir)
1087 | for item in resources_items:
1088 | if item.endswith('.lproj') or item == "package_version":
1089 | # we don't need to copy these to scripts dir
1090 | continue
1091 | source_item = os.path.join(resources_dir, item)
1092 | dest_item = os.path.join(scripts_dir, item)
1093 | if os.path.isdir(source_item):
1094 | shutil.copytree(source_item, dest_item)
1095 | else:
1096 | shutil.copy2(source_item, dest_item)
1097 |
1098 | # rename pre- and post- scripts or print a warning
1099 | for kind in ['pre', 'post']:
1100 | found_scripts = [item for item in os.listdir(scripts_dir)
1101 | if item in script_names(kind)]
1102 | supported_name = kind + 'install'
1103 | if len(found_scripts) == 1 and found_scripts[0] != supported_name:
1104 | # rename it
1105 | current_script_path = os.path.join(
1106 | scripts_dir, found_scripts[0])
1107 | new_script_path = os.path.join(scripts_dir, supported_name)
1108 | display('Renaming %s script to %s'
1109 | % (found_scripts[0], supported_name), options.quiet)
1110 | os.rename(current_script_path, new_script_path)
1111 | elif len(found_scripts) > 1:
1112 | print("WARNING: Multiple %sXXXXXX scripts found. "
1113 | "Flat packages support only '%sinstall'." % (kind, kind),
1114 | file=sys.stderr)
1115 |
1116 |
1117 | def import_bundle_pkg(pkg_path, project_dir, options):
1118 | '''Imports a bundle-style package'''
1119 | try:
1120 | dist_files = glob.glob(os.path.join(pkg_path, 'Contents/*.dist'))
1121 | if dist_files:
1122 | raise PkgImportError(
1123 | "Bundle-style distribution packages are not supported for "
1124 | 'import. Consider importing the included sub-package(s).')
1125 |
1126 | # create the project dir
1127 | os.mkdir(project_dir)
1128 |
1129 | # export Bom.txt
1130 | bomfile = os.path.join(pkg_path, 'Contents/Archive.bom')
1131 | export_bom(bomfile, project_dir)
1132 |
1133 | # export Archive as payload
1134 | archive = os.path.join(pkg_path, 'Contents/Archive.pax.gz')
1135 | payload_dir = os.path.join(project_dir, 'payload')
1136 | try:
1137 | os.mkdir(payload_dir)
1138 | except OSError as err:
1139 | raise PkgImportError(err)
1140 | cmd = [DITTO, '-x', archive, payload_dir]
1141 | retcode = subprocess.call(cmd)
1142 | if retcode:
1143 | raise PkgImportError("Ditto failed to expand Payload")
1144 |
1145 | copy_bundle_pkg_scripts(pkg_path, project_dir, options)
1146 | convert_info_plist(pkg_path, project_dir, options)
1147 | if (non_recommended_permissions_in_bom(project_dir) and
1148 | os.geteuid() != 0):
1149 | print(
1150 | '\nWARNING: package contains non-default owner/group on some '
1151 | 'files. build-info ownership has been set to "preserve". '
1152 | '\nCheck the bom for accuracy.'
1153 | '\nRun munkipkg --sync with sudo to apply the correct owner '
1154 | 'and group to payload files.\n', file=sys.stderr
1155 | )
1156 | sync_from_bom_info(project_dir, options)
1157 | create_default_gitignore(project_dir)
1158 | display("Created new package project at %s"
1159 | % project_dir, options.quiet)
1160 | return 0
1161 | except (MunkiPkgError, OSError) as err:
1162 | print('ERROR: %s' % err, file=sys.stderr)
1163 | return -1
1164 |
1165 |
1166 | def import_flat_pkg(pkg_path, project_dir, options):
1167 | '''Imports a flat package'''
1168 | try:
1169 | # expand flat pkg into project dir
1170 | cmd = [PKGUTIL, '--expand', pkg_path, project_dir]
1171 | retcode = subprocess.call(cmd)
1172 | if retcode:
1173 | raise PkgImportError("Could not expand package.")
1174 |
1175 | handle_distribution_pkg(project_dir)
1176 |
1177 | # export Bom.txt
1178 | bomfile = os.path.join(project_dir, 'Bom')
1179 | export_bom(bomfile, project_dir)
1180 | unlink_if_possible(bomfile)
1181 |
1182 | # rename Scripts directory
1183 | uppercase_scripts_dir = os.path.join(project_dir, 'Scripts')
1184 | lowercase_scripts_dir = os.path.join(project_dir, 'scripts')
1185 | if os.path.exists(uppercase_scripts_dir):
1186 | os.rename(uppercase_scripts_dir, lowercase_scripts_dir)
1187 |
1188 | expand_payload(project_dir)
1189 | convert_packageinfo(pkg_path, project_dir, options)
1190 | if (non_recommended_permissions_in_bom(project_dir) and
1191 | os.geteuid() != 0):
1192 | print(
1193 | '\nWARNING: package contains non-default owner/group on some '
1194 | 'files. build-info ownership has been set to "preserve". '
1195 | '\nCheck the bom for accuracy.'
1196 | '\nRun munkipkg --sync with sudo to apply the correct owner '
1197 | 'and group to payload files.\n', file=sys.stderr
1198 | )
1199 | sync_from_bom_info(project_dir, options)
1200 | create_default_gitignore(project_dir)
1201 | display("Created new package project at %s"
1202 | % project_dir, options.quiet)
1203 | return 0
1204 | except (MunkiPkgError, OSError) as err:
1205 | print('ERROR: %s' % err, file=sys.stderr)
1206 | return -1
1207 |
1208 |
1209 | def import_pkg(pkg_path, project_dir, options):
1210 | '''Imports an existing pkg into a project directory. Returns a
1211 | boolean to indicate success or failure.'''
1212 | if os.path.exists(project_dir):
1213 | print("ERROR: Directory %s already exists." % project_dir,
1214 | file=sys.stderr)
1215 | return False
1216 |
1217 | if os.path.isdir(pkg_path):
1218 | return import_bundle_pkg(pkg_path, project_dir, options)
1219 | return import_flat_pkg(pkg_path, project_dir, options)
1220 |
1221 |
1222 | def valid_project_dir(project_dir):
1223 | '''validate project dir. Returns a boolean'''
1224 | if not os.path.exists(project_dir):
1225 | print(("ERROR: %s: Project not found." % project_dir), file=sys.stderr)
1226 | return False
1227 | elif not os.path.isdir(project_dir):
1228 | print(("ERROR: %s is not a directory." % project_dir), file=sys.stderr)
1229 | return False
1230 | return True
1231 |
1232 |
1233 | def main():
1234 | '''Main'''
1235 | usage = """usage: %prog [options] pkg_project_directory
1236 | A tool for building a package from the contents of a
1237 | pkg_project_directory."""
1238 | parser = optparse.OptionParser(usage=usage, version=VERSION)
1239 | parser.add_option('--create', action='store_true',
1240 | help='Creates a new empty project with default settings '
1241 | 'at given path.')
1242 | parser.add_option('--import', dest='import_pkg', metavar='PKG',
1243 | help='Imports an existing package PKG as a package '
1244 | 'project, creating pkg_project_directory.')
1245 | parser.add_option('--json', action='store_true',
1246 | help='Create build-info file in JSON format. '
1247 | 'Useful only with --create and --import options.')
1248 | parser.add_option('--yaml', action='store_true',
1249 | help='Create build-info file in YAML format. '
1250 | 'Useful only with --create and --import options.')
1251 | parser.add_option('--export-bom-info', action='store_true',
1252 | help='Extracts the Bill-Of-Materials file from the '
1253 | 'output package and exports it as Bom.txt under the '
1254 | 'pkg_project_folder. Useful for tracking owner, '
1255 | 'group and mode of the payload in git.')
1256 | parser.add_option('--sync', action='store_true',
1257 | help='Use Bom.txt to set modes of files in payload '
1258 | 'directory and create missing empty directories. '
1259 | 'Useful after a git clone or pull. No build is '
1260 | 'performed.')
1261 | parser.add_option('--quiet', action='store_true',
1262 | help='Inhibits status messages on stdout. '
1263 | 'Any error messages are still sent to stderr.')
1264 | parser.add_option('-f', '--force', action='store_true',
1265 | help='Forces creation of project directory if it already '
1266 | 'exists. ')
1267 | parser.add_option('--skip-notarization', action='store_true',
1268 | help='Skips whole notarization process when '
1269 | 'notarization is specified in build-info')
1270 | parser.add_option('--skip-stapling', action='store_true',
1271 | help='Skips only stapling part of notarization process '
1272 | 'when notarization is specified in build-info')
1273 | options, arguments = parser.parse_args()
1274 |
1275 | if not arguments:
1276 | parser.print_usage()
1277 | sys.exit(0)
1278 |
1279 | if len(arguments) > 1:
1280 | print("ERROR: Only a single package project can be built at a time!",
1281 | file=sys.stderr)
1282 | sys.exit(-1)
1283 |
1284 | if options.json and options.yaml:
1285 | print("ERROR: Only a single build-info file can be built at a time!",
1286 | file=sys.stderr)
1287 | sys.exit(-1)
1288 |
1289 | if options.yaml and not YAML_INSTALLED:
1290 | print(
1291 | "ERROR: PyYAML missing. Please run 'sudo easy_install pip' "
1292 | "followed by 'sudo pip install -r requirements.txt'",
1293 | file=sys.stderr
1294 | )
1295 | sys.exit(-1)
1296 |
1297 | if options.create:
1298 | result = create_template_project(arguments[0], options)
1299 | sys.exit(result)
1300 |
1301 | if options.import_pkg:
1302 | result = import_pkg(options.import_pkg, arguments[0], options)
1303 | sys.exit(result)
1304 |
1305 | # options past here require a valid project_dir
1306 | if not valid_project_dir(arguments[0]):
1307 | sys.exit(-1)
1308 |
1309 | if options.sync:
1310 | result = sync_from_bom_info(arguments[0], options)
1311 | else:
1312 | result = build(arguments[0], options)
1313 | sys.exit(result)
1314 |
1315 | if __name__ == '__main__':
1316 | main()
1317 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | PyYAML>=3.11
--------------------------------------------------------------------------------