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

Last change on this file was ba4648b, checked in by Alex White <alex.white@…>, on 10/28/21 at 19:58:37

rtems-bsp-builder: Fix mail support

This fixes a problem with mailer options support that occurred because
check.py uses argparse.ArgumentParser? instead of tester.rt.options.

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