from dataclasses import dataclass from pathlib import Path from typing import Optional import os import shutil from ..cli import CLI from ..config import Config, ConfigError, load_config from ..paths import get_templates_path from ..utils import zip_dir @dataclass(frozen=True) class CompileContext: config: Config project_root: Path build_dir: Path entry_path: Path output_name: str class Compiler: def __init__(self, context: CompileContext): self.context = context def run(self) -> None: return None def _resolve_project_path(project_root: Path, value: Path) -> Path: if value.is_absolute(): return value return (project_root / value).resolve() def _format_to_filename(name: str) -> str: formatted = name.strip().lower().replace(" ", "_") if not formatted.endswith(".nutsi"): formatted += ".nutsi" return formatted def _clear_directory(directory: Path) -> None: for child in directory.iterdir(): if child.is_dir(): shutil.rmtree(child) else: child.unlink() def _is_chipnut_workspace(path: str = ".") -> bool: return os.path.isfile(os.path.join(path, "Nutfile")) def prepare_build_directory(build_dir: Path) -> Path: if build_dir.exists() and not build_dir.is_dir(): raise RuntimeError(f"Build path exists but is not a directory: {build_dir}") build_dir.mkdir(parents=True, exist_ok=True) _clear_directory(build_dir) builtins_path = get_templates_path() / "builtins" if not builtins_path.is_dir(): raise RuntimeError( "Builtins path does not exist: " f"{builtins_path}. Installation may be corrupted." ) shutil.copytree( builtins_path, build_dir / "builtins", dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), ) return build_dir def stage_project_sources(project_root: Path, entry_path: Path, build_dir: Path) -> None: if entry_path.is_absolute(): raise RuntimeError( f"build.entry must be a relative path, got absolute path: {entry_path}" ) source_entry = (project_root / entry_path).resolve() if not source_entry.is_file(): raise RuntimeError( f"build.entry does not point to a file: {entry_path} (resolved: {source_entry})" ) entry_parent = entry_path.parent if str(entry_parent) == ".": destination_file = build_dir / source_entry.name destination_file.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source_entry, destination_file) return source_dir = (project_root / entry_parent).resolve() destination_dir = build_dir / entry_parent shutil.copytree( source_dir, destination_dir, dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.nutsi"), ) def generate_nutsi_file(build_dir: Path, file_name: str) -> Path: output_path = build_dir / file_name zip_dir( str(build_dir), str(output_path), ignore_patterns=[ "__pycache__", "*.pyc", "*.nutsi", ], ) return output_path def _create_context(config: Config, build_folder: Optional[str], file_name: Optional[str]) -> CompileContext: project_root = config.path.parent.resolve() entry_path = Path(config.build.entry) selected_folder = build_folder or config.build.build_folder build_dir = _resolve_project_path(project_root, Path(selected_folder)) selected_name = file_name or config.build.name output_name = _format_to_filename(selected_name) if build_dir == project_root: raise RuntimeError("buildFolder cannot point to project root") return CompileContext( config=config, project_root=project_root, build_dir=build_dir, entry_path=entry_path, output_name=output_name, ) def compile(config: Config, build_folder: Optional[str] = None, file_name: Optional[str] = None) -> Path: context = _create_context(config, build_folder=build_folder, file_name=file_name) prepared_dir = prepare_build_directory(context.build_dir) stage_project_sources(context.project_root, context.entry_path, prepared_dir) Compiler(context).run() return generate_nutsi_file(prepared_dir, context.output_name) def main(args, flags) -> int: config_path = flags["config"] build_folder_override = flags["build_folder"] file_name_override = flags["name"] if config_path == "Nutfile" and not _is_chipnut_workspace(): print("Error: No Nutfile found. Please run 'chipnut init' to create a workspace.") return 1 try: config = load_config(config_path) except FileNotFoundError: print(f"Error: Config file not found: {config_path}") return 1 except ConfigError as error: print(f"Error: Invalid Nutfile: {error}") return 1 try: compile( config, build_folder=build_folder_override, file_name=file_name_override, ) except (RuntimeError, OSError) as error: print(f"Error: Build failed: {error}") return 1 return 0 def setup(cli: CLI): cli.command( "build", help="Compile the project", description=( "Compile the project according to the configuration in the Nutfile. " ), ).option( "config", "c", default="Nutfile", help="Path to Nutfile (default: ./Nutfile)", ).option( "build-folder", "o", default=None, help="Override output build folder from config", ).option( "name", "n", default=None, help="Override output file name from config", ).handle(main)