source: rtems-source-builder/source-builder/sb/options.py @ 0e22c3c

4.11
Last change on this file since 0e22c3c was 0e22c3c, checked in by Chris Johns <chrisj@…>, on 03/17/16 at 05:39:57

sb: Support --dry-run --with-download for 3rd party RTEMS BSP packages.

The building of 3rd party packages for an RTEMS BSP requires a valid
BSP so the standard method to download the source for releasing does
not work. This change adds support to allow this. The RTEMS BSP support
will not generate an error is no BSP or tools are provided or found.

The change addis logic operators to the %if statement so you can '
'

to 'or' and '&&' to 'and' logic expressions.

A new %log directive has been added to clean up the messages.

A new %{!define ...} has been added to aid checking within logic
expressions.

All command line --with/--without now appear as macros.

Add version.version to get just the RTEMS major and minor version.

Some pkg-config issues have been resolved.

Closes #2655.

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