1# Copyright 2022 NXP
2# SPDX-License-Identifier: Apache-2.0
3
4'''Runner for Lauterbach TRACE32.'''
5
6import argparse
7import os
8import platform
9import subprocess
10from pathlib import Path
11from tempfile import TemporaryDirectory
12
13from runners.core import BuildConfiguration, RunnerCaps, RunnerConfig, ZephyrBinaryRunner
14
15DEFAULT_T32_CONFIG = Path('config.t32')
16
17
18class TRACE32BinaryRunner(ZephyrBinaryRunner):
19    '''
20    Runner front-end for Lauterbach TRACE32.
21
22    The runner is a wrapper around Lauterbach TRACE32 PowerView. It executes a Lauterbach Practice
23    script (.cmm) after launching the debugger, which should be located at
24    zephyr/boards/<board>/support/<command>.cmm, where <board> is the board directory and <command>
25    is the name of the west runner command executed (e.g. flash or debug). Extra arguments can be
26    passed to the startup script by using the command line option --startup-args.
27    '''
28
29    def __init__(self,
30                 cfg: RunnerConfig,
31                 t32_cfg: Path,
32                 arch: str,
33                 startup_args: list[str] | None = None,
34                 timeout: int = 60) -> None:
35        super().__init__(cfg)
36        self.arch = arch
37        self.t32_cfg = t32_cfg
38        self.t32_exec: Path | None = None
39        self.startup_dir = Path(cfg.board_dir) / 'support'
40        self.startup_args = startup_args
41        self.timeout = timeout
42
43    @classmethod
44    def name(cls) -> str:
45        return 'trace32'
46
47    @classmethod
48    def capabilities(cls) -> RunnerCaps:
49        return RunnerCaps(commands={'flash', 'debug'})
50
51    @classmethod
52    def do_add_parser(cls, parser: argparse.ArgumentParser) -> None:
53        parser.add_argument('--arch',
54                            default='auto',
55                            choices=('auto', 'arm', 'riscv', 'xtensa'),
56                            help='Target architecture. Set to "auto" to select the architecture '
57                                 'based on CONFIG_ARCH value')
58        parser.add_argument('--config',
59                            default=DEFAULT_T32_CONFIG,
60                            type=Path,
61                            help='Override TRACE32 configuration file path. Can be a relative path '
62                                 'to T32_DIR environment variable, or an absolute path')
63        parser.add_argument('--startup-args',
64                            nargs='*',
65                            help='Arguments to pass to the start-up script')
66        parser.add_argument('--timeout',
67                            default=60,
68                            type=int,
69                            help='Timeout, in seconds, of the flash operation')
70
71    @classmethod
72    def do_create(cls, cfg: RunnerConfig, args: argparse.Namespace) -> 'TRACE32BinaryRunner':
73        build_conf = BuildConfiguration(cfg.build_dir)
74        if args.arch == 'auto':
75            arch = build_conf.get('CONFIG_ARCH').replace('"', '')
76            # there is a single binary for all ARM architectures
77            arch = arch.replace('arm64', 'arm')
78        else:
79            arch = args.arch
80        return TRACE32BinaryRunner(cfg, args.config, arch, startup_args=args.startup_args,
81                                   timeout=args.timeout)
82
83    def do_run(self, command, **kwargs) -> None:
84        t32_dir = os.environ.get('T32_DIR')
85        if not t32_dir:
86            raise RuntimeError('T32_DIR environment variable undefined')
87
88        if platform.system() == 'Windows':
89            os_name = 'windows64'
90            suffix = '.exe'
91        elif platform.system() == 'Linux':
92            os_name = 'pc_linux64'
93            suffix = ''
94        else:
95            raise RuntimeError('Host OS not supported by this runner')
96
97        self.t32_exec = Path(t32_dir) / 'bin' / os_name / f't32m{self.arch}{suffix}'
98        if not self.t32_exec.exists():
99            raise RuntimeError(f'Cannot find Lauterbach executable at {self.t32_exec}')
100
101        if not self.t32_cfg.is_absolute():
102            self.t32_cfg = Path(t32_dir) / self.t32_cfg
103        if not self.t32_cfg.exists():
104            raise RuntimeError(f'Cannot find Lauterbach configuration at {self.t32_cfg}')
105
106        startup_script = self.startup_dir / f'{command}.cmm'
107        if not startup_script.exists():
108            raise RuntimeError(f'Cannot find start-up script at {startup_script}')
109
110        if command == 'flash':
111            self.flash(**kwargs)
112        elif command == 'debug':
113            self.debug(**kwargs)
114
115    def flash(self, **kwargs) -> None:
116        with TemporaryDirectory(suffix='t32') as tmp_dir:
117            # use a temporary config file, based on the provided configuration,
118            # to hide the TRACE32 software graphical interface
119            cfg_content = f'{self.t32_cfg.read_text()}\n\nSCREEN=OFF\n'
120            tmp_cfg = Path(tmp_dir) / DEFAULT_T32_CONFIG.name
121            tmp_cfg.write_text(cfg_content)
122
123            cmd = self.get_launch_command('flash', cfg=tmp_cfg)
124            self.logger.info(f'Launching TRACE32: {" ".join(cmd)}')
125            try:
126                self.check_call(cmd, timeout=self.timeout)
127                self.logger.info('Finished')
128            except subprocess.TimeoutExpired:
129                self.logger.error(f'Timed out after {self.timeout} seconds')
130
131    def debug(self, **kwargs) -> None:
132        cmd = self.get_launch_command('debug')
133        self.logger.info(f'Launching TRACE32: {" ".join(cmd)}')
134        self.check_call(cmd)
135
136    def get_launch_command(self, command_name: str,
137                           cfg: Path | None = None) -> list[str]:
138        cmd = [
139            str(self.t32_exec),
140            '-c', str(cfg if cfg else self.t32_cfg),
141            '-s', str(self.startup_dir / f'{command_name}.cmm')
142        ]
143        if self.startup_args:
144            cmd.extend(self.startup_args)
145        return cmd
146