1#!/usr/bin/env python 2# encoding: utf-8 3# Laurent Birtz, 2011 4# moved the code into a separate tool (ita) 5 6""" 7There are several things here: 8- a different command-line option management making options persistent 9- the review command to display the options set 10 11Assumptions: 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 17import os, textwrap, shutil 18from waflib import Logs, Context, ConfigSet, Options, Build, Configure 19 20class 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 89review_options = Odict() 90""" 91Ordered dictionary mapping configuration option names to their optparse option. 92""" 93 94review_defaults = {} 95""" 96Dictionary mapping configuration option names to their default value. 97""" 98 99old_review_set = None 100""" 101Review set containing the configuration values before parsing the command line. 102""" 103 104new_review_set = None 105""" 106Review set containing the configuration values after parsing the command line. 107""" 108 109class 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 135class 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. 320old_configure_execute = Configure.ConfigurationContext.execute 321def new_configure_execute(self): 322 old_configure_execute(self) 323 Context.create_context('review').store_review_set(new_review_set) 324Configure.ConfigurationContext.execute = new_configure_execute 325 326