[PATCH] sb: Back port changes to support mailing list posting.
Chris Johns
chrisj at rtems.org
Thu Feb 1 23:59:21 UTC 2018
Close #3287
---
rtems/config/4.10/rtems-autotools.bset | 5 +
source-builder/sb/macros.py | 3 +
source-builder/sb/reports.py | 283 +++++++++++++++++++++++----------
source-builder/sb/setbuilder.py | 138 ++++++++++++----
4 files changed, 307 insertions(+), 122 deletions(-)
diff --git a/rtems/config/4.10/rtems-autotools.bset b/rtems/config/4.10/rtems-autotools.bset
index f7f1929..a15aa1b 100644
--- a/rtems/config/4.10/rtems-autotools.bset
+++ b/rtems/config/4.10/rtems-autotools.bset
@@ -18,5 +18,10 @@
#
%define _internal_autotools_path %{_tmppath}/sb-%{_uid}/${SB_PREFIX_CLEAN}
+#
+# Disable emailing reports of this building for RTEMS.
+#
+%define mail_disable
+
4.10/rtems-autotools-internal
4.10/rtems-autotools-base
diff --git a/source-builder/sb/macros.py b/source-builder/sb/macros.py
index 2af8d36..28a52b2 100644
--- a/source-builder/sb/macros.py
+++ b/source-builder/sb/macros.py
@@ -447,6 +447,9 @@ class macros:
if key in self.macros[map]:
del self.macros[map][key]
+ def defined(self, key, globals = True, maps = None):
+ return self.get(key, globals, maps) is not None
+
def expand(self, _str):
"""Simple basic expander of config file macros."""
expanded = True
diff --git a/source-builder/sb/reports.py b/source-builder/sb/reports.py
index 5eb8bb8..9d3a342 100644
--- a/source-builder/sb/reports.py
+++ b/source-builder/sb/reports.py
@@ -24,6 +24,7 @@
from __future__ import print_function
+import base64
import copy
import datetime
import os
@@ -63,6 +64,18 @@ def _make_path(p, *args):
p = path.join(p, arg)
return os.path.abspath(path.host(p))
+def platform(mode = 'all'):
+ import platform
+ if mode == 'system':
+ return platform.system()
+ compact = platform.platform(aliased = True)
+ if mode == 'compact':
+ return compact
+ extended = ' '.join(platform.uname())
+ if mode == 'extended':
+ return extended
+ return '%s (%s)' % (short, extended)
+
class formatter(object):
def __init__(self):
self.content = ''
@@ -139,127 +152,212 @@ class formatter(object):
def post_process(self):
return self.content
-class asciidoc_formatter(formatter):
+class markdown_formatter(formatter):
def __init__(self):
- super(asciidoc_formatter, self).__init__()
+ super(markdown_formatter, self).__init__()
+ self.level_current = 1
+ self.level_path = '0.'
+ self.levels = { '0.': 0 }
+ self.cols = [20, 55]
+
+ def _heading(self, heading, level):
+ return '%s %s' % ('#' * level, heading)
+
+ def _strong(self, s):
+ return '__' + s + '__'
+
+ def _bold(self, s):
+ return '__' + s + '__'
+
+ def _italic(self, s):
+ return '_' + s + '_'
+
+ def _table_line(self):
+ l = '|'
+ for c in self.cols:
+ l += '-' * c + '|'
+ return l
+
+ def _table_row(self, cols):
+ if len(cols) != len(self.cols):
+ raise error.general('invalid table column count')
+ l = '|'
+ for c in range(0, len(cols)):
+ l += '%-*s|' % (self.cols[c], cols[c])
+ return l
+
+ def _btext(self, level, text):
+ return '> ' * (level - 1) + text
+
+ def _bline(self, level, text):
+ self.line(self._btext(level, text))
+
+ def _level(self, nest_level):
+ if nest_level > self.level_current:
+ self.level_path += '%d.' % (self.levels[self.level_path])
+ if nest_level < self.level_current:
+ self.level_path = self.level_path[:-2]
+ if self.level_path not in self.levels:
+ self.levels[self.level_path] = 0
+ self.level_current = nest_level
+ self.levels[self.level_path] += 1
+ return '%s%d.' % (self.level_path[2:], self.levels[self.level_path])
def format(self):
- return 'asciidoc'
+ return 'markdown'
def ext(self):
- return '.txt'
+ return '.md'
def introduction(self, name, now, intro_text):
- h = 'RTEMS Source Builder Report'
- self.line(h)
- self.line('=' * len(h))
- self.line(':doctype: book')
- self.line(':toc2:')
- self.line(':toclevels: 5')
- self.line(':icons:')
- self.line(':numbered:')
- self.line(':data-uri:')
+ self.line('- - -')
+ self.line(self._heading('RTEMS Source Builder Report', 1))
+ self.line(self._strong(_title))
self.line('')
- self.line(_title)
- self.line(now)
- self.line('')
- image = _make_path(self.sbpath, options.basepath, 'images', 'rtemswhitebg.jpg')
- self.line('image:%s["RTEMS",width="20%%"]' % (image))
+ self.line(self._bold('Generated: ' + now))
self.line('')
if intro_text:
self.line('%s' % ('\n'.join(intro_text)))
+ self.line('')
+ self.line('')
+ self.line('- - -')
+ self.line(self._heading('Table Of Contents', 2))
+ self.line('')
+ self.line('[TOC]')
+ self.line('')
def release_status(self, release_string):
self.line('')
- self.line("'''")
+ self.line(self._heading(_release_status_text, 2))
self.line('')
- self.line('.%s' % (_release_status_text))
self.line('*Version*: %s;;' % (release_string))
self.line('')
- self.line("'''")
- self.line('')
def git_status(self, valid, dirty, head, remotes):
self.line('')
- self.line("'''")
- self.line('')
- self.line('.%s' % (_git_status_text))
+ self.line('- - -')
+ self.line(self._heading(_git_status_text, 2))
if valid:
- self.line('*Remotes*:;;')
+ self.line(self._strong('Remotes:'))
+ self.line('')
+ rc = 1
for r in remotes:
if 'url' in remotes[r]:
text = remotes[r]['url']
else:
text = 'no URL found'
- text = '%s: %s' % (r, text)
- self.line('. %s' % (text))
- self.line('*Status*:;;')
+ self.line('%d. %s: %s' % (rc, r, text))
+ rc += 1
+ self.line('')
+ self.line(self._strong('Status:'))
+ self.line('')
if dirty:
- self.line('_Repository is dirty_')
+ self.line('> ' + self._italic('Repository is dirty'))
else:
- self.line('Clean')
- self.line('*Head*:;;')
- self.line('Commit: %s' % (head))
+ self.line('> Clean')
+ self.line('>')
+ self.line('> ' + self._bold('Head: ') + head)
else:
- self.line('_Not a valid GIT repository_')
- self.line('')
- self.line("'''")
+ self.line('> ' + self._italic('Not a valid GIT repository'))
self.line('')
def config(self, nest_level, name, _config):
- self.line('*Package*: _%s_ +' % (name))
- self.line('*Config*: %s' % (_config.file_name()))
- self.line('')
+ self._bline(nest_level, self._bold('Package:'))
+ self._bline(nest_level, '')
+ self._bline(nest_level + 1, self._table_row([self._bold('Item'),
+ self._bold('Description')]))
+ self._bline(nest_level + 1, self._table_line())
+ self._bline(nest_level + 1, self._table_row(['Package', name]))
+ self._bline(nest_level + 1, self._table_row(['Config',
+ _config.file_name()]))
def config_end(self, nest_level, name):
- self.line('')
- self.line("'''")
- self.line('')
+ self._bline(nest_level + 1, '')
def buildset_start(self, nest_level, name):
- h = '%s' % (name)
- self.line('=%s %s' % ('=' * int(nest_level), h))
+ if nest_level == 1:
+ self.line('- - -')
+ self._bline(nest_level,
+ self._heading('RTEMS Source Builder Packages', 2))
+ self._bline(nest_level,
+ self._heading('%s Build %s' % (self._level(nest_level), name), 3))
def info(self, nest_level, name, info, separated):
- end = ''
- if separated:
- self.line('*%s:*::' % (name))
- self.line('')
- else:
- self.line('*%s:* ' % (name))
- end = ' +'
- spaces = ''
- for l in info:
- self.line('%s%s%s' % (spaces, l, end))
- if separated:
- self.line('')
+ self._bline(nest_level + 1,
+ self._table_row([name, ' '.join(info)]))
def directive(self, nest_level, name, data):
- self.line('')
- self.line('*%s*:' % (name))
- self.line('--------------------------------------------')
+ self._bline(nest_level, '')
+ self._bline(nest_level, self._bold(name + ':'))
for l in data:
- self.line(l)
- self.line('--------------------------------------------')
+ self._bline(nest_level + 1, ' ' * 4 + l)
def files(self, nest_level, singular, plural, _files):
- self.line('')
- self.line('*' + plural + ':*::')
+ self._bline(nest_level, '')
+ self._bline(nest_level, self._bold(plural + ':'))
+ self._bline(nest_level, '')
if len(_files) == 0:
- self.line('No ' + plural.lower())
+ self._bline(nest_level + 1, 'No ' + plural.lower())
+ fc = 0
for name in _files:
for s in _files[name]:
- self.line('. %s' % (s[0]))
+ fc += 1
if s[1] is None:
- h = 'No checksum'
+ h = self._bold('No checksum')
else:
hash = s[1].split()
h = '%s: %s' % (hash[0], hash[1])
- self.line('+\n%s\n' % (h))
+ self._bline(nest_level,
+ '%d. [%s](%s "%s %s")<br/>' % (fc, s[0], s[0],
+ name, singular.lower()))
+ self._bline(nest_level,
+ ' <span class=checksum>%s</span>' % (h))
-class html_formatter(asciidoc_formatter):
+class html_formatter(markdown_formatter):
def __init__(self):
super(html_formatter, self).__init__()
+ self.html_header = '<!DOCTYPE html>' + os.linesep + \
+ '<html lang="en">' + os.linesep + \
+ '<head>' + os.linesep + \
+ '<title>RTEMS RSB - @BUILD@</title>' + os.linesep + \
+ '<meta http-equiv="content-type" content="text/html; charset=UTF-8" />' + os.linesep + \
+ '<meta name="created" content="@NOW@" />' + os.linesep + \
+ '<meta name="description" content="RTEMS RSB Report" />' + os.linesep + \
+ '<meta name="keywords" content="RTEMS RSB" />' + os.linesep + \
+ '<meta charset="utf-8">' + os.linesep + \
+ '<meta http-equiv="X-UA-Compatible" content="IE=edge">' + os.linesep + \
+ '<meta name="viewport" content="width=device-width, initial-scale=1">' + os.linesep + \
+ '<style type="text/css">' + os.linesep + \
+ 'body {' + os.linesep + \
+ ' font-family: arial, helvetica, serif;' + os.linesep + \
+ ' font-style: normal;' + os.linesep + \
+ ' font-weight: 400;' + os.linesep + \
+ '}' + os.linesep + \
+ 'h1, h2 { margin: 10px 5px 10px 5px; }' + os.linesep + \
+ 'h1 { font-size: 28px; }' + os.linesep + \
+ 'h2 { font-size: 22px;}' + os.linesep + \
+ 'h3 { font-size: 18px; }' + os.linesep + \
+ 'p, ol, blockquote, h3, table, pre { margin: 1px 20px 2px 7px; }' + os.linesep + \
+ 'table, th, td, pre { border: 1px solid gray; border-spacing: 0px; }' + os.linesep + \
+ 'table { width: 100%; }' + os.linesep + \
+ 'th, td { padding: 1px; }' + os.linesep + \
+ 'pre { padding: 4px; }' + os.linesep + \
+ '.checksum { font-size: 12px; }' + os.linesep + \
+ '</style>' + os.linesep + \
+ '</head>' + os.linesep + \
+ '<body>' + os.linesep
+ self.html_footer = '</body>' + os.linesep + \
+ '</html>' + os.linesep
+
+ def _logo(self):
+ logo = _make_path(self.sbpath, options.basepath, 'images', 'rtemswhitebg.jpg')
+ try:
+ with open(logo, "rb") as image:
+ b64 = base64.b64encode(image.read())
+ except:
+ raise error.general('installation error: no logo found')
+ logo = '<img alt="RTEMS Project" height="100" src="data:image/png;base64,' + b64 + '" />'
+ return logo
def format(self):
return 'html'
@@ -267,24 +365,30 @@ class html_formatter(asciidoc_formatter):
def ext(self):
return '.html'
+ def introduction(self, name, now, intro_text):
+ self.name = name
+ self.now = now
+ super(html_formatter, self).introduction(name, now, intro_text)
+
def post_process(self):
- import io
- infile = io.StringIO(self.content)
- outfile = io.StringIO()
try:
- import asciidocapi
+ import markdown
except:
- raise error.general('installation error: no asciidocapi found')
- asciidoc_py = _make_path(self.sbpath, options.basepath, 'asciidoc', 'asciidoc.py')
+ raise error.general('installation error: no markdown found')
try:
- asciidoc = asciidocapi.AsciiDocAPI(asciidoc_py)
+ out = markdown.markdown(self.content,
+ output_format = 'html5',
+ extensions = ['markdown.extensions.toc',
+ 'markdown.extensions.tables',
+ 'markdown.extensions.sane_lists',
+ 'markdown.extensions.smarty'])
except:
- raise error.general('application error: asciidocapi failed')
- asciidoc.execute(infile, outfile)
- out = outfile.getvalue()
- infile.close()
- outfile.close()
- return out
+ raise
+ raise error.general('application error: markdown failed')
+ header = self.html_header.replace('@BUILD@', self.name).replace('@NOW@', self.now)
+ footer = self.html_footer
+ logo = self._logo()
+ return header + logo + out + footer
class text_formatter(formatter):
def __init__(self):
@@ -503,8 +607,8 @@ class report:
if type(formatter) == str:
if formatter == 'text':
self.formatter = text_formatter()
- elif formatter == 'asciidoc':
- self.formatter = asciidoc_formatter()
+ elif formatter == 'markdown':
+ self.formatter = markdown_formatter()
elif formatter == 'html':
self.formatter = html_formatter()
elif formatter == 'ini':
@@ -736,6 +840,9 @@ class report:
self.generate_ini_source(sources)
self.generate_ini_hash(sources)
+ def get_output(self):
+ return self.formatter.post_process()
+
def write(self, name):
self.out = self.formatter.post_process()
if name is not None:
@@ -784,9 +891,9 @@ def run(args):
try:
optargs = { '--list-bsets': 'List available build sets',
'--list-configs': 'List available configurations',
- '--format': 'Output format (text, html, asciidoc, ini, xml)',
+ '--format': 'Output format (text, html, markdown, ini, xml)',
'--output': 'File name to output the report' }
- opts = options.load(args, optargs)
+ opts = options.load(args, optargs, logfile = False)
if opts.get_arg('--output') and len(opts.params()) > 1:
raise error.general('--output can only be used with a single config')
print('RTEMS Source Builder, Reporter, %s' % (version.str()))
@@ -805,8 +912,8 @@ def run(args):
raise error.general('invalid format option: %s' % ('='.join(format_opt)))
if format_opt[1] == 'text':
pass
- elif format_opt[1] == 'asciidoc':
- formatter = asciidoc_formatter()
+ elif format_opt[1] == 'markdown':
+ formatter = markdown_formatter()
elif format_opt[1] == 'html':
formatter = html_formatter()
elif format_opt[1] == 'ini':
@@ -824,7 +931,7 @@ def run(args):
outname = output
config = build.find_config(_config, configs)
if config is None:
- raise error.general('config file not found: %s' % (inname))
+ raise error.general('config file not found: %s' % (_config))
r.create(config, outname)
del r
else:
diff --git a/source-builder/sb/setbuilder.py b/source-builder/sb/setbuilder.py
index 9fa19ec..17b781a 100644
--- a/source-builder/sb/setbuilder.py
+++ b/source-builder/sb/setbuilder.py
@@ -30,6 +30,7 @@ import glob
import operator
import os
import sys
+import textwrap
try:
import build
@@ -49,6 +50,23 @@ except:
print('error: unknown application load error', file = sys.stderr)
sys.exit(1)
+class log_capture(object):
+ def __init__(self):
+ self.log = []
+ log.capture = self.capture
+
+ def __str__(self):
+ return os.linesep.join(self.log)
+
+ def capture(self, text):
+ self.log += [l for l in text.replace(chr(13), '').splitlines()]
+
+ def get(self):
+ return self.log
+
+ def clear(self):
+ self.log = []
+
class buildset:
"""Build a set builds a set of packages."""
@@ -71,30 +89,43 @@ class buildset:
self.bset_pkg = '%s-%s-set' % (pkg_prefix, self.bset)
self.mail_header = ''
self.mail_report = ''
+ self.mail_report_0subject = ''
self.build_failure = None
- def write_mail_header(self, text, prepend = False):
- if len(text) == 0 or text[-1] != '\n' or text[-1] != '\r':
+ def write_mail_header(self, text = '', prepend = False):
+ if type(text) is list:
+ text = os.linesep.join(text)
+ text = text.replace('\r', '').replace('\n', os.linesep)
+ if len(text) == 0 or text[-1] != os.linesep:
text += os.linesep
if prepend:
self.mail_header = text + self.mail_header
else:
self.mail_header += text
+ def get_mail_header(self):
+ return self.mail_header
+
def write_mail_report(self, text, prepend = False):
- if len(text) == 0 or text[-1] != '\n' or text[-1] != '\r':
+ if type(text) is list:
+ text = os.linesep.join(text)
+ text = text.replace('\r', '').replace('\n', os.linesep)
+ if len(text) == 0 or text[-1] != os.linesep:
text += os.linesep
if prepend:
self.mail_report = text + self.mail_report
else:
self.mail_report += text
+ def get_mail_report(self):
+ return self.mail_report
+
def copy(self, src, dst):
log.output('copy: %s => %s' % (path.host(src), path.host(dst)))
if not self.opts.dry_run():
path.copy_tree(src, dst)
- def report(self, _config, _build, opts, macros, format = None):
+ def report(self, _config, _build, opts, macros, format = None, mail = None):
if len(_build.main_package().name()) > 0 \
and not _build.macros.get('%{_disable_reporting}') \
and (not _build.opts.get_arg('--no-report') \
@@ -139,13 +170,13 @@ class buildset:
_build.mkdir(outpath)
r.write(outname)
del r
- if _build.opts.get_arg('--mail'):
+ if mail:
r = reports.report('text', self.configs,
copy.copy(opts), copy.copy(macros))
r.introduction(_build.config.file_name())
r.generate(_build.config.file_name())
r.epilogue(_build.config.file_name())
- self.write_mail_report(r.out)
+ self.write_mail_report(r.get_output())
del r
def root_copy(self, src, dst):
@@ -299,17 +330,20 @@ class buildset:
configs = self.parse(bset)
return configs
- def build(self, deps = None, nesting_count = 0):
+ def build(self, deps = None, nesting_count = 0, mail = None):
build_error = False
nesting_count += 1
+ if mail:
+ mail['output'].clear()
+
log.trace('_bset: %s: make' % (self.bset))
log.notice('Build Set: %s' % (self.bset))
- if self.opts.get_arg('--mail'):
- mail_report_subject = '%s %s' % (self.bset, self.macros.expand('%{_host}'))
+ mail_subject = '%s on %s' % (self.bset,
+ self.macros.expand('%{_host}'))
current_path = os.environ['PATH']
@@ -318,6 +352,9 @@ class buildset:
mail_report = False
have_errors = False
+ if mail:
+ mail['output'].clear()
+
try:
configs = self.load()
@@ -337,14 +374,17 @@ class buildset:
if configs[s].endswith('.bset'):
log.trace('_bset: == %2d %s' % (nesting_count + 1, '=' * 75))
bs = buildset(configs[s], self.configs, opts, macros)
- bs.build(deps, nesting_count)
+ bs.build(deps, nesting_count, mail)
del bs
elif configs[s].endswith('.cfg'):
- mail_report = self.opts.get_arg('--mail')
+ if mail:
+ mail_report = True
log.trace('_bset: -- %2d %s' % (nesting_count + 1, '-' * 75))
try:
- b = build.build(configs[s], self.opts.get_arg('--pkg-tar-files'),
- opts, macros)
+ b = build.build(configs[s],
+ self.opts.get_arg('--pkg-tar-files'),
+ opts,
+ macros)
except:
build_error = True
raise
@@ -354,12 +394,14 @@ class buildset:
self.build_package(configs[s], b)
self.report(configs[s], b,
copy.copy(self.opts),
- copy.copy(self.macros))
- # Always product an XML report.
+ copy.copy(self.macros),
+ mail = mail)
+ # Always produce an XML report.
self.report(configs[s], b,
copy.copy(self.opts),
copy.copy(self.macros),
- format = 'xml')
+ format = 'xml',
+ mail = mail)
if s == len(configs) - 1 and not have_errors:
self.bset_tar(b)
else:
@@ -428,25 +470,29 @@ class buildset:
end = datetime.datetime.now()
os.environ['PATH'] = current_path
build_time = str(end - start)
- if mail_report:
- to_addr = self.opts.get_arg('--mail-to')
- if to_addr is not None:
- to_addr = to_addr[1]
- else:
- to_addr = self.macros.expand('%{_mail_tools_to}')
- log.notice('Mailing report: %s' % (to_addr))
- self.write_mail_header('Build Time %s' % (build_time), True)
- self.write_mail_header('')
- m = mailer.mail(self.opts)
+ if mail_report and not self.macros.defined('mail_disable'):
+ self.write_mail_header('Build Time: %s' % (build_time), True)
+ self.write_mail_header('', True)
if self.build_failure is not None:
- mail_report_subject = 'Build: FAILED %s (%s)' %\
- (mail_report_subject, self.build_failure)
- pass_fail = 'FAILED'
+ mail_subject = 'FAILED %s (%s)' % \
+ (mail_subject, self.build_failure)
else:
- mail_report_subject = 'Build: PASSED %s' % (mail_report_subject)
- if not self.opts.dry_run():
- m.send(to_addr, mail_report_subject,
- self.mail_header + self.mail_report)
+ mail_subject = 'PASSED %s' % (mail_subject)
+ mail_subject = 'Build %s: %s' % (reports.platform(mode = 'system'),
+ mail_subject)
+ self.write_mail_header(mail['header'], True)
+ self.write_mail_header('')
+ log.notice('Mailing report: %s' % (mail['to']))
+ body = self.get_mail_header()
+ body += 'Output' + os.linesep
+ body += '======' + os.linesep + os.linesep
+ body += os.linesep.join(mail['output'].get())
+ body += os.linesep + os.linesep
+ body += 'Report' + os.linesep
+ body += '======' + os.linesep + os.linesep
+ body += self.get_mail_report()
+ if not opts.dry_run():
+ mail['mail'].send(mail['to'], mail_subject, body)
log.notice('Build Set: Time %s' % (build_time))
def list_bset_cfg_files(opts, configs):
@@ -467,6 +513,7 @@ def run():
import sys
ec = 0
setbuilder_error = False
+ mail = None
try:
optargs = { '--list-configs': 'List available configurations',
'--list-bsets': 'List available build sets',
@@ -477,10 +524,27 @@ def run():
'--report-format': 'The report format (text, html, asciidoc).' }
mailer.append_options(optargs)
opts = options.load(sys.argv, optargs)
+ if opts.get_arg('--mail'):
+ mail = { 'mail' : mailer.mail(opts),
+ 'output': log_capture() }
+ to_addr = opts.get_arg('--mail-to')
+ if to_addr is not None:
+ mail['to'] = to_addr[1]
+ else:
+ mail['to'] = opts.defaults.expand('%{_mail_tools_to}')
+ mail['from'] = mail['mail'].from_address()
log.notice('RTEMS Source Builder - Set Builder, %s' % (version.str()))
opts.log_info()
if not check.host_setup(opts):
raise error.general('host build environment is not set up correctly')
+ if mail:
+ mail['header'] = os.linesep.join(mail['output'].get()) + os.linesep
+ mail['header'] += os.linesep
+ mail['header'] += 'Host: ' + reports.platform('compact') + os.linesep
+ indent = ' '
+ for l in textwrap.wrap(reports.platform('extended'),
+ width = 80 - len(indent)):
+ mail['header'] += indent + l + os.linesep
configs = build.get_configs(opts)
if opts.get_arg('--list-deps'):
deps = []
@@ -496,12 +560,14 @@ def run():
not opts.no_install() and \
not path.ispathwritable(prefix):
raise error.general('prefix is not writable: %s' % (path.host(prefix)))
+
for bset in opts.params():
setbuilder_error = True
b = buildset(bset, configs, opts)
- b.build(deps)
+ b.build(deps, mail = mail)
b = None
setbuilder_error = False
+
if deps is not None:
c = 0
for d in sorted(set(deps)):
@@ -522,6 +588,10 @@ def run():
except KeyboardInterrupt:
log.notice('abort: user terminated')
ec = 1
+ except:
+ raise
+ log.notice('abort: unknown error')
+ ec = 1
sys.exit(ec)
if __name__ == "__main__":
--
2.14.3 (Apple Git-98)
More information about the devel
mailing list