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