waf/waflib/extras/review.py

326 lines
8.9 KiB
Python

#!/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