vsc.py 17 KB

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