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