Refactor CLI setup and command structure; streamline project initialization and compilation processes
This commit is contained in:
parent
d51b16ce8e
commit
2300c37fde
@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.0.1"
|
||||||
@ -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())
|
||||||
|
|||||||
@ -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)
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user