source: rtems-tools/rtemstoolkit/options.py @ 7e5cdea

Last change on this file since 7e5cdea was 7e5cdea, checked in by Chris Johns <chrisj@…>, on Nov 23, 2018 at 4:02:52 AM

rtemstoolkit: Add unit testing for the python modules

  • Add support to run the unit tests for the rtemstoolkit python modules from waf. Enter './waf test' for the tests to be run on python2 and python3.
  • Update the importing of rtemstoolkit modules to the standard method which works on python2 and python3.
  • Update the README.
  • Property mode set to 100644
File size: 20.1 KB
Line 
1#
2# RTEMS Tools Project (http://www.rtems.org/)
3# Copyright 2010-2016 Chris Johns (chrisj@rtems.org)
4# All rights reserved.
5#
6# This file is part of the RTEMS Tools package in 'rtems-tools'.
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions are met:
10#
11# 1. Redistributions of source code must retain the above copyright notice,
12# this list of conditions and the following disclaimer.
13#
14# 2. Redistributions in binary form must reproduce the above copyright notice,
15# this list of conditions and the following disclaimer in the documentation
16# and/or other materials provided with the distribution.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28# POSSIBILITY OF SUCH DAMAGE.
29#
30
31#
32# Determine the defaults and load the specific file.
33#
34
35from __future__ import print_function
36
37import copy
38import glob
39import pprint
40import re
41import os
42import string
43import sys
44
45from rtemstoolkit import error
46from rtemstoolkit import execute
47from rtemstoolkit import git
48from rtemstoolkit import host
49from rtemstoolkit import log
50from rtemstoolkit import macros
51from rtemstoolkit import path
52from rtemstoolkit import version
53
54basepath = 'tb'
55
56class command_line(object):
57    """Process the command line in a common way for all Tool Builder commands."""
58
59    def __init__(self, base_path = None, argv = None, optargs = None,
60                 defaults = None, long_opts = None, long_opts_help = None,
61                 command_path = '', log_default = None):
62
63        if argv is None:
64            return
65
66        global basepath
67
68        if long_opts == None:
69            long_opts = {}
70
71        basepath = base_path
72
73        if log_default is not None and type(log_default) is not list:
74            raise error.general('log default is not a list')
75        self.log_default = log_default
76
77        if defaults is None:
78            defaults = macros.macros()
79
80        self.long_opts = {
81            # key                 macro                handler            param  defs       init
82            '--jobs'           : ('_jobs',             self._lo_jobs,     True,  'default', True),
83            '--log'            : ('_logfile',          self._lo_string,   True,  None,      False),
84            '--macros'         : ('_macros',           self._lo_string,   True,  None,      False),
85            '--force'          : ('_force',            self._lo_bool,     False, '0',       True),
86            '--quiet'          : ('_quiet',            self._lo_bool,     False, '0',       True),
87            '--trace'          : ('_trace',            self._lo_bool,     False, '0',       True),
88            '--dry-run'        : ('_dry_run',          self._lo_bool,     False, '0',       True),
89            '--warn-all'       : ('_warn_all',         self._lo_bool,     False, '0',       True),
90            '--no-clean'       : ('_no_clean',         self._lo_bool,     False, '0',       True),
91            '--keep-going'     : ('_keep_going',       self._lo_bool,     False, '0',       True),
92            '--always-clean'   : ('_always_clean',     self._lo_bool,     False, '0',       True),
93            '--no-install'     : ('_no_install',       self._lo_bool,     False, '0',       True),
94            '--help'           : (None,                self._lo_help,     False, None,      False)
95        }
96        self.long_opts_help = {
97            '--force':                      'Force the build to proceed',
98            '--quiet':                      'Quiet output (not used)',
99            '--trace':                      'Trace the execution',
100            '--dry-run':                    'Do everything but actually run the build',
101            '--warn-all':                   'Generate warnings',
102            '--no-clean':                   'Do not clean up the build tree',
103            '--always-clean':               'Always clean the build tree, even with an error',
104            '--keep-going':                 'Do not stop on an error.',
105            '--jobs=[0..n,none,half,full]': 'Run with specified number of jobs, default: num CPUs.',
106            '--macros file[,file]':         'Macro format files to load after the defaults',
107            '--log file':                   'Log file where all build output is written to',
108        }
109        self.opts = { 'params' : [] }
110        self.command_path = command_path
111        self.command_name = path.basename(argv[0])
112        self.argv = argv
113        self.args = argv[1:]
114        self.optargs = optargs
115        self.defaults = defaults
116        for lo in self.long_opts:
117            self.opts[lo[2:]] = self.long_opts[lo][3]
118            if self.long_opts[lo][4]:
119                self.defaults[self.long_opts[lo][0]] = ('none', 'none', self.long_opts[lo][3])
120        for lo in long_opts:
121            if lo in self.long_opts:
122                raise error.general('suplicate option: %s' % (lo))
123            self.opts[lo[2:]] = long_opts[lo][3]
124            if long_opts[lo][4]:
125                self.defaults[long_opts[lo][0]] = ('none', 'none', long_opts[lo][3])
126            if long_opts[lo][1] == 'int':
127                handler = self._lo_int
128            elif long_opts[lo][1] == 'string':
129                handler = self._lo_string
130            elif long_opts[lo][1] == 'path':
131                handler = self._lo_path
132            elif long_opts[lo][1] == 'jobs':
133                handler = self._lo_jobs
134            elif long_opts[lo][1] == 'bool':
135                handler = self._lo_bool
136            elif long_opts[lo][1] == 'triplet':
137                handler = self._lo_triplets
138            else:
139                raise error.general('invalid option handler: %s: %s' % (lo, long_opts[lo][1]))
140            self.long_opts[lo] = (long_opts[lo][0], handler, long_opts[lo][2],
141                                   long_opts[lo][3], long_opts[lo][4])
142            if long_opts_help is not None:
143                if lo not in long_opts_help:
144                    raise error.general('no help for option: %s' % (lo))
145                self.long_opts_help[lo] = long_opts_help[lo]
146
147    def __copy__(self):
148        new = type(self)()
149        #new.__dict__.update(self.__dict__)
150        new.opts = copy.copy(self.opts)
151        new.command_path = copy.copy(self.command_path)
152        new.command_name = copy.copy(self.command_name)
153        new.argv = self.argv
154        new.args = self.args
155        new.optargs = copy.copy(self.optargs)
156        new.defaults = copy.copy(self.defaults)
157        new.long_opts = copy.copy(self.long_opts)
158        return new
159
160    def __str__(self):
161        def _dict(dd):
162            s = ''
163            ddl = list(dd.keys())
164            ddl.sort()
165            for d in ddl:
166                s += '  ' + d + ': ' + str(dd[d]) + '\n'
167            return s
168
169        s = 'command: ' + self.command() + \
170            '\nargs: ' + str(self.args) + \
171            '\nopts:\n' + _dict(self.opts)
172
173        return s
174
175    def _lo_int(self, opt, macro, value):
176        if value is None:
177            raise error.general('option requires a value: %s' % (opt))
178        try:
179            num = int(value)
180        except:
181            raise error.general('option conversion to int failed: %s' % (opt))
182        self.opts[opt[2:]] = value
183        self.defaults[macro] = value
184
185    def _lo_string(self, opt, macro, value):
186        if value is None:
187            raise error.general('option requires a value: %s' % (opt))
188        self.opts[opt[2:]] = value
189        self.defaults[macro] = value
190
191    def _lo_path(self, opt, macro, value):
192        if value is None:
193            raise error.general('option requires a path: %s' % (opt))
194        value = path.abspath(value)
195        self.opts[opt[2:]] = value
196        self.defaults[macro] = value
197
198    def _lo_jobs(self, opt, macro, value):
199        if value is None:
200            raise error.general('option requires a value: %s' % (opt))
201        ok = False
202        if value in ['max', 'none', 'half']:
203            ok = True
204        else:
205            try:
206                i = int(value)
207                ok = True
208            except:
209                pass
210            if not ok:
211                try:
212                    f = float(value)
213                    ok = True
214                except:
215                    pass
216        if not ok:
217            raise error.general('invalid jobs option: %s' % (value))
218        self.defaults[macro] = value
219        self.opts[opt[2:]] = value
220
221    def _lo_bool(self, opt, macro, value):
222        if value is not None:
223            raise error.general('option does not take a value: %s' % (opt))
224        self.opts[opt[2:]] = '1'
225        self.defaults[macro] = '1'
226
227    def _lo_triplets(self, opt, macro, value):
228        #
229        # This is a target triplet. Run it past config.sub to make make sure it
230        # is ok. The target triplet is 'cpu-vendor-os'.
231        #
232        e = execute.capture_execution()
233        config_sub = path.join(self.command_path,
234                               basepath, 'config.sub')
235        exit_code, proc, output = e.shell(config_sub + ' ' + value)
236        if exit_code == 0:
237            value = output
238        self.defaults[macro] = ('triplet', 'none', value)
239        self.opts[opt[2:]] = value
240        _cpu = macro + '_cpu'
241        _arch = macro + '_arch'
242        _vendor = macro + '_vendor'
243        _os = macro + '_os'
244        _arch_value = ''
245        _vendor_value = ''
246        _os_value = ''
247        dash = value.find('-')
248        if dash >= 0:
249            _arch_value = value[:dash]
250            value = value[dash + 1:]
251        dash = value.find('-')
252        if dash >= 0:
253            _vendor_value = value[:dash]
254            value = value[dash + 1:]
255        if len(value):
256            _os_value = value
257        self.defaults[_cpu]    = _arch_value
258        self.defaults[_arch]   = _arch_value
259        self.defaults[_vendor] = _vendor_value
260        self.defaults[_os]     = _os_value
261
262    def _lo_help(self, opt, macro, value):
263        self.help()
264
265    def _help_indent(self):
266        indent = 0
267        if self.optargs:
268            for o in self.optargs:
269                if len(o) > indent:
270                    indent = len(o)
271        for o in self.long_opts_help:
272            if len(o) > indent:
273                indent = len(o)
274        return indent
275
276    def help(self):
277        print('%s: [options] [args]' % (self.command_name))
278        print('RTEMS Tools Project, %s' % (version.string()))
279        print('Options and arguments:')
280        opts = list(self.long_opts_help.keys())
281        if self.optargs:
282            opts += self.optargs.keys()
283        indent = self._help_indent()
284        for o in sorted(opts):
285            if o in self.long_opts_help:
286                h = self.long_opts_help[o]
287            elif self.optargs:
288                h = self.optargs[o]
289            else:
290                raise error.general('invalid help data: %s' %(o))
291            print('%-*s : %s' % (indent, o, h))
292        raise error.exit()
293
294    def process(self):
295        arg = 0
296        while arg < len(self.args):
297            a = self.args[arg]
298            if a == '-?':
299                self.help()
300            elif a.startswith('--'):
301                los = a.split('=')
302                lo = los[0]
303                if lo in self.long_opts:
304                    long_opt = self.long_opts[lo]
305                    if len(los) == 1:
306                        if long_opt[2]:
307                            if arg == len(self.args) - 1:
308                                raise error.general('option requires a parameter: %s' % (lo))
309                            arg += 1
310                            value = self.args[arg]
311                        else:
312                            value = None
313                    else:
314                        value = '='.join(los[1:])
315                    long_opt[1](lo, long_opt[0], value)
316            else:
317                self.opts['params'].append(a)
318            arg += 1
319
320    def post_process(self):
321        # Handle the log first.
322        log.default = log.log(self.logfiles())
323        if self.trace():
324            log.tracing = True
325        if self.quiet():
326            log.quiet = True
327        # Handle the jobs for make
328        if '_ncpus' not in self.defaults:
329            raise error.general('host number of CPUs not set')
330        ncpus = self.jobs(self.defaults['_ncpus'])
331        if ncpus > 1:
332            self.defaults['_smp_mflags'] = '-j %d' % (ncpus)
333        else:
334            self.defaults['_smp_mflags'] = self.defaults['nil']
335        # Load user macro files
336        um = self.user_macros()
337        if um:
338            checked = path.exists(um)
339            if False in checked:
340                raise error.general('macro file not found: %s' % (um[checked.index(False)]))
341            for m in um:
342                self.defaults.load(m)
343        # Check if the user has a private set of macros to load
344        if 'RSB_MACROS' in os.environ:
345            if path.exists(os.environ['RSB_MACROS']):
346                self.defaults.load(os.environ['RSB_MACROS'])
347        if 'HOME' in os.environ:
348            rsb_macros = path.join(os.environ['HOME'], '.rsb_macros')
349            if path.exists(rsb_macros):
350                self.defaults.load(rsb_macros)
351
352    def local_git(self):
353        repo = git.repo(self.defaults.expand('%{_rtdir}'), self)
354        if repo.valid():
355            repo_valid = '1'
356            repo_head = repo.head()
357            repo_clean = not repo.dirty()
358            repo_id = repo_head
359            if not repo_clean:
360                repo_id += '-modified'
361            repo_mail = repo.email()
362        else:
363            repo_valid = '0'
364            repo_head = '%{nil}'
365            repo_clean = '%{nil}'
366            repo_id = 'no-repo'
367            repo_mail = None
368        self.defaults['_local_git_valid'] = repo_valid
369        self.defaults['_local_git_head']  = repo_head
370        self.defaults['_local_git_clean'] = str(repo_clean)
371        self.defaults['_local_git_id']    = repo_id
372        if repo_mail is not None:
373            self.defaults['_localgit_mail'] = repo_mail
374
375    def command(self):
376        return path.join(self.command_path, self.command_name)
377
378    def force(self):
379        return self.opts['force'] != '0'
380
381    def dry_run(self):
382        return self.opts['dry-run'] != '0'
383
384    def set_dry_run(self):
385        self.opts['dry-run'] = '1'
386
387    def quiet(self):
388        return self.opts['quiet'] != '0'
389
390    def trace(self):
391        return self.opts['trace'] != '0'
392
393    def warn_all(self):
394        return self.opts['warn-all'] != '0'
395
396    def keep_going(self):
397        return self.opts['keep-going'] != '0'
398
399    def no_clean(self):
400        return self.opts['no-clean'] != '0'
401
402    def always_clean(self):
403        return self.opts['always-clean'] != '0'
404
405    def no_install(self):
406        return self.opts['no-install'] != '0'
407
408    def user_macros(self):
409        #
410        # Return something even if it does not exist.
411        #
412        if self.opts['macros'] is None:
413            return None
414        um = []
415        configs = self.defaults.expand('%{_configdir}').split(':')
416        for m in self.opts['macros'].split(','):
417            if path.exists(m):
418                um += [m]
419            else:
420                # Get the expanded config macros then check them.
421                cm = path.expand(m, configs)
422                ccm = path.exists(cm)
423                if True in ccm:
424                    # Pick the first found
425                    um += [cm[ccm.index(True)]]
426                else:
427                    um += [m]
428        return um if len(um) else None
429
430    def jobs(self, cpus):
431        try:
432            cpus = int(cpus)
433        except:
434            raise error.general('invalid host cpu value')
435        opt_jobs = self.opts['jobs']
436        if opt_jobs == 'default':
437            _jobs = self.defaults.get_value('jobs')
438            if _jobs is not None:
439                if _jobs == 'none':
440                    cpus = 0
441                elif _jobs == 'max':
442                    pass
443                elif _jobs == 'half':
444                    cpus = cpus / 2
445                else:
446                    try:
447                        cpus = int(_jobs)
448                    except:
449                        raise error.general('invalid %%{jobs} value: %s' % (_jobs))
450            else:
451                opt_jobs = 'max'
452        if opt_jobs != 'default':
453            if opt_jobs == 'none':
454                cpus = 0
455            elif opt_jobs == 'max':
456                pass
457            elif opt_jobs == 'half':
458                cpus = cpus / 2
459            else:
460                ok = False
461                try:
462                    i = int(opt_jobs)
463                    cpus = i
464                    ok = True
465                except:
466                    pass
467                if not ok:
468                    try:
469                        f = float(opt_jobs)
470                        cpus = f * cpus
471                        ok = True
472                    except:
473                        pass
474                    if not ok:
475                        raise error.internal('bad jobs option: %s' % (opt_jobs))
476        if cpus <= 0:
477            cpu = 1
478        return cpus
479
480    def params(self):
481        return self.opts['params']
482
483    def get_args(self):
484        for arg in self.args:
485            yield arg
486
487    def find_arg(self, arg):
488        if self.optargs is None or arg not in self.optargs:
489            raise error.internal('bad arg: %s' % (arg))
490        for a in self.args:
491            sa = a.split('=')
492            if sa[0].startswith(arg):
493                return sa
494        return None
495
496    def logfiles(self):
497        if 'log' in self.opts and self.opts['log'] is not None:
498            log = self.opts['log'].split(',')
499        elif self.log_default is None:
500            log = ['stdout']
501        else:
502            log = self.log_default
503        return log
504
505    def urls(self):
506        if self.opts['url'] is not None:
507            return self.opts['url'].split(',')
508        return None
509
510    def log_info(self):
511        log.output(' Command Line: %s' % (' '.join(self.argv)))
512        log.output(' Python: %s' % (sys.version.replace('\n', '')))
513
514def load(opts):
515    """
516    Copy the defaults, get the host specific values and merge them overriding
517    any matching defaults, then create an options object to handle the command
518    line merging in any command line overrides. Finally post process the
519    command line.
520    """
521
522    if not isinstance(opts, command_line):
523        raise error.general('invalid options type passed to options loader')
524
525    overrides = host.overrides()
526    for k in overrides:
527        opts.defaults[k] = overrides[k]
528
529    opts.local_git()
530    opts.process()
531    opts.post_process()
532
533def run(args):
534    try:
535        long_opts = {
536            # key              macro         handler   param  defs   init
537            '--test-path'  : ('_test_path',  'path',   True,  None,  False),
538            '--test-jobs'  : ('_test_jobs',  'jobs',   True,  'max', True),
539            '--test-bool'  : ('_test_bool',  'bool',   False, '0',   True)
540        }
541        opts = command_line(base_path = '.',
542                            argv = args,
543                            optargs = None,
544                            defaults = macros.macros(),
545                            long_opts = long_opts,
546                            command_path = '.')
547        load(opts)
548        log.notice('RTEMS Tools Project - Defaults, v%s' % (version.string()))
549        opts.log_info()
550        log.notice('Options:')
551        log.notice(str(opts))
552        log.notice('Defaults:')
553        log.notice(str(opts.defaults))
554    except error.general as gerr:
555        print(gerr)
556        sys.exit(1)
557    except error.internal as ierr:
558        print(ierr)
559        sys.exit(1)
560    except error.exit:
561        pass
562    except KeyboardInterrupt:
563        _notice(opts, 'abort: user terminated')
564        sys.exit(1)
565    sys.exit(0)
566
567if __name__ == '__main__':
568    run(sys.argv)
Note: See TracBrowser for help on using the repository browser.