Skip to content

Instantly share code, notes, and snippets.

@homebysix
Last active January 25, 2019 19:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save homebysix/382a56f4d7c3429dd92f8da9ff590ab3 to your computer and use it in GitHub Desktop.
Save homebysix/382a56f4d7c3429dd92f8da9ff590ab3 to your computer and use it in GitHub Desktop.
MunkiPkg linting
variables:
TZ: America/Los_Angeles
munkipkg_linting:
script: python munkipkg_linting.py
#!/usr/bin/python
import logging
import os
import plistlib
import json
import sys
from xml.parsers.expat import ExpatError
from glob import glob
from distutils.version import StrictVersion
try:
import yaml
YAML_INSTALLED = True
except ImportError:
YAML_INSTALLED = False
BUILD_INFO_NAMES = ('build-info.plist', 'build-info.json', 'build-info.yaml', 'build-info.yml')
# Object for handling log output.
logging.basicConfig(stream=sys.stdout,
format='%(asctime)s [%(levelname)s] %(message)s',
level=logging.INFO)
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def test_executables(status, project):
'''Ensures that all outset scripts included in MunkiPkg projects are executable.'''
for script in glob(project + '/payload/usr/local/outset/*/*'):
if not os.access(script, os.X_OK):
logger.error('%s is an outset script, but is not set to executable.', script)
status = False
return status
def test_gitignore(status, project):
'''Ensures that all MunkiPkg projects contain a .gitignore file.'''
if not os.path.isfile(os.path.join(project, '.gitignore')):
logger.error('%s does not contain a .gitignore file.', project)
status = False
return status
def test_build_info(status, project):
'''Ensures that build-info files are valid, and returns dict of contents for further testing.'''
info_dict = None
for info_path in (os.path.join(project, info_name) for info_name in BUILD_INFO_NAMES):
if os.path.isfile(info_path):
try:
if info_path.endswith('.plist'):
info_dict = plistlib.readPlist(info_path)
break
elif info_path.endswith('.json'):
with open(info_path, 'r') as openfile:
info_dict = json.load(openfile)
break
elif info_path.endswith(('.yaml', '.yml')):
if YAML_INSTALLED:
with open(info_path, 'r') as openfile:
info_dict = yaml.load(openfile)
break
else:
logger.warning('Cannot import yaml, skipping build info: %s', info_path)
except (ExpatError, ValueError, yaml.scanner.ScannerError) as err:
logger.error('%s is not a valid %s file:\n'
'%s', info_path, info_path.split('.')[-1], err)
return status, info_dict
def test_bundle_id(status, project, info_dict):
'''Ensures that all MunkiPkg projects use company-specific bundle identifiers.'''
required_prefix = 'com.example.'
if not 'identifier' in info_dict:
logger.error('The build-info file for %s does not have a bundle identifier.', project)
status = False
elif not info_dict['identifier'].startswith(required_prefix):
logger.error('The bundle identifier for %s does not start with %s.', project, required_prefix)
status = False
return status
def test_version(status, project, info_dict):
'''Ensures that all MunkiPkg projects have parseable versions.'''
if not 'version' in info_dict:
logger.error('The build-info file for %s does not have a version.', project)
status = False
else:
try:
version = StrictVersion(info_dict['version'])
if version < StrictVersion('1.0'):
logger.error('The version for %s is less than 1.0: %s', project, version)
status = False
except ValueError:
logger.error('The version for %s is not valid: %s', project, info_dict['version'])
status = False
except TypeError:
logger.error('The version for %s is stored as a %s, but should be a string: '
'%s', project, type(info_dict['version']), info_dict['version'])
status = False
return status
def test_name(status, project, info_dict):
'''Ensures that all MunkiPkg projects have parseable versions.'''
if not 'name' in info_dict:
logger.error('The build-info file for %s does not have a name.', project)
status = False
elif '-${version}' not in info_dict['name']:
logger.error('The name for %s does not contain a version variable: '
'%s', project, info_dict['name'])
status = False
elif not info_dict['name'].endswith('.pkg'):
logger.error('The name for %s does not end with .pkg: '
'%s', project, info_dict['name'])
status = False
return status
def main():
'''Main process.'''
status = True
# Extensions to include in linting.
munkipkg_project_dirs = []
# Gather list of eligible files.
for root, dirs, files in os.walk('.'):
dirs[:] = [d for d in dirs if not d.startswith('.')]
if any(build_info in files for build_info in BUILD_INFO_NAMES):
munkipkg_project_dirs.append(root)
# Process files.
for project in munkipkg_project_dirs:
status = test_executables(status, project)
status = test_gitignore(status, project)
status, info_dict = test_build_info(status, project)
status = test_bundle_id(status, project, info_dict)
status = test_version(status, project, info_dict)
status = test_name(status, project, info_dict)
if status:
logger.info('Tested %d munkipkg projects. All tests passed.', len(munkipkg_project_dirs))
else:
logger.error('Tested %d munkipkg projects. Some tests failed. See details above.', len(munkipkg_project_dirs))
sys.exit(1)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment