source: rtems-source-builder/source-builder/sb/reports.py @ 14e5d2e

4.104.114.95
Last change on this file since 14e5d2e was 14e5d2e, checked in by Sebastian Huber <sebastian.huber@…>, on 12/05/14 at 05:48:35

sb: Move git status to formatter classes

  • Property mode set to 100644
File size: 25.2 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
52_line_len = 78
53
54_title = 'RTEMS Tools Project <users@rtems.org>'
55
56_git_status_text = 'RTEMS Source Builder Repository Status'
57
58def _make_path(p, *args):
59    for arg in args:
60        p = path.join(p, arg)
61    return os.path.abspath(path.host(p))
62
63class chunk:
64    def __init__(self):
65        self.data = ''
66
67    def line(self, text):
68        self.data += text + '\n'
69
70    def add(self, text):
71        self.data += text
72
73    def get(self):
74        return self.data
75
76class formatter(object):
77    def set_sbpath(self, sbpath):
78        self.sbpath = sbpath
79
80    def format(self):
81        raise error.general('internal error: formatter.format() not implemented')
82
83    def ext(self):
84        raise error.general('internal error: formatter.ext() not implemented')
85
86    def introduction(self, name, now, intro_text):
87        c = chunk()
88        c.line('=' * _line_len)
89        c.line('%s %s' % (_title, now))
90        if intro_text:
91            c.line('')
92            c.line('%s' % ('\n'.join(intro_text)))
93        c.line('=' * _line_len)
94        c.line('Report: %s' % (name))
95        return c.get()
96
97    def epilogue(self, name):
98        return ''
99
100class asciidoc_formatter(formatter):
101    def format(self):
102        return 'asciidoc'
103
104    def ext(self):
105        return '.txt'
106
107    def introduction(self, name, now, intro_text):
108        c = chunk()
109        h = 'RTEMS Source Builder Report'
110        c.line(h)
111        c.line('=' * len(h))
112        c.line(':doctype: book')
113        c.line(':toc2:')
114        c.line(':toclevels: 5')
115        c.line(':icons:')
116        c.line(':numbered:')
117        c.line(':data-uri:')
118        c.line('')
119        c.line(_title)
120        c.line(now)
121        c.line('')
122        image = _make_path(self.sbpath, options.basepath, 'images', 'rtemswhitebg.jpg')
123        c.line('image:%s["RTEMS",width="20%%"]' % (image))
124        c.line('')
125        if intro_text:
126            c.line('%s' % ('\n'.join(intro_text)))
127        return c.get()
128
129    def git_status(self, valid, dirty, head, remotes):
130        c = chunk()
131        c.line('')
132        c.line("'''")
133        c.line('')
134        c.line('.%s' % (_git_status_text))
135        if valid:
136            c.line('*Remotes*:;;')
137            for r in remotes:
138                if 'url' in remotes[r]:
139                    text = remotes[r]['url']
140                else:
141                    text = 'no URL found'
142                text = '%s: %s' % (r, text)
143                c.line('. %s' % (text))
144            c.line('*Status*:;;')
145            if dirty:
146                c.line('_Repository is dirty_')
147            else:
148                c.line('Clean')
149            c.line('*Head*:;;')
150            c.line('Commit: %s' % (head))
151        else:
152            c.line('_Not a valid GIT repository_')
153        c.line('')
154        c.line("'''")
155        c.line('')
156        return c.get()
157
158class html_formatter(asciidoc_formatter):
159    def format(self):
160        return 'html'
161
162    def ext(self):
163        return '.html'
164
165class text_formatter(formatter):
166    def __init__(self):
167        super(text_formatter, self).__init__()
168        self.cini = ''
169
170    def format(self):
171        return 'text'
172
173    def ext(self):
174        return '.txt'
175
176    def introduction(self, name, now, intro_text):
177        c = chunk()
178        c.line('=' * _line_len)
179        c.line('%s %s' % (_title, now))
180        if intro_text:
181            c.line('')
182            c.line('%s' % ('\n'.join(intro_text)))
183        c.line('=' * _line_len)
184        c.line('Report: %s' % (name))
185        return c.get()
186
187    def git_status_header(self):
188        c = chunk()
189        c.line('-' * _line_len)
190        c.line('%s' % (_git_status_text))
191        return c.get()
192
193    def git_status(self, valid, dirty, head, remotes):
194        c = chunk()
195        c.add(self.git_status_header())
196        if valid:
197            c.line('%s Remotes:' % (self.cini))
198            rc = 0
199            for r in remotes:
200                rc += 1
201                if 'url' in remotes[r]:
202                    text = remotes[r]['url']
203                else:
204                    text = 'no URL found'
205                text = '%s: %s' % (r, text)
206                c.line('%s  %2d: %s' % (self.cini, rc, text))
207            c.line('%s Status:' % (self.cini))
208            if dirty:
209                c.line('%s  Repository is dirty' % (self.cini))
210            else:
211                c.line('%s  Clean' % (self.cini))
212            c.line('%s Head:' % (self.cini))
213            c.line('%s  Commit: %s' % (self.cini, head))
214        else:
215            c.line('%s Not a valid GIT repository' % (self.cini))
216        return c.get()
217
218class ini_formatter(text_formatter):
219    def __init__(self):
220        super(ini_formatter, self).__init__()
221        self.cini = ';'
222
223    def format(self):
224        return 'ini'
225
226    def ext(self):
227        return '.ini'
228
229    def introduction(self, name, now, intro_text):
230        c = chunk()
231        c.line(';')
232        c.line('; %s %s' % (_title, now))
233        if intro_text:
234            c.line(';')
235            c.line('; %s' % ('\n; '.join(intro_text)))
236            c.line(';')
237        return c.get()
238
239    def git_status_header(self):
240        c = chunk()
241        c.line(';')
242        c.line('; %s' % (_git_status_text))
243        c.line(';')
244        return c.get()
245
246class xml_formatter(formatter):
247    def format(self):
248        return 'xml'
249
250    def ext(self):
251        return '.xml'
252
253    def introduction(self, name, now, intro_text):
254        c = chunk()
255        c.line('<RTEMSSourceBuilderReport>')
256        if intro_text:
257            c.line('\t<Introduction>%s</Introduction>' % (intro_text))
258        return c.get()
259
260    def epilogue(self, name):
261        c = chunk()
262        c.line('</RTEMSSourceBuilderReport>')
263        return c.get()
264
265    def git_status(self, valid, dirty, head, remotes):
266        c = chunk()
267        c.line('\t<Git>')
268        if valid:
269            if dirty:
270                c.line('\t\t<Status>dirty</Status>')
271            else:
272                c.line('\t\t<Status>clean</Status>')
273            c.line('\t\t<Commit>' + head + '</Commit>')
274        else:
275            c.line('\t\t<Status>invalid</Status>')
276        c.line('\t</Git>')
277        return c.get()
278
279def _tree_name(path_):
280    return path.splitext(path.basename(path_))[0]
281
282def _merge(_dict, new):
283    new = copy.deepcopy(new)
284    for i in new:
285        if i not in _dict:
286            _dict[i] = new[i]
287        else:
288            _dict[i] += new[i]
289
290class report:
291    """Report the build details about a package given a config file."""
292
293    def __init__(self, formatter, _configs, opts, macros = None):
294        self.formatter = formatter
295        self.format = formatter.format()
296        self.configs = _configs
297        self.opts = opts
298        if macros is None:
299            self.macros = opts.defaults
300        else:
301            self.macros = macros
302        self.sbpath = self.macros.expand('%{_sbdir}')
303        self.formatter.set_sbpath(self.sbpath)
304        self.bset_nesting = 0
305        self.out = ''
306        self.asciidoc = None
307        if self.is_ini():
308            self.cini = ';'
309        else:
310            self.cini = ''
311        self.tree = {}
312        self.files = { 'buildsets':[], 'configs':[] }
313
314    def output(self, text):
315        self.out += text + '\n'
316
317    def is_text(self):
318        return self.format == 'text'
319
320    def is_asciidoc(self):
321        return self.format == 'asciidoc' or self.format == 'html'
322
323    def is_html(self):
324        return self.format == 'html'
325
326    def is_ini(self):
327        return self.format == 'ini'
328
329    def setup(self):
330        if self.is_html():
331            try:
332                import asciidocapi
333            except:
334                raise error.general('installation error: no asciidocapi found')
335            asciidoc_py = _make_path(self.sbpath, options.basepath, 'asciidoc', 'asciidoc.py')
336            try:
337                self.asciidoc = asciidocapi.AsciiDocAPI(asciidoc_py)
338            except:
339                raise error.general('application error: asciidocapi failed')
340
341    def header(self):
342        pass
343
344    def footer(self):
345        pass
346
347    def git_status(self):
348        r = git.repo('.', self.opts, self.macros)
349        self.out += self.formatter.git_status(r.valid(), r.dirty(), r.head(), r.remotes())
350
351    def introduction(self, name, intro_text = None):
352        now = datetime.datetime.now().ctime()
353        self.out += self.formatter.introduction(name, now, intro_text)
354        self.git_status()
355
356    def epilogue(self, name):
357        self.out += self.formatter.epilogue(name)
358
359    def config_start(self, name, _config):
360        self.files['configs'] += [name]
361        for cf in _config.includes():
362            cfbn = path.basename(cf)
363            if cfbn not in self.files['configs']:
364                self.files['configs'] += [cfbn]
365
366    def config_end(self, name, _config):
367        if self.is_asciidoc():
368            self.output('')
369            self.output("'''")
370            self.output('')
371
372    def buildset_start(self, name):
373        self.files['buildsets'] += [name]
374        if self.is_asciidoc():
375            h = '%s' % (name)
376            self.output('=%s %s' % ('=' * self.bset_nesting, h))
377        elif self.is_ini():
378            pass
379        else:
380            self.output('=-' * (_line_len / 2))
381            self.output('Build Set: %s' % (name))
382
383    def buildset_end(self, name):
384        return
385
386    def source(self, macros):
387        def err(msg):
388            raise error.general('%s' % (msg))
389        _srcs = {}
390        for p in sources.get_source_names(macros, err):
391            if 'setup' in sources.get_source_keys(p, macros, err):
392                _srcs[p] = \
393                    [s for s in sources.get_sources(p, macros, err) if not s.startswith('%setup')]
394                _srcs[p] = [macros.expand(s) for s in _srcs[p]]
395        srcs = {}
396        for p in _srcs:
397            srcs[p] = [(s, sources.get_hash(path.basename(s).lower(), macros)) for s in _srcs[p]]
398        return srcs
399
400    def patch(self, macros):
401        def err(msg):
402            raise error.general('%s' % (msg))
403        _patches = {}
404        for n in sources.get_patch_names(macros, err):
405            if 'setup' in sources.get_patch_keys(n, macros, err):
406                _patches[n] = \
407                    [p for p in sources.get_patches(n, macros, err) if not p.startswith('%setup')]
408                _patches[n] = [macros.expand(p.split()[-1]) for p in _patches[n]]
409        patches = {}
410        for n in _patches:
411            patches[n] = [(p, sources.get_hash(path.basename(p).lower(), macros)) for p in _patches[n]]
412        return patches
413
414    def output_info(self, name, info, separated = False):
415        if info is not None:
416            end = ''
417            if self.is_asciidoc():
418                if separated:
419                    self.output('*%s:*::' % (name))
420                    self.output('')
421                else:
422                    self.output('*%s:* ' % (name))
423                    end = ' +'
424                spaces = ''
425            else:
426                self.output(' %s:' % (name))
427                spaces = '  '
428            for l in info:
429                self.output('%s%s%s' % (spaces, l, end))
430            if self.is_asciidoc() and separated:
431                self.output('')
432
433    def output_directive(self, name, directive):
434        if directive is not None:
435            if self.is_asciidoc():
436                self.output('')
437                self.output('*%s*:' % (name))
438                self.output('--------------------------------------------')
439                spaces = ''
440            else:
441                self.output(' %s:' % (name))
442                spaces = '  '
443            for l in directive:
444                self.output('%s%s' % (spaces, l))
445            if self.is_asciidoc():
446                self.output('--------------------------------------------')
447
448    def tree_sources(self, name, tree, sources = []):
449        if 'cfg' in tree:
450            packages = {}
451            if 'sources' in tree['cfg']:
452                _merge(packages, tree['cfg']['sources'])
453            if 'patches' in tree['cfg']:
454                _merge(packages, tree['cfg']['patches'])
455            for package in packages:
456                for source in packages[package]:
457                    if not source[0].startswith('git') and not source[0].startswith('cvs'):
458                        sources += [(path.basename(source[0]), source[0], source[1])]
459        if 'bset' in tree:
460            for node in sorted(tree['bset'].keys()):
461                self.tree_sources(_tree_name(node), tree['bset'][node], sources)
462        return sources
463
464    def config(self, _config, tree, opts, macros):
465        packages = _config.packages()
466        package = packages['main']
467        name = package.name()
468        if len(name) == 0:
469            return
470        tree['file'] += [_config.file_name()]
471        sources = self.source(macros)
472        patches = self.patch(macros)
473        if len(sources):
474            if 'sources' in tree:
475                tree['sources'] = dict(tree['sources'].items() + sources.items())
476            else:
477                tree['sources'] = sources
478        if len(patches):
479            if 'patches' in tree:
480                tree['patches'] = dict(tree['patches'].items() + patches.items())
481            else:
482                tree['patches'] = patches
483        self.config_start(name, _config)
484        if self.is_ini():
485            return
486        if self.is_asciidoc():
487            self.output('*Package*: _%s_ +' % (name))
488            self.output('*Config*: %s' % (_config.file_name()))
489            self.output('')
490        else:
491            self.output('-' * _line_len)
492            self.output('Package: %s' % (name))
493            self.output(' Config: %s' % (_config.file_name()))
494        self.output_info('Summary', package.get_info('summary'), True)
495        self.output_info('URL', package.get_info('url'))
496        self.output_info('Version', package.get_info('version'))
497        self.output_info('Release', package.get_info('release'))
498        self.output_info('Build Arch', package.get_info('buildarch'))
499        if self.is_asciidoc():
500            self.output('')
501        if self.is_asciidoc():
502            self.output('*Sources:*::')
503            if len(sources) == 0:
504                self.output('No sources')
505        else:
506            self.output('  Sources: %d' % (len(sources)))
507        c = 0
508        for name in sources:
509            for s in sources[name]:
510                c += 1
511                if self.is_asciidoc():
512                    self.output('. %s' % (s[0]))
513                else:
514                    self.output('   %2d: %s' % (c, s[0]))
515                if s[1] is None:
516                    h = 'No checksum'
517                else:
518                    hash = s[1].split()
519                    h = '%s: %s' % (hash[0], hash[1])
520                if self.is_asciidoc():
521                    self.output('+\n%s\n' % (h))
522                else:
523                    self.output('       %s' % (h))
524        if self.is_asciidoc():
525            self.output('')
526            self.output('*Patches:*::')
527            if len(patches) == 0:
528                self.output('No patches')
529        else:
530            self.output('  Patches: %s' % (len(patches)))
531        c = 0
532        for name in patches:
533            for p in patches[name]:
534                c += 1
535                if self.is_asciidoc():
536                    self.output('. %s' % (p[0]))
537                else:
538                    self.output('   %2d: %s' % (c, p[0]))
539                hash = p[1]
540                if hash is None:
541                    h = 'No checksum'
542                else:
543                    hash = hash.split()
544                    h = '%s: %s' % (hash[0], hash[1])
545                if self.is_asciidoc():
546                    self.output('+\n(%s)\n' % (h))
547                else:
548                    self.output('       %s' % (h))
549        self.output_directive('Preparation', package.prep())
550        self.output_directive('Build', package.build())
551        self.output_directive('Install', package.install())
552        self.output_directive('Clean', package.clean())
553        self.config_end(name, _config)
554
555    def generate_ini_tree(self, name, tree, prefix_char, prefix = ''):
556        if prefix_char == '|':
557            c = '|'
558        else:
559            c = '+'
560        self.output('; %s  %s- %s' % (prefix, c, name))
561        prefix += '  %s ' % (prefix_char)
562        if 'cfg' in tree:
563            files = sorted(tree['cfg']['file'])
564            if len(files):
565                for f in range(0, len(files) - 1):
566                    self.output('; %s  |- %s' % (prefix, files[f]))
567                if 'bset' in tree and len(tree['bset'].keys()):
568                    c = '|'
569                else:
570                    c = '+'
571                self.output('; %s  %s- %s' % (prefix, c, files[f + 1]))
572        if 'bset' in tree:
573            nodes = sorted(tree['bset'].keys())
574            for node in range(0, len(nodes)):
575                if node == len(nodes) - 1:
576                    prefix_char = ' '
577                else:
578                    prefix_char = '|'
579                self.generate_ini_tree(nodes[node],
580                                       tree['bset'][nodes[node]],
581                                       prefix_char,
582                                       prefix)
583
584    def generate_ini_node(self, name, tree, sections = []):
585        if name not in sections:
586            sections += [name]
587            self.output('')
588            self.output('[%s]' % (name))
589            if 'bset' in tree and len(tree['bset']):
590                self.output(' packages = %s' % \
591                                (', '.join([_tree_name(n) for n in sorted(tree['bset'])])))
592            if 'cfg' in tree:
593                packages = {}
594                if 'sources' in tree['cfg']:
595                    _merge(packages, tree['cfg']['sources'])
596                if 'patches' in tree['cfg']:
597                    _merge(packages, tree['cfg']['patches'])
598                for package in packages:
599                    self.output(' %s = %s' % (package, ', '.join([s[0] for s in packages[package]])))
600            if 'bset' in tree:
601                for node in sorted(tree['bset'].keys()):
602                    self.generate_ini_node(_tree_name(node), tree['bset'][node], sections)
603
604    def generate_ini_source(self, sources):
605        self.output('')
606        self.output('[source]')
607        for source in sources:
608            self.output(' %s = %s' % (source[0], source[1]))
609
610    def generate_ini_hash(self, sources):
611        self.output('')
612        self.output('[hash]')
613        for source in sources:
614            if source[2] is None:
615                hash = ''
616            else:
617                hash = source[2].split()
618                hash = '%s:%s' % (hash[0], hash[1])
619            self.output(' %s = %s' % (source[0], hash))
620
621    def generate_ini(self):
622        #self.output(pp.pformat(self.tree))
623        nodes = sorted([node for node in self.tree.keys() if node != 'bset'])
624        self.output(';')
625        self.output('; Configuration Tree:')
626        for node in range(0, len(nodes)):
627            if node == len(nodes) - 1:
628                prefix_char = ' '
629            else:
630                prefix_char = '|'
631            self.generate_ini_tree(nodes[node], self.tree[nodes[node]], prefix_char)
632        self.output(';')
633        sources = []
634        for node in nodes:
635            sources += self.tree_sources(_tree_name(node), self.tree[node])
636        sources = sorted(set(sources))
637        self.generate_ini_source(sources)
638        self.generate_ini_hash(sources)
639        for node in nodes:
640            self.generate_ini_node(_tree_name(node), self.tree[node])
641
642    def write(self, name):
643        if self.is_html():
644            if self.asciidoc is None:
645                raise error.general('asciidoc not initialised')
646            import StringIO
647            infile = StringIO.StringIO(self.out)
648            outfile = StringIO.StringIO()
649            self.asciidoc.execute(infile, outfile)
650            self.out = outfile.getvalue()
651            infile.close()
652            outfile.close()
653        elif self.is_ini():
654            self.generate_ini()
655        if name is not None:
656            try:
657                o = open(path.host(name), "w")
658                o.write(self.out)
659                o.close()
660                del o
661            except IOError, err:
662                raise error.general('writing output file: %s: %s' % (name, err))
663
664    def generate(self, name, tree = None, opts = None, defaults = None):
665        self.bset_nesting += 1
666        self.buildset_start(name)
667        if tree is None:
668            tree = self.tree
669        if opts is None:
670            opts = self.opts
671        bset = setbuilder.buildset(name, self.configs, opts, defaults)
672        if name in tree:
673            raise error.general('duplicate build set in tree: %s' % (name))
674        tree[name] = { 'bset': { }, 'cfg': { 'file': []  } }
675        for c in bset.load():
676            macros = copy.copy(bset.macros)
677            if c.endswith('.bset'):
678                self.generate(c, tree[name]['bset'], bset.opts, macros)
679            elif c.endswith('.cfg'):
680                self.config(config.file(c, bset.opts, macros),
681                            tree[name]['cfg'], bset.opts, macros)
682            else:
683                raise error.general('invalid config type: %s' % (c))
684        self.buildset_end(name)
685        self.bset_nesting -= 1
686
687    def create(self, inname, outname = None, intro_text = None):
688        self.setup()
689        self.introduction(inname, intro_text)
690        self.generate(inname)
691        self.epilogue(inname)
692        self.write(outname)
693
694def run(args):
695    try:
696        optargs = { '--list-bsets':   'List available build sets',
697                    '--list-configs': 'List available configurations',
698                    '--format':       'Output format (text, html, asciidoc, ini, xml)',
699                    '--output':       'File name to output the report' }
700        opts = options.load(args, optargs)
701        if opts.get_arg('--output') and len(opts.params()) > 1:
702            raise error.general('--output can only be used with a single config')
703        print 'RTEMS Source Builder, Reporter v%s' % (version.str())
704        opts.log_info()
705        if not check.host_setup(opts):
706            log.warning('forcing build with known host setup problems')
707        configs = build.get_configs(opts)
708        if not setbuilder.list_bset_cfg_files(opts, configs):
709            output = opts.get_arg('--output')
710            if output is not None:
711                output = output[1]
712            formatter = text_formatter()
713            format_opt = opts.get_arg('--format')
714            if format_opt:
715                if len(format_opt) != 2:
716                    raise error.general('invalid format option: %s' % ('='.join(format_opt)))
717                if format_opt[1] == 'text':
718                    pass
719                elif format_opt[1] == 'asciidoc':
720                    formatter = asciidoc_formatter()
721                elif format_opt[1] == 'html':
722                    formatter = html_formatter()
723                elif format_opt[1] == 'ini':
724                    formatter = ini_formatter()
725                elif format_opt[1] == 'xml':
726                    formatter = xml_formatter()
727                else:
728                    raise error.general('invalid format: %s' % (format_opt[1]))
729            r = report(formatter, configs, opts)
730            for _config in opts.params():
731                if output is None:
732                    outname = path.splitext(_config)[0] + formatter.ext()
733                    outname = outname.replace('/', '-')
734                else:
735                    outname = output
736                config = build.find_config(_config, configs)
737                if config is None:
738                    raise error.general('config file not found: %s' % (inname))
739                r.create(config, outname)
740            del r
741        else:
742            raise error.general('invalid config type: %s' % (config))
743    except error.general, gerr:
744        print gerr
745        sys.exit(1)
746    except error.internal, ierr:
747        print ierr
748        sys.exit(1)
749    except error.exit, eerr:
750        pass
751    except KeyboardInterrupt:
752        log.notice('abort: user terminated')
753        sys.exit(1)
754    sys.exit(0)
755
756if __name__ == "__main__":
757    run(sys.argv)
Note: See TracBrowser for help on using the repository browser.