diff --git a/README.md b/README.md index 1432519..d33f353 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A Python utility for working with Zig package manager files and Zig Object Notat - Parse ZON files into Python dictionaries - Convert ZON files to JSON - Download and extract dependencies from ZON files +- Recursively download nested dependencies +- Scan directories for `build.zig.zon` files ## Installation @@ -59,13 +61,27 @@ The package provides a command-line interface with the following commands: #### Download Dependencies -Download and extract dependencies from a ZON file: +Download dependencies from a ZON file or directory: ```bash -uv run zig-fetch download examples/test.zon +# Download dependencies from a single ZON file +zig-fetch download examples/test.zon + +# Download dependencies from a directory (finds all build.zig.zon files) +zig-fetch download lib/ + +# Download dependencies recursively (finds dependencies of dependencies) +zig-fetch -v download examples/test.zon --recursive + +# Combine directory scanning with recursive downloading +zig-fetch -v download lib/ --recursive ``` -This will download all dependencies specified in the ZON file to `~/.cache/zig/p` and extract them to directories named after their hash values. +Options: +- `--recursive`, `-r`: Recursively process dependencies of dependencies +- `--verbose`, `-v`: Enable verbose logging (on the parent command) + +This will download all dependencies to `~/.cache/zig/p` and extract them to directories named after their hash values. #### Convert ZON to JSON diff --git a/zig_fetch_py/__main__.py b/zig_fetch_py/__main__.py index c2242ce..f578a11 100644 --- a/zig_fetch_py/__main__.py +++ b/zig_fetch_py/__main__.py @@ -88,15 +88,28 @@ def cli(ctx: click.Context, verbose: bool): @cli.command() @click.argument("zon_file", type=click.Path(exists=True, readable=True, path_type=Path)) +@click.option( + "--recursive", + "-r", + is_flag=True, + help="Recursively process dependencies from downloaded artifacts or scan directories", +) @click.pass_context -def download(ctx: click.Context, zon_file: Path): +def download(ctx: click.Context, zon_file: Path, recursive: bool): """ - Download dependencies from a ZON file. + Download dependencies from a ZON file or directory. - ZON_FILE: Path to the ZON file + If ZON_FILE is a directory, all build.zig.zon files will be processed. + If --recursive is specified, all dependencies of dependencies will also be processed. + + ZON_FILE: Path to the ZON file or directory to process """ logger.info(f"Processing dependencies from {zon_file}") - dependencies = process_dependencies(str(zon_file)) + + if zon_file.is_dir(): + logger.info(f"{zon_file} is a directory, searching for build.zig.zon files") + + dependencies = process_dependencies(str(zon_file), recursive=recursive) if dependencies: logger.info(f"Successfully processed {len(dependencies)} dependencies:") diff --git a/zig_fetch_py/downloader.py b/zig_fetch_py/downloader.py index f21f8a5..05fa332 100644 --- a/zig_fetch_py/downloader.py +++ b/zig_fetch_py/downloader.py @@ -5,10 +5,12 @@ This module handles downloading and extracting dependencies specified in ZON fil """ import shutil +import click +import sys import tarfile import tempfile from pathlib import Path -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, Set, List import httpx from loguru import logger @@ -133,18 +135,85 @@ def process_dependency( return None -def process_dependencies(zon_file_path: str) -> Dict[str, Path]: +def find_build_zig_zon_files(directory: Path) -> List[Path]: + """ + Find all build.zig.zon files in a directory and its subdirectories. + + Args: + directory: Directory to search in + + Returns: + List of paths to build.zig.zon files + """ + logger.debug(f"Searching for build.zig.zon files in {directory}") + + zon_files = [] + for zon_file in directory.glob("**/build.zig.zon"): + logger.debug(f"Found build.zig.zon file: {zon_file}") + zon_files.append(zon_file) + + return zon_files + + +def process_dependencies( + zon_file_path: str, recursive: bool = False +) -> Dict[str, Path]: """ Process all dependencies from a ZON file. Args: - zon_file_path: Path to the ZON file + zon_file_path: Path to the ZON file or directory + recursive: Whether to process dependencies recursively Returns: Dictionary mapping dependency names to their extracted paths """ + # Convert to Path object + zon_file = Path(zon_file_path) + + # If the path is a directory, find all build.zig.zon files + if zon_file.is_dir(): + logger.info(f"Processing directory: {zon_file}") + all_zon_files = find_build_zig_zon_files(zon_file) + + if not all_zon_files: + logger.warning(f"No build.zig.zon files found in {zon_file}") + return {} + + # Process all found ZON files + result = {} + for zon_path in all_zon_files: + logger.info(f"Processing {zon_path}") + deps = process_dependencies_from_file(str(zon_path), recursive) + result.update(deps) + + return result + + # Otherwise, process the single ZON file + return process_dependencies_from_file(zon_file_path, recursive) + + +def process_dependencies_from_file( + zon_file_path: str, recursive: bool = False +) -> Dict[str, Path]: + """ + Process all dependencies from a single ZON file. + + Args: + zon_file_path: Path to the ZON file + recursive: Whether to process dependencies recursively + + Returns: + Dictionary mapping dependency names to their extracted paths + """ + logger.info(f"Processing dependencies from file: {zon_file_path}") + # Parse the ZON file - zon_data = parse_zon_file(zon_file_path) + try: + zon_data = parse_zon_file(zon_file_path) + except Exception as e: + logger.error(f"Error parsing {zon_file_path}: {e}") + return {} # Get the dependencies section dependencies = zon_data.get("dependencies", {}) @@ -157,23 +226,79 @@ def process_dependencies(zon_file_path: str) -> Dict[str, Path]: # Process each dependency result = {} + processed_paths = set() + for name, dep_info in dependencies.items(): path = process_dependency(name, dep_info, cache_dir) if path: result[name] = path + processed_paths.add(path) + + # Recursively process dependencies if requested + if recursive and path.exists(): + process_nested_dependencies(path, result, processed_paths) return result -def main(zon_file_path: str) -> None: +def process_nested_dependencies( + dep_path: Path, result: Dict[str, Path], processed_paths: Set[Path] +) -> None: + """ + Process nested dependencies from a dependency directory. + + Args: + dep_path: Path to the dependency directory + result: Dictionary to update with new dependencies + processed_paths: Set of paths that have already been processed + """ + # Find all build.zig.zon files in the dependency directory + zon_files = find_build_zig_zon_files(dep_path) + + if not zon_files: + logger.debug(f"No nested build.zig.zon files found in {dep_path}") + return + + for zon_file in zon_files: + logger.info(f"Processing nested dependency file: {zon_file}") + + # Parse the ZON file + try: + zon_data = parse_zon_file(str(zon_file)) + except Exception as e: + logger.error(f"Error parsing {zon_file}: {e}") + continue + + # Get the dependencies section + dependencies = zon_data.get("dependencies", {}) + if not dependencies: + logger.debug(f"No dependencies found in {zon_file}") + continue + + # Get the cache directory + cache_dir = get_cache_dir() + + # Process each dependency + for name, dep_info in dependencies.items(): + path = process_dependency(name, dep_info, cache_dir) + if path and path not in processed_paths: + result[name] = path + processed_paths.add(path) + + # Recursively process this dependency's dependencies + process_nested_dependencies(path, result, processed_paths) + + +def main(zon_file_path: str, recursive: bool = False) -> None: """ Main entry point for the dependency downloader. Args: - zon_file_path: Path to the ZON file + zon_file_path: Path to the ZON file or directory + recursive: Whether to process dependencies recursively """ logger.info(f"Processing dependencies from {zon_file_path}") - dependencies = process_dependencies(zon_file_path) + dependencies = process_dependencies(zon_file_path, recursive=recursive) if dependencies: logger.info(f"Successfully processed {len(dependencies)} dependencies:") @@ -183,11 +308,25 @@ def main(zon_file_path: str) -> None: logger.warning("No dependencies were processed") +@click.command() +@click.argument("zon_file", type=click.Path(exists=True, readable=True)) +@click.option( + "--recursive", + "-r", + is_flag=True, + help="Recursively process dependencies from downloaded artifacts", +) +@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging") +def cli(zon_file, recursive, verbose): + """Download dependencies from a ZON file.""" + # Set up logging + log_level = "DEBUG" if verbose else "INFO" + logger.remove() + logger.add(sys.stderr, level=log_level) + + # Run the main function + main(zon_file, recursive=recursive) + + if __name__ == "__main__": - import sys - - if len(sys.argv) != 2: - print(f"Usage: {sys.argv[0]} ") - sys.exit(1) - - main(sys.argv[1]) + cli() # pylint: disable=no-value-for-parameter