mirror of
https://github.com/munki/munki.git
synced 2026-05-06 20:39:30 -05:00
munkiimport now offers to extract and upload product icons
This commit is contained in:
+182
-22
@@ -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)
|
||||
|
||||
@@ -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 []
|
||||
Reference in New Issue
Block a user