| # |
| # 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" |
| 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) |
| src_file_map["/usr/src/debug"] = os.path.join( |
| gdb_cross_config.image_recipe.rootfs_dbg, "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 = [self.vscode_launch_bin_dbg( |
| gdb_cross_config) for gdb_cross_config in self.gdb_cross_configs] |
| 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: |
| 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: |
| 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 |