nut/chipnut/commands/compiler.py
2026-04-04 12:02:15 +02:00

152 lines
4.5 KiB
Python

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