Refactor CLI setup and command structure; streamline project initialization and compilation processes

This commit is contained in:
Chipperfluff 2026-04-04 14:44:45 +02:00
parent d51b16ce8e
commit 2300c37fde
5 changed files with 120 additions and 113 deletions

View File

@ -0,0 +1 @@
__version__ = "0.0.1"

View File

@ -1,9 +1,5 @@
from .cli import CLI from .cli import CLI
from .commands import setup
from .config import ConfigError, load_config
from .commands.setup import is_chipnut_workspace, find_template_conflicts, copy_template_files
from .commands.compiler import compile as compile_project
def help_text(): def help_text():
return [ return [
@ -12,96 +8,8 @@ def help_text():
def main() -> int: def main() -> int:
cli = CLI(help_callback=help_text) cli = CLI(help_callback=help_text)
setup(cli)
cli.command(
"init",
help="Create an empty chipnut workspace or reinitialize with --force",
description=(
"Initialize a chipnut development workspace by creating a Nutfile and default project files. "
"If a Nutfile already exists, initialization should fail unless --force is used."
),
).flag(
"force",
"f",
help="Reinitialize even when a Nutfile already exists",
).arg(
"path",
nargs="?",
default=".",
help="Directory to initialize (default: current directory)",
).handle(cmd_init)
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(cmd_build)
return cli.run() return cli.run()
def cmd_build(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_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
def cmd_init(args, flags) -> int:
dest_dir = args[0]
force = flags["force"]
conflicts = find_template_conflicts(dest_dir)
if conflicts and not force:
print("Some files already exist that would be overwritten:")
for file in conflicts:
print(f" {file}")
print("Use --force to overwrite existing files.")
return 1
copy_template_files(dest_dir, force=force, checked=True)
return 0
if __name__ == "__main__": if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())

View File

@ -0,0 +1,8 @@
from ..cli import CLI
from .setup import setup as cmd_setup
from .compiler import setup as cmd_build
def setup(cli: CLI):
cmd_setup(cli)
cmd_build(cli)

View File

@ -2,12 +2,14 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import os
import shutil import shutil
from ..config import Config from ..cli import CLI
from ..config import Config, ConfigError, load_config
from ..paths import get_templates_path from ..paths import get_templates_path
from ..utils import zip_dir from ..utils import zip_dir
from ..logger import logger
@dataclass(frozen=True) @dataclass(frozen=True)
class CompileContext: class CompileContext:
@ -18,28 +20,27 @@ class CompileContext:
output_name: str output_name: str
_ACTIVE_CONTEXT: Optional[CompileContext] = None
class Compiler: class Compiler:
def __init__(self, context: CompileContext):
self.context = context
def run(self) -> None: def run(self) -> None:
return 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: def _resolve_project_path(project_root: Path, value: Path) -> Path:
if value.is_absolute(): if value.is_absolute():
return value return value
return (project_root / value).resolve() return (project_root / value).resolve()
def _format_to_filename(name: str) -> str: def _format_to_filename(name: str) -> str:
formatted = name.strip().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
def _clear_directory(directory: Path) -> None: def _clear_directory(directory: Path) -> None:
for child in directory.iterdir(): for child in directory.iterdir():
if child.is_dir(): if child.is_dir():
@ -47,6 +48,11 @@ def _clear_directory(directory: Path) -> None:
else: else:
child.unlink() 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: 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}")
@ -70,6 +76,7 @@ def prepare_build_directory(build_dir: Path) -> Path:
return build_dir return build_dir
def stage_project_sources(project_root: Path, entry_path: Path, build_dir: Path) -> None: def stage_project_sources(project_root: Path, entry_path: Path, build_dir: Path) -> None:
if entry_path.is_absolute(): if entry_path.is_absolute():
raise RuntimeError( raise RuntimeError(
@ -99,9 +106,9 @@ def stage_project_sources(project_root: Path, entry_path: Path, build_dir: Path)
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.nutsi"), ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.nutsi"),
) )
def generate_nutsi_file(build_dir: Path, file_name: str) -> Path: 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( zip_dir(
str(build_dir), str(build_dir),
str(output_path), str(output_path),
@ -113,6 +120,7 @@ def generate_nutsi_file(build_dir: Path, file_name: str) -> Path:
) )
return output_path return output_path
def _create_context(config: Config, build_folder: Optional[str], file_name: Optional[str]) -> CompileContext: def _create_context(config: Config, build_folder: Optional[str], file_name: Optional[str]) -> CompileContext:
project_root = config.path.parent.resolve() project_root = config.path.parent.resolve()
entry_path = Path(config.build.entry) entry_path = Path(config.build.entry)
@ -134,18 +142,66 @@ def _create_context(config: Config, build_folder: Optional[str], file_name: Opti
output_name=output_name, output_name=output_name,
) )
def compile(config: Config, build_folder: Optional[str] = None, file_name: Optional[str] = None) -> Path:
global _ACTIVE_CONTEXT
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) context = _create_context(config, build_folder=build_folder, file_name=file_name)
prepared_dir = prepare_build_directory(context.build_dir) prepared_dir = prepare_build_directory(context.build_dir)
stage_project_sources(context.project_root, context.entry_path, prepared_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
_ACTIVE_CONTEXT = context
try: try:
Compiler().run() config = load_config(config_path)
finally: except FileNotFoundError:
_ACTIVE_CONTEXT = None print(f"Error: Config file not found: {config_path}")
return 1
except ConfigError as error:
print(f"Error: Invalid Nutfile: {error}")
return 1
archive_path = generate_nutsi_file(prepared_dir, context.output_name) try:
return archive_path 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)

View File

@ -1,4 +1,5 @@
from ..paths import get_templates_path from ..paths import get_templates_path
from ..cli import CLI
import shutil import shutil
import os import os
@ -43,5 +44,38 @@ def copy_template_files(dest_dir: str, force: bool = False, checked: bool = Fals
dirs_exist_ok=True dirs_exist_ok=True
) )
def is_chipnut_workspace(path: str = ".") -> bool: def main(args, flags) -> int:
return os.path.isfile(os.path.join(path, "Nutfile")) dest_dir = args[0]
force = flags["force"]
conflicts = find_template_conflicts(dest_dir)
if conflicts and not force:
print("Some files already exist that would be overwritten:")
for file in conflicts:
print(f" {file}")
print("Use --force to overwrite existing files.")
return 1
copy_template_files(dest_dir, force=force, checked=True)
return 0
def setup(cli: CLI):
cli.command(
"init",
help="Create an empty chipnut workspace or reinitialize with --force",
description=(
"Initialize a chipnut development workspace by creating a Nutfile and default project files. "
"If a Nutfile already exists, initialization should fail unless --force is used."
),
).flag(
"force",
"f",
help="Reinitialize even when a Nutfile already exists",
).arg(
"path",
nargs="?",
default=".",
help="Directory to initialize (default: current directory)",
).handle(main)