1#
2# Copyright (C) 2023-2024 Siemens AG
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6"""Devtool ide-sdk IDE plugin interface definition and helper functions"""
7
8import errno
9import json
10import logging
11import os
12import stat
13from enum import Enum, auto
14from devtool import DevtoolError
15from bb.utils import mkdirhier
16
17logger = logging.getLogger('devtool')
18
19
20class BuildTool(Enum):
21    UNDEFINED = auto()
22    CMAKE = auto()
23    MESON = auto()
24
25    @property
26    def is_c_ccp(self):
27        if self is BuildTool.CMAKE:
28            return True
29        if self is BuildTool.MESON:
30            return True
31        return False
32
33
34class GdbCrossConfig:
35    """Base class defining the GDB configuration generator interface
36
37    Generate a GDB configuration for a binary on the target device.
38    Only one instance per binary is allowed. This allows to assign unique port
39    numbers for all gdbserver instances.
40    """
41    _gdbserver_port_next = 1234
42    _binaries = []
43
44    def __init__(self, image_recipe, modified_recipe, binary, gdbserver_multi=True):
45        self.image_recipe = image_recipe
46        self.modified_recipe = modified_recipe
47        self.gdb_cross = modified_recipe.gdb_cross
48        self.binary = binary
49        if binary in GdbCrossConfig._binaries:
50            raise DevtoolError(
51                "gdbserver config for binary %s is already generated" % binary)
52        GdbCrossConfig._binaries.append(binary)
53        self.script_dir = modified_recipe.ide_sdk_scripts_dir
54        self.gdbinit_dir = os.path.join(self.script_dir, 'gdbinit')
55        self.gdbserver_multi = gdbserver_multi
56        self.binary_pretty = self.binary.replace(os.sep, '-').lstrip('-')
57        self.gdbserver_port = GdbCrossConfig._gdbserver_port_next
58        GdbCrossConfig._gdbserver_port_next += 1
59        self.id_pretty = "%d_%s" % (self.gdbserver_port, self.binary_pretty)
60        # gdbserver start script
61        gdbserver_script_file = 'gdbserver_' + self.id_pretty
62        if self.gdbserver_multi:
63            gdbserver_script_file += "_m"
64        self.gdbserver_script = os.path.join(
65            self.script_dir, gdbserver_script_file)
66        # gdbinit file
67        self.gdbinit = os.path.join(
68            self.gdbinit_dir, 'gdbinit_' + self.id_pretty)
69        # gdb start script
70        self.gdb_script = os.path.join(
71            self.script_dir, 'gdb_' + self.id_pretty)
72
73    def _gen_gdbserver_start_script(self):
74        """Generate a shell command starting the gdbserver on the remote device via ssh
75
76        GDB supports two modes:
77        multi: gdbserver remains running over several debug sessions
78        once: gdbserver terminates after the debugged process terminates
79        """
80        cmd_lines = ['#!/bin/sh']
81        if self.gdbserver_multi:
82            temp_dir = "TEMP_DIR=/tmp/gdbserver_%s; " % self.id_pretty
83            gdbserver_cmd_start = temp_dir
84            gdbserver_cmd_start += "test -f \\$TEMP_DIR/pid && exit 0; "
85            gdbserver_cmd_start += "mkdir -p \\$TEMP_DIR; "
86            gdbserver_cmd_start += "%s --multi :%s > \\$TEMP_DIR/log 2>&1 & " % (
87                self.gdb_cross.gdbserver_path, self.gdbserver_port)
88            gdbserver_cmd_start += "echo \\$! > \\$TEMP_DIR/pid;"
89
90            gdbserver_cmd_stop = temp_dir
91            gdbserver_cmd_stop += "test -f \\$TEMP_DIR/pid && kill \\$(cat \\$TEMP_DIR/pid); "
92            gdbserver_cmd_stop += "rm -rf \\$TEMP_DIR; "
93
94            gdbserver_cmd_l = []
95            gdbserver_cmd_l.append('if [ "$1" = "stop" ]; then')
96            gdbserver_cmd_l.append('  shift')
97            gdbserver_cmd_l.append("  %s %s %s %s 'sh -c \"%s\"'" % (
98                self.gdb_cross.target_device.ssh_sshexec, self.gdb_cross.target_device.ssh_port, self.gdb_cross.target_device.extraoptions, self.gdb_cross.target_device.target, gdbserver_cmd_stop))
99            gdbserver_cmd_l.append('else')
100            gdbserver_cmd_l.append("  %s %s %s %s 'sh -c \"%s\"'" % (
101                self.gdb_cross.target_device.ssh_sshexec, self.gdb_cross.target_device.ssh_port, self.gdb_cross.target_device.extraoptions, self.gdb_cross.target_device.target, gdbserver_cmd_start))
102            gdbserver_cmd_l.append('fi')
103            gdbserver_cmd = os.linesep.join(gdbserver_cmd_l)
104        else:
105            gdbserver_cmd_start = "%s --once :%s %s" % (
106                self.gdb_cross.gdbserver_path, self.gdbserver_port, self.binary)
107            gdbserver_cmd = "%s %s %s %s 'sh -c \"%s\"'" % (
108                self.gdb_cross.target_device.ssh_sshexec, self.gdb_cross.target_device.ssh_port, self.gdb_cross.target_device.extraoptions, self.gdb_cross.target_device.target, gdbserver_cmd_start)
109        cmd_lines.append(gdbserver_cmd)
110        GdbCrossConfig.write_file(self.gdbserver_script, cmd_lines, True)
111
112    def _gen_gdbinit_config(self):
113        """Generate a gdbinit file for this binary and the corresponding gdbserver configuration"""
114        gdbinit_lines = ['# This file is generated by devtool ide-sdk']
115        if self.gdbserver_multi:
116            target_help = '#   gdbserver --multi :%d' % self.gdbserver_port
117            remote_cmd = 'target extended-remote'
118        else:
119            target_help = '#   gdbserver :%d %s' % (
120                self.gdbserver_port, self.binary)
121            remote_cmd = 'target remote'
122        gdbinit_lines.append('# On the remote target:')
123        gdbinit_lines.append(target_help)
124        gdbinit_lines.append('# On the build machine:')
125        gdbinit_lines.append('#   cd ' + self.modified_recipe.real_srctree)
126        gdbinit_lines.append(
127            '#   ' + self.gdb_cross.gdb + ' -ix ' + self.gdbinit)
128
129        gdbinit_lines.append('set sysroot ' + self.modified_recipe.d)
130        gdbinit_lines.append('set substitute-path "/usr/include" "' +
131                             os.path.join(self.modified_recipe.recipe_sysroot, 'usr', 'include') + '"')
132        # Disable debuginfod for now, the IDE configuration uses rootfs-dbg from the image workdir.
133        gdbinit_lines.append('set debuginfod enabled off')
134        if self.image_recipe.rootfs_dbg:
135            gdbinit_lines.append(
136                'set solib-search-path "' + self.modified_recipe.solib_search_path_str(self.image_recipe) + '"')
137            # First: Search for sources of this recipe in the workspace folder
138            if self.modified_recipe.pn in self.modified_recipe.target_dbgsrc_dir:
139                gdbinit_lines.append('set substitute-path "%s" "%s"' %
140                                     (self.modified_recipe.target_dbgsrc_dir, self.modified_recipe.real_srctree))
141            else:
142                logger.error(
143                    "TARGET_DBGSRC_DIR must contain the recipe name PN.")
144            # Second: Search for sources of other recipes in the rootfs-dbg
145            if self.modified_recipe.target_dbgsrc_dir.startswith("/usr/src/debug"):
146                gdbinit_lines.append('set substitute-path "/usr/src/debug" "%s"' % os.path.join(
147                    self.image_recipe.rootfs_dbg, "usr", "src", "debug"))
148            else:
149                logger.error(
150                    "TARGET_DBGSRC_DIR must start with /usr/src/debug.")
151        else:
152            logger.warning(
153                "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.")
154        gdbinit_lines.append(
155            '%s %s:%d' % (remote_cmd, self.gdb_cross.host, self.gdbserver_port))
156        gdbinit_lines.append('set remote exec-file ' + self.binary)
157        gdbinit_lines.append(
158            'run ' + os.path.join(self.modified_recipe.d, self.binary))
159
160        GdbCrossConfig.write_file(self.gdbinit, gdbinit_lines)
161
162    def _gen_gdb_start_script(self):
163        """Generate a script starting GDB with the corresponding gdbinit configuration."""
164        cmd_lines = ['#!/bin/sh']
165        cmd_lines.append('cd ' + self.modified_recipe.real_srctree)
166        cmd_lines.append(self.gdb_cross.gdb + ' -ix ' +
167                         self.gdbinit + ' "$@"')
168        GdbCrossConfig.write_file(self.gdb_script, cmd_lines, True)
169
170    def initialize(self):
171        self._gen_gdbserver_start_script()
172        self._gen_gdbinit_config()
173        self._gen_gdb_start_script()
174
175    @staticmethod
176    def write_file(script_file, cmd_lines, executable=False):
177        script_dir = os.path.dirname(script_file)
178        mkdirhier(script_dir)
179        with open(script_file, 'w') as script_f:
180            script_f.write(os.linesep.join(cmd_lines))
181            script_f.write(os.linesep)
182        if executable:
183            st = os.stat(script_file)
184            os.chmod(script_file, st.st_mode | stat.S_IEXEC)
185        logger.info("Created: %s" % script_file)
186
187
188class IdeBase:
189    """Base class defining the interface for IDE plugins"""
190
191    def __init__(self):
192        self.ide_name = 'undefined'
193        self.gdb_cross_configs = []
194
195    @classmethod
196    def ide_plugin_priority(cls):
197        """Used to find the default ide handler if --ide is not passed"""
198        return 10
199
200    def setup_shared_sysroots(self, shared_env):
201        logger.warn("Shared sysroot mode is not supported for IDE %s" %
202                    self.ide_name)
203
204    def setup_modified_recipe(self, args, image_recipe, modified_recipe):
205        logger.warn("Modified recipe mode is not supported for IDE %s" %
206                    self.ide_name)
207
208    def initialize_gdb_cross_configs(self, image_recipe, modified_recipe, gdb_cross_config_class=GdbCrossConfig):
209        binaries = modified_recipe.find_installed_binaries()
210        for binary in binaries:
211            gdb_cross_config = gdb_cross_config_class(
212                image_recipe, modified_recipe, binary)
213            gdb_cross_config.initialize()
214            self.gdb_cross_configs.append(gdb_cross_config)
215
216    @staticmethod
217    def gen_oe_scrtips_sym_link(modified_recipe):
218        # create a sym-link from sources to the scripts directory
219        if os.path.isdir(modified_recipe.ide_sdk_scripts_dir):
220            IdeBase.symlink_force(modified_recipe.ide_sdk_scripts_dir,
221                                  os.path.join(modified_recipe.real_srctree, 'oe-scripts'))
222
223    @staticmethod
224    def update_json_file(json_dir, json_file, update_dict):
225        """Update a json file
226
227        By default it uses the dict.update function. If this is not sutiable
228        the update function might be passed via update_func parameter.
229        """
230        json_path = os.path.join(json_dir, json_file)
231        logger.info("Updating IDE config file: %s (%s)" %
232                    (json_file, json_path))
233        if not os.path.exists(json_dir):
234            os.makedirs(json_dir)
235        try:
236            with open(json_path) as f:
237                orig_dict = json.load(f)
238        except json.decoder.JSONDecodeError:
239            logger.info(
240                "Decoding %s failed. Probably because of comments in the json file" % json_path)
241            orig_dict = {}
242        except FileNotFoundError:
243            orig_dict = {}
244        orig_dict.update(update_dict)
245        with open(json_path, 'w') as f:
246            json.dump(orig_dict, f, indent=4)
247
248    @staticmethod
249    def symlink_force(tgt, dst):
250        try:
251            os.symlink(tgt, dst)
252        except OSError as err:
253            if err.errno == errno.EEXIST:
254                if os.readlink(dst) != tgt:
255                    os.remove(dst)
256                    os.symlink(tgt, dst)
257            else:
258                raise err
259
260
261def get_devtool_deploy_opts(args):
262    """Filter args for devtool deploy-target args"""
263    if not args.target:
264        return None
265    devtool_deploy_opts = [args.target]
266    if args.no_host_check:
267        devtool_deploy_opts += ["-c"]
268    if args.show_status:
269        devtool_deploy_opts += ["-s"]
270    if args.no_preserve:
271        devtool_deploy_opts += ["-p"]
272    if args.no_check_space:
273        devtool_deploy_opts += ["--no-check-space"]
274    if args.ssh_exec:
275        devtool_deploy_opts += ["-e", args.ssh.exec]
276    if args.port:
277        devtool_deploy_opts += ["-P", args.port]
278    if args.key:
279        devtool_deploy_opts += ["-I", args.key]
280    if args.strip is False:
281        devtool_deploy_opts += ["--no-strip"]
282    return devtool_deploy_opts
283