Enhance build command with config options and error handling; add builtins template
This commit is contained in:
parent
0a71a35b44
commit
7935eb6e25
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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}")
|
|
||||||
|
|
||||||
for item in listdir(path):
|
def _format_to_filename(name: str) -> str:
|
||||||
item_path = path.join(path, item)
|
formatted = name.lower().replace(" ", "_")
|
||||||
if path.isdir(item_path):
|
if not formatted.endswith(".nutsi"):
|
||||||
rmdir(item_path)
|
formatted += ".nutsi"
|
||||||
else:
|
return formatted
|
||||||
remove(item_path)
|
|
||||||
else:
|
def _clear_directory(directory: Path) -> None:
|
||||||
makedirs(path)
|
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"
|
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
|
||||||
|
|
||||||
|
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...")
|
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.")
|
|
||||||
|
|||||||
@ -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 _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():
|
def _read_string(mapping: Dict[str, Any], key: str, default: str) -> str:
|
||||||
return Config("Nutfile")
|
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,
|
||||||
|
)
|
||||||
|
|||||||
0
nut/templates/builtins/__init__.py
Normal file
0
nut/templates/builtins/__init__.py
Normal file
Loading…
x
Reference in New Issue
Block a user