mirror of
https://github.com/munki/munki.git
synced 2026-01-01 12:10:24 -06:00
419 lines
15 KiB
Python
Executable File
419 lines
15 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
#
|
|
# Copyright 2009-2016 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.
|
|
"""
|
|
makecatalogs
|
|
|
|
Created by Greg Neagle on 2009-03-30.
|
|
|
|
Recursively scans a directory, looking for installer item info files.
|
|
Builds a repo catalog from these files.
|
|
|
|
Assumes a pkgsinfo directory under repopath.
|
|
User calling this needs to be able to write to repo/catalogs.
|
|
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import optparse
|
|
import hashlib
|
|
|
|
try:
|
|
from munkilib import FoundationPlist as plistlib
|
|
LOCAL_PREFS_SUPPORT = True
|
|
except ImportError:
|
|
try:
|
|
import FoundationPlist as plistlib
|
|
LOCAL_PREFS_SUPPORT = True
|
|
except ImportError:
|
|
# maybe we're not on an OS X machine...
|
|
print >> sys.stderr, ("WARNING: FoundationPlist is not available, "
|
|
"using plistlib instead.")
|
|
import plistlib
|
|
LOCAL_PREFS_SUPPORT = False
|
|
|
|
try:
|
|
from munkilib.munkicommon import listdir, get_version
|
|
except ImportError:
|
|
# munkilib is not available
|
|
def listdir(path):
|
|
"""OS X HFS+ string encoding safe listdir().
|
|
|
|
Args:
|
|
path: path to list contents of
|
|
Returns:
|
|
list of contents, items as str or unicode types
|
|
"""
|
|
# if os.listdir() is supplied a unicode object for the path,
|
|
# it will return unicode filenames instead of their raw fs-dependent
|
|
# version, which is decomposed utf-8 on OS X.
|
|
#
|
|
# we use this to our advantage here and have Python do the decoding
|
|
# work for us, instead of decoding each item in the output list.
|
|
#
|
|
# references:
|
|
# https://docs.python.org/howto/unicode.html#unicode-filenames
|
|
# https://developer.apple.com/library/mac/#qa/qa2001/qa1235.html
|
|
# http://lists.zerezo.com/git/msg643117.html
|
|
# http://unicode.org/reports/tr15/ section 1.2
|
|
if type(path) is str:
|
|
path = unicode(path, 'utf-8')
|
|
elif type(path) is not unicode:
|
|
path = unicode(path)
|
|
return os.listdir(path)
|
|
|
|
def get_version():
|
|
'''Placeholder if munkilib is not available'''
|
|
return 'UNKNOWN'
|
|
|
|
|
|
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 makecatalogs(repopath, options):
|
|
'''Assembles all pkginfo files into catalogs.
|
|
Assumes a pkgsinfo directory under repopath.
|
|
User calling this needs to be able to write to the repo/catalogs
|
|
directory.'''
|
|
|
|
# start with no errors!
|
|
errors = []
|
|
exit_code = 0
|
|
|
|
# Make sure the icons directory exists
|
|
iconspath = os.path.join(repopath, 'icons')
|
|
# make sure iconspath is Unicode so that os.walk later gives us
|
|
# Unicode names back.
|
|
if type(iconspath) is str:
|
|
iconspath = unicode(iconspath, 'utf-8')
|
|
elif type(iconspath) is not unicode:
|
|
iconspath = unicode(iconspath)
|
|
|
|
if not os.path.exists(iconspath):
|
|
print_err_utf8("icons path %s doesn't exist, skipping hashing!"
|
|
% iconspath)
|
|
iconhashing = False
|
|
# exit(-1) don't exit so we don't break when no icons dir
|
|
else:
|
|
icons = {}
|
|
iconhashing = True
|
|
|
|
# Walk through the icon files
|
|
for dirpath, dirnames, filenames in os.walk(iconspath):
|
|
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)
|
|
|
|
iconpath = filepath.rsplit(iconspath + '/', 1)[1]
|
|
|
|
# Try to read the icon file
|
|
try:
|
|
print_utf8("Hashing %s..." % (iconpath))
|
|
icons[iconpath] = (
|
|
hashlib.sha256(open(filepath, 'rb').read()).hexdigest())
|
|
except IOError, inst:
|
|
errors.append("IO error for %s: %s" % (filepath, inst))
|
|
exit_code = -1
|
|
continue
|
|
except BaseException, inst:
|
|
errors.append("Unexpected error for %s: %s"
|
|
% (filepath, inst))
|
|
exit_code = -1
|
|
continue
|
|
|
|
# Make sure the pkgsinfo directory exists
|
|
pkgsinfopath = os.path.join(repopath, 'pkgsinfo')
|
|
# make sure pkgsinfopath is Unicode so that os.walk later gives us
|
|
# Unicode names back.
|
|
if type(pkgsinfopath) is str:
|
|
pkgsinfopath = unicode(pkgsinfopath, 'utf-8')
|
|
elif type(pkgsinfopath) is not unicode:
|
|
pkgsinfopath = unicode(pkgsinfopath)
|
|
|
|
if not os.path.exists(pkgsinfopath):
|
|
print_err_utf8("pkgsinfo path %s doesn't exist!" % pkgsinfopath)
|
|
exit(-1)
|
|
|
|
# start with empty catalogs dict
|
|
catalogs = {}
|
|
catalogs['all'] = []
|
|
|
|
# Walk through the pkginfo files
|
|
for dirpath, dirnames, filenames in os.walk(pkgsinfopath, followlinks=True):
|
|
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 pkginfo file
|
|
try:
|
|
pkginfo = plistlib.readPlist(filepath)
|
|
except IOError, inst:
|
|
errors.append("IO error for %s: %s" % (filepath, inst))
|
|
exit_code = -1
|
|
continue
|
|
except BaseException, inst:
|
|
errors.append("Unexpected error for %s: %s" % (filepath, inst))
|
|
exit_code = -1
|
|
continue
|
|
|
|
if not 'name' in pkginfo:
|
|
errors.append(
|
|
"WARNING: file %s is missing name"
|
|
% filepath[len(pkgsinfopath)+1:])
|
|
continue
|
|
|
|
# don't copy admin notes to catalogs.
|
|
if pkginfo.get('notes'):
|
|
del pkginfo['notes']
|
|
# strip out any keys that start with "_"
|
|
# (example: pkginfo _metadata)
|
|
for key in pkginfo.keys():
|
|
if key.startswith('_'):
|
|
del pkginfo[key]
|
|
|
|
if iconhashing:
|
|
|
|
name = pkginfo.get('name')
|
|
|
|
if pkginfo.get('icon_name'):
|
|
iconhash = (icons.get(pkginfo['icon_name']) or
|
|
icons.get(pkginfo['icon_name'] + '.png'))
|
|
if not iconhash:
|
|
errors.append("WARNING: icon_name specified"
|
|
" in info file %s but it does not exist"
|
|
% filepath[len(pkgsinfopath) + 1:])
|
|
else:
|
|
iconhash = icons.get(name + '.png')
|
|
|
|
if iconhash is not None:
|
|
pkginfo['icon_hash'] = iconhash
|
|
iconhash = None
|
|
|
|
#simple sanity checking
|
|
do_pkg_check = True
|
|
installer_type = pkginfo.get('installer_type')
|
|
if installer_type in ['nopkg', 'apple_update_metadata']:
|
|
do_pkg_check = False
|
|
if pkginfo.get('PackageCompleteURL'):
|
|
do_pkg_check = False
|
|
if pkginfo.get('PackageURL'):
|
|
do_pkg_check = False
|
|
|
|
|
|
if do_pkg_check:
|
|
if not 'installer_item_location' in pkginfo:
|
|
errors.append(
|
|
"WARNING: file %s is missing installer_item_location"
|
|
% filepath[len(pkgsinfopath)+1:])
|
|
# Skip this pkginfo unless we're running with force flag
|
|
if not options.force:
|
|
exit_code = -1
|
|
continue
|
|
|
|
# Try to form a path and fail if the
|
|
# installer_item_location is not a valid type
|
|
try:
|
|
installeritempath = os.path.join(
|
|
repopath, "pkgs", pkginfo['installer_item_location'])
|
|
except TypeError:
|
|
errors.append("WARNING: invalid installer_item_location "
|
|
"in info file %s"
|
|
% filepath[len(pkgsinfopath)+1:])
|
|
exit_code = -1
|
|
continue
|
|
|
|
# Check if the installer item actually exists
|
|
if not os.path.exists(installeritempath):
|
|
errors.append("WARNING: Info file %s refers to "
|
|
"missing installer item: %s" %
|
|
(filepath[len(pkgsinfopath)+1:],
|
|
pkginfo['installer_item_location']))
|
|
# Skip this pkginfo unless we're running with force flag
|
|
if not options.force:
|
|
exit_code = -1
|
|
continue
|
|
|
|
#uninstaller sanity checking
|
|
uninstaller_type = pkginfo.get('uninstall_method')
|
|
if uninstaller_type in ['AdobeCCPUninstaller']:
|
|
# uninstaller_item_location is required
|
|
if not 'uninstaller_item_location' in pkginfo:
|
|
errors.append(
|
|
"WARNING: file %s is missing uninstaller_item_location"
|
|
% filepath[len(pkgsinfopath)+1:])
|
|
# Skip this pkginfo unless we're running with force flag
|
|
if not options.force:
|
|
exit_code = -1
|
|
continue
|
|
|
|
# if an uninstaller_item_location is specified, sanity-check it
|
|
if 'uninstaller_item_location' in pkginfo:
|
|
try:
|
|
uninstalleritempath = os.path.join(
|
|
repopath, "pkgs", pkginfo['uninstaller_item_location'])
|
|
except TypeError:
|
|
errors.append("WARNING: invalid uninstaller_item_location "
|
|
"in info file %s"
|
|
% filepath[len(pkgsinfopath)+1:])
|
|
exit_code = -1
|
|
continue
|
|
|
|
# Check if the uninstaller item actually exists
|
|
if not os.path.exists(uninstalleritempath):
|
|
errors.append("WARNING: Info file %s refers to "
|
|
"missing uninstaller item: %s" %
|
|
(filepath[len(pkgsinfopath)+1:],
|
|
pkginfo['uninstaller_item_location']))
|
|
# Skip this pkginfo unless we're running with force flag
|
|
if not options.force:
|
|
exit_code = -1
|
|
continue
|
|
|
|
catalogs['all'].append(pkginfo)
|
|
for catalogname in pkginfo.get("catalogs", []):
|
|
infofilename = filepath[len(pkgsinfopath)+1:]
|
|
if not catalogname:
|
|
errors.append("WARNING: Info file %s has an empty "
|
|
"catalog name!" % infofilename)
|
|
exit_code = -1
|
|
continue
|
|
if not catalogname in catalogs:
|
|
catalogs[catalogname] = []
|
|
catalogs[catalogname].append(pkginfo)
|
|
print_utf8("Adding %s to %s..." % (infofilename, catalogname))
|
|
|
|
if errors:
|
|
# group all errors at the end for better visibility
|
|
print
|
|
for error in errors:
|
|
print_err_utf8(error)
|
|
|
|
# clear out old catalogs
|
|
catalogpath = os.path.join(repopath, "catalogs")
|
|
if not os.path.exists(catalogpath):
|
|
os.mkdir(catalogpath)
|
|
else:
|
|
for item in listdir(catalogpath):
|
|
itempath = os.path.join(catalogpath, item)
|
|
if os.path.isfile(itempath):
|
|
os.remove(itempath)
|
|
|
|
# write the new catalogs
|
|
print
|
|
for key in catalogs.keys():
|
|
catalogpath = os.path.join(repopath, "catalogs", key)
|
|
if os.path.exists(catalogpath):
|
|
print_err_utf8(
|
|
"WARNING: catalog %s already exists at %s. "
|
|
"Perhaps this is a non-case sensitive filesystem and you "
|
|
"have catalogs with names differing only in case?"
|
|
% (key, catalogpath))
|
|
exit_code = -1
|
|
elif len(catalogs[key]) != 0:
|
|
plistlib.writePlist(catalogs[key], catalogpath)
|
|
print "Created catalog %s..." % (catalogpath)
|
|
else:
|
|
print_err_utf8(
|
|
"WARNING: Did not create catalog %s "
|
|
"because it is empty "
|
|
% (key))
|
|
exit_code = -1
|
|
|
|
# Exit with "exit_code" if we got this far.
|
|
# This will be -1 if there were any errors
|
|
# that prevented the catalogs to be written.
|
|
exit(exit_code)
|
|
|
|
|
|
def pref(prefname):
|
|
"""Returns a preference for prefname"""
|
|
if not LOCAL_PREFS_SUPPORT:
|
|
return None
|
|
try:
|
|
_prefs = plistlib.readPlist(PREFSPATH)
|
|
except BaseException:
|
|
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]"
|
|
parser = optparse.OptionParser(usage=usage)
|
|
parser.add_option('--version', '-V', action='store_true',
|
|
help='Print the version of the munki tools and exit.')
|
|
parser.add_option('--force', '-f', action='store_true', dest='force',
|
|
help='Disable sanity checks.')
|
|
parser.set_defaults(force=False)
|
|
options, arguments = parser.parse_args()
|
|
|
|
if options.version:
|
|
print get_version()
|
|
exit(0)
|
|
|
|
# Make sure we have a path to work with
|
|
repopath = None
|
|
if len(arguments) == 0:
|
|
repopath = pref('repo_path')
|
|
if not repopath:
|
|
print_err_utf8("Need to specify a path to the repo root!")
|
|
exit(-1)
|
|
else:
|
|
print_utf8("Using repo path: %s" % repopath)
|
|
else:
|
|
repopath = arguments[0].rstrip("/")
|
|
|
|
# Make sure the repo path exists
|
|
if not os.path.exists(repopath):
|
|
print_err_utf8("Repo root path %s doesn't exist!" % repopath)
|
|
exit(-1)
|
|
|
|
# Make the catalogs
|
|
makecatalogs(repopath, options)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|