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    def _write_disk_guid(self):
534        if self.ptable_format in ('gpt', 'gpt-hybrid'):
535            if os.getenv('SOURCE_DATE_EPOCH'):
536                self.disk_guid = uuid.UUID(int=int(os.getenv('SOURCE_DATE_EPOCH')))
537            else:
538                self.disk_guid = uuid.uuid4()
539
540            logger.debug("Set disk guid %s", self.disk_guid)
541            sfdisk_cmd = "sfdisk --disk-id %s %s" % (self.path, self.disk_guid)
542            exec_native_cmd(sfdisk_cmd, self.native_sysroot)
543
544    def create(self):
545        self._make_disk(self.path,
546                        "gpt" if self.ptable_format == "gpt-hybrid" else self.ptable_format,
547                        self.min_size)
548
549        self._write_identifier(self.path, self.identifier)
550        self._write_disk_guid()
551
552        if self.ptable_format == "gpt-hybrid":
553            mbr_path = self.path + ".mbr"
554            self._make_disk(mbr_path, "msdos", self.min_size)
555            self._write_identifier(mbr_path, self.identifier)
556
557        logger.debug("Creating partitions")
558
559        hybrid_mbr_part_num = 0
560
561        for part in self.partitions:
562            if part.num == 0:
563                continue
564
565            if self.ptable_format == "msdos" and part.num == self.extendedpart:
566                # Create an extended partition (note: extended
567                # partition is described in MBR and contains all
568                # logical partitions). The logical partitions save a
569                # sector for an EBR just before the start of a
570                # partition. The extended partition must start one
571                # sector before the start of the first logical
572                # partition. This way the first EBR is inside of the
573                # extended partition. Since the extended partitions
574                # starts a sector before the first logical partition,
575                # add a sector at the back, so that there is enough
576                # room for all logical partitions.
577                self._create_partition(self.path, "extended",
578                                       None, part.start - 2,
579                                       self.extended_size_sec)
580
581            if part.fstype == "swap":
582                parted_fs_type = "linux-swap"
583            elif part.fstype == "vfat":
584                parted_fs_type = "fat32"
585            elif part.fstype == "msdos":
586                parted_fs_type = "fat16"
587                if not part.system_id:
588                    part.system_id = '0x6' # FAT16
589            else:
590                # Type for ext2/ext3/ext4/btrfs
591                parted_fs_type = "ext2"
592
593            # Boot ROM of OMAP boards require vfat boot partition to have an
594            # even number of sectors.
595            if part.mountpoint == "/boot" and part.fstype in ["vfat", "msdos"] \
596               and part.size_sec % 2:
597                logger.debug("Subtracting one sector from '%s' partition to "
598                             "get even number of sectors for the partition",
599                             part.mountpoint)
600                part.size_sec -= 1
601
602            self._create_partition(self.path, part.type,
603                                   parted_fs_type, part.start, part.size_sec)
604
605            if self.ptable_format == "gpt-hybrid" and part.mbr:
606                hybrid_mbr_part_num += 1
607                if hybrid_mbr_part_num > 4:
608                    raise WicError("Extended MBR partitions are not supported in hybrid MBR")
609                self._create_partition(mbr_path, "primary",
610                                       parted_fs_type, part.start, part.size_sec)
611
612            if self.ptable_format in ("gpt", "gpt-hybrid") and (part.part_name or part.label):
613                partition_label = part.part_name if part.part_name else part.label
614                logger.debug("partition %d: set name to %s",
615                             part.num, partition_label)
616                exec_native_cmd("sgdisk --change-name=%d:%s %s" % \
617                                         (part.num, partition_label,
618                                          self.path), self.native_sysroot)
619
620            if part.part_type:
621                logger.debug("partition %d: set type UID to %s",
622                             part.num, part.part_type)
623                exec_native_cmd("sgdisk --typecode=%d:%s %s" % \
624                                         (part.num, part.part_type,
625                                          self.path), self.native_sysroot)
626
627            if part.uuid and self.ptable_format in ("gpt", "gpt-hybrid"):
628                logger.debug("partition %d: set UUID to %s",
629                             part.num, part.uuid)
630                exec_native_cmd("sgdisk --partition-guid=%d:%s %s" % \
631                                (part.num, part.uuid, self.path),
632                                self.native_sysroot)
633
634            if part.active:
635                flag_name = "legacy_boot" if self.ptable_format in ('gpt', 'gpt-hybrid') else "boot"
636                logger.debug("Set '%s' flag for partition '%s' on disk '%s'",
637                             flag_name, part.num, self.path)
638                exec_native_cmd("parted -s %s set %d %s on" % \
639                                (self.path, part.num, flag_name),
640                                self.native_sysroot)
641                if self.ptable_format == 'gpt-hybrid' and part.mbr:
642                    exec_native_cmd("parted -s %s set %d %s on" % \
643                                    (mbr_path, hybrid_mbr_part_num, "boot"),
644                                    self.native_sysroot)
645            if part.system_id:
646                exec_native_cmd("sfdisk --part-type %s %s %s" % \
647                                (self.path, part.num, part.system_id),
648                                self.native_sysroot)
649
650            if part.hidden and self.ptable_format == "gpt":
651                logger.debug("Set hidden attribute for partition '%s' on disk '%s'",
652                             part.num, self.path)
653                exec_native_cmd("sfdisk --part-attrs %s %s RequiredPartition" % \
654                                (self.path, part.num),
655                                self.native_sysroot)
656
657        if self.ptable_format == "gpt-hybrid":
658            # Write a protective GPT partition
659            hybrid_mbr_part_num += 1
660            if hybrid_mbr_part_num > 4:
661                raise WicError("Extended MBR partitions are not supported in hybrid MBR")
662
663            # parted cannot directly create a protective GPT partition, so
664            # create with an arbitrary type, then change it to the correct type
665            # with sfdisk
666            self._create_partition(mbr_path, "primary", "fat32", 1, GPT_OVERHEAD)
667            exec_native_cmd("sfdisk --part-type %s %d 0xee" % (mbr_path, hybrid_mbr_part_num),
668                            self.native_sysroot)
669
670            # Copy hybrid MBR
671            with open(mbr_path, "rb") as mbr_file:
672                with open(self.path, "r+b") as image_file:
673                    mbr = mbr_file.read(512)
674                    image_file.write(mbr)
675
676    def cleanup(self):
677        pass
678
679    def assemble(self):
680        logger.debug("Installing partitions")
681
682        for part in self.partitions:
683            source = part.source_file
684            if source:
685                # install source_file contents into a partition
686                sparse_copy(source, self.path, seek=part.start * self.sector_size)
687
688                logger.debug("Installed %s in partition %d, sectors %d-%d, "
689                             "size %d sectors", source, part.num, part.start,
690                             part.start + part.size_sec - 1, part.size_sec)
691
692                partimage = self.path + '.p%d' % part.num
693                os.rename(source, partimage)
694                self.partimages.append(partimage)
695