diff --git a/cppython/console/entry.py b/cppython/console/entry.py index 6839e2e..929d6de 100644 --- a/cppython/console/entry.py +++ b/cppython/console/entry.py @@ -1,5 +1,6 @@ """A Typer CLI for CPPython interfacing""" +from importlib.metadata import entry_points from pathlib import Path from typing import Annotated @@ -14,6 +15,12 @@ app = typer.Typer(no_args_is_help=True) +info_app = typer.Typer(no_args_is_help=True, help='Prints project information including plugin configuration, managed files, and templates.') +app.add_typer(info_app, name='info') + +list_app = typer.Typer(no_args_is_help=True, help='List project entities.') +app.add_typer(list_app, name='list') + def get_enabled_project(context: typer.Context) -> Project: """Helper to load and validate an enabled Project from CLI context.""" @@ -123,43 +130,62 @@ def main( context.obj = ConsoleConfiguration(project_configuration=project_configuration, interface=interface) -@app.command() -def info( +def _print_plugin_report(role: str, name: str, report: PluginReport) -> None: + """Print a single plugin's report to the console. + + Args: + role: The plugin role label (e.g. 'Provider', 'Generator') + name: The plugin name + report: The plugin report to display + """ + print(f'\n[bold]{role}:[/bold] {name}') + + if report.configuration: + print(' [bold]Configuration:[/bold]') + for key, value in report.configuration.items(): + print(f' {key}: {value}') + + if report.managed_files: + print(' [bold]Managed files:[/bold]') + for file_path in report.managed_files: + print(f' {file_path}') + + if report.template_files: + print(' [bold]Templates:[/bold]') + for filename, content in report.template_files.items(): + print(f' [cyan]{filename}[/cyan]') + print() + print(Syntax(content, 'python', theme='monokai', line_numbers=True)) + + +@info_app.command() +def info_provider( context: typer.Context, ) -> None: - """Prints project information including plugin configuration, managed files, and templates.""" + """Show provider plugin information.""" project = get_enabled_project(context) project_info = project.info() - if not project_info: + entry = project_info.get('provider') + if entry is None: return - for role in ('provider', 'generator'): - entry = project_info.get(role) - if entry is None: - continue - - name: str = entry['name'] - report: PluginReport = entry['report'] + _print_plugin_report('Provider', entry['name'], entry['report']) - print(f'\n[bold]{role.title()}:[/bold] {name}') - if report.configuration: - print(' [bold]Configuration:[/bold]') - for key, value in report.configuration.items(): - print(f' {key}: {value}') +@info_app.command() +def info_generator( + context: typer.Context, +) -> None: + """Show generator plugin information.""" + project = get_enabled_project(context) + project_info = project.info() - if report.managed_files: - print(' [bold]Managed files:[/bold]') - for path in report.managed_files: - print(f' {path}') + entry = project_info.get('generator') + if entry is None: + return - if report.template_files: - print(' [bold]Templates:[/bold]') - for filename, content in report.template_files.items(): - print(f' [cyan]{filename}[/cyan]') - print() - print(Syntax(content, 'python', theme='monokai', line_numbers=True)) + _print_plugin_report('Generator', entry['name'], entry['report']) @app.command() @@ -218,11 +244,40 @@ def update( project.update(groups=group_list) -@app.command(name='list') -def list_command( - _: typer.Context, +@list_app.command() +def plugins() -> None: + """List all installed CPPython plugins.""" + groups = { + 'Generators': 'cppython.generator', + 'Providers': 'cppython.provider', + 'SCM': 'cppython.scm', + } + + for label, group in groups.items(): + entries = entry_points(group=group) + print(f'\n[bold]{label}:[/bold]') + if not entries: + print(' (none installed)') + else: + for ep in sorted(entries, key=lambda e: e.name): + print(f' {ep.name}') + + +@list_app.command() +def targets( + context: typer.Context, ) -> None: - """Prints project information""" + """List discovered build targets.""" + project = get_enabled_project(context) + target_list = project.list_targets() + + if not target_list: + print('[dim]No targets found. Have you run install and build?[/dim]') + return + + print('\n[bold]Targets:[/bold]') + for target_name in sorted(target_list): + print(f' {target_name}') @app.command() diff --git a/cppython/core/plugin_schema/generator.py b/cppython/core/plugin_schema/generator.py index 9e170bd..ab4b58d 100644 --- a/cppython/core/plugin_schema/generator.py +++ b/cppython/core/plugin_schema/generator.py @@ -109,3 +109,12 @@ def run(self, target: str, configuration: str | None = None) -> None: configuration: Optional named configuration override. """ raise NotImplementedError + + @abstractmethod + def list_targets(self) -> list[str]: + """Lists discovered build targets/executables. + + Returns: + A list of target names found in the build directory. + """ + raise NotImplementedError diff --git a/cppython/plugins/cmake/plugin.py b/cppython/plugins/cmake/plugin.py index f082225..3798fb2 100644 --- a/cppython/plugins/cmake/plugin.py +++ b/cppython/plugins/cmake/plugin.py @@ -182,6 +182,29 @@ def run(self, target: str, configuration: str | None = None) -> None: executable = executables[0] subprocess.run([str(executable)], check=True, cwd=self.data.preset_file.parent) + def list_targets(self) -> list[str]: + """Lists discovered build targets/executables in the CMake build directory. + + Searches the build directory for executable files, excluding common + non-target files. + + Returns: + A sorted list of unique target names found. + """ + build_path = self.core_data.cppython_data.build_path + + if not build_path.exists(): + return [] + + # Collect executable files from the build directory + targets: set[str] = set() + for candidate in build_path.rglob('*'): + if candidate.is_file() and (candidate.stat().st_mode & 0o111 or candidate.suffix == '.exe'): + # Use the stem (name without extension) as the target name + targets.add(candidate.stem) + + return sorted(targets) + def plugin_info(self) -> PluginReport: """Return a report describing the CMake generator's configuration and managed files. diff --git a/cppython/plugins/meson/plugin.py b/cppython/plugins/meson/plugin.py index e66abe9..1b666e1 100644 --- a/cppython/plugins/meson/plugin.py +++ b/cppython/plugins/meson/plugin.py @@ -194,3 +194,23 @@ def run(self, target: str, configuration: str | None = None) -> None: executable = executables[0] subprocess.run([str(executable)], check=True, cwd=self.data.build_file.parent) + + def list_targets(self) -> list[str]: + """Lists discovered build targets/executables in the Meson build directory. + + Searches the build directory for executable files. + + Returns: + A sorted list of unique target names found. + """ + build_dir = self._build_dir() + + if not build_dir.exists(): + return [] + + targets: set[str] = set() + for candidate in build_dir.rglob('*'): + if candidate.is_file() and (candidate.stat().st_mode & 0o111 or candidate.suffix == '.exe'): + targets.add(candidate.stem) + + return sorted(targets) diff --git a/cppython/project.py b/cppython/project.py index 6a5f56a..9c239c1 100644 --- a/cppython/project.py +++ b/cppython/project.py @@ -268,3 +268,16 @@ def run(self, target: str, configuration: str | None = None) -> None: self.logger.info('Running target: %s', target) self._data.sync() self._data.plugins.generator.run(target, configuration=configuration) + + def list_targets(self) -> list[str]: + """Lists discovered build targets/executables. + + Returns: + A list of target names found in the build directory, or an empty list + if the project is not enabled. + """ + if not self._enabled: + self.logger.info('Skipping list_targets because the project is not enabled') + return [] + + return self._data.plugins.generator.list_targets() diff --git a/cppython/schema.py b/cppython/schema.py index 522d9e3..59d1bc8 100644 --- a/cppython/schema.py +++ b/cppython/schema.py @@ -71,3 +71,12 @@ def run(self, target: str, configuration: str | None = None) -> None: configuration: Optional named configuration to use. Interpretation is generator-specific. """ raise NotImplementedError() + + @abstractmethod + def list_targets(self) -> list[str]: + """Lists discovered build targets/executables. + + Returns: + A list of target names found in the build directory. + """ + raise NotImplementedError() diff --git a/cppython/test/mock/generator.py b/cppython/test/mock/generator.py index 536b3b6..48e0ede 100644 --- a/cppython/test/mock/generator.py +++ b/cppython/test/mock/generator.py @@ -72,3 +72,7 @@ def bench(self, configuration: str | None = None) -> None: def run(self, target: str, configuration: str | None = None) -> None: """No-op run for testing""" + + def list_targets(self) -> list[str]: + """No-op list_targets for testing""" + return []