nut/chipnut/commands/compiler.py

208 lines
5.7 KiB
Python

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)