1
0

vsc.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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. """
  28. Utils for VSCode
  29. """
  30. import os
  31. import json
  32. import utils
  33. import rtconfig
  34. from SCons.Script import GetLaunchDir
  35. from utils import _make_path_relative
  36. def find_first_node_with_two_children(tree):
  37. for key, subtree in tree.items():
  38. if len(subtree) >= 2:
  39. return key, subtree
  40. result = find_first_node_with_two_children(subtree)
  41. if result:
  42. return result
  43. return None, None
  44. def filt_tree(tree):
  45. key, subtree = find_first_node_with_two_children(tree)
  46. if key:
  47. return {key: subtree}
  48. return {}
  49. def add_path_to_tree(tree, path):
  50. parts = path.split(os.sep)
  51. current_level = tree
  52. for part in parts:
  53. if part not in current_level:
  54. current_level[part] = {}
  55. current_level = current_level[part]
  56. def build_tree(paths):
  57. tree = {}
  58. current_working_directory = os.getcwd()
  59. current_folder_name = os.path.basename(current_working_directory)
  60. # Filter out invalid and non-existent paths
  61. relative_dirs = []
  62. for path in paths:
  63. normalized_path = os.path.normpath(path)
  64. try:
  65. rel_path = os.path.relpath(normalized_path, start=current_working_directory)
  66. add_path_to_tree(tree, normalized_path)
  67. except ValueError:
  68. print(f"Remove unexcpect dir:{path}")
  69. return tree
  70. def print_tree(tree, indent=''):
  71. for key, subtree in sorted(tree.items()):
  72. print(indent + key)
  73. print_tree(subtree, indent + ' ')
  74. def extract_source_dirs(compile_commands):
  75. source_dirs = set()
  76. for entry in compile_commands:
  77. file_path = os.path.abspath(entry['file'])
  78. if file_path.endswith('.c'):
  79. dir_path = os.path.dirname(file_path)
  80. source_dirs.add(dir_path)
  81. # command or arguments
  82. command = entry.get('command') or entry.get('arguments')
  83. if isinstance(command, str):
  84. parts = command.split()
  85. else:
  86. parts = command
  87. # 读取-I或者/I
  88. for i, part in enumerate(parts):
  89. if part.startswith('-I'):
  90. include_dir = part[2:] if len(part) > 2 else parts[i + 1]
  91. source_dirs.add(os.path.abspath(include_dir))
  92. elif part.startswith('/I'):
  93. include_dir = part[2:] if len(part) > 2 else parts[i + 1]
  94. source_dirs.add(os.path.abspath(include_dir))
  95. return sorted(source_dirs)
  96. def is_path_in_tree(path, tree):
  97. parts = path.split(os.sep)
  98. current_level = tree
  99. found_first_node = False
  100. root_key = list(tree.keys())[0]
  101. index_start = parts.index(root_key)
  102. length = len(parts)
  103. try:
  104. for i in range(index_start, length):
  105. current_level = current_level[parts[i]]
  106. return True
  107. except KeyError:
  108. return False
  109. def generate_code_workspace_file(source_dirs,command_json_path,root_path):
  110. current_working_directory = os.getcwd()
  111. current_folder_name = os.path.basename(current_working_directory)
  112. relative_dirs = []
  113. for dir_path in source_dirs:
  114. try:
  115. rel_path = os.path.relpath(dir_path, root_path)
  116. relative_dirs.append(rel_path)
  117. except ValueError:
  118. continue
  119. root_rel_path = os.path.relpath(root_path, current_working_directory)
  120. command_json_path = os.path.relpath(current_working_directory, root_path) + os.sep
  121. workspace_data = {
  122. "folders": [
  123. {
  124. "path": f"{root_rel_path}"
  125. }
  126. ],
  127. "settings": {
  128. "clangd.arguments": [
  129. f"--compile-commands-dir={command_json_path}",
  130. "--header-insertion=never"
  131. ],
  132. "files.exclude": {dir.replace('\\','/'): True for dir in sorted(relative_dirs)}
  133. }
  134. }
  135. workspace_filename = f'{current_folder_name}.code-workspace'
  136. with open(workspace_filename, 'w') as f:
  137. json.dump(workspace_data, f, indent=4)
  138. print(f'Workspace file {workspace_filename} created.')
  139. def command_json_to_workspace(root_path,command_json_path):
  140. with open('build/compile_commands.json', 'r') as f:
  141. compile_commands = json.load(f)
  142. source_dirs = extract_source_dirs(compile_commands)
  143. tree = build_tree(source_dirs)
  144. #print_tree(tree)
  145. filtered_tree = filt_tree(tree)
  146. print("Filtered Directory Tree:")
  147. #print_tree(filtered_tree)
  148. # 打印filtered_tree的root节点的相对路径
  149. root_key = list(filtered_tree.keys())[0]
  150. print(f"Root node relative path: {root_key}")
  151. # 初始化exclude_fold集合
  152. exclude_fold = set()
  153. # os.chdir(root_path)
  154. # 轮询root文件夹下面的每一个文件夹和子文件夹
  155. for root, dirs, files in os.walk(root_path):
  156. # 检查当前root是否在filtered_tree中
  157. if not is_path_in_tree(root, filtered_tree):
  158. exclude_fold.add(root)
  159. dirs[:] = [] # 不往下轮询子文件夹
  160. continue
  161. for dir in dirs:
  162. dir_path = os.path.join(root, dir)
  163. if not is_path_in_tree(dir_path, filtered_tree):
  164. exclude_fold.add(dir_path)
  165. generate_code_workspace_file(exclude_fold,command_json_path,root_path)
  166. def delete_repeatelist(data):
  167. temp_dict = set([str(item) for item in data])
  168. data = [eval(i) for i in temp_dict]
  169. return data
  170. def GenerateCFiles(env):
  171. """
  172. Generate c_cpp_properties.json and build/compile_commands.json files
  173. """
  174. if not os.path.exists('.vscode'):
  175. os.mkdir('.vscode')
  176. with open('.vscode/c_cpp_properties.json', 'w') as vsc_file:
  177. info = utils.ProjectInfo(env)
  178. cc = os.path.join(rtconfig.EXEC_PATH, rtconfig.CC)
  179. cc = os.path.abspath(cc).replace('\\', '/')
  180. config_obj = {}
  181. config_obj['name'] = 'Linux'
  182. config_obj['defines'] = info['CPPDEFINES']
  183. intelliSenseMode = 'linux-gcc-arm'
  184. if cc.find('aarch64') != -1:
  185. intelliSenseMode = 'linux-gcc-arm64'
  186. elif cc.find('arm') != -1:
  187. intelliSenseMode = 'linux-gcc-arm'
  188. config_obj['intelliSenseMode'] = intelliSenseMode
  189. config_obj['compilerPath'] = cc
  190. config_obj['cStandard'] = "c99"
  191. config_obj['cppStandard'] = "c++11"
  192. config_obj['compileCommands'] ="build/compile_commands.json"
  193. # format "a/b," to a/b. remove first quotation mark("),and remove end (",)
  194. includePath = []
  195. for i in info['CPPPATH']:
  196. if i[0] == '\"' and i[len(i) - 2:len(i)] == '\",':
  197. includePath.append(_make_path_relative(os.getcwd(), i[1:len(i) - 2]))
  198. else:
  199. includePath.append(_make_path_relative(os.getcwd(), i))
  200. config_obj['includePath'] = includePath
  201. json_obj = {}
  202. json_obj['configurations'] = [config_obj]
  203. vsc_file.write(json.dumps(json_obj, ensure_ascii=False, indent=4))
  204. """
  205. Generate vscode.code-workspace files by build/compile_commands.json
  206. """
  207. if os.path.exists('build/compile_commands.json'):
  208. command_json_to_workspace(env['RTT_ROOT'],'build/compile_commands.json')
  209. return
  210. """
  211. Generate vscode.code-workspace files
  212. """
  213. with open('vscode.code-workspace', 'w') as vsc_space_file:
  214. info = utils.ProjectInfo(env)
  215. path_list = []
  216. for i in info['CPPPATH']:
  217. if _make_path_relative(os.getcwd(), i)[0] == '.':
  218. if i[0] == '\"' and i[len(i) - 2:len(i)] == '\",':
  219. path_list.append({'path':_make_path_relative(os.getcwd(), i[1:len(i) - 2])})
  220. else:
  221. path_list.append({'path':_make_path_relative(os.getcwd(), i)})
  222. for i in info['DIRS']:
  223. if _make_path_relative(os.getcwd(), i)[0] == '.':
  224. if i[0] == '\"' and i[len(i) - 2:len(i)] == '\",':
  225. path_list.append({'path':_make_path_relative(os.getcwd(), i[1:len(i) - 2])})
  226. else:
  227. path_list.append({'path':_make_path_relative(os.getcwd(), i)})
  228. json_obj = {}
  229. path_list = delete_repeatelist(path_list)
  230. path_list = sorted(path_list, key=lambda x: x["path"])
  231. for path in path_list:
  232. if path['path'] != '.':
  233. normalized_path = path['path'].replace('\\', os.path.sep)
  234. segments = [p for p in normalized_path.split(os.path.sep) if p != '..']
  235. path['name'] = 'rtthread/' + '/'.join(segments)
  236. json_obj['folders'] = path_list
  237. if os.path.exists('build/compile_commands.json'):
  238. json_obj['settings'] = {
  239. "clangd.arguments": [
  240. "--compile-commands-dir=.",
  241. "--header-insertion=never"
  242. ]
  243. }
  244. vsc_space_file.write(json.dumps(json_obj, ensure_ascii=False, indent=4))
  245. return
  246. def GenerateProjectFiles(env):
  247. """
  248. Generate project.json file
  249. """
  250. if not os.path.exists('.vscode'):
  251. os.mkdir('.vscode')
  252. project = env['project']
  253. with open('.vscode/project.json', 'w') as vsc_file:
  254. groups = []
  255. for group in project:
  256. if len(group['src']) > 0:
  257. item = {}
  258. item['name'] = group['name']
  259. item['path'] = _make_path_relative(os.getcwd(), group['path'])
  260. item['files'] = []
  261. for fn in group['src']:
  262. item['files'].append(str(fn))
  263. # append SConscript if exist
  264. if os.path.exists(os.path.join(item['path'], 'SConscript')):
  265. item['files'].append(os.path.join(item['path'], 'SConscript'))
  266. groups.append(item)
  267. json_dict = {}
  268. json_dict['RT-Thread'] = env['RTT_ROOT']
  269. json_dict['Groups'] = groups
  270. # write groups to project.json
  271. vsc_file.write(json.dumps(json_dict, ensure_ascii=False, indent=4))
  272. return
  273. def GenerateVSCode(env):
  274. print('Update setting files for VSCode...')
  275. GenerateProjectFiles(env)
  276. GenerateCFiles(env)
  277. print('Done!')
  278. return
  279. import os
  280. def find_rtconfig_dirs(bsp_dir, project_dir):
  281. """
  282. Search for subdirectories containing 'rtconfig.h' under 'bsp_dir' (up to 4 levels deep), excluding 'project_dir'.
  283. Args:
  284. bsp_dir (str): The root directory to search (absolute path).
  285. project_dir (str): The subdirectory to exclude from the search (absolute path).
  286. Returns
  287. list: A list of absolute paths to subdirectories containing 'rtconfig.h'.
  288. """
  289. result = []
  290. project_dir = os.path.normpath(project_dir)
  291. # list the bsp_dir to add result
  292. list = os.listdir(bsp_dir)
  293. for item in list:
  294. item = os.path.join(bsp_dir, item)
  295. # if item is a directory
  296. if not os.path.isdir(item):
  297. continue
  298. # print(item, project_dir)
  299. if not project_dir.startswith(item):
  300. result.append(os.path.abspath(item))
  301. parent_dir = os.path.dirname(project_dir)
  302. if parent_dir != bsp_dir:
  303. list = os.listdir(parent_dir)
  304. for item in list:
  305. item = os.path.join(parent_dir, item)
  306. rtconfig_path = os.path.join(item, 'rtconfig.h')
  307. if os.path.isfile(rtconfig_path):
  308. abs_path = os.path.abspath(item)
  309. if abs_path != project_dir:
  310. result.append(abs_path)
  311. # print(result)
  312. return result
  313. def GenerateVSCodeWorkspace(env):
  314. """
  315. Generate vscode.code files
  316. """
  317. print('Update workspace files for VSCode...')
  318. # get the launch directory
  319. cwd = GetLaunchDir()
  320. # get .vscode/workspace.json file
  321. workspace_file = os.path.join(cwd, '.vscode', 'workspace.json')
  322. if not os.path.exists(workspace_file):
  323. print('Workspace file not found, skip generating.')
  324. return
  325. try:
  326. # read the workspace file
  327. with open(workspace_file, 'r') as f:
  328. workspace_data = json.load(f)
  329. # get the bsp directories from the workspace data, bsps/folder
  330. bsp_dir = os.path.join(cwd, workspace_data.get('bsps', {}).get('folder', ''))
  331. if not bsp_dir:
  332. print('No BSP directories found in the workspace file, skip generating.')
  333. return
  334. except Exception as e:
  335. print('Error reading workspace file, skip generating.')
  336. return
  337. # check if .vscode folder exists, if not, create it
  338. if not os.path.exists(os.path.join(cwd, '.vscode')):
  339. os.mkdir(os.path.join(cwd, '.vscode'))
  340. with open(os.path.join(cwd, '.vscode/c_cpp_properties.json'), 'w') as vsc_file:
  341. info = utils.ProjectInfo(env)
  342. cc = os.path.join(rtconfig.EXEC_PATH, rtconfig.CC)
  343. cc = os.path.abspath(cc).replace('\\', '/')
  344. config_obj = {}
  345. config_obj['name'] = 'Linux'
  346. config_obj['defines'] = info['CPPDEFINES']
  347. intelliSenseMode = 'linux-gcc-arm'
  348. if cc.find('aarch64') != -1:
  349. intelliSenseMode = 'linux-gcc-arm64'
  350. elif cc.find('arm') != -1:
  351. intelliSenseMode = 'linux-gcc-arm'
  352. config_obj['intelliSenseMode'] = intelliSenseMode
  353. config_obj['compilerPath'] = cc
  354. config_obj['cStandard'] = "c99"
  355. config_obj['cppStandard'] = "c++11"
  356. # format "a/b," to a/b. remove first quotation mark("),and remove end (",)
  357. includePath = []
  358. for i in info['CPPPATH']:
  359. if i[0] == '\"' and i[len(i) - 2:len(i)] == '\",':
  360. includePath.append(_make_path_relative(cwd, i[1:len(i) - 2]))
  361. else:
  362. includePath.append(_make_path_relative(cwd, i))
  363. # make sort for includePath
  364. includePath = sorted(includePath, key=lambda x: x.lower())
  365. config_obj['includePath'] = includePath
  366. json_obj = {}
  367. json_obj['configurations'] = [config_obj]
  368. vsc_file.write(json.dumps(json_obj, ensure_ascii=False, indent=4))
  369. # generate .vscode/settings.json
  370. vsc_settings = {}
  371. settings_path = os.path.join(cwd, '.vscode/settings.json')
  372. if os.path.exists(settings_path):
  373. with open(settings_path, 'r') as f:
  374. # read the existing settings file and load to vsc_settings
  375. vsc_settings = json.load(f)
  376. with open(settings_path, 'w') as vsc_file:
  377. vsc_settings['files.exclude'] = {
  378. "**/__pycache__": True,
  379. "tools/kconfig-frontends": True,
  380. }
  381. result = find_rtconfig_dirs(bsp_dir, os.getcwd())
  382. if result:
  383. # sort the result
  384. result = sorted(result, key=lambda x: x.lower())
  385. for item in result:
  386. # make the path relative to the current working directory
  387. rel_path = os.path.relpath(item, cwd)
  388. # add the path to files.exclude
  389. vsc_settings['files.exclude'][rel_path] = True
  390. vsc_settings['search.exclude'] = vsc_settings['files.exclude']
  391. # write the settings to the file
  392. vsc_file.write(json.dumps(vsc_settings, ensure_ascii=False, indent=4))
  393. print('Done!')
  394. return