tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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