source: rtems-source-builder/source-builder/sb/build.py @ 96c414c

5
Last change on this file since 96c414c was 96c414c, checked in by Chris Johns <chrisj@…>, on Oct 22, 2018 at 12:59:10 AM

windows: Remove BuildRoot? from all configs, add a short tmp path.

Closes #3562.

  • Property mode set to 100644
File size: 24.9 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):
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 'symlink' in src:
250                sname = name.replace('-', '_')
251                src['script'] = '%%{__ln_s} %s ${source_dir_%s}' % (src['symlink'], sname)
252            elif 'compressed' in src:
253                #
254                # Zip files unpack as well so do not use tar.
255                #
256                src['script'] = '%s %s' % (src['compressed'], src['local'])
257                if src['compressed-type'] != 'zip':
258                    src['script'] += ' | %{__tar_extract} -'
259            else:
260                src['script'] = '%%{__tar_extract} %s' % (src['local'])
261            srcs += [src]
262        return srcs
263
264    def source_setup(self, package, args):
265        log.output('source setup: %s: %s' % (package.name(), ' '.join(args)))
266        setup_name = args[1]
267        args = args[1:]
268        try:
269            opts, args = getopt.getopt(args[1:], 'qDcn:ba')
270        except getopt.GetoptError as ge:
271            raise error.general('source setup error: %s' % str(ge))
272        quiet = False
273        unpack_before_chdir = True
274        delete_before_unpack = True
275        create_dir = False
276        deleted_dir = False
277        created_dir = False
278        changed_dir = False
279        opt_name = None
280        for o in opts:
281            if o[0] == '-q':
282                quiet = True
283            elif o[0] == '-D':
284                delete_before_unpack = False
285            elif o[0] == '-c':
286                create_dir = True
287            elif o[0] == '-n':
288                opt_name = o[1]
289            elif o[0] == '-b':
290                unpack_before_chdir = True
291            elif o[0] == '-a':
292                unpack_before_chdir = False
293        name = None
294        for source in self.source(setup_name):
295            if name is None:
296                if opt_name is None:
297                    if source:
298                        opt_name = source['name']
299                    else:
300                        raise error.general('setup source tag not found: %d' % (source_tag))
301                else:
302                    name = opt_name
303            self.script_build.append(self.config.expand('cd %{_builddir}'))
304            if not deleted_dir and  delete_before_unpack:
305                self.script_build.append(self.config.expand('%{__rm} -rf ' + name))
306                deleted_dir = True
307            if not created_dir and create_dir:
308                self.script_build.append(self.config.expand('%{__mkdir_p} ' + name))
309                created_dir = True
310            if not changed_dir and (not unpack_before_chdir or create_dir):
311                self.script_build.append(self.config.expand('cd ' + name))
312                changed_dir = True
313            self.script_build.append(self.config.expand(source['script']))
314        if not changed_dir and (unpack_before_chdir and not create_dir):
315            self.script_build.append(self.config.expand('cd ' + name))
316            changed_dir = True
317        self.script_build.append(self.config.expand('%{__setup_post}'))
318
319    def patch_setup(self, package, args):
320        name = args[1]
321        args = args[2:]
322        _map = 'patch-%s' % (name)
323        default_opts = ' '.join(args)
324        patch_keys = [p for p in self.macros.map_keys(_map) if p != 'setup']
325        patches = []
326        for p in patch_keys:
327            pm = self.macros.get(p, globals = False, maps = _map)
328            if pm is None:
329                raise error.internal('patch macro not found: %s in %s (%s)' % \
330                                         (p, name, _map))
331            opts = []
332            url = []
333            for pp in pm[2].split():
334                if len(url) == 0 and pp[0] == '-':
335                    opts += [pp]
336                else:
337                    url += [pp]
338            if len(url) == 0:
339                raise error.general('patch URL not found: %s' % (' '.join(args)))
340            #
341            # Look for --rsb-file as an option we use as a local file name.
342            # This can be used if a URL has no reasonable file name the
343            # download URL parser can figure out.
344            #
345            file_override = None
346            if len(opts) > 0:
347                for o in opts:
348                    if o.startswith('--rsb-file'):
349                       os_ = o.split('=')
350                       if len(os_) != 2:
351                           raise error.general('invalid --rsb-file option: %s' % (' '.join(args)))
352                       if os_[0] != '--rsb-file':
353                           raise error.general('invalid --rsb-file option: %s' % (' '.join(args)))
354                       file_override = os_[1]
355                opts = [o for o in opts if not o.startswith('--rsb-')]
356            if len(opts) == 0:
357                opts = default_opts
358            else:
359                opts = ' '.join(opts)
360            opts = self.config.expand(opts)
361            url = self.config.expand(' '.join(url))
362            #
363            # Parse the URL first in the source builder's patch directory.
364            #
365            patch = download.parse_url(url, '_patchdir', self.config, self.opts, file_override)
366            #
367            # Download the patch
368            #
369            download.get_file(patch['url'], patch['local'], self.opts, self.config)
370            if 'compressed' in patch:
371                patch['script'] = patch['compressed'] + ' ' +  patch['local']
372            else:
373                patch['script'] = '%{__cat} ' + patch['local']
374            patch['script'] += ' | %%{__patch} %s' % (opts)
375            self.script_build.append(self.config.expand(patch['script']))
376
377    def run(self, command, shell_opts = '', cwd = None):
378        e = execute.capture_execution(log = log.default, dump = self.opts.quiet())
379        cmd = self.config.expand('%{___build_shell} -ex ' + shell_opts + ' ' + command)
380        log.output('run: ' + cmd)
381        exit_code, proc, output = e.shell(cmd, cwd = path.host(cwd))
382        if exit_code != 0:
383            log.output('shell cmd failed: %s' % (cmd))
384            raise error.general('building %s' % (self.macros['buildname']))
385
386    def builddir(self):
387        builddir = self.config.abspath('_builddir')
388        self.rmdir(builddir)
389        if not self.opts.dry_run():
390            self.mkdir(builddir)
391
392    def prep(self, package):
393        self.script_build.append('echo "==> %prep:"')
394        _prep = package.prep()
395        if _prep:
396            for l in _prep:
397                args = l.split()
398                if len(args):
399                    def err(msg):
400                        raise error.general('%s: %s' % (package, msg))
401                    if args[0] == '%setup':
402                        if len(args) == 1:
403                            raise error.general('invalid %%setup directive: %s' % \
404                                                (' '.join(args)))
405                        if args[1] == 'source':
406                            self.source_setup(package, args[1:])
407                        elif args[1] == 'patch':
408                            self.patch_setup(package, args[1:])
409                    elif args[0] in ['%patch', '%source']:
410                        sources.process(args[0][1:], args[1:], self.macros, err)
411                    elif args[0] == '%hash':
412                        sources.hash(args[1:], self.macros, err)
413                        self.hash(package, args)
414                    else:
415                        self.script_build.append(' '.join(args))
416
417    def build(self, package):
418        self.script_build.append('echo "==> clean %{buildroot}: ${SB_BUILD_ROOT}"')
419        self.script_build.append('%s ${SB_BUILD_ROOT}' %
420                                 (self.config.expand('%{__rmdir}')))
421        self.script_build.append('%s ${SB_BUILD_ROOT}' %
422                                 (self.config.expand('%{__mkdir_p}')))
423        self.script_build.append('echo "==> %build:"')
424        _build = package.build()
425        if _build:
426            for l in _build:
427                self.script_build.append(l)
428
429    def install(self, package):
430        self.script_build.append('echo "==> %install:"')
431        _install = package.install()
432        if _install:
433            for l in _install:
434                args = l.split()
435                self.script_build.append(' '.join(args))
436
437    def files(self, package):
438        if self.create_tar_files \
439           and not self.macros.get('%{_disable_packaging'):
440            self.script_build.append('echo "==> %files:"')
441            inpath = path.abspath(self.config.expand('%{buildroot}'))
442            tardir = path.abspath(self.config.expand('%{_tardir}'))
443            self.script_build.append(self.config.expand('if test -d %s; then' % (inpath)))
444            self.script_build.append(self.config.expand(%%{__mkdir_p} %s' % tardir))
445            self.script_build.append(self.config.expand('  cd ' + inpath))
446            tar = path.join(tardir, package.long_name() + '.tar.bz2')
447            cmd = self.config.expand('  %{__tar} -cf - . ' + '| %{__bzip2} > ' + tar)
448            self.script_build.append(cmd)
449            self.script_build.append(self.config.expand('  cd %{_builddir}'))
450            self.script_build.append('fi')
451
452    def clean(self, package):
453        self.script_clean.reset()
454        self.script_clean.append(self.config.expand('%{___build_template}'))
455        self.script_clean.append('echo "=> ' + package.name() + ': CLEAN"')
456        self.script_clean.append('echo "==> %clean:"')
457        _clean = package.clean()
458        if _clean is not None:
459            for l in _clean:
460                args = l.split()
461                self.script_clean.append(' '.join(args))
462
463    def sizes(self, package):
464        def _sizes(package, what, path):
465            package.set_size(what, path)
466            s = humanize_number(package.get_size(what), 'B')
467            log.trace('size: %s (%s): %s (%d)' % (what, path, s, package.get_size(what)))
468            return s
469        s = {}
470        for p in [('build', '%{_builddir}'),
471                  ('build', '%{buildroot}'),
472                  ('installed', '%{buildroot}')]:
473            hs = _sizes(package, p[0], self.config.expand(p[1]))
474            s[p[0]] = hs
475        log.notice('sizes: %s: %s (installed: %s)' % (package.name(),
476                                                      s['build'],
477                                                      s['installed']))
478
479    def build_package(self, package):
480        if self.canadian_cross():
481            if not self.config.defined('%{allow_cxc}'):
482                raise error.general('Canadian Cross is not allowed')
483            self.script_build.append('echo "==> Candian-cross build/target:"')
484            self.script_build.append('SB_CXC="yes"')
485        else:
486            self.script_build.append('SB_CXC="no"')
487        self.build(package)
488        self.install(package)
489        self.files(package)
490        if not self.opts.no_clean():
491            self.clean(package)
492
493    def cleanup(self):
494        package = self.main_package()
495        if not package.disabled() and not self.opts.no_clean():
496            buildroot = self.config.abspath('buildroot')
497            builddir = self.config.abspath('_builddir')
498            buildcxcdir = self.config.abspath('_buildcxcdir')
499            tmproot = self.config.abspath('_tmproot')
500            log.trace('cleanup: %s' % (buildroot))
501            self.rmdir(buildroot)
502            log.trace('cleanup: %s' % (builddir))
503            self.rmdir(builddir)
504            if self.canadian_cross():
505                log.trace('cleanup: %s' % (buildcxcdir))
506                self.rmdir(buildcxcdir)
507            log.trace('cleanup: %s' % (tmproot))
508            self.rmdir(tmproot)
509
510    def main_package(self):
511        packages = self.config.packages()
512        return packages['main']
513
514    def reload(self):
515        self.config.load(self.init_name)
516
517    def make(self):
518        package = self.main_package()
519        if package.disabled():
520            log.notice('package: nothing to build')
521        else:
522            try:
523                name = package.name()
524                if self.canadian_cross():
525                    cxc_label = '(Cxc) '
526                else:
527                    cxc_label = ''
528                log.notice('package: %s%s' % (cxc_label, name))
529                log.trace('---- macro maps %s' % ('-' * 55))
530                log.trace('%s' % (str(self.config.macros)))
531                log.trace('-' * 70)
532                self.script_build.reset()
533                self.script_build.append(self.config.expand('%{___build_template}'))
534                self.script_build.append('echo "=> ' + name + ': BUILD"')
535                self.prep(package)
536                self.build_package(package)
537                if not self.opts.dry_run():
538                    self.builddir()
539                    build_sn = path.join(self.config.expand('%{_builddir}'), 'do-build')
540                    log.output('write script: ' + build_sn)
541                    self.script_build.write(build_sn)
542                    clean_sn = path.join(self.config.expand('%{_builddir}'), 'do-clean')
543                    log.output('write script: ' + clean_sn)
544                    self.script_clean.write(clean_sn)
545                    log.notice('building: %s%s' % (cxc_label, name))
546                    self.run(build_sn)
547                    self.sizes(package)
548                    log.notice('cleaning: %s%s' % (cxc_label, name))
549                    self.run(clean_sn)
550            except error.general as gerr:
551                log.notice(str(gerr))
552                log.stderr('Build FAILED')
553                self._generate_report_('Build: %s' % (gerr))
554                raise
555            except error.internal as ierr:
556                log.notice(str(ierr))
557                log.stderr('Internal Build FAILED')
558                self._generate_report_('Build: %s' % (ierr))
559                raise
560            except:
561                raise
562            if self.opts.dry_run():
563                self._generate_report_('Build: dry run (no actual error)',
564                                       'Build: dry run (no actual error)')
565
566    def name(self):
567        packages = self.config.packages()
568        package = packages['main']
569        return package.name()
570
571    def disabled(self):
572        packages = self.config.packages()
573        package = packages['main']
574        return package.disabled()
575
576    def get_build_size(self):
577        package = self.main_package()
578        if package.disabled():
579            return 0
580        return package.get_size('build')
581
582    def get_installed_size(self):
583        package = self.main_package()
584        if package.disabled():
585            return 0
586        return package.get_size('installed')
587
588def get_configs(opts):
589
590    def _scan(_path, ext):
591        configs = []
592        for root, dirs, files in os.walk(_path):
593            prefix = root[len(_path) + 1:]
594            for file in files:
595                for e in ext:
596                    if file.endswith(e):
597                        configs += [path.join(prefix, file)]
598        return configs
599
600    configs = { 'paths': [], 'files': [] }
601    for cp in opts.defaults.expand('%{_configdir}').split(':'):
602        hcp = path.host(path.abspath(cp))
603        configs['paths'] += [hcp]
604        configs['files'] += _scan(hcp, ['.cfg', '.bset'])
605    configs['files'] = sorted(set(configs['files']))
606    return configs
607
608def find_config(config, configs):
609    config_root, config_ext = path.splitext(config)
610    if config_ext not in ['', '.bset', '.cfg']:
611        config_root = config
612        config_ext = ''
613    for c in configs['files']:
614        r, e = path.splitext(c)
615        if config_root == r:
616            if config_ext == '' or config_ext == e:
617                return c
618    return None
619
620def run(args):
621    ec = 0
622    try:
623        optargs = { '--list-configs': 'List available configurations' }
624        opts = options.load(args, optargs)
625        log.notice('RTEMS Source Builder, Package Builder, %s' % (version.str()))
626        opts.log_info()
627        if not check.host_setup(opts):
628            if not opts.force():
629                raise error.general('host build environment is not set up' +
630                                    ' correctly (use --force to proceed)')
631            log.notice('warning: forcing build with known host setup problems')
632        if opts.get_arg('--list-configs'):
633            configs = get_configs(opts)
634            for p in configs['paths']:
635                print('Examining: %s' % (os.path.relpath(p)))
636                for c in configs['files']:
637                    if c.endswith('.cfg'):
638                        print('    %s' % (c))
639        else:
640            for config_file in opts.config_files():
641                b = build(config_file, True, opts)
642                b.make()
643                b = None
644    except error.general as gerr:
645        log.stderr('Build FAILED')
646        ec = 1
647    except error.internal as ierr:
648        log.stderr('Internal Build FAILED')
649        ec = 1
650    except error.exit as eerr:
651        pass
652    except KeyboardInterrupt:
653        log.notice('abort: user terminated')
654        ec = 1
655    sys.exit(ec)
656
657if __name__ == "__main__":
658    run(sys.argv)
Note: See TracBrowser for help on using the repository browser.