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                # Searched by systemd-boot:
336                # https://systemd.io/BOOT_LOADER_SPECIFICATION/#type-2-efi-unified-kernel-images
337                install_cmd = "install -d %s/EFI/Linux" % hdddir
338                exec_cmd(install_cmd)
339
340                staging_dir_host = get_bitbake_var("STAGING_DIR_HOST")
341                target_sys = get_bitbake_var("TARGET_SYS")
342
343                objdump_cmd = "%s-objdump" % target_sys
344                objdump_cmd += " -p %s" % efi_stub
345                objdump_cmd += " | awk '{ if ($1 == \"SectionAlignment\"){print $2} }'"
346
347                ret, align_str = exec_native_cmd(objdump_cmd, native_sysroot)
348                align = int(align_str, 16)
349
350                objdump_cmd = "%s-objdump" % target_sys
351                objdump_cmd += " -h %s | tail -2" % efi_stub
352                ret, output = exec_native_cmd(objdump_cmd, native_sysroot)
353
354                offset = int(output.split()[2], 16) + int(output.split()[3], 16)
355
356                osrel_off = offset + align - offset % align
357                osrel_path = "%s/usr/lib/os-release" % staging_dir_host
358                osrel_sz = os.stat(osrel_path).st_size
359
360                cmdline_off = osrel_off + osrel_sz
361                cmdline_off = cmdline_off + align - cmdline_off % align
362                cmdline_sz = os.stat(cmdline.name).st_size
363
364                dtb_off = cmdline_off + cmdline_sz
365                dtb_off = dtb_off + align - dtb_off % align
366
367                dtb = source_params.get('dtb')
368                if dtb:
369                    if ';' in dtb:
370                        raise WicError("Only one DTB supported, exiting")
371                    dtb_path = "%s/%s" % (deploy_dir, dtb)
372                    dtb_params = '--add-section .dtb=%s --change-section-vma .dtb=0x%x' % \
373                            (dtb_path, dtb_off)
374                    linux_off = dtb_off + os.stat(dtb_path).st_size
375                    linux_off = linux_off + align - linux_off % align
376                else:
377                    dtb_params = ''
378                    linux_off = dtb_off
379
380                linux_path = "%s/%s" % (staging_kernel_dir, kernel)
381                linux_sz = os.stat(linux_path).st_size
382
383                initrd_off = linux_off + linux_sz
384                initrd_off = initrd_off + align - initrd_off % align
385
386                # https://www.freedesktop.org/software/systemd/man/systemd-stub.html
387                objcopy_cmd = "%s-objcopy" % target_sys
388                objcopy_cmd += " --enable-deterministic-archives"
389                objcopy_cmd += " --preserve-dates"
390                objcopy_cmd += " --add-section .osrel=%s" % osrel_path
391                objcopy_cmd += " --change-section-vma .osrel=0x%x" % osrel_off
392                objcopy_cmd += " --add-section .cmdline=%s" % cmdline.name
393                objcopy_cmd += " --change-section-vma .cmdline=0x%x" % cmdline_off
394                objcopy_cmd += dtb_params
395                objcopy_cmd += " --add-section .linux=%s" % linux_path
396                objcopy_cmd += " --change-section-vma .linux=0x%x" % linux_off
397                objcopy_cmd += " --add-section .initrd=%s" % initrd.name
398                objcopy_cmd += " --change-section-vma .initrd=0x%x" % initrd_off
399                objcopy_cmd += " %s %s/EFI/Linux/linux.efi" % (efi_stub, hdddir)
400
401                exec_native_cmd(objcopy_cmd, native_sysroot)
402        else:
403            install_cmd = "install -m 0644 %s/%s %s/%s" % \
404                (staging_kernel_dir, kernel, hdddir, kernel)
405            exec_cmd(install_cmd)
406
407        if get_bitbake_var("IMAGE_EFI_BOOT_FILES"):
408            for src_path, dst_path in cls.install_task:
409                install_cmd = "install -m 0644 -D %s %s" \
410                              % (os.path.join(kernel_dir, src_path),
411                                 os.path.join(hdddir, dst_path))
412                exec_cmd(install_cmd)
413
414        try:
415            if source_params['loader'] == 'grub-efi':
416                shutil.copyfile("%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir,
417                                "%s/grub.cfg" % cr_workdir)
418                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("grub-efi-")]:
419                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[9:])
420                    exec_cmd(cp_cmd, True)
421                shutil.move("%s/grub.cfg" % cr_workdir,
422                            "%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir)
423            elif source_params['loader'] == 'systemd-boot':
424                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("systemd-")]:
425                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[8:])
426                    exec_cmd(cp_cmd, True)
427            elif source_params['loader'] == 'uefi-kernel':
428                kernel = get_bitbake_var("KERNEL_IMAGETYPE")
429                if not kernel:
430                    raise WicError("Empty KERNEL_IMAGETYPE %s\n" % target)
431                target = get_bitbake_var("TARGET_SYS")
432                if not target:
433                    raise WicError("Unknown arch (TARGET_SYS) %s\n" % target)
434
435                if re.match("x86_64", target):
436                    kernel_efi_image = "bootx64.efi"
437                elif re.match('i.86', target):
438                    kernel_efi_image = "bootia32.efi"
439                elif re.match('aarch64', target):
440                    kernel_efi_image = "bootaa64.efi"
441                elif re.match('arm', target):
442                    kernel_efi_image = "bootarm.efi"
443                else:
444                    raise WicError("UEFI stub kernel is incompatible with target %s" % target)
445
446                for mod in [x for x in os.listdir(kernel_dir) if x.startswith(kernel)]:
447                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, kernel_efi_image)
448                    exec_cmd(cp_cmd, True)
449            else:
450                raise WicError("unrecognized bootimg-efi loader: %s" %
451                               source_params['loader'])
452        except KeyError:
453            raise WicError("bootimg-efi requires a loader, none specified")
454
455        startup = os.path.join(kernel_dir, "startup.nsh")
456        if os.path.exists(startup):
457            cp_cmd = "cp %s %s/" % (startup, hdddir)
458            exec_cmd(cp_cmd, True)
459
460        for paths in part.include_path or []:
461            for path in paths:
462                cp_cmd = "cp -r %s %s/" % (path, hdddir)
463                exec_cmd(cp_cmd, True)
464
465        du_cmd = "du -bks %s" % hdddir
466        out = exec_cmd(du_cmd)
467        blocks = int(out.split()[0])
468
469        extra_blocks = part.get_extra_block_count(blocks)
470
471        if extra_blocks < BOOTDD_EXTRA_SPACE:
472            extra_blocks = BOOTDD_EXTRA_SPACE
473
474        blocks += extra_blocks
475
476        logger.debug("Added %d extra blocks to %s to get to %d total blocks",
477                     extra_blocks, part.mountpoint, blocks)
478
479        # required for compatibility with certain devices expecting file system
480        # block count to be equal to partition block count
481        if blocks < part.fixed_size:
482            blocks = part.fixed_size
483            logger.debug("Overriding %s to %d total blocks for compatibility",
484                     part.mountpoint, blocks)
485
486        # dosfs image, created by mkdosfs
487        bootimg = "%s/boot.img" % cr_workdir
488
489        label = part.label if part.label else "ESP"
490
491        dosfs_cmd = "mkdosfs -n %s -i %s -C %s %d" % \
492                    (label, part.fsuuid, bootimg, blocks)
493        exec_native_cmd(dosfs_cmd, native_sysroot)
494
495        mcopy_cmd = "mcopy -i %s -s %s/* ::/" % (bootimg, hdddir)
496        exec_native_cmd(mcopy_cmd, native_sysroot)
497
498        chmod_cmd = "chmod 644 %s" % bootimg
499        exec_cmd(chmod_cmd)
500
501        du_cmd = "du -Lbks %s" % bootimg
502        out = exec_cmd(du_cmd)
503        bootimg_size = out.split()[0]
504
505        part.size = int(bootimg_size)
506        part.source_file = bootimg
507