aboutsummaryrefslogtreecommitdiffstats
path: root/waflib/extras/review.py
blob: 561e06219da1d52ca00e7a461e39df02548798d0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
#!/usr/bin/env python
# encoding: utf-8
# Laurent Birtz, 2011
# moved the code into a separate tool (ita)

"""
There are several things here:
- a different command-line option management making options persistent
- the review command to display the options set

Assumptions:
- configuration options are not always added to the right group (and do not count on the users to do it...)
- the options are persistent between the executions (waf options are NOT persistent by design), even for the configuration
- when the options change, the build is invalidated (forcing a reconfiguration)
"""

import os, textwrap, shutil
from waflib import Logs, Context, ConfigSet, Options, Build, Configure

class Odict(dict):
	"""Ordered dictionary"""
	def __init__(self, data=None):
		self._keys = []
		dict.__init__(self)
		if data:
			# we were provided a regular dict
			if isinstance(data, dict):
				self.append_from_dict(data)

			# we were provided a tuple list
			elif type(data) == list:
				self.append_from_plist(data)

			# we were provided invalid input
			else:
				raise Exception("expected a dict or a tuple list")

	def append_from_dict(self, dict):
		map(self.__setitem__, dict.keys(), dict.values())

	def append_from_plist(self, plist):
		for pair in plist:
			if len(pair) != 2:
				raise Exception("invalid pairs list")
		for (k, v) in plist:
			self.__setitem__(k, v)

	def __delitem__(self, key):
		if not key in self._keys:
			raise KeyError(key)
		dict.__delitem__(self, key)
		self._keys.remove(key)

	def __setitem__(self, key, item):
		dict.__setitem__(self, key, item)
		if key not in self._keys:
			self._keys.append(key)

	def clear(self):
		dict.clear(self)
		self._keys = []

	def copy(self):
		return Odict(self.plist())

	def items(self):
		return zip(self._keys, self.values())

	def keys(self):
		return list(self._keys) # return a copy of the list

	def values(self):
		return map(self.get, self._keys)

	def plist(self):
		p = []
		for k, v in self.items():
			p.append( (k, v) )
		return p

	def __str__(self):
		buf = []
		buf.append("{ ")
		for k, v in self.items():
			buf.append('%r : %r, ' % (k, v))
		buf.append("}")
		return ''.join(buf)

review_options = Odict()
"""
Ordered dictionary mapping configuration option names to their optparse option.
"""

review_defaults = {}
"""
Dictionary mapping configuration option names to their default value.
"""

old_review_set = None
"""
Review set containing the configuration values before parsing the command line.
"""

new_review_set = None
"""
Review set containing the configuration values after parsing the command line.
"""

class OptionsReview(Options.OptionsContext):
	def __init__(self, **kw):
		super(self.__class__, self).__init__(**kw)

	def prepare_config_review(self):
		"""
		Find the configuration options that are reviewable, detach
		their default value from their optparse object and store them
		into the review dictionaries.
		"""
		gr = self.get_option_group('configure options')
		for opt in gr.option_list:
			if opt.action != 'store' or opt.dest in ("out", "top"):
				continue
			review_options[opt.dest] = opt
			review_defaults[opt.dest] = opt.default
			if gr.defaults.has_key(opt.dest):
				del gr.defaults[opt.dest]
			opt.default = None

	def parse_args(self):
		self.prepare_config_review()
		self.parser.get_option('--prefix').help = 'installation prefix'
		super(OptionsReview, self).parse_args()
		Context.create_context('review').refresh_review_set()

class ReviewContext(Context.Context):
	'''reviews the configuration values'''

	cmd = 'review'

	def __init__(self, **kw):
		super(self.__class__, self).__init__(**kw)

		out = Options.options.out
		if not out:
			out = getattr(Context.g_module, Context.OUT, None)
		if not out:
			out = Options.lockfile.replace('.lock-waf', '')
		self.build_path = (os.path.isabs(out) and self.root or self.path).make_node(out).abspath()
		"""Path to the build directory"""

		self.cache_path = os.path.join(self.build_path, Build.CACHE_DIR)
		"""Path to the cache directory"""

		self.review_path = os.path.join(self.cache_path, 'review.cache')
		"""Path to the review cache file"""

	def execute(self):
		"""
		Display and store the review set. Invalidate the cache as required.
		"""
		if not self.compare_review_set(old_review_set, new_review_set):
			self.invalidate_cache()
		self.store_review_set(new_review_set)
		print(self.display_review_set(new_review_set))

	def invalidate_cache(self):
		"""Invalidate the cache to prevent bad builds."""
		try:
			Logs.warn("Removing the cached configuration since the options have changed")
			shutil.rmtree(self.cache_path)
		except:
			pass

	def refresh_review_set(self):
		"""
		Obtain the old review set and the new review set, and import the new set.
		"""
		global old_review_set, new_review_set
		old_review_set = self.load_review_set()
		new_review_set = self.update_review_set(old_review_set)
		self.import_review_set(new_review_set)

	def load_review_set(self):
		"""
		Load and return the review set from the cache if it exists.
		Otherwise, return an empty set.
		"""
		if os.path.isfile(self.review_path):
			return ConfigSet.ConfigSet(self.review_path)
		return ConfigSet.ConfigSet()

	def store_review_set(self, review_set):
		"""
		Store the review set specified in the cache.
		"""
		if not os.path.isdir(self.cache_path):
			os.makedirs(self.cache_path)
		review_set.store(self.review_path)

	def update_review_set(self, old_set):
		"""
		Merge the options passed on the command line with those imported
		from the previous review set and return the corresponding
		preview set.
		"""

		# Convert value to string. It's important that 'None' maps to
		# the empty string.
		def val_to_str(val):
			if val == None or val == '':
				return ''
			return str(val)

		new_set = ConfigSet.ConfigSet()
		opt_dict = Options.options.__dict__

		for name in review_options.keys():
			# the option is specified explicitly on the command line
			if name in opt_dict:
				# if the option is the default, pretend it was never specified
				if val_to_str(opt_dict[name]) != val_to_str(review_defaults[name]):
					new_set[name] = opt_dict[name]
			# the option was explicitly specified in a previous command
			elif name in old_set:
				new_set[name] = old_set[name]

		return new_set

	def import_review_set(self, review_set):
		"""
		Import the actual value of the reviewable options in the option
		dictionary, given the current review set.
		"""
		for name in review_options.keys():
			if name in review_set:
				value = review_set[name]
			else:
				value = review_defaults[name]
			setattr(Options.options, name, value)

	def compare_review_set(self, set1, set2):
		"""
		Return true if the review sets specified are equal.
		"""
		if len(set1.keys()) != len(set2.keys()):
			return False
		for key in set1.keys():
			if not key in set2 or set1[key] != set2[key]:
				return False
		return True

	def display_review_set(self, review_set):
		"""
		Return the string representing the review set specified.
		"""
		term_width = Logs.get_term_cols()
		lines = []
		for dest in review_options.keys():
			opt = review_options[dest]
			name = ", ".join(opt._short_opts + opt._long_opts)
			help = opt.help
			actual = None
			if dest in review_set:
				actual = review_set[dest]
			default = review_defaults[dest]
			lines.append(self.format_option(name, help, actual, default, term_width))
		return "Configuration:\n\n" + "\n\n".join(lines) + "\n"

	def format_option(self, name, help, actual, default, term_width):
		"""
		Return the string representing the option specified.
		"""
		def val_to_str(val):
			if val == None or val == '':
				return "(void)"
			return str(val)

		max_name_len = 20
		sep_len = 2

		w = textwrap.TextWrapper()
		w.width = term_width - 1
		if w.width < 60:
			w.width = 60

		out = ""

		# format the help
		out += w.fill(help) + "\n"

		# format the name
		name_len = len(name)
		out += Logs.colors.CYAN + name + Logs.colors.NORMAL

		# set the indentation used when the value wraps to the next line
		w.subsequent_indent = " ".rjust(max_name_len + sep_len)
		w.width -= (max_name_len + sep_len)

		# the name string is too long, switch to the next line
		if name_len > max_name_len:
			out += "\n" + w.subsequent_indent

		# fill the remaining of the line with spaces
		else:
			out += " ".rjust(max_name_len + sep_len - name_len)

		# format the actual value, if there is one
		if actual != None:
			out += Logs.colors.BOLD + w.fill(val_to_str(actual)) + Logs.colors.NORMAL + "\n" + w.subsequent_indent

		# format the default value
		default_fmt = val_to_str(default)
		if actual != None:
			default_fmt = "default: " + default_fmt
		out += Logs.colors.NORMAL + w.fill(default_fmt) + Logs.colors.NORMAL

		return out

# Monkey-patch ConfigurationContext.execute() to have it store the review set.
old_configure_execute = Configure.ConfigurationContext.execute
def new_configure_execute(self):
	old_configure_execute(self)
	Context.create_context('review').store_review_set(new_review_set)
Configure.ConfigurationContext.execute = new_configure_execute