source: rtems-tools/misc/tools/boot.py @ 98f2f02

5
Last change on this file since 98f2f02 was 98f2f02, checked in by Chris Johns <chrisj@…>, on 05/30/19 at 10:37:50

misc/boot-image: Add a tool to create boot images.

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