source: rtems-source-builder/source-builder/sb/build.py @ 6dc551c

5
Last change on this file since 6dc551c was 6dc551c, checked in by Chris Johns <chrisj@…>, on 10/12/17 at 02:40:12

sb: Move the option check for reporting errors to the error reporter.

Updates #2536.

  • Property mode set to 100644
File size: 22.8 KB
RevLine 
[bf13d27]1#
2# RTEMS Tools Project (http://www.rtems.org/)
[649a64c]3# Copyright 2010-2013 Chris Johns (chrisj@rtems.org)
[bf13d27]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
[3a972f6]25from __future__ import print_function
26
[6444d58]27import copy
[bf13d27]28import getopt
[015fa1b]29import glob
[bf13d27]30import os
31import shutil
32import stat
33import sys
34
[efb6688]35try:
36    import check
37    import config
[649a64c]38    import download
[efb6688]39    import error
[74da24c]40    import ereport
[efb6688]41    import execute
42    import log
[cb12e48]43    import options
[efb6688]44    import path
[9a15c40]45    import sources
[efb6688]46    import version
47except KeyboardInterrupt:
[3a972f6]48    print('abort: user terminated')
[efb6688]49    sys.exit(1)
50except:
[3a972f6]51    print('error: unknown application load error')
[efb6688]52    sys.exit(1)
[bf13d27]53
54class script:
55    """Create and manage a shell script."""
56
[5142bec]57    def __init__(self):
[bf13d27]58        self.reset()
59
60    def reset(self):
61        self.body = []
62        self.lc = 0
63
64    def append(self, text):
65        if type(text) is str:
66            text = text.splitlines()
[5142bec]67        if not log.quiet:
[bf13d27]68            i = 0
69            for l in text:
70                i += 1
[cafbcc6]71                log.output('script:%3d: %s' % (self.lc + i, l))
[bf13d27]72        self.lc += len(text)
73        self.body.extend(text)
74
75    def write(self, name, check_for_errors = False):
76        s = None
77        try:
[ab8319a]78            s = open(path.host(name), 'w')
[bf13d27]79            s.write('\n'.join(self.body))
80            s.close()
[ab8319a]81            os.chmod(path.host(name), stat.S_IRWXU | \
[bf13d27]82                         stat.S_IRGRP | stat.S_IXGRP | \
83                         stat.S_IROTH | stat.S_IXOTH)
[3a972f6]84        except IOError as err:
[bf13d27]85            raise error.general('creating script: ' + name)
86        except:
87            if s is not None:
88                s.close()
89            raise
90        if s is not None:
91            s.close()
92
93class build:
94    """Build a package given a config file."""
95
[5237f1cc]96    def _name_(self, name):
97        #
98        # If on Windows use shorter names to keep the build paths.
99        #
100        if options.host_windows:
101            buildname = ''
102            add = True
103            for c in name:
104                if c == '-':
105                    add = True
106                elif add:
107                    buildname += c
108                    add = False
109            return buildname
110        else:
111            return name
112
[53290f9]113    def _generate_report_(self, header, footer = None):
[6dc551c]114        ereport.generate('rsb-report-%s.txt' % self.macros['name'],
115                         self.opts, header, footer)
[65d9457]116
[cb12e48]117    def __init__(self, name, create_tar_files, opts, macros = None):
[339f92f]118        try:
119            self.opts = opts
[6444d58]120            self.init_name = name
121            self.init_macros = macros
122            self.config = None
[339f92f]123            self.create_tar_files = create_tar_files
124            log.notice('config: ' + name)
[6444d58]125            self.set_macros(macros)
[339f92f]126            self.config = config.file(name, opts, self.macros)
127            self.script = script()
128            self.macros['buildname'] = self._name_(self.macros['name'])
[3a972f6]129        except error.general as gerr:
[339f92f]130            log.notice(str(gerr))
131            log.stderr('Build FAILED')
132            raise
[3a972f6]133        except error.internal as ierr:
[339f92f]134            log.notice(str(ierr))
135            log.stderr('Internal Build FAILED')
136            raise
137        except:
138            raise
[bf13d27]139
[6444d58]140    def copy_init_macros(self):
141        return copy.copy(self.init_macros)
142
143    def copy_macros(self):
144        return copy.copy(self.macros)
145
146    def set_macros(self, macros):
147        if macros is None:
[610ae57]148            self.macros = copy.copy(self.opts.defaults)
[6444d58]149        else:
150            self.macros = copy.copy(macros)
151        if self.config:
152            self.config.set_macros(self.macros)
153
[ab8319a]154    def rmdir(self, rmpath):
[5142bec]155        log.output('removing: %s' % (path.host(rmpath)))
[bf13d27]156        if not self.opts.dry_run():
[ab8319a]157            if path.exists(rmpath):
[ee47d72]158                path.removeall(rmpath)
[bf13d27]159
[ab8319a]160    def mkdir(self, mkpath):
[5142bec]161        log.output('making dir: %s' % (path.host(mkpath)))
[bf13d27]162        if not self.opts.dry_run():
[ee47d72]163            path.mkdir(mkpath)
[bf13d27]164
[4f26bdb]165    def canadian_cross(self):
166        _host = self.config.expand('%{_host}')
167        _build = self.config.expand('%{_build}')
168        _target = self.config.expand('%{_target}')
[6444d58]169        _allowed = self.config.defined('%{allow_cxc}')
170        if len(_host) and len(_build) and (_target) and \
171           _allowed and _host != _build and _host != _target:
172            return True
173        return False
174
175    def installable(self):
176        _host = self.config.expand('%{_host}')
177        _build = self.config.expand('%{_build}')
178        _canadian_cross = self.canadian_cross()
179        if self.macros.get('_disable_installing') and \
180           self.config.expand('%{_disable_installing}') == 'yes':
181            _disable_installing = True
182        else:
183            _disable_installing = False
184        _no_install = self.opts.no_install()
185        log.trace('_build: installable: host=%s build=%s ' \
186                  'no-install=%r Cxc=%r disable_installing=%r disabled=%r' % \
187                  (_host, _build, _no_install, _canadian_cross, _disable_installing, \
188                   self.disabled()))
189        return len(_host) and len(_build) and \
190            not self.disabled() and \
191            not _disable_installing and \
192            not _canadian_cross
[4f26bdb]193
[9a15c40]194    def source(self, name):
195        #
196        # Return the list of sources. Merge in any macro defined sources as
197        # these may be overridden by user loaded macros.
198        #
199        _map = 'source-%s' % (name)
[d3629a9]200        src_keys = [s for s in self.macros.map_keys(_map) if s != 'setup']
[9a15c40]201        if len(src_keys) == 0:
202            raise error.general('no source set: %s (%s)' % (name, _map))
203        srcs = []
204        for s in src_keys:
205            sm = self.macros.get(s, globals = False, maps = _map)
206            if sm is None:
207                raise error.internal('source macro not found: %s in %s (%s)' % \
208                                         (s, name, _map))
[383f7e6]209            opts = []
210            url = []
211            for sp in sm[2].split():
212                if len(url) == 0 and sp[0] == '-':
213                    opts += [sp]
214                else:
215                    url += [sp]
216            if len(url) == 0:
217                raise error.general('source URL not found: %s' % (' '.join(args)))
218            #
219            # Look for --rsb-file as an option we use as a local file name.
220            # This can be used if a URL has no reasonable file name the
221            # download URL parser can figure out.
222            #
223            file_override = None
224            if len(opts) > 0:
225                for o in opts:
226                    if o.startswith('--rsb-file'):
227                       os_ = o.split('=')
228                       if len(os_) != 2:
229                           raise error.general('invalid --rsb-file option: %s' % (' '.join(args)))
230                       if os_[0] != '--rsb-file':
231                           raise error.general('invalid --rsb-file option: %s' % (' '.join(args)))
232                       file_override = os_[1]
233                opts = [o for o in opts if not o.startswith('--rsb-')]
234            url = self.config.expand(' '.join(url))
235            src = download.parse_url(url, '_sourcedir', self.config, self.opts, file_override)
[9a15c40]236            download.get_file(src['url'], src['local'], self.opts, self.config)
237            if 'symlink' in src:
[5fdd664]238                sname = name.replace('-', '_')
239                src['script'] = '%%{__ln_s} %s ${source_dir_%s}' % (src['symlink'], sname)
[9a15c40]240            elif 'compressed' in src:
241                #
242                # Zip files unpack as well so do not use tar.
243                #
244                src['script'] = '%s %s' % (src['compressed'], src['local'])
245                if src['compressed-type'] != 'zip':
246                    src['script'] += ' | %{__tar_extract} -'
247            else:
[83586f7]248                src['script'] = '%%{__tar_extract} %s' % (src['local'])
[9a15c40]249            srcs += [src]
250        return srcs
251
252    def source_setup(self, package, args):
253        log.output('source setup: %s: %s' % (package.name(), ' '.join(args)))
254        setup_name = args[1]
255        args = args[1:]
256        try:
[4a315d3]257            opts, args = getopt.getopt(args[1:], 'qDcn:ba')
[3a972f6]258        except getopt.GetoptError as ge:
[9a15c40]259            raise error.general('source setup error: %s' % str(ge))
[bf13d27]260        quiet = False
[7618a74c]261        unpack_before_chdir = True
[bf13d27]262        delete_before_unpack = True
263        create_dir = False
[9a15c40]264        deleted_dir = False
265        created_dir = False
266        changed_dir = False
267        opt_name = None
[bf13d27]268        for o in opts:
269            if o[0] == '-q':
270                quiet = True
271            elif o[0] == '-D':
272                delete_before_unpack = False
273            elif o[0] == '-c':
274                create_dir = True
275            elif o[0] == '-n':
[9a15c40]276                opt_name = o[1]
[bf13d27]277            elif o[0] == '-b':
278                unpack_before_chdir = True
279            elif o[0] == '-a':
280                unpack_before_chdir = False
[9a15c40]281        name = None
282        for source in self.source(setup_name):
283            if name is None:
284                if opt_name is None:
285                    if source:
286                        opt_name = source['name']
287                    else:
288                        raise error.general('setup source tag not found: %d' % (source_tag))
289                else:
290                    name = opt_name
291            self.script.append(self.config.expand('cd %{_builddir}'))
292            if not deleted_dir and  delete_before_unpack:
293                self.script.append(self.config.expand('%{__rm} -rf ' + name))
294                deleted_dir = True
295            if not created_dir and create_dir:
296                self.script.append(self.config.expand('%{__mkdir_p} ' + name))
297                created_dir = True
298            if not changed_dir and (not unpack_before_chdir or create_dir):
299                self.script.append(self.config.expand('cd ' + name))
300                changed_dir = True
301            self.script.append(self.config.expand(source['script']))
302        if not changed_dir and (unpack_before_chdir and not create_dir):
[bf13d27]303            self.script.append(self.config.expand('cd ' + name))
[9a15c40]304            changed_dir = True
[bf13d27]305        self.script.append(self.config.expand('%{__setup_post}'))
306
[9a15c40]307    def patch_setup(self, package, args):
308        name = args[1]
309        args = args[2:]
310        _map = 'patch-%s' % (name)
311        default_opts = ' '.join(args)
[d3629a9]312        patch_keys = [p for p in self.macros.map_keys(_map) if p != 'setup']
[9a15c40]313        patches = []
314        for p in patch_keys:
315            pm = self.macros.get(p, globals = False, maps = _map)
316            if pm is None:
317                raise error.internal('patch macro not found: %s in %s (%s)' % \
318                                         (p, name, _map))
319            opts = []
320            url = []
321            for pp in pm[2].split():
322                if len(url) == 0 and pp[0] == '-':
323                    opts += [pp]
324                else:
325                    url += [pp]
326            if len(url) == 0:
327                raise error.general('patch URL not found: %s' % (' '.join(args)))
[383f7e6]328            #
329            # Look for --rsb-file as an option we use as a local file name.
330            # This can be used if a URL has no reasonable file name the
331            # download URL parser can figure out.
332            #
333            file_override = None
334            if len(opts) > 0:
335                for o in opts:
336                    if o.startswith('--rsb-file'):
337                       os_ = o.split('=')
338                       if len(os_) != 2:
339                           raise error.general('invalid --rsb-file option: %s' % (' '.join(args)))
340                       if os_[0] != '--rsb-file':
341                           raise error.general('invalid --rsb-file option: %s' % (' '.join(args)))
342                       file_override = os_[1]
343                opts = [o for o in opts if not o.startswith('--rsb-')]
[9a15c40]344            if len(opts) == 0:
345                opts = default_opts
346            else:
347                opts = ' '.join(opts)
348            opts = self.config.expand(opts)
349            url = self.config.expand(' '.join(url))
350            #
351            # Parse the URL first in the source builder's patch directory.
352            #
[383f7e6]353            patch = download.parse_url(url, '_patchdir', self.config, self.opts, file_override)
[9a15c40]354            #
[383f7e6]355            # Download the patch
[9a15c40]356            #
357            download.get_file(patch['url'], patch['local'], self.opts, self.config)
358            if 'compressed' in patch:
359                patch['script'] = patch['compressed'] + ' ' +  patch['local']
360            else:
361                patch['script'] = '%{__cat} ' + patch['local']
362            patch['script'] += ' | %%{__patch} %s' % (opts)
363            self.script.append(self.config.expand(patch['script']))
364
[bf13d27]365    def run(self, command, shell_opts = '', cwd = None):
366        e = execute.capture_execution(log = log.default, dump = self.opts.quiet())
367        cmd = self.config.expand('%{___build_shell} -ex ' + shell_opts + ' ' + command)
[5142bec]368        log.output('run: ' + cmd)
[ab8319a]369        exit_code, proc, output = e.shell(cmd, cwd = path.host(cwd))
[bf13d27]370        if exit_code != 0:
[74da24c]371            log.output('shell cmd failed: %s' % (cmd))
372            raise error.general('building %s' % (self.macros['buildname']))
[bf13d27]373
374    def builddir(self):
375        builddir = self.config.abspath('_builddir')
376        self.rmdir(builddir)
377        if not self.opts.dry_run():
378            self.mkdir(builddir)
379
380    def prep(self, package):
381        self.script.append('echo "==> %prep:"')
382        _prep = package.prep()
[ebf8a1f]383        if _prep:
384            for l in _prep:
385                args = l.split()
[9a15c40]386                if len(args):
[a083b52]387                    def err(msg):
388                        raise error.general('%s: %s' % (package, msg))
[9a15c40]389                    if args[0] == '%setup':
390                        if len(args) == 1:
[6444d58]391                            raise error.general('invalid %%setup directive: %s' % \
392                                                (' '.join(args)))
[9a15c40]393                        if args[1] == 'source':
394                            self.source_setup(package, args[1:])
395                        elif args[1] == 'patch':
396                            self.patch_setup(package, args[1:])
[a083b52]397                    elif args[0] in ['%patch', '%source']:
398                        sources.process(args[0][1:], args[1:], self.macros, err)
399                    elif args[0] == '%hash':
400                        sources.hash(args[1:], self.macros, err)
401                        self.hash(package, args)
[9a15c40]402                    else:
403                        self.script.append(' '.join(args))
[bf13d27]404
405    def build(self, package):
[4f26bdb]406        self.script.append('echo "==> clean %{buildroot}: ${SB_BUILD_ROOT}"')
407        self.script.append('%s ${SB_BUILD_ROOT}' %
408                           (self.config.expand('%{__rmdir}')))
409        self.script.append('%s ${SB_BUILD_ROOT}' %
410                           (self.config.expand('%{__mkdir_p}')))
[bf13d27]411        self.script.append('echo "==> %build:"')
412        _build = package.build()
[ebf8a1f]413        if _build:
414            for l in _build:
415                self.script.append(l)
[bf13d27]416
417    def install(self, package):
418        self.script.append('echo "==> %install:"')
419        _install = package.install()
[ebf8a1f]420        if _install:
421            for l in _install:
422                args = l.split()
423                self.script.append(' '.join(args))
[bf13d27]424
425    def files(self, package):
[ebf8a1f]426        if self.create_tar_files \
427           and not self.macros.get('%{_disable_packaging'):
[cafbcc6]428            self.script.append('echo "==> %files:"')
[0add2ea]429            inpath = path.abspath(self.config.expand('%{buildroot}'))
[cafbcc6]430            tardir = path.abspath(self.config.expand('%{_tardir}'))
431            self.script.append(self.config.expand('if test -d %s; then' % (inpath)))
[4f26bdb]432            self.script.append(self.config.expand('  %%{__mkdir_p} %s' % tardir))
[cafbcc6]433            self.script.append(self.config.expand('  cd ' + inpath))
434            tar = path.join(tardir, package.long_name() + '.tar.bz2')
435            cmd = self.config.expand('  %{__tar} -cf - . ' + '| %{__bzip2} > ' + tar)
436            self.script.append(cmd)
437            self.script.append(self.config.expand('  cd %{_builddir}'))
438            self.script.append('fi')
[bf13d27]439
440    def clean(self, package):
441        self.script.append('echo "==> %clean:"')
442        _clean = package.clean()
443        if _clean is not None:
444            for l in _clean:
445                args = l.split()
446                self.script.append(' '.join(args))
447
[4f26bdb]448    def build_package(self, package):
449        if self.canadian_cross():
[6444d58]450            if not self.config.defined('%{allow_cxc}'):
451                raise error.general('Canadian Cross is not allowed')
[4f26bdb]452            self.script.append('echo "==> Candian-cross build/target:"')
453            self.script.append('SB_CXC="yes"')
454        else:
455            self.script.append('SB_CXC="no"')
456        self.build(package)
457        self.install(package)
458        self.files(package)
459        if not self.opts.no_clean():
460            self.clean(package)
461
[bf13d27]462    def cleanup(self):
[ebf8a1f]463        package = self.main_package()
464        if not package.disabled() and not self.opts.no_clean():
[bf13d27]465            buildroot = self.config.abspath('buildroot')
466            builddir = self.config.abspath('_builddir')
[4f26bdb]467            buildcxcdir = self.config.abspath('_buildcxcdir')
[8d7624e]468            tmproot = self.config.abspath('_tmproot')
[5142bec]469            log.trace('cleanup: %s' % (buildroot))
[bf13d27]470            self.rmdir(buildroot)
[5142bec]471            log.trace('cleanup: %s' % (builddir))
[bf13d27]472            self.rmdir(builddir)
[4f26bdb]473            if self.canadian_cross():
[5142bec]474                log.trace('cleanup: %s' % (buildcxcdir))
[4f26bdb]475                self.rmdir(buildcxcdir)
[5142bec]476            log.trace('cleanup: %s' % (tmproot))
[8d7624e]477            self.rmdir(tmproot)
[bf13d27]478
[54d76bb]479    def main_package(self):
[bf13d27]480        packages = self.config.packages()
[54d76bb]481        return packages['main']
482
[6444d58]483    def reload(self):
484        self.config.load(self.init_name)
485
[54d76bb]486    def make(self):
487        package = self.main_package()
[ebf8a1f]488        if package.disabled():
489            log.notice('package: nothing to build')
[4f26bdb]490        else:
[65d9457]491            try:
492                name = package.name()
[ebf8a1f]493                if self.canadian_cross():
[6444d58]494                    cxc_label = '(Cxc) '
[ebf8a1f]495                else:
[6444d58]496                    cxc_label = ''
497                log.notice('package: %s%s' % (cxc_label, name))
498                log.trace('---- macro maps %s' % ('-' * 55))
499                log.trace('%s' % (str(self.config.macros)))
500                log.trace('-' * 70)
[65d9457]501                self.script.reset()
502                self.script.append(self.config.expand('%{___build_template}'))
503                self.script.append('echo "=> ' + name + ':"')
504                self.prep(package)
505                self.build_package(package)
506                if not self.opts.dry_run():
507                    self.builddir()
508                    sn = path.join(self.config.expand('%{_builddir}'), 'doit')
509                    log.output('write script: ' + sn)
510                    self.script.write(sn)
[6444d58]511                    log.notice('building: %s%s' % (cxc_label, name))
[65d9457]512                    self.run(sn)
[3a972f6]513            except error.general as gerr:
[65d9457]514                log.notice(str(gerr))
515                log.stderr('Build FAILED')
516                self._generate_report_('Build: %s' % (gerr))
517                raise
[3a972f6]518            except error.internal as ierr:
[65d9457]519                log.notice(str(ierr))
520                log.stderr('Internal Build FAILED')
521                self._generate_report_('Build: %s' % (ierr))
522                raise
523            except:
524                raise
525            if self.opts.dry_run():
[53290f9]526                self._generate_report_('Build: dry run (no actual error)',
527                                       'Build: dry run (no actual error)')
[bf13d27]528
529    def name(self):
530        packages = self.config.packages()
531        package = packages['main']
532        return package.name()
533
[ebf8a1f]534    def disabled(self):
535        packages = self.config.packages()
536        package = packages['main']
537        return package.disabled()
538
[cb12e48]539def get_configs(opts):
[e908afb]540
541    def _scan(_path, ext):
542        configs = []
543        for root, dirs, files in os.walk(_path):
544            prefix = root[len(_path) + 1:]
545            for file in files:
[fba1136]546                for e in ext:
547                    if file.endswith(e):
548                        configs += [path.join(prefix, file)]
[e908afb]549        return configs
550
[fba1136]551    configs = { 'paths': [], 'files': [] }
[cb12e48]552    for cp in opts.defaults.expand('%{_configdir}').split(':'):
[bc71066]553        hcp = path.host(path.abspath(cp))
554        configs['paths'] += [hcp]
555        configs['files'] += _scan(hcp, ['.cfg', '.bset'])
[4900498]556    configs['files'] = sorted(set(configs['files']))
[fba1136]557    return configs
[71b8893]558
[0759d98]559def find_config(config, configs):
560    config_root, config_ext = path.splitext(config)
561    if config_ext not in ['', '.bset', '.cfg']:
562        config_root = config
563        config_ext = ''
564    for c in configs['files']:
565        r, e = path.splitext(c)
566        if config_root == r:
567            if config_ext == '' or config_ext == e:
568                return c
569    return None
570
[bf13d27]571def run(args):
[74da24c]572    ec = 0
[bf13d27]573    try:
[71b8893]574        optargs = { '--list-configs': 'List available configurations' }
[cb12e48]575        opts = options.load(args, optargs)
[47d703fd]576        log.notice('RTEMS Source Builder, Package Builder, %s' % (version.str()))
[610ae57]577        opts.log_info()
[cb12e48]578        if not check.host_setup(opts):
[71b8893]579            if not opts.force():
[0add2ea]580                raise error.general('host build environment is not set up' +
[4f26bdb]581                                    ' correctly (use --force to proceed)')
[5142bec]582            log.notice('warning: forcing build with known host setup problems')
[71b8893]583        if opts.get_arg('--list-configs'):
[cb12e48]584            configs = get_configs(opts)
[cafbcc6]585            for p in configs['paths']:
[3a972f6]586                print('Examining: %s' % (os.path.relpath(p)))
[cafbcc6]587                for c in configs['files']:
588                    if c.endswith('.cfg'):
[3a972f6]589                        print('    %s' % (c))
[71b8893]590        else:
591            for config_file in opts.config_files():
[cb12e48]592                b = build(config_file, True, opts)
[71b8893]593                b.make()
[74da24c]594                b = None
[3a972f6]595    except error.general as gerr:
[74da24c]596        log.stderr('Build FAILED')
597        ec = 1
[3a972f6]598    except error.internal as ierr:
[74da24c]599        log.stderr('Internal Build FAILED')
600        ec = 1
[3a972f6]601    except error.exit as eerr:
[bf13d27]602        pass
603    except KeyboardInterrupt:
[5142bec]604        log.notice('abort: user terminated')
[74da24c]605        ec = 1
606    sys.exit(ec)
[bf13d27]607
608if __name__ == "__main__":
609    run(sys.argv)
Note: See TracBrowser for help on using the repository browser.