source: rtems-source-builder/source-builder/sb/build.py @ 8db33fb

5
Last change on this file since 8db33fb was 8db33fb, checked in by Chris Johns <chrisj@…>, on 07/11/19 at 22:25:15

rtems: Add kernel, libbsd and BSP building as packages.

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