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 preprocess_toml(self, config_dir, toml_basename, subdir):
425        'Runs the C pre-processor on config_dir/toml_basename.h'
426
427        compiler_path = self.cmake_cache.get("CMAKE_C_COMPILER")
428        preproc_cmd = [compiler_path, '-E', str(config_dir / (toml_basename + '.h'))]
429        # -P removes line markers to keep the .toml output reproducible.  To
430        # trace #includes, temporarily comment out '-P' (-f*-prefix-map
431        # unfortunately don't seem to make any difference here and they're
432        # gcc-specific)
433        preproc_cmd += ['-P']
434
435        # "REM" escapes _leading_ '#' characters from cpp and allows
436        # such comments to be preserved in generated/*.toml files:
437        #
438        #      REM # my comment...
439        #
440        # Note _trailing_ '#' characters and comments are ignored by cpp
441        # and don't need any REM trick.
442        preproc_cmd += ['-DREM=']
443
444        preproc_cmd += ['-I', str(self.sof_src_dir / 'src')]
445        preproc_cmd += ['-imacros',
446                        str(pathlib.Path('zephyr') / 'include' / 'generated' / 'zephyr' / 'autoconf.h')]
447        # Need to preprocess the TOML file twice: once with
448        # LLEXT_FORCE_ALL_MODULAR defined and once without it
449        full_preproc_cmd = preproc_cmd + ['-o', str(subdir / 'rimage_config_full.toml'), '-DLLEXT_FORCE_ALL_MODULAR']
450        preproc_cmd += ['-o', str(subdir / 'rimage_config.toml')]
451        self.command.inf(quote_sh_list(preproc_cmd))
452        subprocess.run(preproc_cmd, check=True, cwd=self.build_dir)
453        subprocess.run(full_preproc_cmd, check=True, cwd=self.build_dir)
454
455    def sign(self, command, build_dir, build_conf, formats):
456        self.command = command
457        args = command.args
458
459        b = pathlib.Path(build_dir)
460        self.build_dir = b
461        cache = CMakeCache.from_build_dir(build_dir)
462        self.cmake_cache = cache
463
464        # Warning: RIMAGE_TARGET in Zephyr is a duplicate of
465        # CONFIG_RIMAGE_SIGNING_SCHEMA in SOF.
466        target = cache.get('RIMAGE_TARGET')
467
468        if not target:
469            msg = 'rimage target not defined in board.cmake'
470            if args.if_tool_available:
471                command.inf(msg)
472                sys.exit(0)
473            else:
474                command.die(msg)
475
476        kernel_name = build_conf.get('CONFIG_KERNEL_BIN_NAME', 'zephyr')
477
478        bootloader = None
479        cold = None
480        kernel = str(b / 'zephyr' / f'{kernel_name}.elf')
481        out_bin = str(b / 'zephyr' / f'{kernel_name}.ri')
482        out_xman = str(b / 'zephyr' / f'{kernel_name}.ri.xman')
483        out_tmp = str(b / 'zephyr' / f'{kernel_name}.rix')
484
485        # Intel platforms generate a "boot.mod" and "main.mod" as
486        # separate intermediates to use.  Other platforms just use
487        # zephyr.elf directly.
488        if os.path.exists(str(b / 'zephyr' / 'boot.mod')):
489            bootloader = str(b / 'zephyr' / 'boot.mod')
490        if os.path.exists(str(b / 'zephyr' / 'cold.mod')):
491            cold = str(b / 'zephyr' / 'cold.mod')
492            with open(cold, 'rb') as f_cold:
493                elf = ELFFile(f_cold)
494                if elf.get_section_by_name('.cold') is None:
495                    cold = None
496        if os.path.exists(str(b / 'zephyr' / 'main.mod')):
497            kernel = str(b / 'zephyr' / 'main.mod')
498
499        # Clean any stale output. This is especially important when using --if-tool-available
500        # (but not just)
501        for o in [ out_bin, out_xman, out_tmp ]:
502            pathlib.Path(o).unlink(missing_ok=True)
503
504        tool_path = (
505            args.tool_path if args.tool_path else
506            command.config_get('rimage.path', None)
507        )
508        err_prefix = '--tool-path' if args.tool_path else 'west config'
509
510        if tool_path:
511            command.check_force(shutil.which(tool_path),
512                                f'{err_prefix} {tool_path}: not an executable')
513        else:
514            tool_path = shutil.which('rimage')
515            if not tool_path:
516                err_msg = 'rimage not found; either install it or provide --tool-path'
517                if args.if_tool_available:
518                    command.wrn(err_msg)
519                    command.wrn('zephyr binary _not_ signed!')
520                    return
521                else:
522                    command.die(err_msg)
523
524        #### -c sof/rimage/config/signing_schema.toml  ####
525
526        if not args.quiet:
527            command.inf('Signing with tool {}'.format(tool_path))
528
529        try:
530            sof_proj = command.manifest.get_projects(['sof'], allow_paths=False)
531            sof_src_dir = pathlib.Path(sof_proj[0].abspath)
532        except ValueError: # sof is the manifest
533            sof_src_dir = pathlib.Path(manifest.manifest_path()).parent
534
535        self.sof_src_dir = sof_src_dir
536
537
538        command.inf('Signing for SOC target ' + target)
539
540        # FIXME: deprecate --no-manifest and replace it with a much
541        # simpler and more direct `-- -e` which the user can _already_
542        # pass today! With unclear consequences right now...
543        if '--no-manifest' in args.tool_args:
544            no_manifest = True
545            args.tool_args.remove('--no-manifest')
546        else:
547            no_manifest = False
548
549        # Non-SOF build does not have extended manifest data for
550        # rimage to process, which might result in rimage error.
551        # So skip it when not doing SOF builds.
552        is_sof_build = build_conf.getboolean('CONFIG_SOF')
553        if not is_sof_build:
554            no_manifest = True
555
556        if no_manifest:
557            extra_ri_args = [ ]
558        else:
559            extra_ri_args = ['-e']
560
561        sign_base = [tool_path]
562
563        # Align rimage verbosity.
564        # Sub-command arg 'west sign -q' takes precedence over west '-v'
565        if not args.quiet and args.verbose:
566            sign_base += ['-v'] * args.verbose
567
568        # Order is important
569        components = [ ] if bootloader is None else [ bootloader ]
570        if cold is not None:
571            components += [ cold ]
572        components += [ kernel ]
573
574        sign_config_extra_args = command.config_get_words('rimage.extra-args', [])
575
576        if '-k' not in sign_config_extra_args + args.tool_args:
577            # rimage requires a key argument even when it does not sign
578            cmake_default_key = cache.get('RIMAGE_SIGN_KEY', 'key placeholder from sign.py')
579            extra_ri_args += [ '-k', str(sof_src_dir / 'keys' / cmake_default_key) ]
580
581        if args.tool_data and '-c' in args.tool_args:
582            command.wrn('--tool-data ' + args.tool_data + ' ignored! Overridden by: -- -c ... ')
583
584        if '-c' not in sign_config_extra_args + args.tool_args:
585            conf_dir = self.rimage_config_dir()
586            toml_basename = target + '.toml'
587            if ((conf_dir / toml_basename).exists() and
588               (conf_dir / (toml_basename + '.h')).exists()):
589                command.die(f"Cannot have both {toml_basename + '.h'} and {toml_basename} in {conf_dir}")
590
591            if (conf_dir / (toml_basename + '.h')).exists():
592                generated_subdir = pathlib.Path('zephyr') / 'misc' / 'generated'
593                self.preprocess_toml(conf_dir, toml_basename, generated_subdir)
594                extra_ri_args += ['-c', str(b / generated_subdir / 'rimage_config.toml')]
595            else:
596                toml_dir = conf_dir
597                extra_ri_args += ['-c', str(toml_dir / toml_basename)]
598
599        # Warning: while not officially supported (yet?), the rimage --option that is last
600        # on the command line currently wins in case of duplicate options. So pay
601        # attention to the _args order below.
602        sign_base += (['-o', out_bin] + sign_config_extra_args +
603                      extra_ri_args + args.tool_args + components)
604
605        command.inf(quote_sh_list(sign_base))
606        subprocess.check_call(sign_base)
607
608        if no_manifest:
609            filenames = [out_bin]
610        else:
611            filenames = [out_xman, out_bin]
612            if not args.quiet:
613                command.inf('Prefixing ' + out_bin + ' with manifest ' + out_xman)
614        with open(out_tmp, 'wb') as outfile:
615            for fname in filenames:
616                with open(fname, 'rb') as infile:
617                    outfile.write(infile.read())
618
619        os.remove(out_bin)
620        os.rename(out_tmp, out_bin)
621