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            if source_params.get('install-kernel-into-boot-dir') != 'false':
404                install_cmd = "install -m 0644 %s/%s %s/%s" % \
405                    (staging_kernel_dir, kernel, hdddir, kernel)
406                exec_cmd(install_cmd)
407
408        if get_bitbake_var("IMAGE_EFI_BOOT_FILES"):
409            for src_path, dst_path in cls.install_task:
410                install_cmd = "install -m 0644 -D %s %s" \
411                              % (os.path.join(kernel_dir, src_path),
412                                 os.path.join(hdddir, dst_path))
413                exec_cmd(install_cmd)
414
415        try:
416            if source_params['loader'] == 'grub-efi':
417                shutil.copyfile("%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir,
418                                "%s/grub.cfg" % cr_workdir)
419                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("grub-efi-")]:
420                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[9:])
421                    exec_cmd(cp_cmd, True)
422                shutil.move("%s/grub.cfg" % cr_workdir,
423                            "%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir)
424            elif source_params['loader'] == 'systemd-boot':
425                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("systemd-")]:
426                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[8:])
427                    exec_cmd(cp_cmd, True)
428            elif source_params['loader'] == 'uefi-kernel':
429                kernel = get_bitbake_var("KERNEL_IMAGETYPE")
430                if not kernel:
431                    raise WicError("Empty KERNEL_IMAGETYPE %s\n" % target)
432                target = get_bitbake_var("TARGET_SYS")
433                if not target:
434                    raise WicError("Unknown arch (TARGET_SYS) %s\n" % target)
435
436                if re.match("x86_64", target):
437                    kernel_efi_image = "bootx64.efi"
438                elif re.match('i.86', target):
439                    kernel_efi_image = "bootia32.efi"
440                elif re.match('aarch64', target):
441                    kernel_efi_image = "bootaa64.efi"
442                elif re.match('arm', target):
443                    kernel_efi_image = "bootarm.efi"
444                else:
445                    raise WicError("UEFI stub kernel is incompatible with target %s" % target)
446
447                for mod in [x for x in os.listdir(kernel_dir) if x.startswith(kernel)]:
448                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, kernel_efi_image)
449                    exec_cmd(cp_cmd, True)
450            else:
451                raise WicError("unrecognized bootimg-efi loader: %s" %
452                               source_params['loader'])
453        except KeyError:
454            raise WicError("bootimg-efi requires a loader, none specified")
455
456        startup = os.path.join(kernel_dir, "startup.nsh")
457        if os.path.exists(startup):
458            cp_cmd = "cp %s %s/" % (startup, hdddir)
459            exec_cmd(cp_cmd, True)
460
461        for paths in part.include_path or []:
462            for path in paths:
463                cp_cmd = "cp -r %s %s/" % (path, hdddir)
464                exec_cmd(cp_cmd, True)
465
466        du_cmd = "du -bks %s" % hdddir
467        out = exec_cmd(du_cmd)
468        blocks = int(out.split()[0])
469
470        extra_blocks = part.get_extra_block_count(blocks)
471
472        if extra_blocks < BOOTDD_EXTRA_SPACE:
473            extra_blocks = BOOTDD_EXTRA_SPACE
474
475        blocks += extra_blocks
476
477        logger.debug("Added %d extra blocks to %s to get to %d total blocks",
478                     extra_blocks, part.mountpoint, blocks)
479
480        # required for compatibility with certain devices expecting file system
481        # block count to be equal to partition block count
482        if blocks < part.fixed_size:
483            blocks = part.fixed_size
484            logger.debug("Overriding %s to %d total blocks for compatibility",
485                     part.mountpoint, blocks)
486
487        # dosfs image, created by mkdosfs
488        bootimg = "%s/boot.img" % cr_workdir
489
490        label = part.label if part.label else "ESP"
491
492        dosfs_cmd = "mkdosfs -n %s -i %s -C %s %d" % \
493                    (label, part.fsuuid, bootimg, blocks)
494        exec_native_cmd(dosfs_cmd, native_sysroot)
495
496        mcopy_cmd = "mcopy -i %s -s %s/* ::/" % (bootimg, hdddir)
497        exec_native_cmd(mcopy_cmd, native_sysroot)
498
499        chmod_cmd = "chmod 644 %s" % bootimg
500        exec_cmd(chmod_cmd)
501
502        du_cmd = "du -Lbks %s" % bootimg
503        out = exec_cmd(du_cmd)
504        bootimg_size = out.split()[0]
505
506        part.size = int(bootimg_size)
507        part.source_file = bootimg
508