# # Copyright (C) 2023-2024 Siemens AG # # SPDX-License-Identifier: GPL-2.0-only # """Devtool ide-sdk IDE plugin for VSCode and VSCodium""" import json import logging import os import shutil from devtool.ide_plugins import BuildTool, IdeBase, GdbCrossConfig, get_devtool_deploy_opts logger = logging.getLogger('devtool') class GdbCrossConfigVSCode(GdbCrossConfig): def __init__(self, image_recipe, modified_recipe, binary): super().__init__(image_recipe, modified_recipe, binary, False) def initialize(self): self._gen_gdbserver_start_script() class IdeVSCode(IdeBase): """Manage IDE configurations for VSCode Modified recipe mode: - cmake: use the cmake-preset generated by devtool ide-sdk - meson: meson is called via a wrapper script generated by devtool ide-sdk Shared sysroot mode: In shared sysroot mode, the cross tool-chain is exported to the user's global configuration. A workspace cannot be created because there is no recipe that defines how a workspace could be set up. - cmake: adds a cmake-kit to .local/share/CMakeTools/cmake-tools-kits.json The cmake-kit uses the environment script and the tool-chain file generated by meta-ide-support. - meson: Meson needs manual workspace configuration. """ @classmethod def ide_plugin_priority(cls): """If --ide is not passed this is the default plugin""" if shutil.which('code'): return 100 return 0 def setup_shared_sysroots(self, shared_env): """Expose the toolchain of the shared sysroots SDK""" datadir = shared_env.ide_support.datadir deploy_dir_image = shared_env.ide_support.deploy_dir_image real_multimach_target_sys = shared_env.ide_support.real_multimach_target_sys standalone_sysroot_native = shared_env.build_sysroots.standalone_sysroot_native vscode_ws_path = os.path.join( os.environ['HOME'], '.local', 'share', 'CMakeTools') cmake_kits_path = os.path.join(vscode_ws_path, 'cmake-tools-kits.json') oecmake_generator = "Ninja" env_script = os.path.join( deploy_dir_image, 'environment-setup-' + real_multimach_target_sys) if not os.path.isdir(vscode_ws_path): os.makedirs(vscode_ws_path) cmake_kits_old = [] if os.path.exists(cmake_kits_path): with open(cmake_kits_path, 'r', encoding='utf-8') as cmake_kits_file: cmake_kits_old = json.load(cmake_kits_file) cmake_kits = cmake_kits_old.copy() cmake_kit_new = { "name": "OE " + real_multimach_target_sys, "environmentSetupScript": env_script, "toolchainFile": standalone_sysroot_native + datadir + "/cmake/OEToolchainConfig.cmake", "preferredGenerator": { "name": oecmake_generator } } def merge_kit(cmake_kits, cmake_kit_new): i = 0 while i < len(cmake_kits): if 'environmentSetupScript' in cmake_kits[i] and \ cmake_kits[i]['environmentSetupScript'] == cmake_kit_new['environmentSetupScript']: cmake_kits[i] = cmake_kit_new return i += 1 cmake_kits.append(cmake_kit_new) merge_kit(cmake_kits, cmake_kit_new) if cmake_kits != cmake_kits_old: logger.info("Updating: %s" % cmake_kits_path) with open(cmake_kits_path, 'w', encoding='utf-8') as cmake_kits_file: json.dump(cmake_kits, cmake_kits_file, indent=4) else: logger.info("Already up to date: %s" % cmake_kits_path) cmake_native = os.path.join( shared_env.build_sysroots.standalone_sysroot_native, 'usr', 'bin', 'cmake') if os.path.isfile(cmake_native): 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) else: logger.error("Cannot find cmake native at: %s" % cmake_native) def dot_code_dir(self, modified_recipe): return os.path.join(modified_recipe.srctree, '.vscode') def __vscode_settings_meson(self, settings_dict, modified_recipe): if modified_recipe.build_tool is not BuildTool.MESON: return settings_dict["mesonbuild.mesonPath"] = modified_recipe.meson_wrapper confopts = modified_recipe.mesonopts.split() confopts += modified_recipe.meson_cross_file.split() confopts += modified_recipe.extra_oemeson.split() settings_dict["mesonbuild.configureOptions"] = confopts settings_dict["mesonbuild.buildFolder"] = modified_recipe.b def __vscode_settings_cmake(self, settings_dict, modified_recipe): """Add cmake specific settings to settings.json. Note: most settings are passed to the cmake preset. """ if modified_recipe.build_tool is not BuildTool.CMAKE: return settings_dict["cmake.configureOnOpen"] = True settings_dict["cmake.sourceDirectory"] = modified_recipe.real_srctree def vscode_settings(self, modified_recipe, image_recipe): files_excludes = { "**/.git/**": True, "**/oe-logs/**": True, "**/oe-workdir/**": True, "**/source-date-epoch/**": True } python_exclude = [ "**/.git/**", "**/oe-logs/**", "**/oe-workdir/**", "**/source-date-epoch/**" ] files_readonly = { modified_recipe.recipe_sysroot + '/**': True, modified_recipe.recipe_sysroot_native + '/**': True, } if image_recipe.rootfs_dbg is not None: files_readonly[image_recipe.rootfs_dbg + '/**'] = True settings_dict = { "files.watcherExclude": files_excludes, "files.exclude": files_excludes, "files.readonlyInclude": files_readonly, "python.analysis.exclude": python_exclude } self.__vscode_settings_cmake(settings_dict, modified_recipe) self.__vscode_settings_meson(settings_dict, modified_recipe) settings_file = 'settings.json' IdeBase.update_json_file( self.dot_code_dir(modified_recipe), settings_file, settings_dict) def __vscode_extensions_cmake(self, modified_recipe, recommendations): if modified_recipe.build_tool is not BuildTool.CMAKE: return recommendations += [ "twxs.cmake", "ms-vscode.cmake-tools", "ms-vscode.cpptools", "ms-vscode.cpptools-extension-pack", "ms-vscode.cpptools-themes" ] def __vscode_extensions_meson(self, modified_recipe, recommendations): if modified_recipe.build_tool is not BuildTool.MESON: return recommendations += [ 'mesonbuild.mesonbuild', "ms-vscode.cpptools", "ms-vscode.cpptools-extension-pack", "ms-vscode.cpptools-themes" ] def vscode_extensions(self, modified_recipe): recommendations = [] self.__vscode_extensions_cmake(modified_recipe, recommendations) self.__vscode_extensions_meson(modified_recipe, recommendations) extensions_file = 'extensions.json' IdeBase.update_json_file( self.dot_code_dir(modified_recipe), extensions_file, {"recommendations": recommendations}) def vscode_c_cpp_properties(self, modified_recipe): properties_dict = { "name": modified_recipe.recipe_id_pretty, } if modified_recipe.build_tool is BuildTool.CMAKE: properties_dict["configurationProvider"] = "ms-vscode.cmake-tools" elif modified_recipe.build_tool is BuildTool.MESON: properties_dict["configurationProvider"] = "mesonbuild.mesonbuild" properties_dict["compilerPath"] = os.path.join(modified_recipe.staging_bindir_toolchain, modified_recipe.cxx.split()[0]) else: # no C/C++ build return properties_dicts = { "configurations": [ properties_dict ], "version": 4 } prop_file = 'c_cpp_properties.json' IdeBase.update_json_file( self.dot_code_dir(modified_recipe), prop_file, properties_dicts) def vscode_launch_bin_dbg(self, gdb_cross_config): modified_recipe = gdb_cross_config.modified_recipe launch_config = { "name": gdb_cross_config.id_pretty, "type": "cppdbg", "request": "launch", "program": os.path.join(modified_recipe.d, gdb_cross_config.binary.lstrip('/')), "stopAtEntry": True, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": False, "MIMode": "gdb", "preLaunchTask": gdb_cross_config.id_pretty, "miDebuggerPath": modified_recipe.gdb_cross.gdb, "miDebuggerServerAddress": "%s:%d" % (modified_recipe.gdb_cross.host, gdb_cross_config.gdbserver_port) } # Search for header files in recipe-sysroot. src_file_map = { "/usr/include": os.path.join(modified_recipe.recipe_sysroot, "usr", "include") } # First of all search for not stripped binaries in the image folder. # These binaries are copied (and optionally stripped) by deploy-target setup_commands = [ { "description": "sysroot", "text": "set sysroot " + modified_recipe.d } ] if gdb_cross_config.image_recipe.rootfs_dbg: launch_config['additionalSOLibSearchPath'] = modified_recipe.solib_search_path_str( gdb_cross_config.image_recipe) # First: Search for sources of this recipe in the workspace folder if modified_recipe.pn in modified_recipe.target_dbgsrc_dir: src_file_map[modified_recipe.target_dbgsrc_dir] = "${workspaceFolder}" else: logger.error( "TARGET_DBGSRC_DIR must contain the recipe name PN.") # Second: Search for sources of other recipes in the rootfs-dbg if modified_recipe.target_dbgsrc_dir.startswith("/usr/src/debug"): src_file_map["/usr/src/debug"] = os.path.join( gdb_cross_config.image_recipe.rootfs_dbg, "usr", "src", "debug") else: logger.error( "TARGET_DBGSRC_DIR must start with /usr/src/debug.") else: logger.warning( "Cannot setup debug symbols configuration for GDB. IMAGE_GEN_DEBUGFS is not enabled.") launch_config['sourceFileMap'] = src_file_map launch_config['setupCommands'] = setup_commands return launch_config def vscode_launch(self, modified_recipe): """GDB Launch configuration for binaries (elf files)""" configurations = [] for gdb_cross_config in self.gdb_cross_configs: if gdb_cross_config.modified_recipe is modified_recipe: configurations.append(self.vscode_launch_bin_dbg(gdb_cross_config)) launch_dict = { "version": "0.2.0", "configurations": configurations } launch_file = 'launch.json' IdeBase.update_json_file( self.dot_code_dir(modified_recipe), launch_file, launch_dict) def vscode_tasks_cpp(self, args, modified_recipe): run_install_deploy = modified_recipe.gen_install_deploy_script(args) install_task_name = "install && deploy-target %s" % modified_recipe.recipe_id_pretty tasks_dict = { "version": "2.0.0", "tasks": [ { "label": install_task_name, "type": "shell", "command": run_install_deploy, "problemMatcher": [] } ] } for gdb_cross_config in self.gdb_cross_configs: if gdb_cross_config.modified_recipe is not modified_recipe: continue tasks_dict['tasks'].append( { "label": gdb_cross_config.id_pretty, "type": "shell", "isBackground": True, "dependsOn": [ install_task_name ], "command": gdb_cross_config.gdbserver_script, "problemMatcher": [ { "pattern": [ { "regexp": ".", "file": 1, "location": 2, "message": 3 } ], "background": { "activeOnStart": True, "beginsPattern": ".", "endsPattern": ".", } } ] }) tasks_file = 'tasks.json' IdeBase.update_json_file( self.dot_code_dir(modified_recipe), tasks_file, tasks_dict) def vscode_tasks_fallback(self, args, modified_recipe): oe_init_dir = modified_recipe.oe_init_dir oe_init = ". %s %s > /dev/null && " % (modified_recipe.oe_init_build_env, modified_recipe.topdir) dt_build = "devtool build " dt_build_label = dt_build + modified_recipe.recipe_id_pretty dt_build_cmd = dt_build + modified_recipe.bpn clean_opt = " --clean" dt_build_clean_label = dt_build + modified_recipe.recipe_id_pretty + clean_opt dt_build_clean_cmd = dt_build + modified_recipe.bpn + clean_opt dt_deploy = "devtool deploy-target " dt_deploy_label = dt_deploy + modified_recipe.recipe_id_pretty dt_deploy_cmd = dt_deploy + modified_recipe.bpn dt_build_deploy_label = "devtool build & deploy-target %s" % modified_recipe.recipe_id_pretty deploy_opts = ' '.join(get_devtool_deploy_opts(args)) tasks_dict = { "version": "2.0.0", "tasks": [ { "label": dt_build_label, "type": "shell", "command": "bash", "linux": { "options": { "cwd": oe_init_dir } }, "args": [ "--login", "-c", "%s%s" % (oe_init, dt_build_cmd) ], "problemMatcher": [] }, { "label": dt_deploy_label, "type": "shell", "command": "bash", "linux": { "options": { "cwd": oe_init_dir } }, "args": [ "--login", "-c", "%s%s %s" % ( oe_init, dt_deploy_cmd, deploy_opts) ], "problemMatcher": [] }, { "label": dt_build_deploy_label, "dependsOrder": "sequence", "dependsOn": [ dt_build_label, dt_deploy_label ], "problemMatcher": [], "group": { "kind": "build", "isDefault": True } }, { "label": dt_build_clean_label, "type": "shell", "command": "bash", "linux": { "options": { "cwd": oe_init_dir } }, "args": [ "--login", "-c", "%s%s" % (oe_init, dt_build_clean_cmd) ], "problemMatcher": [] } ] } if modified_recipe.gdb_cross: for gdb_cross_config in self.gdb_cross_configs: if gdb_cross_config.modified_recipe is not modified_recipe: continue tasks_dict['tasks'].append( { "label": gdb_cross_config.id_pretty, "type": "shell", "isBackground": True, "dependsOn": [ dt_build_deploy_label ], "command": gdb_cross_config.gdbserver_script, "problemMatcher": [ { "pattern": [ { "regexp": ".", "file": 1, "location": 2, "message": 3 } ], "background": { "activeOnStart": True, "beginsPattern": ".", "endsPattern": ".", } } ] }) tasks_file = 'tasks.json' IdeBase.update_json_file( self.dot_code_dir(modified_recipe), tasks_file, tasks_dict) def vscode_tasks(self, args, modified_recipe): if modified_recipe.build_tool.is_c_ccp: self.vscode_tasks_cpp(args, modified_recipe) else: self.vscode_tasks_fallback(args, modified_recipe) def setup_modified_recipe(self, args, image_recipe, modified_recipe): self.vscode_settings(modified_recipe, image_recipe) self.vscode_extensions(modified_recipe) self.vscode_c_cpp_properties(modified_recipe) if args.target: self.initialize_gdb_cross_configs( image_recipe, modified_recipe, gdb_cross_config_class=GdbCrossConfigVSCode) self.vscode_launch(modified_recipe) self.vscode_tasks(args, modified_recipe) def register_ide_plugin(ide_plugins): ide_plugins['code'] = IdeVSCode