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            else:
224                raise WicError("unrecognized bootimg-efi loader: %s" % source_params['loader'])
225        except KeyError:
226            raise WicError("bootimg-efi requires a loader, none specified")
227
228        if get_bitbake_var("IMAGE_EFI_BOOT_FILES") is None:
229            logger.debug('No boot files defined in IMAGE_EFI_BOOT_FILES')
230        else:
231            boot_files = None
232            for (fmt, id) in (("_uuid-%s", part.uuid), ("_label-%s", part.label), (None, None)):
233                if fmt:
234                    var = fmt % id
235                else:
236                    var = ""
237
238                boot_files = get_bitbake_var("IMAGE_EFI_BOOT_FILES" + var)
239                if boot_files:
240                    break
241
242            logger.debug('Boot files: %s', boot_files)
243
244            # list of tuples (src_name, dst_name)
245            deploy_files = []
246            for src_entry in re.findall(r'[\w;\-\./\*]+', boot_files):
247                if ';' in src_entry:
248                    dst_entry = tuple(src_entry.split(';'))
249                    if not dst_entry[0] or not dst_entry[1]:
250                        raise WicError('Malformed boot file entry: %s' % src_entry)
251                else:
252                    dst_entry = (src_entry, src_entry)
253
254                logger.debug('Destination entry: %r', dst_entry)
255                deploy_files.append(dst_entry)
256
257            cls.install_task = [];
258            for deploy_entry in deploy_files:
259                src, dst = deploy_entry
260                if '*' in src:
261                    # by default install files under their basename
262                    entry_name_fn = os.path.basename
263                    if dst != src:
264                        # unless a target name was given, then treat name
265                        # as a directory and append a basename
266                        entry_name_fn = lambda name: \
267                                        os.path.join(dst,
268                                                     os.path.basename(name))
269
270                    srcs = glob(os.path.join(kernel_dir, src))
271
272                    logger.debug('Globbed sources: %s', ', '.join(srcs))
273                    for entry in srcs:
274                        src = os.path.relpath(entry, kernel_dir)
275                        entry_dst_name = entry_name_fn(entry)
276                        cls.install_task.append((src, entry_dst_name))
277                else:
278                    cls.install_task.append((src, dst))
279
280    @classmethod
281    def do_prepare_partition(cls, part, source_params, creator, cr_workdir,
282                             oe_builddir, bootimg_dir, kernel_dir,
283                             rootfs_dir, native_sysroot):
284        """
285        Called to do the actual content population for a partition i.e. it
286        'prepares' the partition to be incorporated into the image.
287        In this case, prepare content for an EFI (grub) boot partition.
288        """
289        if not kernel_dir:
290            kernel_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
291            if not kernel_dir:
292                raise WicError("Couldn't find DEPLOY_DIR_IMAGE, exiting")
293
294        staging_kernel_dir = kernel_dir
295
296        hdddir = "%s/hdd/boot" % cr_workdir
297
298        kernel = get_bitbake_var("KERNEL_IMAGETYPE")
299        if get_bitbake_var("INITRAMFS_IMAGE_BUNDLE") == "1":
300            if get_bitbake_var("INITRAMFS_IMAGE"):
301                kernel = "%s-%s.bin" % \
302                    (get_bitbake_var("KERNEL_IMAGETYPE"), get_bitbake_var("INITRAMFS_LINK_NAME"))
303
304        if source_params.get('create-unified-kernel-image') == "true":
305            initrd = source_params.get('initrd')
306            if not initrd:
307                raise WicError("initrd= must be specified when create-unified-kernel-image=true, exiting")
308
309            deploy_dir = get_bitbake_var("DEPLOY_DIR_IMAGE")
310            efi_stub = glob("%s/%s" % (deploy_dir, "linux*.efi.stub"))
311            if len(efi_stub) == 0:
312                raise WicError("Unified Kernel Image EFI stub not found, exiting")
313            efi_stub = efi_stub[0]
314
315            with tempfile.TemporaryDirectory() as tmp_dir:
316                label = source_params.get('label')
317                label_conf = "root=%s" % creator.rootdev
318                if label:
319                    label_conf = "LABEL=%s" % label
320
321                bootloader = creator.ks.bootloader
322                cmdline = open("%s/cmdline" % tmp_dir, "w")
323                cmdline.write("%s %s" % (label_conf, bootloader.append))
324                cmdline.close()
325
326                initrds = initrd.split(';')
327                initrd = open("%s/initrd" % tmp_dir, "wb")
328                for f in initrds:
329                    with open("%s/%s" % (deploy_dir, f), 'rb') as in_file:
330                        shutil.copyfileobj(in_file, initrd)
331                initrd.close()
332
333                dtb = source_params.get('dtb')
334                if dtb:
335                    if ';' in dtb:
336                        raise WicError("Only one DTB supported, exiting")
337                    dtb_params = '--add-section .dtb=%s/%s --change-section-vma .dtb=0x40000' % \
338                        (deploy_dir, dtb)
339                else:
340                    dtb_params = ''
341
342                # Searched by systemd-boot:
343                # https://systemd.io/BOOT_LOADER_SPECIFICATION/#type-2-efi-unified-kernel-images
344                install_cmd = "install -d %s/EFI/Linux" % hdddir
345                exec_cmd(install_cmd)
346
347                staging_dir_host = get_bitbake_var("STAGING_DIR_HOST")
348                target_sys = get_bitbake_var("TARGET_SYS")
349
350                # https://www.freedesktop.org/software/systemd/man/systemd-stub.html
351                objcopy_cmd = "%s-objcopy" % target_sys
352                objcopy_cmd += " --add-section .osrel=%s/usr/lib/os-release" % staging_dir_host
353                objcopy_cmd += " --change-section-vma .osrel=0x20000"
354                objcopy_cmd += " --add-section .cmdline=%s" % cmdline.name
355                objcopy_cmd += " --change-section-vma .cmdline=0x30000"
356                objcopy_cmd += dtb_params
357                objcopy_cmd += " --add-section .linux=%s/%s" % (staging_kernel_dir, kernel)
358                objcopy_cmd += " --change-section-vma .linux=0x2000000"
359                objcopy_cmd += " --add-section .initrd=%s" % initrd.name
360                objcopy_cmd += " --change-section-vma .initrd=0x3000000"
361                objcopy_cmd += " %s %s/EFI/Linux/linux.efi" % (efi_stub, hdddir)
362                exec_native_cmd(objcopy_cmd, native_sysroot)
363        else:
364            install_cmd = "install -m 0644 %s/%s %s/%s" % \
365                (staging_kernel_dir, kernel, hdddir, kernel)
366            exec_cmd(install_cmd)
367
368        if get_bitbake_var("IMAGE_EFI_BOOT_FILES"):
369            for src_path, dst_path in cls.install_task:
370                install_cmd = "install -m 0644 -D %s %s" \
371                              % (os.path.join(kernel_dir, src_path),
372                                 os.path.join(hdddir, dst_path))
373                exec_cmd(install_cmd)
374
375        try:
376            if source_params['loader'] == 'grub-efi':
377                shutil.copyfile("%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir,
378                                "%s/grub.cfg" % cr_workdir)
379                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("grub-efi-")]:
380                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[9:])
381                    exec_cmd(cp_cmd, True)
382                shutil.move("%s/grub.cfg" % cr_workdir,
383                            "%s/hdd/boot/EFI/BOOT/grub.cfg" % cr_workdir)
384            elif source_params['loader'] == 'systemd-boot':
385                for mod in [x for x in os.listdir(kernel_dir) if x.startswith("systemd-")]:
386                    cp_cmd = "cp %s/%s %s/EFI/BOOT/%s" % (kernel_dir, mod, hdddir, mod[8:])
387                    exec_cmd(cp_cmd, True)
388            else:
389                raise WicError("unrecognized bootimg-efi loader: %s" %
390                               source_params['loader'])
391        except KeyError:
392            raise WicError("bootimg-efi requires a loader, none specified")
393
394        startup = os.path.join(kernel_dir, "startup.nsh")
395        if os.path.exists(startup):
396            cp_cmd = "cp %s %s/" % (startup, hdddir)
397            exec_cmd(cp_cmd, True)
398
399        du_cmd = "du -bks %s" % hdddir
400        out = exec_cmd(du_cmd)
401        blocks = int(out.split()[0])
402
403        extra_blocks = part.get_extra_block_count(blocks)
404
405        if extra_blocks < BOOTDD_EXTRA_SPACE:
406            extra_blocks = BOOTDD_EXTRA_SPACE
407
408        blocks += extra_blocks
409
410        logger.debug("Added %d extra blocks to %s to get to %d total blocks",
411                     extra_blocks, part.mountpoint, blocks)
412
413        # dosfs image, created by mkdosfs
414        bootimg = "%s/boot.img" % cr_workdir
415
416        label = part.label if part.label else "ESP"
417
418        dosfs_cmd = "mkdosfs -n %s -i %s -C %s %d" % \
419                    (label, part.fsuuid, bootimg, blocks)
420        exec_native_cmd(dosfs_cmd, native_sysroot)
421
422        mcopy_cmd = "mcopy -i %s -s %s/* ::/" % (bootimg, hdddir)
423        exec_native_cmd(mcopy_cmd, native_sysroot)
424
425        chmod_cmd = "chmod 644 %s" % bootimg
426        exec_cmd(chmod_cmd)
427
428        du_cmd = "du -Lbks %s" % bootimg
429        out = exec_cmd(du_cmd)
430        bootimg_size = out.split()[0]
431
432        part.size = int(bootimg_size)
433        part.source_file = bootimg
434