Files
slsDetectorPackage/python/scripts/generate_functions.py
Erik Fröjdh 62488a8ce6 WIP
2025-10-29 09:19:28 +01:00

273 lines
8.7 KiB
Python

# SPDX-License-Identifier: LGPL-3.0-or-other
# Copyright (C) 2021 Contributors to the SLS Detector Package
"""
This file is used to auto generate Python bindings for the
sls::Detector class. The tool needs the libclang bindings
to be installed.
When the Detector API is updated this file should be run
manually.
"""
from clang import cindex
import subprocess
import argparse
import sys
import time
import ctypes.util, re # to check libclang version
from pathlib import Path
from parse import system_include_paths, clang_format_version
REDC = "\033[91m"
GREENC = "\033[92m"
YELLOWC = "\033[93m"
ENDC = "\033[0m"
def yellow(msg):
return f"{YELLOWC}{msg}{ENDC}"
def red(msg):
return f"{REDC}{msg}{ENDC}"
def green(msg):
return f"{GREENC}{msg}{ENDC}"
QUALIFY_KINDS = {
cindex.CursorKind.NAMESPACE,
cindex.CursorKind.CLASS_DECL,
cindex.CursorKind.STRUCT_DECL,
cindex.CursorKind.CLASS_TEMPLATE,
cindex.CursorKind.ENUM_DECL,
cindex.CursorKind.UNION_DECL,
cindex.CursorKind.TYPE_ALIAS_DECL,
}
def qualified_name(cur: cindex.Cursor, include_leaf: bool = True) -> str:
"""
Build a fully-qualified name by following semantic_parent up to the TU.
If include_leaf is False, returns just the enclosing scope (namespaces, classes).
"""
parts = []
c = cur if include_leaf else cur.semantic_parent
while c and c.kind != cindex.CursorKind.TRANSLATION_UNIT:
name = c.spelling or c.displayname # displayname helps for anonymous namespaces
if name and (c.kind in QUALIFY_KINDS or c is cur):
parts.append(name)
c = c.semantic_parent
return "::".join(reversed(parts))
def check_libclang_version(required="12"):
# Use already-loaded libclang, or let cindex resolve it
lib = ctypes.CDLL(cindex.Config.library_file or ctypes.util.find_library("clang"))
# Get version string
lib.clang_getClangVersion.restype = ctypes.c_void_p
version_ptr = lib.clang_getClangVersion()
version_str = ctypes.cast(version_ptr, ctypes.c_char_p).value.decode()
# Parse and check version
match = re.search(r"version\s+(\d+)", version_str)
if not match or match.group(1) != required:
msg = red(f"libclang version {match.group(1) if match else '?'} found, but version {required} required. Bye!")
print(msg)
sys.exit(1)
msg = green(f"Found libclang version {required}")
print(msg)
def check_clang_format_version(required_version):
if (ver := clang_format_version()) != required_version:
msg = red(
f"Clang format version {required_version} required, detected: {ver}. Bye!"
)
print(msg)
sys.exit(1)
else:
msg = green(f"Found clang-format version {ver}")
print(msg)
def check_for_compile_commands_json(path):
# print(f"Looking for compile data base in: {path}")
compile_data_base_file = path / "compile_commands.json"
if not compile_data_base_file.exists():
msg = red(f"No compile_commands.json file found in {path}. Bye!")
print(msg)
sys.exit(1)
else:
msg = green(f"Found: {compile_data_base_file}")
print(msg)
default_build_path = Path("../../build/")
fpath = Path("../../slsDetectorSoftware/src/Detector.cpp")
m = []
ag = []
lines = []
ag2 = []
cn = []
def get_arguments_with_default(node):
args = []
for arg in node.get_arguments():
tokens = [t.spelling for t in arg.get_tokens()]
if "=" in tokens:
# if arg.type.spelling == "sls::Positions": # TODO! automate+
if arg.type.spelling == "Positions": # TODO! automate
args.append("py::arg() = Positions{}")
else:
args.append("py::arg()" + "".join(tokens[tokens.index("=") :]))
else:
args.append("py::arg()")
args = ", ".join(args)
if args:
args = f", {args}"
return args
def get_fdec(node):
args = [a.type.spelling for a in node.get_arguments()]
if node.result_type.spelling:
return_type = node.result_type.spelling
else:
return_type = "void"
if node.is_const_method():
const = "const"
else:
const = ""
args = ", ".join(args)
args = f"({return_type}(Detector::*)({args}){const})"
return args
def time_return_lambda(node, args):
names = ['a', 'b', 'c', 'd']
fa = [a.type.spelling for a in node.get_arguments()]
ca = ','.join(f'{arg} {n}' for arg, n in zip(fa, names))
na = ','.join(names[0:len(fa)])
s = f'CppDetectorApi.def("{node.spelling}",[](sls::Detector& self, {ca}){{ auto r = self.{node.spelling}({na}); \n return std::vector<sls::Duration>(r.begin(), r.end()); }}{args});'
return s
def visit(node):
loc = node.location
# skip if ndoe is outside project directory
if loc.file and not str(loc.file).startswith(str(cargs.build_path.parent)):
return
'''
# to see which file was causing the error (not in Detector.h, so skipping others in the above code)
try:
kind = node.kind
except ValueError as e:
loc = node.location
file_name = loc.file.name if loc.file else "<unknown file>"
msg = yellow(f"\nWarning: skipping node with unknown CursorKind id {node._kind_id} at {file_name}:{loc.line}:{loc.column}")
print(msg)
return
'''
if node.kind == cindex.CursorKind.CLASS_DECL:
if node.displayname == "Detector":
for child in node.get_children():
# Skip assignment operators
if child.kind == cindex.CursorKind.CXX_METHOD and child.spelling == "operator=":
continue
if (
child.kind == cindex.CursorKind.CXX_METHOD
and child.access_specifier == cindex.AccessSpecifier.PUBLIC
):
m.append(child)
args = get_arguments_with_default(child)
fs = get_fdec(child)
qualified_class = qualified_name(child.semantic_parent) # "ns1::ns2::Detector"
# lines.append(
# f'CppDetectorApi.def("{child.spelling}", {fs} &{qualified_class}::{child.spelling}{args});'
# )
lines.append(
f'CppDetectorApi.def("{child.spelling}",{fs} &Detector::{child.spelling}{args});'
)
if cargs.verbose:
print(f"&Detector::{child.spelling}{args})")
cn.append(child)
for child in node.get_children():
visit(child)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"-p",
"--build_path",
help="Path to the build database",
type=Path,
default=default_build_path,
)
parser.add_argument(
"-v",
"--verbose",
help="more output",
action="store_true",
)
cargs = parser.parse_args()
# check_libclang_version("12")
# check_clang_format_version(12)
cargs.build_path = cargs.build_path.resolve()
print(f'Using build path: {cargs.build_path}')
check_for_compile_commands_json(cargs.build_path)
print("Parsing functions in Detector.h - ", end="", flush=True)
t0 = time.perf_counter()
# parse functions
db = cindex.CompilationDatabase.fromDirectory(cargs.build_path)
index = cindex.Index.create()
args = db.getCompileCommands(fpath)
args = list(iter(args).__next__().arguments)[0:-1]
# print('\n\nUsing compile commands:', args)
# args = args + "-x c++ --std=c++17".split()
syspath = system_include_paths("clang++")
incargs = ["-I" + inc for inc in syspath]
args = args + incargs
args = [arg for arg in args if arg.startswith("-I") or arg.startswith("-D")]
# args = incargs
# print(f"\n\nUsing compile args: {' '.join(args)}")
tu = index.parse(fpath, args=args)
visit(tu.cursor)
print(green("OK"))
print(f"Parsing took {time.perf_counter()-t0:.3f}s")
print("Read detector_in.cpp - ", end="")
with open("../src/detector_in.cpp") as f:
data = f.read()
s = "".join(lines)
s += ";"
text = data.replace("[[FUNCTIONS]]", s)
warning = "/* WARINING This file is auto generated any edits might be overwritten without warning */\n\n"
print(green("OK"))
print("Writing to detector.cpp - ", end="")
with open("../src/detector.cpp", "w") as f:
f.write(warning)
f.write(text)
print(green("OK"))
# run clang format on the output
print("Running clang format on generated source -", end="")
subprocess.run(["clang-format", "../src/detector.cpp", "-i"])
print(green(" OK"))
# print("Changes since last commit:")
# subprocess.run(["git", "diff", "../src/detector.cpp"])