Add recursive dependency downloading and directory scanning

- Implement recursive downloading of nested dependencies from ZON files
- Enhance CLI to support downloading from directories containing `build.zig.zon` files
- Update README with new usage instructions and command options
- Refactor dependency processing to handle both single files and directories
This commit is contained in:
2025-03-19 11:22:46 +08:00
parent 09cbad23d6
commit cc0d988012
3 changed files with 189 additions and 21 deletions

View File

@ -7,6 +7,8 @@ A Python utility for working with Zig package manager files and Zig Object Notat
- Parse ZON files into Python dictionaries - Parse ZON files into Python dictionaries
- Convert ZON files to JSON - Convert ZON files to JSON
- Download and extract dependencies from ZON files - Download and extract dependencies from ZON files
- Recursively download nested dependencies
- Scan directories for `build.zig.zon` files
## Installation ## Installation
@ -59,13 +61,27 @@ The package provides a command-line interface with the following commands:
#### Download Dependencies #### Download Dependencies
Download and extract dependencies from a ZON file: Download dependencies from a ZON file or directory:
```bash ```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 #### Convert ZON to JSON

View File

@ -88,15 +88,28 @@ def cli(ctx: click.Context, verbose: bool):
@cli.command() @cli.command()
@click.argument("zon_file", type=click.Path(exists=True, readable=True, path_type=Path)) @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 @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}") 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: if dependencies:
logger.info(f"Successfully processed {len(dependencies)} dependencies:") logger.info(f"Successfully processed {len(dependencies)} dependencies:")

View File

@ -5,10 +5,12 @@ This module handles downloading and extracting dependencies specified in ZON fil
""" """
import shutil import shutil
import click
import sys
import tarfile import tarfile
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Optional from typing import Dict, Any, Optional, Set, List
import httpx import httpx
from loguru import logger from loguru import logger
@ -133,18 +135,85 @@ def process_dependency(
return None 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. Process all dependencies from a ZON file.
Args: 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: Returns:
Dictionary mapping dependency names to their extracted paths 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 # 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 # Get the dependencies section
dependencies = zon_data.get("dependencies", {}) dependencies = zon_data.get("dependencies", {})
@ -157,23 +226,79 @@ def process_dependencies(zon_file_path: str) -> Dict[str, Path]:
# Process each dependency # Process each dependency
result = {} result = {}
processed_paths = set()
for name, dep_info in dependencies.items(): for name, dep_info in dependencies.items():
path = process_dependency(name, dep_info, cache_dir) path = process_dependency(name, dep_info, cache_dir)
if path: if path:
result[name] = 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 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. Main entry point for the dependency downloader.
Args: 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}") 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: if dependencies:
logger.info(f"Successfully processed {len(dependencies)} 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") 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__": if __name__ == "__main__":
import sys cli() # pylint: disable=no-value-for-parameter
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <zon_file_path>")
sys.exit(1)
main(sys.argv[1])