xref: /openbmc/openbmc/poky/scripts/lib/wic/plugins/imager/direct.py (revision 8460358c3d24c71d9d38fd126c745854a6301564)
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        sector_size_str = get_bitbake_var('WIC_SECTOR_SIZE')
325        if sector_size_str is not None:
326            try:
327                self.sector_size = int(sector_size_str)
328            except ValueError:
329                self.sector_size = SECTOR_SIZE
330        else:
331            self.sector_size = SECTOR_SIZE
332
333        self.native_sysroot = native_sysroot
334        num_real_partitions = len([p for p in self.partitions if not p.no_table])
335        self.extra_space = extra_space
336
337        # calculate the real partition number, accounting for partitions not
338        # in the partition table and logical partitions
339        realnum = 0
340        for part in self.partitions:
341            if part.no_table:
342                part.realnum = 0
343            else:
344                realnum += 1
345                if self.ptable_format == 'msdos' and realnum > 3 and num_real_partitions > 4:
346                    part.realnum = realnum + 1
347                    continue
348                part.realnum = realnum
349
350        # generate parition and filesystem UUIDs
351        for part in self.partitions:
352            if not part.uuid and part.use_uuid:
353                if self.ptable_format in ('gpt', 'gpt-hybrid'):
354                    part.uuid = str(uuid.uuid4())
355                else: # msdos partition table
356                    part.uuid = '%08x-%02d' % (self.identifier, part.realnum)
357            if not part.fsuuid:
358                if part.fstype == 'vfat' or part.fstype == 'msdos':
359                    part.fsuuid = '0x' + str(uuid.uuid4())[:8].upper()
360                else:
361                    part.fsuuid = str(uuid.uuid4())
362            else:
363                #make sure the fsuuid for vfat/msdos align with format 0xYYYYYYYY
364                if part.fstype == 'vfat' or part.fstype == 'msdos':
365                    if part.fsuuid.upper().startswith("0X"):
366                        part.fsuuid = '0x' + part.fsuuid.upper()[2:].rjust(8,"0")
367                    else:
368                        part.fsuuid = '0x' + part.fsuuid.upper().rjust(8,"0")
369
370    def prepare(self, imager):
371        """Prepare an image. Call prepare method of all image partitions."""
372        for part in self.partitions:
373            # need to create the filesystems in order to get their
374            # sizes before we can add them and do the layout.
375            part.prepare(imager, imager.workdir, imager.oe_builddir,
376                         imager.rootfs_dir, imager.bootimg_dir,
377                         imager.kernel_dir, imager.native_sysroot,
378                         imager.updated_fstab_path)
379
380            # Converting kB to sectors for parted
381            part.size_sec = part.disk_size * 1024 // self.sector_size
382
383    def layout_partitions(self):
384        """ Layout the partitions, meaning calculate the position of every
385        partition on the disk. The 'ptable_format' parameter defines the
386        partition table format and may be "msdos". """
387
388        logger.debug("Assigning %s partitions to disks", self.ptable_format)
389
390        # The number of primary and logical partitions. Extended partition and
391        # partitions not listed in the table are not included.
392        num_real_partitions = len([p for p in self.partitions if not p.no_table])
393
394        # Go through partitions in the order they are added in .ks file
395        for num in range(len(self.partitions)):
396            part = self.partitions[num]
397
398            if self.ptable_format == 'msdos' and part.part_name:
399                raise WicError("setting custom partition name is not " \
400                               "implemented for msdos partitions")
401
402            if self.ptable_format == 'msdos' and part.part_type:
403                # The --part-type can also be implemented for MBR partitions,
404                # in which case it would map to the 1-byte "partition type"
405                # filed at offset 3 of the partition entry.
406                raise WicError("setting custom partition type is not " \
407                               "implemented for msdos partitions")
408
409            if part.mbr and self.ptable_format != 'gpt-hybrid':
410                raise WicError("Partition may only be included in MBR with " \
411                               "a gpt-hybrid partition table")
412
413            # Get the disk where the partition is located
414            self.numpart += 1
415            if not part.no_table:
416                self.realpart += 1
417
418            if self.numpart == 1:
419                if self.ptable_format == "msdos":
420                    overhead = MBR_OVERHEAD
421                elif self.ptable_format in ("gpt", "gpt-hybrid"):
422                    overhead = GPT_OVERHEAD
423
424                # Skip one sector required for the partitioning scheme overhead
425                self.offset += overhead
426
427            if self.ptable_format == "msdos":
428                if self.primary_part_num > 3 or \
429                   (self.extendedpart == 0 and self.primary_part_num >= 3 and num_real_partitions > 4):
430                    part.type = 'logical'
431                # Reserve a sector for EBR for every logical partition
432                # before alignment is performed.
433                if part.type == 'logical':
434                    self.offset += 2
435
436            align_sectors = 0
437            if part.align:
438                # If not first partition and we do have alignment set we need
439                # to align the partition.
440                # FIXME: This leaves a empty spaces to the disk. To fill the
441                # gaps we could enlargea the previous partition?
442
443                # Calc how much the alignment is off.
444                align_sectors = self.offset % (part.align * 1024 // self.sector_size)
445
446                if align_sectors:
447                    # If partition is not aligned as required, we need
448                    # to move forward to the next alignment point
449                    align_sectors = (part.align * 1024 // self.sector_size) - align_sectors
450
451                    logger.debug("Realignment for %s%s with %s sectors, original"
452                                 " offset %s, target alignment is %sK.",
453                                 part.disk, self.numpart, align_sectors,
454                                 self.offset, part.align)
455
456                    # increase the offset so we actually start the partition on right alignment
457                    self.offset += align_sectors
458
459            if part.offset is not None:
460                offset = part.offset // self.sector_size
461
462                if offset * self.sector_size != part.offset:
463                    raise WicError("Could not place %s%s at offset %d with sector size %d" % (part.disk, self.numpart, part.offset, self.sector_size))
464
465                delta = offset - self.offset
466                if delta < 0:
467                    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))
468
469                logger.debug("Skipping %d sectors to place %s%s at offset %dK",
470                             delta, part.disk, self.numpart, part.offset)
471
472                self.offset = offset
473
474            part.start = self.offset
475            self.offset += part.size_sec
476
477            if not part.no_table:
478                part.num = self.realpart
479            else:
480                part.num = 0
481
482            if self.ptable_format == "msdos" and not part.no_table:
483                if part.type == 'logical':
484                    self.logical_part_cnt += 1
485                    part.num = self.logical_part_cnt + 4
486                    if self.extendedpart == 0:
487                        # Create extended partition as a primary partition
488                        self.primary_part_num += 1
489                        self.extendedpart = part.num
490                    else:
491                        self.extended_size_sec += align_sectors
492                    self.extended_size_sec += part.size_sec + 2
493                else:
494                    self.primary_part_num += 1
495                    part.num = self.primary_part_num
496
497            logger.debug("Assigned %s to %s%d, sectors range %d-%d size %d "
498                         "sectors (%d bytes).", part.mountpoint, part.disk,
499                         part.num, part.start, self.offset - 1, part.size_sec,
500                         part.size_sec * self.sector_size)
501
502        # Once all the partitions have been layed out, we can calculate the
503        # minumim disk size
504        self.min_size = self.offset
505        if self.ptable_format in ("gpt", "gpt-hybrid"):
506            self.min_size += GPT_OVERHEAD
507
508        self.min_size *= self.sector_size
509        self.min_size += self.extra_space
510
511    def _create_partition(self, device, parttype, fstype, start, size):
512        """ Create a partition on an image described by the 'device' object. """
513
514        # Start is included to the size so we need to substract one from the end.
515        end = start + size - 1
516        logger.debug("Added '%s' partition, sectors %d-%d, size %d sectors",
517                     parttype, start, end, size)
518
519        cmd = "export PARTED_SECTOR_SIZE=%d; parted -s %s unit s mkpart %s" % \
520                     (self.sector_size, device, parttype)
521        if fstype:
522            cmd += " %s" % fstype
523        cmd += " %d %d" % (start, end)
524
525        return exec_native_cmd(cmd, self.native_sysroot)
526
527    def _write_identifier(self, device, identifier):
528        logger.debug("Set disk identifier %x", identifier)
529        with open(device, 'r+b') as img:
530            img.seek(0x1B8)
531            img.write(identifier.to_bytes(4, 'little'))
532
533    def _make_disk(self, device, ptable_format, min_size):
534        logger.debug("Creating sparse file %s", device)
535        with open(device, 'w') as sparse:
536            os.ftruncate(sparse.fileno(), min_size)
537
538        logger.debug("Initializing partition table for %s", device)
539        exec_native_cmd("export PARTED_SECTOR_SIZE=%d; parted -s %s mklabel %s" %
540                        (self.sector_size, device, ptable_format), self.native_sysroot)
541
542    def _write_disk_guid(self):
543        if self.ptable_format in ('gpt', 'gpt-hybrid'):
544            if os.getenv('SOURCE_DATE_EPOCH'):
545                self.disk_guid = uuid.UUID(int=int(os.getenv('SOURCE_DATE_EPOCH')))
546            else:
547                self.disk_guid = uuid.uuid4()
548
549            logger.debug("Set disk guid %s", self.disk_guid)
550            sfdisk_cmd = "sfdisk --sector-size %s --disk-id %s %s" % \
551                        (self.sector_size, self.path, self.disk_guid)
552            exec_native_cmd(sfdisk_cmd, self.native_sysroot)
553
554    def create(self):
555        self._make_disk(self.path,
556                        "gpt" if self.ptable_format == "gpt-hybrid" else self.ptable_format,
557                        self.min_size)
558
559        self._write_identifier(self.path, self.identifier)
560        self._write_disk_guid()
561
562        if self.ptable_format == "gpt-hybrid":
563            mbr_path = self.path + ".mbr"
564            self._make_disk(mbr_path, "msdos", self.min_size)
565            self._write_identifier(mbr_path, self.identifier)
566
567        logger.debug("Creating partitions")
568
569        hybrid_mbr_part_num = 0
570
571        for part in self.partitions:
572            if part.num == 0:
573                continue
574
575            if self.ptable_format == "msdos" and part.num == self.extendedpart:
576                # Create an extended partition (note: extended
577                # partition is described in MBR and contains all
578                # logical partitions). The logical partitions save a
579                # sector for an EBR just before the start of a
580                # partition. The extended partition must start one
581                # sector before the start of the first logical
582                # partition. This way the first EBR is inside of the
583                # extended partition. Since the extended partitions
584                # starts a sector before the first logical partition,
585                # add a sector at the back, so that there is enough
586                # room for all logical partitions.
587                self._create_partition(self.path, "extended",
588                                       None, part.start - 2,
589                                       self.extended_size_sec)
590
591            if part.fstype == "swap":
592                parted_fs_type = "linux-swap"
593            elif part.fstype == "vfat":
594                parted_fs_type = "fat32"
595            elif part.fstype == "msdos":
596                parted_fs_type = "fat16"
597                if not part.system_id:
598                    part.system_id = '0x6' # FAT16
599            else:
600                # Type for ext2/ext3/ext4/btrfs
601                parted_fs_type = "ext2"
602
603            # Boot ROM of OMAP boards require vfat boot partition to have an
604            # even number of sectors.
605            if part.mountpoint == "/boot" and part.fstype in ["vfat", "msdos"] \
606               and part.size_sec % 2:
607                logger.debug("Subtracting one sector from '%s' partition to "
608                             "get even number of sectors for the partition",
609                             part.mountpoint)
610                part.size_sec -= 1
611
612            self._create_partition(self.path, part.type,
613                                   parted_fs_type, part.start, part.size_sec)
614
615            if self.ptable_format == "gpt-hybrid" and part.mbr:
616                hybrid_mbr_part_num += 1
617                if hybrid_mbr_part_num > 4:
618                    raise WicError("Extended MBR partitions are not supported in hybrid MBR")
619                self._create_partition(mbr_path, "primary",
620                                       parted_fs_type, part.start, part.size_sec)
621
622            if self.ptable_format in ("gpt", "gpt-hybrid") and (part.part_name or part.label):
623                partition_label = part.part_name if part.part_name else part.label
624                logger.debug("partition %d: set name to %s",
625                             part.num, partition_label)
626                exec_native_cmd("sfdisk --sector-size %s --part-label %s %d %s" % \
627                                         (self.sector_size, self.path, part.num,
628                                          partition_label), self.native_sysroot)
629            if part.part_type:
630                logger.debug("partition %d: set type UID to %s",
631                             part.num, part.part_type)
632                exec_native_cmd("sfdisk --sector-size %s --part-type %s %d %s" % \
633                                         (self.sector_size, self.path, part.num,
634                                          part.part_type), self.native_sysroot)
635
636            if part.uuid and self.ptable_format in ("gpt", "gpt-hybrid"):
637                logger.debug("partition %d: set UUID to %s",
638                             part.num, part.uuid)
639                exec_native_cmd("sfdisk --sector-size %s --part-uuid %s %d %s" % \
640                                (self.sector_size, self.path, part.num, part.uuid),
641                                self.native_sysroot)
642
643            if part.active:
644                flag_name = "legacy_boot" if self.ptable_format in ('gpt', 'gpt-hybrid') else "boot"
645                logger.debug("Set '%s' flag for partition '%s' on disk '%s'",
646                             flag_name, part.num, self.path)
647                exec_native_cmd("export PARTED_SECTOR_SIZE=%d; parted -s %s set %d %s on" % \
648                                (self.sector_size, self.path, part.num, flag_name),
649                                self.native_sysroot)
650                if self.ptable_format == 'gpt-hybrid' and part.mbr:
651                    exec_native_cmd("export PARTED_SECTOR_SIZE=%d; parted -s %s set %d %s on" % \
652                                    (self.sector_size, mbr_path, hybrid_mbr_part_num, "boot"),
653                                    self.native_sysroot)
654            if part.system_id:
655                exec_native_cmd("sfdisk --sector-size %s --part-type %s %s %s" % \
656                                (self.sector_size, self.path, part.num, part.system_id),
657                                self.native_sysroot)
658
659            if part.hidden and self.ptable_format == "gpt":
660                logger.debug("Set hidden attribute for partition '%s' on disk '%s'",
661                             part.num, self.path)
662                exec_native_cmd("sfdisk --sector-size %s --part-attrs %s %s RequiredPartition" % \
663                                (self.sector_size, self.path, part.num),
664                                self.native_sysroot)
665
666        if self.ptable_format == "gpt-hybrid":
667            # Write a protective GPT partition
668            hybrid_mbr_part_num += 1
669            if hybrid_mbr_part_num > 4:
670                raise WicError("Extended MBR partitions are not supported in hybrid MBR")
671
672            # parted cannot directly create a protective GPT partition, so
673            # create with an arbitrary type, then change it to the correct type
674            # with sfdisk
675            self._create_partition(mbr_path, "primary", "fat32", 1, GPT_OVERHEAD)
676            exec_native_cmd("sfdisk --sector-size %s --part-type %s %d 0xee" % \
677                            (self.sector_size, mbr_path, hybrid_mbr_part_num),
678                            self.native_sysroot)
679
680            # Copy hybrid MBR
681            with open(mbr_path, "rb") as mbr_file:
682                with open(self.path, "r+b") as image_file:
683                    mbr = mbr_file.read(512)
684                    image_file.write(mbr)
685
686    def cleanup(self):
687        pass
688
689    def assemble(self):
690        logger.debug("Installing partitions")
691
692        for part in self.partitions:
693            source = part.source_file
694            if source:
695                # install source_file contents into a partition
696                sparse_copy(source, self.path, seek=part.start * self.sector_size)
697
698                logger.debug("Installed %s in partition %d, sectors %d-%d, "
699                             "size %d sectors", source, part.num, part.start,
700                             part.start + part.size_sec - 1, part.size_sec)
701
702                partimage = self.path + '.p%d' % part.num
703                os.rename(source, partimage)
704                self.partimages.append(partimage)
705