source: rtems-source-builder/source-builder/sb/reports.py @ 5d56584

4.104.114.95
Last change on this file since 5d56584 was 5d56584, checked in by Sebastian Huber <sebastian.huber@…>, on 12/04/14 at 05:22:15

sb: Introduce formatter classes

Use inheritance instead of ifs.

  • Property mode set to 100644
File size: 23.4 KB
Line 
1#
2# RTEMS Tools Project (http://www.rtems.org/)
3# Copyright 2010-2013 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# Permission to use, copy, modify, and/or distribute this software for any
9# purpose with or without fee is hereby granted, provided that the above
10# copyright notice and this permission notice appear in all copies.
11#
12# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19
20#
21# This code builds a package given a config file. It only builds to be
22# installed not to be package unless you run a packager around this.
23#
24
25import copy
26import datetime
27import os
28import sys
29
30import pprint
31pp = pprint.PrettyPrinter(indent = 2)
32
33try:
34    import build
35    import check
36    import config
37    import error
38    import git
39    import log
40    import options
41    import path
42    import setbuilder
43    import sources
44    import version
45except KeyboardInterrupt:
46    print 'user terminated'
47    sys.exit(1)
48except:
49    print 'error: unknown application load error'
50    sys.exit(1)
51
52class formatter(object):
53    def format(self):
54        raise error.general('internal error: formatter.format() not implemented')
55
56    def ext(self):
57        raise error.general('internal error: formatter.ext() not implemented')
58
59class asciidoc_formatter(formatter):
60    def format(self):
61        return 'asciidoc'
62
63    def ext(self):
64        return '.txt'
65
66class ini_formatter(formatter):
67    def format(self):
68        return 'ini'
69
70    def ext(self):
71        return '.ini'
72
73class html_formatter(formatter):
74    def format(self):
75        return 'html'
76
77    def ext(self):
78        return '.html'
79
80class text_formatter(formatter):
81    def format(self):
82        return 'text'
83
84    def ext(self):
85        return '.txt'
86
87def _tree_name(path_):
88    return path.splitext(path.basename(path_))[0]
89
90def _merge(_dict, new):
91    new = copy.deepcopy(new)
92    for i in new:
93        if i not in _dict:
94            _dict[i] = new[i]
95        else:
96            _dict[i] += new[i]
97
98class report:
99    """Report the build details about a package given a config file."""
100
101    line_len = 78
102
103    def __init__(self, formatter, _configs, opts, macros = None):
104        self.formatter = formatter
105        self.format = formatter.format()
106        self.configs = _configs
107        self.opts = opts
108        if macros is None:
109            self.macros = opts.defaults
110        else:
111            self.macros = macros
112        self.bset_nesting = 0
113        self.configs_active = False
114        self.out = ''
115        self.asciidoc = None
116        if self.is_ini():
117            self.cini = ';'
118        else:
119            self.cini = ''
120        self.tree = {}
121        self.files = { 'buildsets':[], 'configs':[] }
122
123    def _sbpath(self, *args):
124        p = self.macros.expand('%{_sbdir}')
125        for arg in args:
126            p = path.join(p, arg)
127        return os.path.abspath(path.host(p))
128
129    def output(self, text):
130        self.out += '%s\n' % (text)
131
132    def is_text(self):
133        return self.format == 'text'
134
135    def is_asciidoc(self):
136        return self.format == 'asciidoc' or self.format == 'html'
137
138    def is_html(self):
139        return self.format == 'html'
140
141    def is_ini(self):
142        return self.format == 'ini'
143
144    def setup(self):
145        if self.is_html():
146            try:
147                import asciidocapi
148            except:
149                raise error.general('installation error: no asciidocapi found')
150            asciidoc_py = self._sbpath(options.basepath, 'asciidoc', 'asciidoc.py')
151            try:
152                self.asciidoc = asciidocapi.AsciiDocAPI(asciidoc_py)
153            except:
154                raise error.general('application error: asciidocapi failed')
155
156    def header(self):
157        pass
158
159    def footer(self):
160        pass
161
162    def git_status(self):
163        text = 'RTEMS Source Builder Repository Status'
164        if self.is_asciidoc():
165            self.output('')
166            self.output("'''")
167            self.output('')
168            self.output('.%s' % (text))
169        elif self.is_ini():
170            self.output(';')
171            self.output('; %s' % (text))
172            self.output(';')
173        else:
174            self.output('-' * self.line_len)
175            self.output('%s' % (text))
176        repo = git.repo('.', self.opts, self.macros)
177        repo_valid = repo.valid()
178        if repo_valid:
179            if self.is_asciidoc():
180                self.output('*Remotes*:;;')
181            else:
182                self.output('%s Remotes:' % (self.cini))
183            repo_remotes = repo.remotes()
184            rc = 0
185            for r in repo_remotes:
186                rc += 1
187                if 'url' in repo_remotes[r]:
188                    text = repo_remotes[r]['url']
189                else:
190                    text = 'no URL found'
191                text = '%s: %s' % (r, text)
192                if self.is_asciidoc():
193                    self.output('. %s' % (text))
194                else:
195                    self.output('%s  %2d: %s' % (self.cini, rc, text))
196            if self.is_asciidoc():
197                self.output('*Status*:;;')
198            else:
199                self.output('%s Status:' % (self.cini))
200            if repo.dirty():
201                if self.is_asciidoc():
202                    self.output('_Repository is dirty_')
203                else:
204                    self.output('%s  Repository is dirty' % (self.cini))
205            else:
206                if self.is_asciidoc():
207                    self.output('Clean')
208                else:
209                    self.output('%s  Clean' % (self.cini))
210            repo_head = repo.head()
211            if self.is_asciidoc():
212                self.output('*Head*:;;')
213                self.output('Commit: %s' % (repo_head))
214            else:
215                self.output('%s Head:' % (self.cini))
216                self.output('%s  Commit: %s' % (self.cini, repo_head))
217        else:
218            if self.is_asciidoc():
219                self.output('_Not a valid GIT repository_')
220            else:
221                self.output('%s Not a valid GIT repository' % (self.cini))
222        if self.is_asciidoc():
223            self.output('')
224            self.output("'''")
225            self.output('')
226
227    def introduction(self, name, intro_text = None):
228        now = datetime.datetime.now().ctime()
229        title = 'RTEMS Tools Project <users@rtems.org>'
230        if self.is_asciidoc():
231            h = 'RTEMS Source Builder Report'
232            self.output(h)
233            self.output('=' * len(h))
234            self.output(':doctype: book')
235            self.output(':toc2:')
236            self.output(':toclevels: 5')
237            self.output(':icons:')
238            self.output(':numbered:')
239            self.output(':data-uri:')
240            self.output('')
241            self.output(title)
242            self.output(datetime.datetime.now().ctime())
243            self.output('')
244            image = self._sbpath(options.basepath, 'images', 'rtemswhitebg.jpg')
245            self.output('image:%s["RTEMS",width="20%%"]' % (image))
246            self.output('')
247            if intro_text:
248                self.output('%s' % ('\n'.join(intro_text)))
249        elif self.is_ini():
250            self.output(';')
251            self.output('; %s %s' % (title, now))
252            if intro_text:
253                self.output(';')
254                self.output('; %s' % ('\n; '.join(intro_text)))
255                self.output(';')
256        else:
257            self.output('=' * self.line_len)
258            self.output('%s %s' % (title, now))
259            if intro_text:
260                self.output('')
261                self.output('%s' % ('\n'.join(intro_text)))
262            self.output('=' * self.line_len)
263            self.output('Report: %s' % (name))
264        self.git_status()
265
266    def config_start(self, name, _config):
267        self.files['configs'] += [name]
268        for cf in _config.includes():
269            cfbn = path.basename(cf)
270            if cfbn not in self.files['configs']:
271                self.files['configs'] += [cfbn]
272        first = not self.configs_active
273        self.configs_active = True
274
275    def config_end(self, name, _config):
276        if self.is_asciidoc():
277            self.output('')
278            self.output("'''")
279            self.output('')
280
281    def buildset_start(self, name):
282        self.files['buildsets'] += [name]
283        if self.is_asciidoc():
284            h = '%s' % (name)
285            self.output('=%s %s' % ('=' * self.bset_nesting, h))
286        elif self.is_ini():
287            pass
288        else:
289            self.output('=-' * (self.line_len / 2))
290            self.output('Build Set: %s' % (name))
291
292    def buildset_end(self, name):
293        self.configs_active = False
294
295    def source(self, macros):
296        def err(msg):
297            raise error.general('%s' % (msg))
298        _srcs = {}
299        for p in sources.get_source_names(macros, err):
300            if 'setup' in sources.get_source_keys(p, macros, err):
301                _srcs[p] = \
302                    [s for s in sources.get_sources(p, macros, err) if not s.startswith('%setup')]
303                _srcs[p] = [macros.expand(s) for s in _srcs[p]]
304        srcs = {}
305        for p in _srcs:
306            srcs[p] = [(s, sources.get_hash(path.basename(s).lower(), macros)) for s in _srcs[p]]
307        return srcs
308
309    def patch(self, macros):
310        def err(msg):
311            raise error.general('%s' % (msg))
312        _patches = {}
313        for n in sources.get_patch_names(macros, err):
314            if 'setup' in sources.get_patch_keys(n, macros, err):
315                _patches[n] = \
316                    [p for p in sources.get_patches(n, macros, err) if not p.startswith('%setup')]
317                _patches[n] = [macros.expand(p.split()[-1]) for p in _patches[n]]
318        patches = {}
319        for n in _patches:
320            patches[n] = [(p, sources.get_hash(path.basename(p).lower(), macros)) for p in _patches[n]]
321        return patches
322
323    def output_info(self, name, info, separated = False):
324        if info is not None:
325            end = ''
326            if self.is_asciidoc():
327                if separated:
328                    self.output('*%s:*::' % (name))
329                    self.output('')
330                else:
331                    self.output('*%s:* ' % (name))
332                    end = ' +'
333                spaces = ''
334            else:
335                self.output(' %s:' % (name))
336                spaces = '  '
337            for l in info:
338                self.output('%s%s%s' % (spaces, l, end))
339            if self.is_asciidoc() and separated:
340                self.output('')
341
342    def output_directive(self, name, directive):
343        if directive is not None:
344            if self.is_asciidoc():
345                self.output('')
346                self.output('*%s*:' % (name))
347                self.output('--------------------------------------------')
348                spaces = ''
349            else:
350                self.output(' %s:' % (name))
351                spaces = '  '
352            for l in directive:
353                self.output('%s%s' % (spaces, l))
354            if self.is_asciidoc():
355                self.output('--------------------------------------------')
356
357    def tree_sources(self, name, tree, sources = []):
358        if 'cfg' in tree:
359            packages = {}
360            if 'sources' in tree['cfg']:
361                _merge(packages, tree['cfg']['sources'])
362            if 'patches' in tree['cfg']:
363                _merge(packages, tree['cfg']['patches'])
364            for package in packages:
365                for source in packages[package]:
366                    if not source[0].startswith('git') and not source[0].startswith('cvs'):
367                        sources += [(path.basename(source[0]), source[0], source[1])]
368        if 'bset' in tree:
369            for node in sorted(tree['bset'].keys()):
370                self.tree_sources(_tree_name(node), tree['bset'][node], sources)
371        return sources
372
373    def config(self, _config, tree, opts, macros):
374        packages = _config.packages()
375        package = packages['main']
376        name = package.name()
377        if len(name) == 0:
378            return
379        tree['file'] += [_config.file_name()]
380        sources = self.source(macros)
381        patches = self.patch(macros)
382        if len(sources):
383            if 'sources' in tree:
384                tree['sources'] = dict(tree['sources'].items() + sources.items())
385            else:
386                tree['sources'] = sources
387        if len(patches):
388            if 'patches' in tree:
389                tree['patches'] = dict(tree['patches'].items() + patches.items())
390            else:
391                tree['patches'] = patches
392        self.config_start(name, _config)
393        if self.is_ini():
394            return
395        if self.is_asciidoc():
396            self.output('*Package*: _%s_ +' % (name))
397            self.output('*Config*: %s' % (_config.file_name()))
398            self.output('')
399        else:
400            self.output('-' * self.line_len)
401            self.output('Package: %s' % (name))
402            self.output(' Config: %s' % (_config.file_name()))
403        self.output_info('Summary', package.get_info('summary'), True)
404        self.output_info('URL', package.get_info('url'))
405        self.output_info('Version', package.get_info('version'))
406        self.output_info('Release', package.get_info('release'))
407        self.output_info('Build Arch', package.get_info('buildarch'))
408        if self.is_asciidoc():
409            self.output('')
410        if self.is_asciidoc():
411            self.output('*Sources:*::')
412            if len(sources) == 0:
413                self.output('No sources')
414        else:
415            self.output('  Sources: %d' % (len(sources)))
416        c = 0
417        for name in sources:
418            for s in sources[name]:
419                c += 1
420                if self.is_asciidoc():
421                    self.output('. %s' % (s[0]))
422                else:
423                    self.output('   %2d: %s' % (c, s[0]))
424                if s[1] is None:
425                    h = 'No checksum'
426                else:
427                    hash = s[1].split()
428                    h = '%s: %s' % (hash[0], hash[1])
429                if self.is_asciidoc():
430                    self.output('+\n%s\n' % (h))
431                else:
432                    self.output('       %s' % (h))
433        if self.is_asciidoc():
434            self.output('')
435            self.output('*Patches:*::')
436            if len(patches) == 0:
437                self.output('No patches')
438        else:
439            self.output('  Patches: %s' % (len(patches)))
440        c = 0
441        for name in patches:
442            for p in patches[name]:
443                c += 1
444                if self.is_asciidoc():
445                    self.output('. %s' % (p[0]))
446                else:
447                    self.output('   %2d: %s' % (c, p[0]))
448                hash = p[1]
449                if hash is None:
450                    h = 'No checksum'
451                else:
452                    hash = hash.split()
453                    h = '%s: %s' % (hash[0], hash[1])
454                if self.is_asciidoc():
455                    self.output('+\n(%s)\n' % (h))
456                else:
457                    self.output('       %s' % (h))
458        self.output_directive('Preparation', package.prep())
459        self.output_directive('Build', package.build())
460        self.output_directive('Install', package.install())
461        self.output_directive('Clean', package.clean())
462        self.config_end(name, _config)
463
464    def generate_ini_tree(self, name, tree, prefix_char, prefix = ''):
465        if prefix_char == '|':
466            c = '|'
467        else:
468            c = '+'
469        self.output('; %s  %s- %s' % (prefix, c, name))
470        prefix += '  %s ' % (prefix_char)
471        if 'cfg' in tree:
472            files = sorted(tree['cfg']['file'])
473            if len(files):
474                for f in range(0, len(files) - 1):
475                    self.output('; %s  |- %s' % (prefix, files[f]))
476                if 'bset' in tree and len(tree['bset'].keys()):
477                    c = '|'
478                else:
479                    c = '+'
480                self.output('; %s  %s- %s' % (prefix, c, files[f + 1]))
481        if 'bset' in tree:
482            nodes = sorted(tree['bset'].keys())
483            for node in range(0, len(nodes)):
484                if node == len(nodes) - 1:
485                    prefix_char = ' '
486                else:
487                    prefix_char = '|'
488                self.generate_ini_tree(nodes[node],
489                                       tree['bset'][nodes[node]],
490                                       prefix_char,
491                                       prefix)
492
493    def generate_ini_node(self, name, tree, sections = []):
494        if name not in sections:
495            sections += [name]
496            self.output('')
497            self.output('[%s]' % (name))
498            if 'bset' in tree and len(tree['bset']):
499                self.output(' packages = %s' % \
500                                (', '.join([_tree_name(n) for n in sorted(tree['bset'])])))
501            if 'cfg' in tree:
502                packages = {}
503                if 'sources' in tree['cfg']:
504                    _merge(packages, tree['cfg']['sources'])
505                if 'patches' in tree['cfg']:
506                    _merge(packages, tree['cfg']['patches'])
507                for package in packages:
508                    self.output(' %s = %s' % (package, ', '.join([s[0] for s in packages[package]])))
509            if 'bset' in tree:
510                for node in sorted(tree['bset'].keys()):
511                    self.generate_ini_node(_tree_name(node), tree['bset'][node], sections)
512
513    def generate_ini_source(self, sources):
514        self.output('')
515        self.output('[source]')
516        for source in sources:
517            self.output(' %s = %s' % (source[0], source[1]))
518
519    def generate_ini_hash(self, sources):
520        self.output('')
521        self.output('[hash]')
522        for source in sources:
523            if source[2] is None:
524                hash = ''
525            else:
526                hash = source[2].split()
527                hash = '%s:%s' % (hash[0], hash[1])
528            self.output(' %s = %s' % (source[0], hash))
529
530    def generate_ini(self):
531        #self.output(pp.pformat(self.tree))
532        nodes = sorted([node for node in self.tree.keys() if node != 'bset'])
533        self.output(';')
534        self.output('; Configuration Tree:')
535        for node in range(0, len(nodes)):
536            if node == len(nodes) - 1:
537                prefix_char = ' '
538            else:
539                prefix_char = '|'
540            self.generate_ini_tree(nodes[node], self.tree[nodes[node]], prefix_char)
541        self.output(';')
542        sources = []
543        for node in nodes:
544            sources += self.tree_sources(_tree_name(node), self.tree[node])
545        sources = sorted(set(sources))
546        self.generate_ini_source(sources)
547        self.generate_ini_hash(sources)
548        for node in nodes:
549            self.generate_ini_node(_tree_name(node), self.tree[node])
550
551    def write(self, name):
552        if self.is_html():
553            if self.asciidoc is None:
554                raise error.general('asciidoc not initialised')
555            import StringIO
556            infile = StringIO.StringIO(self.out)
557            outfile = StringIO.StringIO()
558            self.asciidoc.execute(infile, outfile)
559            self.out = outfile.getvalue()
560            infile.close()
561            outfile.close()
562        elif self.is_ini():
563            self.generate_ini()
564        if name is not None:
565            try:
566                o = open(path.host(name), "w")
567                o.write(self.out)
568                o.close()
569                del o
570            except IOError, err:
571                raise error.general('writing output file: %s: %s' % (name, err))
572
573    def generate(self, name, tree = None, opts = None, defaults = None):
574        self.bset_nesting += 1
575        self.buildset_start(name)
576        if tree is None:
577            tree = self.tree
578        if opts is None:
579            opts = self.opts
580        bset = setbuilder.buildset(name, self.configs, opts, defaults)
581        if name in tree:
582            raise error.general('duplicate build set in tree: %s' % (name))
583        tree[name] = { 'bset': { }, 'cfg': { 'file': []  } }
584        for c in bset.load():
585            macros = copy.copy(bset.macros)
586            if c.endswith('.bset'):
587                self.generate(c, tree[name]['bset'], bset.opts, macros)
588            elif c.endswith('.cfg'):
589                self.config(config.file(c, bset.opts, macros),
590                            tree[name]['cfg'], bset.opts, macros)
591            else:
592                raise error.general('invalid config type: %s' % (c))
593        self.buildset_end(name)
594        self.bset_nesting -= 1
595
596    def create(self, inname, outname = None, intro_text = None):
597        self.setup()
598        self.introduction(inname, intro_text)
599        self.generate(inname)
600        self.write(outname)
601
602def run(args):
603    try:
604        optargs = { '--list-bsets':   'List available build sets',
605                    '--list-configs': 'List available configurations',
606                    '--format':       'Output format (text, html, asciidoc, ini)',
607                    '--output':       'File name to output the report' }
608        opts = options.load(args, optargs)
609        if opts.get_arg('--output') and len(opts.params()) > 1:
610            raise error.general('--output can only be used with a single config')
611        print 'RTEMS Source Builder, Reporter v%s' % (version.str())
612        opts.log_info()
613        if not check.host_setup(opts):
614            log.warning('forcing build with known host setup problems')
615        configs = build.get_configs(opts)
616        if not setbuilder.list_bset_cfg_files(opts, configs):
617            output = opts.get_arg('--output')
618            if output is not None:
619                output = output[1]
620            formatter = text_formatter()
621            format_opt = opts.get_arg('--format')
622            if format_opt:
623                if len(format_opt) != 2:
624                    raise error.general('invalid format option: %s' % ('='.join(format_opt)))
625                if format_opt[1] == 'text':
626                    pass
627                elif format_opt[1] == 'asciidoc':
628                    formatter = asciidoc_formatter()
629                elif format_opt[1] == 'html':
630                    formatter = html_formatter()
631                elif format_opt[1] == 'ini':
632                    formatter = ini_formatter()
633                else:
634                    raise error.general('invalid format: %s' % (format_opt[1]))
635            r = report(formatter, configs, opts)
636            for _config in opts.params():
637                if output is None:
638                    outname = path.splitext(_config)[0] + formatter.ext()
639                    outname = outname.replace('/', '-')
640                else:
641                    outname = output
642                config = build.find_config(_config, configs)
643                if config is None:
644                    raise error.general('config file not found: %s' % (inname))
645                r.create(config, outname)
646            del r
647        else:
648            raise error.general('invalid config type: %s' % (config))
649    except error.general, gerr:
650        print gerr
651        sys.exit(1)
652    except error.internal, ierr:
653        print ierr
654        sys.exit(1)
655    except error.exit, eerr:
656        pass
657    except KeyboardInterrupt:
658        log.notice('abort: user terminated')
659        sys.exit(1)
660    sys.exit(0)
661
662if __name__ == "__main__":
663    run(sys.argv)
Note: See TracBrowser for help on using the repository browser.