-
-
Save deanishe/b16f018119ef3fe951af to your computer and use it in GitHub Desktop.
#!/usr/bin/python | |
# encoding: utf-8 | |
# | |
# Copyright (c) 2013 [email protected]. | |
# | |
# 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 <[email protected]>" | |
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:])) |
Perhaps it would be useful to change all the patterns that way?
I've changed the way excludes are handled. It should work much better now.
Might be too tired to see what's up right now, but smacking into this which seems close to what you were just editing:
DEBUG - [.*] .idea
DEBUG - [.*] .git
DEBUG - [*.git] .git
Traceback (most recent call last):
File "./workflow-build.py", line 326, in <module>
sys.exit(main(sys.argv[1:]))
File "./workflow-build.py", line 315, in main
ok = build_workflow(path, outputdir, force, verbose, dry_run)
File "./workflow-build.py", line 241, in build_workflow
wffiles = get_workflow_files('.')
File "./workflow-build.py", line 197, in get_workflow_files
del dirnames[i]
IndexError: list assignment index out of range
@chrisbro, that looks like the same problem I am having.
My .git
directory is getting matched by both the .*
and the .git
exclude patterns.
This removes 2 entries from the list of directories and causing one of my library directories to be missed.
@deanishe
Just need to add a break after line #209 the del dirnems[i]
and seems to work fine now
https://gist.github.com/duanemay/6663ae0a429bc0f78a0ab16b8065a47b
@deanishe this is a great script! I noticed it was using python 2 functions (unicode
, pslistlib.readPlist
, etc). I'm happy to update it, myself, but I figured I'd ask if you had a py3 version handy before I do that.
Thanks!
Actually, the changes were just a couple of lines:
diff --git a/bin/workflow-build b/bin/workflow-build
index 9c7b24c..dc76468 100755
--- a/bin/workflow-build
+++ b/bin/workflow-build
@@ -37,7 +37,6 @@ Options:
from __future__ import print_function
from contextlib import contextmanager
-from fnmatch import fnmatch
import logging
import os
import plistlib
@@ -104,8 +103,6 @@ def tempdir():
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)
@@ -113,8 +110,9 @@ def safename(name):
clean = []
for c in b:
- if c in OK_CHARS:
- clean.append(c)
+ char = chr(c)
+ if char in OK_CHARS:
+ clean.append(char)
else:
clean.append("-")
@@ -132,7 +130,8 @@ def build_workflow(workflow_dir, outputdir, overwrite=False, verbose=False):
with chdir(tmpdir):
# ------------------------------------------------------------
# Read workflow metadata from info.plist
- info = plistlib.readPlist(u"info.plist")
+ with open("info.plist", "rb") as fp:
+ info = plistlib.load(fp)
version = info.get("version")
name = safename(info["name"])
zippath = os.path.join(outputdir, name)
@@ -147,7 +146,8 @@ def build_workflow(workflow_dir, outputdir, overwrite=False, verbose=False):
for k in info.get("variablesdontexport", {}):
info["variables"][k] = ""
- plistlib.writePlist(info, "info.plist")
+ with open("info.plist", "wb") as fp:
+ plistlib.dump(info, fp)
# ------------------------------------------------------------
# Build workflow
Updated for Python 3 with code from @xavdid :
https://gist.github.com/muyexi/3601a76d96bdcfa90eb9b8bf3910cd2a
Maybe it's just me, but trying
./workflow-build.py -d workflow-dir
shows that it is actually zipping the .git file. In the exclude patterns, I changed'*.git'
to'*.git*'
and it correctly excluded the file. Perhaps it would be useful to change all the patterns that way?