From 2f756433b2870cbc66e60a15876660a0aabc43e1 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Mon, 4 Aug 2025 13:45:03 -0400 Subject: [PATCH] feat(libplugin): added libplugin as a dependency and added fourdst-cli --- build-python/meson.build | 19 +++ fourdst/__init__.py | 0 fourdst/cli/__init__.py | 0 fourdst/cli/main.py | 169 +++++++++++++++++++++++++++ fourdst/cli/templates/meson.build.in | 15 +++ fourdst/cli/templates/plugin.cpp.in | 15 +++ meson.build | 2 +- pyproject.toml | 12 +- subprojects/libplugin.wrap | 4 + 9 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 fourdst/__init__.py create mode 100644 fourdst/cli/__init__.py create mode 100644 fourdst/cli/main.py create mode 100644 fourdst/cli/templates/meson.build.in create mode 100644 fourdst/cli/templates/plugin.cpp.in create mode 100644 subprojects/libplugin.wrap diff --git a/build-python/meson.build b/build-python/meson.build index f7ee11b..44c813b 100644 --- a/build-python/meson.build +++ b/build-python/meson.build @@ -18,3 +18,22 @@ py_mod = py_installation.extension_module( cpp_args : ['-UNDEBUG'], install : true, ) + +py_installation.install_sources( + meson.project_source_root() + '/fourdst/__init__.py', + subdir: 'fourdst', +) +py_installation.install_sources( + files( + meson.project_source_root() + '/fourdst/cli/__init__.py', + meson.project_source_root() + '/fourdst/cli/main.py', + ), + subdir: 'fourdst/cli', +) +py_installation.install_sources( + files( + meson.project_source_root() + '/fourdst/cli/templates/meson.build.in', + meson.project_source_root() + '/fourdst/cli/templates/plugin.cpp.in', + ), + subdir: 'fourdst/cli/templates', +) diff --git a/fourdst/__init__.py b/fourdst/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fourdst/cli/__init__.py b/fourdst/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fourdst/cli/main.py b/fourdst/cli/main.py new file mode 100644 index 0000000..e5a22b0 --- /dev/null +++ b/fourdst/cli/main.py @@ -0,0 +1,169 @@ +# fourdst/cli/main.py + +import typer +import os +import sys +import shutil +import subprocess +from pathlib import Path +import importlib.resources +import questionary +from clang import cindex + +# --- Main Typer application --- +app = typer.Typer( + name="fourdst-cli", + help="A command-line tool for managing fourdst projects and plugins." +) +plugin_app = typer.Typer(name="plugin", help="Commands for managing fourdst plugins.") +app.add_typer(plugin_app, name="plugin") + +def get_template_content(template_name: str) -> str: + """Safely reads content from a template file packaged with the CLI.""" + try: + return importlib.resources.files('fourdst.cli.templates').joinpath(template_name).read_text() + except FileNotFoundError: + print(f"Error: Template file '{template_name}' not found.", file=sys.stderr) + sys.exit(1) + +def parse_cpp_header(header_path: Path): + """ + Parses a C++ header file using libclang to find classes and their pure virtual methods. + """ + if not cindex.Config.loaded: + try: + cindex.Config.set_library_file(cindex.conf.get_filename()) + except cindex.LibclangError as e: + print(f"Error: libclang library not found. Please ensure it's installed and in your system's path.", file=sys.stderr) + print(f"Details: {e}", file=sys.stderr) + raise typer.Exit(code=1) + + # --- Get compiler flags from pkg-config to help clang find includes --- + try: + pkg_config_proc = subprocess.run( + ['pkg-config', '--cflags', 'fourdst_plugin'], + capture_output=True, + text=True, + check=True + ) + # Split the flags string into a list of arguments for libclang + compiler_flags = pkg_config_proc.stdout.strip().split() + print(f"Using compiler flags from pkg-config: {' '.join(compiler_flags)}") + except (subprocess.CalledProcessError, FileNotFoundError): + print("Warning: `pkg-config --cflags fourdst-plugin` failed. Parsing may not succeed if the header has dependencies.", file=sys.stderr) + print("Please ensure 'pkg-config' is installed and 'fourdst-plugin.pc' is in your PKG_CONFIG_PATH.", file=sys.stderr) + compiler_flags = [] + + index = cindex.Index.create() + # Add the pkg-config flags to the parser arguments + translation_unit = index.parse(str(header_path), args=['-x', 'c++', '-std=c++23'] + compiler_flags) + + interfaces = {} + for cursor in translation_unit.cursor.walk_preorder(): + if cursor.kind == cindex.CursorKind.CLASS_DECL and cursor.is_definition(): + class_name = cursor.spelling + methods = [] + for child in cursor.get_children(): + if child.kind == cindex.CursorKind.CXX_METHOD and child.is_pure_virtual_method(): + method_name = child.spelling + result_type = child.result_type.spelling + # Recreate the full method signature + params = [p.spelling or f"param{i+1}" for i, p in enumerate(child.get_arguments())] + param_str = ", ".join(f"{p.type.spelling} {p.spelling}" for p in child.get_arguments()) + const_qualifier = " const" if child.is_const_method() else "" + + signature = f"{result_type} {method_name}({param_str}){const_qualifier}" + + # Generate a placeholder body + body = f" // TODO: Implement the {method_name} method.\n" + if result_type != "void": + body += f" return {{}};" # Default return + + methods.append({'signature': signature, 'body': body}) + + if methods: # Only consider classes with pure virtual methods as interfaces + interfaces[class_name] = methods + + return interfaces + +@plugin_app.command("init") +def plugin_init( + project_name: str = typer.Argument(..., help="The name of the new plugin project."), + header: Path = typer.Option(..., "--header", "-H", help="Path to the C++ header file defining the plugin interface.", exists=True, file_okay=True, dir_okay=False, readable=True), + directory: Path = typer.Option(".", "-d", "--directory", help="The directory to create the project in.", resolve_path=True), + version: str = typer.Option("0.1.0", "--ver", help="The initial SemVer version of the plugin.") +): + """ + Initializes a new Meson-based C++ plugin project from an interface header. + """ + print(f"Parsing interface header: {header.name}") + interfaces = parse_cpp_header(header) + + if not interfaces: + print(f"Error: No suitable interfaces (classes with pure virtual methods) found in {header}", file=sys.stderr) + raise typer.Exit(code=1) + + # --- Interactive Selection --- + chosen_interface = questionary.select( + "Which interface would you like to implement?", + choices=list(interfaces.keys()) + ).ask() + + if not chosen_interface: + raise typer.Exit() # User cancelled + + print(f"Initializing plugin '{project_name}' implementing interface '{chosen_interface}'...") + + # --- Code Generation --- + method_stubs = "\n".join( + f" {method['signature']} override {{\n{method['body']}\n }}" + for method in interfaces[chosen_interface] + ) + + class_name = ''.join(filter(str.isalnum, project_name.replace('_', ' ').title().replace(' ', ''))) + "Plugin" + root_path = directory / project_name + src_path = root_path / "src" + include_path = root_path / "include" + + try: + src_path.mkdir(parents=True, exist_ok=True) + include_path.mkdir(exist_ok=True) + + # --- Create meson.build from template --- + meson_template = get_template_content("meson.build.in") + meson_content = meson_template.format( + project_name=project_name, + version=version, + interface_include_dir=header.parent.resolve() + ) + (root_path / "meson.build").write_text(meson_content) + print(f" -> Created {root_path / 'meson.build'}") + + # --- Create C++ source file from template --- + cpp_template = get_template_content("plugin.cpp.in") + cpp_content = cpp_template.format( + class_name=class_name, + project_name=project_name, + interface=chosen_interface, + interface_header_path=header.absolute(), + method_stubs=method_stubs + ) + (src_path / f"{project_name}.cpp").write_text(cpp_content) + print(f" -> Created {src_path / f'{project_name}.cpp'}") + + # --- Create .gitignore --- + (root_path / ".gitignore").write_text("builddir/\n") + print(f" -> Created {root_path / '.gitignore'}") + + except OSError as e: + print(f"Error creating project structure: {e}", file=sys.stderr) + raise typer.Exit(code=1) + + print("\nProject initialized successfully!") + print("To build your new plugin:") + print(f" cd {root_path}") + print(" meson setup builddir") + print(" meson compile -C builddir") + +if __name__ == "__main__": + app() \ No newline at end of file diff --git a/fourdst/cli/templates/meson.build.in b/fourdst/cli/templates/meson.build.in new file mode 100644 index 0000000..5b88fce --- /dev/null +++ b/fourdst/cli/templates/meson.build.in @@ -0,0 +1,15 @@ +project('{project_name}', 'cpp', + version : '{version}', + default_options : ['warning_level=3', 'cpp_std=c++23']) + +# Find the fourdst-plugin dependency +plugin_dep = dependency('fourdst_plugin', required : true) + +# Define the shared library for the plugin +shared_library('{project_name}', + 'src/{project_name}.cpp', + dependencies : [plugin_dep], + # Add the path to the user-provided interface header + include_directories: include_directories('{interface_include_dir}'), + install : true, +) diff --git a/fourdst/cli/templates/plugin.cpp.in b/fourdst/cli/templates/plugin.cpp.in new file mode 100644 index 0000000..e11de85 --- /dev/null +++ b/fourdst/cli/templates/plugin.cpp.in @@ -0,0 +1,15 @@ +#include "{interface_header_path}" +#include +#include + +class {class_name} final : public {interface} {{ +public: + ~{class_name}() override {{ + // Implement any custom destruction logic here + }} + + // --- Implemented Abstract Methods --- +{method_stubs} +}}; + +FOURDST_DECLARE_PLUGIN({class_name}, "{project_name}", "0.1.0"); // Version can be static or dynamic \ No newline at end of file diff --git a/meson.build b/meson.build index 99b2dc2..f3e0091 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('fourdst', 'cpp', version: 'v0.5.3', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0') +project('fourdst', 'cpp', version: 'v0.6.0', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0') add_project_arguments('-fvisibility=default', language: 'cpp') diff --git a/pyproject.toml b/pyproject.toml index 9764b1f..0a4cb29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "mesonpy" [project] name = "fourdst" # Choose your Python package name -version = "0.5.0" # Your project's version +version = "0.6.0" # Your project's version description = "Python interface to the utility fourdst modules from the 4D-STAR project" readme = "readme.md" license = { file = "LICENSE.txt" } # Reference your license file [cite: 2] @@ -19,3 +19,13 @@ authors = [ maintainers = [ {name = "Emily M. Boudreaux", email = "emily@boudreauxmail.com"} ] + +dependencies = [ + "typer[all]", + "libclang", + "questionary", +] + +[project.scripts] +fourdst-cli = "fourdst.cli.main:app" + diff --git a/subprojects/libplugin.wrap b/subprojects/libplugin.wrap new file mode 100644 index 0000000..14944eb --- /dev/null +++ b/subprojects/libplugin.wrap @@ -0,0 +1,4 @@ +[wrap-git] +url = https://github.com/4D-STAR/libplugin.git +revision = v1.1.2 +depth = 1