review.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. # Laurent Birtz, 2011
  4. # moved the code into a separate tool (ita)
  5. """
  6. There are several things here:
  7. - a different command-line option management making options persistent
  8. - the review command to display the options set
  9. Assumptions:
  10. - configuration options are not always added to the right group (and do not count on the users to do it...)
  11. - the options are persistent between the executions (waf options are NOT persistent by design), even for the configuration
  12. - when the options change, the build is invalidated (forcing a reconfiguration)
  13. """
  14. import os, textwrap, shutil
  15. from waflib import Logs, Context, ConfigSet, Options, Build, Configure
  16. class Odict(dict):
  17. """Ordered dictionary"""
  18. def __init__(self, data=None):
  19. self._keys = []
  20. dict.__init__(self)
  21. if data:
  22. # we were provided a regular dict
  23. if isinstance(data, dict):
  24. self.append_from_dict(data)
  25. # we were provided a tuple list
  26. elif type(data) == list:
  27. self.append_from_plist(data)
  28. # we were provided invalid input
  29. else:
  30. raise Exception("expected a dict or a tuple list")
  31. def append_from_dict(self, dict):
  32. map(self.__setitem__, dict.keys(), dict.values())
  33. def append_from_plist(self, plist):
  34. for pair in plist:
  35. if len(pair) != 2:
  36. raise Exception("invalid pairs list")
  37. for (k, v) in plist:
  38. self.__setitem__(k, v)
  39. def __delitem__(self, key):
  40. if not key in self._keys:
  41. raise KeyError(key)
  42. dict.__delitem__(self, key)
  43. self._keys.remove(key)
  44. def __setitem__(self, key, item):
  45. dict.__setitem__(self, key, item)
  46. if key not in self._keys:
  47. self._keys.append(key)
  48. def clear(self):
  49. dict.clear(self)
  50. self._keys = []
  51. def copy(self):
  52. return Odict(self.plist())
  53. def items(self):
  54. return zip(self._keys, self.values())
  55. def keys(self):
  56. return list(self._keys) # return a copy of the list
  57. def values(self):
  58. return map(self.get, self._keys)
  59. def plist(self):
  60. p = []
  61. for k, v in self.items():
  62. p.append( (k, v) )
  63. return p
  64. def __str__(self):
  65. buf = []
  66. buf.append("{ ")
  67. for k, v in self.items():
  68. buf.append('%r : %r, ' % (k, v))
  69. buf.append("}")
  70. return ''.join(buf)
  71. review_options = Odict()
  72. """
  73. Ordered dictionary mapping configuration option names to their optparse option.
  74. """
  75. review_defaults = {}
  76. """
  77. Dictionary mapping configuration option names to their default value.
  78. """
  79. old_review_set = None
  80. """
  81. Review set containing the configuration values before parsing the command line.
  82. """
  83. new_review_set = None
  84. """
  85. Review set containing the configuration values after parsing the command line.
  86. """
  87. class OptionsReview(Options.OptionsContext):
  88. def __init__(self, **kw):
  89. super(self.__class__, self).__init__(**kw)
  90. def prepare_config_review(self):
  91. """
  92. Find the configuration options that are reviewable, detach
  93. their default value from their optparse object and store them
  94. into the review dictionaries.
  95. """
  96. gr = self.get_option_group('configure options')
  97. for opt in gr.option_list:
  98. if opt.action != 'store' or opt.dest in ("out", "top"):
  99. continue
  100. review_options[opt.dest] = opt
  101. review_defaults[opt.dest] = opt.default
  102. if gr.defaults.has_key(opt.dest):
  103. del gr.defaults[opt.dest]
  104. opt.default = None
  105. def parse_args(self):
  106. self.prepare_config_review()
  107. self.parser.get_option('--prefix').help = 'installation prefix'
  108. super(OptionsReview, self).parse_args()
  109. Context.create_context('review').refresh_review_set()
  110. class ReviewContext(Context.Context):
  111. '''reviews the configuration values'''
  112. cmd = 'review'
  113. def __init__(self, **kw):
  114. super(self.__class__, self).__init__(**kw)
  115. out = Options.options.out
  116. if not out:
  117. out = getattr(Context.g_module, Context.OUT, None)
  118. if not out:
  119. out = Options.lockfile.replace('.lock-waf', '')
  120. self.build_path = (os.path.isabs(out) and self.root or self.path).make_node(out).abspath()
  121. """Path to the build directory"""
  122. self.cache_path = os.path.join(self.build_path, Build.CACHE_DIR)
  123. """Path to the cache directory"""
  124. self.review_path = os.path.join(self.cache_path, 'review.cache')
  125. """Path to the review cache file"""
  126. def execute(self):
  127. """
  128. Display and store the review set. Invalidate the cache as required.
  129. """
  130. if not self.compare_review_set(old_review_set, new_review_set):
  131. self.invalidate_cache()
  132. self.store_review_set(new_review_set)
  133. print(self.display_review_set(new_review_set))
  134. def invalidate_cache(self):
  135. """Invalidate the cache to prevent bad builds."""
  136. try:
  137. Logs.warn("Removing the cached configuration since the options have changed")
  138. shutil.rmtree(self.cache_path)
  139. except:
  140. pass
  141. def refresh_review_set(self):
  142. """
  143. Obtain the old review set and the new review set, and import the new set.
  144. """
  145. global old_review_set, new_review_set
  146. old_review_set = self.load_review_set()
  147. new_review_set = self.update_review_set(old_review_set)
  148. self.import_review_set(new_review_set)
  149. def load_review_set(self):
  150. """
  151. Load and return the review set from the cache if it exists.
  152. Otherwise, return an empty set.
  153. """
  154. if os.path.isfile(self.review_path):
  155. return ConfigSet.ConfigSet(self.review_path)
  156. return ConfigSet.ConfigSet()
  157. def store_review_set(self, review_set):
  158. """
  159. Store the review set specified in the cache.
  160. """
  161. if not os.path.isdir(self.cache_path):
  162. os.makedirs(self.cache_path)
  163. review_set.store(self.review_path)
  164. def update_review_set(self, old_set):
  165. """
  166. Merge the options passed on the command line with those imported
  167. from the previous review set and return the corresponding
  168. preview set.
  169. """
  170. # Convert value to string. It's important that 'None' maps to
  171. # the empty string.
  172. def val_to_str(val):
  173. if val == None or val == '':
  174. return ''
  175. return str(val)
  176. new_set = ConfigSet.ConfigSet()
  177. opt_dict = Options.options.__dict__
  178. for name in review_options.keys():
  179. # the option is specified explicitly on the command line
  180. if name in opt_dict:
  181. # if the option is the default, pretend it was never specified
  182. if val_to_str(opt_dict[name]) != val_to_str(review_defaults[name]):
  183. new_set[name] = opt_dict[name]
  184. # the option was explicitly specified in a previous command
  185. elif name in old_set:
  186. new_set[name] = old_set[name]
  187. return new_set
  188. def import_review_set(self, review_set):
  189. """
  190. Import the actual value of the reviewable options in the option
  191. dictionary, given the current review set.
  192. """
  193. for name in review_options.keys():
  194. if name in review_set:
  195. value = review_set[name]
  196. else:
  197. value = review_defaults[name]
  198. setattr(Options.options, name, value)
  199. def compare_review_set(self, set1, set2):
  200. """
  201. Return true if the review sets specified are equal.
  202. """
  203. if len(set1.keys()) != len(set2.keys()):
  204. return False
  205. for key in set1.keys():
  206. if not key in set2 or set1[key] != set2[key]:
  207. return False
  208. return True
  209. def display_review_set(self, review_set):
  210. """
  211. Return the string representing the review set specified.
  212. """
  213. term_width = Logs.get_term_cols()
  214. lines = []
  215. for dest in review_options.keys():
  216. opt = review_options[dest]
  217. name = ", ".join(opt._short_opts + opt._long_opts)
  218. help = opt.help
  219. actual = None
  220. if dest in review_set:
  221. actual = review_set[dest]
  222. default = review_defaults[dest]
  223. lines.append(self.format_option(name, help, actual, default, term_width))
  224. return "Configuration:\n\n" + "\n\n".join(lines) + "\n"
  225. def format_option(self, name, help, actual, default, term_width):
  226. """
  227. Return the string representing the option specified.
  228. """
  229. def val_to_str(val):
  230. if val == None or val == '':
  231. return "(void)"
  232. return str(val)
  233. max_name_len = 20
  234. sep_len = 2
  235. w = textwrap.TextWrapper()
  236. w.width = term_width - 1
  237. if w.width < 60:
  238. w.width = 60
  239. out = ""
  240. # format the help
  241. out += w.fill(help) + "\n"
  242. # format the name
  243. name_len = len(name)
  244. out += Logs.colors.CYAN + name + Logs.colors.NORMAL
  245. # set the indentation used when the value wraps to the next line
  246. w.subsequent_indent = " ".rjust(max_name_len + sep_len)
  247. w.width -= (max_name_len + sep_len)
  248. # the name string is too long, switch to the next line
  249. if name_len > max_name_len:
  250. out += "\n" + w.subsequent_indent
  251. # fill the remaining of the line with spaces
  252. else:
  253. out += " ".rjust(max_name_len + sep_len - name_len)
  254. # format the actual value, if there is one
  255. if actual != None:
  256. out += Logs.colors.BOLD + w.fill(val_to_str(actual)) + Logs.colors.NORMAL + "\n" + w.subsequent_indent
  257. # format the default value
  258. default_fmt = val_to_str(default)
  259. if actual != None:
  260. default_fmt = "default: " + default_fmt
  261. out += Logs.colors.NORMAL + w.fill(default_fmt) + Logs.colors.NORMAL
  262. return out
  263. # Monkey-patch ConfigurationContext.execute() to have it store the review set.
  264. old_configure_execute = Configure.ConfigurationContext.execute
  265. def new_configure_execute(self):
  266. old_configure_execute(self)
  267. Context.create_context('review').store_review_set(new_review_set)
  268. Configure.ConfigurationContext.execute = new_configure_execute