zephyr/scripts/west_commands/sign.py
Martí Bolívar 9c92baa73f devicetree: use edt.pickle more
Consolidate creation of edtlib.EDT objects from a build directory's
devicetree into one place by loading it from build/zephyr/edt.pickle
everywhere. A previous commit creates edt.pickle from gen_defines.py.

In addition to probably speeding things up slightly by not reparsing
the devicetree, the main benefit of this approach is creating a single
point of truth for the bindings directories and warnings
configuration, meaning we don't have to worry about them getting out
of sync while being passed around between devicetree creation and
usage time.

Signed-off-by: Martí Bolívar <marti.bolivar@nordicsemi.no>
2020-07-23 09:19:45 -05:00

384 lines
14 KiB
Python

# Copyright (c) 2018 Foundries.io
#
# SPDX-License-Identifier: Apache-2.0
import abc
import argparse
import os
import pathlib
import pickle
import shutil
import subprocess
import sys
from west import log
from west.util import quote_sh_list
from build_helpers import find_build_dir, is_zephyr_build, \
FIND_BUILD_DIR_DESCRIPTION
from runners.core import BuildConfiguration
from zcmake import CMakeCache
from zephyr_ext_common import Forceable, cached_runner_config, \
ZEPHYR_SCRIPTS
# This is needed to load edt.pickle files.
sys.path.append(str(ZEPHYR_SCRIPTS / 'dts'))
SIGN_DESCRIPTION = '''\
This command automates some of the drudgery of creating signed Zephyr
binaries for chain-loading by a bootloader.
In the simplest usage, run this from your build directory:
west sign -t your_tool -- ARGS_FOR_YOUR_TOOL
Assuming your binary was properly built for processing and handling by
tool "your_tool", this creates zephyr.signed.bin and zephyr.signed.hex
files (if supported by "your_tool") which are ready for use by your
bootloader. The "ARGS_FOR_YOUR_TOOL" value can be any additional
arguments you want to pass to the tool, such as the location of a
signing key, a version identifier, etc.
See tool-specific help below for details.'''
SIGN_EPILOG = '''\
imgtool
-------
Currently, MCUboot's 'imgtool' tool is supported. To build a signed
binary you can load with MCUboot using imgtool, run this from your
build directory:
west sign -t imgtool -- --key YOUR_SIGNING_KEY.pem
For this to work, either imgtool must be installed (e.g. using pip3),
or you must pass the path to imgtool.py using the -p option.
The image header size, alignment, and slot sizes are determined from
the build directory using .config and the device tree. A default
version number of 0.0.0+0 is used (which can be overridden by passing
"--version x.y.z+w" after "--key"). As shown above, extra arguments
after a '--' are passed to imgtool directly.'''
class ToggleAction(argparse.Action):
def __call__(self, parser, args, ignored, option):
setattr(args, self.dest, not option.startswith('--no-'))
class Sign(Forceable):
def __init__(self):
super(Sign, self).__init__(
'sign',
# Keep this in sync with the string in west-commands.yml.
'sign a Zephyr binary for bootloader chain-loading',
SIGN_DESCRIPTION,
accepts_unknown_args=False)
def do_add_parser(self, parser_adder):
parser = parser_adder.add_parser(
self.name,
epilog=SIGN_EPILOG,
help=self.help,
formatter_class=argparse.RawDescriptionHelpFormatter,
description=self.description)
parser.add_argument('-d', '--build-dir',
help=FIND_BUILD_DIR_DESCRIPTION)
self.add_force_arg(parser)
# general options
group = parser.add_argument_group('tool control options')
group.add_argument('-t', '--tool', choices=['imgtool', 'rimage'],
required=True,
help='''image signing tool name; imgtool and rimage
are currently supported''')
group.add_argument('-p', '--tool-path', default=None,
help='''path to the tool itself, if needed''')
group.add_argument('tool_args', nargs='*', metavar='tool_opt',
help='extra option(s) to pass to the signing tool')
# bin file options
group = parser.add_argument_group('binary (.bin) file options')
group.add_argument('--bin', '--no-bin', dest='gen_bin', nargs=0,
action=ToggleAction,
help='''produce a signed .bin file?
(default: yes, if supported and unsigned bin
exists)''')
group.add_argument('-B', '--sbin', metavar='BIN',
help='''signed .bin file name
(default: zephyr.signed.bin in the build
directory, next to zephyr.bin)''')
# hex file options
group = parser.add_argument_group('Intel HEX (.hex) file options')
group.add_argument('--hex', '--no-hex', dest='gen_hex', nargs=0,
action=ToggleAction,
help='''produce a signed .hex file?
(default: yes, if supported and unsigned hex
exists)''')
group.add_argument('-H', '--shex', metavar='HEX',
help='''signed .hex file name
(default: zephyr.signed.hex in the build
directory, next to zephyr.hex)''')
return parser
def do_run(self, args, ignored):
self.args = args # for check_force
# Find the build directory and parse .config and DT.
build_dir = find_build_dir(args.build_dir)
self.check_force(os.path.isdir(build_dir),
'no such build directory {}'.format(build_dir))
self.check_force(is_zephyr_build(build_dir),
"build directory {} doesn't look like a Zephyr build "
'directory'.format(build_dir))
bcfg = BuildConfiguration(build_dir)
# Decide on output formats.
formats = []
bin_exists = 'CONFIG_BUILD_OUTPUT_BIN' in bcfg
if args.gen_bin:
self.check_force(bin_exists,
'--bin given but CONFIG_BUILD_OUTPUT_BIN not set '
"in build directory's ({}) .config".
format(build_dir))
formats.append('bin')
elif args.gen_bin is None and bin_exists:
formats.append('bin')
hex_exists = 'CONFIG_BUILD_OUTPUT_HEX' in bcfg
if args.gen_hex:
self.check_force(hex_exists,
'--hex given but CONFIG_BUILD_OUTPUT_HEX not set '
"in build directory's ({}) .config".
format(build_dir))
formats.append('hex')
elif args.gen_hex is None and hex_exists:
formats.append('hex')
if not formats:
log.dbg('nothing to do: no output files')
return
# Delegate to the signer.
if args.tool == 'imgtool':
signer = ImgtoolSigner()
elif args.tool == 'rimage':
signer = RimageSigner()
# (Add support for other signers here in elif blocks)
else:
raise RuntimeError("can't happen")
signer.sign(self, build_dir, bcfg, formats)
class Signer(abc.ABC):
'''Common abstract superclass for signers.
To add support for a new tool, subclass this and add support for
it in the Sign.do_run() method.'''
@abc.abstractmethod
def sign(self, command, build_dir, bcfg, formats):
'''Abstract method to perform a signature; subclasses must implement.
:param command: the Sign instance
:param build_dir: the build directory
:param bcfg: BuildConfiguration for build directory
:param formats: list of formats to generate ('bin', 'hex')
'''
class ImgtoolSigner(Signer):
def sign(self, command, build_dir, bcfg, formats):
if not formats:
return
args = command.args
b = pathlib.Path(build_dir)
cache = CMakeCache.from_build_dir(build_dir)
tool_path = self.find_imgtool(command, args)
# The vector table offset is set in Kconfig:
vtoff = self.get_cfg(command, bcfg, 'CONFIG_ROM_START_OFFSET')
# Flash device write alignment and the partition's slot size
# come from devicetree:
flash = self.edt_flash_node(b)
align, addr, size = self.edt_flash_params(flash)
runner_config = cached_runner_config(build_dir, cache)
if 'bin' in formats:
in_bin = runner_config.bin_file
if not in_bin:
log.die("can't find unsigned .bin to sign")
else:
in_bin = None
if 'hex' in formats:
in_hex = runner_config.hex_file
if not in_hex:
log.die("can't find unsigned .hex to sign")
else:
in_hex = None
log.banner('image configuration:')
log.inf('partition offset: {0} (0x{0:x})'.format(addr))
log.inf('partition size: {0} (0x{0:x})'.format(size))
log.inf('rom start offset: {0} (0x{0:x})'.format(vtoff))
# Base sign command.
#
# We provide a default --version in case the user is just
# messing around and doesn't want to set one. It will be
# overridden if there is a --version in args.tool_args.
sign_base = [tool_path, 'sign',
'--version', '0.0.0+0',
'--align', str(align),
'--header-size', str(vtoff),
'--slot-size', str(size)]
sign_base.extend(args.tool_args)
log.banner('signed binaries:')
if in_bin:
out_bin = args.sbin or str(b / 'zephyr' / 'zephyr.signed.bin')
sign_bin = sign_base + [in_bin, out_bin]
log.inf('bin: {}'.format(out_bin))
log.dbg(quote_sh_list(sign_bin))
subprocess.check_call(sign_bin)
if in_hex:
out_hex = args.shex or str(b / 'zephyr' / 'zephyr.signed.hex')
sign_hex = sign_base + [in_hex, out_hex]
log.inf('hex: {}'.format(out_hex))
log.dbg(quote_sh_list(sign_hex))
subprocess.check_call(sign_hex)
@staticmethod
def find_imgtool(command, args):
if args.tool_path:
command.check_force(shutil.which(args.tool_path),
'--tool-path {}: not an executable'.
format(args.tool_path))
tool_path = args.tool_path
else:
tool_path = shutil.which('imgtool') or shutil.which('imgtool.py')
if not tool_path:
log.die('imgtool not found; either install it',
'(e.g. "pip3 install imgtool") or provide --tool-path')
return tool_path
@staticmethod
def get_cfg(command, bcfg, item):
try:
return bcfg[item]
except KeyError:
command.check_force(
False, "build .config is missing a {} value".format(item))
return None
@staticmethod
def edt_flash_node(b):
# Get the EDT Node corresponding to the zephyr,flash chosen DT
# node; 'b' is the build directory as a pathlib object.
# Ensure the build directory has a compiled DTS file
# where we expect it to be.
dts = b / 'zephyr' / 'zephyr.dts'
log.dbg('DTS file:', dts, level=log.VERBOSE_VERY)
edt_pickle = b / 'zephyr' / 'edt.pickle'
if not edt_pickle.is_file():
log.die("can't load devicetree; expected to find:", edt_pickle)
# Load the devicetree.
with open(edt_pickle, 'rb') as f:
edt = pickle.load(f)
# By convention, the zephyr,flash chosen node contains the
# partition information about the zephyr image to sign.
flash = edt.chosen_node('zephyr,flash')
if not flash:
log.die('devicetree has no chosen zephyr,flash node;',
"can't infer flash write block or image-0 slot sizes")
return flash
@staticmethod
def edt_flash_params(flash):
# Get the flash device's write alignment and the image-0
# partition's size out of the build directory's devicetree.
# The node must have a "partitions" child node, which in turn
# must have a child node labeled "image-0". By convention, the
# primary slot for consumption by imgtool is linked into this
# partition.
if 'partitions' not in flash.children:
log.die("DT zephyr,flash chosen node has no partitions,",
"can't find partition for MCUboot slot 0")
for node in flash.children['partitions'].children.values():
if node.label == 'image-0':
image_0 = node
break
else:
log.die("DT zephyr,flash chosen node has no image-0 partition,",
"can't determine its size")
# The partitions node, and its subnode, must provide
# the size of the image-0 partition via the regs property.
if not image_0.regs:
log.die('image-0 flash partition has no regs property;',
"can't determine size of image slot 0")
# Die on missing or zero alignment or slot_size.
if "write-block-size" not in flash.props:
log.die('DT zephyr,flash node has no write-block-size;',
"can't determine imgtool write alignment")
align = flash.props['write-block-size'].val
if align == 0:
log.die('expected nonzero flash alignment, but got '
'DT flash device write-block-size {}'.format(align))
reg = image_0.regs[0]
if reg.size == 0:
log.die('expected nonzero slot size, but got '
'DT image-0 partition size {}'.format(reg.size))
return (align, reg.addr, reg.size)
class RimageSigner(Signer):
def sign(self, command, build_dir, bcfg, formats):
args = command.args
if args.tool_path:
command.check_force(shutil.which(args.tool_path),
'--tool-path {}: not an executable'.
format(args.tool_path))
tool_path = args.tool_path
else:
tool_path = shutil.which('rimage')
if not tool_path:
log.die('rimage not found; either install it',
'or provide --tool-path')
b = pathlib.Path(build_dir)
cache = CMakeCache.from_build_dir(build_dir)
board = cache['CACHED_BOARD']
if board != 'up_squared_adsp':
log.die('Supported only for up_squared_adsp board')
log.inf('Signing with tool {}'.format(tool_path))
bootloader = str(b / 'zephyr' / 'bootloader.elf.mod')
kernel = str(b / 'zephyr' / 'zephyr.elf.mod')
out_bin = str(b / 'zephyr' / 'zephyr.ri')
sign_base = ([tool_path] + args.tool_args +
['-o', out_bin, '-m', 'apl', '-i', '3'] +
[bootloader, kernel])
log.inf(quote_sh_list(sign_base))
subprocess.check_call(sign_base)