1# 2# Copyright (C) 2023-2024 Siemens AG 3# 4# SPDX-License-Identifier: GPL-2.0-only 5# 6"""Devtool ide-sdk IDE plugin for VSCode and VSCodium""" 7 8import json 9import logging 10import os 11import shutil 12from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, get_devtool_deploy_opts 13 14logger = logging.getLogger('devtool') 15 16 17class GdbCrossConfigVSCode(GdbCrossConfig): 18 def __init__(self, image_recipe, modified_recipe, binary): 19 super().__init__(image_recipe, modified_recipe, binary, False) 20 21 def initialize(self): 22 self._gen_gdbserver_start_script() 23 24 25class IdeVSCode(IdeBase): 26 """Manage IDE configurations for VSCode 27 28 Modified recipe mode: 29 - cmake: use the cmake-preset generated by devtool ide-sdk 30 - meson: meson is called via a wrapper script generated by devtool ide-sdk 31 32 Shared sysroot mode: 33 In shared sysroot mode, the cross tool-chain is exported to the user's global configuration. 34 A workspace cannot be created because there is no recipe that defines how a workspace could 35 be set up. 36 - cmake: adds a cmake-kit to .local/share/CMakeTools/cmake-tools-kits.json 37 The cmake-kit uses the environment script and the tool-chain file 38 generated by meta-ide-support. 39 - meson: Meson needs manual workspace configuration. 40 """ 41 42 @classmethod 43 def ide_plugin_priority(cls): 44 """If --ide is not passed this is the default plugin""" 45 if shutil.which('code'): 46 return 100 47 return 0 48 49 def setup_shared_sysroots(self, shared_env): 50 """Expose the toolchain of the shared sysroots SDK""" 51 datadir = shared_env.ide_support.datadir 52 deploy_dir_image = shared_env.ide_support.deploy_dir_image 53 real_multimach_target_sys = shared_env.ide_support.real_multimach_target_sys 54 standalone_sysroot_native = shared_env.build_sysroots.standalone_sysroot_native 55 vscode_ws_path = os.path.join( 56 os.environ['HOME'], '.local', 'share', 'CMakeTools') 57 cmake_kits_path = os.path.join(vscode_ws_path, 'cmake-tools-kits.json') 58 oecmake_generator = "Ninja" 59 env_script = os.path.join( 60 deploy_dir_image, 'environment-setup-' + real_multimach_target_sys) 61 62 if not os.path.isdir(vscode_ws_path): 63 os.makedirs(vscode_ws_path) 64 cmake_kits_old = [] 65 if os.path.exists(cmake_kits_path): 66 with open(cmake_kits_path, 'r', encoding='utf-8') as cmake_kits_file: 67 cmake_kits_old = json.load(cmake_kits_file) 68 cmake_kits = cmake_kits_old.copy() 69 70 cmake_kit_new = { 71 "name": "OE " + real_multimach_target_sys, 72 "environmentSetupScript": env_script, 73 "toolchainFile": standalone_sysroot_native + datadir + "/cmake/OEToolchainConfig.cmake", 74 "preferredGenerator": { 75 "name": oecmake_generator 76 } 77 } 78 79 def merge_kit(cmake_kits, cmake_kit_new): 80 i = 0 81 while i < len(cmake_kits): 82 if 'environmentSetupScript' in cmake_kits[i] and \ 83 cmake_kits[i]['environmentSetupScript'] == cmake_kit_new['environmentSetupScript']: 84 cmake_kits[i] = cmake_kit_new 85 return 86 i += 1 87 cmake_kits.append(cmake_kit_new) 88 merge_kit(cmake_kits, cmake_kit_new) 89 90 if cmake_kits != cmake_kits_old: 91 logger.info("Updating: %s" % cmake_kits_path) 92 with open(cmake_kits_path, 'w', encoding='utf-8') as cmake_kits_file: 93 json.dump(cmake_kits, cmake_kits_file, indent=4) 94 else: 95 logger.info("Already up to date: %s" % cmake_kits_path) 96 97 cmake_native = os.path.join( 98 shared_env.build_sysroots.standalone_sysroot_native, 'usr', 'bin', 'cmake') 99 if os.path.isfile(cmake_native): 100 logger.info('cmake-kits call cmake by default. If the cmake provided by this SDK should be used, please add the following line to ".vscode/settings.json" file: "cmake.cmakePath": "%s"' % cmake_native) 101 else: 102 logger.error("Cannot find cmake native at: %s" % cmake_native) 103 104 def dot_code_dir(self, modified_recipe): 105 return os.path.join(modified_recipe.srctree, '.vscode') 106 107 def __vscode_settings_meson(self, settings_dict, modified_recipe): 108 if modified_recipe.build_tool is not BuildTool.MESON: 109 return 110 settings_dict["mesonbuild.mesonPath"] = modified_recipe.meson_wrapper 111 112 confopts = modified_recipe.mesonopts.split() 113 confopts += modified_recipe.meson_cross_file.split() 114 confopts += modified_recipe.extra_oemeson.split() 115 settings_dict["mesonbuild.configureOptions"] = confopts 116 settings_dict["mesonbuild.buildFolder"] = modified_recipe.b 117 118 def __vscode_settings_cmake(self, settings_dict, modified_recipe): 119 """Add cmake specific settings to settings.json. 120 121 Note: most settings are passed to the cmake preset. 122 """ 123 if modified_recipe.build_tool is not BuildTool.CMAKE: 124 return 125 settings_dict["cmake.configureOnOpen"] = True 126 settings_dict["cmake.sourceDirectory"] = modified_recipe.real_srctree 127 128 def vscode_settings(self, modified_recipe, image_recipe): 129 files_excludes = { 130 "**/.git/**": True, 131 "**/oe-logs/**": True, 132 "**/oe-workdir/**": True, 133 "**/source-date-epoch/**": True 134 } 135 python_exclude = [ 136 "**/.git/**", 137 "**/oe-logs/**", 138 "**/oe-workdir/**", 139 "**/source-date-epoch/**" 140 ] 141 files_readonly = { 142 modified_recipe.recipe_sysroot + '/**': True, 143 modified_recipe.recipe_sysroot_native + '/**': True, 144 } 145 if image_recipe.rootfs_dbg is not None: 146 files_readonly[image_recipe.rootfs_dbg + '/**'] = True 147 settings_dict = { 148 "files.watcherExclude": files_excludes, 149 "files.exclude": files_excludes, 150 "files.readonlyInclude": files_readonly, 151 "python.analysis.exclude": python_exclude 152 } 153 self.__vscode_settings_cmake(settings_dict, modified_recipe) 154 self.__vscode_settings_meson(settings_dict, modified_recipe) 155 156 settings_file = 'settings.json' 157 IdeBase.update_json_file( 158 self.dot_code_dir(modified_recipe), settings_file, settings_dict) 159 160 def __vscode_extensions_cmake(self, modified_recipe, recommendations): 161 if modified_recipe.build_tool is not BuildTool.CMAKE: 162 return 163 recommendations += [ 164 "twxs.cmake", 165 "ms-vscode.cmake-tools", 166 "ms-vscode.cpptools", 167 "ms-vscode.cpptools-extension-pack", 168 "ms-vscode.cpptools-themes" 169 ] 170 171 def __vscode_extensions_meson(self, modified_recipe, recommendations): 172 if modified_recipe.build_tool is not BuildTool.MESON: 173 return 174 recommendations += [ 175 'mesonbuild.mesonbuild', 176 "ms-vscode.cpptools", 177 "ms-vscode.cpptools-extension-pack", 178 "ms-vscode.cpptools-themes" 179 ] 180 181 def vscode_extensions(self, modified_recipe): 182 recommendations = [] 183 self.__vscode_extensions_cmake(modified_recipe, recommendations) 184 self.__vscode_extensions_meson(modified_recipe, recommendations) 185 extensions_file = 'extensions.json' 186 IdeBase.update_json_file( 187 self.dot_code_dir(modified_recipe), extensions_file, {"recommendations": recommendations}) 188 189 def vscode_c_cpp_properties(self, modified_recipe): 190 properties_dict = { 191 "name": modified_recipe.recipe_id_pretty, 192 } 193 if modified_recipe.build_tool is BuildTool.CMAKE: 194 properties_dict["configurationProvider"] = "ms-vscode.cmake-tools" 195 elif modified_recipe.build_tool is BuildTool.MESON: 196 properties_dict["configurationProvider"] = "mesonbuild.mesonbuild" 197 properties_dict["compilerPath"] = os.path.join(modified_recipe.staging_bindir_toolchain, modified_recipe.cxx.split()[0]) 198 else: # no C/C++ build 199 return 200 201 properties_dicts = { 202 "configurations": [ 203 properties_dict 204 ], 205 "version": 4 206 } 207 prop_file = 'c_cpp_properties.json' 208 IdeBase.update_json_file( 209 self.dot_code_dir(modified_recipe), prop_file, properties_dicts) 210 211 def vscode_launch_bin_dbg(self, gdb_cross_config): 212 modified_recipe = gdb_cross_config.modified_recipe 213 214 launch_config = { 215 "name": gdb_cross_config.id_pretty, 216 "type": "cppdbg", 217 "request": "launch", 218 "program": os.path.join(modified_recipe.d, gdb_cross_config.binary.lstrip('/')), 219 "stopAtEntry": True, 220 "cwd": "${workspaceFolder}", 221 "environment": [], 222 "externalConsole": False, 223 "MIMode": "gdb", 224 "preLaunchTask": gdb_cross_config.id_pretty, 225 "miDebuggerPath": modified_recipe.gdb_cross.gdb, 226 "miDebuggerServerAddress": "%s:%d" % (modified_recipe.gdb_cross.host, gdb_cross_config.gdbserver_port) 227 } 228 229 # Search for header files in recipe-sysroot. 230 src_file_map = { 231 "/usr/include": os.path.join(modified_recipe.recipe_sysroot, "usr", "include") 232 } 233 # First of all search for not stripped binaries in the image folder. 234 # These binaries are copied (and optionally stripped) by deploy-target 235 setup_commands = [ 236 { 237 "description": "sysroot", 238 "text": "set sysroot " + modified_recipe.d 239 } 240 ] 241 242 if gdb_cross_config.image_recipe.rootfs_dbg: 243 launch_config['additionalSOLibSearchPath'] = modified_recipe.solib_search_path_str( 244 gdb_cross_config.image_recipe) 245 # First: Search for sources of this recipe in the workspace folder 246 if modified_recipe.pn in modified_recipe.target_dbgsrc_dir: 247 src_file_map[modified_recipe.target_dbgsrc_dir] = "${workspaceFolder}" 248 else: 249 logger.error( 250 "TARGET_DBGSRC_DIR must contain the recipe name PN.") 251 # Second: Search for sources of other recipes in the rootfs-dbg 252 if modified_recipe.target_dbgsrc_dir.startswith("/usr/src/debug"): 253 src_file_map["/usr/src/debug"] = os.path.join( 254 gdb_cross_config.image_recipe.rootfs_dbg, "usr", "src", "debug") 255 else: 256 logger.error( 257 "TARGET_DBGSRC_DIR must start with /usr/src/debug.") 258 else: 259 logger.warning( 260 "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.") 261 262 launch_config['sourceFileMap'] = src_file_map 263 launch_config['setupCommands'] = setup_commands 264 return launch_config 265 266 def vscode_launch(self, modified_recipe): 267 """GDB Launch configuration for binaries (elf files)""" 268 269 configurations = [] 270 for gdb_cross_config in self.gdb_cross_configs: 271 if gdb_cross_config.modified_recipe is modified_recipe: 272 configurations.append(self.vscode_launch_bin_dbg(gdb_cross_config)) 273 launch_dict = { 274 "version": "0.2.0", 275 "configurations": configurations 276 } 277 launch_file = 'launch.json' 278 IdeBase.update_json_file( 279 self.dot_code_dir(modified_recipe), launch_file, launch_dict) 280 281 def vscode_tasks_cpp(self, args, modified_recipe): 282 run_install_deploy = modified_recipe.gen_install_deploy_script(args) 283 install_task_name = "install && deploy-target %s" % modified_recipe.recipe_id_pretty 284 tasks_dict = { 285 "version": "2.0.0", 286 "tasks": [ 287 { 288 "label": install_task_name, 289 "type": "shell", 290 "command": run_install_deploy, 291 "problemMatcher": [] 292 } 293 ] 294 } 295 for gdb_cross_config in self.gdb_cross_configs: 296 if gdb_cross_config.modified_recipe is not modified_recipe: 297 continue 298 tasks_dict['tasks'].append( 299 { 300 "label": gdb_cross_config.id_pretty, 301 "type": "shell", 302 "isBackground": True, 303 "dependsOn": [ 304 install_task_name 305 ], 306 "command": gdb_cross_config.gdbserver_script, 307 "problemMatcher": [ 308 { 309 "pattern": [ 310 { 311 "regexp": ".", 312 "file": 1, 313 "location": 2, 314 "message": 3 315 } 316 ], 317 "background": { 318 "activeOnStart": True, 319 "beginsPattern": ".", 320 "endsPattern": ".", 321 } 322 } 323 ] 324 }) 325 tasks_file = 'tasks.json' 326 IdeBase.update_json_file( 327 self.dot_code_dir(modified_recipe), tasks_file, tasks_dict) 328 329 def vscode_tasks_fallback(self, args, modified_recipe): 330 oe_init_dir = modified_recipe.oe_init_dir 331 oe_init = ". %s %s > /dev/null && " % (modified_recipe.oe_init_build_env, modified_recipe.topdir) 332 dt_build = "devtool build " 333 dt_build_label = dt_build + modified_recipe.recipe_id_pretty 334 dt_build_cmd = dt_build + modified_recipe.bpn 335 clean_opt = " --clean" 336 dt_build_clean_label = dt_build + modified_recipe.recipe_id_pretty + clean_opt 337 dt_build_clean_cmd = dt_build + modified_recipe.bpn + clean_opt 338 dt_deploy = "devtool deploy-target " 339 dt_deploy_label = dt_deploy + modified_recipe.recipe_id_pretty 340 dt_deploy_cmd = dt_deploy + modified_recipe.bpn 341 dt_build_deploy_label = "devtool build & deploy-target %s" % modified_recipe.recipe_id_pretty 342 deploy_opts = ' '.join(get_devtool_deploy_opts(args)) 343 tasks_dict = { 344 "version": "2.0.0", 345 "tasks": [ 346 { 347 "label": dt_build_label, 348 "type": "shell", 349 "command": "bash", 350 "linux": { 351 "options": { 352 "cwd": oe_init_dir 353 } 354 }, 355 "args": [ 356 "--login", 357 "-c", 358 "%s%s" % (oe_init, dt_build_cmd) 359 ], 360 "problemMatcher": [] 361 }, 362 { 363 "label": dt_deploy_label, 364 "type": "shell", 365 "command": "bash", 366 "linux": { 367 "options": { 368 "cwd": oe_init_dir 369 } 370 }, 371 "args": [ 372 "--login", 373 "-c", 374 "%s%s %s" % ( 375 oe_init, dt_deploy_cmd, deploy_opts) 376 ], 377 "problemMatcher": [] 378 }, 379 { 380 "label": dt_build_deploy_label, 381 "dependsOrder": "sequence", 382 "dependsOn": [ 383 dt_build_label, 384 dt_deploy_label 385 ], 386 "problemMatcher": [], 387 "group": { 388 "kind": "build", 389 "isDefault": True 390 } 391 }, 392 { 393 "label": dt_build_clean_label, 394 "type": "shell", 395 "command": "bash", 396 "linux": { 397 "options": { 398 "cwd": oe_init_dir 399 } 400 }, 401 "args": [ 402 "--login", 403 "-c", 404 "%s%s" % (oe_init, dt_build_clean_cmd) 405 ], 406 "problemMatcher": [] 407 } 408 ] 409 } 410 if modified_recipe.gdb_cross: 411 for gdb_cross_config in self.gdb_cross_configs: 412 if gdb_cross_config.modified_recipe is not modified_recipe: 413 continue 414 tasks_dict['tasks'].append( 415 { 416 "label": gdb_cross_config.id_pretty, 417 "type": "shell", 418 "isBackground": True, 419 "dependsOn": [ 420 dt_build_deploy_label 421 ], 422 "command": gdb_cross_config.gdbserver_script, 423 "problemMatcher": [ 424 { 425 "pattern": [ 426 { 427 "regexp": ".", 428 "file": 1, 429 "location": 2, 430 "message": 3 431 } 432 ], 433 "background": { 434 "activeOnStart": True, 435 "beginsPattern": ".", 436 "endsPattern": ".", 437 } 438 } 439 ] 440 }) 441 tasks_file = 'tasks.json' 442 IdeBase.update_json_file( 443 self.dot_code_dir(modified_recipe), tasks_file, tasks_dict) 444 445 def vscode_tasks(self, args, modified_recipe): 446 if modified_recipe.build_tool.is_c_ccp: 447 self.vscode_tasks_cpp(args, modified_recipe) 448 else: 449 self.vscode_tasks_fallback(args, modified_recipe) 450 451 def setup_modified_recipe(self, args, image_recipe, modified_recipe): 452 self.vscode_settings(modified_recipe, image_recipe) 453 self.vscode_extensions(modified_recipe) 454 self.vscode_c_cpp_properties(modified_recipe) 455 if args.target: 456 self.initialize_gdb_cross_configs( 457 image_recipe, modified_recipe, gdb_cross_config_class=GdbCrossConfigVSCode) 458 self.vscode_launch(modified_recipe) 459 self.vscode_tasks(args, modified_recipe) 460 461 462def register_ide_plugin(ide_plugins): 463 ide_plugins['code'] = IdeVSCode 464