Snakefile 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. # -*- coding: utf-8; -*-
  2. import locale
  3. import os.path
  4. import regex
  5. import urllib.parse
  6. import os.path
  7. from collections import Iterable, Mapping # in Python 3 use from collections.abc
  8. from distutils.spawn import find_executable
  9. from fnmatch import fnmatch
  10. from subprocess import check_output, check_call
  11. from tempfile import NamedTemporaryFile
  12. from TexSoup import TexSoup
  13. try:
  14. from os import scandir, walk
  15. except ImportError:
  16. from scandir import scandir, walk
  17. def unnest(*args):
  18. '''Un-nest list- and tuple-like elements in arguments.
  19. "List-like" means anything with a len() and whose elments can be
  20. accessed with numeric indexing, except for string-like elements. It
  21. must also be an instance of the collections.Iterable abstract class.
  22. Dict-like elements and iterators/generators are not affected.
  23. This function always returns a list, even if it is passed a single
  24. scalar argument.
  25. '''
  26. result = []
  27. for arg in args:
  28. if isinstance(arg, str):
  29. # String
  30. result.append(arg)
  31. elif isinstance(arg, Mapping):
  32. # Dict-like
  33. result.append(arg)
  34. elif isinstance(arg, Iterable):
  35. try:
  36. # Duck-typing test for list-ness (a stricter condition
  37. # than just "iterable")
  38. for i in range(len(arg)):
  39. result.append(arg[i])
  40. except TypeError:
  41. # Iterable but not list-like
  42. result.append(arg)
  43. else:
  44. # Not iterable
  45. result.append(arg)
  46. return result
  47. def check_output_decode(*args, encoding=locale.getpreferredencoding(), **kwargs):
  48. '''Shortcut for check.output + str.decode'''
  49. return check_output(*args, **kwargs).decode(encoding)
  50. def find_mac_app(name):
  51. try:
  52. return check_output_decode(
  53. ['mdfind',
  54. 'kMDItemDisplayName=={name}&&kMDItemKind==Application'.format(name=name)]).split('\n')[0]
  55. except Exception:
  56. return None
  57. def glob_recursive(pattern, top='.', include_hidden=False, *args, **kwargs):
  58. '''Combination of glob.glob and os.walk.
  59. Reutrns the relative path to every file or directory matching the
  60. pattern anywhere in the specified directory hierarchy. Defaults to the
  61. current working directory. Any additional arguments are passed to
  62. os.walk.'''
  63. for (path, dirs, files) in walk(top, *args, **kwargs):
  64. for f in dirs + files:
  65. if include_hidden or f.startswith('.'):
  66. continue
  67. if fnmatch(f, pattern):
  68. yield os.path.normpath(os.path.join(path, f))
  69. LYXPATH = find_executable('lyx') or \
  70. os.path.join(find_mac_app('LyX'), 'Contents/MacOS/lyx') or \
  71. '/bin/false'
  72. def rsync_list_files(*paths, extra_rsync_args=(), include_dirs=False):
  73. '''Iterate over the files in path that rsync would copy.
  74. By default, only files are listed, not directories, since doit doesn't
  75. like dependencies on directories because it can't hash them.
  76. This uses "rsync --list-only" to make rsync directly indicate which
  77. files it would copy, so any exclusion/inclusion rules are taken into
  78. account.
  79. '''
  80. rsync_list_cmd = [ 'rsync', '-r', '--list-only' ] + unnest(extra_rsync_args) + unnest(paths) + [ '.' ]
  81. rsync_out = check_output_decode(rsync_list_cmd).splitlines()
  82. for line in rsync_out:
  83. s = regex.search('^(-|d)(?:\S+\s+){4}(.*)', line)
  84. if s is not None:
  85. if include_dirs or s.group(1) == '-':
  86. yield s.group(2)
  87. def lyx_bib_deps(lyxfile):
  88. '''Return an iterator over all bib files referenced by a Lyx file.
  89. This will only return the names of existing files, so it will be
  90. unreliable in the case of an auto-generated bib file.
  91. '''
  92. with open(lyxfile) as f:
  93. lyx_text = f.read()
  94. bib_names = regex.search('bibfiles "(.*?)"', lyx_text).group(1).split(',')
  95. for bn in bib_names:
  96. bib_path = bn + '.bib'
  97. if os.path.exists(bib_path):
  98. yield bib_path
  99. def lyx_gfx_deps(lyxfile):
  100. '''Return an iterator over all graphics files included by a LyX file.'''
  101. with open(lyxfile) as f:
  102. lyx_text = f.read()
  103. for m in regex.finditer('\\\\begin_inset Graphics\\s+filename (.*?)$', lyx_text, regex.MULTILINE):
  104. yield m.group(1)
  105. def lyx_hrefs(lyxfile):
  106. '''Return an iterator over hrefs in a LyX file.'''
  107. pattern = '''
  108. (?xsm)
  109. ^ LatexCommand \\s+ href \\s* \\n
  110. (?: name \\b [^\\n]+ \\n )?
  111. target \\s+ "(.*?)" $
  112. '''
  113. with open(lyxfile) as f:
  114. return (urllib.parse.unquote(m.group(1)) for m in
  115. re.finditer(pattern, f.read()))
  116. def tex_gfx_extensions(tex_format = 'xetex'):
  117. '''Return the ordered list of graphics extensions.
  118. This yields the list of extensions that TeX will try for an
  119. \\includegraphics path.
  120. '''
  121. cmdout = check_output_decode(['texdef', '-t', tex_format, '-p', 'graphicx', 'Gin@extensions'])
  122. m = regex.search('^macro:->(.*?)$', cmdout, regex.MULTILINE)
  123. return m.group(1).split(',')
  124. rsync_common_args = ['-rL', '--size-only', '--delete', '--exclude', '.DS_Store', '--delete-excluded',]
  125. rule build_all:
  126. input: 'thesis.pdf'
  127. # Currently assumes the lyx file always exists.
  128. rule lyx_to_pdf:
  129. input: lyxfile = '{basename}.lyx',
  130. gfx_deps = lambda wildcards: lyx_gfx_deps(wildcards.basename + '.lyx'),
  131. bib_deps = lambda wildcards: lyx_bib_deps(wildcards.basename + '.lyx'),
  132. # Need to exclude pdfs in graphics/
  133. output: pdf='{basename,(?!graphics/).*}.pdf'
  134. shell: '{LYXPATH:q} --export-to pdf4 {output.pdf:q} {input.lyxfile:q}'
  135. # rule create_resume_html:
  136. # input: lyxfile='ryan_thompson_resume.lyx',
  137. # bibfiles=list(lyx_bib_deps('ryan_thompson_resume.lyx')),
  138. # example_files=list(resume_example_deps('ryan_thompson_resume.lyx')),
  139. # headshot='headshot-crop.jpg',
  140. # output: html='ryan_thompson_resume.html'
  141. # run:
  142. # with NamedTemporaryFile() as tempf:
  143. # shell('{LYXPATH:q} --export-to xhtml {tempf.name:q} {input.lyxfile:q}')
  144. # shell('''cat {tempf.name:q} | perl -lape 's[<span class="flex_cv_image">(.*?)</span>][<span class="flex_cv_image"><img src="$1" width="100"></span>]g' > {output.html:q}''')
  145. rule R_to_html:
  146. input: '{dirname}/{basename,[^/]+}.R'
  147. output: '{dirname}/{basename}.R.html'
  148. shell: 'pygmentize -f html -O full -l R -o {output:q} {input:q}'