1#! /usr/bin/env python3
2
3# Copyright (c) 2017 Linaro Limited.
4# Copyright (c) 2017 Open Source Foundries Limited.
5#
6# SPDX-License-Identifier: Apache-2.0
7
8"""Zephyr binary runner core interfaces
9
10This provides the core ZephyrBinaryRunner class meant for public use,
11as well as some other helpers for concrete runner classes.
12"""
13
14import abc
15import argparse
16import errno
17import logging
18import os
19import platform
20import shlex
21import shutil
22import signal
23import subprocess
24import re
25from functools import partial
26from enum import Enum
27from inspect import isabstract
28from typing import Dict, List, NamedTuple, NoReturn, Optional, Set, Type, \
29    Union
30
31# Turn on to enable just logging the commands that would be run (at
32# info rather than debug level), without actually running them. This
33# can break runners that are expecting output or if one command
34# depends on another, so it's just for debugging.
35_DRY_RUN = False
36
37_logger = logging.getLogger('runners')
38
39
40class _DebugDummyPopen:
41
42    def terminate(self):
43        pass
44
45    def wait(self):
46        pass
47
48
49MAX_PORT = 49151
50
51
52class NetworkPortHelper:
53    '''Helper class for dealing with local IP network ports.'''
54
55    def get_unused_ports(self, starting_from):
56        '''Find unused network ports, starting at given values.
57
58        starting_from is an iterable of ports the caller would like to use.
59
60        The return value is an iterable of ports, in the same order, using
61        the given values if they were unused, or the next sequentially
62        available unused port otherwise.
63
64        Ports may be bound between this call's check and actual usage, so
65        callers still need to handle errors involving returned ports.'''
66        start = list(starting_from)
67        used = self._used_now()
68        ret = []
69
70        for desired in start:
71            port = desired
72            while port in used:
73                port += 1
74                if port > MAX_PORT:
75                    msg = "ports above {} are in use"
76                    raise ValueError(msg.format(desired))
77            used.add(port)
78            ret.append(port)
79
80        return ret
81
82    def _used_now(self):
83        handlers = {
84            'Windows': self._used_now_windows,
85            'Linux': self._used_now_linux,
86            'Darwin': self._used_now_darwin,
87        }
88        handler = handlers[platform.system()]
89        return handler()
90
91    def _used_now_windows(self):
92        cmd = ['netstat', '-a', '-n', '-p', 'tcp']
93        return self._parser_windows(cmd)
94
95    def _used_now_linux(self):
96        cmd = ['ss', '-a', '-n', '-t']
97        return self._parser_linux(cmd)
98
99    def _used_now_darwin(self):
100        cmd = ['netstat', '-a', '-n', '-p', 'tcp']
101        return self._parser_darwin(cmd)
102
103    @staticmethod
104    def _parser_windows(cmd):
105        out = subprocess.check_output(cmd).split(b'\r\n')
106        used_bytes = [x.split()[1].rsplit(b':', 1)[1] for x in out
107                      if x.startswith(b'  TCP')]
108        return {int(b) for b in used_bytes}
109
110    @staticmethod
111    def _parser_linux(cmd):
112        out = subprocess.check_output(cmd).splitlines()[1:]
113        used_bytes = [s.split()[3].rsplit(b':', 1)[1] for s in out]
114        return {int(b) for b in used_bytes}
115
116    @staticmethod
117    def _parser_darwin(cmd):
118        out = subprocess.check_output(cmd).split(b'\n')
119        used_bytes = [x.split()[3].rsplit(b':', 1)[1] for x in out
120                      if x.startswith(b'tcp')]
121        return {int(b) for b in used_bytes}
122
123
124class BuildConfiguration:
125    '''This helper class provides access to build-time configuration.
126
127    Configuration options can be read as if the object were a dict,
128    either object['CONFIG_FOO'] or object.get('CONFIG_FOO').
129
130    Kconfig configuration values are available (parsed from .config).'''
131
132    def __init__(self, build_dir: str):
133        self.build_dir = build_dir
134        self.options: Dict[str, Union[str, int]] = {}
135        self.path = os.path.join(self.build_dir, 'zephyr', '.config')
136        self._parse()
137
138    def __contains__(self, item):
139        return item in self.options
140
141    def __getitem__(self, item):
142        return self.options[item]
143
144    def get(self, option, *args):
145        return self.options.get(option, *args)
146
147    def getboolean(self, option):
148        '''If a boolean option is explicitly set to y or n,
149        returns its value. Otherwise, falls back to False.
150        '''
151        return self.options.get(option, False)
152
153    def _parse(self):
154        filename = self.path
155        opt_value = re.compile('^(?P<option>CONFIG_[A-Za-z0-9_]+)=(?P<value>.*)$')
156        not_set = re.compile('^# (?P<option>CONFIG_[A-Za-z0-9_]+) is not set$')
157
158        with open(filename, 'r') as f:
159            for line in f:
160                match = opt_value.match(line)
161                if match:
162                    value = match.group('value').rstrip()
163                    if value.startswith('"') and value.endswith('"'):
164                        # A string literal should have the quotes stripped,
165                        # but otherwise be left as is.
166                        value = value[1:-1]
167                    elif value == 'y':
168                        # The character 'y' is a boolean option
169                        # that is set to True.
170                        value = True
171                    else:
172                        # Neither a string nor 'y', so try to parse it
173                        # as an integer.
174                        try:
175                            base = 16 if value.startswith('0x') else 10
176                            self.options[match.group('option')] = int(value, base=base)
177                            continue
178                        except ValueError:
179                            pass
180
181                    self.options[match.group('option')] = value
182                    continue
183
184                match = not_set.match(line)
185                if match:
186                    # '# CONFIG_FOO is not set' means a boolean option is false.
187                    self.options[match.group('option')] = False
188
189class MissingProgram(FileNotFoundError):
190    '''FileNotFoundError subclass for missing program dependencies.
191
192    No significant changes from the parent FileNotFoundError; this is
193    useful for explicitly signaling that the file in question is a
194    program that some class requires to proceed.
195
196    The filename attribute contains the missing program.'''
197
198    def __init__(self, program):
199        super().__init__(errno.ENOENT, os.strerror(errno.ENOENT), program)
200
201
202class RunnerCaps:
203    '''This class represents a runner class's capabilities.
204
205    Each capability is represented as an attribute with the same
206    name. Flag attributes are True or False.
207
208    Available capabilities:
209
210    - commands: set of supported commands; default is {'flash',
211      'debug', 'debugserver', 'attach'}.
212
213    - dev_id: whether the runner supports device identifiers, in the form of an
214      -i, --dev-id option. This is useful when the user has multiple debuggers
215      connected to a single computer, in order to select which one will be used
216      with the command provided.
217
218    - flash_addr: whether the runner supports flashing to an
219      arbitrary address. Default is False. If true, the runner
220      must honor the --dt-flash option.
221
222    - erase: whether the runner supports an --erase option, which
223      does a mass-erase of the entire addressable flash on the target
224      before flashing. On multi-core SoCs, this may only erase portions of
225      flash specific the actual target core. (This option can be useful for
226      things like clearing out old settings values or other subsystem state
227      that may affect the behavior of the zephyr image. It is also sometimes
228      needed by SoCs which have flash-like areas that can't be sector
229      erased by the underlying tool before flashing; UICR on nRF SoCs
230      is one example.)
231
232    - tool_opt: whether the runner supports a --tool-opt (-O) option, which
233      can be given multiple times and is passed on to the underlying tool
234      that the runner wraps.
235    '''
236
237    def __init__(self,
238                 commands: Set[str] = {'flash', 'debug',
239                                       'debugserver', 'attach'},
240                 dev_id: bool = False,
241                 flash_addr: bool = False,
242                 erase: bool = False,
243                 tool_opt: bool = False,
244                 file: bool = False):
245        self.commands = commands
246        self.dev_id = dev_id
247        self.flash_addr = bool(flash_addr)
248        self.erase = bool(erase)
249        self.tool_opt = bool(tool_opt)
250        self.file = bool(file)
251
252    def __str__(self):
253        return (f'RunnerCaps(commands={self.commands}, '
254                f'dev_id={self.dev_id}, '
255                f'flash_addr={self.flash_addr}, '
256                f'erase={self.erase}, '
257                f'tool_opt={self.tool_opt}, '
258                f'file={self.file}'
259                ')')
260
261
262def _missing_cap(cls: Type['ZephyrBinaryRunner'], option: str) -> NoReturn:
263    # Helper function that's called when an option was given on the
264    # command line that corresponds to a missing capability in the
265    # runner class cls.
266
267    raise ValueError(f"{cls.name()} doesn't support {option} option")
268
269
270class FileType(Enum):
271    OTHER = 0
272    HEX = 1
273    BIN = 2
274    ELF = 3
275
276
277class RunnerConfig(NamedTuple):
278    '''Runner execution-time configuration.
279
280    This is a common object shared by all runners. Individual runners
281    can register specific configuration options using their
282    do_add_parser() hooks.
283    '''
284    build_dir: str                  # application build directory
285    board_dir: str                  # board definition directory
286    elf_file: Optional[str]         # zephyr.elf path, or None
287    hex_file: Optional[str]         # zephyr.hex path, or None
288    bin_file: Optional[str]         # zephyr.bin path, or None
289    uf2_file: Optional[str]         # zephyr.uf2 path, or None
290    file: Optional[str]             # binary file path (provided by the user), or None
291    file_type: Optional[FileType] = FileType.OTHER  # binary file type
292    gdb: Optional[str] = None       # path to a usable gdb
293    openocd: Optional[str] = None   # path to a usable openocd
294    openocd_search: List[str] = []  # add these paths to the openocd search path
295
296
297_YN_CHOICES = ['Y', 'y', 'N', 'n', 'yes', 'no', 'YES', 'NO']
298
299
300class _DTFlashAction(argparse.Action):
301
302    def __call__(self, parser, namespace, values, option_string=None):
303        if values.lower().startswith('y'):
304            namespace.dt_flash = True
305        else:
306            namespace.dt_flash = False
307
308
309class _ToggleAction(argparse.Action):
310
311    def __call__(self, parser, args, ignored, option):
312        setattr(args, self.dest, not option.startswith('--no-'))
313
314class DeprecatedAction(argparse.Action):
315
316    def __call__(self, parser, namespace, values, option_string=None):
317        _logger.warning(f'Argument {self.option_strings[0]} is deprecated' +
318                        (f' for your runner {self._cls.name()}'  if self._cls is not None else '') +
319                        f', use {self._replacement} instead.')
320        setattr(namespace, self.dest, values)
321
322def depr_action(*args, cls=None, replacement=None, **kwargs):
323    action = DeprecatedAction(*args, **kwargs)
324    setattr(action, '_cls', cls)
325    setattr(action, '_replacement', replacement)
326    return action
327
328class ZephyrBinaryRunner(abc.ABC):
329    '''Abstract superclass for binary runners (flashers, debuggers).
330
331    **Note**: this class's API has changed relatively rarely since it
332    as added, but it is not considered a stable Zephyr API, and may change
333    without notice.
334
335    With some exceptions, boards supported by Zephyr must provide
336    generic means to be flashed (have a Zephyr firmware binary
337    permanently installed on the device for running) and debugged
338    (have a breakpoint debugger and program loader on a host
339    workstation attached to a running target).
340
341    This is supported by four top-level commands managed by the
342    Zephyr build system:
343
344    - 'flash': flash a previously configured binary to the board,
345      start execution on the target, then return.
346
347    - 'debug': connect to the board via a debugging protocol, program
348      the flash, then drop the user into a debugger interface with
349      symbol tables loaded from the current binary, and block until it
350      exits.
351
352    - 'debugserver': connect via a board-specific debugging protocol,
353      then reset and halt the target. Ensure the user is now able to
354      connect to a debug server with symbol tables loaded from the
355      binary.
356
357    - 'attach': connect to the board via a debugging protocol, then drop
358      the user into a debugger interface with symbol tables loaded from
359      the current binary, and block until it exits. Unlike 'debug', this
360      command does not program the flash.
361
362    This class provides an API for these commands. Every subclass is
363    called a 'runner' for short. Each runner has a name (like
364    'pyocd'), and declares commands it can handle (like
365    'flash'). Boards (like 'nrf52dk_nrf52832') declare which runner(s)
366    are compatible with them to the Zephyr build system, along with
367    information on how to configure the runner to work with the board.
368
369    The build system will then place enough information in the build
370    directory to create and use runners with this class's create()
371    method, which provides a command line argument parsing API. You
372    can also create runners by instantiating subclasses directly.
373
374    In order to define your own runner, you need to:
375
376    1. Define a ZephyrBinaryRunner subclass, and implement its
377       abstract methods. You may need to override capabilities().
378
379    2. Make sure the Python module defining your runner class is
380       imported, e.g. by editing this package's __init__.py (otherwise,
381       get_runners() won't work).
382
383    3. Give your runner's name to the Zephyr build system in your
384       board's board.cmake.
385
386    Additional advice:
387
388    - If you need to import any non-standard-library modules, make sure
389      to catch ImportError and defer complaints about it to a RuntimeError
390      if one is missing. This avoids affecting users that don't require your
391      runner, while still making it clear what went wrong to users that do
392      require it that don't have the necessary modules installed.
393
394    - If you need to ask the user something (e.g. using input()), do it
395      in your create() classmethod, not do_run(). That ensures your
396      __init__() really has everything it needs to call do_run(), and also
397      avoids calling input() when not instantiating within a command line
398      application.
399
400    - Use self.logger to log messages using the standard library's
401      logging API; your logger is named "runner.<your-runner-name()>"
402
403    For command-line invocation from the Zephyr build system, runners
404    define their own argparse-based interface through the common
405    add_parser() (and runner-specific do_add_parser() it delegates
406    to), and provide a way to create instances of themselves from
407    a RunnerConfig and parsed runner-specific arguments via create().
408
409    Runners use a variety of host tools and configuration values, the
410    user interface to which is abstracted by this class. Each runner
411    subclass should take any values it needs to execute one of these
412    commands in its constructor.  The actual command execution is
413    handled in the run() method.'''
414
415    def __init__(self, cfg: RunnerConfig):
416        '''Initialize core runner state.'''
417
418        self.cfg = cfg
419        '''RunnerConfig for this instance.'''
420
421        self.logger = logging.getLogger('runners.{}'.format(self.name()))
422        '''logging.Logger for this instance.'''
423
424    @staticmethod
425    def get_runners() -> List[Type['ZephyrBinaryRunner']]:
426        '''Get a list of all currently defined runner classes.'''
427        def inheritors(klass):
428            subclasses = set()
429            work = [klass]
430            while work:
431                parent = work.pop()
432                for child in parent.__subclasses__():
433                    if child not in subclasses:
434                        if not isabstract(child):
435                            subclasses.add(child)
436                        work.append(child)
437            return subclasses
438
439        return inheritors(ZephyrBinaryRunner)
440
441    @classmethod
442    @abc.abstractmethod
443    def name(cls) -> str:
444        '''Return this runner's user-visible name.
445
446        When choosing a name, pick something short and lowercase,
447        based on the name of the tool (like openocd, jlink, etc.) or
448        the target architecture/board (like xtensa etc.).'''
449
450    @classmethod
451    def capabilities(cls) -> RunnerCaps:
452        '''Returns a RunnerCaps representing this runner's capabilities.
453
454        This implementation returns the default capabilities.
455
456        Subclasses should override appropriately if needed.'''
457        return RunnerCaps()
458
459    @classmethod
460    def add_parser(cls, parser):
461        '''Adds a sub-command parser for this runner.
462
463        The given object, parser, is a sub-command parser from the
464        argparse module. For more details, refer to the documentation
465        for argparse.ArgumentParser.add_subparsers().
466
467        The lone common optional argument is:
468
469        * --dt-flash (if the runner capabilities includes flash_addr)
470
471        Runner-specific options are added through the do_add_parser()
472        hook.'''
473        # Unfortunately, the parser argument's type is not documented
474        # in typeshed, so we can't type annotate much here.
475
476        # Common options that depend on runner capabilities. If a
477        # capability is not supported, the option string or strings
478        # are added anyway, to prevent an individual runner class from
479        # using them to mean something else.
480        caps = cls.capabilities()
481
482        if caps.dev_id:
483            parser.add_argument('-i', '--dev-id',
484                                dest='dev_id',
485                                help=cls.dev_id_help())
486        else:
487            parser.add_argument('-i', '--dev-id', help=argparse.SUPPRESS)
488
489        if caps.flash_addr:
490            parser.add_argument('--dt-flash', default='n', choices=_YN_CHOICES,
491                                action=_DTFlashAction,
492                                help='''If 'yes', try to use flash address
493                                information from devicetree when flash
494                                addresses are unknown (e.g. when flashing a .bin)''')
495        else:
496            parser.add_argument('--dt-flash', help=argparse.SUPPRESS)
497
498        if caps.file:
499            parser.add_argument('-f', '--file',
500                                dest='file',
501                                help="path to binary file")
502            parser.add_argument('-t', '--file-type',
503                                dest='file_type',
504                                help="type of binary file")
505        else:
506            parser.add_argument('-f', '--file', help=argparse.SUPPRESS)
507            parser.add_argument('-t', '--file-type', help=argparse.SUPPRESS)
508
509        parser.add_argument('--elf-file',
510                        metavar='FILE',
511                        action=(partial(depr_action, cls=cls, replacement='-f/--file') if caps.file else None),
512                        help='path to zephyr.elf' if not caps.file else 'Deprecated, use -f/--file instead.')
513        parser.add_argument('--hex-file',
514                        metavar='FILE',
515                        action=(partial(depr_action, cls=cls, replacement='-f/--file') if caps.file else None),
516                        help='path to zephyr.hex' if not caps.file else 'Deprecated, use -f/--file instead.')
517        parser.add_argument('--bin-file',
518                        metavar='FILE',
519                        action=(partial(depr_action, cls=cls, replacement='-f/--file') if caps.file else None),
520                        help='path to zephyr.bin' if not caps.file else 'Deprecated, use -f/--file instead.')
521
522        parser.add_argument('--erase', '--no-erase', nargs=0,
523                            action=_ToggleAction,
524                            help=("mass erase flash before loading, or don't"
525                                  if caps.erase else argparse.SUPPRESS))
526
527        parser.add_argument('-O', '--tool-opt', dest='tool_opt',
528                            default=[], action='append',
529                            help=(cls.tool_opt_help() if caps.tool_opt
530                                  else argparse.SUPPRESS))
531
532        # Runner-specific options.
533        cls.do_add_parser(parser)
534
535    @classmethod
536    @abc.abstractmethod
537    def do_add_parser(cls, parser):
538        '''Hook for adding runner-specific options.'''
539
540    @classmethod
541    def create(cls, cfg: RunnerConfig,
542               args: argparse.Namespace) -> 'ZephyrBinaryRunner':
543        '''Create an instance from command-line arguments.
544
545        - ``cfg``: runner configuration (pass to superclass __init__)
546        - ``args``: arguments parsed from execution environment, as
547          specified by ``add_parser()``.'''
548        caps = cls.capabilities()
549        if args.dev_id and not caps.dev_id:
550            _missing_cap(cls, '--dev-id')
551        if args.dt_flash and not caps.flash_addr:
552            _missing_cap(cls, '--dt-flash')
553        if args.erase and not caps.erase:
554            _missing_cap(cls, '--erase')
555        if args.tool_opt and not caps.tool_opt:
556            _missing_cap(cls, '--tool-opt')
557        if args.file and not caps.file:
558            _missing_cap(cls, '--file')
559        if args.file_type and not args.file:
560            raise ValueError("--file-type requires --file")
561        if args.file_type and not caps.file:
562            _missing_cap(cls, '--file-type')
563
564        ret = cls.do_create(cfg, args)
565        if args.erase:
566            ret.logger.info('mass erase requested')
567        return ret
568
569    @classmethod
570    @abc.abstractmethod
571    def do_create(cls, cfg: RunnerConfig,
572                  args: argparse.Namespace) -> 'ZephyrBinaryRunner':
573        '''Hook for instance creation from command line arguments.'''
574
575    @staticmethod
576    def get_flash_address(args: argparse.Namespace,
577                          build_conf: BuildConfiguration,
578                          default: int = 0x0) -> int:
579        '''Helper method for extracting a flash address.
580
581        If args.dt_flash is true, returns the address obtained from
582        ZephyrBinaryRunner.flash_address_from_build_conf(build_conf).
583
584        Otherwise (when args.dt_flash is False), the default value is
585        returned.'''
586        if args.dt_flash:
587            return ZephyrBinaryRunner.flash_address_from_build_conf(build_conf)
588        else:
589            return default
590
591    @staticmethod
592    def flash_address_from_build_conf(build_conf: BuildConfiguration):
593        '''If CONFIG_HAS_FLASH_LOAD_OFFSET is n in build_conf,
594        return the CONFIG_FLASH_BASE_ADDRESS value. Otherwise, return
595        CONFIG_FLASH_BASE_ADDRESS + CONFIG_FLASH_LOAD_OFFSET.
596        '''
597        if build_conf.getboolean('CONFIG_HAS_FLASH_LOAD_OFFSET'):
598            return (build_conf['CONFIG_FLASH_BASE_ADDRESS'] +
599                    build_conf['CONFIG_FLASH_LOAD_OFFSET'])
600        else:
601            return build_conf['CONFIG_FLASH_BASE_ADDRESS']
602
603    def run(self, command: str, **kwargs):
604        '''Runs command ('flash', 'debug', 'debugserver', 'attach').
605
606        This is the main entry point to this runner.'''
607        caps = self.capabilities()
608        if command not in caps.commands:
609            raise ValueError('runner {} does not implement command {}'.format(
610                self.name(), command))
611        self.do_run(command, **kwargs)
612
613    @abc.abstractmethod
614    def do_run(self, command: str, **kwargs):
615        '''Concrete runner; run() delegates to this. Implement in subclasses.
616
617        In case of an unsupported command, raise a ValueError.'''
618
619    @property
620    def build_conf(self) -> BuildConfiguration:
621        '''Get a BuildConfiguration for the build directory.'''
622        if not hasattr(self, '_build_conf'):
623            self._build_conf = BuildConfiguration(self.cfg.build_dir)
624        return self._build_conf
625
626    @property
627    def thread_info_enabled(self) -> bool:
628        '''Returns True if self.build_conf has
629        CONFIG_DEBUG_THREAD_INFO enabled.
630        '''
631        return self.build_conf.getboolean('CONFIG_DEBUG_THREAD_INFO')
632
633    @classmethod
634    def dev_id_help(cls) -> str:
635        ''' Get the ArgParse help text for the --dev-id option.'''
636        return '''Device identifier. Use it to select
637                  which debugger, device, node or instance to
638                  target when multiple ones are available or
639                  connected.'''
640
641    @classmethod
642    def tool_opt_help(cls) -> str:
643        ''' Get the ArgParse help text for the --tool-opt option.'''
644        return '''Option to pass on to the underlying tool used
645                  by this runner. This can be given multiple times;
646                  the resulting arguments will be given to the tool
647                  in the order they appear on the command line.'''
648
649    @staticmethod
650    def require(program: str) -> str:
651        '''Require that a program is installed before proceeding.
652
653        :param program: name of the program that is required,
654                        or path to a program binary.
655
656        If ``program`` is an absolute path to an existing program
657        binary, this call succeeds. Otherwise, try to find the program
658        by name on the system PATH.
659
660        If the program can be found, its path is returned.
661        Otherwise, raises MissingProgram.'''
662        ret = shutil.which(program)
663        if ret is None:
664            raise MissingProgram(program)
665        return ret
666
667    def run_server_and_client(self, server, client):
668        '''Run a server that ignores SIGINT, and a client that handles it.
669
670        This routine portably:
671
672        - creates a Popen object for the ``server`` command which ignores
673          SIGINT
674        - runs ``client`` in a subprocess while temporarily ignoring SIGINT
675        - cleans up the server after the client exits.
676
677        It's useful to e.g. open a GDB server and client.'''
678        server_proc = self.popen_ignore_int(server)
679        try:
680            self.run_client(client)
681        finally:
682            server_proc.terminate()
683            server_proc.wait()
684
685    def run_client(self, client):
686        '''Run a client that handles SIGINT.'''
687        previous = signal.signal(signal.SIGINT, signal.SIG_IGN)
688        try:
689            self.check_call(client)
690        finally:
691            signal.signal(signal.SIGINT, previous)
692
693    def _log_cmd(self, cmd: List[str]):
694        escaped = ' '.join(shlex.quote(s) for s in cmd)
695        if not _DRY_RUN:
696            self.logger.debug(escaped)
697        else:
698            self.logger.info(escaped)
699
700    def call(self, cmd: List[str], **kwargs) -> int:
701        '''Subclass subprocess.call() wrapper.
702
703        Subclasses should use this method to run command in a
704        subprocess and get its return code, rather than
705        using subprocess directly, to keep accurate debug logs.
706        '''
707        self._log_cmd(cmd)
708        if _DRY_RUN:
709            return 0
710        return subprocess.call(cmd, **kwargs)
711
712    def check_call(self, cmd: List[str], **kwargs):
713        '''Subclass subprocess.check_call() wrapper.
714
715        Subclasses should use this method to run command in a
716        subprocess and check that it executed correctly, rather than
717        using subprocess directly, to keep accurate debug logs.
718        '''
719        self._log_cmd(cmd)
720        if _DRY_RUN:
721            return
722        subprocess.check_call(cmd, **kwargs)
723
724    def check_output(self, cmd: List[str], **kwargs) -> bytes:
725        '''Subclass subprocess.check_output() wrapper.
726
727        Subclasses should use this method to run command in a
728        subprocess and check that it executed correctly, rather than
729        using subprocess directly, to keep accurate debug logs.
730        '''
731        self._log_cmd(cmd)
732        if _DRY_RUN:
733            return b''
734        return subprocess.check_output(cmd, **kwargs)
735
736    def popen_ignore_int(self, cmd: List[str], **kwargs) -> subprocess.Popen:
737        '''Spawn a child command, ensuring it ignores SIGINT.
738
739        The returned subprocess.Popen object must be manually terminated.'''
740        cflags = 0
741        preexec = None
742        system = platform.system()
743
744        if system == 'Windows':
745            # We can't type check this line on Unix operating systems:
746            # mypy thinks the subprocess module has no such attribute.
747            cflags |= subprocess.CREATE_NEW_PROCESS_GROUP  # type: ignore
748        elif system in {'Linux', 'Darwin'}:
749            # We can't type check this on Windows for the same reason.
750            preexec = os.setsid # type: ignore
751
752        self._log_cmd(cmd)
753        if _DRY_RUN:
754            return _DebugDummyPopen()  # type: ignore
755
756        return subprocess.Popen(cmd, creationflags=cflags, preexec_fn=preexec, **kwargs)
757
758    def ensure_output(self, output_type: str) -> None:
759        '''Ensure self.cfg has a particular output artifact.
760
761        For example, ensure_output('bin') ensures that self.cfg.bin_file
762        refers to an existing file. Errors out if it's missing or undefined.
763
764        :param output_type: string naming the output type
765        '''
766        output_file = getattr(self.cfg, f'{output_type}_file', None)
767
768        if output_file is None:
769            err = f'{output_type} file location is unknown.'
770        elif not os.path.isfile(output_file):
771            err = f'{output_file} does not exist.'
772        else:
773            return
774
775        if output_type in ('elf', 'hex', 'bin', 'uf2'):
776            err += f' Try enabling CONFIG_BUILD_OUTPUT_{output_type.upper()}.'
777
778        # RuntimeError avoids a stack trace saved in run_common.
779        raise RuntimeError(err)
780