1# 2# File : vsc.py 3# This file is part of RT-Thread RTOS 4# COPYRIGHT (C) 2006 - 2018, RT-Thread Development Team 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License along 17# with this program; if not, write to the Free Software Foundation, Inc., 18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19# 20# Change Logs: 21# Date Author Notes 22# 2018-05-30 Bernard The first version 23# 2023-03-03 Supperthomas Add the vscode workspace config file 24# 2024-12-13 Supperthomas covert compile_commands.json to vscode workspace file 25# 2025-07-05 Bernard Add support for generating .vscode/c_cpp_properties.json 26# and .vscode/settings.json files 27""" 28Utils for VSCode 29""" 30 31import os 32import json 33import utils 34import rtconfig 35from SCons.Script import GetLaunchDir 36 37from utils import _make_path_relative 38def find_first_node_with_two_children(tree): 39 for key, subtree in tree.items(): 40 if len(subtree) >= 2: 41 return key, subtree 42 result = find_first_node_with_two_children(subtree) 43 if result: 44 return result 45 return None, None 46 47 48def filt_tree(tree): 49 key, subtree = find_first_node_with_two_children(tree) 50 if key: 51 return {key: subtree} 52 return {} 53 54 55def add_path_to_tree(tree, path): 56 parts = path.split(os.sep) 57 current_level = tree 58 for part in parts: 59 if part not in current_level: 60 current_level[part] = {} 61 current_level = current_level[part] 62 63 64def build_tree(paths): 65 tree = {} 66 current_working_directory = os.getcwd() 67 current_folder_name = os.path.basename(current_working_directory) 68 # Filter out invalid and non-existent paths 69 relative_dirs = [] 70 for path in paths: 71 normalized_path = os.path.normpath(path) 72 try: 73 rel_path = os.path.relpath(normalized_path, start=current_working_directory) 74 add_path_to_tree(tree, normalized_path) 75 except ValueError: 76 print(f"Remove unexcpect dir:{path}") 77 78 return tree 79 80def print_tree(tree, indent=''): 81 for key, subtree in sorted(tree.items()): 82 print(indent + key) 83 print_tree(subtree, indent + ' ') 84 85def extract_source_dirs(compile_commands): 86 source_dirs = set() 87 88 for entry in compile_commands: 89 file_path = os.path.abspath(entry['file']) 90 91 if file_path.endswith('.c'): 92 dir_path = os.path.dirname(file_path) 93 source_dirs.add(dir_path) 94 # command or arguments 95 command = entry.get('command') or entry.get('arguments') 96 97 if isinstance(command, str): 98 parts = command.split() 99 else: 100 parts = command 101 # 读取-I或者/I 102 for i, part in enumerate(parts): 103 if part.startswith('-I'): 104 include_dir = part[2:] if len(part) > 2 else parts[i + 1] 105 source_dirs.add(os.path.abspath(include_dir)) 106 elif part.startswith('/I'): 107 include_dir = part[2:] if len(part) > 2 else parts[i + 1] 108 source_dirs.add(os.path.abspath(include_dir)) 109 110 return sorted(source_dirs) 111 112 113def is_path_in_tree(path, tree): 114 parts = path.split(os.sep) 115 current_level = tree 116 found_first_node = False 117 root_key = list(tree.keys())[0] 118 119 index_start = parts.index(root_key) 120 length = len(parts) 121 try: 122 for i in range(index_start, length): 123 current_level = current_level[parts[i]] 124 return True 125 except KeyError: 126 return False 127 128 129def generate_code_workspace_file(source_dirs,command_json_path,root_path): 130 current_working_directory = os.getcwd() 131 current_folder_name = os.path.basename(current_working_directory) 132 133 relative_dirs = [] 134 for dir_path in source_dirs: 135 try: 136 rel_path = os.path.relpath(dir_path, root_path) 137 relative_dirs.append(rel_path) 138 except ValueError: 139 continue 140 141 root_rel_path = os.path.relpath(root_path, current_working_directory) 142 command_json_path = os.path.relpath(current_working_directory, root_path) + os.sep 143 workspace_data = { 144 "folders": [ 145 { 146 "path": f"{root_rel_path}" 147 } 148 ], 149 "settings": { 150 "clangd.arguments": [ 151 f"--compile-commands-dir={command_json_path}", 152 "--header-insertion=never" 153 ], 154 "files.exclude": {dir.replace('\\','/'): True for dir in sorted(relative_dirs)} 155 } 156 } 157 workspace_filename = f'{current_folder_name}.code-workspace' 158 with open(workspace_filename, 'w') as f: 159 json.dump(workspace_data, f, indent=4) 160 161 print(f'Workspace file {workspace_filename} created.') 162 163def command_json_to_workspace(root_path,command_json_path): 164 165 with open('build/compile_commands.json', 'r') as f: 166 compile_commands = json.load(f) 167 168 source_dirs = extract_source_dirs(compile_commands) 169 tree = build_tree(source_dirs) 170 #print_tree(tree) 171 filtered_tree = filt_tree(tree) 172 print("Filtered Directory Tree:") 173 #print_tree(filtered_tree) 174 175 # 打印filtered_tree的root节点的相对路径 176 root_key = list(filtered_tree.keys())[0] 177 print(f"Root node relative path: {root_key}") 178 179 # 初始化exclude_fold集合 180 exclude_fold = set() 181 182 # os.chdir(root_path) 183 # 轮询root文件夹下面的每一个文件夹和子文件夹 184 for root, dirs, files in os.walk(root_path): 185 # 检查当前root是否在filtered_tree中 186 if not is_path_in_tree(root, filtered_tree): 187 exclude_fold.add(root) 188 dirs[:] = [] # 不往下轮询子文件夹 189 continue 190 for dir in dirs: 191 dir_path = os.path.join(root, dir) 192 if not is_path_in_tree(dir_path, filtered_tree): 193 exclude_fold.add(dir_path) 194 195 generate_code_workspace_file(exclude_fold,command_json_path,root_path) 196 197def delete_repeatelist(data): 198 temp_dict = set([str(item) for item in data]) 199 data = [eval(i) for i in temp_dict] 200 return data 201 202def GenerateCFiles(env): 203 """ 204 Generate c_cpp_properties.json and build/compile_commands.json files 205 """ 206 if not os.path.exists('.vscode'): 207 os.mkdir('.vscode') 208 209 with open('.vscode/c_cpp_properties.json', 'w') as vsc_file: 210 info = utils.ProjectInfo(env) 211 212 cc = os.path.join(rtconfig.EXEC_PATH, rtconfig.CC) 213 cc = os.path.abspath(cc).replace('\\', '/') 214 215 config_obj = {} 216 config_obj['name'] = 'Linux' 217 config_obj['defines'] = info['CPPDEFINES'] 218 219 intelliSenseMode = 'linux-gcc-arm' 220 if cc.find('aarch64') != -1: 221 intelliSenseMode = 'linux-gcc-arm64' 222 elif cc.find('arm') != -1: 223 intelliSenseMode = 'linux-gcc-arm' 224 config_obj['intelliSenseMode'] = intelliSenseMode 225 config_obj['compilerPath'] = cc 226 config_obj['cStandard'] = "c99" 227 config_obj['cppStandard'] = "c++11" 228 config_obj['compileCommands'] ="build/compile_commands.json" 229 230 # format "a/b," to a/b. remove first quotation mark("),and remove end (",) 231 includePath = [] 232 for i in info['CPPPATH']: 233 if i[0] == '\"' and i[len(i) - 2:len(i)] == '\",': 234 includePath.append(_make_path_relative(os.getcwd(), i[1:len(i) - 2])) 235 else: 236 includePath.append(_make_path_relative(os.getcwd(), i)) 237 config_obj['includePath'] = includePath 238 239 json_obj = {} 240 json_obj['configurations'] = [config_obj] 241 242 vsc_file.write(json.dumps(json_obj, ensure_ascii=False, indent=4)) 243 244 """ 245 Generate vscode.code-workspace files by build/compile_commands.json 246 """ 247 if os.path.exists('build/compile_commands.json'): 248 249 command_json_to_workspace(env['RTT_ROOT'],'build/compile_commands.json') 250 return 251 """ 252 Generate vscode.code-workspace files 253 """ 254 with open('vscode.code-workspace', 'w') as vsc_space_file: 255 info = utils.ProjectInfo(env) 256 path_list = [] 257 for i in info['CPPPATH']: 258 if _make_path_relative(os.getcwd(), i)[0] == '.': 259 if i[0] == '\"' and i[len(i) - 2:len(i)] == '\",': 260 path_list.append({'path':_make_path_relative(os.getcwd(), i[1:len(i) - 2])}) 261 else: 262 path_list.append({'path':_make_path_relative(os.getcwd(), i)}) 263 for i in info['DIRS']: 264 if _make_path_relative(os.getcwd(), i)[0] == '.': 265 if i[0] == '\"' and i[len(i) - 2:len(i)] == '\",': 266 path_list.append({'path':_make_path_relative(os.getcwd(), i[1:len(i) - 2])}) 267 else: 268 path_list.append({'path':_make_path_relative(os.getcwd(), i)}) 269 270 json_obj = {} 271 path_list = delete_repeatelist(path_list) 272 path_list = sorted(path_list, key=lambda x: x["path"]) 273 for path in path_list: 274 if path['path'] != '.': 275 normalized_path = path['path'].replace('\\', os.path.sep) 276 segments = [p for p in normalized_path.split(os.path.sep) if p != '..'] 277 path['name'] = 'rtthread/' + '/'.join(segments) 278 json_obj['folders'] = path_list 279 if os.path.exists('build/compile_commands.json'): 280 json_obj['settings'] = { 281 "clangd.arguments": [ 282 "--compile-commands-dir=.", 283 "--header-insertion=never" 284 ] 285 } 286 vsc_space_file.write(json.dumps(json_obj, ensure_ascii=False, indent=4)) 287 288 return 289 290def GenerateProjectFiles(env): 291 """ 292 Generate project.json file 293 """ 294 if not os.path.exists('.vscode'): 295 os.mkdir('.vscode') 296 297 project = env['project'] 298 with open('.vscode/project.json', 'w') as vsc_file: 299 groups = [] 300 for group in project: 301 if len(group['src']) > 0: 302 item = {} 303 item['name'] = group['name'] 304 item['path'] = _make_path_relative(os.getcwd(), group['path']) 305 item['files'] = [] 306 307 for fn in group['src']: 308 item['files'].append(str(fn)) 309 310 # append SConscript if exist 311 if os.path.exists(os.path.join(item['path'], 'SConscript')): 312 item['files'].append(os.path.join(item['path'], 'SConscript')) 313 314 groups.append(item) 315 316 json_dict = {} 317 json_dict['RT-Thread'] = env['RTT_ROOT'] 318 json_dict['Groups'] = groups 319 320 # write groups to project.json 321 vsc_file.write(json.dumps(json_dict, ensure_ascii=False, indent=4)) 322 323 return 324 325def GenerateVSCode(env): 326 print('Update setting files for VSCode...') 327 328 GenerateProjectFiles(env) 329 GenerateCFiles(env) 330 print('Done!') 331 332 return 333 334import os 335 336def find_rtconfig_dirs(bsp_dir, project_dir): 337 """ 338 Search for subdirectories containing 'rtconfig.h' under 'bsp_dir' (up to 4 levels deep), excluding 'project_dir'. 339 340 Args: 341 bsp_dir (str): The root directory to search (absolute path). 342 project_dir (str): The subdirectory to exclude from the search (absolute path). 343 344 Returns 345 list: A list of absolute paths to subdirectories containing 'rtconfig.h'. 346 """ 347 348 result = [] 349 project_dir = os.path.normpath(project_dir) 350 351 # list the bsp_dir to add result 352 list = os.listdir(bsp_dir) 353 for item in list: 354 item = os.path.join(bsp_dir, item) 355 356 # if item is a directory 357 if not os.path.isdir(item): 358 continue 359 360 # print(item, project_dir) 361 if not project_dir.startswith(item): 362 result.append(os.path.abspath(item)) 363 364 parent_dir = os.path.dirname(project_dir) 365 366 if parent_dir != bsp_dir: 367 list = os.listdir(parent_dir) 368 for item in list: 369 item = os.path.join(parent_dir, item) 370 rtconfig_path = os.path.join(item, 'rtconfig.h') 371 if os.path.isfile(rtconfig_path): 372 abs_path = os.path.abspath(item) 373 if abs_path != project_dir: 374 result.append(abs_path) 375 376 # print(result) 377 return result 378 379def GenerateVSCodeWorkspace(env): 380 """ 381 Generate vscode.code files 382 """ 383 print('Update workspace files for VSCode...') 384 385 # get the launch directory 386 cwd = GetLaunchDir() 387 388 # get .vscode/workspace.json file 389 workspace_file = os.path.join(cwd, '.vscode', 'workspace.json') 390 if not os.path.exists(workspace_file): 391 print('Workspace file not found, skip generating.') 392 return 393 394 try: 395 # read the workspace file 396 with open(workspace_file, 'r') as f: 397 workspace_data = json.load(f) 398 399 # get the bsp directories from the workspace data, bsps/folder 400 bsp_dir = os.path.join(cwd, workspace_data.get('bsps', {}).get('folder', '')) 401 if not bsp_dir: 402 print('No BSP directories found in the workspace file, skip generating.') 403 return 404 except Exception as e: 405 print('Error reading workspace file, skip generating.') 406 return 407 408 # check if .vscode folder exists, if not, create it 409 if not os.path.exists(os.path.join(cwd, '.vscode')): 410 os.mkdir(os.path.join(cwd, '.vscode')) 411 412 with open(os.path.join(cwd, '.vscode/c_cpp_properties.json'), 'w') as vsc_file: 413 info = utils.ProjectInfo(env) 414 415 cc = os.path.join(rtconfig.EXEC_PATH, rtconfig.CC) 416 cc = os.path.abspath(cc).replace('\\', '/') 417 418 config_obj = {} 419 config_obj['name'] = 'Linux' 420 config_obj['defines'] = info['CPPDEFINES'] 421 422 intelliSenseMode = 'linux-gcc-arm' 423 if cc.find('aarch64') != -1: 424 intelliSenseMode = 'linux-gcc-arm64' 425 elif cc.find('arm') != -1: 426 intelliSenseMode = 'linux-gcc-arm' 427 config_obj['intelliSenseMode'] = intelliSenseMode 428 config_obj['compilerPath'] = cc 429 config_obj['cStandard'] = "c99" 430 config_obj['cppStandard'] = "c++11" 431 432 # format "a/b," to a/b. remove first quotation mark("),and remove end (",) 433 includePath = [] 434 for i in info['CPPPATH']: 435 if i[0] == '\"' and i[len(i) - 2:len(i)] == '\",': 436 includePath.append(_make_path_relative(cwd, i[1:len(i) - 2])) 437 else: 438 includePath.append(_make_path_relative(cwd, i)) 439 # make sort for includePath 440 includePath = sorted(includePath, key=lambda x: x.lower()) 441 config_obj['includePath'] = includePath 442 443 json_obj = {} 444 json_obj['configurations'] = [config_obj] 445 446 vsc_file.write(json.dumps(json_obj, ensure_ascii=False, indent=4)) 447 448 # generate .vscode/settings.json 449 vsc_settings = {} 450 settings_path = os.path.join(cwd, '.vscode/settings.json') 451 if os.path.exists(settings_path): 452 with open(settings_path, 'r') as f: 453 # read the existing settings file and load to vsc_settings 454 vsc_settings = json.load(f) 455 456 with open(settings_path, 'w') as vsc_file: 457 vsc_settings['files.exclude'] = { 458 "**/__pycache__": True, 459 "tools/kconfig-frontends": True, 460 } 461 462 result = find_rtconfig_dirs(bsp_dir, os.getcwd()) 463 if result: 464 # sort the result 465 result = sorted(result, key=lambda x: x.lower()) 466 for item in result: 467 # make the path relative to the current working directory 468 rel_path = os.path.relpath(item, cwd) 469 # add the path to files.exclude 470 vsc_settings['files.exclude'][rel_path] = True 471 472 vsc_settings['search.exclude'] = vsc_settings['files.exclude'] 473 # write the settings to the file 474 vsc_file.write(json.dumps(vsc_settings, ensure_ascii=False, indent=4)) 475 476 print('Done!') 477 478 return 479