Files
munki/code/client/makepkginfo
Greg Neagle 67c7d092db makepkginfo: CS5 changes
git-svn-id: http://munki.googlecode.com/svn/trunk@583 a4e17f2e-e282-11dd-95e1-755cbddbdd66
2010-07-21 02:59:44 +00:00

478 lines
19 KiB
Python
Executable File

#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009-2010 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 distutils import version
import subprocess
import hashlib
from munkilib import munkicommon
from munkilib import FoundationPlist
from munkilib import adobeutils
def DMGhasSLA(dmgpath):
'''Returns true if dmg has a Software License Agreement.
These dmgs cannot be attached without user intervention'''
hasSLA = False
p = subprocess.Popen(['/usr/bin/hdiutil', 'imageinfo', dmgpath, '-plist'],
bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(plist, err) = p.communicate()
if err:
print >>sys.stderr, "hdiutil error %s with image %s." % (err, dmgpath)
if plist:
pl = FoundationPlist.readPlistFromString(plist)
properties = pl.get('Properties')
if properties:
hasSLA = properties.get('Software License Agreement',False)
return hasSLA
def getCatalogInfoFromDmg(dmgpath, pkgname='', appname=''):
"""
* 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(?)
"""
if DMGhasSLA(dmgpath):
print >>sys.stderr, \
"%s has an attached Software License Agreement." % dmgpath
print >>sys.stderr, \
"It cannot be automatically mounted. You'll need to create a new dmg."
exit(-1)
cataloginfo = None
mountpoints = munkicommon.mountdmg(dmgpath)
if not mountpoints:
print >>sys.stderr, "Could not mount %s!" % dmgpath
exit(-1)
if pkgname:
pkgpath = os.path.join(mountpoints[0], pkgname)
if os.path.exists(pkgpath):
cataloginfo = munkicommon.getPackageMetaData(pkgpath)
if cataloginfo:
cataloginfo['package_path'] = pkgname
elif not appname:
# search for first package at root
for fsitem in os.listdir(mountpoints[0]):
itempath = os.path.join(mountpoints[0], fsitem)
if itempath.endswith('.pkg') or itempath.endswith('.mpkg'):
cataloginfo = munkicommon.getPackageMetaData(itempath)
# 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:
pkgid = cataloginfo['receipts'][0].get('packageid')
if pkgid.startswith("com.adobe.Enterprise.install"):
# we have an Adobe CS5 install package, process
# as Adobe install
pkgname = cataloginfo['receipts'][0].get('filename')
cataloginfo = adobeutils.getAdobeCatalogInfo(
mountpoints[0], pkgname)
else:
# maybe an Adobe installer/updater/patcher?
cataloginfo = adobeutils.getAdobeCatalogInfo(mountpoints[0],
pkgname)
if not cataloginfo:
# maybe this is an appdmg
# look for an app at the top level of the dmg
appinfo = {}
if appname:
itempath = os.path.join(mountpoints[0], appname)
if munkicommon.isApplication(itempath):
appinfo = getiteminfo(itempath)
else:
appname = ''
for item in os.listdir(mountpoints[0]):
itempath = os.path.join(mountpoints[0], item)
if munkicommon.isApplication(itempath):
appname = item
appinfo = getiteminfo(itempath)
if appinfo:
break
if appinfo:
appinfo['path'] = os.path.join("/Applications", appname)
cataloginfo = {}
cataloginfo['name'] = appinfo.get('CFBundleName',
os.path.splitext(appname)[0])
cataloginfo['version'] = \
munkicommon.padVersionString(
appinfo.get('CFBundleShortVersionString', "0")
,5)
cataloginfo['installs'] = [appinfo]
cataloginfo['installer_type'] = "appdmg"
cataloginfo['uninstallable'] = True
cataloginfo['uninstall_method'] = "remove_app"
#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:
pl = FoundationPlist.readPlist(infopath)
return pl
except FoundationPlist.NSPropertyListSerializationException:
pass
return None
def getmd5hash(filename):
"""
Returns hex of MD5 checksum of a file
"""
if not os.path.isfile(filename):
return "NOT A FILE"
f = open(filename, 'rb')
m = hashlib.md5()
while 1:
chunk = f.read(2**16)
if not chunk:
break
m.update(chunk)
f.close()
return m.hexdigest()
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
pl = getBundleInfo(itempath)
if 'CFBundleName' in pl:
infodict['CFBundleName'] = pl['CFBundleName']
if 'CFBundleIdentifier' in pl:
infodict['CFBundleIdentifier'] = pl['CFBundleIdentifier']
infodict['CFBundleShortVersionString'] = \
munkicommon.getVersionString(pl)
if 'LSMinimumSystemVersion' in pl:
infodict['minosversion'] = pl['LSMinimumSystemVersion']
elif 'SystemVersionCheck:MinimumSystemVersion' in pl:
infodict['minosversion'] = \
pl['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
pl = getBundleInfo(itempath)
infodict['CFBundleShortVersionString'] = \
munkicommon.getVersionString(pl)
elif itempath.endswith("Info.plist") or \
itempath.endswith("version.plist"):
infodict['type'] = 'plist'
infodict['path'] = itempath
try:
pl = FoundationPlist.readPlist(itempath)
infodict['CFBundleShortVersionString'] = \
munkicommon.getVersionString(pl)
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'] = getmd5hash(itempath)
return infodict
def main():
usage = "usage: %prog [options] [/path/to/installeritem]"
p = optparse.OptionParser(usage=usage)
p.add_option('--file', '-f', action="append",
help='''Path to a filesystem item installed by this
package, typically an application.
Can be specified multiple times.''')
p.add_option('--pkgname', '-p',
help='''Optional flag.
-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.''')
p.add_option('--appname', '-a',
help='''Optional flag.
-If the installer item is a disk image with a
drag-and-drop application, APPNAME is the name or
relative path of the application to be installed.
Useful if there is more than one application at the
root of the dmg.''')
p.add_option('--uninstallerdmg', '-u',
help='''Optional flag.
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('--installxml',
help='''Optional flag.
If the installer item is a disk image containing an
Adobe CS5 product, INSTALLXML specifies the path to
an install.xml file containing installation data.''')
p.add_option('--uninstallxml',
help='''Optional flag.
If the installer item is a disk image containing an
Adobe CS5 product, UNINSTALLXML specifies the path to
an uninstall.xml containing uninstall data.''')
p.add_option('--serialnumber',
help='''Optional flag.
If the installer item is a disk image containing an
Adobe CS5 product, SERIALNUMBER is the product
serial number.''')
options, arguments = p.parse_args()
if len(arguments) == 0 and \
not (options.file or options.installxml or options.uninstallxml
or options.serialnumber):
print >>sys.stderr, \
("Need to specify an installer item (.pkg, .mpkg, .dmg) "
"and/or --file and/or Adobe installer options!")
exit(-1)
if len(arguments) > 1:
print >>sys.stderr, \
("Can process only one installer item at a time. "
"Ignoring additional installer items.")
catinfo = {}
installs = []
if arguments:
item = arguments[0].rstrip("/")
if item and os.path.exists(item):
# get size of installer item
itemsize = 0
if os.path.isfile(item):
itemsize = int(os.path.getsize(item))
if os.path.isdir(item):
# need to walk the dir and add it all up
for (path, dirs, files) in os.walk(item):
for f in files:
filename = os.path.join(path, f)
# use os.lstat so we don't follow symlinks
itemsize += int(os.lstat(filename).st_size)
if item.endswith('.dmg'):
pkgname = ''
appname = ''
if options.pkgname:
pkgname = options.pkgname
if options.appname:
appname = options.appname
catinfo = getCatalogInfoFromDmg(item, pkgname, appname)
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 not catinfo:
print >>sys.stderr, \
"%s doesn't appear to be a valid installer item!" % \
item
exit(-1)
else:
print >>sys.stderr, "%s is not an installer package!" % item
exit(-1)
catinfo['installer_item_size'] = int(itemsize/1024)
# 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))
catinfo['uninstaller_item_size'] = int(itemsize/1024)
else:
print >>sys.stderr, "No uninstaller at %s" % \
uninstallerpath
# some metainfo
catinfo['catalogs'] = ['testing']
if catinfo.get('receipts',None):
catinfo['uninstallable'] = True
catinfo['uninstall_method'] = "removepackages"
minosversion = ""
maxfileversion = "0.0.0.0.0"
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 version.LooseVersion(thisminosversion) < \
version.LooseVersion(minosversion):
minosversion = thisminosversion
if 'CFBundleShortVersionString' in iteminfodict:
thisitemversion = \
munkicommon.padVersionString(
iteminfodict['CFBundleShortVersionString'],5)
if version.LooseVersion(thisitemversion) > \
version.LooseVersion(maxfileversion):
maxfileversion = thisitemversion
installs.append(iteminfodict)
else:
print >>sys.stderr, "Item %s doesn't exist. Skipping." % fitem
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 installs:
catinfo['installs'] = installs
if options.installxml or options.uninstallxml or \
options.serialnumber:
more_info = adobeutils.getAdobeInstallInfo(
mountpoint=None,
installxmlpath=options.installxml,
uninstallxmlpath=options.uninstallxml,
serialnumber=options.serialnumber,
suppressRegistration=True,
suppressUpdates=True)
if not 'adobe_install_info' in catinfo:
catinfo['adobe_install_info'] = {}
for key in more_info.keys():
catinfo['adobe_install_info'][key] = more_info[key]
# and now, what we've all been waiting for...
print FoundationPlist.writePlistToString(catinfo)
if __name__ == '__main__':
main()