Files
soleprint/build.py
2025-12-31 08:21:07 -03:00

372 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Soleprint Build Tool
Builds the soleprint instance using modelgen for model generation.
Both dev and deploy modes copy files (no symlinks) for Docker compatibility.
After editing source files, re-run `python build.py dev` to update gen/.
Usage:
python build.py dev # Build gen/ from source
python build.py dev --cfg amar # Include amar room config
python build.py deploy --output /path/ # Build for production
python build.py models # Only regenerate models
Examples:
# Set up dev environment
python build.py dev
cd gen && .venv/bin/python run.py
# With room config
python build.py dev --cfg amar
# Build for deployment
python build.py deploy --output ../deploy/soleprint/
"""
import argparse
import logging
import os
import shutil
import subprocess
import sys
from pathlib import Path
# SPR root is where this script lives
SPR_ROOT = Path(__file__).resolve().parent
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
)
log = logging.getLogger(__name__)
def ensure_dir(path: Path):
"""Create directory if it doesn't exist."""
path.mkdir(parents=True, exist_ok=True)
def copy_path(source: Path, target: Path):
"""Copy file or directory, resolving symlinks."""
if target.is_symlink():
target.unlink()
elif target.exists():
if target.is_dir():
shutil.rmtree(target)
else:
target.unlink()
if source.is_dir():
shutil.copytree(source, target, symlinks=False)
log.info(f" Copied: {target.name}/ ({count_files(target)} files)")
else:
shutil.copy2(source, target)
log.info(f" Copied: {target.name}")
def count_files(path: Path) -> int:
"""Count files in directory recursively."""
return sum(1 for _ in path.rglob("*") if _.is_file())
def generate_models(output_dir: Path):
"""Generate models using modelgen tool.
Args:
output_dir: Directory where models/pydantic/__init__.py will be created
"""
config_path = SPR_ROOT / "cfg" / "soleprint.config.json"
if not config_path.exists():
log.warning(f"Config not found at {config_path}")
return False
# Soleprint-specific: models go in models/pydantic/__init__.py
models_file = output_dir / "models" / "pydantic" / "__init__.py"
models_file.parent.mkdir(parents=True, exist_ok=True)
# Run modelgen as subprocess
cmd = [
sys.executable,
"-m",
"station.tools.modelgen",
"from-config",
"--config",
str(config_path),
"--output",
str(models_file),
"--format",
"pydantic",
]
result = subprocess.run(cmd, cwd=SPR_ROOT)
return result.returncode == 0
def copy_cfg(output_dir: Path, cfg_name: str | None):
"""Copy configuration files to output directory.
Args:
output_dir: Target directory
cfg_name: Name of room config (e.g., 'amar'), or None for base only
"""
cfg_dir = output_dir / "cfg"
ensure_dir(cfg_dir)
# Always copy base config
base_config = SPR_ROOT / "cfg" / "soleprint.config.json"
if base_config.exists():
copy_path(base_config, cfg_dir / "soleprint.config.json")
# Copy room-specific config if specified
if cfg_name:
room_cfg = SPR_ROOT / "cfg" / cfg_name
if room_cfg.exists() and room_cfg.is_dir():
log.info(f"\nCopying {cfg_name} room config...")
for item in room_cfg.iterdir():
if item.name == ".env.example":
# Copy .env.example to output root as template
copy_path(item, output_dir / ".env.example")
elif item.is_dir():
copy_path(item, cfg_dir / cfg_name / item.name)
else:
ensure_dir(cfg_dir / cfg_name)
copy_path(item, cfg_dir / cfg_name / item.name)
else:
log.warning(f"Room config '{cfg_name}' not found at {room_cfg}")
def build_dev(output_dir: Path, cfg_name: str | None = None):
"""
Build for development using copies (Docker-compatible).
Structure:
gen/
├── main.py
├── run.py
├── index.html
├── requirements.txt
├── Dockerfile
├── dataloader/
├── artery/
├── atlas/
├── station/
├── data/
├── cfg/
├── .env.example # From cfg/<room>/.env.example
└── models/ # Generated
After editing source files, re-run `python build.py dev` to update gen/.
"""
log.info("\n=== Building DEV environment ===")
log.info(f"SPR root: {SPR_ROOT}")
log.info(f"Output: {output_dir}")
if cfg_name:
log.info(f"Room cfg: {cfg_name}")
ensure_dir(output_dir)
# Soleprint core files
log.info("\nCopying soleprint files...")
soleprint = SPR_ROOT / "soleprint"
copy_path(soleprint / "main.py", output_dir / "main.py")
copy_path(soleprint / "run.py", output_dir / "run.py")
copy_path(soleprint / "index.html", output_dir / "index.html")
copy_path(soleprint / "requirements.txt", output_dir / "requirements.txt")
copy_path(soleprint / "dataloader", output_dir / "dataloader")
if (soleprint / "Dockerfile").exists():
copy_path(soleprint / "Dockerfile", output_dir / "Dockerfile")
# System directories
log.info("\nCopying systems...")
for system in ["artery", "atlas", "station"]:
source = SPR_ROOT / system
if source.exists():
copy_path(source, output_dir / system)
# Data directory
log.info("\nCopying data...")
copy_path(SPR_ROOT / "data", output_dir / "data")
# Config
log.info("\nCopying config...")
copy_cfg(output_dir, cfg_name)
# Models (generated)
log.info("\nGenerating models...")
if not generate_models(output_dir):
log.warning("Model generation failed, you may need to run it manually")
log.info("\n✓ Dev build complete!")
log.info(f"\nTo run:")
log.info(f" cd {output_dir}")
log.info(f" python3 -m venv .venv")
log.info(f" .venv/bin/pip install -r requirements.txt")
log.info(f" .venv/bin/python run.py # Single-port bare-metal dev")
log.info(f"\nAfter editing source, rebuild with: python build.py dev")
def build_deploy(output_dir: Path, cfg_name: str | None = None):
"""
Build for deployment by copying all files (no symlinks).
"""
log.info("\n=== Building DEPLOY package ===")
log.info(f"SPR root: {SPR_ROOT}")
log.info(f"Output: {output_dir}")
if cfg_name:
log.info(f"Room cfg: {cfg_name}")
if output_dir.exists():
response = input(f"\nOutput directory exists. Overwrite? [y/N] ")
if response.lower() != "y":
log.info("Aborted.")
return
shutil.rmtree(output_dir)
ensure_dir(output_dir)
# Soleprint core files (copy)
log.info("\nCopying soleprint files...")
soleprint = SPR_ROOT / "soleprint"
copy_path(soleprint / "main.py", output_dir / "main.py")
copy_path(soleprint / "run.py", output_dir / "run.py")
copy_path(soleprint / "index.html", output_dir / "index.html")
copy_path(soleprint / "requirements.txt", output_dir / "requirements.txt")
copy_path(soleprint / "dataloader", output_dir / "dataloader")
if (soleprint / "Dockerfile").exists():
copy_path(soleprint / "Dockerfile", output_dir / "Dockerfile")
# System directories (copy)
log.info("\nCopying systems...")
for system in ["artery", "atlas", "station"]:
source = SPR_ROOT / system
if source.exists():
copy_path(source, output_dir / system)
# Data directory (copy)
log.info("\nCopying data...")
copy_path(SPR_ROOT / "data", output_dir / "data")
# Config (copy)
log.info("\nCopying config...")
copy_cfg(output_dir, cfg_name)
# Models (generate fresh) - pass output_dir, modelgen adds models/pydantic
log.info("\nGenerating models...")
if not generate_models(output_dir):
# Fallback: copy from gen if exists
existing = SPR_ROOT / "gen" / "models"
if existing.exists():
log.info(" Using existing models from gen/")
copy_path(existing, output_dir / "models")
# Copy schema.json for reference
log.info("\nCopying schema...")
copy_path(SPR_ROOT / "schema.json", output_dir / "schema.json")
# Create run script
run_script = output_dir / "start.sh"
run_script.write_text("""#!/bin/bash
# Soleprint runner
cd "$(dirname "$0")"
if [ ! -d ".venv" ]; then
echo "Creating virtual environment..."
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
fi
echo "Starting soleprint on http://localhost:12000"
.venv/bin/python main.py
""")
run_script.chmod(0o755)
log.info(" Created: start.sh")
total_files = count_files(output_dir)
log.info(f"\n✓ Deploy build complete! ({total_files} files)")
log.info(f"\nTo run:")
log.info(f" cd {output_dir}")
log.info(f" ./start.sh")
log.info(f"\nOr deploy to server:")
log.info(f" rsync -av {output_dir}/ server:/app/soleprint/")
log.info(f" ssh server 'cd /app/soleprint && ./start.sh'")
def build_models():
"""Only regenerate models."""
log.info("\n=== Generating models only ===")
output_dir = SPR_ROOT / "gen"
ensure_dir(output_dir)
if generate_models(output_dir):
log.info("\n✓ Models generated!")
else:
log.error("Model generation failed")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Soleprint Build Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
subparsers = parser.add_subparsers(dest="command", required=True)
# dev command
dev_parser = subparsers.add_parser("dev", help="Build for development (copies)")
dev_parser.add_argument(
"--output",
"-o",
type=Path,
default=SPR_ROOT / "gen",
help="Output directory (default: gen/)",
)
dev_parser.add_argument(
"--cfg",
"-c",
type=str,
default=None,
help="Room config to include (e.g., 'amar')",
)
# deploy command
deploy_parser = subparsers.add_parser(
"deploy", help="Build for deployment (copies)"
)
deploy_parser.add_argument(
"--output",
"-o",
type=Path,
required=True,
help="Output directory for deployment package",
)
deploy_parser.add_argument(
"--cfg",
"-c",
type=str,
default=None,
help="Room config to include (e.g., 'amar')",
)
# models command
subparsers.add_parser("models", help="Only regenerate models")
args = parser.parse_args()
if args.command == "dev":
build_dev(args.output.resolve(), args.cfg)
elif args.command == "deploy":
build_deploy(args.output.resolve(), args.cfg)
elif args.command == "models":
build_models()
if __name__ == "__main__":
main()