munkiimport now offers to extract and upload product icons

This commit is contained in:
Greg Neagle
2014-05-21 16:15:38 -07:00
parent 7b6d99c368
commit b390c7f46f
2 changed files with 406 additions and 22 deletions
+182 -22
View File
@@ -30,6 +30,7 @@ import subprocess
import time
from optparse import OptionParser, BadOptionError, AmbiguousOptionError
from munkilib import iconutils
from munkilib import munkicommon
from munkilib import FoundationPlist
@@ -216,6 +217,142 @@ def copyItemToRepo(itempath, vers, subdirectory=''):
return os.path.join(subdirectory, item_name)
def iconExistsInRepo(pkginfo):
"""Returns True if there is an icon for this item in the repo"""
icon_name = pkginfo.get('icon_name') or pkginfo['name']
if not os.path.splitext(icon_name)[1]:
icon_name += u'.png'
icon_path = os.path.join(REPO_PATH, u'icons', icon_name)
if os.path.exists(icon_path):
return True
return False
def generate_png_from_copy_from_dmg_item(dmg_path, pkginfo):
'''Generates a product icon from a copy_from_dmg item
and uploads to the repo'''
destination_path = os.path.join(REPO_PATH, 'icons')
if not os.path.exists(destination_path):
try:
os.makedirs(destination_path)
except OSError, errmsg:
print >> sys.stderr, ('Could not create %s: %s' %
(destination_path, errmsg))
mountpoints = munkicommon.mountdmg(dmg_path)
if mountpoints:
mountpoint = mountpoints[0]
apps = [item for item in pkginfo.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:
png_path = os.path.join(
destination_path, pkginfo['name'] + u'.png')
result = iconutils.convertIconToPNG(icon_path, png_path)
if result:
print 'Created icon: %s' % png_path
else:
print >> sys.stderr, (
u'\tError converting %s to png.' % icon_path)
else:
print 'No application icons found.'
else:
print 'No application icons found.'
munkicommon.unmountdmg(mountpoint)
def generate_pngs_from_installer_pkg(item_path, pkginfo):
'''Generates a product icon (or candidate icons) from
an installer pkg and uploads to the repo'''
destination_path = os.path.join(REPO_PATH, 'icons')
if not os.path.exists(destination_path):
try:
os.makedirs(destination_path)
except OSError, errmsg:
print >> sys.stderr, ('Could not create %s: %s' %
(destination_path, errmsg))
icon_paths = []
mountpoint = None
pkg_path = None
if munkicommon.hasValidDiskImageExt(item_path):
dmg_path = item_path
mountpoints = munkicommon.mountdmg(dmg_path)
if mountpoints:
mountpoint = mountpoints[0]
if pkginfo.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 = iconutils.extractAppIconsFromBundlePkg(pkg_path)
else:
icon_paths = iconutils.extractAppIconsFromFlatPkg(pkg_path)
if mountpoint:
munkicommon.unmountdmg(mountpoint)
if len(icon_paths) == 1:
png_path = os.path.join(
destination_path, pkginfo['name'] + u'.png')
result = convertIconToPNG(icon_paths[0], png_path)
if result:
print 'Created icon: %s' % png_path
else:
print >> sys.stderr, u'Error converting %s to png.' % icon_paths[0]
elif len(icon_paths) > 1:
index = 1
for icon_path in icon_paths:
png_path = os.path.join(
destination_path,
pkginfo['name'] + '_' + str(index) + u'.png')
result = iconutils.convertIconToPNG(icon_path, png_path)
if result:
print 'Created icon: %s' % png_path
else:
print >> sys.stderr, u'Error converting %s to png.' % icon_path
index += 1
else:
print 'No application icons found.'
def copyIconToRepo(iconpath):
"""Saves a product icon to the repo"""
destination_path = os.path.join(REPO_PATH, 'icons')
if not os.path.exists(destination_path):
try:
os.makedirs(destination_path)
except OSError, errmsg:
raise RepoCopyError('Could not create %s: %s' %
(destination_path, errmsg))
icon_name = os.path.basename(iconpath)
destination_path_name = os.path.join(destination_path, icon_name)
if os.path.exists(destination_path_name):
# remove any existing icon in the repo
try:
os.unlink(destination_path_name)
except OSError, errmsg:
raise RepoCopyError('Could not remove existing %s' %
(destination_path_name))
print 'Copying %s to %s...' % (icon_name, destination_path_name)
cmd = ['/bin/cp', iconpath, destination_path_name]
retcode = subprocess.call(cmd)
if retcode:
raise RepoCopyError('Unable to copy %s to %s' %
(iconpath, destination_path_name))
def copyPkginfoToRepo(pkginfo, subdirectory=''):
"""Saves pkginfo to munki_repo_path/pkgsinfo/subdirectory"""
# less error checking because we copy the installer_item
@@ -612,17 +749,17 @@ def main():
help='Print the version of the munki tools and exit.')
parser.add_option('--verbose', '-v', action='store_true',
help='Print more output.')
options, arguments = parser.parse_args()
if options.version:
print munkicommon.get_version()
exit(0)
if options.configure:
configure()
exit(0)
NOINTERACTIVE = options.nointeractive
VERBOSE = options.verbose
REPO_PATH = pref('repo_path')
@@ -630,21 +767,21 @@ def main():
if options.repo_path:
if not os.path.exists(options.repo_path) and not options.repo_url:
print >> sys.stderr, ('Munki repo path override provided but '
'folder does not exist. Please either '
'provide --repo_url if you wish to map a '
'share, or correct the path and try again.')
print >> sys.stderr, (
'Munki repo path override provided but folder does not exist. '
'Please either provide --repo_url if you wish to connect to a'
'file share, or correct the path and try again.')
exit(-1)
REPO_PATH = options.repo_path
if options.repo_url:
REPO_URL = options.repo_url
if len(arguments) == 0:
parser.print_usage()
exit(0)
if '--apple-update' in arguments:
APPLEMETADATA = True
# Verify that arguments, presumed to be for
@@ -652,14 +789,14 @@ def main():
installer_item = makePkgInfo(options=arguments, test_mode=True)
if not installer_item and not APPLEMETADATA:
cleanupAndExit(-1)
if not APPLEMETADATA:
# Remove the installer_item from arguments
arguments.remove(installer_item)
# Strip trailing '/' from installer_item
installer_item = installer_item.rstrip('/')
# Check if the item is a mount point for a disk image
if munkicommon.pathIsVolumeMountPoint(installer_item):
# Get the disk image path for the mount point
@@ -713,7 +850,7 @@ def main():
if not '--catalog' in arguments and not '-c' in arguments:
default_catalog = pref('default_catalog') or 'testing'
arguments.extend(['--catalog', default_catalog])
pkginfo = makePkgInfo(arguments, False)
pkginfo = makePkgInfo(arguments, test_mode=False)
if not pkginfo:
# makepkginfo returned an error
print >> sys.stderr, 'Getting package info failed.'
@@ -792,7 +929,7 @@ def main():
newvalue = raw_input_with_default(prompt, default)
pkginfo['catalogs'] = [item.strip()
for item in newvalue.split(',')]
if not APPLEMETADATA:
if 'receipts' not in pkginfo and 'installs' not in pkginfo:
print >> sys.stderr, ('WARNING: There are no receipts and no '
@@ -800,7 +937,7 @@ def main():
'item. You will need to add at least '
'one item to the \'installs\' list.')
#TO-DO: provide a way to add 'installs' items right here
print
for (name, key) in editfields:
print '%15s: %s' % (name, pkginfo.get(key,'').encode('UTF-8'))
@@ -810,7 +947,7 @@ def main():
answer = raw_input('Import this item? [y/n] ')
if not answer.lower().startswith('y'):
cleanupAndExit(0)
if options.subdirectory == '':
pkgs_path = os.path.join(REPO_PATH, 'pkgs')
if installer_item.startswith(pkgs_path) and not APPLEMETADATA:
@@ -821,11 +958,27 @@ def main():
installer_item_dirpath[len(pkgs_path)+1:]
options.subdirectory = promptForSubdirectory(
options.subdirectory)
if not iconExistsInRepo(pkginfo):
print 'No existing product icon found.'
answer = raw_input('Attempt to create a product icon? [y/n] ')
if answer.lower().startswith('y'):
print 'Attempting to extract and upload icon...'
installer_type = pkginfo.get('installer_type')
if installer_type == 'copy_from_dmg':
generate_png_from_copy_from_dmg_item(
installer_item, pkginfo)
elif installer_type in [None, '']:
generate_pngs_from_installer_pkg(installer_item, pkginfo)
else:
print >> sys.stderr, (
'Can\'t generate icons from installer_type: %s.'
% installer_type)
# fix in case user accidentally starts subdirectory with a slash
if options.subdirectory.startswith('/'):
options.subdirectory = options.subdirectory[1:]
if not APPLEMETADATA:
try:
uploaded_pkgpath = copyItemToRepo(installer_item,
@@ -834,18 +987,25 @@ def main():
except RepoCopyError, errmsg:
print >> sys.stderr, errmsg
cleanupAndExit(-1)
# adjust the installer_item_location to match
# the actual location and name
pkginfo['installer_item_location'] = uploaded_pkgpath
# if we have an icon, upload it
if False: #options.iconpath:
try:
copyIconToRepo(options.iconpath)
except RepoCopyError, errmsg:
print >> sys.stderr, errmsg
# installer_item upload was successful, so upload pkginfo to repo
try:
pkginfo_path = copyPkginfoToRepo(pkginfo, options.subdirectory)
except RepoCopyError, errmsg:
print >> sys.stderr, errmsg
cleanupAndExit(-1)
if not options.nointeractive:
# open the pkginfo file in the user's editor
openPkginfoInEditor(pkginfo_path)
+224
View File
@@ -0,0 +1,224 @@
#!/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.
"""
iconutils
Created by Greg Neagle on 2014-05-15.
Functions to work with product images ('icons') for Managed Software Center
"""
import glob
import sys
import os
import subprocess
import tempfile
import shutil
from Foundation import NSData
from AppKit import NSBitmapImageRep, NSPNGFileType
import munkicommon
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, 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']
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()
if proc.returncode:
print_err_utf8(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, errors) = proc.communicate()
if proc.returncode:
print_err_utf8(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.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:
print_err_utf8(
u'pax could not read files from %s' % archive_path)
return []
else:
print_err_utf8(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):
'''Returns a dict with pkg paths as keys and filename lists
as values'''
pkg_dict = {}
bomfile = os.path.join(pkg_path, u'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, u'Contents')
if os.path.isdir(pkg_contents_dir):
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)
else:
pkgs = []
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):
'''Returns a list of paths for application icons found
inside the bundle pkg at pkg_path'''
pkg_dict = findInfoPlistPathsInBundlePkg(pkg_path)
icon_paths = []
exporttmp = tempfile.mkdtemp(dir='/tmp')
for pkg in pkg_dict:
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 getAppInfoPathsFromBundleComponentPkg(pkg_path):
'''Returns a list of paths to application Info.plists'''
bomfile = os.path.join(pkg_path, u'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 []