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 .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.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(): def help_text():
return [ return [
@ -37,17 +37,48 @@ def main() -> int:
description=( description=(
"Compile the project according to the configuration in the Nutfile. " "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) ).handle(cmd_build)
return cli.run() return cli.run()
def cmd_build(args, flags) -> int: 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.") print("Error: No Nutfile found. Please run 'nut init' to create a workspace.")
return 1 return 1
config = load_config() try:
compile(config) 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 return 0

View File

@ -1,35 +1,67 @@
from nut.config import Config from pathlib import Path
from nut.paths import get_templates_path from typing import Optional
from os import remove, makedirs, rmdir, listdir
import shutil import shutil
def prepare_build_directory(path: str) -> None: from nut.config import Config
if path.exists(path): from nut.paths import get_templates_path
if not path.isdir(path): from nut.utils import zip_dir
raise RuntimeError(f"Build path exists but is not a directory: {path}")
def _format_to_filename(name: str) -> str:
for item in listdir(path): formatted = name.lower().replace(" ", "_")
item_path = path.join(path, item) if not formatted.endswith(".nutsi"):
if path.isdir(item_path): formatted += ".nutsi"
rmdir(item_path) return formatted
else:
remove(item_path) def _clear_directory(directory: Path) -> None:
else: for child in directory.iterdir():
makedirs(path) 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" builtins_path = get_templates_path() / "builtins"
if not path.exists(builtins_path): if not builtins_path.is_dir():
raise RuntimeError(f"Builtins path does not exist: {builtins_path}, this should not happen, installation may be corrupted") raise RuntimeError(
"Builtins path does not exist: "
f"{builtins_path}. Installation may be corrupted."
)
shutil.copytree( shutil.copytree(
builtins_path, builtins_path,
path.join(path, "builtins"), build_dir / "builtins",
dirs_exist_ok=True dirs_exist_ok=True,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
) )
def compile(config: Config) -> None: return build_dir
print("Compiling the project...")
def generate_nutsi_file(build_dir: Path, file_name: str) -> None:
prepare_build_directory(config.buildFolder or "build") 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...")
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 from .utils import read_yaml
class Node: class ConfigError(ValueError):
def __init__(self, data): """Raised when Nutfile contents are invalid."""
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)
def __repr__(self): @dataclass(frozen=True)
return f"<Node {self.__dict__}>" 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: class Config:
def __init__(self, path: str): path: Path
self.path = path raw: Dict[str, Any]
self.raw = read_yaml(path) build: BuildConfig
self.root = Node(self.raw)
def __getattr__(self, item): @property
if not hasattr(self.root, item): def buildFolder(self) -> str:
return None # Compatibility alias for older call-sites.
return self.build.build_folder
return getattr(self.root, item)
def load_config(): def _ensure_dict(value: Any, context: str) -> Dict[str, Any]:
return Config("Nutfile") if not isinstance(value, dict):
raise ConfigError(f"{context} must be a mapping")
return value
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