Refactor build command for improved readability; enhance error handling and add logging functionality
This commit is contained in:
parent
7935eb6e25
commit
120647f7ec
@ -75,7 +75,11 @@ def cmd_build(args, flags) -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
compile_project(config, build_folder=build_folder_override, file_name=file_name_override)
|
compile_project(
|
||||||
|
config,
|
||||||
|
build_folder=build_folder_override,
|
||||||
|
file_name=file_name_override,
|
||||||
|
)
|
||||||
except (RuntimeError, OSError) as error:
|
except (RuntimeError, OSError) as error:
|
||||||
print(f"Error: Build failed: {error}")
|
print(f"Error: Build failed: {error}")
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -6,9 +7,35 @@ import shutil
|
|||||||
from nut.config import Config
|
from nut.config import Config
|
||||||
from nut.paths import get_templates_path
|
from nut.paths import get_templates_path
|
||||||
from nut.utils import zip_dir
|
from nut.utils import zip_dir
|
||||||
|
from nut.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:
|
def _format_to_filename(name: str) -> str:
|
||||||
formatted = name.lower().replace(" ", "_")
|
formatted = name.strip().lower().replace(" ", "_")
|
||||||
if not formatted.endswith(".nutsi"):
|
if not formatted.endswith(".nutsi"):
|
||||||
formatted += ".nutsi"
|
formatted += ".nutsi"
|
||||||
return formatted
|
return formatted
|
||||||
@ -20,7 +47,6 @@ def _clear_directory(directory: Path) -> None:
|
|||||||
else:
|
else:
|
||||||
child.unlink()
|
child.unlink()
|
||||||
|
|
||||||
|
|
||||||
def prepare_build_directory(build_dir: Path) -> Path:
|
def prepare_build_directory(build_dir: Path) -> Path:
|
||||||
if build_dir.exists() and not build_dir.is_dir():
|
if build_dir.exists() and not build_dir.is_dir():
|
||||||
raise RuntimeError(f"Build path exists but is not a directory: {build_dir}")
|
raise RuntimeError(f"Build path exists but is not a directory: {build_dir}")
|
||||||
@ -44,24 +70,82 @@ def prepare_build_directory(build_dir: Path) -> Path:
|
|||||||
|
|
||||||
return build_dir
|
return build_dir
|
||||||
|
|
||||||
def generate_nutsi_file(build_dir: Path, file_name: str) -> None:
|
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
|
output_path = build_dir / file_name
|
||||||
|
|
||||||
zip_dir(build_dir, output_path, ignore_patterns=[
|
zip_dir(
|
||||||
|
str(build_dir),
|
||||||
|
str(output_path),
|
||||||
|
ignore_patterns=[
|
||||||
"__pycache__",
|
"__pycache__",
|
||||||
"*.pyc",
|
"*.pyc",
|
||||||
"*.nutsi",
|
"*.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)
|
||||||
|
|
||||||
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
|
selected_folder = build_folder or config.build.build_folder
|
||||||
prepared_dir = prepare_build_directory(Path(selected_folder))
|
build_dir = _resolve_project_path(project_root, Path(selected_folder))
|
||||||
|
|
||||||
generate_nutsi_file(prepared_dir, selected_name)
|
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
|
||||||
|
|||||||
105
nut/logger.py
Normal file
105
nut/logger.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# logger.py
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from colorama import Fore, Style, init as _colorama_init
|
||||||
|
_colorama_init(autoreset=True)
|
||||||
|
COLOR_ENABLED = True
|
||||||
|
except Exception:
|
||||||
|
COLOR_ENABLED = False
|
||||||
|
|
||||||
|
class Fore:
|
||||||
|
RED = ""
|
||||||
|
YELLOW = ""
|
||||||
|
GREEN = ""
|
||||||
|
CYAN = ""
|
||||||
|
WHITE = ""
|
||||||
|
|
||||||
|
class Style:
|
||||||
|
RESET_ALL = ""
|
||||||
|
|
||||||
|
START_TIME = time.perf_counter()
|
||||||
|
|
||||||
|
LEVEL_COLORS = {
|
||||||
|
"LOG": Fore.WHITE,
|
||||||
|
"INFO": Fore.CYAN,
|
||||||
|
"SUCCESS": Fore.GREEN,
|
||||||
|
"WARNING": Fore.YELLOW,
|
||||||
|
"ERROR": Fore.RED,
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Logger:
|
||||||
|
name: str
|
||||||
|
to_file: bool = False
|
||||||
|
file_path: Optional[str] = None
|
||||||
|
stdio: bool = True
|
||||||
|
enabled: bool = True
|
||||||
|
|
||||||
|
def _time_since_start(self) -> float:
|
||||||
|
return time.perf_counter() - START_TIME
|
||||||
|
|
||||||
|
def _format(self, level: str, message: str) -> str:
|
||||||
|
t = self._time_since_start()
|
||||||
|
prefix = f"[{t:0.3f}][{level}]"
|
||||||
|
if COLOR_ENABLED and level in LEVEL_COLORS:
|
||||||
|
prefix = f"{LEVEL_COLORS[level]}{prefix}{Style.RESET_ALL}"
|
||||||
|
return f"{prefix} {message}"
|
||||||
|
|
||||||
|
def _write_file(self, text: str):
|
||||||
|
if not self.file_path:
|
||||||
|
return
|
||||||
|
with open(self.file_path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(text + "\n")
|
||||||
|
|
||||||
|
def _log(self, level: str, *args):
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
message = " ".join(str(a) for a in args)
|
||||||
|
formatted = self._format(level, message)
|
||||||
|
if self.stdio:
|
||||||
|
print(formatted)
|
||||||
|
if self.to_file:
|
||||||
|
clean = f"[{self._time_since_start():0.3f}][{level}] {message}"
|
||||||
|
self._write_file(clean)
|
||||||
|
|
||||||
|
def log(self, *args):
|
||||||
|
self._log("LOG", *args)
|
||||||
|
|
||||||
|
def info(self, *args):
|
||||||
|
self._log("INFO", *args)
|
||||||
|
|
||||||
|
def success(self, *args):
|
||||||
|
self._log("SUCCESS", *args)
|
||||||
|
|
||||||
|
def warning(self, *args):
|
||||||
|
self._log("WARNING", *args)
|
||||||
|
|
||||||
|
def error(self, *args):
|
||||||
|
self._log("ERROR", *args)
|
||||||
|
|
||||||
|
def enable(self):
|
||||||
|
self.enabled = True
|
||||||
|
|
||||||
|
def disable(self):
|
||||||
|
self.enabled = False
|
||||||
|
|
||||||
|
|
||||||
|
def create_logger(name: str = "default", toFile: bool = False, path: Optional[str] = None, stdio: bool = True) -> Logger:
|
||||||
|
return Logger(
|
||||||
|
name=name,
|
||||||
|
to_file=toFile,
|
||||||
|
file_path=path,
|
||||||
|
stdio=stdio,
|
||||||
|
enabled=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = create_logger(
|
||||||
|
name="main",
|
||||||
|
toFile=False,
|
||||||
|
stdio=True
|
||||||
|
)
|
||||||
13
nut/utils.py
13
nut/utils.py
@ -8,19 +8,21 @@ def _should_ignore(rel_path: str, patterns: list) -> bool:
|
|||||||
"""
|
"""
|
||||||
Check if a file should be ignored based on patterns.
|
Check if a file should be ignored based on patterns.
|
||||||
"""
|
"""
|
||||||
rel_path = rel_path.replace("\\", "/")
|
normalized = rel_path.replace("\\", "/").lstrip("./")
|
||||||
|
basename = os.path.basename(normalized)
|
||||||
|
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
pattern = pattern.strip()
|
pattern = pattern.strip().replace("\\", "/")
|
||||||
|
|
||||||
if not pattern:
|
if not pattern:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if pattern.endswith("/"):
|
if pattern.endswith("/"):
|
||||||
if rel_path.startswith(pattern):
|
prefix = pattern.rstrip("/")
|
||||||
|
if normalized == prefix or normalized.startswith(prefix + "/"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if fnmatch.fnmatch(rel_path, pattern):
|
if fnmatch.fnmatch(normalized, pattern) or fnmatch.fnmatch(basename, pattern):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@ -38,14 +40,13 @@ def zip_dir(source_dir: str, output_zip_path: str, metadata: dict = None, ignore
|
|||||||
|
|
||||||
with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||||
for root, dirs, files in os.walk(source_dir):
|
for root, dirs, files in os.walk(source_dir):
|
||||||
|
|
||||||
rel_root = os.path.relpath(root, source_dir).replace("\\", "/")
|
rel_root = os.path.relpath(root, source_dir).replace("\\", "/")
|
||||||
|
|
||||||
# filter dirs in-place (important for performance)
|
# filter dirs in-place (important for performance)
|
||||||
dirs[:] = [
|
dirs[:] = [
|
||||||
d for d in dirs
|
d for d in dirs
|
||||||
if not _should_ignore(
|
if not _should_ignore(
|
||||||
os.path.join(rel_root, d).replace("\\", "/"),
|
os.path.join(rel_root, d).replace("\\", "/").lstrip("./"),
|
||||||
ignore_patterns
|
ignore_patterns
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user