summaryrefslogtreecommitdiff
path: root/autooptions/__init__.py
blob: 4e8d4018ca972f8044af378c7a45e8993d80004f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
#
# Copyright (C) 2017 Karl Linden
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the
#    distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

import optparse
import sys
from waflib import Configure, Logs, Options, Utils

# A list of AutoOptions. It is local to each module, so all modules that
# use AutoOptions need to run both opt.load and conf.load. In contrast
# to the define and style options this does not need to and cannot be
# declared in the OptionsContext, because it is needed both for the
# options and the configure phase.
auto_options = []

class AutoOption:
    """
    This class represents an auto option that can be used in conjunction
    with the waf build system. By default it adds options --foo and
    --no-foo respectively to turn on or off foo respectively.
    Furthermore it incorporats logic and checks that are required for
    these features.

    An option can have an arbitrary number of dependencies that must be
    present for the option to be enabled. An option can be enabled or
    disabled by default. Here is how the logic works:
     1. If the option is explicitly disabled, through --no-foo, then no
        checks are made and the option is disabled.
     2. If the option is explicitly enabled, through --foo, then check
        for all required dependencies, and if some of them are not
        found, then print a fatal error telling the user there were
        dependencies missing.
     3. Otherwise, if the option is enabled by default, then check for
        all dependencies. If all dependencies are found the option is
        enabled. Otherwise it is disabled.
     4. Lastly, if no option was given and the option is disabled by
        default, then no checks are performed and the option is
        disabled.

    To add a dependency to an option use the check, check_cfg and
    find_program methods of this class. The methods are merely small
    wrappers around their configuration context counterparts and behave
    identically. Note that adding dependencies is done in the options
    phase and not in the configure phase, although the checks are
    actually executed during the configure phase.

    Custom check functions can be added using the add_function method.
    As with the other checks the check function will be invoked during
    the configuration. Refer to the documentation of the add_function
    method for details.

    When all checks have been made and the class has made a decision the
    result is saved in conf.env['NAME'] where 'NAME' by default is the
    uppercase of the name argument to __init__, with hyphens replaced by
    underscores. This default can be changed with the conf_dest argument
    to __init__.

    The class will define a preprocessor symbol with the result. The
    default name is WITH_NAME, to not collide with the standard define
    of check_cfg, but it can be changed using the define argument to
    __init__. It can also be changed globally using
    set_auto_options_define.
    """

    def __init__(self, opt, name, help=None, default=True,
            conf_dest=None, define=None, style=None):
        """
        Class initializing method.

        Arguments:
            opt       OptionsContext
            name      name of the option, e.g. alsa
            help      help text that will be displayed in --help output
            conf_dest conf.env variable to define to the result
            define    the preprocessor symbol to define with the result
            style     the option style to use; see below for options
        """

        # The dependencies to check for. The elements are on the form
        # (func, k, kw) where func is the function or function name that
        # is used for the check and k and kw are the arguments and
        # options to give the function.
        self.deps = []

        # Whether or not the option should be enabled. None indicates
        # that the checks have not been performed yet.
        self.enable = None

        self.help = help
        if help:
            if default:
                help_comment = ' (enabled by default if possible)'
            else:
                help_comment = ' (disabled by default)'
            option_help = help + help_comment
            no_option_help = None
        else:
            option_help = no_option_help = optparse.SUPPRESS_HELP

        self.dest = 'auto_option_' + name

        self.default = default

        safe_name = Utils.quote_define_name(name)
        self.conf_dest = conf_dest or safe_name

        default_define = opt.get_auto_options_define()
        self.define = define or default_define % safe_name

        if not style:
            style = opt.get_auto_options_style()
        self.style = style

        # plain (default):
        #   --foo | --no-foo
        # yesno:
        #   --foo=yes | --foo=no
        # yesno_and_hack:
        #  --foo[=yes] | --foo=no or --no-foo
        # enable:
        #  --enable-foo | --disble-foo
        # with:
        #  --with-foo | --without-foo
        if style in ['plain', 'yesno', 'yesno_and_hack']:
            self.no_option = '--no-' + name
            self.yes_option = '--' + name
        elif style == 'enable':
            self.no_option = '--disable-' + name
            self.yes_option = '--enable-' + name
        elif style == 'with':
            self.no_option = '--without-' + name
            self.yes_option = '--with-' + name
        else:
            opt.fatal('invalid style')

        if style in ['yesno', 'yesno_and_hack']:
            opt.add_option(
                    self.yes_option,
                    dest=self.dest,
                    action='store',
                    choices=['auto', 'no', 'yes'],
                    default='auto',
                    help=option_help,
                    metavar='no|yes')
        else:
            opt.add_option(
                    self.yes_option,
                    dest=self.dest,
                    action='store_const',
                    const='yes',
                    default='auto',
                    help=option_help)
            opt.add_option(
                    self.no_option,
                    dest=self.dest,
                    action='store_const',
                    const='no',
                    default='auto',
                    help=no_option_help)

    def check(self, *k, **kw):
        self.deps.append(('check', k, kw))
    def check_cfg(self, *k, **kw):
        self.deps.append(('check_cfg', k, kw))
    def find_program(self, *k, **kw):
        self.deps.append(('find_program', k, kw))

    def add_function(self, func, *k, **kw):
        """
        Add a custom function to be invoked as part of the
        configuration. During the configuration the function will be
        invoked with the configuration context as first argument
        followed by the arguments to this method, except for the func
        argument. The function must print a 'Checking for...' message,
        because it is referred to if the check fails and this option is
        requested.

        On configuration error the function shall raise
        conf.errors.ConfigurationError.
        """
        self.deps.append((func, k, kw))

    def _check(self, conf, required):
        """
        This private method checks all dependencies. It checks all
        dependencies (even if some dependency was not found) so that the
        user can install all missing dependencies in one go, instead of
        playing the infamous hit-configure-hit-configure game.

        This function returns True if all dependencies were found and
        False otherwise.
        """
        all_found = True

        for (f,k,kw) in self.deps:
            if hasattr(f, '__call__'):
                # This is a function supplied by add_function.
                func = f
                k = list(k)
                k.insert(0, conf)
                k = tuple(k)
            else:
                func = getattr(conf, f)

            try:
                func(*k, **kw)
            except conf.errors.ConfigurationError:
                all_found = False
                if required:
                    Logs.error('The above check failed, but the '
                               'checkee is required for %s.' %
                               self.yes_option)

        return all_found

    def configure(self, conf):
        """
        This function configures the option examining the command line
        option. It sets self.enable to whether this options should be
        enabled or not, that is True or False respectively. If not all
        dependencies were found self.enable will be False.
        conf.env['NAME'] and a preprocessor symbol will be defined with
        the result.

        If the option was desired but one or more dependencies were not
        found the an error message will be printed for each missing
        dependency.

        This function returns True on success and False on if the option
        was requested but cannot be enabled.
        """
        # If the option has already been configured once, do not
        # configure it again.
        if self.enable != None:
            return True

        argument = getattr(Options.options, self.dest)
        if argument == 'no':
            self.enable = False
            retvalue = True
        elif argument == 'yes':
            retvalue = self.enable = self._check(conf, True)
        else:
            self.enable = self.default and self._check(conf, False)
            retvalue = True

        conf.env[self.conf_dest] = self.enable
        conf.define(self.define, int(self.enable))
        return retvalue

    def summarize(self, conf):
        """
        This function displays a result summary with the help text and
        the result of the configuration.
        """
        if self.help:
            if self.enable:
                conf.msg(self.help, 'yes', color='GREEN')
            else:
                conf.msg(self.help, 'no', color='YELLOW')

def options(opt):
    """
    This function declares necessary variables in the option context.
    The reason for saving variables in the option context is to allow
    autooptions to be loaded from modules (which will receive a new
    instance of this module, clearing any global variables) with a
    uniform style and default in the entire project.

    Call this function through opt.load('autooptions').
    """
    if not hasattr(opt, 'auto_options_style'):
        opt.auto_options_style = 'plain'
    if not hasattr(opt, 'auto_options_define'):
        opt.auto_options_define = 'WITH_%s'

def opt(f):
    """
    Decorator: attach a new option function to Options.OptionsContext.

    :param f: method to bind
    :type f: function
    """
    setattr(Options.OptionsContext, f.__name__, f)

@opt
def add_auto_option(self, *k, **kw):
    """
    This function adds an AutoOption to the options context. It takes
    the same arguments as the initializer function of the AutoOptions
    class.
    """
    option = AutoOption(self, *k, **kw)
    auto_options.append(option)
    return option

@opt
def get_auto_options_define(self):
    """
    This function gets the default define name. This default can be
    changed through set_auto_optoins_define.
    """
    return self.auto_options_define

@opt
def set_auto_options_define(self, define):
    """
    This function sets the default define name. The default is
    'WITH_%s', where %s will be replaced with the name of the option in
    uppercase.
    """
    self.auto_options_define = define

@opt
def get_auto_options_style(self):
    """
    This function gets the default option style, which will be used for
    the subsequent options.
    """
    return self.auto_options_style

@opt
def set_auto_options_style(self, style):
    """
    This function sets the default option style, which will be used for
    the subsequent options.
    """
    self.auto_options_style = style

@opt
def apply_auto_options_hack(self):
    """
    This function applies the hack necessary for the yesno_and_hack
    option style. The hack turns --foo into --foo=yes and --no-foo into
    --foo=no.

    It must be called before options are parsed, that is before the
    configure phase.
    """
    for option in auto_options:
        # With the hack the yesno options simply extend plain options.
        if option.style == 'yesno_and_hack':
            for i in range(1, len(sys.argv)):
                if sys.argv[i] == option.yes_option:
                    sys.argv[i] = option.yes_option + '=yes'
                elif sys.argv[i] == option.no_option:
                    sys.argv[i] = option.yes_option + '=no'

@Configure.conf
def summarize_auto_options(self):
    """
    This function prints a summary of the configuration of the auto
    options. Obviously, it must be called after
    conf.load('autooptions').
    """
    for option in auto_options:
        option.summarize(self)

def configure(conf):
    """
    This configures all auto options. Call it through
    conf.load('autooptions').
    """
    ok = True
    for option in auto_options:
        if not option.configure(conf):
            ok = False
    if not ok:
        conf.fatal('Some requested options had unsatisfied ' +
                'dependencies.\n' +
                'See the above configuration for details.')