source: rtems-tools/tester/rt/check.py @ e058db0

5
Last change on this file since e058db0 was e058db0, checked in by Chris Johns <chrisj@…>, on 11/07/18 at 03:55:20

python: Provide support to select a valid python version.

  • Update imports after wrapping the code.
  • Fix python3 issues.
  • Fix config path issues for in repo and install runs.

Closes #3537

  • Property mode set to 100755
File size: 49.0 KB
Line 
1#
2# RTEMS Tools Project (http://www.rtems.org/)
3# Copyright 2016-2018 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
31from __future__ import print_function
32
33import argparse
34import copy
35import datetime
36import operator
37import os
38import re
39import sys
40import textwrap
41import threading
42import time
43import traceback
44
45import pprint
46
47from rtemstoolkit import execute
48from rtemstoolkit import error
49from rtemstoolkit import host
50from rtemstoolkit import log
51from rtemstoolkit import mailer
52from rtemstoolkit import path
53from rtemstoolkit import rtems
54from rtemstoolkit import textbox
55from rtemstoolkit import version
56
57#
58# Group loggin entries together.
59#
60log_lock = threading.Lock()
61
62#
63# The max build label size in the jobs list.
64#
65max_build_label = 0
66
67def _now():
68    return datetime.datetime.now()
69
70def rtems_version():
71    return version.version()
72
73def wrap(line, lineend = '', indent = 0, width = 75):
74    if type(line) is tuple or type(line) is list:
75        if len(line) >= 2:
76            s1 = line[0]
77        else:
78            s1 = ''
79        s2 = line[1:]
80    elif type(line) is str:
81        s1 = ''
82        s2 = [line]
83    else:
84        raise error.internal('line is not a tuple, list or string')
85    s1len = len(s1)
86    s = ''
87    first = True
88    for ss in s2:
89        if type(ss) is not str and type(ss) is not unicode:
90            raise error.internal('text needs to be a string')
91        for l in textwrap.wrap(ss, width = width - s1len - indent - 1):
92            s += '%s%s%s%s%s' % (' ' * indent, s1, l, lineend, os.linesep)
93            if first and s1len > 0:
94                s1 = ' ' * s1len
95    if lineend != '':
96        s = s[:0 - len(os.linesep) - 1] + os.linesep
97    return s
98
99def comma_split(options):
100    if options is None:
101        return None
102    return [o.strip() for o in options.split(',')]
103
104def title():
105    return 'RTEMS Tools Project - RTEMS Kernel BSP Builder, %s' % (version.string())
106
107def command_line():
108    return wrap(('command: ', ' '.join(sys.argv)), lineend = '\\')
109
110def jobs_option_parse(jobs_option):
111    try:
112        if '/' not in jobs_option:
113            return 1, int(jobs_option)
114        jos = jobs_option.split('/')
115        if len(jos) != 2:
116            raise error.general('invalid jobs option: %s' % (jobs_option))
117        return int(jos[0]), int(jos[1])
118    except:
119        pass
120    raise error.general('invalid jobs option: %s' % (jobs_option))
121
122def arch_bsp_build_parse(build):
123    if type(build) is str:
124        build_key = build
125    else:
126        build_key = build.key()
127    abb = build_key.split('.')
128    if len(abb) != 2:
129        raise error.general('invalid build key: %s' % (build_key))
130    ab = abb[0].split('/')
131    if len(ab) != 2:
132        raise error.general('invalid build key: %s' % (build_key))
133    return ab[0], ab[1], abb[1]
134
135def set_max_build_label(jobs):
136    global max_build_label
137    for job in jobs:
138        if len(job.build.key()) > max_build_label:
139            max_build_label = len(job.build.key())
140    max_build_label += 2
141
142class arch_bsp_build:
143
144    def __init__(self, arch, bsp, build, build_config):
145        self.arch = arch
146        self.bsp = bsp
147        self.build = build
148        self.build_config = build_config
149        self.start_time = None
150        self.stop_time = None
151
152    def __str__(self):
153        return self.key() + ': ' + self.build_config
154
155    def key(self):
156        return '%s/%s.%s' % (self.arch, self.bsp, self.build)
157
158    def get_arch_bsp(self):
159        return self.arch, self.bsp
160
161    def start(self):
162        self.start_time = _now()
163
164    def stop(self):
165        self.stop_time = _now()
166
167    def duration(self):
168        return self.stop_time - self.start_time
169
170class output_worker:
171
172    def __init__(self, we, build):
173        self.text = []
174        self.warnings_errors = we
175        self.build = build
176
177    def output(self, text):
178        self.warnings_errors.process_output(text, self.build)
179        self.text += text.splitlines()
180
181    def log_output(self, heading):
182        log_lock.acquire()
183        try:
184            log.output(heading + self.text)
185        except:
186            raise
187        finally:
188            log_lock.release()
189
190class warnings_errors:
191
192    def __init__(self, source_base, groups):
193        self.lock = threading.Lock()
194        self.source_base = path.host(source_base)
195        self.groups = groups
196        self.reset()
197
198    def _get_warnings(self, build):
199        self.lock.acquire()
200        warnings = [w for w in self.warnings]
201        self.lock.release()
202        return sorted(warnings)
203
204    def _total(self, archive):
205        total = 0
206        for a in archive:
207            total += archive[a]
208        return total
209
210    def _analyze(self, warnings, exclude):
211        def _group(data, category, name, warning, count, groups, group_regx):
212            if 'groups' not in data:
213                data['groups'] = { }
214            if category not in data['groups']:
215                data['groups'][category] = { }
216            if 'totals' not in data['groups'][category]:
217                data['groups'][category]['totals'] = { }
218            if name not in data['groups'][category]:
219                data['groups'][category][name] = { }
220            for group in groups:
221                if group not in data['groups'][category]['totals']:
222                    data['groups'][category]['totals'][group] = 0
223                if group not in data['groups'][category][name]:
224                    data['groups'][category][name][group] = 0
225                if group_regx[group].match(warning):
226                    data['groups'][category][name][group] += count
227                    data['groups'][category]['totals'][group] += count
228                    break
229
230        def _update(data, category, name, warning, count, groups, group_regx):
231            if category not in data:
232                data[category] = { }
233            if name not in data[category]:
234                data[category][name] = { }
235            if warning not in data[category][name]:
236                data[category][name][warning] = 0
237            data[category][name][warning] += count
238            _group(data, category, name,  w, count, groups, group_regx)
239
240        categories = ['arch', 'arch_bsp', 'build']
241        data = { 'groups': { } }
242        for category in categories:
243            data[category] = { }
244            data['groups'][category] = { }
245        group_regx = { }
246        for group in self.groups['groups']:
247            group_regx[group] = re.compile(self.groups[group])
248        exclude_regx = re.compile(exclude)
249        for warning in self.warnings:
250            arch, bsp, build = arch_bsp_build_parse(warning)
251            arch_bsp = '%s/%s' % (arch, bsp)
252            for w in self.warnings[warning]:
253                if not exclude_regx.match(w):
254                    count = self.warnings[warning][w]
255                    _update(data, 'arch', arch, w, count,
256                           self.groups['groups'], group_regx)
257                    _update(data, 'arch_bsp', arch_bsp, w, count,
258                           self.groups['groups'], group_regx)
259                    _update(data, 'build', build, w, count,
260                           self.groups['groups'], group_regx)
261        for category in categories:
262            common = {}
263            for name in data[category]:
264                for w in data[category][name]:
265                    if w not in common:
266                        for other in [n for n in data[category] if n != name]:
267                            if w in data[category][other]:
268                                common[w] = data[category][name][w]
269                                _group(data, category, 'common', w, common[w],
270                                       self.groups['groups'], group_regx)
271            data[category]['common'] = common
272        return data
273
274    def _report_category(self, label, warnings, group_counts, summary):
275        width = 70
276        cols_1 = [width]
277        cols_2 = [8, width - 8]
278        cols_4 = textbox.even_columns(4, width)
279        cols_2_4 = textbox.merge_columns([cols_2, cols_4])
280        s = textbox.line(cols_1, line = '=', marker = '+', indent = 1)
281        s += textbox.row(cols_1, [' ' + label], indent = 1)
282        s += textbox.line(cols_1, marker = '+', indent = 1)
283        builds = ['common'] + sorted([b for b in warnings if b != 'common'])
284        common = warnings['common']
285        for build in builds:
286            build_warnings = warnings[build]
287            if build is not 'common':
288                build_warnings = [w for w in build_warnings if w not in common]
289            s += textbox.row(cols_1,
290                             [' %s : %d warning(s)' % (build,
291                                                       len(build_warnings))],
292                             indent = 1)
293            if len(build_warnings) == 0:
294                s += textbox.line(cols_1, marker = '+', indent = 1)
295            else:
296                s += textbox.line(cols_4, marker = '+', indent = 1)
297                if build not in group_counts:
298                    gs = [0 for group in self.groups['groups']]
299                else:
300                    gs = []
301                    for g in range(0, len(self.groups['groups'])):
302                        group = self.groups['groups'][g]
303                        if group in group_counts[build]:
304                            count = group_counts[build][group]
305                        else:
306                            count = 0
307                        gs += ['%*s' % (cols_4[g % 4] - 2,
308                                        '%s : %4d' % (group, count))]
309                    for row in range(0, len(self.groups['groups']), 4):
310                        if row + 4 > len(self.groups['groups']):
311                            d = gs[row:] + \
312                                ['' for r in range(row,
313                                                   len(self.groups['groups']))]
314                        else:
315                            d = gs[row:+4]
316                        s += textbox.row(cols_4, d, indent = 1)
317                s += textbox.line(cols_2_4, marker = '+', indent = 1)
318                if not summary:
319                    vw = sorted([(w, warnings[build][w]) for w in build_warnings],
320                                key = operator.itemgetter(1),
321                                reverse = True)
322                    for w in vw:
323                        c1 = '%6d' % w[1]
324                        for l in textwrap.wrap(' ' + w[0], width = cols_2[1] - 3):
325                            s += textbox.row(cols_2, [c1, l], indent = 1)
326                            c1 = ' ' * 6
327                    s += textbox.line(cols_2, marker = '+', indent = 1)
328        return s
329
330    def _report_warning_map(self):
331        builds = self.messages['warnings']
332        width = 70
333        cols_1 = [width]
334        s = textbox.line(cols_1, line = '=', marker = '+', indent = 1)
335        s += textbox.row(cols_1, [' Warning Map'], indent = 1)
336        s += textbox.line(cols_1, marker = '+', indent = 1)
337        for build in builds:
338            messages = builds[build]
339            s += textbox.row(cols_1, [' %s : %d' % (build, len(messages))], indent = 1)
340            s += textbox.line(cols_1, marker = '+', indent = 1)
341            for msg in messages:
342                for l in textwrap.wrap(msg, width = width - 3):
343                    s += textbox.row(cols_1, [' ' + l], indent = 1)
344                for l in textwrap.wrap(messages[msg], width = width - 3 - 4):
345                    s += textbox.row(cols_1, ['    ' + l], indent = 1)
346            s += textbox.line(cols_1, marker = '+', indent = 1)
347        return s
348
349    def warnings_report(self, summary = False):
350        self.lock.acquire()
351        s = ' No warnings' + os.linesep
352        try:
353            total = 0
354            for build in self.warnings:
355                total += self._total(self.warnings[build])
356            if total != 0:
357                data = self._analyze(self.warnings, self.groups['exclude'])
358                s = self._report_category('By Architecture (total : %d)' % (total),
359                                          data['arch'], data['groups']['arch'],
360                                          summary)
361                s += os.linesep
362                s += self._report_category('By BSP (total : %d)' % (total),
363                                           data['arch_bsp'], data['groups']['arch_bsp'],
364                                           summary)
365                s += os.linesep
366                s += self._report_category('By Build (total : %d)' % (total),
367                                           data['build'], data['groups']['build'],
368                                           summary)
369                s += os.linesep
370                if not summary:
371                    s += self._report_warning_map()
372                    s += os.linesep
373        finally:
374            self.lock.release()
375        return s
376
377    def clear_build(self, build):
378        self.lock.acquire()
379        self.warnings[build.key()] = {}
380        self.errors[build.key()] = {}
381        self.lock.release()
382
383    def get_warning_count(self):
384        self.lock.acquire()
385        count = self.warning_count
386        self.lock.release()
387        return count
388
389    def get_error_count(self):
390        self.lock.acquire()
391        count = self.error_count
392        self.lock.release()
393        return count
394
395    def reset(self):
396        self.lock.acquire()
397        self.warnings = { }
398        self.warning_count = 0
399        self.errors = { }
400        self.error_count = 0
401        self.messages = { 'warnings' : { }, 'errors' : { } }
402        self.lock.release()
403
404    def _get_messages(self, build, key):
405        self.lock.acquire()
406        if type(build) is str:
407            build_key = build
408        else:
409            build_key = build.key()
410        if build_key not in self.messages[key]:
411            messages = []
412        else:
413            messages = self.messages[key][build_key]
414        messages = ['%s %s' % (m, messages[m]) for m in messages]
415        self.lock.release()
416        return messages
417
418    def get_warning_messages(self, build):
419        return self._get_messages(build, 'warning')
420
421    def get_error_messages(self, build):
422        return self._get_messages(build, 'errors')
423
424    def process_output(self, text, build):
425        def _line_split(line, source_base):
426            if line.count(':') < 2:
427                return None
428            ls = line.split(' ', 1)
429            fname = ls[0].strip().split(':', 2)
430            if len(fname) != 3:
431                return None
432            p = path.abspath(fname[0])
433            p = p.replace(source_base, '')
434            if path.isabspath(p):
435                p = p[1:]
436            if len(fname[2]) == 0:
437                pos = None
438            else:
439                pos = fname[2]
440            return p, fname[1], pos, ls[1]
441
442        def _create_build_errors(build, archive):
443            if build.key() not in archive:
444                archive[build.key()] = { }
445            return archive[build.key()]
446
447        #
448        # The GNU linker does not supply 'error' in error messages. There is no
449        # line information which is understandable. Look for `bin/ld:` and
450        # `collect2:` in the output and then create the error when `collect2:`
451        # is seen.
452        #
453        # The order we inspect each line is important.
454        #
455        if ' warning:' in text or \
456           ' error:' in text or \
457           ' Error:' in text or \
458           'bin/ld:' in text:
459            self.lock.acquire()
460            try:
461                for l in text.splitlines():
462                    if 'bin/ld:' in l:
463                        archive =_create_build_errors(build, self.errors)
464                        if 'linker' not in archive:
465                            archive['linker'] = []
466                        archive['linker'] += [l.split(':', 1)[1].strip()]
467                        messages = 'errors'
468                    elif l.startswith('collect2:'):
469                        archive =_create_build_errors(build, self.errors)
470                        l = '/ld/collect2:0: error: '
471                        if 'linker' not in archive or len(archive['linker']) == 0:
472                            l += 'no error message found!'
473                        else:
474                            l += '; '.join(archive['linker'])
475                            archive['linker'] = []
476                        messages = 'errors'
477                    elif ' warning:' in l:
478                        self.warning_count += 1
479                        archive = _create_build_errors(build, self.warnings)
480                        messages = 'warnings'
481                    elif ' error:' in l or ' Error:' in l:
482                        self.error_count += 1
483                        archive =_create_build_errors(build, self.errors)
484                        messages = 'errors'
485                    else:
486                        continue
487                    line_parts = _line_split(l, self.source_base)
488                    if line_parts is not None:
489                        src, line, pos, msg = line_parts
490                        if pos is not None:
491                            where = '%s:%s:%s' % (src, line, pos)
492                        else:
493                            where = '%s:%s' % (src, line)
494                        if where not in archive:
495                            archive[where] = 1
496                        else:
497                            archive[where] += 1
498                        if build.key() not in self.messages[messages]:
499                            self.messages[messages][build.key()] = { }
500                        self.messages[messages][build.key()][where] = msg
501            finally:
502                self.lock.release()
503
504class results:
505
506    def __init__(self, source_base, groups):
507        self.lock = threading.Lock()
508        self.errors = { 'pass':      0,
509                        'configure': 0,
510                        'build':     0,
511                        'tests':     0,
512                        'passes':    { },
513                        'fails':     { } }
514        self.counts = { 'h'        : 0,
515                        'exes'     : 0,
516                        'objs'     : 0,
517                        'libs'     : 0 }
518        self.warnings_errors = warnings_errors(source_base, groups)
519
520    def _arch_bsp(self, arch, bsp):
521        return '%s/%s' % (arch, bsp)
522
523    def _arch_bsp_passes(self, build):
524        if build.key() not in self.errors['passes']:
525            self.errors['passes'][build.key()] = []
526        return self.errors['passes'][build.key()]
527
528    def _arch_bsp_fails(self, build):
529        if build.key() not in self.errors['fails']:
530            self.errors['fails'][build.key()] = []
531        return self.errors['fails'][build.key()]
532
533    def _count(self, label):
534        count = 0
535        for build in self.errors[label]:
536            count += len(self.errors[label][build])
537        return count
538
539    def _max_col(self, label):
540        max_col = 0
541        for build in self.errors[label]:
542            arch, bsp, build_config = arch_bsp_build_parse(build)
543            arch_bsp = self._arch_bsp(arch, bsp)
544            if len(arch_bsp) > max_col:
545                max_col = len(arch_bsp)
546        return max_col
547
548    def get_warning_count(self):
549        return self.warnings_errors.get_warning_count()
550
551    def get_error_count(self):
552        return self.warnings_errors.get_error_count()
553
554    def get_warning_messages(self, build):
555        return self.warnings_errors.get_warning_messages(build)
556
557    def get_error_messages(self, build):
558        return self.warnings_errors.get_error_messages(build)
559
560    def status(self):
561        self.lock.acquire()
562        try:
563            s = 'Pass: %4d  Fail: %4d (configure:%d build:%d)' % \
564                (self.errors['pass'],
565                 self.errors['configure'] + self.errors['build'],
566                 self.errors['configure'], self.errors['build'])
567        except:
568            raise
569        finally:
570            self.lock.release()
571        return s;
572
573    def add_fail(self, phase, build, configure, warnings, error_messages):
574        fails = self._arch_bsp_fails(build)
575        self.lock.acquire()
576        try:
577            self.errors[phase] += 1
578            fails += [(phase, build.build_config, configure, error_messages)]
579        finally:
580            self.lock.release()
581
582    def add_pass(self, build, configure, warnings):
583        passes = self._arch_bsp_passes(build)
584        self.lock.acquire()
585        try:
586            self.errors['pass'] += 1
587            passes += [(build.build_config, configure, warnings, None)]
588        finally:
589            self.lock.release()
590
591    def pass_count(self):
592        return self._count('passes')
593
594    def fail_count(self):
595        return self._count('fails')
596
597    def _failures_report(self, build, count):
598        if type(build) is str:
599            build_key = build
600        else:
601            build_key = build.key()
602        if build_key not in self.errors['fails'] or \
603           len(self.errors['fails'][build_key]) == 0:
604            return count, 0, ' No failures'
605        absize = 0
606        bsize = 0
607        ssize = 0
608        arch, bsp, build_set = arch_bsp_build_parse(build_key)
609        arch_bsp = self._arch_bsp(arch, bsp)
610        fails = self.errors['fails'][build_key]
611        for f in fails:
612            if len(f[0]) > ssize:
613                ssize = len(f[0])
614        s = ''
615        for f in fails:
616            count += 1
617            fcl = ' %3d' % (count)
618            state = f[0]
619            s += '%s %s %s %-*s:%s' % \
620                 (fcl, build_set, arch_bsp, ssize, state, os.linesep)
621            s1 = ' ' * 6
622            s += wrap((s1, 'configure: ' + f[2]), lineend = '\\', width = 75)
623            s1 = ' ' * 5
624            for e in self.warnings_errors.get_error_messages(build):
625                s += wrap([s1 + 'error: ', e])
626        return count, len(fails), s
627
628    def failures_report(self, build = None):
629        s = ''
630        count = 0
631        if build is not None:
632            count, build_fails, bs = self._failures_report(build, count)
633            if build_fails > 0:
634                s += bs + os.linesep
635        else:
636            self.lock.acquire()
637            builds = sorted(self.errors['fails'].keys())
638            self.lock.release()
639            for build in builds:
640                count, build_fails, bs = self._failures_report(build, count)
641                if build_fails > 0:
642                    s += bs + os.linesep
643        if count == 0:
644            s = ' No failures' + os.linesep
645        return s
646
647    def warnings_report(self, summary = False):
648        return self.warnings_errors.warnings_report(summary)
649
650    def report(self):
651        self.lock.acquire()
652        log_lock.acquire()
653        passes = self.pass_count()
654        fails = self.fail_count()
655        log.notice('Passes: %d   Failures: %d' % (passes, fails))
656        log.output()
657        log.output('Build Report')
658        log.output('   Passes: %d   Failures: %d' % (passes, fails))
659        log.output(' Failures:')
660        if fails == 0:
661            log.output('  None')
662        else:
663            max_col = self._max_col('fails')
664            for build in self.errors['fails']:
665                arch, bsp, build_config = arch_bsp_build_parse(build)
666                arch_bsp = self._arch_bsp(arch, bsp)
667                for f in self.errors['fails'][build]:
668                    config_cmd = f[2]
669                    config_at = config_cmd.find('configure')
670                    if config_at != -1:
671                        config_cmd = config_cmd[config_at:]
672                    log.output(' %*s:' % (max_col + 2, arch_bsp))
673                    s1 = ' ' * 6
674                    log.output(wrap([s1, config_cmd], lineend = '\\', width = 75))
675                    if f[3] is not None:
676                        s1 = ' ' * len(s1)
677                        for msg in f[3]:
678                            log.output(wrap([s1, msg], lineend = '\\'))
679        log.output(' Passes:')
680        if passes == 0:
681            log.output('  None')
682        else:
683            max_col = self._max_col('passes')
684            for build in self.errors['passes']:
685                arch, bsp, build_config = arch_bsp_build_parse(build)
686                arch_bsp = self._arch_bsp(arch, bsp)
687                for f in self.errors['passes'][build]:
688                    config_cmd = f[1]
689                    config_at = config_cmd.find('configure')
690                    if config_at != -1:
691                        config_cmd = config_cmd[config_at:]
692                    log.output(' %s (%5d):' % (arch_bsp, f[2]))
693                    log.output(wrap([' ' * 6, config_cmd], lineend = '\\', width = 75))
694        log_lock.release()
695        self.lock.release()
696
697class arch_bsp_builder:
698
699    def __init__(self, results_, build, commands, build_dir, tag):
700        self.lock = threading.Lock()
701        self.state = 'ready'
702        self.thread = None
703        self.proc = None
704        self.results = results_
705        self.build = build
706        self.commands = commands
707        self.build_dir = build_dir
708        self.tag = tag
709        self.output = output_worker(results_.warnings_errors, build)
710        self.counts = { 'h'        : 0,
711                        'exes'     : 0,
712                        'objs'     : 0,
713                        'libs'     : 0 }
714
715    def _notice(self, text):
716        global max_build_label
717        arch, bsp, build_set = arch_bsp_build_parse(self.build.key())
718        label = '%s/%s (%s)' % (arch, bsp, build_set)
719        log.notice('[%s] %-*s %s' % (self.tag, max_build_label, label, text))
720
721    def _build_dir(self):
722        return path.join(self.build_dir, self.build.key())
723
724    def _make_build_dir(self):
725        if not path.exists(self._build_dir()):
726            path.mkdir(self._build_dir())
727
728    def _count_files(self):
729        for root, dirs, files in os.walk(self._build_dir()):
730            for file in files:
731                if file.endswith('.exe'):
732                    self.counts['exes'] += 1
733                elif file.endswith('.o'):
734                    self.counts['objs'] += 1
735                elif file.endswith('.a'):
736                    self.counts['libs'] += 1
737                elif file.endswith('.h'):
738                    self.counts['h'] += 1
739
740    def _execute(self, phase):
741        exit_code = 0
742        cmd = self.commands[phase]
743        try:
744            # This should locked; not sure how to do that
745            self.proc = execute.capture_execution(log = self.output)
746            log.output(wrap(('run:', self.build.key(), cmd), lineend = '\\'))
747            if not self.commands['dry-run']:
748                exit_code, proc, output = self.proc.shell(cmd,
749                                                          cwd = path.host(self._build_dir()))
750        except:
751            traceback.print_exc()
752            self.lock.acquire()
753            if self.proc is not None:
754                self.proc.kill()
755            self.lock.release()
756            exit_code = 1
757        self.lock.acquire()
758        self.proc = None
759        self.lock.release()
760        return exit_code == 0
761
762    def _configure(self):
763        return self._execute('configure')
764
765    def _make(self):
766        return self._execute('build')
767
768    def _worker(self):
769        self.lock.acquire()
770        self.state = 'running'
771        self.lock.release()
772        self.build.start()
773        warnings = self.results.get_warning_count()
774        ok = False
775        try:
776            log_lock.acquire()
777            try:
778                self._notice('Start')
779                self._notice('Creating: %s' % (self._build_dir()))
780            except:
781                raise
782            finally:
783                log_lock.release()
784            self._make_build_dir()
785            self._notice('Configuring')
786            ok = self._configure()
787            if not ok:
788                warnings = self.results.get_warning_count() - warnings
789                self.results.add_fail('configure',
790                                      self.build,
791                                      self.commands['configure'],
792                                      warnings,
793                                      self.results.get_error_messages(self.build))
794            self.lock.acquire()
795            if self.state == 'killing':
796                ok = False
797            self.lock.release()
798            if ok:
799                self._notice('Building')
800                ok = self._make()
801                if not ok:
802                    warnings = self.results.get_warning_count() - warnings
803                    self.results.add_fail('build',
804                                          self.build,
805                                          self.commands['configure'],
806                                          warnings,
807                                          self.results.get_error_messages(self.build))
808            if ok:
809                warnings = self.results.get_warning_count() - warnings
810                self.results.add_pass(self.build,
811                                      self.commands['configure'],
812                                      warnings)
813        except:
814            ok = False
815            self._notice('Build Exception')
816            traceback.print_exc()
817        self.build.stop()
818        log_lock.acquire()
819        try:
820            self._count_files()
821            if ok:
822                self._notice('PASS')
823            else:
824                self._notice('FAIL')
825            self._notice('Warnings:%d  exes:%d  objs:%d  libs:%d' % \
826                         (warnings, self.counts['exes'],
827                          self.counts['objs'], self.counts['libs']))
828            log.output('  %s: Failure Report:' % (self.build.key()))
829            log.output(self.results.failures_report(self.build))
830            self._notice('Finished (duration:%s)' % (str(self.build.duration())))
831            self._notice('Status: %s' % (self.results.status()))
832        except:
833            self._notice('Build Exception:')
834            traceback.print_exc()
835        finally:
836            log_lock.release()
837        self.lock.acquire()
838        self.state = 'finished'
839        self.lock.release()
840
841    def get_file_counts(self):
842        return self.counts
843
844    def run(self):
845        self.lock.acquire()
846        try:
847            if self.state != 'ready':
848                raise error.general('builder already run')
849            self.state = 'starting'
850            self.thread = threading.Thread(target = self._worker)
851            self.thread.start()
852        except:
853            raise
854        finally:
855            self.lock.release()
856
857    def kill(self):
858        self.lock.acquire()
859        if self.thread is not None:
860            self.state = 'killing'
861            if self.proc is not None:
862                try:
863                    self.proc.kill()
864                except:
865                    pass
866            self.lock.release()
867            self.thread.join(5)
868            self.lock.acquire()
869        self.state = 'finished'
870        self.lock.release()
871
872    def current_state(self):
873        self.lock.acquire()
874        state = self.state
875        self.lock.release()
876        return state
877
878    def log_output(self):
879        self.output.log_output(['-' * 79, '] %s: Build output:' % (self.build.key())])
880
881    def clean(self):
882        if not self.commands['no-clean']:
883            self._notice('Cleaning: %s' % (self._build_dir()))
884            path.removeall(self._build_dir())
885
886class build_jobs:
887
888    def __init__(self, config, arch, bsp):
889        self.arch = arch
890        self.bsp = bsp
891        self.builds = config.builds()
892        if self.builds is None:
893            raise error.general('build not found: %s' % (config.build()))
894        valid_configs = config.configs()
895        excludes = config.excludes(self.arch, self.bsp)
896        for e in excludes:
897            if e.startswith('no-'):
898                raise error.general('excludes cannot start with "no-": %s' % (e))
899            if e not in valid_configs:
900                raise error.general('invalid exclude: %s' % (e))
901        #
902        # The build can be in the build string delimited by '-'.
903        #
904        remove = []
905        for e in excludes:
906            remove += [b for b in self.builds if e in b.split('-')]
907        self.builds = [b for b in self.builds if b not in remove]
908        self.build_set = { }
909        exclude_options = ' ' + config.exclude_options(self.arch, self.bsp)
910        for build in self.builds:
911            self.build_set[build] = config.build_options(build) + exclude_options
912
913    def jobs(self):
914        return [arch_bsp_build(self.arch, self.bsp, b, self.build_set[b]) \
915                for b in sorted(self.build_set.keys())]
916
917class builder:
918
919    def __init__(self, config, version, prefix, tools, rtems, build_dir, options):
920        self.config = config
921        self.build_dir = build_dir
922        self.rtems_version = version
923        self.prefix = prefix
924        self.tools = tools
925        self.rtems = rtems
926        self.options = options
927        self.counts = { 'h'        : 0,
928                        'exes'     : 0,
929                        'objs'     : 0,
930                        'libs'     : 0 }
931        self.results = results(rtems,
932                               { 'groups'  : ['Shared', 'BSP', 'Network', 'Tests',
933                                              'LibCPU', 'CPU Kit'],
934                                 'exclude' : '.*Makefile.*',
935                                 'CPU Kit' : '.*cpukit/.*',
936                                 'Network' : '.*libnetworking/.*|.*librpc/.*',
937                                 'Tests'   : '.*testsuites/.*',
938                                 'BSP'     : '.*libbsp/.*',
939                                 'LibCPU'  : '.*libcpu/.*',
940                                 'Shared'  : '.*shared/.*' })
941        if not path.exists(path.join(rtems, 'configure')) or \
942           not path.exists(path.join(rtems, 'Makefile.in')) or \
943           not path.exists(path.join(rtems, 'cpukit')):
944            raise error.general('RTEMS source path does not look like RTEMS')
945
946    def _bsps(self, arch):
947        return self.config.arch_bsps(arch)
948
949    def _create_build_jobs(self, jobs, build_job_count):
950        max_job_size = len('%d' % len(jobs))
951        build_jobs = []
952        job_index = 1
953        for job in jobs:
954            tag = '%*d/%d' % (max_job_size, job_index, len(jobs))
955            build_jobs += [arch_bsp_builder(self.results,
956                                            job,
957                                            self._commands(job, build_job_count),
958                                            self.build_dir,
959                                            tag)]
960            job_index += 1
961        set_max_build_label(build_jobs)
962        return build_jobs
963
964    def _commands(self, build, build_jobs):
965        commands = { 'dry-run'  : self.options['dry-run'],
966                     'no-clean' : self.options['no-clean'],
967                     'configure': None,
968                     'build'    : None }
969        cmds = build.build_config.split()
970        cmds += self.config.bspopts(build.arch, build.bsp)
971        cmd = [path.join(self.rtems, 'configure')]
972        for c in cmds:
973            c = c.replace('@PREFIX@', self.prefix)
974            c = c.replace('@RTEMS_VERSION@', str(self.rtems_version))
975            c = c.replace('@ARCH@', build.arch)
976            c = c.replace('@BSP@', build.bsp)
977            cmd += [c]
978        commands['configure'] = ' '.join(cmd)
979        cmd = 'make -j %s' % (build_jobs)
980        commands['build'] = cmd
981        return commands
982
983    def _update_file_counts(self, counts):
984        for f in self.counts:
985            if f in counts:
986                self.counts[f] += counts[f]
987        return counts
988
989    def _warnings_report(self):
990        if self.options['warnings-report'] is not None:
991            with open(self.options['warnings-report'], 'w') as f:
992                f.write(title() + os.linesep)
993                f.write(os.linesep)
994                f.write('Date: %s%s' % (_now().strftime('%c'), os.linesep))
995                f.write(os.linesep)
996                f.write(command_line() + os.linesep)
997                f.write(self.results.warnings_errors.warnings_report())
998
999    def _failures_report(self):
1000        if self.options['failures-report'] is not None:
1001            with open(self.options['failures-report'], 'w') as f:
1002                f.write(title() + os.linesep)
1003                f.write(os.linesep)
1004                f.write('Date: %s%s' % (_now().strftime('%c'), os.linesep))
1005                f.write(os.linesep)
1006                f.write(command_line() + os.linesep)
1007                f.write(self.results.failures_report())
1008
1009    def _finished(self):
1010        log.notice('Total: Warnings:%d  exes:%d  objs:%d  libs:%d' % \
1011                   (self.results.get_warning_count(), self.counts['exes'],
1012                    self.counts['objs'], self.counts['libs']))
1013        log.output()
1014        log.output('Warnings:')
1015        log.output(self.results.warnings_report())
1016        log.output()
1017        log.notice('Failures:')
1018        log.notice(self.results.failures_report())
1019        self._warnings_report()
1020        self._failures_report()
1021
1022    def run_jobs(self, jobs):
1023        if path.exists(self.build_dir):
1024            log.notice('Cleaning: %s' % (self.build_dir))
1025            path.removeall(self.build_dir)
1026        self.start = _now()
1027        self.end = _now()
1028        self.duration = self.end - self.start
1029        self.average = self.duration
1030        env_path = os.environ['PATH']
1031        os.environ['PATH'] = path.host(path.join(self.tools, 'bin')) + \
1032                             os.pathsep + os.environ['PATH']
1033        job_count, build_job_count = jobs_option_parse(self.options['jobs'])
1034        builds = self._create_build_jobs(jobs, build_job_count)
1035        active_jobs = []
1036        self.jobs_completed = 0
1037        try:
1038            while len(builds) > 0 or len(active_jobs) > 0:
1039                new_jobs = job_count - len(active_jobs)
1040                if new_jobs > 0:
1041                    active_jobs += builds[:new_jobs]
1042                    builds = builds[new_jobs:]
1043                finished_jobs = []
1044                for job in active_jobs:
1045                    state = job.current_state()
1046                    if state == 'ready':
1047                        job.run()
1048                    elif state != 'running':
1049                        finished_jobs += [job]
1050                for job in finished_jobs:
1051                    self._update_file_counts(job.get_file_counts())
1052                    job.log_output()
1053                    job.clean()
1054                    active_jobs.remove(job)
1055                    self.jobs_completed += 1
1056                if self.options['dry-run']:
1057                    time.sleep(0)
1058                else:
1059                    time.sleep(0.100)
1060        except:
1061            for job in active_jobs:
1062                try:
1063                    job.kill()
1064                except:
1065                    pass
1066            raise
1067        self.end = _now()
1068        os.environ['PATH'] = env_path
1069        self.duration = self.end - self.start
1070        if self.jobs_completed == 0:
1071            self.jobs_completed = 1
1072        self._finished()
1073        self.average = self.duration / self.jobs_completed
1074        log.notice('Average BSP Build Time: %s' % (str(self.average)))
1075        log.notice('Total Time %s' % (str(self.duration)))
1076
1077    def arch_bsp_jobs(self, arch, bsps):
1078        jobs = []
1079        for bsp in bsps:
1080            jobs += build_jobs(self.config, arch, bsp).jobs()
1081        return jobs
1082
1083    def bsp_jobs(self, bsps):
1084        jobs = []
1085        for bsp in bsps:
1086            if bsp.count('/') != 1:
1087                raise error.general('invalid bsp format (use: arch/bsp): %s' % (bsp))
1088            arch, bsp = bsp.split('/')
1089            jobs += build_jobs(self.config, arch, bsp).jobs()
1090        return jobs
1091
1092    def arch_jobs(self, archs):
1093        jobs = []
1094        for arch in archs:
1095            if not self.config.arch_present(arch):
1096                raise error.general('Architecture not found: %s' % (arch))
1097            jobs += self.arch_bsp_jobs(arch, self._bsps(arch))
1098        return jobs
1099
1100    def profile_jobs(self, profiles):
1101        jobs = []
1102        for profile in profiles:
1103            if not self.config.profile_present(profile):
1104                raise error.general('Profile not found: %s' % (profile))
1105            for arch in self.config.profile_archs(profile):
1106                jobs += self.bsp_jobs(self.config.profile_arch_bsps(profile, arch))
1107        return jobs
1108
1109    def build_bsps(self, bsps):
1110        log.notice('BSPS(s): %s' % (', '.join(bsps)))
1111        self.run_jobs(self.bsp_jobs(bsps))
1112
1113    def build_archs(self, archs):
1114        log.notice('Architecture(s): %s' % (', '.join(archs)))
1115        self.run_jobs(self.arch_jobs(archs))
1116
1117    def build_profiles(self, profiles):
1118        log.notice('Profile(s): %s' % (', '.join(profiles)))
1119        self.run_jobs(self.profile_jobs(profiles))
1120
1121def run(args):
1122    b = None
1123    ec = 0
1124    try:
1125        rtems.clean_windows_path()
1126
1127        start = _now()
1128        prefix = '/opt/rtems/%s' % (rtems_version())
1129        tools = prefix
1130        build_dir = 'bsp-builds'
1131        logf = 'bsp-build-%s.txt' % (_now().strftime('%Y%m%d-%H%M%S'))
1132        config_file = rtems.bsp_configuration_file(prog = args[0])
1133
1134        description  = 'RTEMS BSP Builder is a BSP build tester. It builds BSPs '
1135        description += 'in various ways to test build regressions in the kernel. You '
1136        description += 'can build based on tier, architecture, or BSP. You can control '
1137        description += 'the profile of build with various build configuration settings.'
1138
1139        argsp = argparse.ArgumentParser(prog = 'rtems-bsp-builder',
1140                                        description = description)
1141        argsp.add_argument('--prefix', help = 'Prefix to build the BSP.',
1142                           type = str)
1143        argsp.add_argument('--rtems-tools', help = 'The RTEMS tools directory.',
1144                           type = str)
1145        argsp.add_argument('--rtems', help = 'The RTEMS source tree.',
1146                           type = str)
1147        argsp.add_argument('--build-path', help = 'Path to build in.',
1148                           type = str)
1149        argsp.add_argument('--log', help = 'Log file.', type = str)
1150        argsp.add_argument('--config-report', help = 'Report the configuration.',
1151                           type = str, default = None,
1152                           choices = ['all', 'profiles', 'builds', 'archs'])
1153        argsp.add_argument('--warnings-report', help = 'Report the warnings to a file.',
1154                           type = str, default = None)
1155        argsp.add_argument('--failures-report', help = 'Report the failures to a file.',
1156                           type = str, default = None)
1157        argsp.add_argument('--stop-on-error', help = 'Stop on an error.',
1158                           action = 'store_true')
1159        argsp.add_argument('--no-clean', help = 'Do not clean the build output.',
1160                           action = 'store_true')
1161        argsp.add_argument('--profiles', help = 'Build the listed profiles (profile,profile,..).',
1162                           type = str, default = 'tier-1')
1163        argsp.add_argument('--arch', help = 'Build the architectures (arch,arch,..).',
1164                           type = str)
1165        argsp.add_argument('--bsp', help = 'Build the BSPs (arch/bsp,arch/bsp,..).',
1166                           type = str)
1167        argsp.add_argument('--build', help = 'Build name to build (see --config-report).',
1168                           type = str, default='all')
1169        argsp.add_argument('--jobs', help = 'Number of jobs to run.',
1170                           type = str, default = '1/%d' % (host.cpus()))
1171        argsp.add_argument('--dry-run', help = 'Do not run the actual builds.',
1172                           action = 'store_true')
1173        mailer.add_arguments(argsp)
1174
1175        opts = argsp.parse_args(args[1:])
1176        mail = None
1177        if opts.mail:
1178            mail = mailer.mail(opts)
1179            # Request these now to generate any errors.
1180            from_addr = mail.from_address()
1181            smtp_host = mail.smtp_host()
1182            if 'mail_to' in opts and opts.mail_to is not None:
1183                to_addr = opts.mail_to
1184            else:
1185                to_addr = 'build@rtems.org'
1186        if opts.log is not None:
1187            logf = opts.log
1188        log.default = log.log([logf])
1189        log.notice(title())
1190        log.output(command_line())
1191        if mail:
1192            log.notice('Mail: from:%s to:%s smtp:%s' % (from_addr,
1193                                                        to_addr,
1194                                                        smtp_host))
1195
1196        config = rtems.configuration()
1197        config.load(config_file, opts.build)
1198
1199        if opts.config_report:
1200            log.notice('Configuration Report: %s' % (opts.config_report))
1201            c_profiles = False
1202            c_builds = False
1203            c_archs = False
1204            if opts.config_report == 'all':
1205                c_profiles = True
1206                c_builds = True
1207                c_archs = True
1208            elif opts.config_report == 'profiles':
1209                c_profiles = True
1210            elif opts.config_report == 'builds':
1211                c_builds = True
1212            elif opts.config_report == 'archs':
1213                c_archs = True
1214            log.notice(config.report(c_profiles, c_builds, c_archs))
1215            sys.exit(0)
1216
1217        if opts.rtems is None:
1218            raise error.general('No RTEMS source provided on the command line')
1219        if opts.prefix is not None:
1220            prefix = path.shell(opts.prefix)
1221        if opts.rtems_tools is not None:
1222            tools = path.shell(opts.rtems_tools)
1223        if opts.build_path is not None:
1224            build_dir = path.shell(opts.build_path)
1225
1226        options = { 'stop-on-error'   : opts.stop_on_error,
1227                    'no-clean'        : opts.no_clean,
1228                    'dry-run'         : opts.dry_run,
1229                    'jobs'            : opts.jobs,
1230                    'warnings-report' : opts.warnings_report,
1231                    'failures-report' : opts.failures_report }
1232
1233        b = builder(config, rtems_version(), prefix, tools,
1234                    path.shell(opts.rtems), build_dir, options)
1235
1236        profiles = comma_split(opts.profiles)
1237        archs = comma_split(opts.arch)
1238        bsps = comma_split(opts.bsp)
1239
1240        #
1241        # The default is build a profile.
1242        #
1243        if bsps is not None:
1244            if archs is not None:
1245                raise error.general('--arch supplied with --bsp;' \
1246                                    ' use --bsp=arch/bsp,arch/bsp,..')
1247            what = 'BSPs: %s' % (' '.join(bsps))
1248            b.build_bsps(bsps)
1249        elif archs is not None:
1250            what = 'Archs: %s' % (' '.join(archs))
1251            b.build_archs(archs)
1252        else:
1253            what = 'Profile(s): %s' % (' '.join(profiles))
1254            b.build_profiles(profiles)
1255        end = _now()
1256
1257        #
1258        # Email the results of the build.
1259        #
1260        if mail is not None:
1261            subject = '[rtems-bsp-builder] %s: %s' % (str(start).split('.')[0],
1262                                                      what)
1263            t = title()
1264            body = t + os.linesep
1265            body += '=' * len(t) + os.linesep
1266            body += os.linesep
1267            body += 'Host: %s' % (os.uname()[3]) + os.linesep
1268            body += os.linesep
1269            body += command_line()
1270            body += os.linesep
1271            body += 'Total Time            : %s for %d completed job(s)' % \
1272                    (str(b.duration), b.jobs_completed)
1273            body += os.linesep
1274            body += 'Average BSP Build Time: %s' % (str(b.average))
1275            body += os.linesep + os.linesep
1276            body += 'Builds' + os.linesep
1277            body += '======' + os.linesep
1278            body += os.linesep.join([' ' + cb for cb in config.builds()])
1279            body += os.linesep + os.linesep
1280            body += 'Failures Report' + os.linesep
1281            body += '===============' + os.linesep
1282            body += b.results.failures_report()
1283            body += os.linesep
1284            body += 'Warnings Report' + os.linesep
1285            body += '===============' + os.linesep
1286            body += b.results.warnings_report(summary = True)
1287            mail.send(to_addr, subject, body)
1288
1289    except error.general as gerr:
1290        print(gerr)
1291        print('BSP Build FAILED', file = sys.stderr)
1292        ec = 1
1293    except error.internal as ierr:
1294        print(ierr)
1295        print('BSP Build FAILED', file = sys.stderr)
1296        ec = 1
1297    except error.exit as eerr:
1298        pass
1299    except KeyboardInterrupt:
1300        log.notice('abort: user terminated')
1301        ec = 1
1302    if b is not None:
1303        b.results.report()
1304    sys.exit(ec)
1305
1306if __name__ == "__main__":
1307    run(sys.argv)
Note: See TracBrowser for help on using the repository browser.