waf

FORK: waf with some random patches
git clone https://git.neptards.moe/neptards/waf.git
Log | Files | Refs | README

review.py (9090B)


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