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