mirror of
https://github.com/munki/munki.git
synced 2026-03-12 20:48:30 -05:00
Using the new '--apple-update' option available in makepkginfo (and therefore munkiimport), an admin can generate metadata pkginfo files for Apple updates. Supported metadata keys will modify munki's behavior of the specified Apple update offered to the client. Examples would include 'force_install_after_date' and 'RestartAction'. The 'makecatalogs' utility has also been updated to allow for this new installer_type when creating catalogs. NOTE: Client-side implementation is not included in this commit. This commit only deals with the creation of these specialized pkginfo files.
954 lines
39 KiB
Python
Executable File
954 lines
39 KiB
Python
Executable File
#!/usr/bin/python
|
|
# encoding: utf-8
|
|
#
|
|
# Copyright 2008-2013 Greg Neagle.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""
|
|
makepkginfo
|
|
|
|
Created by Greg Neagle on 2008-11-25.
|
|
Creates a managed install pkg info plist given an Installer item:
|
|
a .pkg, a .mpkg, or a .dmg containing a .pkg or .mpkg
|
|
at the root of the mounted disk image.
|
|
|
|
You may also pass additional items that are installed by the package. These
|
|
are added to the 'installs' key of the catalog item plist and are used when
|
|
processing the catalog to check if the package needs to be installed or
|
|
reinstalled.
|
|
|
|
The generated plist is printed to STDOUT.
|
|
|
|
Usage: makepkginfo /path/to/package_or_dmg [-f /path/to/item/it/installs ...]
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import re
|
|
import optparse
|
|
from optparse import OptionValueError
|
|
|
|
from munkilib import munkicommon
|
|
from munkilib import FoundationPlist
|
|
from munkilib import adobeutils
|
|
|
|
# circumvent cfprefsd plist scanning
|
|
os.environ['__CFPREFERENCES_AVOID_DAEMON'] = "1"
|
|
|
|
|
|
def getCatalogInfoFromDmg(dmgpath, options):
|
|
"""
|
|
* Mounts a disk image
|
|
* Gets catalog info for the first installer item found at the root level.
|
|
* Unmounts the disk image
|
|
|
|
To-do: handle multiple installer items on a disk image(?)
|
|
"""
|
|
cataloginfo = None
|
|
mountpoints = munkicommon.mountdmg(dmgpath)
|
|
if not mountpoints:
|
|
print >> sys.stderr, "Could not mount %s!" % dmgpath
|
|
exit(-1)
|
|
|
|
if options.pkgname:
|
|
pkgpath = os.path.join(mountpoints[0], options.pkgname)
|
|
if os.path.exists(pkgpath):
|
|
cataloginfo = munkicommon.getPackageMetaData(pkgpath)
|
|
if options.installer_choices_xml:
|
|
installer_choices_xml = munkicommon.getChoiceChangesXML(pkgpath)
|
|
if installer_choices_xml:
|
|
cataloginfo['installer_choices_xml'] = installer_choices_xml
|
|
if cataloginfo:
|
|
cataloginfo['package_path'] = options.pkgname
|
|
elif not options.item:
|
|
# search for first package at root
|
|
for fsitem in munkicommon.listdir(mountpoints[0]):
|
|
itempath = os.path.join(mountpoints[0], fsitem)
|
|
if munkicommon.hasValidInstallerItemExt(itempath):
|
|
cataloginfo = munkicommon.getPackageMetaData(itempath)
|
|
if options.installer_choices_xml:
|
|
installer_choices_xml = munkicommon.getChoiceChangesXML(
|
|
itempath)
|
|
if installer_choices_xml:
|
|
cataloginfo['installer_choices_xml'] = \
|
|
installer_choices_xml
|
|
# get out of fsitem loop
|
|
break
|
|
|
|
if cataloginfo:
|
|
# we found a package, but let's see if it's an Adobe CS5 install
|
|
# (AAMEE) package
|
|
if 'receipts' in cataloginfo:
|
|
try:
|
|
pkgid = cataloginfo['receipts'][0].get('packageid')
|
|
except IndexError:
|
|
pkgid = ""
|
|
if pkgid.startswith("com.adobe.Enterprise.install"):
|
|
# we have an Adobe CS5 install package, process
|
|
# as Adobe install
|
|
adobepkgname = cataloginfo['receipts'][0].get('filename')
|
|
cataloginfo = adobeutils.getAdobeCatalogInfo(
|
|
mountpoints[0], adobepkgname)
|
|
|
|
else:
|
|
# maybe an Adobe installer/updater/patcher?
|
|
cataloginfo = adobeutils.getAdobeCatalogInfo(mountpoints[0],
|
|
options.pkgname or '')
|
|
|
|
if not cataloginfo:
|
|
# maybe this is a drag-n-drop dmg
|
|
# look for given item or an app at the top level of the dmg
|
|
iteminfo = {}
|
|
if options.item:
|
|
item = options.item
|
|
itempath = os.path.join(mountpoints[0], item)
|
|
if os.path.exists(itempath):
|
|
iteminfo = getiteminfo(itempath)
|
|
else:
|
|
print >> sys.stderr, \
|
|
"%s not found on disk image." % item
|
|
else:
|
|
# no item specified; look for an application at root of
|
|
# mounted dmg
|
|
item = ''
|
|
for itemname in munkicommon.listdir(mountpoints[0]):
|
|
itempath = os.path.join(mountpoints[0], itemname)
|
|
if munkicommon.isApplication(itempath):
|
|
item = itemname
|
|
iteminfo = getiteminfo(itempath)
|
|
if iteminfo:
|
|
break
|
|
|
|
if iteminfo:
|
|
item_to_copy = {}
|
|
if options.destitemname:
|
|
# An alternate 'destination_item' name has been specified
|
|
dest_item = options.destitemname
|
|
item_to_copy['destination_item'] = options.destitemname
|
|
else:
|
|
dest_item = item
|
|
if options.destinationpath:
|
|
iteminfo['path'] = os.path.join(options.destinationpath,
|
|
dest_item)
|
|
else:
|
|
iteminfo['path'] = os.path.join("/Applications", dest_item)
|
|
cataloginfo = {}
|
|
cataloginfo['name'] = iteminfo.get('CFBundleName',
|
|
os.path.splitext(item)[0])
|
|
cataloginfo['version'] = \
|
|
iteminfo.get('CFBundleShortVersionString', "0")
|
|
cataloginfo['installs'] = [iteminfo]
|
|
cataloginfo['installer_type'] = "copy_from_dmg"
|
|
item_to_copy['source_item'] = item
|
|
item_to_copy['destination_path'] = \
|
|
options.destinationpath or "/Applications"
|
|
if options.user:
|
|
item_to_copy['user'] = options.user
|
|
if options.group:
|
|
item_to_copy['group'] = options.group
|
|
if options.mode:
|
|
item_to_copy['mode'] = options.mode
|
|
cataloginfo['items_to_copy'] = [item_to_copy]
|
|
cataloginfo['uninstallable'] = True
|
|
cataloginfo['uninstall_method'] = "remove_copied_items"
|
|
|
|
#eject the dmg
|
|
munkicommon.unmountdmg(mountpoints[0])
|
|
return cataloginfo
|
|
|
|
|
|
def getBundleInfo(path):
|
|
"""
|
|
Returns Info.plist data if available
|
|
for bundle at path
|
|
"""
|
|
infopath = os.path.join(path, "Contents", "Info.plist")
|
|
if not os.path.exists(infopath):
|
|
infopath = os.path.join(path, "Resources", "Info.plist")
|
|
|
|
if os.path.exists(infopath):
|
|
try:
|
|
plist = FoundationPlist.readPlist(infopath)
|
|
return plist
|
|
except FoundationPlist.NSPropertyListSerializationException:
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
def readfile(path):
|
|
'''Reads file at path. Returns a string.'''
|
|
try:
|
|
fileobject = open(os.path.expanduser(path), mode='r', buffering=1)
|
|
data = fileobject.read()
|
|
fileobject.close()
|
|
return data
|
|
except (OSError, IOError):
|
|
print >> sys.stderr, "Couldn't read %s" % path
|
|
return ""
|
|
|
|
|
|
def readFileOrString(option_value):
|
|
"""
|
|
If option_value is a path to a file,
|
|
return contents of file.
|
|
|
|
Otherwise, return the string.
|
|
"""
|
|
if os.path.exists(os.path.expanduser(option_value)):
|
|
string = readfile(option_value)
|
|
else:
|
|
string = option_value
|
|
|
|
return string
|
|
|
|
|
|
def getiteminfo(itempath):
|
|
"""
|
|
Gets info for filesystem items passed to makecatalog item, to be used for
|
|
the "installs" key.
|
|
Determines if the item is an application, bundle, Info.plist, or a file or
|
|
directory and gets additional metadata for later comparison.
|
|
"""
|
|
infodict = {}
|
|
if munkicommon.isApplication(itempath):
|
|
infodict['type'] = 'application'
|
|
infodict['path'] = itempath
|
|
plist = getBundleInfo(itempath)
|
|
for key in ['CFBundleName', 'CFBundleIdentifier',
|
|
'CFBundleShortVersionString', 'CFBundleVersion']:
|
|
if key in plist:
|
|
infodict[key] = plist[key]
|
|
if 'LSMinimumSystemVersion' in plist:
|
|
infodict['minosversion'] = plist['LSMinimumSystemVersion']
|
|
elif 'SystemVersionCheck:MinimumSystemVersion' in plist:
|
|
infodict['minosversion'] = \
|
|
plist['SystemVersionCheck:MinimumSystemVersion']
|
|
|
|
elif os.path.exists(os.path.join(itempath, 'Contents', 'Info.plist')) or \
|
|
os.path.exists(os.path.join(itempath, 'Resources', 'Info.plist')):
|
|
infodict['type'] = 'bundle'
|
|
infodict['path'] = itempath
|
|
plist = getBundleInfo(itempath)
|
|
for key in ['CFBundleShortVersionString', 'CFBundleVersion']:
|
|
if key in plist:
|
|
infodict[key] = plist[key]
|
|
|
|
elif itempath.endswith("Info.plist") or \
|
|
itempath.endswith("version.plist"):
|
|
infodict['type'] = 'plist'
|
|
infodict['path'] = itempath
|
|
try:
|
|
plist = FoundationPlist.readPlist(itempath)
|
|
for key in ['CFBundleShortVersionString', 'CFBundleVersion']:
|
|
if key in plist:
|
|
infodict[key] = plist[key]
|
|
except FoundationPlist.NSPropertyListSerializationException:
|
|
pass
|
|
|
|
# let's help the admin -- if CFBundleShortVersionString is empty
|
|
# or doesn't start with a digit, and CFBundleVersion is there
|
|
# use CFBundleVersion as the version_comparison_key
|
|
if (not infodict.get('CFBundleShortVersionString') or
|
|
infodict['CFBundleShortVersionString'][0]
|
|
not in '0123456789'):
|
|
if infodict.get('CFBundleVersion'):
|
|
infodict['version_comparison_key'] = 'CFBundleVersion'
|
|
elif 'CFBundleShortVersionString' in infodict:
|
|
infodict['version_comparison_key'] = 'CFBundleShortVersionString'
|
|
|
|
if not 'CFBundleShortVersionString' in infodict and \
|
|
not 'CFBundleVersion' in infodict:
|
|
infodict['type'] = 'file'
|
|
infodict['path'] = itempath
|
|
if os.path.isfile(itempath):
|
|
infodict['md5checksum'] = munkicommon.getmd5hash(itempath)
|
|
return infodict
|
|
|
|
|
|
def check_mode(option, opt, value, parser):
|
|
'''Callback to check --mode options'''
|
|
modes = value.lower().replace(',', ' ').split()
|
|
value = None
|
|
rex = re.compile("[augo]+[=+-][rstwxXugo]+")
|
|
for mode in modes:
|
|
if rex.match(mode):
|
|
value = mode if not value else (value + "," + mode)
|
|
else:
|
|
raise OptionValueError("option %s: invalid mode: %s" %
|
|
(opt, mode))
|
|
setattr(parser.values, option.dest, value)
|
|
|
|
|
|
def main():
|
|
'''Main routine'''
|
|
usage = """usage: %prog [options] [/path/to/installeritem]
|
|
%prog --help for more information."""
|
|
p = optparse.OptionParser(usage=usage)
|
|
p.add_option(
|
|
'--verify-options-only',
|
|
action="store_true",
|
|
help=optparse.SUPPRESS_HELP
|
|
)
|
|
p.add_option(
|
|
'--version', '-V',
|
|
action='store_true',
|
|
help='Print the version of the munki tools and exit.'
|
|
)
|
|
|
|
# Default override options
|
|
default_override_options = optparse.OptionGroup(
|
|
p, 'Default Override Options',
|
|
('Options specified will override information automatically derived '
|
|
'from the package.'))
|
|
default_override_options.add_option(
|
|
'--name',
|
|
metavar='NAME',
|
|
help='Name of the package.'
|
|
)
|
|
default_override_options.add_option(
|
|
'--displayname',
|
|
metavar='DISPLAY_NAME',
|
|
help='Display name of the package.'
|
|
)
|
|
default_override_options.add_option(
|
|
'--description',
|
|
metavar='STRING|PATH',
|
|
help=('Description of the package. '
|
|
'Can be a PATH to a file (plain text or html).')
|
|
)
|
|
default_override_options.add_option(
|
|
'--pkgvers',
|
|
metavar='PACKAGE_VERSION',
|
|
help='Version of the package.'
|
|
)
|
|
default_override_options.add_option(
|
|
'--RestartAction',
|
|
metavar='ACTION',
|
|
help=('Specify a \'RestartAction\' for the package. '
|
|
'Supported actions: RequireRestart, RequireLogout, or '
|
|
'RecommendRestart')
|
|
)
|
|
default_override_options.add_option(
|
|
'--uninstall_method', '--uninstall-method',
|
|
metavar='METHOD|PATH',
|
|
help=('Specify an \'uninstall_method\' for the package. '
|
|
'Default method depends on the package type: i.e. '
|
|
'drag-n-drop, Apple package, or an embedded uninstall script. '
|
|
'Can be a path to a script on the client computer.')
|
|
)
|
|
p.add_option_group(default_override_options)
|
|
|
|
# Script options
|
|
script_options = optparse.OptionGroup(
|
|
p, 'Script Options',
|
|
'All scripts are read and embedded into the pkginfo.')
|
|
script_options.add_option(
|
|
'--installcheck_script', '--installcheck-script',
|
|
metavar='SCRIPT_PATH',
|
|
help=('Path to an optional installcheck script to be '
|
|
'run to determine if item should be installed. '
|
|
'An exit code of 0 indicates installation should occur. '
|
|
'Takes precendence over installs items and receipts.')
|
|
)
|
|
script_options.add_option(
|
|
'--uninstallcheck_script', '--uninstallcheck-script',
|
|
metavar='SCRIPT_PATH',
|
|
help=('Path to an optional uninstallcheck script to be '
|
|
'run to determine if item should be uninstalled. '
|
|
'An exit code of 0 indicates uninstallation should occur. '
|
|
'Takes precendence over installs items and receipts.')
|
|
)
|
|
script_options.add_option(
|
|
'--preinstall_script', '--preinstall-script',
|
|
metavar='SCRIPT_PATH',
|
|
help=('Path to an optional preinstall script to be '
|
|
'run before installation of the item.')
|
|
)
|
|
script_options.add_option(
|
|
'--postinstall_script', '--postinstall-script',
|
|
metavar='SCRIPT_PATH',
|
|
help=('Path to an optional postinstall script to be '
|
|
'run after installation of the item.')
|
|
)
|
|
script_options.add_option(
|
|
'--preuninstall_script', '--preuninstall-script',
|
|
metavar='SCRIPT_PATH',
|
|
help=('Path to an optional preuninstall script to be run '
|
|
'before removal of the item.')
|
|
)
|
|
script_options.add_option(
|
|
'--postuninstall_script', '--postuninstall-script',
|
|
metavar='SCRIPT_PATH',
|
|
help=('Path to an optional postuninstall script to be run '
|
|
'after removal of the item.')
|
|
)
|
|
script_options.add_option(
|
|
'--uninstall_script', '--uninstall-script',
|
|
metavar='SCRIPT_PATH',
|
|
help=('Path to an uninstall script to be run in order '
|
|
'to uninstall this item.')
|
|
)
|
|
p.add_option_group(script_options)
|
|
|
|
# Drag-n-Drop options
|
|
dragdrop_options = optparse.OptionGroup(
|
|
p, 'Drag-n-Drop Options',
|
|
('These options apply to installer items that are "drag-n-drop" '
|
|
'disk images.')
|
|
)
|
|
dragdrop_options.add_option(
|
|
'--itemname', '-i', '--appname', '-a',
|
|
metavar='ITEM',
|
|
dest='item',
|
|
help=('Name or relative path of the item to be installed. '
|
|
'Useful if there is more than one item at the root of the dmg.')
|
|
)
|
|
dragdrop_options.add_option(
|
|
'--destinationpath', '-d',
|
|
metavar='PATH',
|
|
help=('Path to which the item should be copied. Defaults to '
|
|
'"/Applications".')
|
|
)
|
|
dragdrop_options.add_option(
|
|
'--destinationitemname', '--destinationitem',
|
|
metavar='NAME',
|
|
dest='destitemname',
|
|
help=('Alternate name for which the item should be copied as. '
|
|
'Specifying this option also alters the corresponding '
|
|
'"installs" item\'s path with the provided name.')
|
|
)
|
|
dragdrop_options.add_option(
|
|
'-o', '--owner',
|
|
metavar='USER',
|
|
dest='user',
|
|
help=('Sets the owner of the copied item. '
|
|
'The owner may be either a UID or a symbolic name. '
|
|
'The owner will be set recursively on the item.')
|
|
)
|
|
dragdrop_options.add_option(
|
|
'-g', '--group',
|
|
metavar='GROUP',
|
|
dest='group',
|
|
help=('Sets the group of the copied item. '
|
|
'The group may be either a GID or a symbolic name. '
|
|
'The group will be set recursively on the item.')
|
|
)
|
|
dragdrop_options.add_option(
|
|
'-m', '--mode',
|
|
metavar='MODE',
|
|
dest='mode',
|
|
action='callback',
|
|
type='string',
|
|
callback=check_mode,
|
|
help=('Sets the mode of the copied item. '
|
|
'The specified mode must be in symbolic form. '
|
|
'See the manpage for chmod(1) for more information. '
|
|
'The mode is applied recursively.')
|
|
)
|
|
p.add_option_group(dragdrop_options)
|
|
|
|
# Apple package specific options
|
|
apple_options = optparse.OptionGroup(p, 'Apple Package Options')
|
|
apple_options.add_option(
|
|
'--pkgname', '-p',
|
|
help=('If the installer item is a disk image containing multiple '
|
|
'packages, or the package to be installed is not at the root '
|
|
'of the mounted disk image, PKGNAME is a relative path from '
|
|
'the root of the mounted disk image to the specific package to '
|
|
'be installed.'
|
|
'If the installer item is a disk image containing an Adobe '
|
|
'CS4 Deployment Toolkit installation, PKGNAME is the name of '
|
|
'an Adobe CS4 Deployment Toolkit installer package folder at '
|
|
'the top level of the mounted dmg.'
|
|
'If this flag is missing, the AdobeUber* files should be at '
|
|
'the top level of the mounted dmg.')
|
|
)
|
|
apple_options.add_option(
|
|
'--installer_choices_xml', '--installer-choices-xml',
|
|
action='store_true',
|
|
help=('Generate installer choices for metapackages. '
|
|
'Note: Requires Mac OS X 10.6.6 or later.')
|
|
)
|
|
apple_options.add_option(
|
|
'--installer_environment', '--installer-environment', '-E',
|
|
action="append",
|
|
metavar='KEY=VALUE',
|
|
help=('Specifies key/value pairs to set environment variables for use '
|
|
'by /usr/sbin/installer. A key/value pair of '
|
|
'USER=CURRENT_CONSOLE_USER indicates that USER be set to the '
|
|
'GUI user, otherwise root. Can be specified multiple times.')
|
|
)
|
|
p.add_option_group(apple_options)
|
|
|
|
# Adobe package specific options
|
|
adobe_options = optparse.OptionGroup(p, 'Adobe-specific Options')
|
|
adobe_options.add_option(
|
|
'--uninstallerdmg', '-U',
|
|
help=('If the installer item is a disk image containing an Adobe CS4 '
|
|
'Deployment Toolkit installation package or Adobe CS3 deployment '
|
|
'package, UNINSTALLERDMG is a path to a disk image containing an '
|
|
'AdobeUberUninstaller for this item.')
|
|
)
|
|
p.add_option_group(adobe_options)
|
|
|
|
# Forced/Unattended (install) options
|
|
forced_unattended_options = optparse.OptionGroup(
|
|
p, 'Forced/Unattended Options')
|
|
forced_unattended_options.add_option(
|
|
'--unattended_install', '--unattended-install',
|
|
action='store_true',
|
|
help='Item can be installed without notifying the user.')
|
|
forced_unattended_options.add_option(
|
|
'--unattended_uninstall', '--unattended-uninstall',
|
|
action='store_true',
|
|
help='Item can be uninstalled without notifiying the user.')
|
|
forced_unattended_options.add_option(
|
|
'--force_install_after_date', '--force-install-after-date',
|
|
metavar='DATE',
|
|
help=('Specify a date, in local time, after which the package will '
|
|
'be forcefully installed. DATE format: yyyy-mm-ddThh:mm:ssZ '
|
|
'Example: \'2011-08-11T12:55:00Z\' equates to 11 August 2011 '
|
|
'at 12:55 PM local time.')
|
|
)
|
|
p.add_option_group(forced_unattended_options)
|
|
|
|
# 'installs' generation options
|
|
# (by itself since no installer_item needs to be specified)
|
|
gen_installs_options = optparse.OptionGroup(
|
|
p, 'Generating \'installs\' items')
|
|
gen_installs_options.add_option(
|
|
'--file', '-f',
|
|
action="append",
|
|
metavar='PATH',
|
|
help=('Path to a filesystem item installed by this package, typically '
|
|
'an application. This generates an "installs" item for the '
|
|
'pkginfo, to be used to determine if this software has been '
|
|
'installed. Can be specified multiple times.')
|
|
)
|
|
p.add_option_group(gen_installs_options)
|
|
|
|
# Apple update metadata pkg options
|
|
# (by itself since no installer_item needs to be specified)
|
|
apple_update_metdata_options = optparse.OptionGroup(
|
|
p, 'Generating Apple update metadata items')
|
|
apple_update_metdata_options.add_option(
|
|
'--apple_update', '--apple-update',
|
|
metavar='PRODUCTKEY',
|
|
help=('Specify an Apple update \'productKey\' used to manipulate '
|
|
'the behavior of a pending Apple software update. '
|
|
'For example, a \'force_install_after_date\' key could be added '
|
|
'as opposed to importing the update into the munki repo.')
|
|
)
|
|
p.add_option_group(apple_update_metdata_options)
|
|
|
|
# Additional options - misc. options that don't fit into other categories,
|
|
# and don't necessarily warrant the creation of their own option group
|
|
additional_options = optparse.OptionGroup(p, 'Additional Options')
|
|
additional_options.add_option(
|
|
'--autoremove',
|
|
action='store_true',
|
|
help=('Indicates this package should be automatically removed if it is '
|
|
'not listed in any applicable \'managed_installs\'.')
|
|
)
|
|
additional_options.add_option(
|
|
'--minimum_munki_version', '--minimum-munki-version',
|
|
metavar='VERSION',
|
|
help=('Minimum version of munki required to perform installation. '
|
|
'Uses format produced by \'--version\' query from any munki '
|
|
'utility.')
|
|
)
|
|
additional_options.add_option(
|
|
'--minimum_os_version', '--minimum-os-version', '--min-os-ver',
|
|
metavar='VERSION',
|
|
help='Minimum OS version for the installer item.'
|
|
)
|
|
additional_options.add_option(
|
|
'--maximum_os_version', '--maximum-os-version', '--max-os-ver',
|
|
metavar='VERSION',
|
|
help='Maximum OS version for the installer item.'
|
|
)
|
|
additional_options.add_option(
|
|
'--update_for', '--update-for', '-u',
|
|
action="append",
|
|
metavar='PKG_NAME',
|
|
help=('Specifies a package for which the current package is an update. '
|
|
'Can be specified multiple times to build an array of packages.')
|
|
)
|
|
additional_options.add_option(
|
|
'--requires', '-r',
|
|
action="append",
|
|
metavar='PKG_NAME',
|
|
help=('Specifies a package required by the current package. Can be '
|
|
'specified multiple times to build an array of required '
|
|
'packages.')
|
|
)
|
|
additional_options.add_option(
|
|
'--blocking_application', '--blocking-application', '-b',
|
|
action="append",
|
|
metavar='APP_NAME',
|
|
help=('Specifies an application that blocks installation. Can be '
|
|
'specified multiple times to build an array of blocking '
|
|
'applications.')
|
|
)
|
|
additional_options.add_option(
|
|
'--catalog', '-c',
|
|
action="append",
|
|
metavar='CATALOG_NAME',
|
|
help=('Specifies in which catalog the item should appear. The default '
|
|
'is \'testing\'. Can be specified multiple times to add the item '
|
|
'to multiple catalogs.')
|
|
)
|
|
additional_options.add_option(
|
|
'--notes',
|
|
metavar='STRING|PATH',
|
|
help=('Specifies administrator provided notes to be embedded into the '
|
|
'pkginfo. Can be a PATH to a file.')
|
|
)
|
|
p.add_option_group(additional_options)
|
|
|
|
options, arguments = p.parse_args()
|
|
|
|
if options.version:
|
|
print munkicommon.get_version()
|
|
exit(0)
|
|
|
|
if options.verify_options_only:
|
|
if len(arguments) == 0 and not options.apple_update:
|
|
print >> sys.stderr, 'makepkginfo options FAILED verification'
|
|
print >> sys.stderr, 'No installer item was provided.'
|
|
exit(1)
|
|
if len(arguments) > 1:
|
|
print >> sys.stderr, 'makepkginfo options FAILED verification'
|
|
print >> sys.stderr, \
|
|
'Can process only one installer item at a time.'
|
|
exit(3)
|
|
if not options.apple_update:
|
|
print >> sys.stdout, arguments[0]
|
|
exit(0)
|
|
|
|
if (len(arguments) == 0
|
|
and not options.file
|
|
and not options.installer_environment
|
|
and not options.installcheck_script
|
|
and not options.uninstallcheck_script
|
|
and not options.preinstall_script
|
|
and not options.postinstall_script
|
|
and not options.preuninstall_script
|
|
and not options.postuninstall_script
|
|
and not options.uninstall_script
|
|
and not options.apple_update):
|
|
p.print_usage()
|
|
exit(-1)
|
|
|
|
if options.minimum_os_version and \
|
|
not options.minimum_os_version[0].isdigit():
|
|
print >> sys.stderr, \
|
|
'Minimum OS Version must start with a number, e.g. 10.7.2.'
|
|
exit(-1)
|
|
|
|
if len(arguments) > 1:
|
|
print >> sys.stderr, 'Can process only one installer item at a time.'
|
|
print >> sys.stderr, 'Ignoring additional installer items:'
|
|
print >> sys.stderr, '\t', '\n\t'.join(arguments[1:])
|
|
|
|
os_version = munkicommon.getOsVersion(
|
|
only_major_minor=False, as_tuple=True)
|
|
if options.installer_choices_xml:
|
|
if os_version < (10, 6, 6):
|
|
options.installer_choices_xml = False
|
|
|
|
catinfo = {}
|
|
installs = []
|
|
if arguments:
|
|
item = arguments[0].rstrip("/")
|
|
if item and os.path.exists(item):
|
|
# get size of installer item
|
|
itemsize = 0
|
|
itemhash = "N/A"
|
|
if os.path.isfile(item):
|
|
itemsize = int(os.path.getsize(item))
|
|
itemhash = munkicommon.getsha256hash(item)
|
|
|
|
if munkicommon.hasValidDiskImageExt(item):
|
|
if munkicommon.DMGisWritable(item):
|
|
print >> sys.stderr, ("WARNING: %s is a writable disk "
|
|
"image. Checksum verification is not supported."
|
|
% item)
|
|
print >> sys.stderr, ("WARNING: Consider converting "
|
|
"%s to a read-only disk image."
|
|
% item)
|
|
itemhash = "N/A"
|
|
catinfo = getCatalogInfoFromDmg(item, options)
|
|
if (catinfo and
|
|
catinfo.get('installer_type') == "AdobeCS5Installer"):
|
|
print >> sys.stderr, (
|
|
"This disk image appears to contain an Adobe CS5 "
|
|
"product install.\n"
|
|
"Please use Adobe Application Manager, Enterprise "
|
|
"Edition (AAMEE) to create an installation package "
|
|
"for this product.")
|
|
exit(-1)
|
|
if not catinfo:
|
|
print >> sys.stderr, \
|
|
"Could not find a supported installer item in %s!" % \
|
|
item
|
|
exit(-1)
|
|
|
|
elif munkicommon.hasValidPackageExt(item):
|
|
catinfo = munkicommon.getPackageMetaData(item)
|
|
if options.installer_choices_xml:
|
|
installer_choices_xml = munkicommon.getChoiceChangesXML(
|
|
item)
|
|
if installer_choices_xml:
|
|
catinfo['installer_choices_xml'] = installer_choices_xml
|
|
if not catinfo:
|
|
print >> sys.stderr, (
|
|
"%s doesn't appear to be a valid installer item!" %
|
|
item)
|
|
exit(-1)
|
|
if os.path.isdir(item):
|
|
print >> sys.stderr, (
|
|
"WARNING: %s is a bundle-style package!\n"
|
|
"To use it with munki, you should encapsulate it "
|
|
"in a disk image.\n") % item
|
|
# need to walk the dir and add it all up
|
|
for (path, unused_dirs, files) in os.walk(item):
|
|
for name in files:
|
|
filename = os.path.join(path, name)
|
|
# use os.lstat so we don't follow symlinks
|
|
itemsize += int(os.lstat(filename).st_size)
|
|
# convert to kbytes
|
|
itemsize = int(itemsize/1024)
|
|
|
|
else:
|
|
print >> sys.stderr, "%s is not an installer package!" % item
|
|
exit(-1)
|
|
|
|
if options.description:
|
|
catinfo['description'] = readFileOrString(options.description)
|
|
if options.displayname:
|
|
catinfo['display_name'] = options.displayname
|
|
if options.name:
|
|
catinfo['name'] = options.name
|
|
if options.pkgvers:
|
|
catinfo['version'] = options.pkgvers
|
|
|
|
catinfo['installer_item_size'] = int(itemsize/1024)
|
|
if itemhash != "N/A":
|
|
catinfo['installer_item_hash'] = itemhash
|
|
|
|
# try to generate the correct item location
|
|
temppath = item
|
|
location = ""
|
|
while len(temppath) > 4:
|
|
if temppath.endswith('/pkgs'):
|
|
location = item[len(temppath)+1:]
|
|
break
|
|
else:
|
|
temppath = os.path.dirname(temppath)
|
|
|
|
if not location:
|
|
#just the filename
|
|
location = os.path.split(item)[1]
|
|
catinfo['installer_item_location'] = location
|
|
|
|
# ADOBE STUFF - though maybe generalizable in the future?
|
|
if options.uninstallerdmg:
|
|
uninstallerpath = options.uninstallerdmg
|
|
if os.path.exists(uninstallerpath):
|
|
# try to generate the correct item location
|
|
temppath = uninstallerpath
|
|
location = ""
|
|
while len(temppath) > 4:
|
|
if temppath.endswith('/pkgs'):
|
|
location = uninstallerpath[len(temppath)+1:]
|
|
break
|
|
else:
|
|
temppath = os.path.dirname(temppath)
|
|
|
|
if not location:
|
|
#just the filename
|
|
location = os.path.split(uninstallerpath)[1]
|
|
catinfo['uninstaller_item_location'] = location
|
|
itemsize = int(os.path.getsize(uninstallerpath))
|
|
itemhash = munkicommon.getsha256hash(uninstallerpath)
|
|
catinfo['uninstaller_item_size'] = int(itemsize/1024)
|
|
catinfo['uninstaller_item_hash'] = itemhash
|
|
else:
|
|
print >> sys.stderr, "No uninstaller at %s" % \
|
|
uninstallerpath
|
|
|
|
# some metainfo
|
|
if options.catalog:
|
|
catinfo['catalogs'] = options.catalog
|
|
else:
|
|
catinfo['catalogs'] = ['testing']
|
|
if catinfo.get('receipts', None):
|
|
catinfo['uninstallable'] = True
|
|
catinfo['uninstall_method'] = "removepackages"
|
|
|
|
default_minosversion = "10.4.0"
|
|
maxfileversion = "0.0.0.0.0"
|
|
if catinfo:
|
|
catinfo['autoremove'] = False
|
|
if not 'version' in catinfo:
|
|
if maxfileversion != "0.0.0.0.0":
|
|
catinfo['version'] = maxfileversion
|
|
else:
|
|
catinfo['version'] = "1.0.0.0.0 (Please edit me!)"
|
|
|
|
if options.file:
|
|
for fitem in options.file:
|
|
# no trailing slashes, please.
|
|
fitem = fitem.rstrip('/')
|
|
if fitem.startswith('/Library/Receipts'):
|
|
# no receipts, please!
|
|
print >> sys.stderr, \
|
|
"Item %s appears to be a receipt. Skipping." % fitem
|
|
continue
|
|
if os.path.exists(fitem):
|
|
iteminfodict = getiteminfo(fitem)
|
|
if 'CFBundleShortVersionString' in iteminfodict:
|
|
thisitemversion = \
|
|
iteminfodict['CFBundleShortVersionString']
|
|
if (munkicommon.MunkiLooseVersion(thisitemversion) >
|
|
munkicommon.MunkiLooseVersion(maxfileversion)):
|
|
maxfileversion = thisitemversion
|
|
installs.append(iteminfodict)
|
|
else:
|
|
print >> sys.stderr, (
|
|
"Item %s doesn't exist. Skipping." % fitem)
|
|
|
|
if installs:
|
|
catinfo['installs'] = installs
|
|
|
|
# determine minimum_os_version from identified apps in the installs array
|
|
if 'installs' in catinfo:
|
|
# build a list of minosversions using a list comprehension
|
|
item_minosversions = [ munkicommon.MunkiLooseVersion(
|
|
item['minosversion']) \
|
|
for item in catinfo['installs'] if 'minosversion' in item ]
|
|
# add the default in case it's an empty list
|
|
item_minosversions.append(
|
|
munkicommon.MunkiLooseVersion(default_minosversion))
|
|
if 'minimum_os_version' in catinfo:
|
|
# handle case where value may have been set (e.g. flat package)
|
|
item_minosversions.append(munkicommon.MunkiLooseVersion(
|
|
catinfo['minimum_os_version']))
|
|
# get the maximum from the list and covert back to string
|
|
catinfo['minimum_os_version'] = str(max(item_minosversions))
|
|
|
|
if not 'minimum_os_version' in catinfo:
|
|
# ensure a minimum_os_version is set unless using --file option only
|
|
catinfo['minimum_os_version'] = default_minosversion
|
|
|
|
if options.file and not arguments:
|
|
# remove minimum_os_version as we don't include it for --file only
|
|
catinfo.pop('minimum_os_version')
|
|
|
|
if options.installcheck_script:
|
|
scriptstring = readfile(options.installcheck_script)
|
|
if scriptstring:
|
|
catinfo['installcheck_script'] = scriptstring
|
|
if options.uninstallcheck_script:
|
|
scriptstring = readfile(options.uninstallcheck_script)
|
|
if scriptstring:
|
|
catinfo['uninstallcheck_script'] = scriptstring
|
|
if options.postinstall_script:
|
|
scriptstring = readfile(options.postinstall_script)
|
|
if scriptstring:
|
|
catinfo['postinstall_script'] = scriptstring
|
|
if options.preinstall_script:
|
|
scriptstring = readfile(options.preinstall_script)
|
|
if scriptstring:
|
|
catinfo['preinstall_script'] = scriptstring
|
|
if options.postuninstall_script:
|
|
scriptstring = readfile(options.postuninstall_script)
|
|
if scriptstring:
|
|
catinfo['postuninstall_script'] = scriptstring
|
|
if options.preuninstall_script:
|
|
scriptstring = readfile(options.preuninstall_script)
|
|
if scriptstring:
|
|
catinfo['preuninstall_script'] = scriptstring
|
|
if options.uninstall_script:
|
|
scriptstring = readfile(options.uninstall_script)
|
|
if scriptstring:
|
|
catinfo['uninstall_script'] = scriptstring
|
|
catinfo['uninstall_method'] = 'uninstall_script'
|
|
if options.autoremove:
|
|
catinfo['autoremove'] = True
|
|
if options.minimum_munki_version:
|
|
catinfo['minimum_munki_version'] = options.minimum_munki_version
|
|
if options.unattended_install:
|
|
catinfo['unattended_install'] = True
|
|
if options.unattended_uninstall:
|
|
catinfo['unattended_uninstall'] = True
|
|
if options.minimum_os_version:
|
|
catinfo['minimum_os_version'] = options.minimum_os_version
|
|
if options.maximum_os_version:
|
|
catinfo['maximum_os_version'] = options.maximum_os_version
|
|
if options.force_install_after_date:
|
|
force_install_after_date = (
|
|
munkicommon.validateDateFormat(options.force_install_after_date))
|
|
if force_install_after_date:
|
|
catinfo['force_install_after_date'] = force_install_after_date
|
|
if options.RestartAction:
|
|
validActions = ['RequireRestart', 'RequireLogout', 'RecommendRestart']
|
|
if options.RestartAction in validActions:
|
|
catinfo['RestartAction'] = options.RestartAction
|
|
elif 'restart' in options.RestartAction.lower():
|
|
catinfo['RestartAction'] = 'RequireRestart'
|
|
elif 'logout' in options.RestartAction.lower():
|
|
catinfo['RestartAction'] = 'RequireLogout'
|
|
if options.update_for:
|
|
catinfo['update_for'] = options.update_for
|
|
if options.requires:
|
|
catinfo['requires'] = options.requires
|
|
if options.blocking_application:
|
|
catinfo['blocking_applications'] = options.blocking_application
|
|
if options.uninstall_method:
|
|
catinfo['uninstall_method'] = options.uninstall_method
|
|
if options.installer_environment:
|
|
try:
|
|
installer_environment_dict = dict(
|
|
(k,v) for k,v in (
|
|
kv.split('=') for kv in options.installer_environment))
|
|
except Exception:
|
|
installer_environment_dict = {}
|
|
if installer_environment_dict:
|
|
catinfo['installer_environment'] = installer_environment_dict
|
|
if options.notes:
|
|
catinfo['notes'] = readFileOrString(options.notes)
|
|
if options.apple_update:
|
|
# remove minimum_os_version as we don't include it for this option
|
|
catinfo.pop('minimum_os_version')
|
|
if options.catalog:
|
|
catinfo['catalogs'] = options.catalog
|
|
else:
|
|
catinfo['catalogs'] = ['testing']
|
|
if options.pkgvers:
|
|
catinfo['version'] = options.pkgvers
|
|
else:
|
|
catinfo['version'] = "1.0"
|
|
catinfo['name'] = options.apple_update
|
|
catinfo['installer_type'] = 'apple-update-metadata'
|
|
|
|
# and now, what we've all been waiting for...
|
|
print FoundationPlist.writePlistToString(catinfo)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|