mirror of
https://github.com/munki/munki.git
synced 2026-01-23 07:30:47 -06:00
846 lines
36 KiB
Python
Executable File
846 lines
36 KiB
Python
Executable File
#!/usr/bin/python
|
|
# encoding: utf-8
|
|
#
|
|
# Copyright 2009-2011 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
|
|
import subprocess
|
|
|
|
from munkilib import munkicommon
|
|
from munkilib import FoundationPlist
|
|
from munkilib import adobeutils
|
|
|
|
|
|
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 itempath.endswith('.pkg') or itempath.endswith('.mpkg'):
|
|
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:
|
|
if options.destinationpath:
|
|
iteminfo['path'] = os.path.join(options.destinationpath,
|
|
item)
|
|
else:
|
|
iteminfo['path'] = os.path.join("/Applications", 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 = {}
|
|
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)
|
|
if 'CFBundleName' in plist:
|
|
infodict['CFBundleName'] = plist['CFBundleName']
|
|
if 'CFBundleIdentifier' in plist:
|
|
infodict['CFBundleIdentifier'] = plist['CFBundleIdentifier']
|
|
infodict['CFBundleShortVersionString'] = \
|
|
munkicommon.getVersionString(plist)
|
|
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)
|
|
infodict['CFBundleShortVersionString'] = \
|
|
munkicommon.getVersionString(plist)
|
|
|
|
elif itempath.endswith("Info.plist") or \
|
|
itempath.endswith("version.plist"):
|
|
infodict['type'] = 'plist'
|
|
infodict['path'] = itempath
|
|
try:
|
|
plist = FoundationPlist.readPlist(itempath)
|
|
infodict['CFBundleShortVersionString'] = \
|
|
munkicommon.getVersionString(plist)
|
|
except FoundationPlist.NSPropertyListSerializationException:
|
|
pass
|
|
|
|
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''')
|
|
)
|
|
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(
|
|
'--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(
|
|
'-o', '--owner',
|
|
metavar='USER',
|
|
dest='user',
|
|
help=('''Sets the owner of the item specified by the '''
|
|
'''--item flag. 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 item specified by the '''
|
|
'''--item flag. 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 item specified by the '''
|
|
'''--item flag. 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='''Implies 'true' for 'unattended_install' '''
|
|
'''for the installer item.'''
|
|
)
|
|
forced_unattended_options.add_option(
|
|
'--unattended_uninstall', '--unattended-uninstall',
|
|
action='store_true',
|
|
help='''Implies 'true' for 'unattended_uninstall' '''
|
|
'''for the installer item.'''
|
|
)
|
|
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 '''
|
|
'''August 11th 2011 at 12:55 PM.''')
|
|
)
|
|
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)
|
|
|
|
# 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:
|
|
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)
|
|
print >> sys.stdout, arguments[0]
|
|
exit(0)
|
|
|
|
if (len(arguments) == 0
|
|
and not options.file
|
|
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):
|
|
p.print_usage()
|
|
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:])
|
|
|
|
if options.installer_choices_xml:
|
|
os_version = munkicommon.getOsVersion(only_major_minor=False,as_tuple=True)
|
|
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 item.endswith('.dmg'):
|
|
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 item.endswith('.pkg') or item.endswith('.mpkg'):
|
|
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)
|
|
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"
|
|
|
|
minosversion = ""
|
|
maxfileversion = "0.0.0.0.0"
|
|
if catinfo:
|
|
catinfo['autoremove'] = False
|
|
if minosversion:
|
|
catinfo['minimum_os_version'] = minosversion
|
|
else:
|
|
catinfo['minimum_os_version'] = "10.4.0"
|
|
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 'minosversion' in iteminfodict:
|
|
thisminosversion = iteminfodict.pop('minosversion')
|
|
if not minosversion:
|
|
minosversion = thisminosversion
|
|
elif (munkicommon.MunkiLooseVersion(thisminosversion) <
|
|
munkicommon.MunkiLooseVersion(minosversion)):
|
|
minosversion = thisminosversion
|
|
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
|
|
|
|
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_application'] = options.blocking_application
|
|
if options.installer_environment:
|
|
try:
|
|
installer_environment_dict = dict(
|
|
(k,v) for k,v in (
|
|
kv.split('=') for kv in options.installer_environment))
|
|
except:
|
|
pass
|
|
if installer_environment_dict:
|
|
catinfo['installer_environment'] = installer_environment_dict
|
|
if options.notes:
|
|
catinfo['notes'] = readFileOrString(options.notes)
|
|
|
|
# and now, what we've all been waiting for...
|
|
print FoundationPlist.writePlistToString(catinfo)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|