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