Skip to content

Instantly share code, notes, and snippets.

@deanishe
Last active September 17, 2024 06:27
Show Gist options
  • Save deanishe/b16f018119ef3fe951af to your computer and use it in GitHub Desktop.
Save deanishe/b16f018119ef3fe951af to your computer and use it in GitHub Desktop.
Build Alfred Workflows into .alfredworkflow (zip) files
#!/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:]))
@muyexi
Copy link

muyexi commented Mar 12, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment