1#
2# Copyright (c) 2013, Intel Corporation.
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6# DESCRIPTION
7# This implements the 'direct' imager plugin class for 'wic'
8#
9# AUTHORS
10# Tom Zanussi <tom.zanussi (at] linux.intel.com>
11#
12
13import logging
14import os
15import random
16import shutil
17import tempfile
18import uuid
19
20from time import strftime
21
22from oe.path import copyhardlinktree
23
24from wic import WicError
25from wic.filemap import sparse_copy
26from wic.ksparser import KickStart, KickStartError
27from wic.pluginbase import PluginMgr, ImagerPlugin
28from wic.misc import get_bitbake_var, exec_cmd, exec_native_cmd
29
30logger = logging.getLogger('wic')
31
32class DirectPlugin(ImagerPlugin):
33    """
34    Install a system into a file containing a partitioned disk image.
35
36    An image file is formatted with a partition table, each partition
37    created from a rootfs or other OpenEmbedded build artifact and dd'ed
38    into the virtual disk. The disk image can subsequently be dd'ed onto
39    media and used on actual hardware.
40    """
41    name = 'direct'
42
43    def __init__(self, wks_file, rootfs_dir, bootimg_dir, kernel_dir,
44                 native_sysroot, oe_builddir, options):
45        try:
46            self.ks = KickStart(wks_file)
47        except KickStartError as err:
48            raise WicError(str(err))
49
50        # parse possible 'rootfs=name' items
51        self.rootfs_dir = dict(rdir.split('=') for rdir in rootfs_dir.split(' '))
52        self.bootimg_dir = bootimg_dir
53        self.kernel_dir = kernel_dir
54        self.native_sysroot = native_sysroot
55        self.oe_builddir = oe_builddir
56
57        self.debug = options.debug
58        self.outdir = options.outdir
59        self.compressor = options.compressor
60        self.bmap = options.bmap
61        self.no_fstab_update = options.no_fstab_update
62        self.updated_fstab_path = None
63
64        self.name = "%s-%s" % (os.path.splitext(os.path.basename(wks_file))[0],
65                               strftime("%Y%m%d%H%M"))
66        self.workdir = self.setup_workdir(options.workdir)
67        self._image = None
68        self.ptable_format = self.ks.bootloader.ptable
69        self.parts = self.ks.partitions
70
71        # as a convenience, set source to the boot partition source
72        # instead of forcing it to be set via bootloader --source
73        for part in self.parts:
74            if not self.ks.bootloader.source and part.mountpoint == "/boot":
75                self.ks.bootloader.source = part.source
76                break
77
78        image_path = self._full_path(self.workdir, self.parts[0].disk, "direct")
79        self._image = PartitionedImage(image_path, self.ptable_format,
80                                       self.parts, self.native_sysroot,
81                                       options.extra_space)
82
83    def setup_workdir(self, workdir):
84        if workdir:
85            if os.path.exists(workdir):
86                raise WicError("Internal workdir '%s' specified in wic arguments already exists!" % (workdir))
87
88            os.makedirs(workdir)
89            return workdir
90        else:
91            return tempfile.mkdtemp(dir=self.outdir, prefix='tmp.wic.')
92
93    def do_create(self):
94        """
95        Plugin entry point.
96        """
97        try:
98            self.create()
99            self.assemble()
100            self.finalize()
101            self.print_info()
102        finally:
103            self.cleanup()
104
105    def update_fstab(self, image_rootfs):
106        """Assume partition order same as in wks"""
107        if not image_rootfs:
108            return
109
110        fstab_path = image_rootfs + "/etc/fstab"
111        if not os.path.isfile(fstab_path):
112            return
113
114        with open(fstab_path) as fstab:
115            fstab_lines = fstab.readlines()
116
117        updated = False
118        for part in self.parts:
119            if not part.realnum or not part.mountpoint \
120               or part.mountpoint == "/" or not (part.mountpoint.startswith('/') or part.mountpoint == "swap"):
121                continue
122
123            if part.use_uuid:
124                if part.fsuuid:
125                    # FAT UUID is different from others
126                    if len(part.fsuuid) == 10:
127                        device_name = "UUID=%s-%s" % \
128                                       (part.fsuuid[2:6], part.fsuuid[6:])
129                    else:
130                        device_name = "UUID=%s" % part.fsuuid
131                else:
132                    device_name = "PARTUUID=%s" % part.uuid
133            elif part.use_label:
134                device_name = "LABEL=%s" % part.label
135            else:
136                # mmc device partitions are named mmcblk0p1, mmcblk0p2..
137                prefix = 'p' if  part.disk.startswith('mmcblk') else ''
138                device_name = "/dev/%s%s%d" % (part.disk, prefix, part.realnum)
139
140            opts = part.fsopts if part.fsopts else "defaults"
141            passno = part.fspassno if part.fspassno else "0"
142            line = "\t".join([device_name, part.mountpoint, part.fstype,
143                              opts, "0", passno]) + "\n"
144
145            fstab_lines.append(line)
146            updated = True
147
148        if updated:
149            self.updated_fstab_path = os.path.join(self.workdir, "fstab")
150            with open(self.updated_fstab_path, "w") as f:
151                f.writelines(fstab_lines)
152            if os.getenv('SOURCE_DATE_EPOCH'):
153                fstab_time = int(os.getenv('SOURCE_DATE_EPOCH'))
154                os.utime(self.updated_fstab_path, (fstab_time, fstab_time))
155
156    def _full_path(self, path, name, extention):
157        """ Construct full file path to a file we generate. """
158        return os.path.join(path, "%s-%s.%s" % (self.name, name, extention))
159
160    #
161    # Actual implemention
162    #
163    def create(self):
164        """
165        For 'wic', we already have our build artifacts - we just create
166        filesystems from the artifacts directly and combine them into
167        a partitioned image.
168        """
169        if not self.no_fstab_update:
170            self.update_fstab(self.rootfs_dir.get("ROOTFS_DIR"))
171
172        for part in self.parts:
173            # get rootfs size from bitbake variable if it's not set in .ks file
174            if not part.size:
175                # and if rootfs name is specified for the partition
176                image_name = self.rootfs_dir.get(part.rootfs_dir)
177                if image_name and os.path.sep not in image_name:
178                    # Bitbake variable ROOTFS_SIZE is calculated in
179                    # Image._get_rootfs_size method from meta/lib/oe/image.py
180                    # using IMAGE_ROOTFS_SIZE, IMAGE_ROOTFS_ALIGNMENT,
181                    # IMAGE_OVERHEAD_FACTOR and IMAGE_ROOTFS_EXTRA_SPACE
182                    rsize_bb = get_bitbake_var('ROOTFS_SIZE', image_name)
183                    if rsize_bb:
184                        part.size = int(round(float(rsize_bb)))
185
186        self._image.prepare(self)
187        self._image.layout_partitions()
188        self._image.create()
189
190    def assemble(self):
191        """
192        Assemble partitions into disk image
193        """
194        self._image.assemble()
195
196    def finalize(self):
197        """
198        Finalize the disk image.
199
200        For example, prepare the image to be bootable by e.g.
201        creating and installing a bootloader configuration.
202        """
203        source_plugin = self.ks.bootloader.source
204        disk_name = self.parts[0].disk
205        if source_plugin:
206            plugin = PluginMgr.get_plugins('source')[source_plugin]
207            plugin.do_install_disk(self._image, disk_name, self, self.workdir,
208                                   self.oe_builddir, self.bootimg_dir,
209                                   self.kernel_dir, self.native_sysroot)
210
211        full_path = self._image.path
212        # Generate .bmap
213        if self.bmap:
214            logger.debug("Generating bmap file for %s", disk_name)
215            python = os.path.join(self.native_sysroot, 'usr/bin/python3-native/python3')
216            bmaptool = os.path.join(self.native_sysroot, 'usr/bin/bmaptool')
217            exec_native_cmd("%s %s create %s -o %s.bmap" % \
218                            (python, bmaptool, full_path, full_path), self.native_sysroot)
219        # Compress the image
220        if self.compressor:
221            logger.debug("Compressing disk %s with %s", disk_name, self.compressor)
222            exec_cmd("%s %s" % (self.compressor, full_path))
223
224    def print_info(self):
225        """
226        Print the image(s) and artifacts used, for the user.
227        """
228        msg = "The new image(s) can be found here:\n"
229
230        extension = "direct" + {"gzip": ".gz",
231                                "bzip2": ".bz2",
232                                "xz": ".xz",
233                                None: ""}.get(self.compressor)
234        full_path = self._full_path(self.outdir, self.parts[0].disk, extension)
235        msg += '  %s\n\n' % full_path
236
237        msg += 'The following build artifacts were used to create the image(s):\n'
238        for part in self.parts:
239            if part.rootfs_dir is None:
240                continue
241            if part.mountpoint == '/':
242                suffix = ':'
243            else:
244                suffix = '["%s"]:' % (part.mountpoint or part.label)
245            rootdir = part.rootfs_dir
246            msg += '  ROOTFS_DIR%s%s\n' % (suffix.ljust(20), rootdir)
247
248        msg += '  BOOTIMG_DIR:                  %s\n' % self.bootimg_dir
249        msg += '  KERNEL_DIR:                   %s\n' % self.kernel_dir
250        msg += '  NATIVE_SYSROOT:               %s\n' % self.native_sysroot
251
252        logger.info(msg)
253
254    @property
255    def rootdev(self):
256        """
257        Get root device name to use as a 'root' parameter
258        in kernel command line.
259
260        Assume partition order same as in wks
261        """
262        for part in self.parts:
263            if part.mountpoint == "/":
264                if part.uuid:
265                    return "PARTUUID=%s" % part.uuid
266                elif part.label and self.ptable_format != 'msdos':
267                    return "PARTLABEL=%s" % part.label
268                else:
269                    suffix = 'p' if part.disk.startswith('mmcblk') else ''
270                    return "/dev/%s%s%-d" % (part.disk, suffix, part.realnum)
271
272    def cleanup(self):
273        if self._image:
274            self._image.cleanup()
275
276        # Move results to the output dir
277        if not os.path.exists(self.outdir):
278            os.makedirs(self.outdir)
279
280        for fname in os.listdir(self.workdir):
281            path = os.path.join(self.workdir, fname)
282            if os.path.isfile(path):
283                shutil.move(path, os.path.join(self.outdir, fname))
284
285        # remove work directory when it is not in debugging mode
286        if not self.debug:
287            shutil.rmtree(self.workdir, ignore_errors=True)
288
289# Overhead of the MBR partitioning scheme (just one sector)
290MBR_OVERHEAD = 1
291
292# Overhead of the GPT partitioning scheme
293GPT_OVERHEAD = 34
294
295# Size of a sector in bytes
296SECTOR_SIZE = 512
297
298class PartitionedImage():
299    """
300    Partitioned image in a file.
301    """
302
303    def __init__(self, path, ptable_format, partitions, native_sysroot=None, extra_space=0):
304        self.path = path  # Path to the image file
305        self.numpart = 0  # Number of allocated partitions
306        self.realpart = 0 # Number of partitions in the partition table
307        self.primary_part_num = 0  # Number of primary partitions (msdos)
308        self.extendedpart = 0      # Create extended partition before this logical partition (msdos)
309        self.extended_size_sec = 0 # Size of exteded partition (msdos)
310        self.logical_part_cnt = 0  # Number of total logical paritions (msdos)
311        self.offset = 0   # Offset of next partition (in sectors)
312        self.min_size = 0 # Minimum required disk size to fit
313                          # all partitions (in bytes)
314        self.ptable_format = ptable_format  # Partition table format
315        # Disk system identifier
316        if os.getenv('SOURCE_DATE_EPOCH'):
317            self.identifier = random.Random(int(os.getenv('SOURCE_DATE_EPOCH'))).randint(1, 0xffffffff)
318        else:
319            self.identifier = random.SystemRandom().randint(1, 0xffffffff)
320
321        self.partitions = partitions
322        self.partimages = []
323        # Size of a sector used in calculations
324        self.sector_size = SECTOR_SIZE
325        self.native_sysroot = native_sysroot
326        num_real_partitions = len([p for p in self.partitions if not p.no_table])
327        self.extra_space = extra_space
328
329        # calculate the real partition number, accounting for partitions not
330        # in the partition table and logical partitions
331        realnum = 0
332        for part in self.partitions:
333            if part.no_table:
334                part.realnum = 0
335            else:
336                realnum += 1
337                if self.ptable_format == 'msdos' and realnum > 3 and num_real_partitions > 4:
338                    part.realnum = realnum + 1
339                    continue
340                part.realnum = realnum
341
342        # generate parition and filesystem UUIDs
343        for part in self.partitions:
344            if not part.uuid and part.use_uuid:
345                if self.ptable_format in ('gpt', 'gpt-hybrid'):
346                    part.uuid = str(uuid.uuid4())
347                else: # msdos partition table
348                    part.uuid = '%08x-%02d' % (self.identifier, part.realnum)
349            if not part.fsuuid:
350                if part.fstype == 'vfat' or part.fstype == 'msdos':
351                    part.fsuuid = '0x' + str(uuid.uuid4())[:8].upper()
352                else:
353                    part.fsuuid = str(uuid.uuid4())
354            else:
355                #make sure the fsuuid for vfat/msdos align with format 0xYYYYYYYY
356                if part.fstype == 'vfat' or part.fstype == 'msdos':
357                    if part.fsuuid.upper().startswith("0X"):
358                        part.fsuuid = '0x' + part.fsuuid.upper()[2:].rjust(8,"0")
359                    else:
360                        part.fsuuid = '0x' + part.fsuuid.upper().rjust(8,"0")
361
362    def prepare(self, imager):
363        """Prepare an image. Call prepare method of all image partitions."""
364        for part in self.partitions:
365            # need to create the filesystems in order to get their
366            # sizes before we can add them and do the layout.
367            part.prepare(imager, imager.workdir, imager.oe_builddir,
368                         imager.rootfs_dir, imager.bootimg_dir,
369                         imager.kernel_dir, imager.native_sysroot,
370                         imager.updated_fstab_path)
371
372            # Converting kB to sectors for parted
373            part.size_sec = part.disk_size * 1024 // self.sector_size
374
375    def layout_partitions(self):
376        """ Layout the partitions, meaning calculate the position of every
377        partition on the disk. The 'ptable_format' parameter defines the
378        partition table format and may be "msdos". """
379
380        logger.debug("Assigning %s partitions to disks", self.ptable_format)
381
382        # The number of primary and logical partitions. Extended partition and
383        # partitions not listed in the table are not included.
384        num_real_partitions = len([p for p in self.partitions if not p.no_table])
385
386        # Go through partitions in the order they are added in .ks file
387        for num in range(len(self.partitions)):
388            part = self.partitions[num]
389
390            if self.ptable_format == 'msdos' and part.part_name:
391                raise WicError("setting custom partition name is not " \
392                               "implemented for msdos partitions")
393
394            if self.ptable_format == 'msdos' and part.part_type:
395                # The --part-type can also be implemented for MBR partitions,
396                # in which case it would map to the 1-byte "partition type"
397                # filed at offset 3 of the partition entry.
398                raise WicError("setting custom partition type is not " \
399                               "implemented for msdos partitions")
400
401            if part.mbr and self.ptable_format != 'gpt-hybrid':
402                raise WicError("Partition may only be included in MBR with " \
403                               "a gpt-hybrid partition table")
404
405            # Get the disk where the partition is located
406            self.numpart += 1
407            if not part.no_table:
408                self.realpart += 1
409
410            if self.numpart == 1:
411                if self.ptable_format == "msdos":
412                    overhead = MBR_OVERHEAD
413                elif self.ptable_format in ("gpt", "gpt-hybrid"):
414                    overhead = GPT_OVERHEAD
415
416                # Skip one sector required for the partitioning scheme overhead
417                self.offset += overhead
418
419            if self.ptable_format == "msdos":
420                if self.primary_part_num > 3 or \
421                   (self.extendedpart == 0 and self.primary_part_num >= 3 and num_real_partitions > 4):
422                    part.type = 'logical'
423                # Reserve a sector for EBR for every logical partition
424                # before alignment is performed.
425                if part.type == 'logical':
426                    self.offset += 2
427
428            align_sectors = 0
429            if part.align:
430                # If not first partition and we do have alignment set we need
431                # to align the partition.
432                # FIXME: This leaves a empty spaces to the disk. To fill the
433                # gaps we could enlargea the previous partition?
434
435                # Calc how much the alignment is off.
436                align_sectors = self.offset % (part.align * 1024 // self.sector_size)
437
438                if align_sectors:
439                    # If partition is not aligned as required, we need
440                    # to move forward to the next alignment point
441                    align_sectors = (part.align * 1024 // self.sector_size) - align_sectors
442
443                    logger.debug("Realignment for %s%s with %s sectors, original"
444                                 " offset %s, target alignment is %sK.",
445                                 part.disk, self.numpart, align_sectors,
446                                 self.offset, part.align)
447
448                    # increase the offset so we actually start the partition on right alignment
449                    self.offset += align_sectors
450
451            if part.offset is not None:
452                offset = part.offset // self.sector_size
453
454                if offset * self.sector_size != part.offset:
455                    raise WicError("Could not place %s%s at offset %d with sector size %d" % (part.disk, self.numpart, part.offset, self.sector_size))
456
457                delta = offset - self.offset
458                if delta < 0:
459                    raise WicError("Could not place %s%s at offset %d: next free sector is %d (delta: %d)" % (part.disk, self.numpart, part.offset, self.offset, delta))
460
461                logger.debug("Skipping %d sectors to place %s%s at offset %dK",
462                             delta, part.disk, self.numpart, part.offset)
463
464                self.offset = offset
465
466            part.start = self.offset
467            self.offset += part.size_sec
468
469            if not part.no_table:
470                part.num = self.realpart
471            else:
472                part.num = 0
473
474            if self.ptable_format == "msdos" and not part.no_table:
475                if part.type == 'logical':
476                    self.logical_part_cnt += 1
477                    part.num = self.logical_part_cnt + 4
478                    if self.extendedpart == 0:
479                        # Create extended partition as a primary partition
480                        self.primary_part_num += 1
481                        self.extendedpart = part.num
482                    else:
483                        self.extended_size_sec += align_sectors
484                    self.extended_size_sec += part.size_sec + 2
485                else:
486                    self.primary_part_num += 1
487                    part.num = self.primary_part_num
488
489            logger.debug("Assigned %s to %s%d, sectors range %d-%d size %d "
490                         "sectors (%d bytes).", part.mountpoint, part.disk,
491                         part.num, part.start, self.offset - 1, part.size_sec,
492                         part.size_sec * self.sector_size)
493
494        # Once all the partitions have been layed out, we can calculate the
495        # minumim disk size
496        self.min_size = self.offset
497        if self.ptable_format in ("gpt", "gpt-hybrid"):
498            self.min_size += GPT_OVERHEAD
499
500        self.min_size *= self.sector_size
501        self.min_size += self.extra_space
502
503    def _create_partition(self, device, parttype, fstype, start, size):
504        """ Create a partition on an image described by the 'device' object. """
505
506        # Start is included to the size so we need to substract one from the end.
507        end = start + size - 1
508        logger.debug("Added '%s' partition, sectors %d-%d, size %d sectors",
509                     parttype, start, end, size)
510
511        cmd = "parted -s %s unit s mkpart %s" % (device, parttype)
512        if fstype:
513            cmd += " %s" % fstype
514        cmd += " %d %d" % (start, end)
515
516        return exec_native_cmd(cmd, self.native_sysroot)
517
518    def _write_identifier(self, device, identifier):
519        logger.debug("Set disk identifier %x", identifier)
520        with open(device, 'r+b') as img:
521            img.seek(0x1B8)
522            img.write(identifier.to_bytes(4, 'little'))
523
524    def _make_disk(self, device, ptable_format, min_size):
525        logger.debug("Creating sparse file %s", device)
526        with open(device, 'w') as sparse:
527            os.ftruncate(sparse.fileno(), min_size)
528
529        logger.debug("Initializing partition table for %s", device)
530        exec_native_cmd("parted -s %s mklabel %s" % (device, ptable_format),
531                        self.native_sysroot)
532
533
534    def create(self):
535        self._make_disk(self.path,
536                        "gpt" if self.ptable_format == "gpt-hybrid" else self.ptable_format,
537                        self.min_size)
538
539        self._write_identifier(self.path, self.identifier)
540
541        if self.ptable_format == "gpt-hybrid":
542            mbr_path = self.path + ".mbr"
543            self._make_disk(mbr_path, "msdos", self.min_size)
544            self._write_identifier(mbr_path, self.identifier)
545
546        logger.debug("Creating partitions")
547
548        hybrid_mbr_part_num = 0
549
550        for part in self.partitions:
551            if part.num == 0:
552                continue
553
554            if self.ptable_format == "msdos" and part.num == self.extendedpart:
555                # Create an extended partition (note: extended
556                # partition is described in MBR and contains all
557                # logical partitions). The logical partitions save a
558                # sector for an EBR just before the start of a
559                # partition. The extended partition must start one
560                # sector before the start of the first logical
561                # partition. This way the first EBR is inside of the
562                # extended partition. Since the extended partitions
563                # starts a sector before the first logical partition,
564                # add a sector at the back, so that there is enough
565                # room for all logical partitions.
566                self._create_partition(self.path, "extended",
567                                       None, part.start - 2,
568                                       self.extended_size_sec)
569
570            if part.fstype == "swap":
571                parted_fs_type = "linux-swap"
572            elif part.fstype == "vfat":
573                parted_fs_type = "fat32"
574            elif part.fstype == "msdos":
575                parted_fs_type = "fat16"
576                if not part.system_id:
577                    part.system_id = '0x6' # FAT16
578            else:
579                # Type for ext2/ext3/ext4/btrfs
580                parted_fs_type = "ext2"
581
582            # Boot ROM of OMAP boards require vfat boot partition to have an
583            # even number of sectors.
584            if part.mountpoint == "/boot" and part.fstype in ["vfat", "msdos"] \
585               and part.size_sec % 2:
586                logger.debug("Subtracting one sector from '%s' partition to "
587                             "get even number of sectors for the partition",
588                             part.mountpoint)
589                part.size_sec -= 1
590
591            self._create_partition(self.path, part.type,
592                                   parted_fs_type, part.start, part.size_sec)
593
594            if self.ptable_format == "gpt-hybrid" and part.mbr:
595                hybrid_mbr_part_num += 1
596                if hybrid_mbr_part_num > 4:
597                    raise WicError("Extended MBR partitions are not supported in hybrid MBR")
598                self._create_partition(mbr_path, "primary",
599                                       parted_fs_type, part.start, part.size_sec)
600
601            if self.ptable_format in ("gpt", "gpt-hybrid") and (part.part_name or part.label):
602                partition_label = part.part_name if part.part_name else part.label
603                logger.debug("partition %d: set name to %s",
604                             part.num, partition_label)
605                exec_native_cmd("sgdisk --change-name=%d:%s %s" % \
606                                         (part.num, partition_label,
607                                          self.path), self.native_sysroot)
608
609            if part.part_type:
610                logger.debug("partition %d: set type UID to %s",
611                             part.num, part.part_type)
612                exec_native_cmd("sgdisk --typecode=%d:%s %s" % \
613                                         (part.num, part.part_type,
614                                          self.path), self.native_sysroot)
615
616            if part.uuid and self.ptable_format in ("gpt", "gpt-hybrid"):
617                logger.debug("partition %d: set UUID to %s",
618                             part.num, part.uuid)
619                exec_native_cmd("sgdisk --partition-guid=%d:%s %s" % \
620                                (part.num, part.uuid, self.path),
621                                self.native_sysroot)
622
623            if part.active:
624                flag_name = "legacy_boot" if self.ptable_format in ('gpt', 'gpt-hybrid') else "boot"
625                logger.debug("Set '%s' flag for partition '%s' on disk '%s'",
626                             flag_name, part.num, self.path)
627                exec_native_cmd("parted -s %s set %d %s on" % \
628                                (self.path, part.num, flag_name),
629                                self.native_sysroot)
630                if self.ptable_format == 'gpt-hybrid' and part.mbr:
631                    exec_native_cmd("parted -s %s set %d %s on" % \
632                                    (mbr_path, hybrid_mbr_part_num, "boot"),
633                                    self.native_sysroot)
634            if part.system_id:
635                exec_native_cmd("sfdisk --part-type %s %s %s" % \
636                                (self.path, part.num, part.system_id),
637                                self.native_sysroot)
638
639            if part.hidden and self.ptable_format == "gpt":
640                logger.debug("Set hidden attribute for partition '%s' on disk '%s'",
641                             part.num, self.path)
642                exec_native_cmd("sfdisk --part-attrs %s %s RequiredPartition" % \
643                                (self.path, part.num),
644                                self.native_sysroot)
645
646        if self.ptable_format == "gpt-hybrid":
647            # Write a protective GPT partition
648            hybrid_mbr_part_num += 1
649            if hybrid_mbr_part_num > 4:
650                raise WicError("Extended MBR partitions are not supported in hybrid MBR")
651
652            # parted cannot directly create a protective GPT partition, so
653            # create with an arbitrary type, then change it to the correct type
654            # with sfdisk
655            self._create_partition(mbr_path, "primary", "fat32", 1, GPT_OVERHEAD)
656            exec_native_cmd("sfdisk --part-type %s %d 0xee" % (mbr_path, hybrid_mbr_part_num),
657                            self.native_sysroot)
658
659            # Copy hybrid MBR
660            with open(mbr_path, "rb") as mbr_file:
661                with open(self.path, "r+b") as image_file:
662                    mbr = mbr_file.read(512)
663                    image_file.write(mbr)
664
665    def cleanup(self):
666        pass
667
668    def assemble(self):
669        logger.debug("Installing partitions")
670
671        for part in self.partitions:
672            source = part.source_file
673            if source:
674                # install source_file contents into a partition
675                sparse_copy(source, self.path, seek=part.start * self.sector_size)
676
677                logger.debug("Installed %s in partition %d, sectors %d-%d, "
678                             "size %d sectors", source, part.num, part.start,
679                             part.start + part.size_sec - 1, part.size_sec)
680
681                partimage = self.path + '.p%d' % part.num
682                os.rename(source, partimage)
683                self.partimages.append(partimage)
684