summaryrefslogtreecommitdiff
path: root/zephyr/zmake/zmake/output_packers.py
blob: 78ee7649e62ebf5d8376bb821209fb4ea595588b (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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# 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
from pathlib import Path
from typing import Dict, Optional

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


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

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

    @staticmethod
    def configs():
        """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: zmake.jobserver.JobClient,
        dir_map: Dict[str, Path],
        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.
            dir_map: A dict of build dirs such as {'ro': path_to_ro_dir}.
            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")

    @staticmethod
    def _get_max_image_bytes(dir_map) -> Optional[int]:
        """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.

        Args:
            file: A file to test.
            dir_map: A dict of build dirs such as {'ro': path_to_ro_dir}.

        Returns:
            The maximum allowed size of the image in bytes, or None if the size
            is not limited.
        """
        del dir_map

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

        Args:
            file: A file to test.
            dir_map: A dict of build dirs such as {'ro': path_to_ro_dir}.


        Returns:
            The file if it passes the test.
        """
        max_size = self._get_max_image_bytes(  # pylint: disable=assignment-from-none
            dir_map
        )
        if max_size is None or file.stat().st_size <= max_size:
            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, dir_map, version_string=""):
        del version_string
        yield dir_map["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, dir_map, version_string=""):
        del version_string
        yield dir_map["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: zmake.jobserver.JobClient, dir_map, 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.
            dir_map: A dict of build dirs such as {'ro': path_to_ro_dir}.
            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.
        """
        ro_dir = dir_map["ro"]
        rw_dir = dir_map["rw"]
        dts_file_path = ro_dir / "zephyr" / "zephyr.dts"

        # Copy the inputs into the work directory so that Binman can
        # find them under a hard-coded name.
        shutil.copy2(ro_dir / "zephyr" / self.ro_file, work_dir / "zephyr_ro.bin")
        shutil.copy2(rw_dir / "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.LogWriter.log_output(self.logger, logging.DEBUG, proc.stdout)
        zmake.multiproc.LogWriter.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_dir / "zephyr" / "zephyr.elf", "zephyr.ro.elf"
        yield rw_dir / "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"

    def _get_max_image_bytes(self, dir_map):
        ro_dir = dir_map["ro"]
        rw_dir = dir_map["rw"]
        ro_size = util.read_kconfig_autoconf_value(
            ro_dir / "zephyr" / "include" / "generated",
            "CONFIG_PLATFORM_EC_FLASH_SIZE_BYTES",
        )
        rw_size = util.read_kconfig_autoconf_value(
            rw_dir / "zephyr" / "include" / "generated",
            "CONFIG_PLATFORM_EC_FLASH_SIZE_BYTES",
        )
        return max(int(ro_size, 0), int(rw_size, 0))

    # 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, dir_map, version_string=""):
        ro_dir = dir_map["ro"]
        for path, output_file in super().pack_firmware(
            work_dir,
            jobclient,
            dir_map,
            version_string=version_string,
        ):
            if output_file == "zephyr.bin":
                yield (
                    self._check_packed_file_size(path, dir_map),
                    "zephyr.bin",
                )
            else:
                yield path, output_file

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


# MCHP all we do is set binman's ro file to zephyr.mchp.bin
class MchpPacker(BinmanPacker):
    """Packer for RO/RW image to generate a .bin build using FMAP.

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

    ro_file = "zephyr.mchp.bin"


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