Files
munki/code/client/munkilib/iconutils.py

225 lines
8.7 KiB
Python

#!/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.
"""
iconutils
Created by Greg Neagle on 2014-05-15.
Functions to work with product images ('icons') for Managed Software Center
"""
import glob
import sys
import os
import subprocess
import tempfile
import shutil
from Foundation import NSData
from AppKit import NSBitmapImageRep, NSPNGFileType
import munkicommon
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, u'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, u'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()
if proc.returncode:
print >> sys.stderr, u'Could not get bom files from %s' % pkg_path
return []
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))
if not pkgname.endswith(u'.pkg'):
# no subpackages; this is a component pkg
pkgname = ''
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()
if proc.returncode:
print >> sys.stderr, u'Could not lsbom %s' % bomfile
# record paths to all app Info.plist files
pkg_dict[pkgname] = [os.path.normpath(line)
for line in output.decode('utf-8').splitlines()
if line.endswith(u'.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=u'/tmp'), u'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, u'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)
else:
print >> sys.stderr, (
u'pax could not read files from %s' % archive_path)
return []
else:
print >> sys.stderr, u'Could not expand %s' % pkg_path
# clean up our expanded flat package; we no longer need it
shutil.rmtree(pkgtmp)
return icon_paths
def findInfoPlistPathsInBundlePkg(pkg_path):
'''Returns a dict with pkg paths as keys and filename lists
as values'''
pkg_dict = {}
bomfile = os.path.join(pkg_path, u'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, u'Contents')
if os.path.isdir(pkg_contents_dir):
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)
else:
pkgs = []
for pkg in pkgs:
full_path = os.path.join(pkg_contents_dir, pkg)
pkg_dict.update(findInfoPlistPathsInBundlePkg(full_path))
return pkg_dict
def extractAppIconsFromBundlePkg(pkg_path):
'''Returns a list of paths for application icons found
inside the bundle pkg at pkg_path'''
pkg_dict = findInfoPlistPathsInBundlePkg(pkg_path)
icon_paths = []
exporttmp = tempfile.mkdtemp(dir='/tmp')
for pkg in pkg_dict:
archive_path = os.path.join(pkg, u'Contents/Archive.pax.gz')
err = extractAppBitsFromPkgArchive(archive_path, exporttmp)
if err == 0:
for info_path in pkg_dict[pkg]:
full_path = os.path.normpath(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, u'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 []