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