#!/usr/bin/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. """ iconimporter Created by Greg Neagle on 2014-03-03. Converts and imports icons as png files for Munki repo """ # standard libs import os from optparse import OptionParser import sys import tempfile import urllib import urlparse # our libs from munkilib import dmgutils from munkilib import iconutils from munkilib import munkirepo from munkilib import osutils from munkilib import pkgutils from munkilib import FoundationPlist # 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 CFPreferencesCopyAppValue # pylint: enable=E0611 class TempFile(object): '''A class that creates a temp file that is automatically deleted when the object goes out of scope.''' def __init__(self): filedesc, filepath = tempfile.mkstemp() # we just want the path; close the file descriptor os.close(filedesc) self.path = filepath def __del__(self): try: os.unlink(self.path) except OSError: pass def copy_icon_to_repo(repo, name, path): '''Copies png file in path to repo as icons/name.png''' icon_ref = os.path.join(u'icons', name + u'.png') try: repo.put_from_local_file(icon_ref, path) print_utf8(u'\tWrote: %s' % icon_ref) except munkirepo.RepoError, err: print_err_utf8(u'\tError uploading %s: %s' % (icon_ref, unicode(err))) def generate_png_from_dmg_item(repo, install_item): '''Generate a PNG from a disk image containing an application''' # Since the repo might be a remote repo reached by a web API, we have # to download the file first. We might want to extend the Repo plugin # "API" to let us get the direct filepath, skipping the need to download for # the FileRepo-type repos at least. dmg_ref = os.path.join('pkgs', install_item['installer_item_location']) dmg_temp = TempFile() try: repo.get_to_local_file(dmg_ref, dmg_temp.path) except munkirepo.RepoError, err: print_err_utf8(u'\tCan\'t download %s from repo: %s' % (dmg_ref, unicode(err))) return mountpoints = dmgutils.mountdmg(dmg_temp.path) 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 = iconutils.findIconForApp(app_path) if icon_path: # use a local temp file to create our png icon_temp = TempFile() result = iconutils.convertIconToPNG(icon_path, icon_temp.path) if result: copy_icon_to_repo( repo, install_item['name'], icon_temp.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.') dmgutils.unmountdmg(mountpoint) def generate_pngs_from_pkg(repo, install_item): '''Generate PNGS from applications inside a pkg''' icon_paths = [] mountpoint = None pkg_path = None item_path = os.path.join(u'pkgs', install_item['installer_item_location']) if pkgutils.hasValidDiskImageExt(item_path): dmg_ref = item_path dmg_temp = TempFile() try: repo.get_to_local_file(dmg_ref, dmg_temp.path) except munkirepo.RepoError, err: print_err_utf8(u'\tCan\'t download %s from repo: %s' % (dmg_ref, unicode(err))) return mountpoints = dmgutils.mountdmg(dmg_temp.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 osutils.listdir(mountpoints[0]): if pkgutils.hasValidPackageExt(fileitem): pkg_path = os.path.join(mountpoint, fileitem) break elif pkgutils.hasValidPackageExt(item_path): pkg_ref = item_path pkg_temp = TempFile() try: repo.get_to_local_file(pkg_ref, pkg_temp.path) pkg_path = pkg_temp.path except munkirepo.RepoError, err: print_err_utf8(u'\tCan\'t download %s from repo: %s' % (pkg_ref, unicode(err))) return if pkg_path: if os.path.isdir(pkg_path): icon_paths = iconutils.extractAppIconsFromBundlePkg(pkg_path) else: icon_paths = iconutils.extractAppIconsFromFlatPkg(pkg_path) if mountpoint: dmgutils.unmountdmg(mountpoint) if len(icon_paths) == 1: icon_temp = TempFile() result = iconutils.convertIconToPNG(icon_paths[0], icon_temp.path) if result: copy_icon_to_repo( repo, install_item['name'], icon_temp.path) elif len(icon_paths) > 1: index = 1 for icon_path in icon_paths: icon_name = install_item['name'] + '_' + str(index) icon_temp = TempFile() result = iconutils.convertIconToPNG(icon_path, icon_temp.path) if result: copy_icon_to_repo(repo, icon_name, icon_temp.path) index += 1 else: print_utf8(u'\tNo application icons found.') def find_items_to_check(repo, 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.''' try: all_catalog_data = repo.get('catalogs/all') catalogitems = FoundationPlist.readPlistFromString(all_catalog_data) except (munkirepo.RepoError, FoundationPlist.FoundationPlistException), err: print_err_utf8( 'Error getting catalog data from repo: %s' % unicode(err)) return [] 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 (pkgutils.MunkiLooseVersion(catalogitem['version']) > pkgutils.MunkiLooseVersion(itemdb[name]['version'])): itemdb[name] = catalogitem pkg_list = [] for key in itemdb: pkg_list.append(itemdb[key]) return pkg_list def generate_pngs_from_munki_items(repo, force=False, itemlist=None): '''Generate PNGs from either pkgs or disk images containing applications''' itemlist = find_items_to_check(repo, itemlist=itemlist) try: icons_list = repo.itemlist('icons') except munkirepo.RepoError: icons_list = [] 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 += u'.png' if icon_name in icons_list 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_dmg_item(repo, item) elif installer_type in [None, '']: generate_pngs_from_pkg(repo, item) else: print_utf8(u'\tCan\'t process installer_type: %s' % installer_type) 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') BUNDLE_ID = 'com.googlecode.munki.munkiimport' def pref(prefname): """Return a preference. Since this uses CFPreferencesCopyAppValue, Preferences can be defined several places. Precedence is: - MCX/Configuration Profile - ~/Library/Preferences/ByHost/com.googlecode.munki.munkiimport.XX.plist - ~/Library/Preferences/com.googlecode.munki.munkiimport.plist - /Library/Preferences/com.googlecode.munki.munkiimport.plist """ return CFPreferencesCopyAppValue(prefname, BUNDLE_ID) def path2url(path): '''Converts a path to a file: url''' return urlparse.urljoin('file:', urllib.pathname2url(path)) def main(): '''Main''' usage = "usage: %prog [options] [/path/to/repo_root]" parser = OptionParser(usage=usage) parser.add_option( '--force', '-f', action='store_true', dest='force', help='Create pngs even if there is an existing icon in the repo.') parser.add_option( '--item', '-i', action='append', type='string', dest='items', help='Only run for given pkginfo item name(s).') parser.add_option('--plugin', '--plugin', default=pref('plugin'), help='Optional. Custom plugin to connect to repo.') parser.add_option('--repo_url', '--repo-url', default=pref('repo_url'), help='Optional repo fileshare URL used by repo plugin.') parser.set_defaults(force=False) options, arguments = parser.parse_args() # Make sure we have a path to work with if len(arguments): repo_path = arguments[0].rstrip("/") else: repo_path = pref('repo_path') if not options.repo_url and repo_path: options.repo_url = path2url(repo_path) if options.plugin is None: options.plugin = 'FileRepo' # Make sure the repo exists try: repo = munkirepo.connect(options.repo_url, options.plugin) except munkirepo.RepoError, err: print_err_utf8(u'Could not connect to munki repo: %s' % unicode(err)) exit(-1) # generate icons! generate_pngs_from_munki_items( repo, force=options.force, itemlist=options.items) # clean up osutils.cleanUpTmpDir() if __name__ == '__main__': main()