diff --git a/CHANGELOG.md b/CHANGELOG.md index debb737..22b2801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,3 +12,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial creation of API - Initial spec for manifest defintion +- Initial creation of CLI tool diff --git a/README.md b/README.md index b3e36b3..79dabb9 100644 --- a/README.md +++ b/README.md @@ -24,27 +24,68 @@ Quick Start Installation: -.. code-block:: bash +```bash +# For UV Based projects +uv add fastsandpm - # For UV Based projects - uv add fastsandpm +# For pip-based projects +pip install fastsandpm +``` - # For pip-based projects - pip install fastsandpm +### Command Line Usage -Basic Usage: +The simplest way to use FastSandPM is via the `fspm` command: - >>> import pathlib - >>> import fastsandpm - >>> manifest = fastsandpm.get_manifest("./my-project") - >>> print(manifest.package.name) - 'my-package' - >>> resolved = fastsandpm.dependencies.resolve(manifest) - >>> print(type(resolved)) - - >>> build_library(resolved, pathlib.Path("my-library")) +```bash +# Install dependencies from proj.toml in current or parent directory +fspm + +# Install from a specific manifest file +fspm --manifest /path/to/proj.toml + +# Install to a custom output directory +fspm --output ./vendor + +# Install with optional dependency groups +fspm --optional dev,test + +# Clean conflicting directories during installation +fspm --clean +``` + +#### CLI Options + +| Option | Description | +|--------|-------------| +| `-m, --manifest PATH` | Path to manifest file or directory (default: search up tree for `proj.toml`) | +| `-o, --output PATH` | Output directory for installed libraries (default: `./lib`) | +| `-c, --clean` | Clean conflicting directories during installation | +| `--no-clean` | Don't clean conflicting directories (default) | +| `--optional GROUPS` | Comma-separated list of optional dependency groups | +| `-v, --verbose` | Increase verbosity (can stack: `-v`, `-vv`, `-vvv`) | +| `-q, --quiet` | Suppress all output except errors | +| `-V, --version` | Show version and exit | + +### Python API Usage + +```python +import pathlib +import fastsandpm + +# Load a manifest +manifest = fastsandpm.get_manifest("./my-project") +print(manifest.package.name) +# 'my-package' + +# Resolve dependencies +resolved = fastsandpm.dependencies.resolve(manifest) + +# Build the library +fastsandpm.build_library(resolved, pathlib.Path("my-library")) +``` This will bring in the library dependencies for a project into the specified directory. -Additionally, a 'dependencies.f' file will be created which will point to the dependencies file list in the required order. +Additionally, a `library.f` file will be created which will point to the dependencies +file list in the required order. For more examples, see [the docs](https://fastsandpm.readthedocs.io/en/latest/usage_guide/index.html) for details. diff --git a/docs/source/index.rst b/docs/source/index.rst index 85ced4a..84b3736 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,7 +31,24 @@ Installation: # For pip-based projects pip install fastsandpm -Basic Usage: +Command Line Usage: + +The simplest way to use FastSandPM is via the ``fspm`` command: + +.. code-block:: bash + + # Install dependencies from proj.toml in current or parent directory + fspm + + # Install to a custom directory + fspm --output ./vendor + + # Install with optional dependency groups + fspm --optional dev,test + +See :doc:`usage_guide/cli` for the complete CLI reference. + +Python API Usage: >>> import pathlib >>> import fastsandpm @@ -41,10 +58,11 @@ Basic Usage: >>> resolved = fastsandpm.dependencies.resolve(manifest) >>> print(type(resolved)) - >>> build_library(resolved, pathlib.Path("my-library")) + >>> fastsandpm.build_library(resolved, pathlib.Path("my-library")) This will bring in the library dependencies for a project into the specified directory. -Additionally, a 'dependencies.f' file will be created which will point to the dependencies file list in the required order. +Additionally, a ``library.f`` file will be created which will point to the dependencies +file list in the required order. For more examples, see the :doc:`usage_guide/index`. diff --git a/docs/source/usage_guide/cli.rst b/docs/source/usage_guide/cli.rst new file mode 100644 index 0000000..f04f65d --- /dev/null +++ b/docs/source/usage_guide/cli.rst @@ -0,0 +1,193 @@ +Command Line Interface +====================== + +FastSandPM provides the ``fspm`` command-line tool for managing HDL/RTL library +dependencies directly from your terminal. + +Basic Usage +----------- + +The simplest way to install dependencies is to run ``fspm`` from a directory +containing (or with a parent containing) a ``proj.toml`` manifest file: + +.. prompt:: bash + + fspm + +This will: + +1. Search up the directory tree to find a ``proj.toml`` manifest file +2. Resolve all dependencies specified in the manifest +3. Install them to the ``./lib`` directory +4. Create a ``library.f`` file listing all dependencies in the correct order + +Command Reference +----------------- + +.. autoprogram:: fastsandpm.cli:create_parser() + :prog: fspm + +Examples +-------- + +Install from Current Directory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Search for ``proj.toml`` in the current directory or any parent directory, +then install dependencies to ``./lib``: + +.. prompt:: bash + + fspm + +Specify a Manifest File +~~~~~~~~~~~~~~~~~~~~~~~ + +Install dependencies from a specific manifest file: + +.. prompt:: bash + + fspm --manifest /path/to/my-project/proj.toml + +You can also specify a directory containing a ``proj.toml``: + +.. prompt:: bash + + fspm --manifest /path/to/my-project + +Custom Output Directory +~~~~~~~~~~~~~~~~~~~~~~~ + +Install dependencies to a custom directory: + +.. prompt:: bash + + fspm --output ./vendor + fspm -o /absolute/path/to/libs + +Install Optional Dependencies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install optional dependency groups defined in your manifest: + +.. prompt:: bash + + # Install the 'dev' optional group + fspm --optional dev + + # Install multiple optional groups + fspm --optional dev,test,simulation + +Clean Installation +~~~~~~~~~~~~~~~~~~ + +By default, ``fspm`` will not overwrite directories that have local changes +or are in an unexpected state. Use the ``--clean`` flag to force replacement: + +.. prompt:: bash + + # Clean conflicting directories during installation + fspm --clean + + # Explicitly disable cleaning (default behavior) + fspm --no-clean + +.. warning:: + + The ``--clean`` flag will delete directories with uncommitted changes. + Make sure to commit or backup any local modifications before using this flag. + +Verbose Output +~~~~~~~~~~~~~~ + +Increase logging verbosity for debugging: + +.. prompt:: bash + + # Show INFO level messages + fspm -v + + # Show DEBUG level messages + fspm -vv + + # Maximum verbosity + fspm -vvv + +Quiet Mode +~~~~~~~~~~ + +Suppress all output except errors: + +.. prompt:: bash + + fspm --quiet + +Version Information +~~~~~~~~~~~~~~~~~~~ + +Display the installed version: + +.. prompt:: bash + + fspm --version + +Exit Codes +---------- + +The ``fspm`` command returns the following exit codes: + +.. list-table:: + :header-rows: 1 + :widths: 10 90 + + * - Code + - Meaning + * - 0 + - Success - all dependencies installed successfully + * - 1 + - Error - manifest not found, parse error, or installation failure + +Typical Workflow +---------------- + +A typical workflow for using ``fspm`` in an HDL/RTL project: + +1. **Create a manifest file** (``proj.toml``) in your project root: + + .. code-block:: toml + + [package] + name = "my-rtl-project" + version = "1.0.0" + description = "My RTL design project" + + [dependencies] + uvm = { git = "https://github.com/accellera/uvm.git", tag = "1800.2-2020-2.0" } + my-lib = "^1.0.0" + + [optional_dependencies.sim] + vip-axi = "2.0.0" + +2. **Install dependencies**: + + .. prompt:: bash + + fspm + +3. **Include the library** in your simulation by referencing ``lib/library.f``: + + .. code-block:: bash + + vcs -f lib/library.f -f my_project.f + +4. **Update dependencies** after modifying the manifest: + + .. prompt:: bash + + fspm --clean + +See Also +-------- + +- :doc:`index` - General usage guide +- :doc:`../manifest_reference/index` - Manifest file format reference diff --git a/docs/source/usage_guide/index.rst b/docs/source/usage_guide/index.rst index 0a1aefc..fc59536 100644 --- a/docs/source/usage_guide/index.rst +++ b/docs/source/usage_guide/index.rst @@ -20,9 +20,45 @@ Or with pip: pip install fastsandpm -Basic Usage ------------ +Command Line Interface +---------------------- -.. NOTE:: +FastSandPM provides the ``fspm`` command for quick dependency management: - TODO: Work in progress +.. code-block:: bash + + # Install dependencies from proj.toml + fspm + + # Install to a custom directory + fspm --output ./vendor + + # Install with optional dependencies + fspm --optional dev,test + +See :doc:`cli` for the complete CLI reference. + +Python API +---------- + +For programmatic usage, fastsandpm provides a Python API: + +.. code-block:: python + + import pathlib + import fastsandpm + + # Load a manifest + manifest = fastsandpm.get_manifest("./my-project") + + # Resolve dependencies + resolved = fastsandpm.dependencies.resolve(manifest) + + # Build the library + fastsandpm.build_library(resolved, pathlib.Path("lib")) + +.. toctree:: + :maxdepth: 2 + :caption: Contents + + cli diff --git a/pyproject.toml b/pyproject.toml index 104c716..23db016 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,9 @@ typing=[ "types-pyyaml>=6.0.12.20250516", ] +[project.scripts] +fspm = "fastsandpm.cli:main" + [project.urls] Homepage = "https://github.com/RISC-Lib/fastsandpm" Documentation = "https://fastsandpm.readthedocs.io/en/latest/index.html" diff --git a/src/fastsandpm/__main__.py b/src/fastsandpm/__main__.py new file mode 100644 index 0000000..27eeae7 --- /dev/null +++ b/src/fastsandpm/__main__.py @@ -0,0 +1,25 @@ +#################################################################################################### +# FastSandPM is a package management and dependency resolution tool for HDL Design and DV projects +# Copyright (C) 2026, Benjamin Davis +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, see +# . +#################################################################################################### + +from __future__ import annotations + +from fastsandpm.cli import main + +if __name__ == "__main__": + main() diff --git a/src/fastsandpm/cli.py b/src/fastsandpm/cli.py new file mode 100644 index 0000000..1f2a935 --- /dev/null +++ b/src/fastsandpm/cli.py @@ -0,0 +1,302 @@ +#################################################################################################### +# FastSandPM is a package management and dependency resolution tool for HDL Design and DV projects +# Copyright (C) 2026, Benjamin Davis +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, see +# . +#################################################################################################### +"""Command-line interface for FastSandPM. + +This module provides the main entry point for the ``fspm`` command-line tool. +It handles dependency resolution and installation for HDL/RTL projects. + +Usage:: + + fspm [OPTIONS] + +Options: + +- ``-m, --manifest PATH``: Path to manifest file (default: search up directory tree) +- ``-o, --output PATH``: Output directory for installed libraries (default: ./lib) +- ``-c, --clean``: Clean conflicting directories during installation +- ``--no-clean``: Don't clean conflicting directories (default) +- ``--optional GROUPS``: Comma-separated list of optional dependency groups to install + +Example:: + + # Install dependencies from proj.toml in current or parent directory + fspm + + # Install from a specific manifest file + fspm --manifest /path/to/proj.toml + + # Install to a custom output directory + fspm --output ./vendor + + # Install with optional dependencies + fspm --optional dev,test + + # Clean conflicting directories + fspm --clean +""" + +from __future__ import annotations + +import argparse +import logging +import pathlib +import sys + +from fastsandpm._info import __version__ +from fastsandpm.install import library_from_manifest +from fastsandpm.manifest import ( + MANIFEST_FILENAME, + ManifestNotFoundError, + ManifestParseError, + get_manifest, +) + + +def find_manifest(start_path: pathlib.Path | None = None) -> pathlib.Path: + """Search up the directory tree to find a manifest file. + + Starting from the given path (or current working directory if not provided), + searches up through parent directories until a proj.toml file is found. + + Args: + start_path: The directory to start searching from. Defaults to cwd. + + Returns: + The path to the directory containing the manifest file. + + Raises: + ManifestNotFoundError: If no manifest file is found in any parent directory. + """ + if start_path is None: + start_path = pathlib.Path.cwd() + + current = start_path.resolve() + + # Search up the directory tree + while True: + manifest_path = current / MANIFEST_FILENAME + if manifest_path.exists() and manifest_path.is_file(): + return current + + # Move to parent directory + parent = current.parent + if parent == current: + # Reached root without finding manifest + raise ManifestNotFoundError(start_path) + + current = parent + + +def create_parser() -> argparse.ArgumentParser: + """Create the argument parser for the fspm CLI. + + Returns: + Configured ArgumentParser instance. + """ + parser = argparse.ArgumentParser( + prog="fspm", + description="FastSandPM - Package manager for HDL Design and DV projects", + epilog="For more information, visit https://fastsandpm.readthedocs.io/", + ) + + parser.add_argument( + "-V", + "--version", + action="version", + version=f"%(prog)s {__version__}", + ) + + parser.add_argument( + "-m", + "--manifest", + type=pathlib.Path, + metavar="PATH", + help="Path to manifest file or directory containing it " + f"(default: search up directory tree for {MANIFEST_FILENAME})", + ) + + parser.add_argument( + "-o", + "--output", + type=pathlib.Path, + default=pathlib.Path("lib"), + metavar="PATH", + help="Output directory for installed libraries (default: ./lib)", + ) + + # Use mutually exclusive group for clean flags + clean_group = parser.add_mutually_exclusive_group() + clean_group.add_argument( + "-c", + "--clean", + action="store_true", + dest="clean", + help="Clean conflicting directories during installation", + ) + clean_group.add_argument( + "--no-clean", + action="store_false", + dest="clean", + help="Don't clean conflicting directories (default)", + ) + parser.set_defaults(clean=False) + + parser.add_argument( + "--optional", + type=str, + metavar="GROUPS", + help="Comma-separated list of optional dependency groups to install", + ) + + parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="Increase verbosity (can be used multiple times: -v, -vv, -vvv)", + ) + + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="Suppress all output except errors", + ) + + return parser + + +def parse_args(args: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments. + + Args: + args: Command-line arguments to parse. Defaults to sys.argv[1:]. + + Returns: + Parsed arguments namespace. + """ + parser = create_parser() + try: + import argcomplete # type: ignore + argcomplete.autocomplete(parser) + except ImportError: + pass + + return parser.parse_args(args) + + +def setup_logging(verbose: int, quiet: bool) -> None: + """Configure logging based on verbosity level. + + Args: + verbose: Verbosity level (0=WARNING, 1=INFO, 2+=DEBUG). + quiet: If True, only show ERROR level messages. + """ + if quiet: + level = logging.ERROR + elif verbose == 0: + level = logging.WARNING + elif verbose == 1: + level = logging.INFO + else: + level = logging.DEBUG + + logging.basicConfig( + level=level, + format="%(levelname)s: %(message)s", + ) + + +def main(args: list[str] | None = None) -> int: + """Main entry point for the fspm CLI. + + Args: + args: Command-line arguments. Defaults to sys.argv[1:]. + + Returns: + Exit code (0 for success, non-zero for errors). + """ + parsed_args = parse_args(args) + setup_logging(parsed_args.verbose, parsed_args.quiet) + + logger = logging.getLogger(__name__) + + # Find or resolve manifest path + try: + if parsed_args.manifest is not None: + manifest_path = parsed_args.manifest.resolve() + # If user provided a file path, use its parent directory + if manifest_path.is_file(): + manifest_path = manifest_path.parent + else: + # Search up the directory tree for manifest + manifest_path = find_manifest() + logger.info("Found manifest at %s", manifest_path / MANIFEST_FILENAME) + except ManifestNotFoundError as e: + logger.error( + "No %s found in current directory or any parent directory. " + "Use --manifest to specify a path.", + MANIFEST_FILENAME, + ) + logger.debug("Search started from: %s", e.path) + return 1 + + # Load and parse the manifest + try: + manifest = get_manifest(manifest_path) + logger.info( + "Loaded manifest for %s version %s", + manifest.package.name, + manifest.package.version, + ) + except ManifestNotFoundError: + logger.error("Manifest file not found at %s", manifest_path / MANIFEST_FILENAME) + return 1 + except ManifestParseError as e: + logger.error("Failed to parse manifest: %s", e.reason) + return 1 + + # Parse optional dependencies + optional_deps: list[str] | None = None + if parsed_args.optional: + optional_deps = [g.strip() for g in parsed_args.optional.split(",") if g.strip()] + logger.info("Including optional dependency groups: %s", ", ".join(optional_deps)) + + # Resolve output path + output_path = parsed_args.output.resolve() + logger.info("Installing dependencies to %s", output_path) + + # Install dependencies + try: + library_from_manifest( + manifest=manifest, + dest=output_path, + optional_deps=optional_deps, + clean=parsed_args.clean, + ) + logger.info("Successfully installed dependencies") + except NotImplementedError as e: + logger.error("%s", e) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main())