Last active
September 17, 2024 06:27
-
-
Save deanishe/b16f018119ef3fe951af to your computer and use it in GitHub Desktop.
Build Alfred Workflows into .alfredworkflow (zip) files
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: 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:])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated for Python 3 with code from @xavdid :
https://gist.github.com/muyexi/3601a76d96bdcfa90eb9b8bf3910cd2a