Added iconimporter tool

This commit is contained in:
Greg Neagle
2014-03-24 15:01:31 -07:00
parent e5a2a7569e
commit c03cb77fff

427
code/client/iconimporter Executable file
View File

@@ -0,0 +1,427 @@
#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2010-2014 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.
"""
iconimporter
Created by Greg Neagle on 2014-03-03.
Converts and imports icons as png files for Munki repo
"""
import glob
import sys
import os
from optparse import OptionParser
import subprocess
import tempfile
import shutil
from Foundation import NSData
from AppKit import NSBitmapImageRep, NSPNGFileType
from munkilib import munkicommon
from munkilib import FoundationPlist
def convertIconToPNG(icon_path, destination_path, desired_pixel_height=350):
'''Converts an icns file to a png file, choosing the representation
closest to (but >= if possible) the desired_pixel_height.
Returns True if successful, False otherwise'''
if os.path.exists(icon_path):
image_data = NSData.dataWithContentsOfFile_(icon_path)
bitmap_reps = NSBitmapImageRep.imageRepsWithData_(image_data)
chosen_rep = None
for bitmap_rep in bitmap_reps:
if not chosen_rep:
chosen_rep = bitmap_rep
elif (bitmap_rep.pixelsHigh() >= desired_pixel_height
and bitmap_rep.pixelsHigh() < chosen_rep.pixelsHigh()):
chosen_rep = bitmap_rep
if chosen_rep:
png_data = chosen_rep.representationUsingType_properties_(
NSPNGFileType, None)
png_data.writeToFile_atomically_(destination_path, False)
return True
return False
def findIconForApp(app_path):
'''Finds the icon file for app_path. Returns a path or None.'''
if not os.path.exists(app_path):
return None
try:
info = FoundationPlist.readPlist(
os.path.join(app_path, 'Contents/Info.plist'))
except (FoundationPlist.FoundationPlistException):
return None
app_name = os.path.basename(app_path)
icon_filename = info.get('CFBundleIconFile', app_name)
icon_path = os.path.join(app_path, 'Contents/Resources', icon_filename)
if not os.path.splitext(icon_path)[1]:
# no file extension, so add '.icns'
icon_path += '.icns'
if os.path.exists(icon_path):
return icon_path
return None
def extractAppBitsFromPkgArchive(archive_path, target_dir):
'''Extracts application Info.plist and .icns files into target_dir
from a package archive file. Returns the result code of the
pax extract operation.'''
result = -999
if os.path.exists(archive_path):
original_dir = os.getcwd()
os.chdir(target_dir)
cmd = ['/bin/pax', '-rzf', archive_path,
'*.app/Contents/Info.plist',
'*.app/Contents/Resources/*.icns']
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
(output, errors) = proc.communicate()
result = proc.returncode
os.chdir(original_dir)
return result
def extractAppIconsFromFlatPkg(pkg_path):
'''Extracts application icons from a flat package.
Returns a list of paths to icns files.'''
cmd = ['/usr/sbin/pkgutil', '--bom', pkg_path]
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
(output, errors) = proc.communicate()
bomfilepaths = output.splitlines()
pkg_dict = {}
for bomfile in bomfilepaths:
# bomfile path is of the form:
# /tmp/FlashPlayer.pkg.boms.2Rxa1z/AdobeFlashPlayerComponent.pkg/Bom
pkgname = os.path.basename(os.path.dirname(bomfile))
cmd = ['/usr/bin/lsbom', '-s', bomfile]
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
(output, errors) = proc.communicate()
# record paths to all app Info.plist files
pkg_dict[pkgname]= [line for line in output.splitlines()
if line.endswith('.app/Contents/Info.plist')]
if not pkg_dict[pkgname]:
# remove empty lists
del(pkg_dict[pkgname])
if not pkg_dict:
return []
icon_paths = []
pkgtmp = os.path.join(tempfile.mkdtemp(dir='/tmp'), 'pkg')
exporttmp = tempfile.mkdtemp(dir='/tmp')
cmd = ['/usr/sbin/pkgutil', '--expand', pkg_path, pkgtmp]
result = subprocess.call(cmd)
if result == 0:
for pkg in pkg_dict:
archive_path = os.path.join(pkgtmp, pkg, 'Payload')
err = extractAppBitsFromPkgArchive(archive_path, exporttmp)
if err == 0:
for info_path in pkg_dict[pkg]:
full_path = os.path.join(exporttmp, info_path)
# convert path to Info.plist to path to app
app_path = os.path.dirname(os.path.dirname(full_path))
icon_path = findIconForApp(app_path)
if icon_path:
icon_paths.append(icon_path)
# clean up our expanded flat package; we no longer need it
shutil.rmtree(pkgtmp)
return icon_paths
def extractAppIconsFromBundlePkg(pkg_path):
pkg_dict = {}
bomfile = os.path.join(pkg_path, 'Contents', 'Archive.bom')
if os.path.exists(bomfile):
info_paths = getAppInfoPathsFromBundleComponentPkg(pkg_path)
if info_paths:
pkg_dict[pkg_path] = info_paths
else:
# mpkg or dist pkg; look for component pkgs within
pkg_dict = {}
original_dir = os.getcwd()
pkg_contents_dir = os.path.join(pkg_path, 'Contents')
os.chdir(pkg_contents_dir)
pkgs = (glob.glob('*.pkg') + glob.glob('*/*.pkg')
+ glob.glob('*/*/*.pkg') + glob.glob('*.mpkg') +
glob.glob('*/*.mpkg') + glob.glob('*/*/*.mpkg'))
os.chdir(original_dir)
for pkg in pkgs:
full_path = os.path.join(pkg_contents_dir, pkg)
info_paths = getAppInfoPathsFromBundleComponentPkg(full_path)
if info_paths:
pkg_dict[full_path] = info_paths
icon_paths = []
exporttmp = tempfile.mkdtemp(dir='/tmp')
for pkg in pkg_dict:
archive_path = os.path.join(pkg, 'Contents/Archive.pax.gz')
err = extractAppBitsFromPkgArchive(archive_path, exporttmp)
if err == 0:
for info_path in pkg_dict[pkg]:
full_path = os.path.join(exporttmp, info_path)
app_path = os.path.dirname(os.path.dirname(full_path))
icon_path = findIconForApp(app_path)
if icon_path:
icon_paths.append(icon_path)
return icon_paths
def getAppInfoPathsFromBundleComponentPkg(pkg_path):
'''Returns a list of paths to application Info.plists'''
bomfile = os.path.join(pkg_path, 'Contents', 'Archive.bom')
if os.path.exists(bomfile):
cmd = ['/usr/bin/lsbom', '-s', bomfile]
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
(output, errors) = proc.communicate()
return [line for line in output.splitlines()
if line.endswith('.app/Contents/Info.plist')]
return []
def generate_png_from_copy_from_dmg_item(install_item, repo_path):
dmgpath = os.path.join(
repo_path, 'pkgs', install_item['installer_item_location'])
mountpoints = munkicommon.mountdmg(dmgpath)
if mountpoints:
mountpoint = mountpoints[0]
apps = [item for item in install_item.get('items_to_copy', [])
if item.get('source_item', '').endswith('.app')]
if len(apps):
app_path = os.path.join(mountpoint, apps[0]['source_item'])
icon_path = findIconForApp(app_path)
if icon_path:
png_path = os.path.join(
repo_path, 'icons', install_item['name'] + '.png')
result = convertIconToPNG(icon_path, png_path)
if result:
print_utf8(u'\tWrote: %s' % png_path)
else:
print_err_utf8(u'\tError converting %s to png.' % icon_path)
else:
print_utf8(u'\tNo application icons found.')
else:
print_utf8(u'\tNo application icons found.')
munkicommon.unmountdmg(mountpoint)
def generate_pngs_from_installer_pkg(install_item, repo_path):
icon_paths = []
mountpoint = None
pkg_path = None
item_path = os.path.join(
repo_path, 'pkgs', install_item['installer_item_location'])
if munkicommon.hasValidDiskImageExt(item_path):
dmg_path = item_path
mountpoints = munkicommon.mountdmg(dmg_path)
if mountpoints:
mountpoint = mountpoints[0]
if install_item.get('package_path'):
pkg_path = os.path.join(
mountpoint, install_item['package_path'])
else:
# find first item that appears to be a pkg at the root
for fileitem in munkicommon.listdir(mountpoints[0]):
if munkicommon.hasValidPackageExt(fileitem):
pkg_path = os.path.join(mountpoint, fileitem)
break
elif munkicommon.hasValidPackageExt(item_path):
pkg_path = item_path
if pkg_path:
if os.path.isdir(pkg_path):
icon_paths = extractAppIconsFromBundlePkg(pkg_path)
else:
icon_paths = extractAppIconsFromFlatPkg(pkg_path)
if mountpoint:
munkicommon.unmountdmg(mountpoint)
if len(icon_paths) == 1:
png_path = os.path.join(
repo_path, 'icons', install_item['name'] + '.png')
result = convertIconToPNG(icon_paths[0], png_path)
if result:
print_utf8(u'\tWrote: %s' % png_path)
elif len(icon_paths) > 1:
index = 1
for icon_path in icon_paths:
png_path = os.path.join(
repo_path, 'icons',
install_item['name'] + '_' + str(index) + '.png')
result = convertIconToPNG(icon_path, png_path)
if result:
print_utf8(u'\tWrote: %s' % png_path)
index += 1
else:
print_utf8(u'\tNo application icons found.')
def findItemsToCheck(repo_path, itemlist=None):
'''Builds a list of items to check; only the latest version
of an item is retained. If itemlist is given, include items
only on that list.'''
all_catalog_path = os.path.join(repo_path, 'catalogs/all')
catalogitems = FoundationPlist.readPlist(all_catalog_path)
itemdb = {}
for catalogitem in catalogitems:
if itemlist and catalogitem['name'] not in itemlist:
continue
name = catalogitem['name']
if name not in itemdb:
itemdb[name] = catalogitem
elif (munkicommon.MunkiLooseVersion(catalogitem['version'])
> munkicommon.MunkiLooseVersion(itemdb[name]['version'])):
itemdb[name] = catalogitem
pkg_list = []
for key in itemdb.keys():
pkg_list.append(itemdb[key])
return pkg_list
def generate_pngs_from_munki_items(repo_path, force=False, itemlist=None):
itemlist = findItemsToCheck(repo_path, itemlist=None)
icons_dir = os.path.join(repo_path, 'icons')
if not os.path.exists(icons_dir):
os.makedir(icons_dir)
for item in itemlist:
print_utf8(u'Processing %s...' % item['name'])
icon_name = item.get('icon_name') or item['name']
if not os.path.splitext(icon_name)[1]:
icon_name += '.png'
icon_path = os.path.join(
repo_path, 'icons', icon_name)
if os.path.exists(icon_path) and not force:
print_utf8(u'Found existing icon at %s' % icon_name)
continue
installer_type = item.get('installer_type')
if installer_type == 'copy_from_dmg':
generate_png_from_copy_from_dmg_item(item, repo_path)
elif installer_type in [None, '']:
generate_pngs_from_installer_pkg(item, repo_path)
else:
print_utf8(u'\tCan\'t process installer_type: %s' % installer_type)
def getConditionalOptionalItems(plist):
'''Returns a set of optional_installs names from any
conditional_items in the plist'''
optional_items = set()
for item in plist.get('conditional_items', []):
if item.get('conditional_items'):
optional_items.update(getConditionalOptionalItems(item))
if item.get('optional_items'):
optional_items.update(item['optional_items'])
return optional_items
def findAllOptionalInstalls(repo_path):
optional_items = set()
errors = []
manifests_path = os.path.join(repo_path, 'manifests')
# Walk through the manifest files
for dirpath, dirnames, filenames in os.walk(manifests_path):
for dirname in dirnames:
# don't recurse into directories that start
# with a period.
if dirname.startswith('.'):
dirnames.remove(dirname)
for filename in filenames:
if filename.startswith('.'):
# skip files that start with a period as well
continue
filepath = os.path.join(dirpath, filename)
# Try to read the manifest file
try:
manifest = FoundationPlist.readPlist(filepath)
except FoundationPlist.FoundationPlistException, inst:
errors.append("Unexpected error for %s: %s" % (filepath, inst))
continue
if manifest:
optional_items.update(
set(manifest.get('optional_installs', [])))
optional_items.update(getConditionalOptionalItems(manifest))
return list(optional_items)
def print_utf8(text):
'''Print Unicode text as UTF-8'''
print text.encode('UTF-8')
def print_err_utf8(text):
'''Print Unicode text to stderr as UTF-8'''
print >> sys.stderr, text.encode('UTF-8')
def pref(prefname):
"""Returns a preference for prefname"""
try:
_prefs = plistlib.readPlist(PREFSPATH)
except Exception:
return None
if prefname in _prefs:
return _prefs[prefname]
else:
return None
PREFSNAME = 'com.googlecode.munki.munkiimport.plist'
PREFSPATH = os.path.expanduser(os.path.join('~/Library/Preferences',
PREFSNAME))
def main():
'''Main'''
usage = "usage: %prog [options] [/path/to/repo_root]"
p = OptionParser(usage=usage)
p.add_option('--force', '-f', action='store_true', dest='force',
help='Create pngs even if there is an existing icon in the repo.')
p.set_defaults(force=False)
options, arguments = p.parse_args()
# Make sure we have a path to work with
repo_path = None
if len(arguments) == 0:
repo_path = pref('repo_path')
if not repo_path:
print_err_utf8("Need to specify a path to the repo root!")
exit(-1)
else:
print_utf8("Using repo path: %s" % repo_path)
else:
repo_path = arguments[0].rstrip("/")
# Make sure the repo path exists
if not os.path.exists(repo_path):
print_err_utf8("Repo root path %s doesn't exist!" % repo_path)
exit(-1)
# generate icons!
generate_pngs_from_munki_items(repo_path, force=options.force)
if __name__ == '__main__':
main()