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