#!/usr/bin/python # encoding: utf-8 # # Copyright (c) 2013 deanishe@deanishe.net. # # MIT Licence. See http://opensource.org/licenses/MIT # # Created on 2013-11-01 # """workflow-build [options] <workflow-dir> Build Alfred Workflows. Compile contents of <workflow-dir> to a ZIP file (with extension `.alfredworkflow`). The name of the output file is generated from the workflow name, which is extracted from the workflow's `info.plist`. If a `version` file is contained within the workflow directory, it's contents will be appended to the compiled workflow's filename. Usage: workflow-build [-v|-q|-d] [-f] [-o <outputdir>] <workflow-dir>... workflow-build (-h|--version) Options: -o, --output=<outputdir> directory to save workflow(s) to default is current working directory -f, --force overwrite existing files -h, --help show this message and exit -V, --version show version number and exit -q, --quiet only show errors and above -v, --verbose show info messages and above -d, --debug show debug messages """ from __future__ import print_function from contextlib import contextmanager from fnmatch import fnmatch import logging import os import plistlib import re import shutil import string from subprocess import check_call, CalledProcessError import sys from tempfile import mkdtemp from unicodedata import normalize from docopt import docopt __version__ = "0.6" __author__ = "Dean Jackson <deanishe@deanishe.net>" DEFAULT_LOG_LEVEL = logging.WARNING # Characters permitted in workflow filenames OK_CHARS = set(string.ascii_letters + string.digits + '-.') EXCLUDE_PATTERNS = [ '.*', '*.pyc', '*.log', '*.acorn', '*.swp', '*.bak', '*.sublime-project', '*.sublime-workflow', '*.git', '*.dist-info', '*.egg-info', '__pycache__', ] log = logging.getLogger('[%(levelname)s] %(message)s') logging.basicConfig(format='', level=logging.DEBUG) @contextmanager def chdir(dirpath): """Context-manager to change working directory.""" startdir = os.path.abspath(os.curdir) os.chdir(dirpath) log.debug('cwd=%s', dirpath) yield os.chdir(startdir) log.debug('cwd=%s', startdir) @contextmanager def tempdir(): """Context-manager to create and cd to a temporary directory.""" startdir = os.path.abspath(os.curdir) dirpath = mkdtemp() try: yield dirpath finally: shutil.rmtree(dirpath) def safename(name): """Make name filesystem and web-safe.""" if isinstance(name, str): name = unicode(name, 'utf-8') # remove non-ASCII s = normalize('NFKD', name) b = s.encode('us-ascii', 'ignore') clean = [] for c in b: if c in OK_CHARS: clean.append(c) else: clean.append('-') return re.sub(r'-+', '-', ''.join(clean)).strip('-') def build_workflow(workflow_dir, outputdir, overwrite=False, verbose=False): """Create an .alfredworkflow file from the contents of `workflow_dir`.""" with tempdir() as dirpath: tmpdir = os.path.join(dirpath, 'workflow') shutil.copytree(workflow_dir, tmpdir, ignore=shutil.ignore_patterns(*EXCLUDE_PATTERNS)) with chdir(tmpdir): # ------------------------------------------------------------ # Read workflow metadata from info.plist info = plistlib.readPlist(u'info.plist') version = info.get('version') name = safename(info['name']) zippath = os.path.join(outputdir, name) if version: zippath = '{}-{}'.format(zippath, version) zippath += '.alfredworkflow' # ------------------------------------------------------------ # Remove unexported vars from info.plist for k in info.get('variablesdontexport', {}): info['variables'][k] = '' plistlib.writePlist(info, 'info.plist') # ------------------------------------------------------------ # Build workflow if os.path.exists(zippath): if overwrite: log.info('overwriting existing workflow') os.unlink(zippath) else: log.error('File "%s" exists. Use -f to overwrite', zippath) return False # build workflow command = ['zip', '-r'] if not verbose: command.append(u'-q') command.extend([zippath, '.']) log.debug('command=%r', command) try: check_call(command) except CalledProcessError as err: log.error('zip exited with %d', err.returncode) return False log.info('wrote %s', zippath) return True def main(args=None): """Run CLI.""" # ------------------------------------------------------------ # CLI flags args = docopt(__doc__, version=__version__) if args.get('--verbose'): log.setLevel(logging.INFO) elif args.get('--quiet'): log.setLevel(logging.ERROR) elif args.get('--debug'): log.setLevel(logging.DEBUG) else: log.setLevel(DEFAULT_LOG_LEVEL) log.debug('log level=%s', logging.getLevelName(log.level)) log.debug('args=%r', args) # Build options force = args['--force'] outputdir = os.path.abspath(args['--output'] or os.curdir) workflow_dirs = [os.path.abspath(p) for p in args['<workflow-dir>']] verbose = log.level == logging.DEBUG log.debug(u'outputdir=%r, workflow_dirs=%r', outputdir, workflow_dirs) # ------------------------------------------------------------ # Build workflow(s) errors = False for path in workflow_dirs: ok = build_workflow(path, outputdir, force, verbose) if not ok: errors = True if errors: return 1 return 0 if __name__ == '__main__': sys.exit(main(sys.argv[1:]))