4 # moved the code into a separate tool (ita)
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
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)
17 import os, textwrap, shutil
18 from waflib import Logs, Context, ConfigSet, Options, Build, Configure
21 """Ordered dictionary"""
22 def __init__(self, data=None):
26 # we were provided a regular dict
27 if isinstance(data, dict):
28 self.append_from_dict(data)
30 # we were provided a tuple list
31 elif type(data) == list:
32 self.append_from_plist(data)
34 # we were provided invalid input
36 raise Exception("expected a dict or a tuple list")
38 def append_from_dict(self, dict):
39 map(self.__setitem__, dict.keys(), dict.values())
41 def append_from_plist(self, plist):
44 raise Exception("invalid pairs list")
46 self.__setitem__(k, v)
48 def __delitem__(self, key):
49 if not key in self._keys:
51 dict.__delitem__(self, key)
52 self._keys.remove(key)
54 def __setitem__(self, key, item):
55 dict.__setitem__(self, key, item)
56 if key not in self._keys:
57 self._keys.append(key)
64 return Odict(self.plist())
67 return zip(self._keys, self.values())
70 return list(self._keys) # return a copy of the list
73 return map(self.get, self._keys)
77 for k, v in self.items():
84 for k, v in self.items():
85 buf.append('%r : %r, ' % (k, v))
89 review_options = Odict()
91 Ordered dictionary mapping configuration option names to their optparse option.
96 Dictionary mapping configuration option names to their default value.
101 Review set containing the configuration values before parsing the command line.
104 new_review_set = None
106 Review set containing the configuration values after parsing the command line.
109 class OptionsReview(Options.OptionsContext):
110 def __init__(self, **kw):
111 super(self.__class__, self).__init__(**kw)
113 def prepare_config_review(self):
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.
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"):
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]
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()
135 class ReviewContext(Context.Context):
136 '''reviews the configuration values'''
140 def __init__(self, **kw):
141 super(self.__class__, self).__init__(**kw)
143 out = Options.options.out
145 out = getattr(Context.g_module, Context.OUT, None)
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"""
151 self.cache_path = os.path.join(self.build_path, Build.CACHE_DIR)
152 """Path to the cache directory"""
154 self.review_path = os.path.join(self.cache_path, 'review.cache')
155 """Path to the review cache file"""
159 Display and store the review set. Invalidate the cache as required.
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))
166 def invalidate_cache(self):
167 """Invalidate the cache to prevent bad builds."""
169 Logs.warn("Removing the cached configuration since the options have changed")
170 shutil.rmtree(self.cache_path)
174 def refresh_review_set(self):
176 Obtain the old review set and the new review set, and import the new set.
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)
183 def load_review_set(self):
185 Load and return the review set from the cache if it exists.
186 Otherwise, return an empty set.
188 if os.path.isfile(self.review_path):
189 return ConfigSet.ConfigSet(self.review_path)
190 return ConfigSet.ConfigSet()
192 def store_review_set(self, review_set):
194 Store the review set specified in the cache.
196 if not os.path.isdir(self.cache_path):
197 os.makedirs(self.cache_path)
198 review_set.store(self.review_path)
200 def update_review_set(self, old_set):
202 Merge the options passed on the command line with those imported
203 from the previous review set and return the corresponding
207 # Convert value to string. It's important that 'None' maps to
210 if val == None or val == '':
214 new_set = ConfigSet.ConfigSet()
215 opt_dict = Options.options.__dict__
217 for name in review_options.keys():
218 # the option is specified explicitly on the command line
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]
229 def import_review_set(self, review_set):
231 Import the actual value of the reviewable options in the option
232 dictionary, given the current review set.
234 for name in review_options.keys():
235 if name in review_set:
236 value = review_set[name]
238 value = review_defaults[name]
239 setattr(Options.options, name, value)
241 def compare_review_set(self, set1, set2):
243 Return true if the review sets specified are equal.
245 if len(set1.keys()) != len(set2.keys()): return False
246 for key in set1.keys():
247 if not key in set2 or set1[key] != set2[key]:
251 def display_review_set(self, review_set):
253 Return the string representing the review set specified.
255 term_width = Logs.get_term_cols()
257 for dest in review_options.keys():
258 opt = review_options[dest]
259 name = ", ".join(opt._short_opts + opt._long_opts)
262 if dest in review_set: actual = review_set[dest]
263 default = review_defaults[dest]
264 lines.append(self.format_option(name, help, actual, default, term_width))
265 return "Configuration:\n\n" + "\n\n".join(lines) + "\n"
267 def format_option(self, name, help, actual, default, term_width):
269 Return the string representing the option specified.
272 if val == None or val == '':
279 w = textwrap.TextWrapper()
280 w.width = term_width - 1
281 if w.width < 60: w.width = 60
286 out += w.fill(help) + "\n"
290 out += Logs.colors.CYAN + name + Logs.colors.NORMAL
292 # set the indentation used when the value wraps to the next line
293 w.subsequent_indent = " ".rjust(max_name_len + sep_len)
294 w.width -= (max_name_len + sep_len)
296 # the name string is too long, switch to the next line
297 if name_len > max_name_len:
298 out += "\n" + w.subsequent_indent
300 # fill the remaining of the line with spaces
302 out += " ".rjust(max_name_len + sep_len - name_len)
304 # format the actual value, if there is one
306 out += Logs.colors.BOLD + w.fill(val_to_str(actual)) + Logs.colors.NORMAL + "\n" + w.subsequent_indent
308 # format the default value
309 default_fmt = val_to_str(default)
311 default_fmt = "default: " + default_fmt
312 out += Logs.colors.NORMAL + w.fill(default_fmt) + Logs.colors.NORMAL
316 # Monkey-patch ConfigurationContext.execute() to have it store the review set.
317 old_configure_execute = Configure.ConfigurationContext.execute
318 def new_configure_execute(self):
319 old_configure_execute(self)
320 Context.create_context('review').store_review_set(new_review_set)
321 Configure.ConfigurationContext.execute = new_configure_execute