Created
July 14, 2017 03:37
-
-
Save rickheil/017d9d3cd915f406d0049071062265e9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# encoding: utf8 | |
# | |
# Portions of code re-used from "makecatalogs" by Greg Neagle | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# Yo may obtain a copy of hte 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. | |
""" | |
munkilint | |
Created by Rick Heil | |
Recursively scans a directory, looking for installer item info files. | |
Runs a series of sanity checks on the found files, and reports any | |
errors. Almost all of this is to ensure that committed pkginfo files | |
conform to the standards of my org - your standards will almost certainly | |
require different checks! | |
Assumes that a pkgs, pkgsinfo, and icon directory are available in the current | |
working directory. | |
""" | |
import sys | |
import os | |
import plistlib | |
import hashlib | |
EXIT_CODE = 0 | |
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 pkglint(pkgspath, pkgsinfopath, iconspath): | |
'''Runs lint checks to ensure that pkginfo files confirm to our standards and | |
are relatively error free.''' | |
# clean slate | |
errors = [] | |
global EXIT_CODE | |
# walk through pkgsinfo files | |
for dirpath, dirnames, filenames in os.walk(pkgsinfopath): | |
for dirname in dirnames: | |
# skip hidden directories | |
if dirname.startswith('.'): | |
dirnames.remove(dirname) | |
for filename in filenames: | |
# skip hidden files | |
if filename.startswith('.'): | |
continue | |
filepath = os.path.join(dirpath, filename) | |
print "Checking %s..." % filename | |
# check pkginfo syntax by loading with plistlib - obvious syntax problems will | |
# cause an error to be thrown. | |
try: | |
pkginfo = plistlib.readPlist(filepath) | |
except IOError, inst: | |
errors.append("IO error for %s: %s" % (pkgsinfopath, inst)) | |
EXIT_CODE = -1 | |
continue | |
except TypeError, inst: | |
errors.append("Unexpected error for %s: %s" % (pkgsinfopath, inst)) | |
EXIT_CODE = -1 | |
continue | |
print " Linting %s..." % filename | |
# check there is a package name | |
if not pkginfo.get('name'): | |
errors.append("WARNING: file %s is missing name" | |
% filepath[len(pkgsinfopath)+1:]) | |
EXIT_CODE = -1 | |
continue | |
# check there is a display name | |
if not pkginfo.get('display_name'): | |
errors.append("WARNING: file %s is missing name" | |
% filepath[len(pkgsinfopath)+1:]) | |
EXIT_CODE = -1 | |
continue | |
# check admin notes are not blank | |
if not pkginfo.get('notes'): | |
errors.append("LINT ERROR: file %s is missing admin notes" | |
% filepath[len(pkgsinfopath)+1:]) | |
# check description is not blank | |
if not pkginfo.get('description'): | |
errors.append("LINT ERROR: file %s is missing description" | |
% filepath[len(pkgsinfopath)+1:]) | |
# check icon is present or available - either an icon should be specified, | |
# or the name of the pkg should be a valid icon name too. | |
if not pkginfo.get('icon_name'): | |
defaulticonpath = os.path.join(iconspath, pkginfo.get('name') + '.png') | |
if not os.path.isfile(defaulticonpath): | |
errors.append("LINT ERROR: file %s is missing an icon" | |
% filepath[len(pkgsinfopath)+1:]) | |
else: | |
# check icon exists and path does not have special characters in it. | |
pkgiconpath = os.path.join(iconspath, pkginfo.get('icon_name')) | |
if not os.path.isfile(pkgiconpath): | |
errors.append("LINT ERROR: file %s has invalid icon path specified" | |
% filepath[len(pkgsinfopath)+1:]) | |
if set('~!@#$%^&*()+{}":;\']+$').intersection(pkgiconpath): | |
errors.append("LINT ERROR: file %s has special characters in icon file path" | |
% filepath[len(pkgsinfopath)+1:]) | |
# check category is present | |
if not pkginfo.get('category'): | |
errors.append("LINT ERROR: file %s is missing a category" | |
% filepath[len(pkgsinfopath)+1:]) | |
# TODO - make this check on a whitelist of categories. | |
# check if a developer is specified | |
if not pkginfo.get('developer'): | |
errors.append("LINT ERROR: file %s is missing a developer" | |
% filepath[len(pkgsinfopath)+1:]) | |
# check at least one catalog is specified | |
if not pkginfo.get('catalogs'): | |
errors.append("LINT ERROR: file %s is not listed in any catalogs" | |
% filepath[len(pkgsinfopath)+1:]) | |
# check installer item is present | |
installer_type = pkginfo.get('installer_type') | |
do_installer_check = True | |
if installer_type in ['nopkg', 'apple_update_metadata']: | |
# don't run for nopkg or metadata types | |
do_installer_check = False | |
print " Skipping installer checks for %s" % filename | |
if do_installer_check: | |
print " Running installer checks for %s" % filename | |
installeritempath = os.path.join( | |
pkgspath, pkginfo['installer_item_location']) | |
# check if item exists at location specified in pkginfo file | |
if not os.path.isfile(installeritempath): | |
errors.append("ERROR: file %s has invalid path to installer item" | |
% filepath[len(pkgsinfopath)+1:]) | |
EXIT_CODE = -1 | |
# check if any special chars are in the path or name | |
if set('~!@#$%^&*()+{}":;\']+$').intersection(installeritempath): | |
errors.append("LINT ERROR: file %s has special characters in installer " | |
"item path" % filepath[len(pkgsinfopath)+1:]) | |
# check if hash of installer item matches the file in CI | |
print " Comparing file hashes for %s..." % os.path.basename(installeritempath) | |
blocksize = 65536 | |
hasher = hashlib.sha256() | |
try: | |
with open(installeritempath, 'rb') as installeritemfile: | |
readbuffer = installeritemfile.read(blocksize) | |
while len(readbuffer) > 0: | |
hasher.update(readbuffer) | |
readbuffer = installeritemfile.read(blocksize) | |
installeritemhash = hasher.hexdigest() | |
if installeritemhash != pkginfo.get('installer_item_hash'): | |
errors.append("ERROR: file %s has invalid installer item hash" | |
% filepath[len(pkgsinfopath)+1:]) | |
EXIT_CODE = -1 | |
except IOError, inst: | |
errors.append("ERROR: IOError reading %s" % filepath[len(pkgsinfopath)+1:]) | |
EXIT_CODE = -1 | |
# check pkginfo file path does not have special characters | |
if set('~!@#$%^&*()+{}":;\']+$').intersection(filepath): | |
errors.append("LINT ERROR: special characters in pkginfo file path for %s:\n" | |
"Pkginfo file hash: %s\n" | |
"Installer item actual hash: %s" | |
% (filepath[len(pkgsinfopath)+1:]), | |
pkginfo.get('installer_item_hash'), | |
installeritemhash) | |
# TODO: add check that pkgsinfopath and item installer path match standards. | |
if errors: | |
# group all errors together at the end for better visibility | |
for error in errors: | |
print_err_utf8(error) | |
else: | |
# print a nice thank you message | |
print "\nNo lint or pkginfo syntax errors found." | |
return | |
def manifestlint(manifestspath): | |
'''Does basic lint checks on manifest files''' | |
errors = [] | |
global EXIT_CODE | |
print "Checking manifests files..." | |
# walk through manifest files | |
for dirpath, dirnames, filenames in os.walk(manifestspath): | |
for dirname in dirnames: | |
# skip hidden folders | |
if dirname.startswith('.'): | |
dirnames.remove(dirname) | |
for filename in filenames: | |
# skip hidden files | |
if filename.startswith('.'): | |
continue | |
filepath = os.path.join(dirpath, filename) | |
# simple check for correct manifest syntax by | |
# looking if plist syntax is right. | |
try: | |
manifest = plistlib.readPlist(filepath) | |
except IOError, inst: | |
errors.append("IO error for %s: %s" % (filepath, inst)) | |
EXIT_CODE = -1 | |
continue | |
except TypeError, inst: | |
errors.append("Unexpected error for %s: %s" % (filepath, inst)) | |
EXIT_CODE = -1 | |
continue | |
except: | |
errors.append("Unknown error in %s" % filepath) | |
EXIT_CODE = 01 | |
continue | |
# Now we run some of the more cosmetic lint checks. | |
# check that there is a display name. Use len because get just checks | |
# if the tag is there! | |
if not manifest.get('display_name'): | |
# don't throw display name errors on templates | |
if "template" not in filepath: | |
errors.append("LINT ERROR: manifest %s is missing display name" | |
% filepath[len(manifestspath)+1:]) | |
# check there is one catalog set if the manifest is top level | |
if not manifest.get('catalogs'): | |
if "groups" not in filepath and "template" not in manifestspath: | |
# only throw catalog errors on top level manifests, and skip | |
# the template manifests. | |
errors.append("LINT ERROR: manifest %s has no catalogs set" | |
% filepath[len(manifestspath)+1:]) | |
if errors: | |
# group all errors together at the end for better visibility | |
for error in errors: | |
print_err_utf8(error) | |
else: | |
# print a nice thank you message | |
print "\nNo lint or manifest syntax errors found." | |
return | |
def main(): | |
'''Main''' | |
global EXIT_CODE | |
# make sure folders we need exist. | |
pkgspath = os.path.join(os.getcwd(), 'pkgs') | |
if not os.path.exists(pkgspath): | |
print_err_utf8("Error: pkgs directory does not exist!") | |
exit(-1) | |
pkgsinfopath = os.path.join(os.getcwd(), 'pkgsinfo') | |
if not os.path.exists(pkgsinfopath): | |
print_err_utf8("Error: pkgsinfo directory does not exist!") | |
exit(-1) | |
iconspath = os.path.join(os.getcwd(), 'icons') | |
if not os.path.exists(iconspath): | |
print_err_utf8("Error: icons directory does not exist!") | |
exit(-1) | |
manifestspath = os.path.join(os.getcwd(), 'manifests') | |
if not os.path.exists(manifestspath): | |
print_err_utf8("Error: manifests directory does not exist!") | |
exit(-1) | |
# run lint for items | |
pkglint(pkgspath, pkgsinfopath, iconspath) | |
# run lint for manifests | |
manifestlint(manifestspath) | |
if EXIT_CODE != 0: | |
exit(-1) | |
else: | |
exit(0) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment