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                return
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 += " --add-section .osrel=%s/usr/lib/os-release" % staging_dir_host
355                objcopy_cmd += " --change-section-vma .osrel=0x20000"
356                objcopy_cmd += " --add-section .cmdline=%s" % cmdline.name
357                objcopy_cmd += " --change-section-vma .cmdline=0x30000"
358                objcopy_cmd += dtb_params
359                objcopy_cmd += " --add-section .linux=%s/%s" % (staging_kernel_dir, kernel)
360                objcopy_cmd += " --change-section-vma .linux=0x2000000"
361                objcopy_cmd += " --add-section .initrd=%s" % initrd.name
362                objcopy_cmd += " --change-section-vma .initrd=0x3000000"
363                objcopy_cmd += " %s %s/EFI/Linux/linux.efi" % (efi_stub, hdddir)
364                exec_native_cmd(objcopy_cmd, native_sysroot)
365        else:
366            install_cmd = "install -m 0644 %s/%s %s/%s" % \
367                (staging_kernel_dir, kernel, hdddir, kernel)
368            exec_cmd(install_cmd)
369
370        if get_bitbake_var("IMAGE_EFI_BOOT_FILES"):
371            for src_path, dst_path in cls.install_task:
372                install_cmd = "install -m 0644 -D %s %s" \
373                              % (os.path.join(kernel_dir, src_path),
374                                 os.path.join(hdddir, dst_path))
375                exec_cmd(install_cmd)
376
377        try:
378            if source_params['loader'] == 'grub-efi':
379                shutil.copyfile("%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir,
380                                "%s/grub.cfg" % cr_workdir)
381                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("grub-efi-")]:
382                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[9:])
383                    exec_cmd(cp_cmd, True)
384                shutil.move("%s/grub.cfg" % cr_workdir,
385                            "%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir)
386            elif source_params['loader'] == 'systemd-boot':
387                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("systemd-")]:
388                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[8:])
389                    exec_cmd(cp_cmd, True)
390            elif source_params['loader'] == 'uefi-kernel':
391                kernel = get_bitbake_var("KERNEL_IMAGETYPE")
392                if not kernel:
393                    raise WicError("Empty KERNEL_IMAGETYPE %s\n" % target)
394                target = get_bitbake_var("TARGET_SYS")
395                if not target:
396                    raise WicError("Unknown arch (TARGET_SYS) %s\n" % target)
397
398                if re.match("x86_64", target):
399                    kernel_efi_image = "bootx64.efi"
400                elif re.match('i.86', target):
401                    kernel_efi_image = "bootia32.efi"
402                elif re.match('aarch64', target):
403                    kernel_efi_image = "bootaa64.efi"
404                elif re.match('arm', target):
405                    kernel_efi_image = "bootarm.efi"
406                else:
407                    raise WicError("UEFI stub kernel is incompatible with target %s" % target)
408
409                for mod in [x for x in os.listdir(kernel_dir) if x.startswith(kernel)]:
410                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, kernel_efi_image)
411                    exec_cmd(cp_cmd, True)
412            else:
413                raise WicError("unrecognized bootimg-efi loader: %s" %
414                               source_params['loader'])
415        except KeyError:
416            raise WicError("bootimg-efi requires a loader, none specified")
417
418        startup = os.path.join(kernel_dir, "startup.nsh")
419        if os.path.exists(startup):
420            cp_cmd = "cp %s %s/" % (startup, hdddir)
421            exec_cmd(cp_cmd, True)
422
423        for paths in part.include_path or []:
424            for path in paths:
425                cp_cmd = "cp -r %s %s/" % (path, hdddir)
426                exec_cmd(cp_cmd, True)
427
428        du_cmd = "du -bks %s" % hdddir
429        out = exec_cmd(du_cmd)
430        blocks = int(out.split()[0])
431
432        extra_blocks = part.get_extra_block_count(blocks)
433
434        if extra_blocks < BOOTDD_EXTRA_SPACE:
435            extra_blocks = BOOTDD_EXTRA_SPACE
436
437        blocks += extra_blocks
438
439        logger.debug("Added %d extra blocks to %s to get to %d total blocks",
440                     extra_blocks, part.mountpoint, blocks)
441
442        # dosfs image, created by mkdosfs
443        bootimg = "%s/boot.img" % cr_workdir
444
445        label = part.label if part.label else "ESP"
446
447        dosfs_cmd = "mkdosfs -n %s -i %s -C %s %d" % \
448                    (label, part.fsuuid, bootimg, blocks)
449        exec_native_cmd(dosfs_cmd, native_sysroot)
450
451        mcopy_cmd = "mcopy -i %s -s %s/* ::/" % (bootimg, hdddir)
452        exec_native_cmd(mcopy_cmd, native_sysroot)
453
454        chmod_cmd = "chmod 644 %s" % bootimg
455        exec_cmd(chmod_cmd)
456
457        du_cmd = "du -Lbks %s" % bootimg
458        out = exec_cmd(du_cmd)
459        bootimg_size = out.split()[0]
460
461        part.size = int(bootimg_size)
462        part.source_file = bootimg
463