1# Development tool - build-image plugin
2#
3# Copyright (C) 2015 Intel Corporation
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8"""Devtool plugin containing the build-image subcommand."""
9
10import os
11import errno
12import logging
13
14from bb.process import ExecutionError
15from devtool import exec_build_env_command, setup_tinfoil, parse_recipe, DevtoolError
16
17logger = logging.getLogger('devtool')
18
19class TargetNotImageError(Exception):
20    pass
21
22def _get_packages(tinfoil, workspace, config):
23    """Get list of packages from recipes in the workspace."""
24    result = []
25    for recipe in workspace:
26        data = parse_recipe(config, tinfoil, recipe, True)
27        if 'class-target' in data.getVar('OVERRIDES').split(':'):
28            if recipe in data.getVar('PACKAGES').split():
29                result.append(recipe)
30            else:
31                logger.warning("Skipping recipe %s as it doesn't produce a "
32                               "package with the same name", recipe)
33    return result
34
35def build_image(args, config, basepath, workspace):
36    """Entry point for the devtool 'build-image' subcommand."""
37
38    image = args.imagename
39    auto_image = False
40    if not image:
41        sdk_targets = config.get('SDK', 'sdk_targets', '').split()
42        if sdk_targets:
43            image = sdk_targets[0]
44            auto_image = True
45    if not image:
46        raise DevtoolError('Unable to determine image to build, please specify one')
47
48    try:
49        if args.add_packages:
50            add_packages = args.add_packages.split(',')
51        else:
52            add_packages = None
53        result, outputdir = build_image_task(config, basepath, workspace, image, add_packages)
54    except TargetNotImageError:
55        if auto_image:
56            raise DevtoolError('Unable to determine image to build, please specify one')
57        else:
58            raise DevtoolError('Specified recipe %s is not an image recipe' % image)
59
60    if result == 0:
61        logger.info('Successfully built %s. You can find output files in %s'
62                    % (image, outputdir))
63    return result
64
65def build_image_task(config, basepath, workspace, image, add_packages=None, task=None, extra_append=None):
66    # remove <image>.bbappend to make sure setup_tinfoil doesn't
67    # break because of it
68    target_basename = config.get('SDK', 'target_basename', '')
69    if target_basename:
70        appendfile = os.path.join(config.workspace_path, 'appends',
71                                  '%s.bbappend' % target_basename)
72        try:
73            os.unlink(appendfile)
74        except OSError as exc:
75            if exc.errno != errno.ENOENT:
76                raise
77
78    tinfoil = setup_tinfoil(basepath=basepath)
79    try:
80        rd = parse_recipe(config, tinfoil, image, True)
81        if not rd:
82            # Error already shown
83            return (1, None)
84        if not bb.data.inherits_class('image', rd):
85            raise TargetNotImageError()
86
87        # Get the actual filename used and strip the .bb and full path
88        target_basename = rd.getVar('FILE')
89        target_basename = os.path.splitext(os.path.basename(target_basename))[0]
90        config.set('SDK', 'target_basename', target_basename)
91        config.write()
92
93        appendfile = os.path.join(config.workspace_path, 'appends',
94                                '%s.bbappend' % target_basename)
95
96        outputdir = None
97        try:
98            if workspace or add_packages:
99                if add_packages:
100                    packages = add_packages
101                else:
102                    packages = _get_packages(tinfoil, workspace, config)
103            else:
104                packages = None
105            if not task:
106                if not packages and not add_packages and workspace:
107                    logger.warning('No recipes in workspace, building image %s unmodified', image)
108                elif not packages:
109                    logger.warning('No packages to add, building image %s unmodified', image)
110
111            if packages or extra_append:
112                bb.utils.mkdirhier(os.path.dirname(appendfile))
113                with open(appendfile, 'w') as afile:
114                    if packages:
115                        # include packages from workspace recipes into the image
116                        afile.write('IMAGE_INSTALL_append = " %s"\n' % ' '.join(packages))
117                        if not task:
118                            logger.info('Building image %s with the following '
119                                        'additional packages: %s', image, ' '.join(packages))
120                    if extra_append:
121                        for line in extra_append:
122                            afile.write('%s\n' % line)
123
124            if task in ['populate_sdk', 'populate_sdk_ext']:
125                outputdir = rd.getVar('SDK_DEPLOY')
126            else:
127                outputdir = rd.getVar('DEPLOY_DIR_IMAGE')
128
129            tmp_tinfoil = tinfoil
130            tinfoil = None
131            tmp_tinfoil.shutdown()
132
133            options = ''
134            if task:
135                options += '-c %s' % task
136
137            # run bitbake to build image (or specified task)
138            try:
139                exec_build_env_command(config.init_path, basepath,
140                                    'bitbake %s %s' % (options, image), watch=True)
141            except ExecutionError as err:
142                return (err.exitcode, None)
143        finally:
144            if os.path.isfile(appendfile):
145                os.unlink(appendfile)
146    finally:
147        if tinfoil:
148            tinfoil.shutdown()
149    return (0, outputdir)
150
151
152def register_commands(subparsers, context):
153    """Register devtool subcommands from the build-image plugin"""
154    parser = subparsers.add_parser('build-image',
155                                   help='Build image including workspace recipe packages',
156                                   description='Builds an image, extending it to include '
157                                   'packages from recipes in the workspace',
158                                   group='testbuild', order=-10)
159    parser.add_argument('imagename', help='Image recipe to build', nargs='?')
160    parser.add_argument('-p', '--add-packages', help='Instead of adding packages for the '
161                        'entire workspace, specify packages to be added to the image '
162                        '(separate multiple packages by commas)',
163                        metavar='PACKAGES')
164    parser.set_defaults(func=build_image)
165