mirror of
https://github.com/munki/munki.git
synced 2026-01-06 22:49:58 -06:00
248 lines
9.4 KiB
Python
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.'
|