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

Last change on this file since 6dc551c was 6dc551c, checked in by Chris Johns <chrisj@…>, on Oct 12, 2017 at 2:40:12 AM

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

Updates #2536.

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