make.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. #!/usr/bin/env python3
  2. # * Imports
  3. import os
  4. import shutil
  5. import subprocess
  6. import sys
  7. import multiprocessing
  8. import functools
  9. from collections import namedtuple
  10. from tempfile import mkstemp
  11. # * Variables
  12. sites_dir="sites"
  13. themes_dir = "themes"
  14. css_dir = "css"
  15. screenshots_dir="screenshots"
  16. phantomjs_command = "phantomjs --ssl-protocol=any --ignore-ssl-errors=true screenshot.js".split()
  17. common_deps = ["styl/index.styl", "styl/mixins.styl"]
  18. CSS = namedtuple("CSS", ['path', 'deps', 'theme', 'site'])
  19. Theme = namedtuple("Theme", ['name', 'styl_path', 'support_files'])
  20. # * Functions
  21. def main():
  22. "Update CSS files by default, or update screenshots."
  23. if len(sys.argv) > 1 and sys.argv[1] == "screenshots":
  24. update_screenshots()
  25. else:
  26. update_css_files()
  27. # ** CSS
  28. def update_css_files():
  29. "Build CSS files that need to be built."
  30. css_files = list_css(themes(), sites())
  31. # Make directories first to avoid race condition
  32. for css in css_files:
  33. dir = os.path.join(css_dir, css.theme.name)
  34. if not os.path.isdir(dir):
  35. os.makedirs(dir)
  36. pool = multiprocessing.Pool(multiprocessing.cpu_count())
  37. pool.map(build, css_files)
  38. def build(css):
  39. "Build CSS file if necessary."
  40. css_mtime = mtime(css.path)
  41. make = False
  42. for dep in css.deps:
  43. if mtime(dep) > css_mtime:
  44. make = True
  45. break
  46. if make:
  47. stylus(css)
  48. def stylus(css):
  49. "Run Stylus to build CSS file."
  50. output_file = css.path
  51. command = ["stylus", "--include", "styl",
  52. "--import", css.theme.styl_path,
  53. "--import", "styl",
  54. "-p", "sites/%s.styl" % css.site]
  55. result = subprocess.check_output(command)
  56. with open(output_file, "wb") as f:
  57. f.write(result)
  58. print(output_file)
  59. # ** Screenshots
  60. def update_screenshots():
  61. "Update screenshots."
  62. css_files = list_css(themes(), sites())
  63. if not os.path.isdir(screenshots_dir):
  64. # If the directory does not exist, create a new worktree for it.
  65. # Assumes the screenshots branch exists.
  66. subprocess.call(["git", "worktree", "prune"])
  67. subprocess.call(["git", "worktree", "add",
  68. screenshots_dir, "screenshots"])
  69. # Make directories first to avoid race condition
  70. for css in css_files:
  71. output_dir = os.path.join(screenshots_dir, css.theme.name)
  72. if not os.path.isdir(output_dir):
  73. os.makedirs(output_dir)
  74. pool = multiprocessing.Pool(multiprocessing.cpu_count())
  75. pool.map(update_screenshot, css_files)
  76. commit_screenshots()
  77. def commit_screenshots():
  78. if os.path.exists(os.path.join(screenshots_dir, ".git")):
  79. subprocess.call(["git", "-C", screenshots_dir,
  80. "add", "-A"])
  81. # amend changes instead of keeping them to save space
  82. subprocess.call(["git", "-C", screenshots_dir,
  83. "commit", "--amend", "-m", "Update screenshots"])
  84. else:
  85. print("screenshot dir was not a worktree, aborting commit")
  86. def update_screenshot(css):
  87. "Update screenshot for CSS if necessary."
  88. screenshot_path = screenshot_path_for_css(css)
  89. if mtime(css.path) > mtime(screenshot_path):
  90. save_screenshot(css)
  91. def screenshot_path_for_css(css):
  92. "Return path of screenshot for CSS."
  93. return os.path.join(screenshots_dir, css.theme.name, "%s.png" % css.site)
  94. def save_screenshot(css):
  95. "Save screenshot for CSS."
  96. # Prepare filename
  97. screenshot_path = screenshot_path_for_css(css)
  98. # Get URL
  99. url = css_screenshot_url(css)
  100. if not url:
  101. # Screenshot disabled
  102. return False
  103. # Prepare command
  104. command = list(phantomjs_command)
  105. command.extend([url, screenshot_path, css.path])
  106. # Run PhantomJS
  107. subprocess.check_output(command)
  108. # Compress with pngcrush
  109. _, tempfile_path = mkstemp(suffix=".png")
  110. subprocess.check_output(["pngcrush", screenshot_path, tempfile_path], stderr=subprocess.DEVNULL)
  111. shutil.move(tempfile_path, screenshot_path)
  112. print(screenshot_path)
  113. def css_screenshot_url(css):
  114. "Return URL for taking screenshots of CSS."
  115. # Get site URL
  116. site_url_filename = os.path.join(sites_dir, css.site + ".url")
  117. if os.path.exists(site_url_filename):
  118. with open(site_url_filename, "r") as f:
  119. url = f.readlines()
  120. if url:
  121. # Use URL given in .url file
  122. url = url[0].strip()
  123. else:
  124. # Use name of site file (without .styl extension)
  125. url = "http://" + css.site
  126. return url
  127. # ** Support
  128. def list_css(themes, sites):
  129. "Return list of CSS files for THEMES and SITES."
  130. return [CSS("%s/%s/%s-%s.css" % (css_dir, theme.name, theme.name,
  131. site.strip('_')),
  132. dependencies(theme, site), theme, site)
  133. for theme in themes
  134. for site in sites]
  135. def themes():
  136. "Return list of themes."
  137. theme_names = []
  138. themes = []
  139. # Make list of theme directories
  140. for d in os.listdir(themes_dir):
  141. theme_names.append(d)
  142. # Iterate over theme directories
  143. for theme in theme_names:
  144. support_files = []
  145. variant_files = []
  146. directory = os.path.join(themes_dir, theme)
  147. # Iterate over files in theme directory
  148. for f in os.listdir(directory):
  149. path = os.path.join(themes_dir, theme, f)
  150. if f == "colors.styl":
  151. # Support file
  152. support_files.append(path)
  153. elif f.endswith(".styl"):
  154. # Theme file
  155. variant_files.append({'variant': without_styl(f), 'path': path})
  156. # Otherwise, not a relevant file
  157. # Add theme object to list
  158. if len(variant_files) == 1:
  159. # Only one variant: omit variant name from theme name
  160. themes.append(Theme(theme, variant_files[0]['path'], support_files))
  161. else:
  162. # Multiple variants: include variant name in theme name
  163. for f in variant_files:
  164. themes.append(Theme("%s-%s" % (theme, f['variant']), f['path'], support_files))
  165. return themes
  166. def sites():
  167. "Return list of sites."
  168. for path, dirs, files in os.walk(sites_dir):
  169. return [site.replace(".styl", "")
  170. for site in files
  171. if site.endswith(".styl")]
  172. def dependencies(theme, site):
  173. "Return list of dependency .styl files for THEME and SITE."
  174. deps = list(common_deps)
  175. deps.append(theme.styl_path)
  176. deps.extend(theme.support_files)
  177. deps.append("sites/%s.styl" % site)
  178. if site == "all-sites":
  179. deps += ["sites/%s.styl" % s for s in sites()]
  180. return deps
  181. @functools.lru_cache()
  182. def mtime(path):
  183. "Return mtime for PATH."
  184. if os.path.isfile(path):
  185. return os.path.getmtime(path)
  186. else:
  187. return 0
  188. def without_styl(s):
  189. """Return string S without ".styl" extension."""
  190. return s.replace(".styl", "")
  191. # * Footer
  192. if __name__ == "__main__":
  193. main()