1# Copyright (c) 2018 Foundries.io
2#
3# SPDX-License-Identifier: Apache-2.0
4
5import abc
6import argparse
7import os
8import pathlib
9import pickle
10import platform
11import shutil
12import subprocess
13import sys
14
15from elftools.elf.elffile import ELFFile
16
17from west import manifest
18from west.commands import Verbosity
19from west.util import quote_sh_list
20
21from build_helpers import find_build_dir, is_zephyr_build, \
22    FIND_BUILD_DIR_DESCRIPTION
23from runners.core import BuildConfiguration
24from zcmake import CMakeCache
25from zephyr_ext_common import Forceable, ZEPHYR_SCRIPTS
26
27# This is needed to load edt.pickle files.
28sys.path.insert(0, str(ZEPHYR_SCRIPTS / 'dts' / 'python-devicetree' / 'src'))
29
30SIGN_DESCRIPTION = '''\
31This command automates some of the drudgery of creating signed Zephyr
32binaries for chain-loading by a bootloader.
33
34In the simplest usage, run this from your build directory:
35
36   west sign -t your_tool -- ARGS_FOR_YOUR_TOOL
37
38The "ARGS_FOR_YOUR_TOOL" value can be any additional arguments you want to
39pass to the tool, such as the location of a signing key etc. Depending on
40which sort of ARGS_FOR_YOUR_TOOLS you use, the `--` separator/sentinel may
41not always be required. To avoid ambiguity and having to find and
42understand POSIX 12.2 Guideline 10, always use `--`.
43
44See tool-specific help below for details.'''
45
46SIGN_EPILOG = '''\
47imgtool
48-------
49
50To build a signed binary you can load with MCUboot using imgtool,
51run this from your build directory:
52
53   west sign -t imgtool -- --key YOUR_SIGNING_KEY.pem
54
55For this to work, either imgtool must be installed (e.g. using pip3),
56or you must pass the path to imgtool.py using the -p option.
57
58Assuming your binary was properly built for processing and handling by
59imgtool, this creates zephyr.signed.bin and zephyr.signed.hex
60files which are ready for use by your bootloader.
61
62The version number, image header size, alignment, and slot sizes are
63determined from the build directory using .config and the device tree.
64As shown above, extra arguments after a '--' are passed to imgtool
65directly.
66
67rimage
68------
69
70To create a signed binary with the rimage tool, run this from your build
71directory:
72
73   west sign -t rimage -- -k YOUR_SIGNING_KEY.pem
74
75For this to work, either rimage must be installed or you must pass
76the path to rimage using the -p option.
77
78You can also pass additional arguments to rimage thanks to [sign] and
79[rimage] sections in your west config file(s); this is especially useful
80when invoking west sign _indirectly_ through CMake/ninja. See how at
81https://docs.zephyrproject.org/latest/develop/west/sign.html
82'''
83
84class ToggleAction(argparse.Action):
85
86    def __call__(self, parser, args, ignored, option):
87        setattr(args, self.dest, not option.startswith('--no-'))
88
89
90class Sign(Forceable):
91    def __init__(self):
92        super(Sign, self).__init__(
93            'sign',
94            # Keep this in sync with the string in west-commands.yml.
95            'sign a Zephyr binary for bootloader chain-loading',
96            SIGN_DESCRIPTION,
97            accepts_unknown_args=False)
98
99    def do_add_parser(self, parser_adder):
100        parser = parser_adder.add_parser(
101            self.name,
102            epilog=SIGN_EPILOG,
103            help=self.help,
104            formatter_class=argparse.RawDescriptionHelpFormatter,
105            description=self.description)
106
107        parser.add_argument('-d', '--build-dir',
108                            help=FIND_BUILD_DIR_DESCRIPTION)
109        parser.add_argument('-q', '--quiet', action='store_true',
110                            help='suppress non-error output')
111        self.add_force_arg(parser)
112
113        # general options
114        group = parser.add_argument_group('tool control options')
115        group.add_argument('-t', '--tool', choices=['imgtool', 'rimage'],
116                           help='''image signing tool name; imgtool and rimage
117                           are currently supported (imgtool is deprecated)''')
118        group.add_argument('-p', '--tool-path', default=None,
119                           help='''path to the tool itself, if needed''')
120        group.add_argument('-D', '--tool-data', default=None,
121                           help='''path to a tool-specific data/configuration directory, if needed''')
122        group.add_argument('--if-tool-available', action='store_true',
123                           help='''Do not fail if the rimage tool is not found or the rimage signing
124schema (rimage "target") is not defined in board.cmake.''')
125        group.add_argument('tool_args', nargs='*', metavar='tool_opt',
126                           help='extra option(s) to pass to the signing tool')
127
128        # bin file options
129        group = parser.add_argument_group('binary (.bin) file options')
130        group.add_argument('--bin', '--no-bin', dest='gen_bin', nargs=0,
131                           action=ToggleAction,
132                           help='''produce a signed .bin file?
133                           (default: yes, if supported and unsigned bin
134                           exists)''')
135        group.add_argument('-B', '--sbin', metavar='BIN',
136                           help='''signed .bin file name
137                           (default: zephyr.signed.bin in the build
138                           directory, next to zephyr.bin)''')
139
140        # hex file options
141        group = parser.add_argument_group('Intel HEX (.hex) file options')
142        group.add_argument('--hex', '--no-hex', dest='gen_hex', nargs=0,
143                           action=ToggleAction,
144                           help='''produce a signed .hex file?
145                           (default: yes, if supported and unsigned hex
146                           exists)''')
147        group.add_argument('-H', '--shex', metavar='HEX',
148                           help='''signed .hex file name
149                           (default: zephyr.signed.hex in the build
150                           directory, next to zephyr.hex)''')
151
152        return parser
153
154    def do_run(self, args, ignored):
155        self.args = args        # for check_force
156
157        # Find the build directory and parse .config and DT.
158        build_dir = find_build_dir(args.build_dir)
159        self.check_force(os.path.isdir(build_dir),
160                         'no such build directory {}'.format(build_dir))
161        self.check_force(is_zephyr_build(build_dir),
162                         "build directory {} doesn't look like a Zephyr build "
163                         'directory'.format(build_dir))
164        build_conf = BuildConfiguration(build_dir)
165
166        if not args.tool:
167            args.tool = self.config_get('sign.tool')
168
169        # Decide on output formats.
170        formats = []
171        bin_exists = build_conf.getboolean('CONFIG_BUILD_OUTPUT_BIN')
172        if args.gen_bin:
173            self.check_force(bin_exists,
174                             '--bin given but CONFIG_BUILD_OUTPUT_BIN not set '
175                             "in build directory's ({}) .config".
176                             format(build_dir))
177            formats.append('bin')
178        elif args.gen_bin is None and bin_exists:
179            formats.append('bin')
180
181        hex_exists = build_conf.getboolean('CONFIG_BUILD_OUTPUT_HEX')
182        if args.gen_hex:
183            self.check_force(hex_exists,
184                             '--hex given but CONFIG_BUILD_OUTPUT_HEX not set '
185                             "in build directory's ({}) .config".
186                             format(build_dir))
187            formats.append('hex')
188        elif args.gen_hex is None and hex_exists:
189            formats.append('hex')
190
191        # Delegate to the signer.
192        if args.tool == 'imgtool':
193            if args.if_tool_available:
194                self.die('imgtool does not support --if-tool-available')
195            signer = ImgtoolSigner()
196        elif args.tool == 'rimage':
197            signer = RimageSigner()
198        # (Add support for other signers here in elif blocks)
199        else:
200            if args.tool is None:
201                self.die('one --tool is required')
202            else:
203                self.die(f'invalid tool: {args.tool}')
204
205        signer.sign(self, build_dir, build_conf, formats)
206
207
208class Signer(abc.ABC):
209    '''Common abstract superclass for signers.
210
211    To add support for a new tool, subclass this and add support for
212    it in the Sign.do_run() method.'''
213
214    @abc.abstractmethod
215    def sign(self, command, build_dir, build_conf, formats):
216        '''Abstract method to perform a signature; subclasses must implement.
217
218        :param command: the Sign instance
219        :param build_dir: the build directory
220        :param build_conf: BuildConfiguration for build directory
221        :param formats: list of formats to generate ('bin', 'hex')
222        '''
223
224
225class ImgtoolSigner(Signer):
226
227    def sign(self, command, build_dir, build_conf, formats):
228        if not formats:
229            return
230
231        args = command.args
232        b = pathlib.Path(build_dir)
233
234        command.wrn("west sign using imgtool is deprecated and will be removed in a future release")
235
236        imgtool = self.find_imgtool(command, args)
237        # The vector table offset and application version are set in Kconfig:
238        appver = self.get_cfg(command, build_conf, 'CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION')
239        vtoff = self.get_cfg(command, build_conf, 'CONFIG_ROM_START_OFFSET')
240        # Flash device write alignment and the partition's slot size
241        # come from devicetree:
242        flash = self.edt_flash_node(command, b, args.quiet)
243        align, addr, size = self.edt_flash_params(command, flash)
244
245        if not build_conf.getboolean('CONFIG_BOOTLOADER_MCUBOOT'):
246            command.wrn("CONFIG_BOOTLOADER_MCUBOOT is not set to y in "
247                        f"{build_conf.path}; this probably won't work")
248
249        kernel = build_conf.get('CONFIG_KERNEL_BIN_NAME', 'zephyr')
250
251        if 'bin' in formats:
252            in_bin = b / 'zephyr' / f'{kernel}.bin'
253            if not in_bin.is_file():
254                command.die(f"no unsigned .bin found at {in_bin}")
255            in_bin = os.fspath(in_bin)
256        else:
257            in_bin = None
258        if 'hex' in formats:
259            in_hex = b / 'zephyr' / f'{kernel}.hex'
260            if not in_hex.is_file():
261                command.die(f"no unsigned .hex found at {in_hex}")
262            in_hex = os.fspath(in_hex)
263        else:
264            in_hex = None
265
266        if not args.quiet:
267            command.banner('image configuration:')
268            command.inf('partition offset: {0} (0x{0:x})'.format(addr))
269            command.inf('partition size: {0} (0x{0:x})'.format(size))
270            command.inf('rom start offset: {0} (0x{0:x})'.format(vtoff))
271
272        # Base sign command.
273        sign_base = imgtool + ['sign',
274                               '--version', str(appver),
275                               '--align', str(align),
276                               '--header-size', str(vtoff),
277                               '--slot-size', str(size)]
278        sign_base.extend(args.tool_args)
279
280        if not args.quiet:
281            command.banner('signing binaries')
282        if in_bin:
283            out_bin = args.sbin or str(b / 'zephyr' / 'zephyr.signed.bin')
284            sign_bin = sign_base + [in_bin, out_bin]
285            if not args.quiet:
286                command.inf(f'unsigned bin: {in_bin}')
287                command.inf(f'signed bin:   {out_bin}')
288                command.dbg(quote_sh_list(sign_bin))
289            subprocess.check_call(sign_bin, stdout=subprocess.PIPE if args.quiet else None)
290        if in_hex:
291            out_hex = args.shex or str(b / 'zephyr' / 'zephyr.signed.hex')
292            sign_hex = sign_base + [in_hex, out_hex]
293            if not args.quiet:
294                command.inf(f'unsigned hex: {in_hex}')
295                command.inf(f'signed hex:   {out_hex}')
296                command.dbg(quote_sh_list(sign_hex))
297            subprocess.check_call(sign_hex, stdout=subprocess.PIPE if args.quiet else None)
298
299    @staticmethod
300    def find_imgtool(cmd, args):
301        if args.tool_path:
302            imgtool = args.tool_path
303            if not os.path.isfile(imgtool):
304                cmd.die(f'--tool-path {imgtool}: no such file')
305        else:
306            imgtool = shutil.which('imgtool') or shutil.which('imgtool.py')
307            if not imgtool:
308                cmd.die('imgtool not found; either install it',
309                        '(e.g. "pip3 install imgtool") or provide --tool-path')
310
311        if platform.system() == 'Windows' and imgtool.endswith('.py'):
312            # Windows users may not be able to run .py files
313            # as executables in subprocesses, regardless of
314            # what the mode says. Always run imgtool as
315            # 'python path/to/imgtool.py' instead of
316            # 'path/to/imgtool.py' in these cases.
317            # https://github.com/zephyrproject-rtos/zephyr/issues/31876
318            return [sys.executable, imgtool]
319
320        return [imgtool]
321
322    @staticmethod
323    def get_cfg(command, build_conf, item):
324        try:
325            return build_conf[item]
326        except KeyError:
327            command.check_force(
328                False, "build .config is missing a {} value".format(item))
329            return None
330
331    @staticmethod
332    def edt_flash_node(cmd, b, quiet=False):
333        # Get the EDT Node corresponding to the zephyr,flash chosen DT
334        # node; 'b' is the build directory as a pathlib object.
335
336        # Ensure the build directory has a compiled DTS file
337        # where we expect it to be.
338        dts = b / 'zephyr' / 'zephyr.dts'
339        if not quiet:
340            cmd.dbg('DTS file:', dts, level=Verbosity.DBG_MORE)
341        edt_pickle = b / 'zephyr' / 'edt.pickle'
342        if not edt_pickle.is_file():
343            cmd.die("can't load devicetree; expected to find:", edt_pickle)
344
345        # Load the devicetree.
346        with open(edt_pickle, 'rb') as f:
347            edt = pickle.load(f)
348
349        # By convention, the zephyr,flash chosen node contains the
350        # partition information about the zephyr image to sign.
351        flash = edt.chosen_node('zephyr,flash')
352        if not flash:
353            cmd.die('devicetree has no chosen zephyr,flash node;',
354                    "can't infer flash write block or slot0_partition slot sizes")
355
356        return flash
357
358    @staticmethod
359    def edt_flash_params(cmd, flash):
360        # Get the flash device's write alignment and offset from the
361        # slot0_partition and the size from slot1_partition , out of the
362        # build directory's devicetree. slot1_partition size is used,
363        # when available, because in swap-move mode it can be one sector
364        # smaller. When not available, fallback to slot0_partition (single slot dfu).
365
366        # The node must have a "partitions" child node, which in turn
367        # must have child nodes with label slot0_partition and may have a child node
368        # with label slot1_partition. By convention, the slots for consumption by
369        # imgtool are linked into these partitions.
370        if 'partitions' not in flash.children:
371            cmd.die("DT zephyr,flash chosen node has no partitions,",
372                    "can't find partitions for MCUboot slots")
373
374        partitions = flash.children['partitions']
375        slots = {
376            label: node for node in partitions.children.values()
377                        for label in node.labels
378                        if label in set(['slot0_partition', 'slot1_partition'])
379        }
380
381        if 'slot0_partition' not in slots:
382            cmd.die("DT zephyr,flash chosen node has no slot0_partition partition,",
383                    "can't determine its address")
384
385        # Die on missing or zero alignment or slot_size.
386        if "write-block-size" not in flash.props:
387            cmd.die('DT zephyr,flash node has no write-block-size;',
388                    "can't determine imgtool write alignment")
389        align = flash.props['write-block-size'].val
390        if align == 0:
391            cmd.die('expected nonzero flash alignment, but got '
392                    'DT flash device write-block-size {}'.format(align))
393
394        # The partitions node, and its subnode, must provide
395        # the size of slot1_partition or slot0_partition partition via the regs property.
396        slot_key = 'slot1_partition' if 'slot1_partition' in slots else 'slot0_partition'
397        if not slots[slot_key].regs:
398            cmd.die(f'{slot_key} flash partition has no regs property;',
399                    "can't determine size of slot")
400
401        # always use addr of slot0_partition, which is where slots are run
402        addr = slots['slot0_partition'].regs[0].addr
403
404        size = slots[slot_key].regs[0].size
405        if size == 0:
406            cmd.die('expected nonzero slot size for {}'.format(slot_key))
407
408        return (align, addr, size)
409
410class RimageSigner(Signer):
411
412    def rimage_config_dir(self):
413        'Returns the rimage/config/ directory with the highest precedence'
414        args = self.command.args
415        if args.tool_data:
416            conf_dir = pathlib.Path(args.tool_data)
417        elif self.cmake_cache.get('RIMAGE_CONFIG_PATH'):
418            conf_dir = pathlib.Path(self.cmake_cache['RIMAGE_CONFIG_PATH'])
419        else:
420            conf_dir = self.sof_src_dir / 'tools' / 'rimage' / 'config'
421        self.command.dbg(f'rimage config directory={conf_dir}')
422        return conf_dir
423
424    def generate_uuid_registry(self):
425        'Runs the uuid-registry.h generator script'
426
427        generate_cmd = [sys.executable, str(self.sof_src_dir / 'scripts' / 'gen-uuid-reg.py'),
428                        str(self.sof_src_dir / 'uuid-registry.txt'),
429                        str(pathlib.Path('zephyr') / 'include' / 'generated' / 'uuid-registry.h')
430                       ]
431
432        self.command.inf(quote_sh_list(generate_cmd))
433        subprocess.run(generate_cmd, check=True, cwd=self.build_dir)
434
435    def preprocess_toml(self, config_dir, toml_basename, subdir):
436        'Runs the C pre-processor on config_dir/toml_basename.h'
437
438        compiler_path = self.cmake_cache.get("CMAKE_C_COMPILER")
439        preproc_cmd = [compiler_path, '-E', str(config_dir / (toml_basename + '.h'))]
440        # -P removes line markers to keep the .toml output reproducible.  To
441        # trace #includes, temporarily comment out '-P' (-f*-prefix-map
442        # unfortunately don't seem to make any difference here and they're
443        # gcc-specific)
444        preproc_cmd += ['-P']
445
446        # "REM" escapes _leading_ '#' characters from cpp and allows
447        # such comments to be preserved in generated/*.toml files:
448        #
449        #      REM # my comment...
450        #
451        # Note _trailing_ '#' characters and comments are ignored by cpp
452        # and don't need any REM trick.
453        preproc_cmd += ['-DREM=']
454
455        preproc_cmd += ['-I', str(self.sof_src_dir / 'src')]
456        preproc_cmd += ['-imacros',
457                        str(pathlib.Path('zephyr') / 'include' / 'generated' / 'zephyr' / 'autoconf.h')]
458        preproc_cmd += ['-imacros',
459                        str(pathlib.Path('zephyr') / 'include' / 'generated' / 'uuid-registry.h')]
460
461        # Need to preprocess the TOML file twice: once with
462        # LLEXT_FORCE_ALL_MODULAR defined and once without it
463        full_preproc_cmd = preproc_cmd + ['-o', str(subdir / 'rimage_config_full.toml'), '-DLLEXT_FORCE_ALL_MODULAR']
464        preproc_cmd += ['-o', str(subdir / 'rimage_config.toml')]
465        self.command.inf(quote_sh_list(preproc_cmd))
466        subprocess.run(preproc_cmd, check=True, cwd=self.build_dir)
467        subprocess.run(full_preproc_cmd, check=True, cwd=self.build_dir)
468
469    def sign(self, command, build_dir, build_conf, formats):
470        self.command = command
471        args = command.args
472
473        b = pathlib.Path(build_dir)
474        self.build_dir = b
475        cache = CMakeCache.from_build_dir(build_dir)
476        self.cmake_cache = cache
477
478        # Warning: RIMAGE_TARGET in Zephyr is a duplicate of
479        # CONFIG_RIMAGE_SIGNING_SCHEMA in SOF.
480        target = cache.get('RIMAGE_TARGET')
481
482        if not target:
483            msg = 'rimage target not defined in board.cmake'
484            if args.if_tool_available:
485                command.inf(msg)
486                sys.exit(0)
487            else:
488                command.die(msg)
489
490        kernel_name = build_conf.get('CONFIG_KERNEL_BIN_NAME', 'zephyr')
491
492        bootloader = None
493        cold = None
494        kernel = str(b / 'zephyr' / f'{kernel_name}.elf')
495        out_bin = str(b / 'zephyr' / f'{kernel_name}.ri')
496        out_xman = str(b / 'zephyr' / f'{kernel_name}.ri.xman')
497        out_tmp = str(b / 'zephyr' / f'{kernel_name}.rix')
498
499        # Intel platforms generate a "boot.mod" and "main.mod" as
500        # separate intermediates to use.  Other platforms just use
501        # zephyr.elf directly.
502        if os.path.exists(str(b / 'zephyr' / 'boot.mod')):
503            bootloader = str(b / 'zephyr' / 'boot.mod')
504        if os.path.exists(str(b / 'zephyr' / 'cold.mod')):
505            cold = str(b / 'zephyr' / 'cold.mod')
506            with open(cold, 'rb') as f_cold:
507                elf = ELFFile(f_cold)
508                if elf.get_section_by_name('.cold') is None:
509                    cold = None
510        if os.path.exists(str(b / 'zephyr' / 'main.mod')):
511            kernel = str(b / 'zephyr' / 'main.mod')
512
513        # Clean any stale output. This is especially important when using --if-tool-available
514        # (but not just)
515        for o in [ out_bin, out_xman, out_tmp ]:
516            pathlib.Path(o).unlink(missing_ok=True)
517
518        tool_path = (
519            args.tool_path if args.tool_path else
520            command.config_get('rimage.path', None)
521        )
522        err_prefix = '--tool-path' if args.tool_path else 'west config'
523
524        if tool_path:
525            command.check_force(shutil.which(tool_path),
526                                f'{err_prefix} {tool_path}: not an executable')
527        else:
528            tool_path = shutil.which('rimage')
529            if not tool_path:
530                err_msg = 'rimage not found; either install it or provide --tool-path'
531                if args.if_tool_available:
532                    command.wrn(err_msg)
533                    command.wrn('zephyr binary _not_ signed!')
534                    return
535                else:
536                    command.die(err_msg)
537
538        #### -c sof/rimage/config/signing_schema.toml  ####
539
540        if not args.quiet:
541            command.inf('Signing with tool {}'.format(tool_path))
542
543        try:
544            sof_proj = command.manifest.get_projects(['sof'], allow_paths=False)
545            sof_src_dir = pathlib.Path(sof_proj[0].abspath)
546        except ValueError: # sof is the manifest
547            sof_src_dir = pathlib.Path(manifest.manifest_path()).parent
548
549        self.sof_src_dir = sof_src_dir
550
551
552        command.inf('Signing for SOC target ' + target)
553
554        # FIXME: deprecate --no-manifest and replace it with a much
555        # simpler and more direct `-- -e` which the user can _already_
556        # pass today! With unclear consequences right now...
557        if '--no-manifest' in args.tool_args:
558            no_manifest = True
559            args.tool_args.remove('--no-manifest')
560        else:
561            no_manifest = False
562
563        # Non-SOF build does not have extended manifest data for
564        # rimage to process, which might result in rimage error.
565        # So skip it when not doing SOF builds.
566        is_sof_build = build_conf.getboolean('CONFIG_SOF')
567        if not is_sof_build:
568            no_manifest = True
569            self.generate_uuid_registry()
570
571        if no_manifest:
572            extra_ri_args = [ ]
573        else:
574            extra_ri_args = ['-e']
575
576        sign_base = [tool_path]
577
578        # Align rimage verbosity.
579        # Sub-command arg 'west sign -q' takes precedence over west '-v'
580        if not args.quiet and args.verbose:
581            sign_base += ['-v'] * args.verbose
582
583        # Order is important
584        components = [ ] if bootloader is None else [ bootloader ]
585        if cold is not None:
586            components += [ cold ]
587        components += [ kernel ]
588
589        sign_config_extra_args = command.config_get_words('rimage.extra-args', [])
590
591        if '-k' not in sign_config_extra_args + args.tool_args:
592            # rimage requires a key argument even when it does not sign
593            cmake_default_key = cache.get('RIMAGE_SIGN_KEY', 'key placeholder from sign.py')
594            extra_ri_args += [ '-k', str(sof_src_dir / 'keys' / cmake_default_key) ]
595
596        if args.tool_data and '-c' in args.tool_args:
597            command.wrn('--tool-data ' + args.tool_data + ' ignored! Overridden by: -- -c ... ')
598
599        if '-c' not in sign_config_extra_args + args.tool_args:
600            conf_dir = self.rimage_config_dir()
601            toml_basename = target + '.toml'
602            if ((conf_dir / toml_basename).exists() and
603               (conf_dir / (toml_basename + '.h')).exists()):
604                command.die(f"Cannot have both {toml_basename + '.h'} and {toml_basename} in {conf_dir}")
605
606            if (conf_dir / (toml_basename + '.h')).exists():
607                generated_subdir = pathlib.Path('zephyr') / 'misc' / 'generated'
608                self.preprocess_toml(conf_dir, toml_basename, generated_subdir)
609                extra_ri_args += ['-c', str(b / generated_subdir / 'rimage_config.toml')]
610            else:
611                toml_dir = conf_dir
612                extra_ri_args += ['-c', str(toml_dir / toml_basename)]
613
614        # Warning: while not officially supported (yet?), the rimage --option that is last
615        # on the command line currently wins in case of duplicate options. So pay
616        # attention to the _args order below.
617        sign_base += (['-o', out_bin] + sign_config_extra_args +
618                      extra_ri_args + args.tool_args + components)
619
620        command.inf(quote_sh_list(sign_base))
621        subprocess.check_call(sign_base)
622
623        if no_manifest:
624            filenames = [out_bin]
625        else:
626            filenames = [out_xman, out_bin]
627            if not args.quiet:
628                command.inf('Prefixing ' + out_bin + ' with manifest ' + out_xman)
629        with open(out_tmp, 'wb') as outfile:
630            for fname in filenames:
631                with open(fname, 'rb') as infile:
632                    outfile.write(infile.read())
633
634        os.remove(out_bin)
635        os.rename(out_tmp, out_bin)
636