parse_c_decl.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. # Copyright 2024 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import glob
  15. import logging
  16. import os
  17. import re
  18. import subprocess
  19. import sys
  20. dump_tree = False
  21. def add_clang_compat_module_to_sys_path_if_needed():
  22. try:
  23. import clang.cindex
  24. except:
  25. sys.path.append(os.path.join(os.path.dirname(__file__),
  26. 'clang_compat'))
  27. logging.info("Importing clang python compatibility module")
  28. add_clang_compat_module_to_sys_path_if_needed()
  29. import clang.cindex
  30. def get_homebrew_llvm_lib_path():
  31. try:
  32. o = subprocess.check_output(['brew', 'ls', 'llvm'])
  33. except subprocess.CalledProcessError:
  34. # No brew llvm installed
  35. return None
  36. # Brittleness alert! Grepping output of `brew info llvm` for llvm bin path:
  37. m = re.search('.*/llvm-config', o.decode("utf8"))
  38. if m:
  39. llvm_config_path = m.group(0)
  40. o = subprocess.check_output([llvm_config_path, '--libdir'])
  41. llvm_lib_path = o.decode("utf8").strip()
  42. # Make sure --enable-clang and --enable-python options were used:
  43. if os.path.exists(os.path.join(llvm_lib_path, 'libclang.dylib')) and \
  44. glob.glob(os.path.join(llvm_lib_path,
  45. 'python*', 'site-packages', 'clang')):
  46. return llvm_lib_path
  47. else:
  48. logging.info("Found llvm from homebrew, but not installed with"
  49. " --with-clang --with-python")
  50. def load_library():
  51. try:
  52. libclang_lib = clang.cindex.conf.lib
  53. except clang.cindex.LibclangError:
  54. pass
  55. except:
  56. raise
  57. else:
  58. return
  59. if sys.platform == 'darwin':
  60. libclang_path = get_homebrew_llvm_lib_path()
  61. if not libclang_path:
  62. # Try using Xcode's libclang:
  63. logging.info("llvm from homebrew not found,"
  64. " trying Xcode's instead")
  65. xcode_path = subprocess.check_output(['xcode-select',
  66. '--print-path']).decode("utf8").strip()
  67. libclang_path = \
  68. os.path.join(xcode_path,
  69. 'Toolchains/XcodeDefault.xctoolchain/usr/lib')
  70. clang.cindex.conf.set_library_path(libclang_path)
  71. elif sys.platform == 'linux2':
  72. libclang_path = subprocess.check_output(['llvm-config',
  73. '--libdir']).decode("utf8").strip()
  74. clang.cindex.conf.set_library_path(libclang_path)
  75. libclang_lib = clang.cindex.conf.lib
  76. def do_libclang_setup():
  77. load_library()
  78. functions = (
  79. ("clang_Cursor_getCommentRange",
  80. [clang.cindex.Cursor],
  81. clang.cindex.SourceRange),
  82. )
  83. for f in functions:
  84. clang.cindex.register_function(clang.cindex.conf.lib, f, False)
  85. def is_node_kind_a_type_decl(kind):
  86. return kind == clang.cindex.CursorKind.STRUCT_DECL or \
  87. kind == clang.cindex.CursorKind.ENUM_DECL or \
  88. kind == clang.cindex.CursorKind.TYPEDEF_DECL
  89. def get_node_spelling(node):
  90. return clang.cindex.conf.lib.clang_getCursorSpelling(node)
  91. def get_comment_range(node):
  92. source_range = clang.cindex.conf.lib.clang_Cursor_getCommentRange(node)
  93. if source_range.start.file is None:
  94. return None
  95. return source_range
  96. def get_comment_range_for_decl(node):
  97. source_range = get_comment_range(node)
  98. if source_range is None:
  99. if node.kind == clang.cindex.CursorKind.TYPEDEF_DECL:
  100. for child in node.get_children():
  101. if is_node_kind_a_type_decl(child.kind) and len(get_node_spelling(child)) == 0:
  102. source_range = get_comment_range(child)
  103. return source_range
  104. def get_comment_string_for_decl(node):
  105. comment_range = get_comment_range_for_decl(node)
  106. comment_string = get_string_from_file(comment_range)
  107. if comment_string is None:
  108. return None
  109. if '@addtogroup' in comment_string:
  110. # This is actually a block comment, not a comment specifically for this type. Ignore it.
  111. return None
  112. return comment_string
  113. def get_string_from_file(source_range):
  114. if source_range is None:
  115. return None
  116. source_range_file = source_range.start.file
  117. if source_range_file is None:
  118. return None
  119. with open(source_range_file.name, "rb") as f:
  120. f.seek(source_range.start.offset)
  121. return f.read(source_range.end.offset -
  122. source_range.start.offset).decode("utf8")
  123. def dump_node(node, indent_level=0):
  124. spelling = node.spelling
  125. if node.kind == clang.cindex.CursorKind.MACRO_DEFINITION:
  126. spelling = get_node_spelling(node)
  127. print("%*s%s> %s" % (indent_level * 2, "", node.kind, spelling))
  128. print("%*sRange: %s" % (4 + (indent_level * 2), "", str(node.extent)))
  129. print("%*sComment: %s" % (4 + (indent_level * 2), "", str(get_comment_range_for_decl(node))))
  130. def return_true(node):
  131. return True
  132. def for_each_node(node, func, level=0, filter_func=return_true):
  133. if not filter_func(node):
  134. return
  135. if dump_tree:
  136. # Skip over nodes that are added by clang internals
  137. if node.location.file is not None:
  138. dump_node(node, level)
  139. func(node)
  140. for child in node.get_children():
  141. for_each_node(child, func, level + 1, filter_func)
  142. def extract_declarations(tu, filenames, func):
  143. matching_basenames = {os.path.basename(f) for f in filenames}
  144. def filename_filter_func(node):
  145. node_file = node.location.file
  146. if node_file is None:
  147. return True
  148. node_filename = node_file.name
  149. if node_filename is None:
  150. return True
  151. base_name = os.path.basename(node_filename)
  152. return base_name in matching_basenames
  153. for_each_node(tu.cursor, func, filter_func=filename_filter_func)
  154. def parse_file(filename, filenames, func, internal_sdk_build=False, compiler_flags=None):
  155. src_dir = os.path.join(os.path.dirname(__file__), "../../src")
  156. args = [ "-I%s/core" % src_dir,
  157. "-I%s/include" % src_dir,
  158. "-I%s/fw" % src_dir,
  159. "-I%s/fw/applib/vendor/uPNG" % src_dir,
  160. "-I%s/fw/applib/vendor/tinflate" % src_dir,
  161. "-I%s/fw/vendor/jerryscript/jerry-core" % src_dir,
  162. "-I%s/libbtutil/include" % src_dir,
  163. "-I%s/libos/include" % src_dir,
  164. "-I%s/libutil/includes" % src_dir,
  165. "-I%s/libc/include" % src_dir,
  166. "-I%s/../build/src/fw" % src_dir,
  167. "-I%s/include" % src_dir,
  168. "-DSDK",
  169. "-fno-builtin-itoa"]
  170. # Add header search paths, recursing subdirs:
  171. for inc_sub_dir in ['fw/util']:
  172. args += [inc_sub_dir]
  173. args += ["-I%s" % d for d in glob.glob(os.path.join(src_dir, "%s/*/" % inc_sub_dir))]
  174. if internal_sdk_build:
  175. args.append("-DINTERNAL_SDK_BUILD")
  176. else:
  177. args.append("-DPUBLIC_SDK")
  178. args.extend(compiler_flags)
  179. # Check Clang for unsigned types being undefined
  180. # https://sourceware.org/ml/newlib/2014/msg00082.html
  181. # this workaround should be removed when fixed in newlib
  182. cmd = ['clang'] + ['-dM', '-E', '-']
  183. try:
  184. out = subprocess.check_output(cmd, stdin=open('/dev/null')).decode("utf8").strip()
  185. if not isinstance(out, str):
  186. out = out.decode(sys.stdout.encoding or 'iso8859-1')
  187. except Exception as err:
  188. print('Could not run clang type checking %r' % err)
  189. raise
  190. if '__UINT8_TYPE__' not in out:
  191. args.insert(0, r"-D__UINT8_TYPE__=unsigned __INT8_TYPE__")
  192. args.insert(0, r"-D__UINT16_TYPE__=unsigned __INT16_TYPE__")
  193. args.insert(0, r"-D__UINT32_TYPE__=unsigned __INT32_TYPE__")
  194. args.insert(0, r"-D__UINT64_TYPE__=unsigned __INT64_TYPE__")
  195. args.insert(0, r"-D__UINTPTR_TYPE__=unsigned __INTPTR_TYPE__")
  196. # Tools pull in time.h from arm toolchain instead of using our core/utils/time/time.h
  197. # with modified definition of struct tm, so disable accidental include of wrong time.h
  198. args.insert(0, r"-D_TIME_H_")
  199. # Try and find our arm toolchain and use the headers from that.
  200. gcc_path = subprocess.check_output(['which', 'arm-none-eabi-gcc']).decode("utf8").strip()
  201. include_path = os.path.join(os.path.dirname(gcc_path), '../arm-none-eabi/include')
  202. args.append("-I%s" % include_path)
  203. # Find the arm-none-eabi-gcc libgcc path including stdbool.h
  204. cmd = ['arm-none-eabi-gcc'] + ['-E', '-v', '-xc', '-']
  205. try:
  206. out = subprocess.check_output(cmd, stdin=open('/dev/null'), stderr=subprocess.STDOUT).decode("utf8").strip().splitlines()
  207. if '#include <...> search starts here:' in out:
  208. libgcc_include_path = out[out.index('#include <...> search starts here:') + 1].strip()
  209. args.append("-I%s" % libgcc_include_path)
  210. except Exception as err:
  211. print('Could not run arm-none-eabi-gcc path detection %r' % err)
  212. if not os.path.isfile(filename):
  213. raise Exception("Invalid filename: " + filename)
  214. args.append("-ffreestanding")
  215. index = clang.cindex.Index.create()
  216. tu = index.parse(filename, args=args, options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD)
  217. extract_declarations(tu, filenames, func)
  218. for d in tu.diagnostics:
  219. if d.severity >= clang.cindex.Diagnostic.Error \
  220. and d.spelling != "conflicting types for 'itoa'":
  221. if d.severity == clang.cindex.Diagnostic.Error:
  222. error_str = "Error: %s" % d.__repr__()
  223. elif d.severity == clang.cindex.Diagnostic.Fatal:
  224. error_str = "Fatal: %s" % d.__repr__()
  225. class ParsingException(Exception):
  226. pass
  227. raise ParsingException(error_str)
  228. do_libclang_setup()