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 .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
from .commands import setup
def help_text():
return [
@ -12,96 +8,8 @@ def help_text():
def main() -> int:
cli = CLI(help_callback=help_text)
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)
setup(cli)
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__":
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 typing import Optional
import os
import shutil
from ..config import Config
from ..cli import CLI
from ..config import Config, ConfigError, load_config
from ..paths import get_templates_path
from ..utils import zip_dir
from ..logger import logger
@dataclass(frozen=True)
class CompileContext:
@ -18,28 +20,27 @@ class CompileContext:
output_name: str
_ACTIVE_CONTEXT: Optional[CompileContext] = None
class Compiler:
def __init__(self, context: CompileContext):
self.context = context
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:
formatted = name.strip().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():
@ -47,6 +48,11 @@ def _clear_directory(directory: Path) -> None:
else:
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:
if build_dir.exists() and not build_dir.is_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
def stage_project_sources(project_root: Path, entry_path: Path, build_dir: Path) -> None:
if entry_path.is_absolute():
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"),
)
def generate_nutsi_file(build_dir: Path, file_name: str) -> Path:
output_path = build_dir / file_name
zip_dir(
str(build_dir),
str(output_path),
@ -113,6 +120,7 @@ def generate_nutsi_file(build_dir: Path, file_name: str) -> Path:
)
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)
@ -134,18 +142,66 @@ def _create_context(config: Config, build_folder: Optional[str], file_name: Opti
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)
prepared_dir = prepare_build_directory(context.build_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:
Compiler().run()
finally:
_ACTIVE_CONTEXT = None
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
archive_path = generate_nutsi_file(prepared_dir, context.output_name)
return archive_path
try:
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 ..cli import CLI
import shutil
import os
@ -43,5 +44,38 @@ def copy_template_files(dest_dir: str, force: bool = False, checked: bool = Fals
dirs_exist_ok=True
)
def is_chipnut_workspace(path: str = ".") -> bool:
return os.path.isfile(os.path.join(path, "Nutfile"))
def main(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
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)