From c8480b453af8940a0384aab71dbb2cccd2269485 Mon Sep 17 00:00:00 2001 From: Chris Johns Date: Thu, 30 May 2019 20:37:50 +1000 Subject: 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. --- misc/rtems-boot-image | 42 ++ misc/tools/boot.py | 1092 ++++++++++++++++++++++++++++++++++++++++++ misc/tools/cmd-boot-image.py | 44 ++ misc/tools/rtems-boot.ini | 126 +++++ misc/wscript | 15 + 5 files changed, 1319 insertions(+) create mode 100755 misc/rtems-boot-image create mode 100644 misc/tools/boot.py create mode 100755 misc/tools/cmd-boot-image.py create mode 100644 misc/tools/rtems-boot.ini diff --git a/misc/rtems-boot-image b/misc/rtems-boot-image new file mode 100755 index 0000000..3d2f07e --- /dev/null +++ b/misc/rtems-boot-image @@ -0,0 +1,42 @@ +#! /bin/sh +# +# RTEMS Tools Project (http://www.rtems.org/) +# Copyright 2019 Chris Johns (chrisj@rtems.org) +# All rights reserved. +# +# This file is part of the RTEMS Tools package in 'rtems-tools'. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +set -e +base=$(dirname $(dirname $0)) +cmd=tools/cmd-boot-image.py +PYTHON_WRAPPER=rtemstoolkit/python-wrapper.sh +if test -f ${base}/${PYTHON_WRAPPER}; then + PYTHON_CMD=${base}/misc/${cmd} + . ${base}/${PYTHON_WRAPPER} +elif test -f ${base}/share/rtems/${PYTHON_WRAPPER}; then + PYTHON_CMD=${base}/share/rtems/${cmd} + . ${base}/share/rtems/${PYTHON_WRAPPER} +fi +echo "error: RTEMS Toolkit python wrapper not found, please report" diff --git a/misc/tools/boot.py b/misc/tools/boot.py new file mode 100644 index 0000000..53253da --- /dev/null +++ b/misc/tools/boot.py @@ -0,0 +1,1092 @@ +# +# RTEMS Tools Project (http://www.rtems.org/) +# Copyright 2019 Chris Johns (chrisj@rtems.org) +# All rights reserved. +# +# This file is part of the RTEMS Tools package in 'rtems-tools'. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# +# This code builds a bootloader image for an SD card for a range of +# boards on a range of hosts. +# + +from __future__ import print_function + +import argparse +import copy +import datetime +import os +import sys +import tempfile + +from rtemstoolkit import check +from rtemstoolkit import configuration +from rtemstoolkit import error +from rtemstoolkit import execute +from rtemstoolkit import host +from rtemstoolkit import log +from rtemstoolkit import macros +from rtemstoolkit import path +from rtemstoolkit import version + +def _check_exes(exes): + ok = True + first = True + for exe in exes: + log.output('check exe: %s' % (exe)) + if not check.check_exe(None, exe): + if first: + log.notice('Host executable(s) not found:') + first = False + log.notice(' %s' % (exe)) + ok = False + return ok + +def _command(cmd, cwd): + e = execute.capture_execution() + cwd = path.abspath(cwd) + log.output('>> cwd: %s' % (cwd)) + log.output('> %s' % (cmd)) + exit_code, proc, output = e.shell(cmd, cwd = path.host(cwd)) + log.output(['> ' + l for l in output.split(os.linesep)]) + log.output('> exit: %d' % (exit_code)) + if exit_code != 0: + err = 'executing failure: (exit:%d) %s' % (exit_code, cmd) + raise error.general(err) + return output + + def si_units(self, units): + siunits = { 'k': 1024, + 'm': 1024 * 1024, + 'g': 1024 * 1024 * 1024 } + if units not in siunits: + raise error.general('invalid SI unit: %s' % (units)) + return siunits[units] + +def _si_parse_size(size): + orig = size + units = 1 + suffix = '' + if size[-1].isalpha(): + suffix = size[-1] + if suffix not in ['k', 'm', 'g']: + err = 'invalid SI unit (k, m, g): %s: %s' % (suffix, size) + raise error.general(err) + units = { 'k': 1024, + 'm': 1024 * 1024, + 'g': 1024 * 1024 * 1024 }[suffix] + size = size[:-1] + if not size.isdigit(): + raise error.general('invalid size: %s' % (orig)) + size = int(size) + return size, suffix, size * units + +def _si_size(size): + si_s, si_u, size = _si_parse_size(size) + return size + +def _si_size_units(size): + si_s, si_u, size = _si_parse_size(size) + return si_s, si_u + +class config(object): + + mandatory_configs = [ + 'image_size', + 'part_type', + 'part_label', + 'fs_format', + 'fs_size', + 'fs_alignment', + 'tool_prefix', + 'bootloaders' + ] + + def __init__(self, command_path, bootloader): + # + # Check if there is a defaults.mc file under the command path. If so + # this is the tester being run from within the git repo. If not found + # assume the tools have been installed and the defaults is in the + # install prefix. + # + boot_ini = 'tools/rtems-boot.ini' + if path.exists(path.join(command_path, boot_ini)): + rtdir = command_path + else: + rtdir = '%{_prefix}/share/rtems' + boot_ini = '%s/%s' % (rtdir, boot_ini) + self.build = 'build' + self.macros = macros.macros(rtdir = rtdir, show_minimal = True) + self.config = configuration.configuration(raw = False) + self.load_config(bootloader, boot_ini) + self.clean = True + + def __getitem__(self, key): + if self.macros.has_key(key) and self.macros[key] != 'None': + r = self.macros.expand('%%{%s}' % (key)) + if r == '1': + return True + elif r == '0': + return False + else: + return r + return None + + def __setitem__(self, key, value): + if value is None: + value = 'None' + self.macros[key] = value + + def get_mandatory_configs(self): + return self.mandatory_configs + + def check_mandatory_configs(self): + for c in self.get_mandatory_configs(): + if not self.macros.has_key(c): + raise error.general('boot config missing: %s' % (c)) + + def load_config(self, bootloader, config): + self.config.load(config) + # + # Check the config file has the basic data and structure. + # + bootloaders = self.config.comma_list('default', 'bootloaders') + for bl in bootloaders: + if not self.config.has_section(bl): + raise error.general('boot config: missing bootloader section: %s' % (bl)) + for b in self.config.comma_list(bl, 'boards'): + if not self.config.has_section(b): + raise error.general('boot config: missing board section: %s' % (b)) + # + # Is the bootloader valid? + # + if bootloader not in bootloaders: + raise error.general('boot config: unknown bootloader: %s' % (bootloader)) + self.macros['bootloader'] = bootloader + self.macros['version_str'] = version.string() + self.macros['version'] = str(version.version()) + self.macros['revision'] = str(version.revision()) + if version.released(): + self.macros['released'] = '1' + # + # Map the config to macros. The [default] section is global. The + # remaining sections are added as macro maps so the specalised + # bootloaders can enable reading from a macro map to provide specific + # values for a specific config such as a board. + # + for s in self.config.get_sections(): + if s != 'default': + self.macros.set_write_map(s, add = True) + for i in self.config.get_items(s): + self.macros[i[0]] = i[1] + self.macros.unset_write_map() + self.macros.set_read_map('global') + self.macros.set_read_map(bootloader) + + def section_macro_map(self, section, nesting_level = 0): + nesting_level += 1 + if nesting_level >= 100: + err = 'boot config: too many map levels (looping?): %s' % (section) + raise error.general(err) + if section not in self.macros.maps(): + raise error.general('boot config: maps section not found: %s' % (section)) + self.macros.set_read_map(section) + for s in self.config.comma_list(section, 'uses', err = False): + self.section_macro_map(s, nesting_level) + + def boards(self): + return self.config.comma_list(self['bootloader'], 'boards') + + def log(self): + log.output('Configuration:') + log.output(' Bootloader: {0}'.format(self.macros['bootloader'])) + log.output(' Image: {0}'.format(self.macros['output'])) + log.output(' Image Size: {0}'.format(self.macros['image_size'])) + log.output(' Part Type: {0}'.format(self.macros['part_type'])) + log.output(' FS Format: {0}'.format(self.macros['fs_format'])) + log.output(' FS Size: {0}'.format(self.macros['fs_size'])) + log.output(' FS Align: {0}'.format(self.macros['fs_alignment'])) + log.output(' Kernel: {0}'.format(self.macros['kernel'])) + log.output(' Net Server IP: {0}'.format(self.macros['net_server_ip'])) + log.output(' Net IP: {0}'.format(self.macros['net_ip'])) + log.output(' Files: {0}'.format(len(self.files()))) + log.output('Macros:') + macro_str = str(self.macros).split(os.linesep) + log.output(os.linesep.join([' ' + l for l in macro_str])) + + def get_exes(self): + return [] + + def check_exes(self): + return _check_exes(self.get_exes()) + + def files(self): + return [f.strip() for f in self.comma_split(self['files']) if len(f) > 0] + + def install_files(self, image, mountpoint): + pass + + def install_configuration(self, image, mountpoint): + pass + + def kernel_image(self): + return self['kernel_image'].replace('@KERNEL@', path.basename(self['kernel'])) + + def fdt_image(self): + return self['fdt_image'].replace('@FDT@', path.basename(self['fdt'])) + + def filter_text(self, lines): + out = [] + for line in lines: + if '@KERNEL@' in line: + line = line.replace('@KERNEL@', path.basename(self['kernel'])) + if '@KERNEL_IMAGE@' in line: + line = line.replace('@KERNEL_IMAGE@', self.kernel_image()) + if '@FDT@' in line: + line = line.replace('@FDT@', path.basename(self['fdt'])) + if '@FDT_IMAGE@' in line: + line = line.replace('@FDT_IMAGE@', self.fdt_image()) + if '@NET_SERVER_IP@' in line: + line = line.replace('@NET_SERVER_IP@', self['net_server_ip']) + if '@NET_IP@' in line: + line = line.replace('@NET_IP@', self['net_ip']) + if '@NET_BOOTEXE@' in line: + line = line.replace('@NET_BOOTEXE@', self['net_bootexe']) + if '@NET_BOOTFDT@' in line: + line = line.replace('@NET_BOOTFDT@', self['fdt']) + out += [line] + return out + + def comma_split(self, value): + if value is not None: + return [s.strip() for s in value.split(',')] + return [] + +class uboot_config(config): + + def __init__(self, command_path, convert_kernel, paths, board): + self.uboot = { 'paths': paths, 'board': board } + self.convert_kernel = convert_kernel + super(uboot_config, self).__init__(command_path, 'u-boot') + if self.board() not in self.boards(): + raise error.general('board not found: %s' %(self.board())) + log.output('Board: %s' % (self.board())) + self.section_macro_map(self.board()) + self.macros.set_read_map(self['bootloader'] + '-templates') + self.macros.lock_read_map() + log.output(str(self.macros)) + + def load_config(self, bootloader, config): + super(uboot_config, self).load_config(bootloader, config) + if not self.convert_kernel: + paths_count = len(self.uboot['paths']) + if paths_count == 1: + self.macros['ubootdir'] = path.abspath(self.uboot['paths'][0]) + if not path.exists(self['first_stage']): + err = 'u-boot: first stage loader not found: %s' % \ + (self['first_stage']) + raise error.general(err) + if not path.exists(self['second_stage']): + err = 'u-boot: second stage loader not found: %s' % \ + (self['second_stage']) + raise error.general(err) + elif paths_count == 2: + self.macros['first_stage'] = self.uboot['paths'][0] + self.macros['second_stage'] = self.uboot['paths'][1] + else: + raise error.general('u-boot: invalid number of paths') + self.macros['mkimage'] = 'mkimage' + + def get_mandatory_configs(self): + cfgs = super(uboot_config, self).get_mandatory_configs() + return cfgs + ['objcopy', + 'arch', + 'vendor', + 'board', + 'config_name', + 'first_stage', + 'second_stage', + 'kernel_converter'] + + def board(self): + return self.uboot['board'] + + def get_exes(self): + exes = super(uboot_config, self).get_exes() + if self['executables'] is not None: + exes += self.comma_split(self['executables']) + return exes + + def install_files(self, image, mountpoint): + if self['kernel'] is not None: + kernel_image = self.kernel_convert(image, self['kernel']) + if self.convert_kernel: + path.copy(kernel_image, self['output']) + else: + image.install(kernel_image, mountpoint) + if self['fdt'] is not None: + fdt_image = self.fdt_convert(image, self['fdt']) + image.install(fdt_image, mountpoint) + + def install_configuration(self, image, mountpoint): + uenv_txt = self['uenv_txt'] + if uenv_txt is not None: + log.output('Uenv txt: %s' % (uenv_txt)) + image.install(path.abspath(uenv_txt), mountpoint) + else: + template = None + if self['net_server_ip'] is not None or \ + self['net_ip'] is not None: + if self['net_boot_ip'] is not None: + template = 'uenv_net_static' + else: + template = 'uenv_net_dhcp' + if self['net_fdt']: + template += '_net' + elif self['kernel'] is not None: + template = 'uenv_exe' + elif self['fdt'] is not None: + template = 'uenv' + if self['fdt'] is not None: + template += '_fdt' + if template is not None: + log.notice('Uenv template: %s' % (template)) + uenv_start = self.comma_split(self['uenv_start']) + uenv_body = self.comma_split(self[template]) + uenv_end = self.comma_split(self['uenv_end']) + uenv = uenv_start + uenv_body + uenv_end + image.install_text(self.filter_text(uenv), + path.join(mountpoint, self['boot_config'])) + + def kernel_convert(self, image, kernel): + dst = path.join(path.abspath(self['build']), path.basename(kernel)) + self['kernel_build'] = dst + log.output('Copy (into build): %s -> %s' % (kernel, dst)) + image.clean_path(dst) + path.copy(kernel, dst) + cmds = self.filter_text(self.comma_split(self['kernel_converter'])) + for cmd in cmds: + _command(cmd, self['build']) + return self['kernel_image'].replace('@KERNEL@', dst) + + def fdt_convert(self, image, fdt): + dst = path.join(path.abspath(self['build']), path.basename(fdt)) + self['fdt_build'] = dst + log.output('Copy (into build): %s -> %s' % (fdt, dst)) + image.clean_path(dst) + path.copy(fdt, dst) + return self['fdt_image'].replace('@FDT@', dst) + +class image(object): + + def __init__(self, conf): + self.conf = conf + self.detach_images = [] + self.unmount_paths = [] + self.remove_paths = [] + + def build(self): + # + # Cleanup if any goes wrong. + # + try: + # + # Ge the absolute paths to fixed locations. + # + output = path.abspath(self.conf['output']) + build = path.abspath(self.conf['build']) + mountpoint = path.join(build, 'mnt') + + # + # Create any paths we need. They are removed when this object is + # deleted. + # + self.create_path(build) + + # + # If only coverting a kernel no need to create an image. + # + if not self.conf.convert_kernel: + self.create_path(mountpoint) + + # + # Create the blank image file. This is attached as a device, + # partitioned, formatted and the files written to it. + # + log.notice('Create image: %s size %s' % (self.conf['output'], + self.conf['image_size'])) + self.image_create(output, self.conf['image_size']) + + # + # Attach the image so it is a device. + # + log.notice('Attach image to device: %s' % (self.conf['output'])) + device = self.image_attach(output) + + # + # Partition the image. The device may change. + # + log.notice('Partition device: %s as %s' % (device, + self.conf['part_type'])) + device = self.partition(output, + device, + self.conf['part_type'], + self.conf['part_label'], + self.conf['fs_format'], + self.conf['fs_size'], + self.conf['fs_alignment']) + part = self.device_partition(device, 1) + + # + # Format the first partition. + # + log.notice('Format: %s as %s' % (part, self.conf['fs_format'])) + self.format_partition(part, self.conf['fs_format']) + + # + # Mount the file system. + # + log.notice('Mount: %s' % (part)) + self.mount(self.conf['fs_format'], part, mountpoint) + + # + # Install the first stage and second stage boot loaders. + # + self.install(self.conf['first_stage'], mountpoint) + self.install(self.conf['second_stage'], mountpoint) + + # + # Install the bootload files. + # + self.conf.install_files(self, mountpoint) + + if not self.conf.convert_kernel: + # + # Install the bootloader configuration. + # + self.conf.install_configuration(self, mountpoint) + + # + # Install any user files if present. + # + for f in self.conf.files(): + self.install(f, mountpoint) + + # + # Done. + # + log.notice('Finished') + finally: + self.cleanup() + + def install(self, src, dst): + src_base = path.basename(src) + log.notice('Install: %s' % (src_base)) + asrc = path.abspath(src) + adst = path.join(path.abspath(dst), src_base) + log.output('Copy: %s -> %s' % (asrc, adst)) + path.copy(asrc, adst) + + def install_text(self, text, dst): + dst_base = path.basename(dst) + log.notice('Install: %s' % (dst_base)) + adst = path.abspath(dst) + log.output('Copy: text[%d] -> %s' % (len(text), adst)) + log.output([' ] ' + l for l in text]) + with open(adst, "w") as o: + o.write(os.linesep.join(text)) + + def image_create(self, path_, size): + self.host_image_create(path_, size, path.exists(path_)) + + def image_attach(self, path_): + device = self.host_image_attach(path_) + self.detach_images += [device] + return device + + def image_detach(self, device): + if device in self.detach_images: + self.detach_images.remove(device) + self.host_image_detach(device) + + def partition(self, image_, device, ptype, plabel, pformat, psize, palign): + return self.host_partition(image_, device, + ptype, plabel, pformat, psize, palign) + + def format_partition(self, device, pformat): + self.host_format_partition(device, pformat) + + def device_partition(self, device, pindex): + return self.host_device_partition(device, pindex) + + def mount(self, pformat, device, path_): + if path_ not in self.unmount_paths: + self.host_mount(pformat, device, path_) + self.unmount_paths += [path_] + + def unmount(self, path_): + if path_ in self.unmount_paths: + self.host_unmount(path_) + self.unmount_paths.remove(path_) + + def cleanup(self): + log.notice('Cleaning up') + for m in self.unmount_paths: + log.output('unmount: %s' % (m)) + self.unmount(m) + for i in self.detach_images: + log.output('detach: %s' % (i)) + self.image_detach(i) + if self.conf.clean: + for r in self.remove_paths: + if path.exists(r): + log.output('remove: %s' % (r)) + path.removeall(r) + + def get_exes(self): + return ['dd'] + + def check_exes(self): + return _check_exes(self.get_exes()) + + def clean_path(self, name): + self.remove_paths += [name] + + def create_path(self, where, recreate = True, cleanup = True): + if path.exists(where): + log.output('remove: %s' % (where)) + path.removeall(where) + try: + log.output('make: %s' % (where)) + path.mkdir(where) + except: + raise error.general('cannot create build path: %s' % (where)) + if not path.isreadable(where): + raise error.general('build path is not readable: %s' % (where)) + if not path.iswritable(where): + raise error.general('build path is not writeable: %s' % (where)) + if cleanup: + self.remove_paths += [where] + + def command(self, cmd): + return _command(cmd, self.conf['build']) + + def host_image_create(self, path_, size, exists): + img_size, img_units = _si_size_units(size) + self.command('dd if=/dev/zero of=%s bs=1%s count=%d' % (path_, + img_units, + img_size)) + + def host_image_attach(self, path_): + raise error.general('no platform support: host_image_attach') + + def host_image_detach(self, device): + raise error.general('no platform support: host_image_detach') + + def host_partition(self, image_, device, ptype, plabel, pformat, psize, palign): + raise error.general('no platform support: host_partition') + + def host_format_partition(self, device, pformat): + raise error.general('no platform support: host_format_partition') + + def host_device_partition(self, device, pindex): + raise error.general('no platform support: host_device_partition') + + def host_mount(self, pformat, device, path_): + raise error.general('no platform support: host_mount') + + def host_unmount(self, path_): + raise error.general('no platform support: host_unmount') + +class freebsd_image(image): + def __init__(self, conf): + super(freebsd_image, self).__init__(conf) + + def get_exes(self): + exes = super(freebsd_image, self).get_exes() + return exes + ['mdconfig', + 'gpart', + 'newfs_msdos', + 'mount', + 'umount'] + + def host_image_attach(self, path_): + return self.command('sudo mdconfig -f %s' % (path_)) + + def host_image_detach(self, device): + self.command('sudo mdconfig -d -u %s' % (device)) + + def host_partition(self, image_, device, ptype, plabel, pformat, psize, palign): + types = { 'MBR': 'MBR' } + formats = { 'fat16': 'fat16', + 'fat32': 'fat32' } + if ptype not in types: + err = 'unknown type of partitioning: %s' % (ptype) + raise error.general(err) + if pformat not in formats: + raise error.general('unknown format: %s' % (pformat)) + self.command('sudo gpart create -s %s %s' % (types[ptype], device)) + self.command('sudo gpart add -s %s -t %s -a %s %s' % (psize, + formats[pformat], + palign, + device)) + self.command('sudo gpart set -a active -i 1 %s' % (device)) + return device + + def host_format_partition(self, device, pformat): + formats = { 'fat16': ('newfs_msdos', '16'), + 'fat32': ('newfs_msdos', '32') } + if pformat not in formats: + raise error.general('unknown format: %s' % (pformat)) + self.command('sudo %s -F %s %s' % (formats[pformat][0], + formats[pformat][1], + device)) + + def host_device_partition(self, device, pindex): + return '/dev/%ss%d' % (device, pindex) + + def host_mount(self, pformat, device, path_): + formats = { 'fat16': 'msdos', + 'fat32': 'msdos' } + if pformat not in formats: + raise error.general('unknown format: %s' % (pformat)) + self.command('sudo mount -t %s %s %s' % (formats[pformat], + device, + path_)) + + def host_unmount(self, path_): + self.command('sudo umount %s' % (path_)) + +class linux_image(image): + def __init__(self, conf): + super(linux_image, self).__init__(conf) + + def get_exes(self): + exes = super(linux_image, self).get_exes() + return exes + ['losetup', + 'fdisk', + 'mkfs.fat', + 'mount', + 'umount'] + + def host_image_create(self, path_, size, exists): + img_size, img_units = _si_size_units(size) + self.command('dd if=/dev/zero of=%s bs=%s count=%d' % (path_, + _si_units(img_units), + img_size)) + def host_image_attach(self, path_): + return self.command('sudo losetup --partscan --find --show %s' % (path_)) + + def host_image_detach(self, device): + self.command('sudo losetup --detach %s' % (device)) + + def host_partition(self, image_, device, ptype, pformat, psize, palign): + types = { 'MBR': 'MBR' } + formats = { 'fat16': '6', + 'fat32': 'b' } + if ptype not in types: + err = 'unknown type of partitioning: %s' % (ptype) + raise error.general(err) + if pformat not in formats: + raise error.general('unknown format: %s' % (pformat)) + # + # Datch the loop back device, we use fdisk on the image to avoid any + # kernel errors related to re-reading the updated partition data. + # + self.host_image_detach(device) + # + # This awkward exchange is needed to script fdisk, hmmm. + # + with tempfile.NamedTemporaryFile() as tmp: + s = os.linesep.join(['o', # create a new empty part table + 'n', # add a new partition + 'p', # primary + '1', # partition 1 + '%d' % (_si_size(palign) / 512), + '%d' % (_si_size(psize) / 512), + 't', # change a partition type + '%s' % (formats[pformat]), # hex code + 'a', # toggle a bootable flag + 'p', # print + 'w', # write table to disk and exit + '']) + log.output('fdisk script:') + log.output(s) + tmp.write(s.encode()) + tmp.seek(0) + self.command('cat %s | fdisk -t %s %s' % (tmp.name, + types[ptype], + image_)) + return self.host_image_attach(image_) + + def host_format_partition(self, device, pformat): + formats = { 'fat16': ('mkfs.fat', '16'), + 'fat32': ('mkfs.fat', '32') } + if pformat not in formats: + raise error.general('unknown format: %s' % (pformat)) + self.command('sudo %s -F %s %s' % (formats[pformat][0], + formats[pformat][1], + device)) + + def host_device_partition(self, device, pindex): + return '%sp%d' % (device, pindex) + + def host_mount(self, pformat, device, path_): + options = { 'fat16': '-o uid=%d' % (os.getuid()), + 'fat32': '-o uid=%d' % (os.getuid()) } + if pformat in options: + opts = options[pformat] + else: + opts = '' + self.command('sudo mount %s %s %s' % (opts, device, path_)) + + def host_unmount(self, path_): + self.command('sudo umount %s' % (path_)) + +class darwin_image(image): + def __init__(self, conf): + super(darwin_image, self).__init__(conf) + if not self.conf['output'].endswith('.img'): + log.notice('Output file does not end with `.img`. ' + \ + 'Needed on MacOS due to a bug (Id: 51283993)') + raise error.general('output file does not end with `.img`') + + def get_exes(self): + exes = super(darwin_image, self).get_exes() + return exes + ['hdiutil', + 'diskutil', + 'fdisk'] + + def host_image_attach(self, path_): + output = self.command('sudo hdiutil attach %s -nomount -nobrowse' % (path_)) + if len(output.split(os.linesep)) != 1 or not output.startswith('/dev/'): + raise error.general('invalid hdiutil attach outputl; see log') + return output.strip() + + def host_image_detach(self, device): + self.command('sudo hdiutil detach %s' % (device)) + + def host_partition(self, image_, device, ptype, plabel, pformat, psize, palign): + types = { 'MBR': 'MBR' } + formats = { 'fat16': 'MS-DOS FAT16', + 'fat32': 'MS-DOS FAT32' } + if ptype not in types: + err = 'unknown type of partitioning: %s' % (ptype) + raise error.general(err) + if pformat not in formats: + raise error.general('unknown format: %s' % (pformat)) + # + # Align the parition by adding free space before. Sign. + # + cmd = "sudo diskutil partitionDisk %s 2 %s " % (device, types[ptype]) + cmd += "'Free Space' '%%noformat%%' %s " % (palign) + cmd += "'%s' %s %s" % (formats[pformat], plabel, psize) + self.command(cmd) + # + # MacOS mounts the filesystem once the partitioning has finished, + # unmount it as we have no control over the mountpoint. + # + self.command('sudo diskutil unmountDisk %s' % (device)) + # + # This awkward exchange is needed to set the active bit. + # + with tempfile.NamedTemporaryFile() as tmp: + s = os.linesep.join(['f 1', # flag toggle on partition 1 + 'w', # write + 'p', # print + 'q', # quit + '']) + tmp.write(s.encode()) + tmp.seek(0) + self.command('cat %s | sudo fdisk -y -e %s' % (tmp.name, device)) + return device + + def host_format_partition(self, device, pformat): + log.output(' * No format stage; done when partitioning') + + def host_device_partition(self, device, pindex): + return '%ss%d' % (device, pindex) + + def host_mount(self, pformat, device, path_): + self.command('sudo diskutil mount -mountPoint %s %s' % (path_, device)) + + def host_unmount(self, path_): + self.command('sudo diskutil unmount %s' % (path_)) + +builders = { + 'freebsd': freebsd_image, + 'linux' : linux_image, + 'darwin' : darwin_image +} + +def load_log(logfile): + log.default = log.log(streams = [logfile]) + +def log_default(): + return 'rtems-log-boot-image.txt' + +class valid_dir(argparse.Action): + def __call__(self, parser, namespace, values, option_string = None): + if type(values) is not list: + values = [values] + for value in values: + if not path.isdir(value): + raise argparse.ArgumentError(self, + 'is not a valid directory: %s' % (value)) + if not path.isreadable(value): + raise argparse.ArgumentError(self, 'is not readable: %s' % (value)) + if not path.iswritable(value): + raise argparse.ArgumentError(self, 'is not writeable: %s' % (value)) + setattr(namespace, self.dest, value) + +class valid_file(argparse.Action): + def __call__(self, parser, namespace, value, option_string = None): + current = getattr(namespace, self.dest) + if not isinstance(current, list) and current is not None: + raise argparse.ArgumentError(self, + ' already provided: %s, have %s' % (value, + current)) + if not path.isfile(value): + raise argparse.ArgumentError(self, 'is not a valid file: %s' % (value)) + if not path.isreadable(value): + raise argparse.ArgumentError(self, 'is not readable: %s' % (value)) + if current is not None: + value = current + [value] + setattr(namespace, self.dest, value) + +class valid_paths(argparse.Action): + def __call__(self, parser, namespace, value, option_string = None): + current = getattr(namespace, self.dest) + if current is None: + current = [] + if isinstance(value, list): + values = value + else: + values = [values] + for value in values: + if not path.isfile(value) and not path.isdir(value): + err = 'is not a valid file or directory: %s' % (value) + raise argparse.ArgumentError(self, err) + if not path.isreadable(value): + raise argparse.ArgumentError(self, 'is not readable: %s' % (value)) + current += [value] + setattr(namespace, self.dest, current) + +class valid_format(argparse.Action): + def __call__(self, parser, namespace, value, option_string = None): + current = getattr(namespace, self.dest) + if not isinstance(current, list) and current is not None: + raise argparse.ArgumentError(self, + ' already provided: %s, have %s' % (value, + current)) + if value not in ['fat16', 'fat32']: + raise argparse.ArgumentError(self, ' invalid format: %s' % (value)) + setattr(namespace, self.dest, value) + +class valid_si(argparse.Action): + def __call__(self, parser, namespace, value, option_string = None): + current = getattr(namespace, self.dest) + units = len(value) + if value[-1].isalpha(): + if value[-1] not in ['k', 'm', 'g']: + raise argparse.ArgumentError(self, + 'invalid SI (k, m, g): %s' % (value[-1])) + units = -1 + if not value[:units].isdigit(): + raise argparse.ArgumentError(self, 'invalid SI size: %s' % (value)) + setattr(namespace, self.dest, value) + +class valid_ip(argparse.Action): + def __call__(self, parser, namespace, value, option_string = None): + current = getattr(namespace, self.dest) + if current is not None: + raise argparse.ArgumentError(self, + ' already provided: %s, have %s' % (value, + current)) + setattr(namespace, self.dest, value) + +def run(args = sys.argv, command_path = None): + ec = 0 + notice = None + builder = None + try: + description = 'Provide one path to a u-boot build or provide two ' + description += 'paths to the built the first and second stage loaders, ' + description += 'for example a first stage loader is \'MLO\' and a second ' + description += '\'u-boot.img\'.' + + argsp = argparse.ArgumentParser(prog = 'rtems-boot-image', + description = description) + argsp.add_argument('-l', '--log', + help = 'log file (default: %(default)s).', + type = str, default = log_default()) + argsp.add_argument('-v', '--trace', + help = 'enable trace logging for debugging.', + action = 'store_true') + argsp.add_argument('-s', '--image-size', + help = 'image size in mega-bytes (default: %(default)s).', + type = str, action = valid_si, default = '64m') + argsp.add_argument('-F', '--fs-format', + help = 'root file system format (default: %(default)s).', + type = str, action = valid_format, default = 'fat16') + argsp.add_argument('-S', '--fs-size', + help = 'root file system size in SI units ' + \ + '(default: %(default)s).', + type = str, action = valid_si, default = 'auto') + argsp.add_argument('-A', '--fs-align', + help = 'root file system alignment in SI units ' + \ + '(default: %(default)s).', + type = str, action = valid_si, default = '1m') + argsp.add_argument('-k', '--kernel', + help = 'install the kernel (default: %(default)r).', + type = str, action = valid_file, default = None) + argsp.add_argument('-d', '--fdt', + help = 'Flat device tree source/blob (default: %(default)r).', + type = str, action = valid_file, default = None) + argsp.add_argument('-f', '--file', + help = 'install the file (default: None).', + type = str, action = valid_file, default = []) + argsp.add_argument('--net-boot', + help = 'configure a network boot using TFTP ' + \ + '(default: %(default)r).', + action = 'store_true') + argsp.add_argument('--net-boot-server', + help = 'network boot server IP address ' + \ + '(default: %(default)r).', + type = str, action = valid_ip, default = None) + argsp.add_argument('--net-boot-ip', + help = 'network boot IP address (default: %(default)r).', + type = str, action = valid_ip, default = None) + argsp.add_argument('--net-boot-file', + help = 'network boot file (default: %(default)r).', + type = str, default = 'rtems.img') + argsp.add_argument('--net-boot-fdt', + help = 'network boot load a fdt file (default: %(default)r).', + action = 'store_true', default = False) + argsp.add_argument('-U', '--custom-uenv', + help = 'install the custom uEnv.txt file ' + \ + '(default: %(default)r).', + type = str, action = valid_file, default = None) + argsp.add_argument('-b', '--board', + help = 'name of the board (default: %(default)r).', + type = str, required = True) + argsp.add_argument('--convert-kernel', + help = 'convert a kernel to a bootoader image ' + \ + '(default: %(default)r).', + action = 'store_true', default = False) + argsp.add_argument('--no-clean', + help = 'do not clean when finished (default: %(default)r).', + action = 'store_false', default = True) + argsp.add_argument('-o', '--output', + help = 'image output file name', + type = str, required = True) + argsp.add_argument('paths', + help = 'files or paths, the number and type sets the mode.', + nargs = '+', action = valid_paths) + + argopts = argsp.parse_args(args[1:]) + + load_log(argopts.log) + log.notice('RTEMS Tools - Boot Image, %s' % (version.string())) + log.output(log.info(args)) + log.tracing = argopts.trace + + if argopts.net_boot_server is not None or \ + argopts.net_boot_ip is not None: + if argopts.convert_kernel: + raise error.general('net boot options not valid with kernel convert.') + if argopts.custom_uenv is not None: + raise error.general('cannot set custom uenv and net boot options.') + + host.load() + + log.output('Platform: %s' % (host.name)) + + conf = uboot_config(command_path, + argopts.convert_kernel, + argopts.paths, + argopts.board) + + conf.check_mandatory_configs() + + if conf.convert_kernel: + if argopts.kernel is not None: + raise error.general('kernel convert does not use the kernel option.') + if len(argopts.paths) != 1: + raise error.general('kernel convert take a single path.') + argopts.kernel = argopts.paths[0] + else: + conf.clean = argopts.no_clean + + conf['board'] = argopts.board + conf['output'] = argopts.output + conf['net_server_ip'] = argopts.net_boot_server + conf['net_ip'] = argopts.net_boot_ip + conf['net_bootexe'] = argopts.net_boot_file + conf['net_fdt'] = '1' if argopts.net_boot_fdt else '0' + conf['build'] = 'build' # need an option for this at some point + conf['image_size'] = argopts.image_size + conf['fs_format'] = argopts.fs_format + conf['fs_size'] = argopts.fs_size + conf['fs_align'] = argopts.fs_align + conf['kernel'] = argopts.kernel + conf['fdt'] = argopts.fdt + conf['files'] = ','.join(argopts.file) + conf['uenv_txt'] = argopts.custom_uenv + + conf.log() + + if not conf.convert_kernel: + if conf['fs_size'] == 'auto': + conf['fs_size'] = \ + str(_si_size(conf['image_size']) - _si_size(conf['fs_align'])) + elif _si_size(conf['image_size']) > \ + _si_size(conf['fs_align']) + _si_size(conf['fs_size']): + raise error.general('filesystem partition size larger than image size.') + + if host.name not in builders: + err = 'no builder; platform not supported: %s' % (host.name) + raise error.general(err) + + builder = builders[host.name](conf) + + if not conf.check_exes() or not builder.check_exes(): + raise error.general('command(s) not found; please fix.') + + builder.build() + + except error.general as gerr: + notice = str(gerr) + ec = 1 + except error.internal as ierr: + notice = str(ierr) + ec = 1 + except error.exit as eerr: + pass + except KeyboardInterrupt: + notice = 'abort: user terminated' + ec = 1 + except: + raise + notice = 'abort: unknown error' + ec = 1 + if builder is not None: + del builder + if notice is not None: + log.stderr(notice) + sys.exit(ec) + +if __name__ == "__main__": + run() diff --git a/misc/tools/cmd-boot-image.py b/misc/tools/cmd-boot-image.py new file mode 100755 index 0000000..f55e3ef --- /dev/null +++ b/misc/tools/cmd-boot-image.py @@ -0,0 +1,44 @@ +# +# RTEMS Tools Project (http://www.rtems.org/) +# Copyright 2019 Chris Johns (chrisj@rtems.org) +# All rights reserved. +# +# This file is part of the RTEMS Tools package in 'rtems-tools'. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import print_function + +import sys, os + +base = os.path.dirname(os.path.abspath(sys.argv[0])) +rtems = os.path.dirname(base) +sys.path = [rtems] + sys.path + +try: + import boot + boot.run(sys.argv[1:], command_path = base) +except ImportError: + print("Incorrect RTEMS Tools installation", file = sys.stderr) + sys.exit(1) diff --git a/misc/tools/rtems-boot.ini b/misc/tools/rtems-boot.ini new file mode 100644 index 0000000..26bc857 --- /dev/null +++ b/misc/tools/rtems-boot.ini @@ -0,0 +1,126 @@ +; +; Bootloader Parameters +; + +[default] +image_size = 64m +part_type = MBR +part_label = RTEMS +fs_format = fat16 +fs_size = 63m +fs_alignment = 1m +tool_prefix = %{arch}-rtems%{version}- +objcopy = %{tool_prefix}objcopy +bootloaders = u-boot + +[u-boot] +boards = u-boot-beaglebone, + u-boot-zedboard +bootdelay = 5 +mkimage = %{ubootdir}/tools/mkimage +executables = cat, gzip, %{mkimage}, %{objcopy} +fdt_compiler = ftc +boot_config = uEnv.txt + +[u-boot-beaglebone] +uses = u-boot-arm-ti-am335x_evm + +[u-boot-zedboard] +uses = u-boot-arm-xilinx-zynq-common + +[u-boot-arm-ti-am335x_evm] +arch = arm +vendor = ti +board = am335x +config_name = am335x_evm +first_stage = %{ubootdir}/MLO +boot_device = mmc 0 +second_stage = %{ubootdir}/u-boot.img +start_address = 0x80000000 +entry_address = 0x80000000 +kernel_loadaddr = 0x82000000 +fdt_loadaddr = 0x88000000 +kernel_converter = %{objcopy} @KERNEL@ -O binary @KERNEL@.bin, + cat @KERNEL@.bin | gzip -9 > @KERNEL@.gz, + %{mkimage} -A arm -O Linux -T kernel -a %{start_address} + -e %{entry_address} -n "RTEMS" -d @KERNEL@.gz @KERNEL@.img +kernel_image = @KERNEL@.img +fdt_image = @FDT@ + +[u-boot-arm-xilinx-zynq-common] +arch = arm +vendor = xilinx +board = zynq +config_name = zynq-common +first_stage = %{ubootdior}/spl/boot.bin +second_state = %{ubootdir}/u-boot.img +kernel_loadaddr = 0x02000000 +fdt_loadaddr = 0x08000000 +kernel_converter = %{objcopy} -R -S --strip-debug -O binary @KERNEL@ @KERNEL@.bin, + cat @KERNEL@.bin | gzip -9 @KERNEL@.gz +kernel_image = @KERNEL@.img + +[u-boot-templates] +uenv_start = setenv bootdelay %{bootdelay}, + uenvcmd=run boot_rtems; reset;, + rtems_banner=echo ""; + echo "RTEMS %{board} (%{arch}-%{vendor}-%{config_name})"; + echo " rtems-boot-image v%{version_str}"; + echo ""; +uenv_boot_exe = bootm %{kernel_loadaddr}; +uenv_boot_exe_fdt = bootm %{kernel_loadaddr} - %{fdt_loadaddr} +uenv_exe_load = load %{boot_device} %{kernel_loadaddr} @KERNEL_IMAGE@ +uenv_fdt_load = echo "Loading @FDT_IMAGE@"; + load %{boot_device} %{fdt_loadaddr} @FDT_IMAGE@ +uenv_tftp_exe = tftpboot %{kernel_loadaddr} @NET_BOOTEXE@ +uenv_tftp_fdt = tftpboot %{fdt_loadaddr} @NET_BOOTFDT@ +uenv_dhcp = echo "Netboot: DHCP"; + echo " Server: @NET_SERVER_IP@"; + set autoload no; + dhcp; + set serverip @NET_SERVER_IP@ +uenv_static_ip = echo "Netboot: STATIC"; + echo " Server: @NET_SERVER_IP@"; + echo " IP: @NET_IP@"; + set autoload no; + set ipaddr @NET_IP@; + set serverip @NET_SERVER_IP@ +uenv_fdt = boot_rtems=run rtems_banner; + ${uenv_load_fdt} +uenv_exe = boot_rtems=run rtems_banner; + echo "Loading @KERNEL_IMAGE@"; + ${uenv_exe_load}; + ${uenv_boot_exe} +uenv_exe_fdt = boot_rtems=run rtems_banner; + echo "Loading @KERNEL_IMAGE@"; + ${uenv_fdt_load}; + ${uenv_fdt_load}; + ${uenv_boot_exe_fdt} +uenv_net_dhcp = boot_rtems=run rtems_banner; + ${uenv_dhcp}; + ${uenv_tftp_exe}; + ${uenv_boot_exe} +uenv_net_dhcp_fdt = boot_rtems=run rtems_banner; + ${uenv_fdt_load}; + ${uenv_dhcp}; + ${uenv_tftp_exe}; + ${uenv_boot_exe_fdt} +uenv_net_dhcp_net_fdt = boot_rtems=run rtems_banner; + ${uenv_dhcp}; + ${uenv_tftp_exe}; + ${uenv_tftp_fdt}; + ${uenv_boot_exe_fdt} +uenv_net_static = boot_rtems=run rtems_banner; + ${uenv_static_ip}; + ${uenv_tftp_exe}; + ${uenv_boot_exe} +uenv_net_static_fdt = boot_rtems=run rtems_banner; + ${uenv_fdt_load}; + ${uenv_static_ip}; + ${uenv_tftp_exe}; + ${uenv_boot_exe_fdt} +uenv_net_static_net_fdt = boot_rtems=run rtems_banner; + ${uenv_static_ip}; + ${uenv_tftp_exe}; + ${uenv_tftp_fdt}; + ${uenv_boot_exe_fdt} diff --git a/misc/wscript b/misc/wscript index cd31091..3458b19 100644 --- a/misc/wscript +++ b/misc/wscript @@ -70,5 +70,20 @@ def build(bld): cflags = conf['cflags'] + conf['warningflags'], linkflags = conf['linkflags']) + # + # Install the boot image code. + # + bld(features = 'py', + source = ['tools/boot.py', + 'tools/cmd-boot-image.py'], + install_from = '.', + install_path = '${PREFIX}/share/rtems') + bld.install_files('${PREFIX}/bin', + ['rtems-boot-image'], + chmod = 0o755) + bld.install_files('${PREFIX}/share/rtems/tools/config', + 'tools/rtems-boot.ini', + relative_trick = True) + def tags(ctx): ctx.exec_command('etags $(find . -name \*.[sSch])', shell = True) -- cgit v1.2.3