source: rtems-source-builder/source-builder/sb/options.py @ 7f49a01

4.11
Last change on this file since 7f49a01 was 7f49a01, checked in by Chris Johns <chrisj@…>, on 01/18/18 at 03:41:45

sb: Back port options module from master.

Update #3274

  • Property mode set to 100644
File size: 26.3 KB
Line 
1#
2# RTEMS Tools Project (http://www.rtems.org/)
3# Copyright 2010-2016 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# Determine the defaults and load the specific file.
22#
23
24from __future__ import print_function
25
26import datetime
27import glob
28import pprint
29import re
30import os
31import string
32
33import error
34import execute
35import git
36import log
37import macros
38import path
39import sys
40
41import version
42
43basepath = 'sb'
44
45#
46# Save the host and POSIX state.
47#
48host_windows = False
49host_posix = True
50
51class command_line:
52    """Process the command line in a common way for all Tool Builder commands."""
53
54    def __init__(self, argv, optargs, _defaults, command_path):
55        self._long_opts = {
56            # key                 macro                handler            param  defs   init
57            '--prefix'               : ('_prefix',           self._lo_path,     True,  None,  False),
58            '--topdir'               : ('_topdir',           self._lo_path,     True,  None,  False),
59            '--configdir'            : ('_configdir',        self._lo_path,     True,  None,  False),
60            '--builddir'             : ('_builddir',         self._lo_path,     True,  None,  False),
61            '--sourcedir'            : ('_sourcedir',        self._lo_path,     True,  None,  False),
62            '--tmppath'              : ('_tmppath',          self._lo_path,     True,  None,  False),
63            '--jobs'                 : ('_jobs',             self._lo_jobs,     True,  'max', True),
64            '--log'                  : ('_logfile',          self._lo_string,   True,  None,  False),
65            '--url'                  : ('_url_base',         self._lo_string,   True,  None,  False),
66            '--no-download'          : ('_disable_download', self._lo_bool,     False, '0',   True),
67            '--macros'               : ('_macros',           self._lo_string,   True,  None,  False),
68            '--source-only-download' : ('_source_download',  self._lo_bool,     False, '0',   True),
69            '--targetcflags'         : ('_targetcflags',     self._lo_string,   True,  None,  False),
70            '--targetcxxflags'       : ('_targetcxxflags',   self._lo_string,   True,  None,  False),
71            '--libstdcxxflags'       : ('_libstdcxxflags',   self._lo_string,   True,  None,  False),
72            '--force'                : ('_force',            self._lo_bool,     False, '0',   True),
73            '--quiet'                : ('_quiet',            self._lo_bool,     False, '0',   True),
74            '--trace'                : ('_trace',            self._lo_bool,     False, '0',   True),
75            '--dry-run'              : ('_dry_run',          self._lo_bool,     False, '0',   True),
76            '--warn-all'             : ('_warn_all',         self._lo_bool,     False, '0',   True),
77            '--no-clean'             : ('_no_clean',         self._lo_bool,     False, '0',   True),
78            '--keep-going'           : ('_keep_going',       self._lo_bool,     False, '0',   True),
79            '--always-clean'         : ('_always_clean',     self._lo_bool,     False, '0',   True),
80            '--no-install'           : ('_no_install',       self._lo_bool,     False, '0',   True),
81            '--regression'           : ('_regression',       self._lo_bool,     False, '0',   True),
82            '--host'                 : ('_host',             self._lo_triplets, True,  None,  False),
83            '--build'                : ('_build',            self._lo_triplets, True,  None,  False),
84            '--target'               : ('_target',           self._lo_triplets, True,  None,  False),
85            '--help'                 : (None,                self._lo_help,     False, None,  False)
86            }
87
88        self.command_path = command_path
89        self.command_name = path.basename(argv[0])
90        self.argv = argv
91        self.args = argv[1:]
92        self.optargs = optargs
93        self.defaults = _defaults
94        self.opts = { 'params' : [] }
95        for lo in self._long_opts:
96            self.opts[lo[2:]] = self._long_opts[lo][3]
97            if self._long_opts[lo][4]:
98                self.defaults[self._long_opts[lo][0]] = ('none', 'none', self._long_opts[lo][3])
99
100    def __str__(self):
101        def _dict(dd):
102            s = ''
103            ddl = list(dd.keys())
104            ddl.sort()
105            for d in ddl:
106                s += '  ' + d + ': ' + str(dd[d]) + '\n'
107            return s
108
109        s = 'command: ' + self.command() + \
110            '\nargs: ' + str(self.args) + \
111            '\nopts:\n' + _dict(self.opts)
112
113        return s
114
115    def _lo_string(self, opt, macro, value):
116        if value is None:
117            raise error.general('option requires a value: %s' % (opt))
118        self.opts[opt[2:]] = value
119        self.defaults[macro] = value
120
121    def _lo_path(self, opt, macro, value):
122        if value is None:
123            raise error.general('option requires a path: %s' % (opt))
124        value = path.abspath(value)
125        self.opts[opt[2:]] = value
126        self.defaults[macro] = value
127
128    def _lo_jobs(self, opt, macro, value):
129        if value is None:
130            raise error.general('option requires a value: %s' % (opt))
131        ok = False
132        if value in ['max', 'none', 'half']:
133            ok = True
134        else:
135            try:
136                i = int(value)
137                ok = True
138            except:
139                pass
140            if not ok:
141                try:
142                    f = float(value)
143                    ok = True
144                except:
145                    pass
146        if not ok:
147            raise error.general('invalid jobs option: %s' % (value))
148        self.defaults[macro] = value
149        self.opts[opt[2:]] = value
150
151    def _lo_bool(self, opt, macro, value):
152        if value is not None:
153            raise error.general('option does not take a value: %s' % (opt))
154        self.opts[opt[2:]] = '1'
155        self.defaults[macro] = '1'
156
157    def _lo_triplets(self, opt, macro, value):
158        #
159        # This is a target triplet. Run it past config.sub to make make sure it
160        # is ok.  The target triplet is 'cpu-vendor-os'.
161        #
162        e = execute.capture_execution()
163        config_sub = path.join(self.command_path,
164                               basepath, 'config.sub')
165        exit_code, proc, output = e.shell(config_sub + ' ' + value)
166        if exit_code == 0:
167            value = output
168        self.defaults[macro] = ('triplet', 'none', value)
169        self.opts[opt[2:]] = value
170        _cpu = macro + '_cpu'
171        _arch = macro + '_arch'
172        _vendor = macro + '_vendor'
173        _os = macro + '_os'
174        _arch_value = ''
175        _vendor_value = ''
176        _os_value = ''
177        dash = value.find('-')
178        if dash >= 0:
179            _arch_value = value[:dash]
180            value = value[dash + 1:]
181        dash = value.find('-')
182        if dash >= 0:
183            _vendor_value = value[:dash]
184            value = value[dash + 1:]
185        if len(value):
186            _os_value = value
187        self.defaults[_cpu]    = _arch_value
188        self.defaults[_arch]   = _arch_value
189        self.defaults[_vendor] = _vendor_value
190        self.defaults[_os]     = _os_value
191
192    def _lo_help(self, opt, macro, value):
193        self.help()
194
195    def help(self):
196        print('%s: [options] [args]' % (self.command_name))
197        print('RTEMS Source Builder, an RTEMS Tools Project (c) 2012-2015 Chris Johns')
198        print('Options and arguments:')
199        print('--force                : Force the build to proceed')
200        print('--quiet                : Quiet output (not used)')
201        print('--trace                : Trace the execution')
202        print('--dry-run              : Do everything but actually run the build')
203        print('--warn-all             : Generate warnings')
204        print('--no-clean             : Do not clean up the build tree')
205        print('--always-clean         : Always clean the build tree, even with an error')
206        print('--keep-going           : Do not stop on an error.')
207        print('--regression           : Set --no-install, --keep-going and --always-clean')
208        print('--jobs                 : Run with specified number of jobs, default: num CPUs.')
209        print('--host                 : Set the host triplet')
210        print('--build                : Set the build triplet')
211        print('--target               : Set the target triplet')
212        print('--prefix path          : Tools build prefix, ie where they are installed')
213        print('--topdir path          : Top of the build tree, default is $PWD')
214        print('--configdir path       : Path to the configuration directory, default: ./config')
215        print('--builddir path        : Path to the build directory, default: ./build')
216        print('--sourcedir path       : Path to the source directory, default: ./source')
217        print('--tmppath path         : Path to the temp directory, default: ./tmp')
218        print('--macros file[,[file]  : Macro format files to load after the defaults')
219        print('--log file             : Log file where all build out is written too')
220        print('--url url[,url]        : URL to look for source')
221        print('--no-download          : Disable the source downloader')
222        print('--no-install           : Do not install the packages to the prefix')
223        print('--targetcflags flags   : List of C flags for the target code')
224        print('--targetcxxflags flags : List of C++ flags for the target code')
225        print('--libstdcxxflags flags : List of C++ flags to build the target libstdc++ code')
226        print('--source-only-download : Only download the source')
227        print('--with-<label>         : Add the --with-<label> to the build')
228        print('--without-<label>      : Add the --without-<label> to the build')
229        print('--rtems-tools path     : Path to an install RTEMS tool set')
230        print('--rtems-bsp arc/bsp    : Standard RTEMS architecure and BSP specifier')
231        print('--rtems-version ver    : The RTEMS major/minor version string')
232        if self.optargs:
233            for a in self.optargs:
234                print('%-22s : %s' % (a, self.optargs[a]))
235        raise error.exit()
236
237    def process(self):
238        arg = 0
239        while arg < len(self.args):
240            a = self.args[arg]
241            if a == '-?':
242                self.help()
243            elif a.startswith('--'):
244                los = a.split('=')
245                lo = los[0]
246                if lo in self._long_opts:
247                    long_opt = self._long_opts[lo]
248                    if len(los) == 1:
249                        if long_opt[2]:
250                            if arg == len(self.args) - 1:
251                                raise error.general('option requires a parameter: %s' % (lo))
252                            arg += 1
253                            value = self.args[arg]
254                        else:
255                            value = None
256                    else:
257                        value = '='.join(los[1:])
258                    long_opt[1](lo, long_opt[0], value)
259                else:
260                    if a.startswith('--with'):
261                        if len(los) != 1:
262                            value = los[1]
263                        else:
264                            value = '1'
265                        self.defaults[los[0][2:].replace('-', '_').lower()] = ('none', 'none', value)
266            else:
267                self.opts['params'].append(a)
268            arg += 1
269
270    def pre_process(self):
271        arg = 0
272        while arg < len(self.args):
273            a = self.args[arg]
274            if a == '--source-only-download':
275                self.args += ['--dry-run',
276                              '--quiet',
277                              '--without-log',
278                              '--without-error-report',
279                              '--without-release-url']
280            arg += 1
281
282    def post_process(self, logfile = True):
283        # Handle the log first.
284        logctrl = self.parse_args('--without-log')
285        if logctrl is None:
286            if logfile:
287                logfiles = self.logfiles()
288            else:
289                logfiles = None
290            log.default = log.log(streams = logfiles)
291        if self.trace():
292            log.tracing = True
293        if self.quiet():
294            log.quiet = True
295        # Must have a host
296        if self.defaults['_host'] == self.defaults['nil']:
297            raise error.general('--host not set')
298        # Must have a host
299        if self.defaults['_build'] == self.defaults['nil']:
300            raise error.general('--build not set')
301        # Manage the regression option
302        if self.opts['regression'] != '0':
303            self.opts['no-install'] = '1'
304            self.defaults['_no_install'] = '1'
305            self.opts['keep-going'] = '1'
306            self.defaults['_keep_going'] = '1'
307            self.opts['always-clean'] = '1'
308            self.defaults['_always_clean'] = '1'
309        # Handle the jobs for make
310        if '_ncpus' not in self.defaults:
311            raise error.general('host number of CPUs not set')
312        ncpus = self.jobs(self.defaults['_ncpus'])
313        if ncpus > 1:
314            self.defaults['_smp_mflags'] = '-j %d' % (ncpus)
315        else:
316            self.defaults['_smp_mflags'] = self.defaults['nil']
317        # Load user macro files
318        um = self.user_macros()
319        if um:
320            checked = path.exists(um)
321            if False in checked:
322                raise error.general('macro file not found: %s' % (um[checked.index(False)]))
323            for m in um:
324                self.defaults.load(m)
325        # Check if the user has a private set of macros to load
326        if 'RSB_MACROS' in os.environ:
327            if path.exists(os.environ['RSB_MACROS']):
328                self.defaults.load(os.environ['RSB_MACROS'])
329        if 'HOME' in os.environ:
330            rsb_macros = path.join(os.environ['HOME'], '.rsb_macros')
331            if path.exists(rsb_macros):
332                self.defaults.load(rsb_macros)
333
334    def sb_released(self):
335        if version.released():
336            self.defaults['rsb_released'] = '1'
337        self.defaults['rsb_version'] = version.str()
338
339    def sb_git(self):
340        repo = git.repo(self.defaults.expand('%{_sbdir}'), self)
341        if repo.valid():
342            repo_valid = '1'
343            repo_head = repo.head()
344            repo_clean = not repo.dirty()
345            repo_remotes = '%{nil}'
346            remotes = repo.remotes()
347            if 'origin' in remotes:
348                repo_remotes = '%s/origin' % (remotes['origin']['url'])
349            repo_id = repo_head
350            if not repo_clean:
351                repo_id += '-modified'
352            repo_mail = repo.email()
353        else:
354            repo_valid = '0'
355            repo_head = '%{nil}'
356            repo_clean = '%{nil}'
357            repo_remotes = '%{nil}'
358            repo_id = 'no-repo'
359            repo_mail = None
360        self.defaults['_sbgit_valid'] = repo_valid
361        self.defaults['_sbgit_head']  = repo_head
362        self.defaults['_sbgit_clean'] = str(repo_clean)
363        self.defaults['_sbgit_remotes'] = str(repo_remotes)
364        self.defaults['_sbgit_id']    = repo_id
365        if repo_mail is not None:
366            self.defaults['_sbgit_mail'] = repo_mail
367
368    def command(self):
369        return path.join(self.command_path, self.command_name)
370
371    def force(self):
372        return self.opts['force'] != '0'
373
374    def dry_run(self):
375        return self.opts['dry-run'] != '0'
376
377    def set_dry_run(self):
378        self.opts['dry-run'] = '1'
379
380    def quiet(self):
381        return self.opts['quiet'] != '0'
382
383    def trace(self):
384        return self.opts['trace'] != '0'
385
386    def warn_all(self):
387        return self.opts['warn-all'] != '0'
388
389    def keep_going(self):
390        return self.opts['keep-going'] != '0'
391
392    def no_clean(self):
393        return self.opts['no-clean'] != '0'
394
395    def always_clean(self):
396        return self.opts['always-clean'] != '0'
397
398    def no_install(self):
399        return self.opts['no-install'] != '0'
400
401    def canadian_cross(self):
402        _host = self.defaults.expand('%{_host}')
403        _build = self.defaults.expand('%{_build}')
404        _target = self.defaults.expand('%{_target}')
405        if len(_target):
406            return len(_host) and len(_build) and (_target) and \
407                _host != _build and _host != _target
408        return len(_host) and len(_build) and _host != _build
409
410    def user_macros(self):
411        #
412        # Return something even if it does not exist.
413        #
414        if self.opts['macros'] is None:
415            return None
416        um = []
417        configs = self.defaults.expand('%{_configdir}').split(':')
418        for m in self.opts['macros'].split(','):
419            if path.exists(m):
420                um += [m]
421            else:
422                # Get the expanded config macros then check them.
423                cm = path.expand(m, configs)
424                ccm = path.exists(cm)
425                if True in ccm:
426                    # Pick the first found
427                    um += [cm[ccm.index(True)]]
428                else:
429                    um += [m]
430        return um if len(um) else None
431
432    def jobs(self, cpus):
433        cpus = int(cpus)
434        if self.opts['jobs'] == 'none':
435            cpus = 0
436        elif self.opts['jobs'] == 'max':
437            pass
438        elif self.opts['jobs'] == 'half':
439            cpus = cpus / 2
440        else:
441            ok = False
442            try:
443                i = int(self.opts['jobs'])
444                cpus = i
445                ok = True
446            except:
447                pass
448            if not ok:
449                try:
450                    f = float(self.opts['jobs'])
451                    cpus = f * cpus
452                    ok = True
453                except:
454                    pass
455                if not ok:
456                    raise error.internal('bad jobs option: %s' % (self.opts['jobs']))
457        if cpus <= 0:
458            cpu = 1
459        return cpus
460
461    def params(self):
462        return self.opts['params']
463
464    def parse_args(self, arg, error = True, extra = True):
465        for a in range(0, len(self.args)):
466            if self.args[a].startswith(arg):
467                lhs = None
468                rhs = None
469                if '=' in self.args[a]:
470                    eqs = self.args[a].split('=')
471                    lhs = eqs[0]
472                    if len(eqs) > 2:
473                        rhs = '='.join(eqs[1:])
474                    else:
475                        rhs = eqs[1]
476                elif extra:
477                    lhs = self.args[a]
478                    a += 1
479                    if a < len(self.args):
480                        rhs = self.args[a]
481                return [lhs, rhs]
482            a += 1
483        return None
484
485    def get_arg(self, arg):
486        if self.optargs is None or arg not in self.optargs:
487            return None
488        return self.parse_args(arg)
489
490    def with_arg(self, label, default = 'not-found'):
491        # the default if there is no option for without.
492        result = default
493        for pre in ['with', 'without']:
494            arg_str = '--%s-%s' % (pre, label)
495            arg_label = '%s_%s' % (pre, label)
496            arg = self.parse_args(arg_str, error = False, extra = False)
497            if arg is not None:
498                if arg[1] is  None:
499                    result = 'yes'
500                else:
501                    result = arg[1]
502                break
503        return [arg_label, result]
504
505    def get_config_files(self, config):
506        #
507        # Convert to shell paths and return shell paths.
508        #
509        # @fixme should this use a passed in set of defaults and not
510        #        not the initial set of values ?
511        #
512        config = path.shell(config)
513        if '*' in config or '?' in config:
514            print(config)
515            configdir = path.dirname(config)
516            configbase = path.basename(config)
517            if len(configbase) == 0:
518                configbase = '*'
519            if not configbase.endswith('.cfg'):
520                configbase = configbase + '.cfg'
521            if len(configdir) == 0:
522                configdir = self.macros.expand(self.defaults['_configdir'])
523            configs = []
524            for cp in configdir.split(':'):
525                hostconfigdir = path.host(cp)
526                for f in glob.glob(os.path.join(hostconfigdir, configbase)):
527                    configs += path.shell(f)
528        else:
529            configs = [config]
530        return configs
531
532    def config_files(self):
533        configs = []
534        for config in self.opts['params']:
535            configs.extend(self.get_config_files(config))
536        return configs
537
538    def logfiles(self):
539        if 'log' in self.opts and self.opts['log'] is not None:
540            return self.opts['log'].split(',')
541        return ['rsb-log-%s.txt' % (datetime.datetime.now().strftime('%Y%m%d-%H%M%S'))]
542
543    def urls(self):
544        if self.opts['url'] is not None:
545            return self.opts['url'].split(',')
546        return None
547
548    def download_disabled(self):
549        return self.opts['no-download'] != '0'
550
551    def disable_install(self):
552        self.opts['no-install'] = '1'
553
554    def info(self):
555        s = ' Command Line: %s%s' % (' '.join(self.argv), os.linesep)
556        s += ' Python: %s' % (sys.version.replace('\n', ''))
557        return s
558
559    def log_info(self):
560        log.output(self.info())
561
562    def rtems_options(self):
563        # Check for RTEMS specific helper options.
564        rtems_tools = self.parse_args('--rtems-tools')
565        if rtems_tools is not None:
566            if self.get_arg('--with-tools') is not None:
567                raise error.general('--rtems-tools and --with-tools cannot be used together')
568            self.args.append('--with-tools=%s' % (rtems_tools[1]))
569        rtems_arch_bsp = self.parse_args('--rtems-bsp')
570        if rtems_arch_bsp is not None:
571            if self.get_arg('--target') is not None:
572                raise error.general('--rtems-bsp and --target cannot be used together')
573            ab = rtems_arch_bsp[1].split('/')
574            if len(ab) != 2:
575                raise error.general('invalid --rtems-bsp option')
576            rtems_version = self.parse_args('--rtems-version')
577            if rtems_version is None:
578                rtems_version = version.version()
579            else:
580                rtems_version = rtems_version[1]
581            self.args.append('--target=%s-rtems%s' % (ab[0], rtems_version))
582            self.args.append('--with-rtems-bsp=%s' % (ab[1]))
583
584def load(args, optargs = None, defaults = '%{_sbdir}/defaults.mc', logfile = True):
585    """
586    Copy the defaults, get the host specific values and merge them overriding
587    any matching defaults, then create an options object to handle the command
588    line merging in any command line overrides. Finally post process the
589    command line.
590    """
591
592    global host_windows
593    global host_posix
594
595    #
596    # The path to this command.
597    #
598    command_path = path.dirname(args[0])
599    if len(command_path) == 0:
600        command_path = '.'
601
602    #
603    # The command line contains the base defaults object all build objects copy
604    # and modify by loading a configuration.
605    #
606    o = command_line(args,
607                     optargs,
608                     macros.macros(name = defaults,
609                                   sbdir = command_path),
610                     command_path)
611
612    overrides = None
613    if os.name == 'nt':
614        try:
615            import windows
616            overrides = windows.load()
617            host_windows = True
618            host_posix = False
619        except:
620            raise error.general('failed to load Windows host support')
621    elif os.name == 'posix':
622        uname = os.uname()
623        try:
624            if uname[0].startswith('MINGW64_NT'):
625                import windows
626                overrides = windows.load()
627                host_windows = True
628            elif uname[0].startswith('CYGWIN_NT'):
629                import windows
630                overrides = windows.load()
631            elif uname[0] == 'Darwin':
632                import darwin
633                overrides = darwin.load()
634            elif uname[0] == 'FreeBSD':
635                import freebsd
636                overrides = freebsd.load()
637            elif uname[0] == 'NetBSD':
638                import netbsd
639                overrides = netbsd.load()
640            elif uname[0] == 'Linux':
641                import linux
642                overrides = linux.load()
643            elif uname[0] == 'SunOS':
644                import solaris
645                overrides = solaris.load()
646        except error.general as ge:
647            raise error.general('failed to load %s host support: %s' % (uname[0], ge))
648        except:
649            raise error.general('failed to load %s host support' % (uname[0]))
650    else:
651        raise error.general('unsupported host type; please add')
652    if overrides is None:
653        raise error.general('no hosts defaults found; please add')
654    for k in overrides:
655        o.defaults[k] = overrides[k]
656
657    o.sb_released()
658    o.sb_git()
659    o.rtems_options()
660    o.pre_process()
661    o.process()
662    o.post_process(logfile)
663
664    #
665    # Load the release settings
666    #
667    version.load_release_settings(o.defaults)
668
669    return o
670
671def run(args):
672    try:
673        _opts = load(args = args, defaults = 'defaults.mc')
674        log.notice('RTEMS Source Builder - Defaults, %s' % (version.str()))
675        _opts.log_info()
676        log.notice('Options:')
677        log.notice(str(_opts))
678        log.notice('Defaults:')
679        log.notice(str(_opts.defaults))
680        log.notice('with-opt1: %r' % (_opts.with_arg('opt1')))
681        log.notice('without-opt2: %r' % (_opts.with_arg('opt2')))
682    except error.general as gerr:
683        print(gerr)
684        sys.exit(1)
685    except error.internal as ierr:
686        print(ierr)
687        sys.exit(1)
688    except error.exit as eerr:
689        pass
690    except KeyboardInterrupt:
691        _notice(opts, 'abort: user terminated')
692        sys.exit(1)
693    sys.exit(0)
694
695if __name__ == '__main__':
696    run(sys.argv)
Note: See TracBrowser for help on using the repository browser.