dodo.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. #!/usr/bin/env doit -f
  2. # -*- coding: utf-8; -*-
  3. from collections import Iterable, Mapping # in Python 3 use from collections.abc
  4. from distutils.spawn import find_executable
  5. from doit import create_after
  6. from fnmatch import fnmatch
  7. from kwonly import kwonly
  8. from subprocess import check_output
  9. from types import StringTypes
  10. import os.path
  11. import regex
  12. try:
  13. from os import scandir, walk
  14. except ImportError:
  15. from scandir import scandir, walk
  16. DOIT_CONFIG = {
  17. 'default_tasks': ['publish_to_mneme'],
  18. }
  19. def unnest(*args):
  20. """Un-nest list- and tuple-like elements in arguments.
  21. "List-like" means anything with a len() and whose elments can be
  22. accessed with numeric indexing, except for string-like elements. It
  23. must also be an instance of the collections.Iterable abstract class.
  24. Dict-like elements and iterators/generators are not affected.
  25. This function always returns a list, even if it is passed a single
  26. scalar argument.
  27. """
  28. result = []
  29. for arg in args:
  30. if isinstance(arg, StringTypes):
  31. # String
  32. result.append(arg)
  33. elif isinstance(arg, Mapping):
  34. # Dict-like
  35. result.append(arg)
  36. elif isinstance(arg, Iterable):
  37. try:
  38. # Duck-typing test for list-ness (a stricter condition
  39. # that just "iterable")
  40. for i in xrange(len(arg)):
  41. result.append(arg[i])
  42. except TypeError:
  43. # Not list-like
  44. result.append(arg)
  45. else:
  46. result.append(arg)
  47. return result
  48. def find_mac_app(name):
  49. return check_output(
  50. ["mdfind",
  51. "kMDItemDisplayName==%s&&kMDItemKind==Application" % (name,) ]).strip()
  52. def glob_recursive(pattern, top=".", include_hidden=False, *args, **kwargs):
  53. """Combination of glob.glob and os.walk.
  54. Reutrns the relative path to every file or directory matching the
  55. pattern anywhere in the specified directory hierarchy. Defaults to the
  56. current working directory. Any additional arguments are passed to
  57. os.walk."""
  58. for (path, dirs, files) in walk(top, *args, **kwargs):
  59. for f in dirs + files:
  60. if include_hidden or f.startswith("."):
  61. continue
  62. if fnmatch(f, pattern):
  63. yield os.path.normpath(os.path.join(path, f))
  64. LYXPATH = find_executable("lyx") or \
  65. os.path.join(find_mac_app("LyX"), "Contents/MacOS/lyx")
  66. @kwonly(0)
  67. def rsync_list_files(extra_rsync_args=(), include_dirs=False, *paths):
  68. """Iterate over the files in path that rsync would copy.
  69. By default, only files are listed, not directories, since doit doesn't
  70. like dependencies on directories because it can't hash them.
  71. This uses "rsync --list-only" to make rsync directly indicate which
  72. files it would copy, so any exclusion/inclusion rules are taken into
  73. account.
  74. """
  75. rsync_list_cmd = [ 'rsync', '-r', "--list-only" ] + unnest(extra_rsync_args) + unnest(paths) + [ "." ]
  76. rsync_out = check_output(rsync_list_cmd).splitlines()
  77. for line in rsync_out:
  78. s = regex.search("^(-|d)(?:\S+\s+){4}(.*)", line)
  79. if s is not None:
  80. if include_dirs or s.group(1) == '-':
  81. yield s.group(2)
  82. rsync_common_args = ["-rL", "--size-only", "--delete", "--exclude", ".DS_Store", "--delete-excluded",]
  83. def task_lyx2pdf():
  84. yield {
  85. 'name': None,
  86. 'doc': "Convert LyX file to PDF."
  87. }
  88. for lyxfile in glob_recursive("*.lyx"):
  89. pdffile = lyxfile[:-3] + "pdf"
  90. lyx_cmd = [LYXPATH, "--export-to", "pdf4" , pdffile, lyxfile]
  91. yield {
  92. 'name': lyxfile,
  93. 'actions': [lyx_cmd],
  94. 'file_dep': [lyxfile] + list(glob_recursive('*.bib')),
  95. 'targets': [pdffile],
  96. 'verbosity': 0,
  97. 'clean': True,
  98. }
  99. def task_readme2index():
  100. yield {
  101. 'name': None,
  102. 'doc': "Convert README.mkdn files to index.html."
  103. }
  104. for mkdnfile in glob_recursive("README.mkdn", top="examples"):
  105. htmlfile = os.path.join(os.path.dirname(mkdnfile), "index.html")
  106. yield {
  107. 'name': mkdnfile,
  108. 'actions': [["pandoc", "-t", "html", "-o", htmlfile, mkdnfile]],
  109. 'file_dep': [mkdnfile],
  110. 'targets': [htmlfile],
  111. 'clean': True,
  112. }
  113. def task_publish_to_mneme():
  114. yield {
  115. 'name': None,
  116. 'doc': "Sync resume and supporting files to mneme."
  117. }
  118. # Resume PDF file
  119. rsync_src = "ryan_thompson_resume.pdf"
  120. rsync_dest = "mneme:public_html/resume/"
  121. file_deps = [rsync_src]
  122. rsync_xfer_cmd = ["rsync"] + rsync_common_args + [ rsync_src, rsync_dest ]
  123. yield {
  124. 'name': rsync_src,
  125. 'actions': [rsync_xfer_cmd],
  126. 'file_dep': file_deps,
  127. 'task_dep': [ 'lyx2pdf' ],
  128. 'doc': "rsync resume PDF file to mneme.",
  129. 'verbosity': 2,
  130. }
  131. # Examples directory
  132. rsync_src = "examples"
  133. rsync_dest = "mneme:public_html/resume/"
  134. file_deps = list(rsync_list_files(rsync_src, extra_rsync_args=rsync_common_args))
  135. # Ensure the generated html files are in file_deps
  136. for f in file_deps:
  137. if os.path.basename(f) == "README.mkdn":
  138. htmlfile = os.path.join(os.path.dirname(f), "index.html")
  139. if htmlfile not in file_deps:
  140. file_deps.append(htmlfile)
  141. file_deps = sorted(file_deps)
  142. rsync_xfer_cmd = ["rsync"] + rsync_common_args + [ rsync_src, rsync_dest ]
  143. yield {
  144. 'name': rsync_src,
  145. 'actions': [rsync_xfer_cmd],
  146. 'file_dep': file_deps,
  147. 'task_dep': [ 'readme2index' ],
  148. 'doc': "rsync examples directory to mneme.",
  149. 'verbosity': 2,
  150. }
  151. # Dummy target if you just want to build everything but not publish
  152. def task_build():
  153. return {
  154. 'doc': 'Build resume and supporting files.',
  155. 'task_dep': [ 'lyx2pdf', 'readme2index' ],
  156. 'actions': [],
  157. }