from dataclasses import dataclass from pathlib import Path from typing import Optional import shutil from ..config import Config from ..paths import get_templates_path from ..utils import zip_dir from ..logger import logger @dataclass(frozen=True) class CompileContext: config: Config project_root: Path build_dir: Path entry_path: Path output_name: str _ACTIVE_CONTEXT: Optional[CompileContext] = None class Compiler: def run(self) -> None: return None def get_compile_context() -> CompileContext: if _ACTIVE_CONTEXT is None: raise RuntimeError("No active compile context. Call only during compile().") return _ACTIVE_CONTEXT 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 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: global _ACTIVE_CONTEXT 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) _ACTIVE_CONTEXT = context try: Compiler().run() finally: _ACTIVE_CONTEXT = None archive_path = generate_nutsi_file(prepared_dir, context.output_name) return archive_path