summaryrefslogtreecommitdiff
path: root/zephyr/zmake/zmake/output_packers.py
blob: 1ba38cf96c969032e7079679fa5619d1ff3c7dea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# Copyright 2020 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Types which provide many builds and composite them into a single binary."""
import logging
import shutil
import subprocess

import zmake.build_config as build_config
import zmake.multiproc
import zmake.util as util


class BasePacker:
    """Abstract base for all packers."""

    def __init__(self, project):
        self.project = project

    def configs(self):
        """Get all of the build configurations necessary.

        Yields:
            2-tuples of config name and a BuildConfig.
        """
        yield "singleimage", build_config.BuildConfig()

    def pack_firmware(self, work_dir, jobclient, version_string=""):
        """Pack a firmware image.

        Config names from the configs generator are passed as keyword
        arguments, with each argument being set to the path of the
        build directory.

        Args:
            work_dir: A directory to write outputs and temporary files
            into.
            jobclient: A JobClient object to use.
            version_string: The version string, which may end up in
               certain parts of the outputs.

        Yields:
            2-tuples of the path of each file in the work_dir (or any
            other directory) which should be copied into the output
            directory, and the output filename.
        """
        raise NotImplementedError("Abstract method not implemented")

    def _get_max_image_bytes(self):
        """Get the maximum allowed image size (in bytes).

        This value will generally be found in CONFIG_FLASH_SIZE but may vary
        depending on the specific way things are being packed.

        Returns:
            The maximum allowed size of the image in bytes.
        """
        raise NotImplementedError("Abstract method not implemented")

    def _is_size_bound(self, path):
        """Check whether the given path should be constrained by size.

        Generally, .elf files will be unconstrained while .bin files will be
        constrained.

        Args:
            path: A file's path to test.

        Returns:
            True if the file size should be checked. False otherwise.
        """
        return path.suffix == ".bin"

    def _check_packed_file_size(self, file, dirs):
        """Check that a packed file passes size constraints.

        Args:
            file: A file to test.
            dirs: A map of the arguments to pass to _get_max_image_bytes

        Returns:
            The file if it passes the test.
        """
        if not self._is_size_bound(
            file
        ) or file.stat().st_size <= self._get_max_image_bytes(**dirs):
            return file
        raise RuntimeError("Output file ({}) too large".format(file))


class ElfPacker(BasePacker):
    """Raw proxy for ELF output of a single build."""

    def pack_firmware(self, work_dir, jobclient, singleimage, version_string=""):
        yield singleimage / "zephyr" / "zephyr.elf", "zephyr.elf"


class RawBinPacker(BasePacker):
    """Raw proxy for zephyr.bin output of a single build."""

    def pack_firmware(self, work_dir, jobclient, singleimage, version_string=""):
        yield singleimage / "zephyr" / "zephyr.bin", "zephyr.bin"


class BinmanPacker(BasePacker):
    """Packer for RO/RW image to generate a .bin build using FMAP."""

    ro_file = "zephyr.bin"
    rw_file = "zephyr.bin"

    def __init__(self, project):
        self.logger = logging.getLogger(self.__class__.__name__)
        super().__init__(project)

    def configs(self):
        yield "ro", build_config.BuildConfig(kconfig_defs={"CONFIG_CROS_EC_RO": "y"})
        yield "rw", build_config.BuildConfig(kconfig_defs={"CONFIG_CROS_EC_RW": "y"})

    def pack_firmware(self, work_dir, jobclient, ro, rw, version_string=""):
        """Pack RO and RW sections using Binman.

        Binman configuration is expected to be found in the RO build
        device-tree configuration.

        Args:
            work_dir: The directory used for packing.
            jobclient: The client used to run subprocesses.
            ro: Directory containing the RO image build.
            rw: Directory containing the RW image build.
            version_string: The version string to use in FRID/FWID.

        Yields:
            2-tuples of the path of each file in the work_dir that
            should be copied into the output directory, and the output
            filename.
        """
        dts_file_path = ro / "zephyr" / "zephyr.dts"

        # Copy the inputs into the work directory so that Binman can
        # find them under a hard-coded name.
        shutil.copy2(ro / "zephyr" / self.ro_file, work_dir / "zephyr_ro.bin")
        shutil.copy2(rw / "zephyr" / self.rw_file, work_dir / "zephyr_rw.bin")

        # Version in FRID/FWID can be at most 31 bytes long (32, minus
        # one for null character).
        if len(version_string) > 31:
            version_string = version_string[:31]

        proc = jobclient.popen(
            [
                "binman",
                "-v",
                "5",
                "build",
                "-a",
                "version={}".format(version_string),
                "-d",
                dts_file_path,
                "-m",
                "-O",
                work_dir,
            ],
            cwd=work_dir,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            encoding="utf-8",
        )

        zmake.multiproc.log_output(self.logger, logging.DEBUG, proc.stdout)
        zmake.multiproc.log_output(self.logger, logging.ERROR, proc.stderr)
        if proc.wait(timeout=60):
            raise OSError("Failed to run binman")

        yield work_dir / "zephyr.bin", "zephyr.bin"
        yield ro / "zephyr" / "zephyr.elf", "zephyr.ro.elf"
        yield rw / "zephyr" / "zephyr.elf", "zephyr.rw.elf"


class NpcxPacker(BinmanPacker):
    """Packer for RO/RW image to generate a .bin build using FMAP.

    This expects that the build is setup to generate a
    zephyr.npcx.bin for the RO image, which should be packed using
    Nuvoton's loader format.
    """

    ro_file = "zephyr.npcx.bin"
    npcx_monitor = "npcx_monitor.bin"

    # TODO(b/192401039): CONFIG_FLASH_SIZE is nuvoton-only.  Since
    # binman already checks sizes, perhaps we can just remove this
    # code?
    def _get_max_image_bytes(self, ro, rw):
        ro_size = util.read_kconfig_autoconf_value(
            ro / "zephyr" / "include" / "generated", "CONFIG_FLASH_SIZE"
        )
        rw_size = util.read_kconfig_autoconf_value(
            ro / "zephyr" / "include" / "generated", "CONFIG_FLASH_SIZE"
        )
        return max(int(ro_size, 0), int(rw_size, 0)) * 1024

    # This can probably be removed too and just rely on binman to
    # check the sizes... see the comment above.
    def pack_firmware(self, work_dir, jobclient, ro, rw, version_string=""):
        for path, output_file in super().pack_firmware(
            work_dir,
            jobclient,
            ro,
            rw,
            version_string=version_string,
        ):
            if output_file == "zephyr.bin":
                yield (
                    self._check_packed_file_size(path, {"ro": ro, "rw": rw}),
                    "zephyr.bin",
                )
            else:
                yield path, output_file

        # Include the NPCX monitor file as an output artifact.
        yield ro / self.npcx_monitor, self.npcx_monitor


# A dictionary mapping packer config names to classes.
packer_registry = {
    "binman": BinmanPacker,
    "elf": ElfPacker,
    "npcx": NpcxPacker,
    "raw": RawBinPacker,
}