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