1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import, print_function, unicode_literals 6 7from collections import OrderedDict 8import inspect 9import os 10import six 11import sys 12 13 14HELP_OPTIONS_CATEGORY = 'Help options' 15# List of whitelisted option categories. If you want to add a new category, 16# simply add it to this list; however, exercise discretion as 17# "./configure --help" becomes less useful if there are an excessive number of 18# categories. 19_ALL_CATEGORIES = ( 20 HELP_OPTIONS_CATEGORY, 21) 22 23 24def _infer_option_category(define_depth): 25 stack_frame = inspect.stack(0)[3 + define_depth] 26 try: 27 path = os.path.relpath(stack_frame[0].f_code.co_filename) 28 except ValueError: 29 # If this call fails, it means the relative path couldn't be determined 30 # (e.g. because this file is on a different drive than the cwd on a 31 # Windows machine). That's fine, just use the absolute filename. 32 path = stack_frame[0].f_code.co_filename 33 return 'Options from ' + path 34 35 36def istupleofstrings(obj): 37 return isinstance(obj, tuple) and len(obj) and all( 38 isinstance(o, six.string_types) for o in obj) 39 40 41class OptionValue(tuple): 42 '''Represents the value of a configure option. 43 44 This class is not meant to be used directly. Use its subclasses instead. 45 46 The `origin` attribute holds where the option comes from (e.g. environment, 47 command line, or default) 48 ''' 49 def __new__(cls, values=(), origin='unknown'): 50 return super(OptionValue, cls).__new__(cls, values) 51 52 def __init__(self, values=(), origin='unknown'): 53 self.origin = origin 54 55 def format(self, option): 56 if option.startswith('--'): 57 prefix, name, values = Option.split_option(option) 58 assert values == () 59 for prefix_set in ( 60 ('disable', 'enable'), 61 ('without', 'with'), 62 ): 63 if prefix in prefix_set: 64 prefix = prefix_set[int(bool(self))] 65 break 66 if prefix: 67 option = '--%s-%s' % (prefix, name) 68 elif self: 69 option = '--%s' % name 70 else: 71 return '' 72 if len(self): 73 return '%s=%s' % (option, ','.join(self)) 74 return option 75 elif self and not len(self): 76 return '%s=1' % option 77 return '%s=%s' % (option, ','.join(self)) 78 79 def __eq__(self, other): 80 # This is to catch naive comparisons against strings and other 81 # types in moz.configure files, as it is really easy to write 82 # value == 'foo'. We only raise a TypeError for instances that 83 # have content, because value-less instances (like PositiveOptionValue 84 # and NegativeOptionValue) are common and it is trivial to 85 # compare these. 86 if not isinstance(other, tuple) and len(self): 87 raise TypeError('cannot compare a populated %s against an %s; ' 88 'OptionValue instances are tuples - did you mean to ' 89 'compare against member elements using [x]?' % ( 90 type(other).__name__, type(self).__name__)) 91 92 # Allow explicit tuples to be compared. 93 if type(other) == tuple: 94 return tuple.__eq__(self, other) 95 elif isinstance(other, bool): 96 return bool(self) == other 97 # Else we're likely an OptionValue class. 98 elif type(other) != type(self): 99 return False 100 else: 101 return super(OptionValue, self).__eq__(other) 102 103 def __ne__(self, other): 104 return not self.__eq__(other) 105 106 def __repr__(self): 107 return '%s%s' % (self.__class__.__name__, 108 super(OptionValue, self).__repr__()) 109 110 @staticmethod 111 def from_(value): 112 if isinstance(value, OptionValue): 113 return value 114 elif value is True: 115 return PositiveOptionValue() 116 elif value is False or value == (): 117 return NegativeOptionValue() 118 elif isinstance(value, six.string_types): 119 return PositiveOptionValue((value,)) 120 elif isinstance(value, tuple): 121 return PositiveOptionValue(value) 122 else: 123 raise TypeError("Unexpected type: '%s'" 124 % type(value).__name__) 125 126 127class PositiveOptionValue(OptionValue): 128 '''Represents the value for a positive option (--enable/--with/--foo) 129 in the form of a tuple for when values are given to the option (in the form 130 --option=value[,value2...]. 131 ''' 132 133 def __nonzero__(self): # py2 134 return True 135 136 def __bool__(self): # py3 137 return True 138 139 140class NegativeOptionValue(OptionValue): 141 '''Represents the value for a negative option (--disable/--without) 142 143 This is effectively an empty tuple with a `origin` attribute. 144 ''' 145 def __new__(cls, origin='unknown'): 146 return super(NegativeOptionValue, cls).__new__(cls, origin=origin) 147 148 def __init__(self, origin='unknown'): 149 return super(NegativeOptionValue, self).__init__(origin=origin) 150 151 152class InvalidOptionError(Exception): 153 pass 154 155 156class ConflictingOptionError(InvalidOptionError): 157 def __init__(self, message, **format_data): 158 if format_data: 159 message = message.format(**format_data) 160 super(ConflictingOptionError, self).__init__(message) 161 for k, v in six.iteritems(format_data): 162 setattr(self, k, v) 163 164 165class Option(object): 166 '''Represents a configure option 167 168 A configure option can be a command line flag or an environment variable 169 or both. 170 171 - `name` is the full command line flag (e.g. --enable-foo). 172 - `env` is the environment variable name (e.g. ENV) 173 - `nargs` is the number of arguments the option may take. It can be a 174 number or the special values '?' (0 or 1), '*' (0 or more), or '+' (1 or 175 more). 176 - `default` can be used to give a default value to the option. When the 177 `name` of the option starts with '--enable-' or '--with-', the implied 178 default is an empty PositiveOptionValue. When it starts with '--disable-' 179 or '--without-', the implied default is a NegativeOptionValue. 180 - `choices` restricts the set of values that can be given to the option. 181 - `help` is the option description for use in the --help output. 182 - `possible_origins` is a tuple of strings that are origins accepted for 183 this option. Example origins are 'mozconfig', 'implied', and 'environment'. 184 - `category` is a human-readable string used only for categorizing command- 185 line options when displaying the output of `configure --help`. If not 186 supplied, the script will attempt to infer an appropriate category based 187 on the name of the file where the option was defined. If supplied it must 188 be in the _ALL_CATEGORIES list above. 189 - `define_depth` should generally only be used by templates that are used 190 to instantiate an option indirectly. Set this to a positive integer to 191 force the script to look into a deeper stack frame when inferring the 192 `category`. 193 ''' 194 __slots__ = ( 195 'id', 'prefix', 'name', 'env', 'nargs', 'default', 'choices', 'help', 196 'possible_origins', 'category', 'define_depth', 197 ) 198 199 def __init__(self, name=None, env=None, nargs=None, default=None, 200 possible_origins=None, choices=None, category=None, help=None, 201 define_depth=0): 202 if not name and not env: 203 raise InvalidOptionError( 204 'At least an option name or an environment variable name must ' 205 'be given') 206 if name: 207 if not isinstance(name, six.string_types): 208 raise InvalidOptionError('Option must be a string') 209 if not name.startswith('--'): 210 raise InvalidOptionError('Option must start with `--`') 211 if '=' in name: 212 raise InvalidOptionError('Option must not contain an `=`') 213 if not name.islower(): 214 raise InvalidOptionError('Option must be all lowercase') 215 if env: 216 if not isinstance(env, six.string_types): 217 raise InvalidOptionError( 218 'Environment variable name must be a string') 219 if not env.isupper(): 220 raise InvalidOptionError( 221 'Environment variable name must be all uppercase') 222 if nargs not in (None, '?', '*', '+') and not ( 223 isinstance(nargs, int) and nargs >= 0): 224 raise InvalidOptionError( 225 "nargs must be a positive integer, '?', '*' or '+'") 226 if (not isinstance(default, six.string_types) and 227 not isinstance(default, (bool, type(None))) and 228 not istupleofstrings(default)): 229 raise InvalidOptionError( 230 'default must be a bool, a string or a tuple of strings') 231 if choices and not istupleofstrings(choices): 232 raise InvalidOptionError( 233 'choices must be a tuple of strings') 234 if category and not isinstance(category, six.string_types): 235 raise InvalidOptionError('Category must be a string') 236 if category and category not in _ALL_CATEGORIES: 237 raise InvalidOptionError( 238 'Category must either be inferred or in the _ALL_CATEGORIES ' 239 'list in options.py: %s' % ', '.join(_ALL_CATEGORIES)) 240 if not isinstance(define_depth, int): 241 raise InvalidOptionError('DefineDepth must be an integer') 242 if not help: 243 raise InvalidOptionError('A help string must be provided') 244 if possible_origins and not istupleofstrings(possible_origins): 245 raise InvalidOptionError( 246 'possible_origins must be a tuple of strings') 247 self.possible_origins = possible_origins 248 249 if name: 250 prefix, name, values = self.split_option(name) 251 assert values == () 252 253 # --disable and --without options mean the default is enabled. 254 # --enable and --with options mean the default is disabled. 255 # However, we allow a default to be given so that the default 256 # can be affected by other factors. 257 if prefix: 258 if default is None: 259 default = prefix in ('disable', 'without') 260 elif default is False: 261 prefix = { 262 'disable': 'enable', 263 'without': 'with', 264 }.get(prefix, prefix) 265 elif default is True: 266 prefix = { 267 'enable': 'disable', 268 'with': 'without', 269 }.get(prefix, prefix) 270 else: 271 prefix = '' 272 273 self.prefix = prefix 274 self.name = name 275 self.env = env 276 if default in (None, False): 277 self.default = NegativeOptionValue(origin='default') 278 elif isinstance(default, tuple): 279 self.default = PositiveOptionValue(default, origin='default') 280 elif default is True: 281 self.default = PositiveOptionValue(origin='default') 282 else: 283 self.default = PositiveOptionValue((default,), origin='default') 284 if nargs is None: 285 nargs = 0 286 if len(self.default) == 1: 287 nargs = '?' 288 elif len(self.default) > 1: 289 nargs = '*' 290 elif choices: 291 nargs = 1 292 self.nargs = nargs 293 has_choices = choices is not None 294 if isinstance(self.default, PositiveOptionValue): 295 if has_choices and len(self.default) == 0: 296 raise InvalidOptionError( 297 'A `default` must be given along with `choices`') 298 if not self._validate_nargs(len(self.default)): 299 raise InvalidOptionError( 300 "The given `default` doesn't satisfy `nargs`") 301 if has_choices and not all(d in choices for d in self.default): 302 raise InvalidOptionError( 303 'The `default` value must be one of %s' % 304 ', '.join("'%s'" % c for c in choices)) 305 elif has_choices: 306 maxargs = self.maxargs 307 if len(choices) < maxargs and maxargs != sys.maxsize: 308 raise InvalidOptionError('Not enough `choices` for `nargs`') 309 self.choices = choices 310 self.help = help 311 self.category = category or _infer_option_category(define_depth) 312 313 @staticmethod 314 def split_option(option): 315 '''Split a flag or variable into a prefix, a name and values 316 317 Variables come in the form NAME=values (no prefix). 318 Flags come in the form --name=values or --prefix-name=values 319 where prefix is one of 'with', 'without', 'enable' or 'disable'. 320 The '=values' part is optional. Values are separated with commas. 321 ''' 322 if not isinstance(option, six.string_types): 323 raise InvalidOptionError('Option must be a string') 324 325 elements = option.split('=', 1) 326 name = elements[0] 327 values = tuple(elements[1].split(',')) if len(elements) == 2 else () 328 if name.startswith('--'): 329 name = name[2:] 330 if not name.islower(): 331 raise InvalidOptionError('Option must be all lowercase') 332 elements = name.split('-', 1) 333 prefix = elements[0] 334 if len(elements) == 2 and prefix in ('enable', 'disable', 335 'with', 'without'): 336 return prefix, elements[1], values 337 else: 338 if name.startswith('-'): 339 raise InvalidOptionError( 340 'Option must start with two dashes instead of one') 341 if name.islower(): 342 raise InvalidOptionError( 343 'Environment variable name "%s" must be all uppercase' % name) 344 return '', name, values 345 346 @staticmethod 347 def _join_option(prefix, name): 348 # The constraints around name and env in __init__ make it so that 349 # we can distinguish between flags and environment variables with 350 # islower/isupper. 351 if name.isupper(): 352 assert not prefix 353 return name 354 elif prefix: 355 return '--%s-%s' % (prefix, name) 356 return '--%s' % name 357 358 @property 359 def option(self): 360 if self.prefix or self.name: 361 return self._join_option(self.prefix, self.name) 362 else: 363 return self.env 364 365 @property 366 def minargs(self): 367 if isinstance(self.nargs, int): 368 return self.nargs 369 return 1 if self.nargs == '+' else 0 370 371 @property 372 def maxargs(self): 373 if isinstance(self.nargs, int): 374 return self.nargs 375 return 1 if self.nargs == '?' else sys.maxsize 376 377 def _validate_nargs(self, num): 378 minargs, maxargs = self.minargs, self.maxargs 379 return num >= minargs and num <= maxargs 380 381 def get_value(self, option=None, origin='unknown'): 382 '''Given a full command line option (e.g. --enable-foo=bar) or a 383 variable assignment (FOO=bar), returns the corresponding OptionValue. 384 385 Note: variable assignments can come from either the environment or 386 from the command line (e.g. `../configure CFLAGS=-O2`) 387 ''' 388 if not option: 389 return self.default 390 391 if self.possible_origins and origin not in self.possible_origins: 392 raise InvalidOptionError( 393 '%s can not be set by %s. Values are accepted from: %s' % 394 (option, origin, ', '.join(self.possible_origins))) 395 396 prefix, name, values = self.split_option(option) 397 option = self._join_option(prefix, name) 398 399 assert name in (self.name, self.env) 400 401 if prefix in ('disable', 'without'): 402 if values != (): 403 raise InvalidOptionError('Cannot pass a value to %s' % option) 404 return NegativeOptionValue(origin=origin) 405 406 if name == self.env: 407 if values == ('',): 408 return NegativeOptionValue(origin=origin) 409 if self.nargs in (0, '?', '*') and values == ('1',): 410 return PositiveOptionValue(origin=origin) 411 412 values = PositiveOptionValue(values, origin=origin) 413 414 if not self._validate_nargs(len(values)): 415 raise InvalidOptionError('%s takes %s value%s' % ( 416 option, 417 { 418 '?': '0 or 1', 419 '*': '0 or more', 420 '+': '1 or more', 421 }.get(self.nargs, str(self.nargs)), 422 's' if (not isinstance(self.nargs, int) or 423 self.nargs != 1) else '' 424 )) 425 426 if len(values) and self.choices: 427 relative_result = None 428 for val in values: 429 if self.nargs in ('+', '*'): 430 if val.startswith(('+', '-')): 431 if relative_result is None: 432 relative_result = list(self.default) 433 sign = val[0] 434 val = val[1:] 435 if sign == '+': 436 if val not in relative_result: 437 relative_result.append(val) 438 else: 439 try: 440 relative_result.remove(val) 441 except ValueError: 442 pass 443 444 if val not in self.choices: 445 raise InvalidOptionError( 446 "'%s' is not one of %s" 447 % (val, ', '.join("'%s'" % c for c in self.choices))) 448 449 if relative_result is not None: 450 values = PositiveOptionValue(relative_result, origin=origin) 451 452 return values 453 454 def __repr__(self): 455 return '<%s [%s]>' % (self.__class__.__name__, self.option) 456 457 458class CommandLineHelper(object): 459 '''Helper class to handle the various ways options can be given either 460 on the command line of through the environment. 461 462 For instance, an Option('--foo', env='FOO') can be passed as --foo on the 463 command line, or as FOO=1 in the environment *or* on the command line. 464 465 If multiple variants are given, command line is prefered over the 466 environment, and if different values are given on the command line, the 467 last one wins. (This mimicks the behavior of autoconf, avoiding to break 468 existing mozconfigs using valid options in weird ways) 469 470 Extra options can be added afterwards through API calls. For those, 471 conflicting values will raise an exception. 472 ''' 473 474 def __init__(self, environ=os.environ, argv=sys.argv): 475 self._environ = dict(environ) 476 self._args = OrderedDict() 477 self._extra_args = OrderedDict() 478 self._origins = {} 479 self._last = 0 480 481 assert(argv and not argv[0].startswith('--')) 482 for arg in argv[1:]: 483 self.add(arg, 'command-line', self._args) 484 485 def add(self, arg, origin='command-line', args=None): 486 assert origin != 'default' 487 prefix, name, values = Option.split_option(arg) 488 if args is None: 489 args = self._extra_args 490 if args is self._extra_args and name in self._extra_args: 491 old_arg = self._extra_args[name][0] 492 old_prefix, _, old_values = Option.split_option(old_arg) 493 if prefix != old_prefix or values != old_values: 494 raise ConflictingOptionError( 495 "Cannot add '{arg}' to the {origin} set because it " 496 "conflicts with '{old_arg}' that was added earlier", 497 arg=arg, origin=origin, old_arg=old_arg, 498 old_origin=self._origins[old_arg]) 499 self._last += 1 500 args[name] = arg, self._last 501 self._origins[arg] = origin 502 503 def _prepare(self, option, args): 504 arg = None 505 origin = 'command-line' 506 from_name = args.get(option.name) 507 from_env = args.get(option.env) 508 if from_name and from_env: 509 arg1, pos1 = from_name 510 arg2, pos2 = from_env 511 arg, pos = (arg1, pos1) if abs(pos1) > abs(pos2) else (arg2, pos2) 512 if args is self._extra_args and (option.get_value(arg1) != 513 option.get_value(arg2)): 514 origin = self._origins[arg] 515 old_arg = arg2 if abs(pos1) > abs(pos2) else arg1 516 raise ConflictingOptionError( 517 "Cannot add '{arg}' to the {origin} set because it " 518 "conflicts with '{old_arg}' that was added earlier", 519 arg=arg, origin=origin, old_arg=old_arg, 520 old_origin=self._origins[old_arg]) 521 elif from_name or from_env: 522 arg, pos = from_name if from_name else from_env 523 elif option.env and args is self._args: 524 env = self._environ.get(option.env) 525 if env is not None: 526 arg = '%s=%s' % (option.env, env) 527 origin = 'environment' 528 529 origin = self._origins.get(arg, origin) 530 531 for k in (option.name, option.env): 532 try: 533 del args[k] 534 except KeyError: 535 pass 536 537 return arg, origin 538 539 def handle(self, option): 540 '''Return the OptionValue corresponding to the given Option instance, 541 depending on the command line, environment, and extra arguments, and 542 the actual option or variable that set it. 543 Only works once for a given Option. 544 ''' 545 assert isinstance(option, Option) 546 547 arg, origin = self._prepare(option, self._args) 548 ret = option.get_value(arg, origin) 549 550 extra_arg, extra_origin = self._prepare(option, self._extra_args) 551 extra_ret = option.get_value(extra_arg, extra_origin) 552 553 if extra_ret.origin == 'default': 554 return ret, arg 555 556 if ret.origin != 'default' and extra_ret != ret: 557 raise ConflictingOptionError( 558 "Cannot add '{arg}' to the {origin} set because it conflicts " 559 "with {old_arg} from the {old_origin} set", arg=extra_arg, 560 origin=extra_ret.origin, old_arg=arg, old_origin=ret.origin) 561 562 return extra_ret, extra_arg 563 564 def __iter__(self): 565 for d in (self._args, self._extra_args): 566 for arg, pos in six.itervalues(d): 567 yield arg 568