379 lines
11 KiB
Python
379 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)
|
|
|
|
# Copy room-specific databrowse depot if exists
|
|
room_databrowse = room_cfg / "databrowse" / "depot"
|
|
if room_databrowse.exists():
|
|
log.info(f" Copying {cfg_name} databrowse depot...")
|
|
target = output_dir / "station" / "monitors" / "databrowse" / "depot"
|
|
copy_path(room_databrowse, target)
|
|
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()
|