Enhance build command with config options and error handling; add builtins template

This commit is contained in:
Chipperfluff 2026-04-03 19:33:02 +02:00
parent 0a71a35b44
commit 7935eb6e25
4 changed files with 152 additions and 56 deletions

View File

@ -1,9 +1,9 @@
from .cli import CLI
from .config import load_config
from .config import ConfigError, load_config
from .commands.setup import is_nut_workspace, find_template_conflicts, copy_template_files
from .commands.compiler import compile
from .commands.compiler import compile as compile_project
def help_text():
return [
@ -37,17 +37,48 @@ def main() -> int:
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(cmd_build)
return cli.run()
def cmd_build(args, flags) -> int:
if not is_nut_workspace():
config_path = flags["config"]
build_folder_override = flags["build_folder"]
file_name_override = flags["name"]
if config_path == "Nutfile" and not is_nut_workspace():
print("Error: No Nutfile found. Please run 'nut init' to create a workspace.")
return 1
config = load_config()
compile(config)
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_project(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

View File

@ -1,35 +1,67 @@
from nut.config import Config
from nut.paths import get_templates_path
from os import remove, makedirs, rmdir, listdir
from pathlib import Path
from typing import Optional
import shutil
def prepare_build_directory(path: str) -> None:
if path.exists(path):
if not path.isdir(path):
raise RuntimeError(f"Build path exists but is not a directory: {path}")
from nut.config import Config
from nut.paths import get_templates_path
from nut.utils import zip_dir
for item in listdir(path):
item_path = path.join(path, item)
if path.isdir(item_path):
rmdir(item_path)
def _format_to_filename(name: str) -> str:
formatted = name.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:
remove(item_path)
else:
makedirs(path)
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 path.exists(builtins_path):
raise RuntimeError(f"Builtins path does not exist: {builtins_path}, this should not happen, installation may be corrupted")
if not builtins_path.is_dir():
raise RuntimeError(
"Builtins path does not exist: "
f"{builtins_path}. Installation may be corrupted."
)
shutil.copytree(
builtins_path,
path.join(path, "builtins"),
dirs_exist_ok=True
build_dir / "builtins",
dirs_exist_ok=True,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
)
def compile(config: Config) -> None:
return build_dir
def generate_nutsi_file(build_dir: Path, file_name: str) -> None:
output_path = build_dir / file_name
zip_dir(build_dir, output_path, ignore_patterns=[
"__pycache__",
"*.pyc",
"*.nutsi",
])
def compile(config: Config, build_folder: Optional[str] = None, file_name: Optional[str] = None) -> None:
print("Compiling the project...")
prepare_build_directory(config.buildFolder or "build")
selected_name = _format_to_filename(file_name or config.build.name)
selected_folder = build_folder or config.build.build_folder
prepared_dir = prepare_build_directory(Path(selected_folder))
generate_nutsi_file(prepared_dir, selected_name)
print("Compilation completed successfully.")

View File

@ -1,31 +1,64 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Optional
from .utils import read_yaml
class Node:
def __init__(self, data):
for key, value in data.items():
if isinstance(value, dict):
value = Node(value)
elif isinstance(value, list):
value = [
Node(item) if isinstance(item, dict) else item
for item in value
]
setattr(self, key, value)
class ConfigError(ValueError):
"""Raised when Nutfile contents are invalid."""
def __repr__(self):
return f"<Node {self.__dict__}>"
@dataclass(frozen=True)
class BuildConfig:
name: str
build_folder: str
entry: str
@property
def buildFolder(self) -> str:
# Compatibility alias for existing camelCase references.
return self.build_folder
@dataclass(frozen=True)
class Config:
def __init__(self, path: str):
self.path = path
self.raw = read_yaml(path)
self.root = Node(self.raw)
path: Path
raw: Dict[str, Any]
build: BuildConfig
def __getattr__(self, item):
if not hasattr(self.root, item):
return None
@property
def buildFolder(self) -> str:
# Compatibility alias for older call-sites.
return self.build.build_folder
return getattr(self.root, item)
def _ensure_dict(value: Any, context: str) -> Dict[str, Any]:
if not isinstance(value, dict):
raise ConfigError(f"{context} must be a mapping")
return value
def load_config():
return Config("Nutfile")
def _read_string(mapping: Dict[str, Any], key: str, default: str) -> str:
value = mapping.get(key, default)
if not isinstance(value, str) or not value.strip():
raise ConfigError(f"build.{key} must be a non-empty string")
return value
def load_config(path: str = "Nutfile") -> Config:
config_path = Path(path)
raw_data: Optional[Dict[str, Any]] = read_yaml(str(config_path))
if raw_data is None:
raw_data = {}
raw_dict = _ensure_dict(raw_data, "Nutfile root")
build_section = raw_dict.get("build", {})
build_dict = _ensure_dict(build_section, "build section")
build = BuildConfig(
name=_read_string(build_dict, "name", "Example Build"),
build_folder=_read_string(build_dict, "buildFolder", "build"),
entry=_read_string(build_dict, "entry", "src/main.nut"),
)
return Config(
path=config_path,
raw=raw_dict,
build=build,
)

View File