mirror of
https://github.com/munki/munki.git
synced 2026-01-07 06:59:57 -06:00
Tweaks in finding apps for appdmg git-svn-id: http://munki.googlecode.com/svn/trunk@406 a4e17f2e-e282-11dd-95e1-755cbddbdd66
458 lines
18 KiB
Python
Executable File
458 lines
18 KiB
Python
Executable File
#!/usr/bin/python
|
|
# encoding: utf-8
|
|
#
|
|
# Copyright 2009 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 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 not cataloginfo:
|
|
# ADOBE STUFF
|
|
# no Apple installer items found.
|
|
# Look for AdobeUberInstaller items
|
|
pkgroot = os.path.join(mountpoints[0], pkgname)
|
|
adobeinstallxml = os.path.join(pkgroot, "AdobeUberInstaller.xml")
|
|
if os.path.exists(adobeinstallxml):
|
|
# this is a CS4 Enterprise Deployment package
|
|
cataloginfo = adobeutils.getAdobePackageInfo(pkgroot)
|
|
if cataloginfo:
|
|
# add some more data
|
|
cataloginfo['name'] = \
|
|
cataloginfo['display_name'].replace(" ",'')
|
|
cataloginfo['uninstallable'] = True
|
|
cataloginfo['uninstall_method'] = "AdobeUberUninstaller"
|
|
cataloginfo['installer_type'] = "AdobeUberInstaller"
|
|
if pkgname:
|
|
cataloginfo['package_path'] = pkgname
|
|
|
|
if not cataloginfo:
|
|
# ADOBE STUFF
|
|
# maybe this is an Adobe update DMG or CS3 installer
|
|
# look for Adobe Setup.app
|
|
setuppath = adobeutils.findSetupApp(mountpoints[0])
|
|
if setuppath:
|
|
cataloginfo = adobeutils.getAdobeSetupInfo(mountpoints[0])
|
|
if cataloginfo:
|
|
# add some more data
|
|
cataloginfo['name'] = \
|
|
cataloginfo['display_name'].replace(" ",'')
|
|
cataloginfo['installer_type'] = "AdobeSetup"
|
|
if cataloginfo.get('AdobeSetupType') == "ProductInstall":
|
|
cataloginfo['uninstallable'] = True
|
|
cataloginfo['uninstall_method'] = "AdobeSetup"
|
|
else:
|
|
cataloginfo['description'] = "Adobe updater"
|
|
cataloginfo['uninstallable'] = False
|
|
cataloginfo['update_for'] = ["PleaseEditMe-1.0.0.0.0"]
|
|
|
|
if not cataloginfo:
|
|
# maybe this is an appdmg
|
|
# look for an app at the top level of the dmg
|
|
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, this
|
|
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, this 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='''If installer item is a DMG 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.''')
|
|
|
|
options, arguments = p.parse_args()
|
|
|
|
if len(arguments) == 0 and not options.file:
|
|
print >>sys.stderr, \
|
|
("Need to specify an installer item (.pkg, .mpkg, .dmg) "
|
|
"and/or --file 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
|
|
|
|
# and now, what we've all been waiting for...
|
|
print FoundationPlist.writePlistToString(catinfo)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|