1#
2# Copyright (c) 2014, Intel Corporation.
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6# DESCRIPTION
7# This implements the 'bootimg-efi' source plugin class for 'wic'
8#
9# AUTHORS
10# Tom Zanussi <tom.zanussi (at] linux.intel.com>
11#
12
13import logging
14import os
15import tempfile
16import shutil
17import re
18
19from glob import glob
20
21from wic import WicError
22from wic.engine import get_custom_config
23from wic.pluginbase import SourcePlugin
24from wic.misc import (exec_cmd, exec_native_cmd,
25                      get_bitbake_var, BOOTDD_EXTRA_SPACE)
26
27logger = logging.getLogger('wic')
28
29class BootimgEFIPlugin(SourcePlugin):
30    """
31    Create EFI boot partition.
32    This plugin supports GRUB 2 and systemd-boot bootloaders.
33    """
34
35    name = 'bootimg-efi'
36
37    @classmethod
38    def _copy_additional_files(cls, hdddir, initrd, dtb):
39        bootimg_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
40        if not bootimg_dir:
41            raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
42
43        if initrd:
44            initrds = initrd.split(';')
45            for rd in initrds:
46                cp_cmd = "cp -v -p %s/%s %s" % (bootimg_dir, rd, hdddir)
47                out = exec_cmd(cp_cmd, True)
48                logger.debug("initrd files:\n%s" % (out))
49        else:
50            logger.debug("Ignoring missing initrd")
51
52        if dtb:
53            if ';' in dtb:
54                raise WicError("Only one DTB supported, exiting")
55            cp_cmd = "cp -v -p %s/%s %s" % (bootimg_dir, dtb, hdddir)
56            out = exec_cmd(cp_cmd, True)
57            logger.debug("dtb files:\n%s" % (out))
58
59    @classmethod
60    def do_configure_grubefi(cls, hdddir, creator, cr_workdir, source_params):
61        """
62        Create loader-specific (grub-efi) config
63        """
64        configfile = creator.ks.bootloader.configfile
65        custom_cfg = None
66        if configfile:
67            custom_cfg = get_custom_config(configfile)
68            if custom_cfg:
69                # Use a custom configuration for grub
70                grubefi_conf = custom_cfg
71                logger.debug("Using custom configuration file "
72                             "%s for grub.cfg", configfile)
73            else:
74                raise WicError("configfile is specified but failed to "
75                               "get it from %s." % configfile)
76
77        initrd = source_params.get('initrd')
78        dtb = source_params.get('dtb')
79
80        cls._copy_additional_files(hdddir, initrd, dtb)
81
82        if not custom_cfg:
83            # Create grub configuration using parameters from wks file
84            bootloader = creator.ks.bootloader
85            title = source_params.get('title')
86
87            grubefi_conf = ""
88            grubefi_conf += "serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1\n"
89            grubefi_conf += "default=boot\n"
90            grubefi_conf += "timeout=%s\n" % bootloader.timeout
91            grubefi_conf += "menuentry '%s'{\n" % (title if title else "boot")
92
93            kernel = get_bitbake_var("KERNEL_IMAGETYPE")
94            if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
95                if get_bitbake_var("INITRAMFS_IMAGE"):
96                    kernel = "%s-%s.bin" % \
97                        (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
98
99            label = source_params.get('label')
100            label_conf = "root=%s" % creator.rootdev
101            if label:
102                label_conf = "LABEL=%s" % label
103
104            grubefi_conf += "linux /%s %s rootwait %s\n" \
105                % (kernel, label_conf, bootloader.append)
106
107            if initrd:
108                initrds = initrd.split(';')
109                grubefi_conf += "initrd"
110                for rd in initrds:
111                    grubefi_conf += " /%s" % rd
112                grubefi_conf += "\n"
113
114            if dtb:
115                grubefi_conf += "devicetree /%s\n" % dtb
116
117            grubefi_conf += "}\n"
118
119        logger.debug("Writing grubefi config %s/hdd/boot/EFI/BOOT/grub.cfg",
120                     cr_workdir)
121        cfg = open("%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir, "w")
122        cfg.write(grubefi_conf)
123        cfg.close()
124
125    @classmethod
126    def do_configure_systemdboot(cls, hdddir, creator, cr_workdir, source_params):
127        """
128        Create loader-specific systemd-boot/gummiboot config. Unified Kernel Image (uki)
129        support is done in image recipe with uki.bbclass and only systemd-boot loader config
130        and ESP partition structure is created here.
131        """
132        # detect uki.bbclass usage
133        image_classes = get_bitbake_var("IMAGE_CLASSES").split()
134        unified_image = False
135        if "uki" in image_classes:
136            unified_image = True
137
138        install_cmd = "install -d %s/loader" % hdddir
139        exec_cmd(install_cmd)
140
141        install_cmd = "install -d %s/loader/entries" % hdddir
142        exec_cmd(install_cmd)
143
144        bootloader = creator.ks.bootloader
145        loader_conf = ""
146
147        # 5 seconds is a sensible default timeout
148        loader_conf += "timeout %d\n" % (bootloader.timeout or 5)
149
150        logger.debug("Writing systemd-boot config "
151                     "%s/hdd/boot/loader/loader.conf", cr_workdir)
152        cfg = open("%s/hdd/boot/loader/loader.conf" % cr_workdir, "w")
153        cfg.write(loader_conf)
154        logger.debug("loader.conf:\n%s" % (loader_conf))
155        cfg.close()
156
157        initrd = source_params.get('initrd')
158        dtb = source_params.get('dtb')
159        if not unified_image:
160            cls._copy_additional_files(hdddir, initrd, dtb)
161
162        configfile = creator.ks.bootloader.configfile
163        custom_cfg = None
164        boot_conf = ""
165        if configfile:
166            custom_cfg = get_custom_config(configfile)
167            if custom_cfg:
168                # Use a custom configuration for systemd-boot
169                boot_conf = custom_cfg
170                logger.debug("Using custom configuration file "
171                             "%s for systemd-boots's boot.conf", configfile)
172            else:
173                raise WicError("configfile is specified but failed to "
174                               "get it from %s.", configfile)
175        else:
176            # Create systemd-boot configuration using parameters from wks file
177            kernel = get_bitbake_var("KERNEL_IMAGETYPE")
178            if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
179                if get_bitbake_var("INITRAMFS_IMAGE"):
180                    kernel = "%s-%s.bin" % \
181                        (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
182
183            title = source_params.get('title')
184
185            boot_conf += "title %s\n" % (title if title else "boot")
186            boot_conf += "linux /%s\n" % kernel
187
188            label = source_params.get('label')
189            label_conf = "LABEL=Boot root=%s" % creator.rootdev
190            if label:
191                label_conf = "LABEL=%s" % label
192
193            boot_conf += "options %s %s\n" % \
194                             (label_conf, bootloader.append)
195
196            if initrd:
197                initrds = initrd.split(';')
198                for rd in initrds:
199                    boot_conf += "initrd /%s\n" % rd
200
201            if dtb:
202                boot_conf += "devicetree /%s\n" % dtb
203
204        if not unified_image:
205            logger.debug("Writing systemd-boot config "
206                         "%s/hdd/boot/loader/entries/boot.conf", cr_workdir)
207            cfg = open("%s/hdd/boot/loader/entries/boot.conf" % cr_workdir, "w")
208            cfg.write(boot_conf)
209            logger.debug("boot.conf:\n%s" % (boot_conf))
210            cfg.close()
211
212
213    @classmethod
214    def do_configure_partition(cls, part, source_params, creator, cr_workdir,
215                               oe_builddir, bootimg_dir, kernel_dir,
216                               native_sysroot):
217        """
218        Called before do_prepare_partition(), creates loader-specific config
219        """
220        hdddir = "%s/hdd/boot" % cr_workdir
221
222        install_cmd = "install -d %s/EFI/BOOT" % hdddir
223        exec_cmd(install_cmd)
224
225        try:
226            if source_params['loader'] == 'grub-efi':
227                cls.do_configure_grubefi(hdddir, creator, cr_workdir, source_params)
228            elif source_params['loader'] == 'systemd-boot':
229                cls.do_configure_systemdboot(hdddir, creator, cr_workdir, source_params)
230            elif source_params['loader'] == 'uefi-kernel':
231                pass
232            else:
233                raise WicError("unrecognized bootimg-efi loader: %s" % source_params['loader'])
234        except KeyError:
235            raise WicError("bootimg-efi requires a loader, none specified")
236
237        if get_bitbake_var("IMAGE_EFI_BOOT_FILES") is None:
238            logger.debug('No boot files defined in IMAGE_EFI_BOOT_FILES')
239        else:
240            boot_files = None
241            for (fmt, id) in (("_uuid-%s", part.uuid), ("_label-%s", part.label), (None, None)):
242                if fmt:
243                    var = fmt % id
244                else:
245                    var = ""
246
247                boot_files = get_bitbake_var("IMAGE_EFI_BOOT_FILES" + var)
248                if boot_files:
249                    break
250
251            logger.debug('Boot files: %s', boot_files)
252
253            # list of tuples (src_name, dst_name)
254            deploy_files = []
255            for src_entry in re.findall(r'[\w;\-\./\*]+', boot_files):
256                if ';' in src_entry:
257                    dst_entry = tuple(src_entry.split(';'))
258                    if not dst_entry[0] or not dst_entry[1]:
259                        raise WicError('Malformed boot file entry: %s' % src_entry)
260                else:
261                    dst_entry = (src_entry, src_entry)
262
263                logger.debug('Destination entry: %r', dst_entry)
264                deploy_files.append(dst_entry)
265
266            cls.install_task = [];
267            for deploy_entry in deploy_files:
268                src, dst = deploy_entry
269                if '*' in src:
270                    # by default install files under their basename
271                    entry_name_fn = os.path.basename
272                    if dst != src:
273                        # unless a target name was given, then treat name
274                        # as a directory and append a basename
275                        entry_name_fn = lambda name: \
276                                        os.path.join(dst,
277                                                     os.path.basename(name))
278
279                    srcs = glob(os.path.join(kernel_dir, src))
280
281                    logger.debug('Globbed sources: %s', ', '.join(srcs))
282                    for entry in srcs:
283                        src = os.path.relpath(entry, kernel_dir)
284                        entry_dst_name = entry_name_fn(entry)
285                        cls.install_task.append((src, entry_dst_name))
286                else:
287                    cls.install_task.append((src, dst))
288
289    @classmethod
290    def do_prepare_partition(cls, part, source_params, creator, cr_workdir,
291                             oe_builddir, bootimg_dir, kernel_dir,
292                             rootfs_dir, native_sysroot):
293        """
294        Called to do the actual content population for a partition i.e. it
295        'prepares' the partition to be incorporated into the image.
296        In this case, prepare content for an EFI (grub) boot partition.
297        """
298        if not kernel_dir:
299            kernel_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
300            if not kernel_dir:
301                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
302
303        staging_kernel_dir = kernel_dir
304
305        hdddir = "%s/hdd/boot" % cr_workdir
306
307        kernel = get_bitbake_var("KERNEL_IMAGETYPE")
308        if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
309            if get_bitbake_var("INITRAMFS_IMAGE"):
310                kernel = "%s-%s.bin" % \
311                    (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
312
313        if source_params.get('create-unified-kernel-image') == "true":
314            raise WicError("create-unified-kernel-image is no longer supported. Please use uki.bbclass.")
315
316        if source_params.get('install-kernel-into-boot-dir') != 'false':
317            install_cmd = "install -v -p -m 0644 %s/%s %s/%s" % \
318                (staging_kernel_dir, kernel, hdddir, kernel)
319            out = exec_cmd(install_cmd)
320            logger.debug("Installed kernel files:\n%s" % out)
321
322        if get_bitbake_var("IMAGE_EFI_BOOT_FILES"):
323            for src_path, dst_path in cls.install_task:
324                install_cmd = "install -v -p -m 0644 -D %s %s" \
325                              % (os.path.join(kernel_dir, src_path),
326                                 os.path.join(hdddir, dst_path))
327                out = exec_cmd(install_cmd)
328                logger.debug("Installed IMAGE_EFI_BOOT_FILES:\n%s" % out)
329
330        try:
331            if source_params['loader'] == 'grub-efi':
332                shutil.copyfile("%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir,
333                                "%s/grub.cfg" % cr_workdir)
334                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("grub-efi-")]:
335                    cp_cmd = "cp -v -p %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[9:])
336                    exec_cmd(cp_cmd, True)
337                shutil.move("%s/grub.cfg" % cr_workdir,
338                            "%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir)
339            elif source_params['loader'] == 'systemd-boot':
340                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("systemd-")]:
341                    cp_cmd = "cp -v -p %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[8:])
342                    out = exec_cmd(cp_cmd, True)
343                    logger.debug("systemd-boot files:\n%s" % out)
344            elif source_params['loader'] == 'uefi-kernel':
345                kernel = get_bitbake_var("KERNEL_IMAGETYPE")
346                if not kernel:
347                    raise WicError("Empty KERNEL_IMAGETYPE")
348                target = get_bitbake_var("TARGET_SYS")
349                if not target:
350                    raise WicError("Empty TARGET_SYS")
351
352                if re.match("x86_64", target):
353                    kernel_efi_image = "bootx64.efi"
354                elif re.match('i.86', target):
355                    kernel_efi_image = "bootia32.efi"
356                elif re.match('aarch64', target):
357                    kernel_efi_image = "bootaa64.efi"
358                elif re.match('arm', target):
359                    kernel_efi_image = "bootarm.efi"
360                else:
361                    raise WicError("UEFI stub kernel is incompatible with target %s" % target)
362
363                for mod in [x for x in os.listdir(kernel_dir) if x.startswith(kernel)]:
364                    cp_cmd = "cp -v -p %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, kernel_efi_image)
365                    out = exec_cmd(cp_cmd, True)
366                    logger.debug("uefi-kernel files:\n%s" % out)
367            else:
368                raise WicError("unrecognized bootimg-efi loader: %s" %
369                               source_params['loader'])
370        except KeyError:
371            raise WicError("bootimg-efi requires a loader, none specified")
372
373        startup = os.path.join(kernel_dir, "startup.nsh")
374        if os.path.exists(startup):
375            cp_cmd = "cp -v -p %s %s/" % (startup, hdddir)
376            out = exec_cmd(cp_cmd, True)
377            logger.debug("startup files:\n%s" % out)
378
379        for paths in part.include_path or []:
380            for path in paths:
381                cp_cmd = "cp -v -p -r %s %s/" % (path, hdddir)
382                exec_cmd(cp_cmd, True)
383                logger.debug("include_path files:\n%s" % out)
384
385        du_cmd = "du -bks %s" % hdddir
386        out = exec_cmd(du_cmd)
387        blocks = int(out.split()[0])
388
389        extra_blocks = part.get_extra_block_count(blocks)
390
391        if extra_blocks < BOOTDD_EXTRA_SPACE:
392            extra_blocks = BOOTDD_EXTRA_SPACE
393
394        blocks += extra_blocks
395
396        logger.debug("Added %d extra blocks to %s to get to %d total blocks",
397                     extra_blocks, part.mountpoint, blocks)
398
399        # required for compatibility with certain devices expecting file system
400        # block count to be equal to partition block count
401        if blocks < part.fixed_size:
402            blocks = part.fixed_size
403            logger.debug("Overriding %s to %d total blocks for compatibility",
404                     part.mountpoint, blocks)
405
406        # dosfs image, created by mkdosfs
407        bootimg = "%s/boot.img" % cr_workdir
408
409        label = part.label if part.label else "ESP"
410
411        dosfs_cmd = "mkdosfs -v -n %s -i %s -C %s %d" % \
412                    (label, part.fsuuid, bootimg, blocks)
413        exec_native_cmd(dosfs_cmd, native_sysroot)
414        logger.debug("mkdosfs:\n%s" % (str(out)))
415
416        mcopy_cmd = "mcopy -v -p -i %s -s %s/* ::/" % (bootimg, hdddir)
417        out = exec_native_cmd(mcopy_cmd, native_sysroot)
418        logger.debug("mcopy:\n%s" % (str(out)))
419
420        chmod_cmd = "chmod 644 %s" % bootimg
421        exec_cmd(chmod_cmd)
422
423        du_cmd = "du -Lbks %s" % bootimg
424        out = exec_cmd(du_cmd)
425        bootimg_size = out.split()[0]
426
427        part.size = int(bootimg_size)
428        part.source_file = bootimg
429