build:wafsamba: Remove unnecessary parameters to cmd_and_log
[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()): return False
246                 for key in set1.keys():
247                         if not key in set2 or set1[key] != set2[key]:
248                                 return False
249                 return True
250
251         def display_review_set(self, review_set):
252                 """
253                 Return the string representing the review set specified.
254                 """
255                 term_width = Logs.get_term_cols()
256                 lines = []
257                 for dest in review_options.keys():
258                         opt = review_options[dest]
259                         name = ", ".join(opt._short_opts + opt._long_opts)
260                         help = opt.help
261                         actual = None
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"
266
267         def format_option(self, name, help, actual, default, term_width):
268                 """
269                 Return the string representing the option specified.
270                 """
271                 def val_to_str(val):
272                         if val == None or val == '':
273                                 return "(void)"
274                         return str(val)
275
276                 max_name_len = 20
277                 sep_len = 2
278
279                 w = textwrap.TextWrapper()
280                 w.width = term_width - 1
281                 if w.width < 60: w.width = 60
282
283                 out = ""
284
285                 # format the help
286                 out += w.fill(help) + "\n"
287
288                 # format the name
289                 name_len = len(name)
290                 out += Logs.colors.CYAN + name + Logs.colors.NORMAL
291
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)
295
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
299
300                 # fill the remaining of the line with spaces
301                 else:
302                         out += " ".rjust(max_name_len + sep_len - name_len)
303
304                 # format the actual value, if there is one
305                 if actual != None:
306                         out += Logs.colors.BOLD + w.fill(val_to_str(actual)) + Logs.colors.NORMAL + "\n" + w.subsequent_indent
307
308                 # format the default value
309                 default_fmt = val_to_str(default)
310                 if actual != None:
311                         default_fmt = "default: " + default_fmt
312                 out += Logs.colors.NORMAL + w.fill(default_fmt) + Logs.colors.NORMAL
313
314                 return out
315
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