text: #!/usr/bin/env python
  # encoding: utf-8
  # Laurent Birtz, 2011
  # moved the code into a separate tool (ita)
  
  """
  There are several things here:
  - a different command-line option management making options persistent
  - the review command to display the options set
  
  Assumptions:
  - configuration options are not always added to the right group (and do not count on the users to do it...)
  - the options are persistent between the executions (waf options are NOT persistent by design), even for the configuration
  - when the options change, the build is invalidated (forcing a reconfiguration)
  """
  
  import os, textwrap, shutil
  from waflib import Logs, Context, ConfigSet, Options, Build, Configure
  
  class Odict(dict):
  	"""Ordered dictionary"""
  	def __init__(self, data=None):
  		self._keys = []
  		dict.__init__(self)
  		if data:
  			# we were provided a regular dict
  			if isinstance(data, dict):
  				self.append_from_dict(data)
  
  			# we were provided a tuple list
  			elif type(data) == list:
  				self.append_from_plist(data)
  
  			# we were provided invalid input
  			else:
  				raise Exception("expected a dict or a tuple list")
  
  	def append_from_dict(self, dict):
  		map(self.__setitem__, dict.keys(), dict.values())
  
  	def append_from_plist(self, plist):
  		for pair in plist:
  			if len(pair) != 2:
  				raise Exception("invalid pairs list")
  		for (k, v) in plist:
  			self.__setitem__(k, v)
  
  	def __delitem__(self, key):
  		if not key in self._keys:
  			raise KeyError(key)
  		dict.__delitem__(self, key)
  		self._keys.remove(key)
  
  	def __setitem__(self, key, item):
  		dict.__setitem__(self, key, item)
  		if key not in self._keys:
  			self._keys.append(key)
  
  	def clear(self):
  		dict.clear(self)
  		self._keys = []
  
  	def copy(self):
  		return Odict(self.plist())
  
  	def items(self):
  		return zip(self._keys, self.values())
  
  	def keys(self):
  		return list(self._keys) # return a copy of the list
  
  	def values(self):
  		return map(self.get, self._keys)
  
  	def plist(self):
  		p = []
  		for k, v in self.items():
  			p.append( (k, v) )
  		return p
  
  	def __str__(self):
  		buf = []
  		buf.append("{ ")
  		for k, v in self.items():
  			buf.append('%r : %r, ' % (k, v))
  		buf.append("}")
  		return ''.join(buf)
  
  review_options = Odict()
  """
  Ordered dictionary mapping configuration option names to their optparse option.
  """
  
  review_defaults = {}
  """
  Dictionary mapping configuration option names to their default value.
  """
  
  old_review_set = None
  """
  Review set containing the configuration values before parsing the command line.
  """
  
  new_review_set = None
  """
  Review set containing the configuration values after parsing the command line.
  """
  
  class OptionsReview(Options.OptionsContext):
  	def __init__(self, **kw):
  		super(self.__class__, self).__init__(**kw)
  
  	def prepare_config_review(self):
  		"""
  		Find the configuration options that are reviewable, detach
  		their default value from their optparse object and store them
  		into the review dictionaries.
  		"""
  		gr = self.get_option_group('configure options')
  		for opt in gr.option_list:
  			if opt.action != 'store' or opt.dest in ("out", "top"):
  				continue
  			review_options[opt.dest] = opt
  			review_defaults[opt.dest] = opt.default
  			if gr.defaults.has_key(opt.dest):
  				del gr.defaults[opt.dest]
  			opt.default = None
  
  	def parse_args(self):
  		self.prepare_config_review()
  		self.parser.get_option('--prefix').help = 'installation prefix'
  		super(OptionsReview, self).parse_args()
  		Context.create_context('review').refresh_review_set()
  
  class ReviewContext(Context.Context):
  	'''reviews the configuration values'''
  
  	cmd = 'review'
  
  	def __init__(self, **kw):
  		super(self.__class__, self).__init__(**kw)
  
  		out = Options.options.out
  		if not out:
  			out = getattr(Context.g_module, Context.OUT, None)
  		if not out:
  			out = Options.lockfile.replace('.lock-waf', '')
  		self.build_path = (os.path.isabs(out) and self.root or self.path).make_node(out).abspath()
  		"""Path to the build directory"""
  
  		self.cache_path = os.path.join(self.build_path, Build.CACHE_DIR)
  		"""Path to the cache directory"""
  
  		self.review_path = os.path.join(self.cache_path, 'review.cache')
  		"""Path to the review cache file"""
  
  	def execute(self):
  		"""
  		Display and store the review set. Invalidate the cache as required.
  		"""
  		if not self.compare_review_set(old_review_set, new_review_set):
  			self.invalidate_cache()
  		self.store_review_set(new_review_set)
  		print(self.display_review_set(new_review_set))
  
  	def invalidate_cache(self):
  		"""Invalidate the cache to prevent bad builds."""
  		try:
  			Logs.warn("Removing the cached configuration since the options have changed")
  			shutil.rmtree(self.cache_path)
  		except:
  			pass
  
  	def refresh_review_set(self):
  		"""
  		Obtain the old review set and the new review set, and import the new set.
  		"""
  		global old_review_set, new_review_set
  		old_review_set = self.load_review_set()
  		new_review_set = self.update_review_set(old_review_set)
  		self.import_review_set(new_review_set)
  
  	def load_review_set(self):
  		"""
  		Load and return the review set from the cache if it exists.
  		Otherwise, return an empty set.
  		"""
  		if os.path.isfile(self.review_path):
  			return ConfigSet.ConfigSet(self.review_path)
  		return ConfigSet.ConfigSet()
  
  	def store_review_set(self, review_set):
  		"""
  		Store the review set specified in the cache.
  		"""
  		if not os.path.isdir(self.cache_path):
  			os.makedirs(self.cache_path)
  		review_set.store(self.review_path)
  
  	def update_review_set(self, old_set):
  		"""
  		Merge the options passed on the command line with those imported
  		from the previous review set and return the corresponding
  		preview set.
  		"""
  
  		# Convert value to string. It's important that 'None' maps to
  		# the empty string.
  		def val_to_str(val):
  			if val == None or val == '':
  				return ''
  			return str(val)
  
  		new_set = ConfigSet.ConfigSet()
  		opt_dict = Options.options.__dict__
  
  		for name in review_options.keys():
  			# the option is specified explicitly on the command line
  			if name in opt_dict:
  				# if the option is the default, pretend it was never specified
  				if val_to_str(opt_dict[name]) != val_to_str(review_defaults[name]):
  					new_set[name] = opt_dict[name]
  			# the option was explicitly specified in a previous command
  			elif name in old_set:
  				new_set[name] = old_set[name]
  
  		return new_set
  
  	def import_review_set(self, review_set):
  		"""
  		Import the actual value of the reviewable options in the option
  		dictionary, given the current review set.
  		"""
  		for name in review_options.keys():
  			if name in review_set:
  				value = review_set[name]
  			else:
  				value = review_defaults[name]
  			setattr(Options.options, name, value)
  
  	def compare_review_set(self, set1, set2):
  		"""
  		Return true if the review sets specified are equal.
  		"""
  		if len(set1.keys()) != len(set2.keys()):
  			return False
  		for key in set1.keys():
  			if not key in set2 or set1[key] != set2[key]:
  				return False
  		return True
  
  	def display_review_set(self, review_set):
  		"""
  		Return the string representing the review set specified.
  		"""
  		term_width = Logs.get_term_cols()
  		lines = []
  		for dest in review_options.keys():
  			opt = review_options[dest]
  			name = ", ".join(opt._short_opts + opt._long_opts)
  			help = opt.help
  			actual = None
  			if dest in review_set:
  				actual = review_set[dest]
  			default = review_defaults[dest]
  			lines.append(self.format_option(name, help, actual, default, term_width))
  		return "Configuration:\n\n" + "\n\n".join(lines) + "\n"
  
  	def format_option(self, name, help, actual, default, term_width):
  		"""
  		Return the string representing the option specified.
  		"""
  		def val_to_str(val):
  			if val == None or val == '':
  				return "(void)"
  			return str(val)
  
  		max_name_len = 20
  		sep_len = 2
  
  		w = textwrap.TextWrapper()
  		w.width = term_width - 1
  		if w.width < 60:
  			w.width = 60
  
  		out = ""
  
  		# format the help
  		out += w.fill(help) + "\n"
  
  		# format the name
  		name_len = len(name)
  		out += Logs.colors.CYAN + name + Logs.colors.NORMAL
  
  		# set the indentation used when the value wraps to the next line
  		w.subsequent_indent = " ".rjust(max_name_len + sep_len)
  		w.width -= (max_name_len + sep_len)
  
  		# the name string is too long, switch to the next line
  		if name_len > max_name_len:
  			out += "\n" + w.subsequent_indent
  
  		# fill the remaining of the line with spaces
  		else:
  			out += " ".rjust(max_name_len + sep_len - name_len)
  
  		# format the actual value, if there is one
  		if actual != None:
  			out += Logs.colors.BOLD + w.fill(val_to_str(actual)) + Logs.colors.NORMAL + "\n" + w.subsequent_indent
  
  		# format the default value
  		default_fmt = val_to_str(default)
  		if actual != None:
  			default_fmt = "default: " + default_fmt
  		out += Logs.colors.NORMAL + w.fill(default_fmt) + Logs.colors.NORMAL
  
  		return out
  
  # Monkey-patch ConfigurationContext.execute() to have it store the review set.
  old_configure_execute = Configure.ConfigurationContext.execute
  def new_configure_execute(self):
  	old_configure_execute(self)
  	Context.create_context('review').store_review_set(new_review_set)
  Configure.ConfigurationContext.execute = new_configure_execute
  
  