Snakefile 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. # -*- coding: utf-8; -*-
  2. import locale
  3. import os.path
  4. import regex
  5. from collections import Iterable, Mapping # in Python 3 use from collections.abc
  6. from distutils.spawn import find_executable
  7. from fnmatch import fnmatch
  8. from subprocess import check_output, check_call
  9. from tempfile import NamedTemporaryFile
  10. try:
  11. from os import scandir, walk
  12. except ImportError:
  13. from scandir import scandir, walk
  14. def unnest(*args):
  15. """Un-nest list- and tuple-like elements in arguments.
  16. "List-like" means anything with a len() and whose elments can be
  17. accessed with numeric indexing, except for string-like elements. It
  18. must also be an instance of the collections.Iterable abstract class.
  19. Dict-like elements and iterators/generators are not affected.
  20. This function always returns a list, even if it is passed a single
  21. scalar argument.
  22. """
  23. result = []
  24. for arg in args:
  25. if isinstance(arg, str):
  26. # String
  27. result.append(arg)
  28. elif isinstance(arg, Mapping):
  29. # Dict-like
  30. result.append(arg)
  31. elif isinstance(arg, Iterable):
  32. try:
  33. # Duck-typing test for list-ness (a stricter condition
  34. # than just "iterable")
  35. for i in range(len(arg)):
  36. result.append(arg[i])
  37. except TypeError:
  38. # Iterable but not list-like
  39. result.append(arg)
  40. else:
  41. # Not iterable
  42. result.append(arg)
  43. return result
  44. def check_output_decode(*args, encoding=locale.getpreferredencoding(), **kwargs):
  45. """Shortcut for check.output + str.decode"""
  46. return check_output(*args, **kwargs).decode(encoding)
  47. def find_mac_app(name):
  48. try:
  49. return check_output_decode(
  50. ["mdfind",
  51. "kMDItemDisplayName=={name}&&kMDItemKind==Application".format(name=name)]).split("\n")[0]
  52. except Exception:
  53. return None
  54. def glob_recursive(pattern, top=".", include_hidden=False, *args, **kwargs):
  55. """Combination of glob.glob and os.walk.
  56. Reutrns the relative path to every file or directory matching the
  57. pattern anywhere in the specified directory hierarchy. Defaults to the
  58. current working directory. Any additional arguments are passed to
  59. os.walk."""
  60. for (path, dirs, files) in walk(top, *args, **kwargs):
  61. for f in dirs + files:
  62. if include_hidden or f.startswith("."):
  63. continue
  64. if fnmatch(f, pattern):
  65. yield os.path.normpath(os.path.join(path, f))
  66. LYXPATH = find_executable("lyx") or \
  67. os.path.join(find_mac_app("LyX"), "Contents/MacOS/lyx") or \
  68. '/bin/false'
  69. def rsync_list_files(*paths, extra_rsync_args=(), include_dirs=False):
  70. """Iterate over the files in path that rsync would copy.
  71. By default, only files are listed, not directories, since doit doesn't
  72. like dependencies on directories because it can't hash them.
  73. This uses "rsync --list-only" to make rsync directly indicate which
  74. files it would copy, so any exclusion/inclusion rules are taken into
  75. account.
  76. """
  77. rsync_list_cmd = [ 'rsync', '-r', "--list-only" ] + unnest(extra_rsync_args) + unnest(paths) + [ "." ]
  78. rsync_out = check_output_decode(rsync_list_cmd).splitlines()
  79. for line in rsync_out:
  80. s = regex.search("^(-|d)(?:\S+\s+){4}(.*)", line)
  81. if s is not None:
  82. if include_dirs or s.group(1) == '-':
  83. yield s.group(2)
  84. def lyx_image_deps(wildcards):
  85. lyxfile = wildcards.filename + ".lyx"
  86. def lyx_bib_deps(wildcards):
  87. # Cheat: Assume every bib file is a dependency of any LaTeX
  88. # operation
  89. return list(glob_recursive('*.bib'))
  90. readme_files = list(glob_recursive("README.mkdn", top="examples"))
  91. index_files = [ os.path.join(os.path.dirname(f), "index.html") for f in readme_files ]
  92. rsync_common_args = ["-rL", "--size-only", "--delete", "--exclude", ".DS_Store", "--delete-excluded",]
  93. all_example_files = set(rsync_list_files('examples', extra_rsync_args=rsync_common_args))
  94. all_example_files = all_example_files.union(index_files)
  95. rsync_dest = "mneme:public_html/resume/"
  96. rule build_all:
  97. input: "ryan_thompson_resume.pdf", "ryan_thompson_resume.html", index_files
  98. rule create_resume_pdf:
  99. input: lyxfile="ryan_thompson_resume.lyx", bibfile="citations.bib", headshot="headshot-crop.jpg"
  100. output: pdf="ryan_thompson_resume.pdf"
  101. shell: '{LYXPATH:q} --export-to pdf4 {output.pdf:q} {input.lyxfile:q}'
  102. rule create_resume_html:
  103. input: lyxfile="ryan_thompson_resume.lyx", bibfile="citations.bib", headshot="headshot-crop.jpg"
  104. output: html="ryan_thompson_resume.html"
  105. run:
  106. with NamedTemporaryFile() as tempf:
  107. shell('{LYXPATH:q} --export-to xhtml {tempf.name:q} {input.lyxfile:q}')
  108. 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}''')
  109. rule link_resume_to_index_html:
  110. input: 'ryan_thompson_resume.html'
  111. output: 'index.html'
  112. shell: 'ln -s {input:q} {output:q}'
  113. rule readme_to_index_html:
  114. input: "{dirname}/README.mkdn"
  115. output: "{dirname}/index.html"
  116. shell: 'pandoc -t html -o {output[0]:q} {input[0]:q}'
  117. rule publish:
  118. input: roots=('ryan_thompson_resume.pdf', 'ryan_thompson_resume.html', 'index.html', 'examples', 'headshot-crop.jpg'),
  119. others=all_example_files
  120. shell: '''
  121. rsync --info=progress2 {rsync_common_args:q} \
  122. {input.roots:q} {rsync_dest:q}
  123. '''