Files
munki/code/client/munkilib/iconutils.py
2017-04-07 22:05:02 -07:00

248 lines
9.4 KiB
Python

# encoding: utf-8
#
# Copyright 2010-2017 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
#
# https://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 os
import shutil
import subprocess
import tempfile
# PyLint cannot properly find names inside Cocoa libraries, so issues bogus
# No name 'Foo' in module 'Bar' warnings. Disable them.
# pylint: disable=E0611
from Foundation import NSData
from AppKit import NSBitmapImageRep, NSPNGFileType
# pylint: enable=E0611
from . import display
from . import FoundationPlist
# we use lots of camelCase-style names. Deal with it.
# pylint: disable=C0103
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']
result = subprocess.call(cmd)
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 = proc.communicate()[0]
if proc.returncode:
display.display_error(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 = proc.communicate()[0]
if proc.returncode:
display.display_error(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:
display.display_error(
u'pax could not read files from %s', archive_path)
return []
else:
display.display_error(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, repo=None):
'''Returns a dict with pkg paths as keys and filename lists
as values'''
pkg_dict = {}
if repo:
repo_bomfile = repo.join(pkg_path, u'Contents/Archive.bom')
handle = repo.open(repo_bomfile, 'r')
bomfile = handle.local_path
else:
bomfile = os.path.join(pkg_path, u'Contents/Archive.bom')
if os.path.exists(bomfile):
info_paths = getAppInfoPathsFromBOM(bomfile)
if info_paths:
pkg_dict[pkg_path] = info_paths
else:
# mpkg or dist pkg; look for component pkgs within
pkg_dict = {}
pkgs = []
if repo:
pkg_contents_dir = repo.join(pkg_path, u'Contents')
if repo.isdir(pkg_contents_dir):
pkgs = repo.glob(
pkg_contents_dir, '*.pkg', '*/*.pkg', '*/*/*.pkg',
'*.mpkg', '*/*.mpkg', '*/*/*.mpkg')
else:
pkg_contents_dir = os.path.join(pkg_path, u'Contents')
if os.path.isdir(pkg_contents_dir):
original_dir = os.getcwd()
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)
pkg_dict.update(findInfoPlistPathsInBundlePkg(full_path))
return pkg_dict
def extractAppIconsFromBundlePkg(pkg_path, repo=None):
'''Returns a list of paths for application icons found
inside the bundle pkg at pkg_path'''
pkg_dict = findInfoPlistPathsInBundlePkg(pkg_path, repo)
icon_paths = []
exporttmp = tempfile.mkdtemp(dir='/tmp')
for pkg in pkg_dict:
handle = None
if repo:
repo_archive_path = repo.join(pkg, u'Contents/Archive.pax.gz')
handle = repo.open(repo_archive_path, 'r')
archive_path = handle.local_path
else:
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 getAppInfoPathsFromBOM(bomfile):
'''Returns a list of paths to application Info.plists'''
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 = proc.communicate()[0]
return [line for line in output.splitlines()
if line.endswith('.app/Contents/Info.plist')]
return []
if __name__ == '__main__':
print 'This is a library of support tools for the Munki Suite.'