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 do_configure_grubefi(cls, hdddir, creator, cr_workdir, source_params):
39        """
40        Create loader-specific (grub-efi) config
41        """
42        configfile = creator.ks.bootloader.configfile
43        custom_cfg = None
44        if configfile:
45            custom_cfg = get_custom_config(configfile)
46            if custom_cfg:
47                # Use a custom configuration for grub
48                grubefi_conf = custom_cfg
49                logger.debug("Using custom configuration file "
50                             "%s for grub.cfg", configfile)
51            else:
52                raise WicError("configfile is specified but failed to "
53                               "get it from %s." % configfile)
54
55        initrd = source_params.get('initrd')
56
57        if initrd:
58            bootimg_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
59            if not bootimg_dir:
60                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
61
62            initrds = initrd.split(';')
63            for rd in initrds:
64                cp_cmd = "cp %s/%s %s" % (bootimg_dir, rd, hdddir)
65                exec_cmd(cp_cmd, True)
66        else:
67            logger.debug("Ignoring missing initrd")
68
69        if not custom_cfg:
70            # Create grub configuration using parameters from wks file
71            bootloader = creator.ks.bootloader
72            title = source_params.get('title')
73
74            grubefi_conf = ""
75            grubefi_conf += "serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1\n"
76            grubefi_conf += "default=boot\n"
77            grubefi_conf += "timeout=%s\n" % bootloader.timeout
78            grubefi_conf += "menuentry '%s'{\n" % (title if title else "boot")
79
80            kernel = get_bitbake_var("KERNEL_IMAGETYPE")
81            if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
82                if get_bitbake_var("INITRAMFS_IMAGE"):
83                    kernel = "%s-%s.bin" % \
84                        (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
85
86            label = source_params.get('label')
87            label_conf = "root=%s" % creator.rootdev
88            if label:
89                label_conf = "LABEL=%s" % label
90
91            grubefi_conf += "linux /%s %s rootwait %s\n" \
92                % (kernel, label_conf, bootloader.append)
93
94            if initrd:
95                initrds = initrd.split(';')
96                grubefi_conf += "initrd"
97                for rd in initrds:
98                    grubefi_conf += " /%s" % rd
99                grubefi_conf += "\n"
100
101            grubefi_conf += "}\n"
102
103        logger.debug("Writing grubefi config %s/hdd/boot/EFI/BOOT/grub.cfg",
104                     cr_workdir)
105        cfg = open("%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir, "w")
106        cfg.write(grubefi_conf)
107        cfg.close()
108
109    @classmethod
110    def do_configure_systemdboot(cls, hdddir, creator, cr_workdir, source_params):
111        """
112        Create loader-specific systemd-boot/gummiboot config
113        """
114        install_cmd = "install -d %s/loader" % hdddir
115        exec_cmd(install_cmd)
116
117        install_cmd = "install -d %s/loader/entries" % hdddir
118        exec_cmd(install_cmd)
119
120        bootloader = creator.ks.bootloader
121
122        loader_conf = ""
123        if source_params.get('create-unified-kernel-image') != "true":
124            loader_conf += "default boot\n"
125        loader_conf += "timeout %d\n" % bootloader.timeout
126
127        initrd = source_params.get('initrd')
128
129        if initrd and source_params.get('create-unified-kernel-image') != "true":
130            # obviously we need to have a common common deploy var
131            bootimg_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
132            if not bootimg_dir:
133                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
134
135            initrds = initrd.split(';')
136            for rd in initrds:
137                cp_cmd = "cp %s/%s %s" % (bootimg_dir, rd, hdddir)
138                exec_cmd(cp_cmd, True)
139        else:
140            logger.debug("Ignoring missing initrd")
141
142        logger.debug("Writing systemd-boot config "
143                     "%s/hdd/boot/loader/loader.conf", cr_workdir)
144        cfg = open("%s/hdd/boot/loader/loader.conf" % cr_workdir, "w")
145        cfg.write(loader_conf)
146        cfg.close()
147
148        configfile = creator.ks.bootloader.configfile
149        custom_cfg = None
150        if configfile:
151            custom_cfg = get_custom_config(configfile)
152            if custom_cfg:
153                # Use a custom configuration for systemd-boot
154                boot_conf = custom_cfg
155                logger.debug("Using custom configuration file "
156                             "%s for systemd-boots's boot.conf", configfile)
157            else:
158                raise WicError("configfile is specified but failed to "
159                               "get it from %s.", configfile)
160
161        if not custom_cfg:
162            # Create systemd-boot configuration using parameters from wks file
163            kernel = get_bitbake_var("KERNEL_IMAGETYPE")
164            if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
165                if get_bitbake_var("INITRAMFS_IMAGE"):
166                    kernel = "%s-%s.bin" % \
167                        (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
168
169            title = source_params.get('title')
170
171            boot_conf = ""
172            boot_conf += "title %s\n" % (title if title else "boot")
173            boot_conf += "linux /%s\n" % kernel
174
175            label = source_params.get('label')
176            label_conf = "LABEL=Boot root=%s" % creator.rootdev
177            if label:
178                label_conf = "LABEL=%s" % label
179
180            boot_conf += "options %s %s\n" % \
181                             (label_conf, bootloader.append)
182
183            if initrd:
184                initrds = initrd.split(';')
185                for rd in initrds:
186                    boot_conf += "initrd /%s\n" % rd
187
188        if source_params.get('create-unified-kernel-image') != "true":
189            logger.debug("Writing systemd-boot config "
190                         "%s/hdd/boot/loader/entries/boot.conf", cr_workdir)
191            cfg = open("%s/hdd/boot/loader/entries/boot.conf" % cr_workdir, "w")
192            cfg.write(boot_conf)
193            cfg.close()
194
195
196    @classmethod
197    def do_configure_partition(cls, part, source_params, creator, cr_workdir,
198                               oe_builddir, bootimg_dir, kernel_dir,
199                               native_sysroot):
200        """
201        Called before do_prepare_partition(), creates loader-specific config
202        """
203        hdddir = "%s/hdd/boot" % cr_workdir
204
205        install_cmd = "install -d %s/EFI/BOOT" % hdddir
206        exec_cmd(install_cmd)
207
208        try:
209            if source_params['loader'] == 'grub-efi':
210                cls.do_configure_grubefi(hdddir, creator, cr_workdir, source_params)
211            elif source_params['loader'] == 'systemd-boot':
212                cls.do_configure_systemdboot(hdddir, creator, cr_workdir, source_params)
213            else:
214                raise WicError("unrecognized bootimg-efi loader: %s" % source_params['loader'])
215        except KeyError:
216            raise WicError("bootimg-efi requires a loader, none specified")
217
218        if get_bitbake_var("IMAGE_EFI_BOOT_FILES") is None:
219            logger.debug('No boot files defined in IMAGE_EFI_BOOT_FILES')
220        else:
221            boot_files = None
222            for (fmt, id) in (("_uuid-%s", part.uuid), ("_label-%s", part.label), (None, None)):
223                if fmt:
224                    var = fmt % id
225                else:
226                    var = ""
227
228                boot_files = get_bitbake_var("IMAGE_EFI_BOOT_FILES" + var)
229                if boot_files:
230                    break
231
232            logger.debug('Boot files: %s', boot_files)
233
234            # list of tuples (src_name, dst_name)
235            deploy_files = []
236            for src_entry in re.findall(r'[\w;\-\./\*]+', boot_files):
237                if ';' in src_entry:
238                    dst_entry = tuple(src_entry.split(';'))
239                    if not dst_entry[0] or not dst_entry[1]:
240                        raise WicError('Malformed boot file entry: %s' % src_entry)
241                else:
242                    dst_entry = (src_entry, src_entry)
243
244                logger.debug('Destination entry: %r', dst_entry)
245                deploy_files.append(dst_entry)
246
247            cls.install_task = [];
248            for deploy_entry in deploy_files:
249                src, dst = deploy_entry
250                if '*' in src:
251                    # by default install files under their basename
252                    entry_name_fn = os.path.basename
253                    if dst != src:
254                        # unless a target name was given, then treat name
255                        # as a directory and append a basename
256                        entry_name_fn = lambda name: \
257                                        os.path.join(dst,
258                                                     os.path.basename(name))
259
260                    srcs = glob(os.path.join(kernel_dir, src))
261
262                    logger.debug('Globbed sources: %s', ', '.join(srcs))
263                    for entry in srcs:
264                        src = os.path.relpath(entry, kernel_dir)
265                        entry_dst_name = entry_name_fn(entry)
266                        cls.install_task.append((src, entry_dst_name))
267                else:
268                    cls.install_task.append((src, dst))
269
270    @classmethod
271    def do_prepare_partition(cls, part, source_params, creator, cr_workdir,
272                             oe_builddir, bootimg_dir, kernel_dir,
273                             rootfs_dir, native_sysroot):
274        """
275        Called to do the actual content population for a partition i.e. it
276        'prepares' the partition to be incorporated into the image.
277        In this case, prepare content for an EFI (grub) boot partition.
278        """
279        if not kernel_dir:
280            kernel_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
281            if not kernel_dir:
282                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
283
284        staging_kernel_dir = kernel_dir
285
286        hdddir = "%s/hdd/boot" % cr_workdir
287
288        kernel = get_bitbake_var("KERNEL_IMAGETYPE")
289        if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
290            if get_bitbake_var("INITRAMFS_IMAGE"):
291                kernel = "%s-%s.bin" % \
292                    (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
293
294        if source_params.get('create-unified-kernel-image') == "true":
295            initrd = source_params.get('initrd')
296            if not initrd:
297                raise WicError("initrd= must be specified when create-unified-kernel-image=true, exiting")
298
299            deploy_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
300            efi_stub = glob("%s/%s" % (deploy_dir, "linux*.efi.stub"))
301            if len(efi_stub) == 0:
302                raise WicError("Unified Kernel Image EFI stub not found, exiting")
303            efi_stub = efi_stub[0]
304
305            with tempfile.TemporaryDirectory() as tmp_dir:
306                label = source_params.get('label')
307                label_conf = "root=%s" % creator.rootdev
308                if label:
309                    label_conf = "LABEL=%s" % label
310
311                bootloader = creator.ks.bootloader
312                cmdline = open("%s/cmdline" % tmp_dir, "w")
313                cmdline.write("%s %s" % (label_conf, bootloader.append))
314                cmdline.close()
315
316                initrds = initrd.split(';')
317                initrd = open("%s/initrd" % tmp_dir, "wb")
318                for f in initrds:
319                    with open("%s/%s" % (deploy_dir, f), 'rb') as in_file:
320                        shutil.copyfileobj(in_file, initrd)
321                initrd.close()
322
323                # Searched by systemd-boot:
324                # https://systemd.io/BOOT_LOADER_SPECIFICATION/#type-2-efi-unified-kernel-images
325                install_cmd = "install -d %s/EFI/Linux" % hdddir
326                exec_cmd(install_cmd)
327
328                staging_dir_host = get_bitbake_var("STAGING_DIR_HOST")
329
330                # https://www.freedesktop.org/software/systemd/man/systemd-stub.html
331                objcopy_cmd = "objcopy \
332                    --add-section .osrel=%s --change-section-vma .osrel=0x20000 \
333                    --add-section .cmdline=%s --change-section-vma .cmdline=0x30000 \
334                    --add-section .linux=%s --change-section-vma .linux=0x2000000 \
335                    --add-section .initrd=%s --change-section-vma .initrd=0x3000000 \
336                    %s %s" % \
337                    ("%s/usr/lib/os-release" % staging_dir_host,
338                    cmdline.name,
339                    "%s/%s" % (staging_kernel_dir, kernel),
340                    initrd.name,
341                    efi_stub,
342                    "%s/EFI/Linux/linux.efi" % hdddir)
343                exec_cmd(objcopy_cmd)
344        else:
345            install_cmd = "install -m 0644 %s/%s %s/%s" % \
346                (staging_kernel_dir, kernel, hdddir, kernel)
347            exec_cmd(install_cmd)
348
349        if get_bitbake_var("IMAGE_EFI_BOOT_FILES"):
350            for src_path, dst_path in cls.install_task:
351                install_cmd = "install -m 0644 -D %s %s" \
352                              % (os.path.join(kernel_dir, src_path),
353                                 os.path.join(hdddir, dst_path))
354                exec_cmd(install_cmd)
355
356        try:
357            if source_params['loader'] == 'grub-efi':
358                shutil.copyfile("%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir,
359                                "%s/grub.cfg" % cr_workdir)
360                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("grub-efi-")]:
361                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[9:])
362                    exec_cmd(cp_cmd, True)
363                shutil.move("%s/grub.cfg" % cr_workdir,
364                            "%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir)
365            elif source_params['loader'] == 'systemd-boot':
366                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("systemd-")]:
367                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[8:])
368                    exec_cmd(cp_cmd, True)
369            else:
370                raise WicError("unrecognized bootimg-efi loader: %s" %
371                               source_params['loader'])
372        except KeyError:
373            raise WicError("bootimg-efi requires a loader, none specified")
374
375        startup = os.path.join(kernel_dir, "startup.nsh")
376        if os.path.exists(startup):
377            cp_cmd = "cp %s %s/" % (startup, hdddir)
378            exec_cmd(cp_cmd, True)
379
380        du_cmd = "du -bks %s" % hdddir
381        out = exec_cmd(du_cmd)
382        blocks = int(out.split()[0])
383
384        extra_blocks = part.get_extra_block_count(blocks)
385
386        if extra_blocks < BOOTDD_EXTRA_SPACE:
387            extra_blocks = BOOTDD_EXTRA_SPACE
388
389        blocks += extra_blocks
390
391        logger.debug("Added %d extra blocks to %s to get to %d total blocks",
392                     extra_blocks, part.mountpoint, blocks)
393
394        # dosfs image, created by mkdosfs
395        bootimg = "%s/boot.img" % cr_workdir
396
397        label = part.label if part.label else "ESP"
398
399        dosfs_cmd = "mkdosfs -n %s -i %s -C %s %d" % \
400                    (label, part.fsuuid, bootimg, blocks)
401        exec_native_cmd(dosfs_cmd, native_sysroot)
402
403        mcopy_cmd = "mcopy -i %s -s %s/* ::/" % (bootimg, hdddir)
404        exec_native_cmd(mcopy_cmd, native_sysroot)
405
406        chmod_cmd = "chmod 644 %s" % bootimg
407        exec_cmd(chmod_cmd)
408
409        du_cmd = "du -Lbks %s" % bootimg
410        out = exec_cmd(du_cmd)
411        bootimg_size = out.split()[0]
412
413        part.size = int(bootimg_size)
414        part.source_file = bootimg
415