mirror of
https://github.com/munki/munki.git
synced 2026-03-14 13:30:47 -05:00
Added iconimporter tool
This commit is contained in:
427
code/client/iconimporter
Executable file
427
code/client/iconimporter
Executable 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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user