third_party/waf: upgrade to waf 2.0.8
[samba.git] / third_party / waf / waflib / extras / review.py
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