# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import glob import logging import os import re import subprocess import sys dump_tree = False def add_clang_compat_module_to_sys_path_if_needed(): try: import clang.cindex except: sys.path.append(os.path.join(os.path.dirname(__file__), 'clang_compat')) logging.info("Importing clang python compatibility module") add_clang_compat_module_to_sys_path_if_needed() import clang.cindex def get_homebrew_llvm_lib_path(): try: o = subprocess.check_output(['brew', 'ls', 'llvm']) except subprocess.CalledProcessError: # No brew llvm installed return None # Brittleness alert! Grepping output of `brew info llvm` for llvm bin path: m = re.search('.*/llvm-config', o) if m: llvm_config_path = m.group(0) o = subprocess.check_output([llvm_config_path, '--libdir']) llvm_lib_path = o.strip() # Make sure --enable-clang and --enable-python options were used: if os.path.exists(os.path.join(llvm_lib_path, 'libclang.dylib')) and \ glob.glob(os.path.join(llvm_lib_path, 'python*', 'site-packages', 'clang')): return llvm_lib_path else: logging.info("Found llvm from homebrew, but not installed with" " --with-clang --with-python") def load_library(): try: libclang_lib = clang.cindex.conf.lib except clang.cindex.LibclangError: pass except: raise else: return if sys.platform == 'darwin': libclang_path = get_homebrew_llvm_lib_path() if not libclang_path: # Try using Xcode's libclang: logging.info("llvm from homebrew not found," " trying Xcode's instead") xcode_path = subprocess.check_output(['xcode-select', '--print-path']).strip() libclang_path = \ os.path.join(xcode_path, 'Toolchains/XcodeDefault.xctoolchain/usr/lib') clang.cindex.conf.set_library_path(libclang_path) elif sys.platform == 'linux2': libclang_path = subprocess.check_output(['llvm-config', '--libdir']).strip() clang.cindex.conf.set_library_path(libclang_path) libclang_lib = clang.cindex.conf.lib def do_libclang_setup(): load_library() functions = ( ("clang_Cursor_getCommentRange", [clang.cindex.Cursor], clang.cindex.SourceRange), ) for f in functions: clang.cindex.register_function(clang.cindex.conf.lib, f, False) def is_node_kind_a_type_decl(kind): return kind == clang.cindex.CursorKind.STRUCT_DECL or \ kind == clang.cindex.CursorKind.ENUM_DECL or \ kind == clang.cindex.CursorKind.TYPEDEF_DECL def get_node_spelling(node): return clang.cindex.conf.lib.clang_getCursorSpelling(node) def get_comment_range(node): source_range = clang.cindex.conf.lib.clang_Cursor_getCommentRange(node) if source_range.start.file is None: return None return source_range def get_comment_range_for_decl(node): source_range = get_comment_range(node) if source_range is None: if node.kind == clang.cindex.CursorKind.TYPEDEF_DECL: for child in node.get_children(): if is_node_kind_a_type_decl(child.kind) and len(get_node_spelling(child)) == 0: source_range = get_comment_range(child) return source_range def get_comment_string_for_decl(node): comment_range = get_comment_range_for_decl(node) comment_string = get_string_from_file(comment_range) if comment_string is None: return None if '@addtogroup' in comment_string: # This is actually a block comment, not a comment specifically for this type. Ignore it. return None return comment_string def get_string_from_file(source_range): if source_range is None: return None source_range_file = source_range.start.file if source_range_file is None: return None with open(source_range_file.name) as f: f.seek(source_range.start.offset) return f.read(source_range.end.offset - source_range.start.offset) def dump_node(node, indent_level=0): spelling = node.spelling if node.kind == clang.cindex.CursorKind.MACRO_DEFINITION: spelling = get_node_spelling(node) print "%*s%s> %s" % (indent_level * 2, "", node.kind, spelling) print "%*sRange: %s" % (4 + (indent_level * 2), "", str(node.extent)) print "%*sComment: %s" % (4 + (indent_level * 2), "", str(get_comment_range_for_decl(node))) def return_true(node): return True def for_each_node(node, func, level=0, filter_func=return_true): if not filter_func(node): return if dump_tree: # Skip over nodes that are added by clang internals if node.location.file is not None: dump_node(node, level) func(node) for child in node.get_children(): for_each_node(child, func, level + 1, filter_func) def extract_declarations(tu, filenames, func): matching_basenames = {os.path.basename(f) for f in filenames} def filename_filter_func(node): node_file = node.location.file if node_file is None: return True node_filename = node_file.name if node_filename is None: return True base_name = os.path.basename(node_filename) return base_name in matching_basenames for_each_node(tu.cursor, func, filter_func=filename_filter_func) def parse_file(filename, filenames, func, internal_sdk_build=False, compiler_flags=None): src_dir = os.path.join(os.path.dirname(__file__), "../src") args = [ "-I%s/core" % src_dir, "-I%s/include" % src_dir, "-I%s/fw" % src_dir, "-I%s/fw/applib/vendor/uPNG" % src_dir, "-I%s/fw/applib/vendor/tinflate" % src_dir, "-I%s/fw/vendor/jerryscript/jerry-core" % src_dir, "-I%s/libbtutil/include" % src_dir, "-I%s/libos/include" % src_dir, "-I%s/libutil/includes" % src_dir, "-I%s/libc/include" % src_dir, "-I%s/../build/src/fw" % src_dir, "-I%s/include" % src_dir, "-DSDK", "-fno-builtin-itoa"] # Add header search paths, recursing subdirs: for inc_sub_dir in ['fw/util']: args += [inc_sub_dir] args += ["-I%s" % d for d in glob.glob(os.path.join(src_dir, "%s/*/" % inc_sub_dir))] if internal_sdk_build: args.append("-DINTERNAL_SDK_BUILD") else: args.append("-DPUBLIC_SDK") args.extend(compiler_flags) # Check Clang for unsigned types being undefined # https://sourceware.org/ml/newlib/2014/msg00082.html # this workaround should be removed when fixed in newlib cmd = ['clang'] + ['-dM', '-E', '-'] try: out = subprocess.check_output(cmd, stdin=open('/dev/null')).strip() if not isinstance(out, str): out = out.decode(sys.stdout.encoding or 'iso8859-1') except Exception as err: print('Could not run clang type checking %r' % err) raise if '__UINT8_TYPE__' not in out: args.insert(0, r"-D__UINT8_TYPE__=unsigned __INT8_TYPE__") args.insert(0, r"-D__UINT16_TYPE__=unsigned __INT16_TYPE__") args.insert(0, r"-D__UINT32_TYPE__=unsigned __INT32_TYPE__") args.insert(0, r"-D__UINT64_TYPE__=unsigned __INT64_TYPE__") args.insert(0, r"-D__UINTPTR_TYPE__=unsigned __INTPTR_TYPE__") # Tools pull in time.h from arm toolchain instead of using our core/utils/time/time.h # with modified definition of struct tm, so disable accidental include of wrong time.h args.insert(0, r"-D_TIME_H_") # Try and find our arm toolchain and use the headers from that. gcc_path = subprocess.check_output(['which', 'arm-none-eabi-gcc']).strip() include_path = os.path.join(os.path.dirname(gcc_path), '../arm-none-eabi/include') args.append("-I%s" % include_path) # Find the arm-none-eabi-gcc libgcc path including stdbool.h cmd = ['arm-none-eabi-gcc'] + ['-E', '-v', '-xc', '-'] try: out = subprocess.check_output(cmd, stdin=open('/dev/null'), stderr=subprocess.STDOUT).strip().splitlines() if '#include <...> search starts here:' in out: libgcc_include_path = out[out.index('#include <...> search starts here:') + 1].strip() args.append("-I%s" % libgcc_include_path) except Exception as err: print('Could not run arm-none-eabi-gcc path detection %r' % err) if not os.path.isfile(filename): raise Exception("Invalid filename: " + filename) index = clang.cindex.Index.create() tu = index.parse(filename, args=args, options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD) extract_declarations(tu, filenames, func) for d in tu.diagnostics: if d.severity >= clang.cindex.Diagnostic.Error \ and d.spelling != "conflicting types for 'itoa'": if d.severity == clang.cindex.Diagnostic.Error: error_str = "Error: %s" % d.__repr__() elif d.severity == clang.cindex.Diagnostic.Fatal: error_str = "Fatal: %s" % d.__repr__() class ParsingException(Exception): pass raise ParsingException(error_str) do_libclang_setup()