source: rtems-source-builder/source-builder/sb/setbuilder.py @ 8f6fb61

4.11
Last change on this file since 8f6fb61 was e70a165, checked in by Chris Johns <chrisj@…>, on 03/11/16 at 01:39:34

sb: Do not expand the package name in a bset file.

If the package references macros yet to be defined an error is
generated. Let the macro expands happen when the package name is
actually used.

Closes #2645.

  • Property mode set to 100644
File size: 20.5 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# This code builds a package compiler tool suite given a tool set. A tool
22# set lists the various tools. These are specific tool configurations.
23#
24
25from __future__ import print_function
26
27import copy
28import datetime
29import glob
30import operator
31import os
32import sys
33
34try:
35    import build
36    import check
37    import error
38    import log
39    import mailer
40    import options
41    import path
42    import reports
43    import sources
44    import version
45except KeyboardInterrupt:
46    print('abort: user terminated', file = sys.stderr)
47    sys.exit(1)
48except:
49    print('error: unknown application load error', file = sys.stderr)
50    sys.exit(1)
51
52class buildset:
53    """Build a set builds a set of packages."""
54
55    def __init__(self, bset, _configs, opts, macros = None):
56        log.trace('_bset: %s: init' % (bset))
57        self.configs = _configs
58        self.opts = opts
59        if macros is None:
60            self.macros = copy.copy(opts.defaults)
61        else:
62            self.macros = copy.copy(macros)
63        self.bset = bset
64        _target = self.macros.expand('%{_target}')
65        if len(_target):
66            pkg_prefix = _target
67        else:
68            pkg_prefix = self.macros.expand('%{_host}')
69        self.bset_pkg = '%s-%s-set' % (pkg_prefix, self.bset)
70        self.mail_header = ''
71        self.mail_report = ''
72        self.build_failure = None
73
74    def write_mail_header(self, text, prepend = False):
75        if len(text) == 0 or text[-1] != '\n' or text[-1] != '\r':
76            text += os.linesep
77        if prepend:
78            self.mail_header = text + self.mail_header
79        else:
80            self.mail_header += text
81
82    def write_mail_report(self, text, prepend = False):
83        if len(text) == 0 or text[-1] != '\n' or text[-1] != '\r':
84            text += os.linesep
85        if prepend:
86            self.mail_report = text + self.mail_report
87        else:
88            self.mail_report += text
89
90    def copy(self, src, dst):
91        log.output('copy: %s => %s' % (path.host(src), path.host(dst)))
92        if not self.opts.dry_run():
93            path.copy_tree(src, dst)
94
95    def report(self, _config, _build, opts, macros, format = None):
96        if len(_build.main_package().name()) > 0 \
97           and not _build.macros.get('%{_disable_reporting}') \
98           and (not _build.opts.get_arg('--no-report') \
99                or _build.opts.get_arg('--mail')):
100            if format is None:
101                format = _build.opts.get_arg('--report-format')
102                if format is not None:
103                    if len(format) != 2:
104                        raise error.general('invalid report format option: %s' % \
105                                            ('='.join(format)))
106                    format = format[1]
107            if format is None:
108                format = 'text'
109            if format == 'text':
110                ext = '.txt'
111            elif format == 'asciidoc':
112                ext = '.txt'
113            elif format == 'html':
114                ext = '.html'
115            elif format == 'xml':
116                ext = '.xml'
117            elif format == 'ini':
118                ext = '.ini'
119            else:
120                raise error.general('invalid report format: %s' % (format))
121            buildroot = _build.config.abspath('%{buildroot}')
122            prefix = _build.macros.expand('%{_prefix}')
123            name = _build.main_package().name() + ext
124            log.notice('reporting: %s -> %s' % (_config, name))
125            if not _build.opts.get_arg('--no-report'):
126                outpath = path.host(path.join(buildroot, prefix, 'share', 'rtems', 'rsb'))
127                if not _build.opts.dry_run():
128                    outname = path.host(path.join(outpath, name))
129                else:
130                    outname = None
131                r = reports.report(format, self.configs,
132                                   copy.copy(opts), copy.copy(macros))
133                r.introduction(_build.config.file_name())
134                r.generate(_build.config.file_name())
135                r.epilogue(_build.config.file_name())
136                if not _build.opts.dry_run():
137                    _build.mkdir(outpath)
138                    r.write(outname)
139                del r
140            if _build.opts.get_arg('--mail'):
141                r = reports.report('text', self.configs,
142                                   copy.copy(opts), copy.copy(macros))
143                r.introduction(_build.config.file_name())
144                r.generate(_build.config.file_name())
145                r.epilogue(_build.config.file_name())
146                self.write_mail_report(r.out)
147                del r
148
149    def root_copy(self, src, dst):
150        what = '%s -> %s' % \
151            (os.path.relpath(path.host(src)), os.path.relpath(path.host(dst)))
152        log.trace('_bset: %s: collecting: %s' % (self.bset, what))
153        self.copy(src, dst)
154
155    def install(self, name, buildroot, prefix):
156        dst = prefix
157        src = path.join(buildroot, prefix)
158        log.notice('installing: %s -> %s' % (name, path.host(dst)))
159        self.copy(src, dst)
160
161    def canadian_cross(self, _build):
162        log.trace('_bset: Cxc for build machine: _build => _host')
163        macros_to_copy = [('%{_host}',        '%{_build}'),
164                          ('%{_host_alias}',  '%{_build_alias}'),
165                          ('%{_host_arch}',   '%{_build_arch}'),
166                          ('%{_host_cpu}',    '%{_build_cpu}'),
167                          ('%{_host_os}',     '%{_build_os}'),
168                          ('%{_host_vendor}', '%{_build_vendor}'),
169                          ('%{_tmproot}',     '%{_tmpcxcroot}'),
170                          ('%{buildroot}',    '%{buildcxcroot}'),
171                          ('%{_builddir}',    '%{_buildcxcdir}')]
172        cxc_macros = _build.copy_init_macros()
173        for m in macros_to_copy:
174            log.trace('_bset: Cxc: %s <= %s' % (m[0], cxc_macros[m[1]]))
175            cxc_macros[m[0]] = cxc_macros[m[1]]
176        _build.set_macros(cxc_macros)
177        _build.reload()
178        _build.make()
179        if not _build.macros.get('%{_disable_collecting}'):
180            self.root_copy(_build.config.expand('%{buildroot}'),
181                           _build.config.expand('%{_tmproot}'))
182        _build.set_macros(_build.copy_init_macros())
183        _build.reload()
184
185    def build_package(self, _config, _build):
186        if not _build.disabled():
187            if _build.canadian_cross():
188                self.canadian_cross(_build)
189            _build.make()
190            if not _build.macros.get('%{_disable_collecting}'):
191                self.root_copy(_build.config.expand('%{buildroot}'),
192                               _build.config.expand('%{_tmproot}'))
193
194    def bset_tar(self, _build):
195        tardir = _build.config.expand('%{_tardir}')
196        if (self.opts.get_arg('--bset-tar-file') or self.opts.canadian_cross()) \
197           and not _build.macros.get('%{_disable_packaging}'):
198            path.mkdir(tardir)
199            tar = path.join(tardir, _build.config.expand('%s.tar.bz2' % (_build.main_package().name())))
200            log.notice('tarball: %s' % (os.path.relpath(path.host(tar))))
201            if not self.opts.dry_run():
202                tmproot = _build.config.expand('%{_tmproot}')
203                cmd = _build.config.expand('"cd ' + tmproot + \
204                                               ' && %{__tar} -cf - . | %{__bzip2} > ' + tar + '"')
205                _build.run(cmd, shell_opts = '-c', cwd = tmproot)
206
207    def parse(self, bset):
208
209        def _clean(line):
210            line = line[0:-1]
211            b = line.find('#')
212            if b >= 0:
213                line = line[1:b]
214            return line.strip()
215
216        bsetname = bset
217
218        if not path.exists(bsetname):
219            for cp in self.macros.expand('%{_configdir}').split(':'):
220                configdir = path.abspath(cp)
221                bsetname = path.join(configdir, bset)
222                if path.exists(bsetname):
223                    break
224                bsetname = None
225            if bsetname is None:
226                raise error.general('no build set file found: %s' % (bset))
227        try:
228            log.trace('_bset: %s: open: %s' % (self.bset, bsetname))
229            bset = open(path.host(bsetname), 'r')
230        except IOError as err:
231            raise error.general('error opening bset file: %s' % (bsetname))
232
233        configs = []
234
235        try:
236            lc = 0
237            for l in bset:
238                lc += 1
239                l = _clean(l)
240                if len(l) == 0:
241                    continue
242                log.trace('_bset: %s: %03d: %s' % (self.bset, lc, l))
243                ls = l.split()
244                if ls[0][-1] == ':' and ls[0][:-1] == 'package':
245                    self.bset_pkg = ls[1].strip()
246                    self.macros['package'] = self.bset_pkg
247                elif ls[0][0] == '%':
248                    def err(msg):
249                        raise error.general('%s:%d: %s' % (self.bset, lc, msg))
250                    if ls[0] == '%define':
251                        if len(ls) > 2:
252                            self.macros.define(ls[1].strip(),
253                                               ' '.join([f.strip() for f in ls[2:]]))
254                        else:
255                            self.macros.define(ls[1].strip())
256                    elif ls[0] == '%undefine':
257                        if len(ls) > 2:
258                            raise error.general('%s:%d: %undefine requires just the name' % \
259                                                    (self.bset, lc))
260                        self.macros.undefine(ls[1].strip())
261                    elif ls[0] == '%include':
262                        configs += self.parse(ls[1].strip())
263                    elif ls[0] in ['%patch', '%source']:
264                        sources.process(ls[0][1:], ls[1:], self.macros, err)
265                    elif ls[0] == '%hash':
266                        sources.hash(ls[1:], self.macros, err)
267                else:
268                    l = l.strip()
269                    c = build.find_config(l, self.configs)
270                    if c is None:
271                        raise error.general('%s:%d: cannot find file: %s' % (self.bset, lc, l))
272                    configs += [c]
273        except:
274            bset.close()
275            raise
276
277        bset.close()
278
279        return configs
280
281    def load(self):
282
283        #
284        # If the build set file ends with .cfg the user has passed to the
285        # buildset builder a configuration so we just return it.
286        #
287        if self.bset.endswith('.cfg'):
288            configs = [self.bset]
289        else:
290            exbset = self.macros.expand(self.bset)
291            self.macros['_bset'] = exbset
292            root, ext = path.splitext(exbset)
293            if exbset.endswith('.bset'):
294                bset = exbset
295            else:
296                bset = '%s.bset' % (exbset)
297            configs = self.parse(bset)
298        return configs
299
300    def build(self, deps = None, nesting_count = 0):
301
302        build_error = False
303
304        nesting_count += 1
305
306        log.trace('_bset: %s: make' % (self.bset))
307        log.notice('Build Set: %s' % (self.bset))
308
309        if self.opts.get_arg('--mail'):
310            mail_report_subject = '%s %s' % (self.bset, self.macros.expand('%{_host}'))
311
312        current_path = os.environ['PATH']
313
314        start = datetime.datetime.now()
315
316        mail_report = False
317        have_errors = False
318
319        try:
320            configs = self.load()
321
322            log.trace('_bset: %s: configs: %s'  % (self.bset, ','.join(configs)))
323
324            builds = []
325            for s in range(0, len(configs)):
326                b = None
327                try:
328                    #
329                    # Each section of the build set gets a separate set of
330                    # macros so we do not contaminate one configuration with
331                    # another.
332                    #
333                    opts = copy.copy(self.opts)
334                    macros = copy.copy(self.macros)
335                    if configs[s].endswith('.bset'):
336                        log.trace('_bset: == %2d %s' % (nesting_count + 1, '=' * 75))
337                        bs = buildset(configs[s], self.configs, opts, macros)
338                        bs.build(deps, nesting_count)
339                        del bs
340                    elif configs[s].endswith('.cfg'):
341                        mail_report = self.opts.get_arg('--mail')
342                        log.trace('_bset: -- %2d %s' % (nesting_count + 1, '-' * 75))
343                        try:
344                            b = build.build(configs[s], self.opts.get_arg('--pkg-tar-files'),
345                                            opts, macros)
346                        except:
347                            build_error = True
348                            raise
349                        if b.macros.get('%{_disable_reporting}'):
350                            mail_report = False
351                        if deps is None:
352                            self.build_package(configs[s], b)
353                            self.report(configs[s], b,
354                                        copy.copy(self.opts),
355                                        copy.copy(self.macros))
356                            # Always product an XML report.
357                            self.report(configs[s], b,
358                                        copy.copy(self.opts),
359                                        copy.copy(self.macros),
360                                        format = 'xml')
361                            if s == len(configs) - 1 and not have_errors:
362                                self.bset_tar(b)
363                        else:
364                            deps += b.config.includes()
365                        builds += [b]
366                    else:
367                        raise error.general('invalid config type: %s' % (configs[s]))
368                except error.general as gerr:
369                    have_errors = True
370                    if b is not None:
371                        if self.build_failure is None:
372                            self.build_failure = b.name()
373                        self.write_mail_header('')
374                        self.write_mail_header('= ' * 40)
375                        self.write_mail_header('Build FAILED: %s' % (b.name()))
376                        self.write_mail_header('- ' * 40)
377                        self.write_mail_header(str(log.default))
378                        self.write_mail_header('- ' * 40)
379                        if self.opts.keep_going():
380                            log.notice(str(gerr))
381                            if self.opts.always_clean():
382                                builds += [b]
383                        else:
384                            raise
385                    else:
386                        raise
387            #
388            # Installing ...
389            #
390            log.trace('_bset: installing: deps:%r no-install:%r' % \
391                      (deps is None, self.opts.no_install()))
392            if deps is None \
393               and not self.opts.no_install() \
394               and not have_errors:
395                for b in builds:
396                    log.trace('_bset: installing: %r' % b.installable())
397                    if b.installable():
398                        self.install(b.name(),
399                                     b.config.expand('%{buildroot}'),
400                                     b.config.expand('%{_prefix}'))
401
402            if deps is None and \
403                    (not self.opts.no_clean() or self.opts.always_clean()):
404                for b in builds:
405                    if not b.disabled():
406                        log.notice('cleaning: %s' % (b.name()))
407                        b.cleanup()
408            for b in builds:
409                del b
410        except error.general as gerr:
411            if not build_error:
412                log.stderr(str(gerr))
413            raise
414        except KeyboardInterrupt:
415            mail_report = False
416            raise
417        except:
418            self.build_failure = 'RSB general failure'
419            raise
420        finally:
421            end = datetime.datetime.now()
422            os.environ['PATH'] = current_path
423            build_time = str(end - start)
424            if mail_report:
425                to_addr = self.opts.get_arg('--mail-to')
426                if to_addr is not None:
427                    to_addr = to_addr[1]
428                else:
429                    to_addr = self.macros.expand('%{_mail_tools_to}')
430                log.notice('Mailing report: %s' % (to_addr))
431                self.write_mail_header('Build Time %s' % (build_time), True)
432                self.write_mail_header('')
433                m = mailer.mail(self.opts)
434                if self.build_failure is not None:
435                    mail_report_subject = 'Build: FAILED %s (%s)' %\
436                        (mail_report_subject, self.build_failure)
437                    pass_fail = 'FAILED'
438                else:
439                    mail_report_subject = 'Build: PASSED %s' % (mail_report_subject)
440                if not self.opts.dry_run():
441                    m.send(to_addr, mail_report_subject,
442                           self.mail_header + self.mail_report)
443            log.notice('Build Set: Time %s' % (build_time))
444
445def list_bset_cfg_files(opts, configs):
446    if opts.get_arg('--list-configs') or opts.get_arg('--list-bsets'):
447        if opts.get_arg('--list-configs'):
448            ext = '.cfg'
449        else:
450            ext = '.bset'
451        for p in configs['paths']:
452            print('Examining: %s' % (os.path.relpath(p)))
453        for c in configs['files']:
454            if c.endswith(ext):
455                print('    %s' % (c))
456        return True
457    return False
458
459def run():
460    import sys
461    ec = 0
462    setbuilder_error = False
463    try:
464        optargs = { '--list-configs':  'List available configurations',
465                    '--list-bsets':    'List available build sets',
466                    '--list-deps':     'List the dependent files.',
467                    '--bset-tar-file': 'Create a build set tar file',
468                    '--pkg-tar-files': 'Create package tar files',
469                    '--no-report':     'Do not create a package report.',
470                    '--report-format': 'The report format (text, html, asciidoc).' }
471        mailer.append_options(optargs)
472        opts = options.load(sys.argv, optargs)
473        log.notice('RTEMS Source Builder - Set Builder, %s' % (version.str()))
474        opts.log_info()
475        if not check.host_setup(opts):
476            raise error.general('host build environment is not set up correctly')
477        configs = build.get_configs(opts)
478        if opts.get_arg('--list-deps'):
479            deps = []
480        else:
481            deps = None
482        if not list_bset_cfg_files(opts, configs):
483            prefix = opts.defaults.expand('%{_prefix}')
484            if opts.canadian_cross():
485                opts.disable_install()
486
487            if not opts.dry_run() and \
488               not opts.canadian_cross() and \
489               not opts.no_install() and \
490               not path.ispathwritable(prefix):
491                raise error.general('prefix is not writable: %s' % (path.host(prefix)))
492            for bset in opts.params():
493                setbuilder_error = True
494                b = buildset(bset, configs, opts)
495                b.build(deps)
496                b = None
497                setbuilder_error = False
498        if deps is not None:
499            c = 0
500            for d in sorted(set(deps)):
501                c += 1
502                print('dep[%d]: %s' % (c, d))
503    except error.general as gerr:
504        if not setbuilder_error:
505            log.stderr(str(gerr))
506        log.stderr('Build FAILED')
507        ec = 1
508    except error.internal as ierr:
509        if not setbuilder_error:
510            log.stderr(str(ierr))
511        log.stderr('Internal Build FAILED')
512        ec = 1
513    except error.exit as eerr:
514        pass
515    except KeyboardInterrupt:
516        log.notice('abort: user terminated')
517        ec = 1
518    sys.exit(ec)
519
520if __name__ == "__main__":
521    run()
Note: See TracBrowser for help on using the repository browser.