Files
munki/code/client/iconimporter

301 lines
11 KiB
Python
Executable File

#!/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()