source: rtems-tools/misc/tools/boot.py @ 6f99d80

5
Last change on this file since 6f99d80 was 6f99d80, checked in by Chris Johns <chrisj@…>, on 06/13/19 at 08:13:06

misc/boot-image: Fix Linux bugs.

  • Property mode set to 100644
File size: 46.7 KB
Line 
1#
2# RTEMS Tools Project (http://www.rtems.org/)
3# Copyright 2019 Chris Johns (chrisj@rtems.org)
4# All rights reserved.
5#
6# This file is part of the RTEMS Tools package in 'rtems-tools'.
7#
8# Permission to use, copy, modify, and/or distribute this software for any
9# purpose with or without fee is hereby granted, provided that the above
10# copyright notice and this permission notice appear in all copies.
11#
12# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19
20#
21# This code builds a bootloader image for an SD card for a range of
22# boards on a range of hosts.
23#
24
25from __future__ import print_function
26
27import argparse
28import copy
29import datetime
30import os
31import sys
32import tempfile
33
34from rtemstoolkit import check
35from rtemstoolkit import configuration
36from rtemstoolkit import error
37from rtemstoolkit import execute
38from rtemstoolkit import host
39from rtemstoolkit import log
40from rtemstoolkit import macros
41from rtemstoolkit import path
42from rtemstoolkit import version
43
44def _check_exes(exes):
45    ok = True
46    first = True
47    for exe in exes:
48        log.output('check exe: %s' % (exe))
49        if not check.check_exe(None, exe):
50            if first:
51                log.notice('Host executable(s) not found:')
52                first = False
53            log.notice(' %s' % (exe))
54            ok = False
55    return ok
56
57def _command(cmd, cwd):
58    e = execute.capture_execution()
59    cwd = path.abspath(cwd)
60    log.output('>> cwd: %s' % (cwd))
61    log.output('> %s' % (cmd))
62    exit_code, proc, output = e.shell(cmd, cwd = path.host(cwd))
63    output_split = output.split(os.linesep)
64    if len(output_split) >= 1 and len(output_split[0]) > 0:
65        log.output(['> ' + l for l in output_split])
66    log.output('> exit: %d' % (exit_code))
67    if exit_code != 0:
68        err = 'executing failure: (exit:%d) %s' % (exit_code, cmd)
69        raise error.general(err)
70    return output
71
72siunits = { 'g': 1024 * 1024 * 1024,
73            'm': 1024 * 1024,
74            'k': 1024 }
75
76def _si_units(units):
77    if units not in siunits:
78        raise error.general('invalid SI unit: %s' % (units))
79    return siunits[units]
80
81def _si_parse_size(size):
82    orig = size
83    units = 1
84    suffix = ''
85    if size[-1].isalpha():
86        suffix = size[-1]
87        if suffix not in list(siunits.keys()):
88            err = 'invalid SI unit (k, m, g): %s: %s' % (suffix, size)
89            raise error.general(err)
90        units = siunits[suffix]
91        size = size[:-1]
92    if not size.isdigit():
93        raise error.general('invalid size: %s' % (orig))
94    size = int(size)
95    return size, suffix, size * units
96
97def _si_size(size):
98    si_s, si_u, size = _si_parse_size(size)
99    return size
100
101def _si_size_units(size):
102    si_s, si_u, size = _si_parse_size(size)
103    return si_s, si_u
104
105def _si_from_size(size):
106    if isinstance(size, str):
107        value = int(size)
108        if str(value) != size:
109            return size
110        size = int(size)
111    for u in siunits:
112        value = int(size / siunits[u])
113        if value != 0:
114            return '%d%s' % (value, u)
115    return str(size)
116
117class bootloader(object):
118
119    mandatory_configs = [
120        'image_size',
121        'part_type',
122        'part_label',
123        'fs_format',
124        'fs_size',
125        'fs_alignment',
126        'tool_prefix',
127        'bootloaders'
128    ]
129
130    def __init__(self, command_path, build = None, bootloader = None):
131        #
132        # Check if there is a defaults.mc file under the command path. If so
133        # this is the tester being run from within the git repo. If not found
134        # assume the tools have been installed and the defaults is in the
135        # install prefix.
136        #
137        boot_ini = 'tools/config/rtems-boot.ini'
138        if path.exists(path.join(command_path, boot_ini)):
139            rtdir = command_path
140        else:
141            rtdir = '%{_prefix}/share/rtems'
142        boot_ini = '%s/%s' % (rtdir, boot_ini)
143        self.build = None
144        self.macros = macros.macros(rtdir = rtdir, show_minimal = True)
145        self.config = configuration.configuration(raw = False)
146        self.load_config(bootloader, self.macros.expand(boot_ini))
147        self.clean = True
148
149    def __getitem__(self, key):
150        if self.macros.has_key(key) and self.macros[key] != 'None':
151            r = self.macros.expand('%%{%s}' % (key))
152            if r == '1':
153                return True
154            elif r == '0':
155                return False
156            else:
157                return r
158        return None
159
160    def __setitem__(self, key, value):
161        if value is None:
162            value = 'None'
163        elif isinstance(value, bool):
164            value = '1' if value else '0'
165        elif isinstance(value, int):
166            value = str(value)
167        self.macros[key] = value
168
169    def get_mandatory_configs(self):
170        return self.mandatory_configs
171
172    def check_mandatory_configs(self):
173        for c in self.get_mandatory_configs():
174            if not self.macros.has_key(c):
175                raise error.general('boot config missing: %s' % (c))
176
177    def load_config(self, bootloader, config):
178        self.config.load(config)
179        #
180        # Check the config file has the basic data and structure.
181        #
182        bootloaders = self.config.comma_list('default', 'bootloaders')
183        for bl in bootloaders:
184            if not self.config.has_section(bl):
185                raise error.general('boot config: missing bootloader section: %s' % (bl))
186            for b in self.config.comma_list(bl, 'boards'):
187                if not self.config.has_section(b):
188                    raise error.general('boot config: missing board section: %s' % (b))
189        #
190        # Is the bootloader valid?
191        #
192        if bootloader is not None and bootloader not in bootloaders:
193            raise error.general('boot config: unknown bootloader: %s' % (bootloader))
194        self.macros['bootloader'] = str(bootloader)
195        self.macros['version_str'] = version.string()
196        self.macros['version'] = str(version.version())
197        self.macros['revision'] = str(version.revision())
198        if version.released():
199            self.macros['released'] = '1'
200        #
201        # Map the config to macros. The [default] section is global. The
202        # remaining sections are added as macro maps so the specalised
203        # bootloaders can enable reading from a macro map to provide specific
204        # values for a specific config such as a board.
205        #
206        for s in self.config.get_sections():
207            if s != 'default':
208                self.macros.set_write_map(s, add = True)
209            for i in self.config.get_items(s):
210                self.macros[i[0]] = i[1]
211            self.macros.unset_write_map()
212        self.macros.set_read_map('global')
213        if bootloader is not None:
214            self.macros.set_read_map(bootloader)
215
216    def list_boards(self):
217        boards = { }
218        for bl in self.config.comma_list('default', 'bootloaders'):
219            boards[bl] = self.config.comma_list(bl, 'boards')
220        return boards
221
222    def section_macro_map(self, section, nesting_level = 0):
223        nesting_level += 1
224        if nesting_level >= 100:
225            err = 'boot config: too many map levels (looping?): %s' % (section)
226            raise error.general(err)
227        if section not in self.macros.maps():
228            raise error.general('boot config: maps section not found: %s' % (section))
229        self.macros.set_read_map(section)
230        for s in self.config.comma_list(section, 'uses', err = False):
231            self.section_macro_map(s, nesting_level)
232
233    def boards(self):
234         return self.config.comma_list(self['bootloader'], 'boards')
235
236    def log(self):
237        log.output('Configuration:')
238        log.output('    Bootloader: {0}'.format(self.macros['bootloader']))
239        log.output('         Image: {0}'.format(self.macros['output']))
240        log.output('    Image Size: {0}'.format(self.macros['image_size']))
241        log.output('     Part Type: {0}'.format(self.macros['part_type']))
242        log.output('     FS Format: {0}'.format(self.macros['fs_format']))
243        log.output('       FS Size: {0}'.format(self.macros['fs_size']))
244        log.output('      FS Align: {0}'.format(self.macros['fs_alignment']))
245        log.output('        Kernel: {0}'.format(self.macros['kernel']))
246        log.output('           FDT: {0}'.format(self.macros['fdt']))
247        log.output('      Net DHCP: {0}'.format(self.macros['net_dhcp']))
248        log.output('        Net IP: {0}'.format(self.macros['net_ip']))
249        log.output(' Net Server IP: {0}'.format(self.macros['net_server_ip']))
250        log.output('      Net File: {0}'.format(self.macros['net_exe']))
251        log.output('       Net FDT: {0}'.format(self.macros['net_fdt']))
252        log.output('         Files: {0}'.format(len(self.files())))
253        log.output('Macros:')
254        macro_str = str(self.macros).split(os.linesep)
255        log.output(os.linesep.join([' ' + l for l in macro_str]))
256
257    def get_exes(self):
258        return []
259
260    def check_exes(self):
261        return _check_exes(self.get_exes())
262
263    def files(self):
264        return [f.strip() for f in self.comma_split(self['files']) if len(f) > 0]
265
266    def install_files(self, image, mountpoint):
267        pass
268
269    def install_configuration(self, image, mountpoint):
270        pass
271
272    def kernel_image(self):
273        return self['kernel_image'].replace('@KERNEL@', path.basename(self['kernel']))
274
275    def fdt_image(self):
276        return self['fdt_image'].replace('@FDT@', path.basename(self['fdt']))
277
278    def filter_text(self, lines):
279        out = []
280        for line in lines:
281            if '@KERNEL@' in line:
282                line = line.replace('@KERNEL@', path.basename(self['kernel']))
283            if '@KERNEL_IMAGE@' in line:
284                line = line.replace('@KERNEL_IMAGE@', self.kernel_image())
285            if '@FDT@' in line:
286                line = line.replace('@FDT@', path.basename(self['fdt']))
287            if '@FDT_IMAGE@' in line:
288                line = line.replace('@FDT_IMAGE@', self.fdt_image())
289            if '@NET_SERVER_IP@' in line and self['net_server_ip']is not None:
290                line = line.replace('@NET_SERVER_IP@', self['net_server_ip'])
291            if '@NET_IP@' in line and self['net_ip'] is not None:
292                line = line.replace('@NET_IP@', self['net_ip'])
293            if '@NET_BOOTEXE@' in line and self['net_exe'] is not None:
294                line = line.replace('@NET_BOOTEXE@', self['net_exe'])
295            if '@NET_BOOTFDT@' in line and self['net_fdt'] is not None:
296                line = line.replace('@NET_BOOTFDT@', self['net_fdt'])
297            out += [line]
298        return out
299
300    def comma_split(self, value):
301        if value is not None:
302            return [s.strip() for s in value.split(',')]
303        return []
304
305class uboot_bootloader(bootloader):
306
307    def __init__(self, command_path, build, convert_kernel, paths, board):
308        self.uboot = { 'paths': paths, 'board': board }
309        self.convert_kernel = convert_kernel
310        super(uboot_bootloader, self).__init__(command_path, build, 'u-boot')
311        if self.board() not in self.boards():
312            raise error.general('board not found: %s' %(self.board()))
313        log.output('Board: %s' % (self.board()))
314        self.section_macro_map(self.board())
315        self.macros.set_read_map(self['bootloader'] + '-templates')
316        self.macros.lock_read_map()
317        self._check_frist_second_stages()
318
319    def _check_frist_second_stages(self):
320        if not self.convert_kernel:
321            if self['first_stage'] is not None and \
322               not path.exists(self['first_stage']):
323                err = 'u-boot: first stage loader not found: %s' % \
324                      (self['first_stage'])
325                raise error.general(err)
326            if self['second_stage'] is not None and \
327               not path.exists(self['second_stage']):
328                err = 'u-boot: second stage loader not found: %s' % \
329                      (self['second_stage'])
330                raise error.general(err)
331
332    def load_config(self, bootloader, config):
333        super(uboot_bootloader, self).load_config(bootloader, config)
334        if not self.convert_kernel:
335            paths_count = len(self.uboot['paths'])
336            if paths_count == 1:
337                self.macros['ubootdir'] = path.abspath(self.uboot['paths'][0])
338            elif paths_count == 2:
339                    self.macros['first_stage'] = self.uboot['paths'][0]
340                    self.macros['second_stage'] = self.uboot['paths'][1]
341            else:
342                raise error.general('u-boot: invalid number of paths')
343        self.macros['mkimage'] = 'mkimage'
344
345    def get_mandatory_configs(self):
346        cfgs = super(uboot_bootloader, self).get_mandatory_configs()
347        return cfgs + ['objcopy',
348                       'arch',
349                       'vendor',
350                       'board',
351                       'config_name',
352                       'first_stage',
353                       'second_stage',
354                       'kernel_converter']
355
356    def board(self):
357        return self.uboot['board']
358
359    def get_exes(self):
360        exes = super(uboot_bootloader, self).get_exes()
361        if self['executables'] is not None:
362            exes += self.comma_split(self['executables'])
363        return exes
364
365    def install_files(self, image, mountpoint):
366        if self['kernel'] is not None:
367            kernel_image = self.kernel_convert(image, self['kernel'])
368            if self.convert_kernel:
369                path.copy(kernel_image, self['output'])
370            else:
371                image.install(kernel_image, mountpoint)
372        if self['fdt'] is not None:
373            fdt_image = self.fdt_convert(image, self['fdt'])
374            image.install(fdt_image, mountpoint)
375
376    def install_configuration(self, image, mountpoint):
377        uenv_txt = self['uenv_txt']
378        if uenv_txt is not None:
379            log.output('Uenv txt: %s' % (uenv_txt))
380            image.install(path.abspath(uenv_txt), mountpoint)
381        else:
382            template = None
383            if self['net_dhcp'] or self['net_ip'] is not None:
384                if self['net_dhcp']:
385                    template = 'uenv_net_dhcp'
386                else:
387                    template = 'uenv_net_static'
388                if self['net_server_ip']:
389                    template += '_sip'
390                if self['net_fdt']:
391                    template += '_net_fdt'
392            else:
393                if self['kernel'] is not None:
394                    template = 'uenv_exe'
395                elif self['fdt'] is not None:
396                    template = 'uenv'
397                if self['fdt'] is not None:
398                    template += '_fdt'
399            if template is not None:
400                log.notice('Uenv template: %s' % (template))
401                uenv_start = self.comma_split(self['uenv_start'])
402                uenv_body = self.comma_split(self[template])
403                uenv_end = self.comma_split(self['uenv_end'])
404                uenv = uenv_start + uenv_body + uenv_end
405                image.install_text(self.filter_text(uenv),
406                                   path.join(mountpoint, self['boot_config']))
407
408    def kernel_convert(self, image, kernel):
409        dst = path.join(path.abspath(self['build']), path.basename(kernel))
410        self['kernel_build'] = dst
411        log.output('Copy (into build): %s -> %s' % (kernel, dst))
412        image.clean_path(dst)
413        path.copy(kernel, dst)
414        cmds = self.filter_text(self.comma_split(self['kernel_converter']))
415        for cmd in cmds:
416            _command(cmd, self['build'])
417        return self['kernel_image'].replace('@KERNEL@', dst)
418
419    def fdt_convert(self, image, fdt):
420        dst = path.join(path.abspath(self['build']), path.basename(fdt))
421        self['fdt_build'] = dst
422        log.output('Copy (into build): %s -> %s' % (fdt, dst))
423        image.clean_path(dst)
424        path.copy(fdt, dst)
425        return self['fdt_image'].replace('@FDT@', dst)
426
427class image(object):
428
429    def __init__(self, bootloader):
430        self.loader = bootloader
431        self.detach_images = []
432        self.unmount_paths = []
433        self.remove_paths = []
434
435    def build(self):
436        #
437        # Cleanup if any goes wrong.
438        #
439        try:
440            #
441            # Ge the absolute paths to fixed locations.
442            #
443            output = path.abspath(self.loader['output'])
444            build = path.abspath(self.loader['build'])
445            mountpoint = path.join(build, 'mnt')
446
447            #
448            # Create any paths we need. They are removed when this object is
449            # deleted.
450            #
451            self.create_path(build)
452
453            #
454            # If only coverting a kernel no need to create an image.
455            #
456            if not self.loader.convert_kernel:
457                self.create_path(mountpoint)
458
459                #
460                # Create the blank image file. This is attached as a device,
461                # partitioned, formatted and the files written to it.
462                #
463                log.notice('Create image: %s size %s' % (self.loader['output'],
464                                                         self.loader['image_size']))
465                self.image_create(output, self.loader['image_size'])
466
467                #
468                # Attach the image so it is a device.
469                #
470                log.notice('Attach image to device: %s' % (self.loader['output']))
471                device = self.image_attach(output)
472
473                #
474                # Partition the image. The device may change.
475                #
476                log.notice('Partition device: %s as %s' % (device,
477                                                           self.loader['part_type']))
478                device = self.partition(output,
479                                        device,
480                                        self.loader['part_type'],
481                                        self.loader['part_label'],
482                                        self.loader['fs_format'],
483                                        self.loader['fs_size'],
484                                        self.loader['fs_alignment'])
485                part = self.device_partition(device, 1)
486
487                #
488                # Format the first partition.
489                #
490                log.notice('Format: %s as %s' % (part, self.loader['fs_format']))
491                self.format_partition(part,
492                                      self.loader['fs_format'],
493                                      self.loader['part_label'])
494
495                #
496                # Mount the file system.
497                #
498                log.notice('Mount: %s' % (part))
499                self.mount(self.loader['fs_format'], part, mountpoint)
500
501                #
502                # Install the first stage and second stage boot loaders.
503                #
504                self.install(self.loader['first_stage'], mountpoint)
505                self.install(self.loader['second_stage'], mountpoint)
506
507            #
508            # Install the bootload files.
509            #
510            self.loader.install_files(self, mountpoint)
511
512            if not self.loader.convert_kernel:
513                #
514                # Install the bootloader configuration.
515                #
516                self.loader.install_configuration(self, mountpoint)
517
518                #
519                # Install any user files if present.
520                #
521                for f in self.loader.files():
522                    self.install(f, mountpoint)
523
524            #
525            # Done.
526            #
527            log.notice('Finished')
528        finally:
529            self.cleanup()
530
531    def install(self, src, dst):
532        src_base = path.basename(src)
533        log.notice('Install: %s' % (src_base))
534        asrc = path.abspath(src)
535        adst =  path.join(path.abspath(dst), src_base)
536        log.output('Copy: %s -> %s' % (asrc, adst))
537        path.copy(asrc, adst)
538
539    def install_text(self, text, dst):
540        dst_base = path.basename(dst)
541        log.notice('Install: %s' % (dst_base))
542        adst =  path.abspath(dst)
543        log.output('Copy: text[%d] -> %s' % (len(text), adst))
544        log.output([' ] ' + l for l in text])
545        with open(adst, "w") as o:
546            o.write(os.linesep.join(text))
547
548    def image_create(self, path_, size):
549        self.host_image_create(path_, size, path.exists(path_))
550
551    def image_attach(self, path_):
552        device = self.host_image_attach(path_)
553        self.detach_images += [device]
554        return device
555
556    def image_detach(self, device):
557        if device in self.detach_images:
558            self.detach_images.remove(device)
559            self.host_image_detach(device)
560
561    def partition(self, image_, device, ptype, plabel, pformat, psize, palign):
562        return self.host_partition(image_, device,
563                                   ptype, plabel, pformat, psize, palign)
564
565    def format_partition(self, device, pformat, plabel):
566        self.host_format_partition(device, pformat, plabel)
567
568    def device_partition(self, device, pindex):
569        return self.host_device_partition(device, pindex)
570
571    def mount(self, pformat, device, path_):
572        if path_ not in self.unmount_paths:
573            self.host_mount(pformat, device, path_)
574            self.unmount_paths += [path_]
575
576    def unmount(self, path_):
577        if path_ in self.unmount_paths:
578            self.host_unmount(path_)
579            self.unmount_paths.remove(path_)
580
581    def cleanup(self):
582        log.notice('Cleaning up')
583        for m in self.unmount_paths:
584            log.output('unmount: %s' % (m))
585            self.unmount(m)
586        for i in self.detach_images:
587            log.output('detach: %s' % (i))
588            self.image_detach(i)
589        if self.loader.clean:
590            for r in self.remove_paths:
591                if path.exists(r):
592                    log.output('remove: %s' % (r))
593                    path.removeall(r)
594
595    def get_exes(self):
596        return ['dd']
597
598    def check_exes(self):
599        return _check_exes(self.get_exes())
600
601    def clean_path(self, name):
602        self.remove_paths += [name]
603
604    def create_path(self, where, recreate = True, cleanup = True):
605        if path.exists(where):
606            log.output('remove: %s' % (where))
607            path.removeall(where)
608        try:
609            log.output('make: %s' % (where))
610            path.mkdir(where)
611        except:
612            raise error.general('cannot create build path: %s' % (where))
613        if not path.isreadable(where):
614            raise error.general('build path is not readable: %s' % (where))
615        if not path.iswritable(where):
616            raise error.general('build path is not writeable: %s' % (where))
617        if cleanup:
618            self.remove_paths += [where]
619
620    def command(self, cmd):
621        return _command(cmd, self.loader['build'])
622
623    def host_image_create(self, path_, size, exists):
624        img_size, img_units = _si_size_units(size)
625        self.command('dd if=/dev/zero of=%s bs=1%s count=%d' % (path_,
626                                                                img_units,
627                                                                img_size))
628
629    def host_image_attach(self, path_):
630        raise error.general('no platform support: host_image_attach')
631
632    def host_image_detach(self, device):
633        raise error.general('no platform support: host_image_detach')
634
635    def host_partition(self, image_, device, ptype, plabel, pformat, psize, palign):
636        raise error.general('no platform support: host_partition')
637
638    def host_format_partition(self, device, pformat, plabel):
639        raise error.general('no platform support: host_format_partition')
640
641    def host_device_partition(self, device, pindex):
642        raise error.general('no platform support: host_device_partition')
643
644    def host_mount(self, pformat, device, path_):
645        raise error.general('no platform support: host_mount')
646
647    def host_unmount(self, path_):
648        raise error.general('no platform support: host_unmount')
649
650class freebsd_image(image):
651    def __init__(self, loader):
652        super(freebsd_image, self).__init__(loader)
653
654    def get_exes(self):
655        exes = super(freebsd_image, self).get_exes()
656        return exes + ['mdconfig',
657                       'gpart',
658                       'newfs_msdos',
659                       'mount',
660                       'umount']
661
662    def host_image_attach(self, path_):
663        return self.command('sudo mdconfig -f %s' % (path_))
664
665    def host_image_detach(self, device):
666        self.command('sudo mdconfig -d -u %s' % (device))
667
668    def host_partition(self, image_, device, ptype, plabel, pformat, psize, palign):
669        types = { 'MBR': 'MBR' }
670        formats = { 'fat16': 'fat16',
671                    'fat32': 'fat32' }
672        if ptype not in types:
673            err = 'unknown type of partitioning: %s' % (ptype)
674            raise error.general(err)
675        if pformat not in formats:
676            raise error.general('unknown format: %s' % (pformat))
677        self.command('sudo gpart create -s %s %s' % (types[ptype], device))
678        self.command('sudo gpart add -s %s -t %s -a %s %s' % (_si_from_size(psize),
679                                                              formats[pformat],
680                                                              palign,
681                                                              device))
682        self.command('sudo gpart set -a active -i 1 %s' % (device))
683        return device
684
685    def host_format_partition(self, device, pformat, plabel):
686        formats = { 'fat16': ('newfs_msdos', '16'),
687                    'fat32': ('newfs_msdos', '32') }
688        if pformat not in formats:
689            raise error.general('unknown format: %s' % (pformat))
690        self.command('sudo %s -F %s %s' % (formats[pformat][0],
691                                           formats[pformat][1],
692                                           device))
693
694    def host_device_partition(self, device, pindex):
695        return '/dev/%ss%d' % (device, pindex)
696
697    def host_mount(self, pformat, device, path_):
698        formats = { 'fat16': 'msdos',
699                    'fat32': 'msdos' }
700        if pformat not in formats:
701            raise error.general('unknown format: %s' % (pformat))
702        self.command('sudo mount -t %s %s %s' % (formats[pformat],
703                                                 device,
704                                                 path_))
705
706    def host_unmount(self, path_):
707        self.command('sudo umount %s' % (path_))
708
709class linux_image(image):
710    def __init__(self, loader):
711        super(linux_image, self).__init__(loader)
712
713    def get_exes(self):
714        exes = super(linux_image, self).get_exes()
715        return exes + ['losetup',
716                       'fdisk',
717                       'mkfs.fat',
718                       'mount',
719                       'umount']
720
721    def host_image_create(self, path_, size, exists):
722        img_size, img_units = _si_size_units(size)
723        self.command('dd if=/dev/zero of=%s bs=%s count=%d' % (path_,
724                                                               _si_units(img_units),
725                                                               img_size))
726    def host_image_attach(self, path_):
727        return self.command('sudo losetup --partscan --find --show %s' % (path_))
728
729    def host_image_detach(self, device):
730        self.command('sudo losetup --detach %s' % (device))
731
732    def host_partition(self, image_, device, ptype, plabel, pformat, psize, palign):
733        types = { 'MBR': 'MBR' }
734        formats = { 'fat16': '6',
735                    'fat32': 'b' }
736        if ptype not in types:
737            err = 'unknown type of partitioning: %s' % (ptype)
738            raise error.general(err)
739        if pformat not in formats:
740            raise error.general('unknown format: %s' % (pformat))
741        #
742        # Datch the loop back device, we use fdisk on the image to avoid any
743        # kernel errors related to re-reading the updated partition data.
744        #
745        self.host_image_detach(device)
746        #
747        # This awkward exchange is needed to script fdisk, hmmm.
748        #
749        with tempfile.NamedTemporaryFile() as tmp:
750            s = os.linesep.join(['o',   # create a new empty part table
751                                 'n',   # add a new partition
752                                 'p',   # primary
753                                 '1',   # partition 1
754                                 '%d' % (_si_size(palign) / 512),
755                                 '%d' % (_si_size(psize) / 512),
756                                 't',   # change a partition type
757                                 '%s' % (formats[pformat]), # hex code
758                                 'a',   # toggle a bootable flag
759                                 'p',   # print
760                                 'w',   # write table to disk and exit
761                                 ''])
762            log.output('fdisk script:')
763            log.output(s)
764            tmp.write(s.encode())
765            tmp.seek(0)
766            self.command('cat %s | fdisk -t %s %s' % (tmp.name,
767                                                      types[ptype],
768                                                      image_))
769        return self.host_image_attach(image_)
770
771    def host_format_partition(self, device, pformat, plabel):
772        formats = { 'fat16': ('mkfs.fat', '16'),
773                    'fat32': ('mkfs.fat', '32') }
774        if pformat not in formats:
775            raise error.general('unknown format: %s' % (pformat))
776        self.command('sudo %s -F %s -n %s %s' % (formats[pformat][0],
777                                                 formats[pformat][1],
778                                                 plabel,
779                                                 device))
780
781    def host_device_partition(self, device, pindex):
782        return '%sp%d' % (device, pindex)
783
784    def host_mount(self, pformat, device, path_):
785        options = { 'fat16': '-o uid=%d' % (os.getuid()),
786                    'fat32': '-o uid=%d' % (os.getuid()) }
787        if pformat in options:
788            opts = options[pformat]
789        else:
790            opts = ''
791        self.command('sudo mount %s %s %s' % (opts, device, path_))
792
793    def host_unmount(self, path_):
794        self.command('sudo umount %s' % (path_))
795
796class darwin_image(image):
797    def __init__(self, loader):
798        super(darwin_image, self).__init__(loader)
799        if not self.loader['output'].endswith('.img'):
800            log.notice('Output file does not end with `.img`. ' + \
801                       'Needed on MacOS due to a bug (Id: 51283993)')
802            raise error.general('output file does not end with `.img`')
803
804    def get_exes(self):
805        exes = super(darwin_image, self).get_exes()
806        return exes + ['hdiutil',
807                       'diskutil',
808                       'fdisk']
809
810    def host_image_attach(self, path_):
811        output = self.command('sudo hdiutil attach %s -nomount -nobrowse' % (path_))
812        if len(output.split(os.linesep)) != 1 or not output.startswith('/dev/'):
813            raise error.general('invalid hdiutil attach outputl; see log')
814        return output.strip()
815
816    def host_image_detach(self, device):
817        self.command('sudo hdiutil detach %s' % (device))
818
819    def host_partition(self, image_, device, ptype, plabel, pformat, psize, palign):
820        types = { 'MBR': 'MBR' }
821        formats = { 'fat16': 'MS-DOS FAT16',
822                    'fat32': 'MS-DOS FAT32' }
823        if ptype not in types:
824            err = 'unknown type of partitioning: %s' % (ptype)
825            raise error.general(err)
826        if pformat not in formats:
827            raise error.general('unknown format: %s' % (pformat))
828        #
829        # Align the parition by adding free space before. Sign.
830        #
831        cmd  = "sudo diskutil partitionDisk %s 2 %s " % (device, types[ptype])
832        cmd += "'Free Space' '%%noformat%%' %s " % (palign)
833        cmd += "'%s' %s %s" % (formats[pformat], plabel, psize)
834        self.command(cmd)
835        #
836        # MacOS mounts the filesystem once the partitioning has finished,
837        # unmount it as we have no control over the mountpoint.
838        #
839        self.command('sudo diskutil unmountDisk %s' % (device))
840        #
841        # This awkward exchange is needed to set the active bit.
842        #
843        with tempfile.NamedTemporaryFile() as tmp:
844            s = os.linesep.join(['f 1', # flag toggle on partition 1
845                                 'w',   # write
846                                 'p',   # print
847                                 'q',   # quit
848                                 ''])
849            tmp.write(s.encode())
850            tmp.seek(0)
851            self.command('cat %s | sudo fdisk -y -e %s' % (tmp.name, device))
852        return device
853
854    def host_format_partition(self, device, pformat, plabel):
855        log.output(' * No format stage; done when partitioning')
856
857    def host_device_partition(self, device, pindex):
858        return '%ss%d' % (device, pindex)
859
860    def host_mount(self, pformat, device, path_):
861        self.command('sudo diskutil mount -mountPoint %s %s' % (path_, device))
862
863    def host_unmount(self, path_):
864        self.command('sudo diskutil unmount %s' % (path_))
865
866builders = {
867    'freebsd': freebsd_image,
868    'linux'  : linux_image,
869    'darwin' : darwin_image
870}
871
872def load_log(logfile):
873    log.default = log.log(streams = [logfile])
874
875def log_default():
876    return 'rtems-log-boot-image.txt'
877
878class valid_dir(argparse.Action):
879    def __call__(self, parser, namespace, values, option_string = None):
880        if type(values) is not list:
881            values = [values]
882        for value in values:
883            if not path.isdir(value):
884                raise argparse.ArgumentError(self,
885                                             'is not a valid directory: %s' % (value))
886            if not path.isreadable(value):
887                raise argparse.ArgumentError(self, 'is not readable: %s' % (value))
888            if not path.iswritable(value):
889                raise argparse.ArgumentError(self, 'is not writeable: %s' % (value))
890            setattr(namespace, self.dest, value)
891
892class valid_file(argparse.Action):
893    def __call__(self, parser, namespace, value, option_string = None):
894        current = getattr(namespace, self.dest)
895        if not isinstance(current, list) and current is not None:
896            raise argparse.ArgumentError(self,
897                                         ' already provided: %s, have %s' % (value,
898                                                                             current))
899        if not path.isfile(value):
900            raise argparse.ArgumentError(self, 'is not a valid file: %s' % (value))
901        if not path.isreadable(value):
902            raise argparse.ArgumentError(self, 'is not readable: %s' % (value))
903        if current is not None:
904            value = current + [value]
905        setattr(namespace, self.dest, value)
906
907class valid_file_if_exists(argparse.Action):
908    def __call__(self, parser, namespace, value, option_string = None):
909        current = getattr(namespace, self.dest)
910        if not isinstance(current, list) and current is not None:
911            raise argparse.ArgumentError(self,
912                                         ' already provided: %s, have %s' % (value,
913                                                                             current))
914        if path.exists(value):
915            if not path.isfile(value):
916                raise argparse.ArgumentError(self, 'is not a valid file: %s' % (value))
917            if not path.isreadable(value):
918                raise argparse.ArgumentError(self, 'is not readable: %s' % (value))
919        if current is not None:
920            value = current + [value]
921        setattr(namespace, self.dest, value)
922
923class valid_paths(argparse.Action):
924    def __call__(self, parser, namespace, value, option_string = None):
925        current = getattr(namespace, self.dest)
926        if current is None:
927            current = []
928        if isinstance(value, list):
929            values = value
930        else:
931            values = [values]
932        for value in values:
933            if not path.isfile(value) and not path.isdir(value):
934                err = 'is not a valid file or directory: %s' % (value)
935                raise argparse.ArgumentError(self, err)
936            if not path.isreadable(value):
937                raise argparse.ArgumentError(self, 'is not readable: %s' % (value))
938            current += [value]
939        setattr(namespace, self.dest, current)
940
941class valid_format(argparse.Action):
942    def __call__(self, parser, namespace, value, option_string = None):
943        current = getattr(namespace, self.dest)
944        if not isinstance(current, list) and current is not None:
945            raise argparse.ArgumentError(self,
946                                         ' already provided: %s, have %s' % (value,
947                                                                             current))
948        if value not in ['fat16', 'fat32']:
949            raise argparse.ArgumentError(self, ' invalid format: %s' % (value))
950        setattr(namespace, self.dest, value)
951
952class valid_si(argparse.Action):
953    def __call__(self, parser, namespace, value, option_string = None):
954        current = getattr(namespace, self.dest)
955        units = len(value)
956        if value[-1].isalpha():
957            if value[-1] not in ['k', 'm', 'g']:
958                raise argparse.ArgumentError(self,
959                                             'invalid SI (k, m, g): %s' % (value[-1]))
960            units = -1
961        if not value[:units].isdigit():
962            raise argparse.ArgumentError(self, 'invalid SI size: %s' % (value))
963        setattr(namespace, self.dest, value)
964
965class valid_ip(argparse.Action):
966    def __call__(self, parser, namespace, value, option_string = None):
967        current = getattr(namespace, self.dest)
968        if current is not None:
969            raise argparse.ArgumentError(self,
970                                         ' already provided: %s, have %s' % (value,
971                                                                             current))
972        setattr(namespace, self.dest, value)
973
974def run(args = sys.argv, command_path = None):
975    ec = 0
976    notice = None
977    builder = None
978    try:
979        description  = 'Provide one path to a u-boot build or provide two '
980        description += 'paths to the built the first and second stage loaders, '
981        description += 'for example a first stage loader is \'MLO\' and a second '
982        description += '\'u-boot.img\'. If converting a kernel only provide the '
983        description += 'executable\'s path.'
984
985        argsp = argparse.ArgumentParser(prog = 'rtems-boot-image',
986                                        description = description)
987        argsp.add_argument('-l', '--log',
988                           help = 'log file (default: %(default)s).',
989                           type = str, default = log_default())
990        argsp.add_argument('-v', '--trace',
991                           help = 'enable trace logging for debugging.',
992                           action = 'store_true')
993        argsp.add_argument('-s', '--image-size',
994                           help = 'image size in mega-bytes (default: %(default)s).',
995                           type = str, action = valid_si, default = '64m')
996        argsp.add_argument('-F', '--fs-format',
997                           help = 'root file system format (default: %(default)s).',
998                           type = str, action = valid_format, default = 'fat16')
999        argsp.add_argument('-S', '--fs-size',
1000                           help = 'root file system size in SI units ' + \
1001                                  '(default: %(default)s).',
1002                           type = str, action = valid_si, default = 'auto')
1003        argsp.add_argument('-A', '--fs-align',
1004                           help = 'root file system alignment in SI units ' + \
1005                                  '(default: %(default)s).',
1006                           type = str, action = valid_si, default = '1m')
1007        argsp.add_argument('-k', '--kernel',
1008                           help = 'install the kernel (default: %(default)r).',
1009                           type = str, action = valid_file, default = None)
1010        argsp.add_argument('-d', '--fdt',
1011                           help = 'Flat device tree source/blob (default: %(default)r).',
1012                           type = str, action = valid_file, default = None)
1013        argsp.add_argument('-f', '--file',
1014                           help = 'install the file (default: None).',
1015                           type = str, action = valid_file, default = [])
1016        argsp.add_argument('--net-boot',
1017                           help = 'configure a network boot using TFTP ' + \
1018                                  '(default: %(default)r).',
1019                           action = 'store_true')
1020        argsp.add_argument('--net-boot-dhcp',
1021                           help = 'network boot using dhcp (default: %(default)r).',
1022                           action = 'store_true', default = False)
1023        argsp.add_argument('--net-boot-ip',
1024                           help = 'network boot IP address (default: %(default)r).',
1025                           type = str, action = valid_ip, default = None)
1026        argsp.add_argument('--net-boot-server',
1027                           help = 'network boot server IP address ' + \
1028                                  '(default: %(default)r).',
1029                           type = str, action = valid_ip, default = None)
1030        argsp.add_argument('--net-boot-file',
1031                           help = 'network boot file (default: %(default)r).',
1032                           type = str, default = 'rtems.img')
1033        argsp.add_argument('--net-boot-fdt',
1034                           help = 'network boot load a fdt file (default: %(default)r).',
1035                           type = str, default = None)
1036        argsp.add_argument('-U', '--custom-uenv',
1037                           help = 'install the custom uEnv.txt file ' + \
1038                                  '(default: %(default)r).',
1039                           type = str, action = valid_file, default = None)
1040        argsp.add_argument('-b', '--board',
1041                           help = 'name of the board (default: %(default)r).',
1042                           type = str, default = 'list')
1043        argsp.add_argument('--convert-kernel',
1044                           help = 'convert a kernel to a bootoader image ' + \
1045                                  '(default: %(default)r).',
1046                           action = 'store_true', default = False)
1047        argsp.add_argument('--build',
1048                           help = 'set the build directory (default: %(default)r).',
1049                           type = str, default = 'ribuild')
1050        argsp.add_argument('--no-clean',
1051                           help = 'do not clean when finished (default: %(default)r).',
1052                           action = 'store_false', default = True)
1053        argsp.add_argument('-o', '--output',
1054                           help = 'image output file name',
1055                           type = str, action = valid_file_if_exists, required = True)
1056        argsp.add_argument('paths',
1057                           help = 'files or paths, the number and type sets the mode.',
1058                           nargs = '+', action = valid_paths)
1059
1060        argopts = argsp.parse_args(args[1:])
1061
1062        load_log(argopts.log)
1063        log.notice('RTEMS Tools - Boot Image, %s' % (version.string()))
1064        log.output(log.info(args))
1065        log.tracing = argopts.trace
1066
1067        if argopts.net_boot_dhcp or \
1068           argopts.net_boot_ip is not None:
1069            if argopts.convert_kernel:
1070                raise error.general('net boot options not valid with kernel convert.')
1071            if argopts.custom_uenv is not None:
1072                raise error.general('cannot set custom uenv and net boot options.')
1073
1074        host.load()
1075
1076        log.output('Platform: %s' % (host.name))
1077
1078        if argopts.board == 'list':
1079            loader = bootloader(command_path)
1080            boards = loader.list_boards()
1081            log.notice(' Board list: bootloaders (%d)' % (len(boards)))
1082            for bl in sorted(boards):
1083                log.notice('  %s: %d' % (bl, len(boards[bl])))
1084                for b in boards[bl]:
1085                    log.notice('   ' + b)
1086            raise error.exit()
1087
1088        loader = uboot_bootloader(command_path,
1089                                  argopts.build,
1090                                  argopts.convert_kernel,
1091                                  argopts.paths,
1092                                  argopts.board)
1093
1094        loader.check_mandatory_configs()
1095
1096        if loader.convert_kernel:
1097            if argopts.kernel is not None:
1098                raise error.general('kernel convert does not use the kernel option.')
1099            if len(argopts.paths) != 1:
1100                raise error.general('kernel convert take a single path.')
1101            argopts.kernel = argopts.paths[0]
1102        else:
1103            loader.clean = argopts.no_clean
1104
1105        loader['build'] = argopts.build
1106        loader['board'] = argopts.board
1107        loader['output'] = argopts.output
1108        loader['image_size'] = argopts.image_size
1109        loader['fs_format'] = argopts.fs_format
1110        loader['fs_size'] = argopts.fs_size
1111        loader['fs_align'] = argopts.fs_align
1112        loader['kernel'] = argopts.kernel
1113        loader['fdt'] = argopts.fdt
1114        loader['files'] = ','.join(argopts.file)
1115        loader['net_dhcp'] = argopts.net_boot_dhcp
1116        loader['net_ip'] = argopts.net_boot_ip
1117        loader['net_server_ip'] = argopts.net_boot_server
1118        loader['net_exe'] = argopts.net_boot_file
1119        loader['net_fdt'] = argopts.net_boot_fdt
1120        loader['uenv_txt'] = argopts.custom_uenv
1121
1122        loader.log()
1123
1124        if not loader.convert_kernel:
1125            if loader['fs_size'] == 'auto':
1126                loader['fs_size'] = \
1127                    str(_si_size(loader['image_size']) - _si_size(loader['fs_align']))
1128            elif _si_size(loader['image_size']) < \
1129                 _si_size(loader['fs_align']) + _si_size(loader['fs_size']):
1130                raise error.general('filesystem partition size larger than image size.')
1131
1132        if host.name not in builders:
1133            err = 'no builder; platform not supported: %s' % (host.name)
1134            raise error.general(err)
1135
1136        builder = builders[host.name](loader)
1137
1138        if not loader.check_exes() or not builder.check_exes():
1139            raise error.general('command(s) not found; please fix.')
1140
1141        builder.build()
1142
1143    except error.general as gerr:
1144        notice = str(gerr)
1145        ec = 1
1146    except error.internal as ierr:
1147        notice = str(ierr)
1148        ec = 1
1149    except error.exit as eerr:
1150        pass
1151    except KeyboardInterrupt:
1152        notice = 'abort: user terminated'
1153        ec = 1
1154    except:
1155        raise
1156        notice = 'abort: unknown error'
1157        ec = 1
1158    if builder is not None:
1159        del builder
1160    if notice is not None:
1161        log.stderr(notice)
1162    sys.exit(ec)
1163
1164if __name__ == "__main__":
1165    run()
Note: See TracBrowser for help on using the repository browser.