#!/usr/bin/env python3 """ Soleprint Build Tool Builds the soleprint instance using modelgen for model generation. Modes: - dev: Uses symlinks for quick development (edit source, run from gen/) - deploy: Copies everything for production deployment (no symlinks) Usage: python build.py dev python build.py dev --cfg amar python build.py deploy --output /path/to/deploy/ python build.py models Examples: # Set up dev environment (soleprint only) python build.py dev # Set up dev environment with amar room config python build.py dev --cfg amar # Build for deployment python build.py deploy --output ../deploy/soleprint/ # Only regenerate models python build.py models """ import argparse 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 def ensure_dir(path: Path): """Create directory if it doesn't exist.""" path.mkdir(parents=True, exist_ok=True) def create_symlink(source: Path, target: Path): """Create a symlink, removing existing if present.""" if target.exists() or target.is_symlink(): if target.is_symlink(): target.unlink() elif target.is_dir(): shutil.rmtree(target) else: target.unlink() # Make relative symlink rel_source = os.path.relpath(source, target.parent) target.symlink_to(rel_source) print(f" Linked: {target.name} -> {rel_source}") def copy_path(source: Path, target: Path): """Copy file or directory, resolving symlinks.""" if target.exists(): if target.is_dir(): shutil.rmtree(target) else: target.unlink() if source.is_dir(): shutil.copytree(source, target, symlinks=False) print(f" Copied: {target.name}/ ({count_files(target)} files)") else: shutil.copy2(source, target) print(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(): print(f"Warning: 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(): print(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: print(f"Warning: Room config '{cfg_name}' not found at {room_cfg}") def build_dev(output_dir: Path, cfg_name: str | None = None): """ Build for development using symlinks. Structure: gen/ ├── main.py -> ../soleprint/main.py ├── run.py -> ../soleprint/run.py ├── index.html -> ../soleprint/index.html ├── requirements.txt -> ../soleprint/requirements.txt ├── dataloader/ -> ../soleprint/dataloader/ ├── artery/ -> ../artery/ ├── atlas/ -> ../atlas/ ├── station/ -> ../station/ ├── data/ -> ../data/ ├── cfg/ # Copied config ├── .env.example # From cfg//.env.example └── models/ # Generated """ print(f"\n=== Building DEV environment ===") print(f"SPR root: {SPR_ROOT}") print(f"Output: {output_dir}") if cfg_name: print(f"Room cfg: {cfg_name}") ensure_dir(output_dir) # Soleprint core files (symlinks) print("\nLinking soleprint files...") soleprint = SPR_ROOT / "soleprint" create_symlink(soleprint / "main.py", output_dir / "main.py") create_symlink(soleprint / "run.py", output_dir / "run.py") create_symlink(soleprint / "index.html", output_dir / "index.html") create_symlink(soleprint / "requirements.txt", output_dir / "requirements.txt") create_symlink(soleprint / "dataloader", output_dir / "dataloader") if (soleprint / "Dockerfile").exists(): create_symlink(soleprint / "Dockerfile", output_dir / "Dockerfile") # System directories (symlinks) print("\nLinking systems...") for system in ["artery", "atlas", "station"]: source = SPR_ROOT / system if source.exists(): create_symlink(source, output_dir / system) # Data directory (symlink) print("\nLinking data...") create_symlink(SPR_ROOT / "data", output_dir / "data") # Config (copy, not symlink - may be customized) print("\nCopying config...") copy_cfg(output_dir, cfg_name) # Models (generated) - pass output_dir, modelgen adds models/pydantic print("\nGenerating models...") if not generate_models(output_dir): print(" Warning: Model generation failed, you may need to run it manually") print("\n✓ Dev build complete!") print(f"\nTo run:") print(f" cd {output_dir}") print(f" python3 -m venv .venv") print(f" .venv/bin/pip install -r requirements.txt") print(f" .venv/bin/python main.py # Multi-port (production-like)") print(f" .venv/bin/python run.py # Single-port (bare-metal dev)") def build_deploy(output_dir: Path, cfg_name: str | None = None): """ Build for deployment by copying all files (no symlinks). """ print(f"\n=== Building DEPLOY package ===") print(f"SPR root: {SPR_ROOT}") print(f"Output: {output_dir}") if cfg_name: print(f"Room cfg: {cfg_name}") if output_dir.exists(): response = input(f"\nOutput directory exists. Overwrite? [y/N] ") if response.lower() != "y": print("Aborted.") return shutil.rmtree(output_dir) ensure_dir(output_dir) # Soleprint core files (copy) print("\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) print("\nCopying systems...") for system in ["artery", "atlas", "station"]: source = SPR_ROOT / system if source.exists(): copy_path(source, output_dir / system) # Data directory (copy) print("\nCopying data...") copy_path(SPR_ROOT / "data", output_dir / "data") # Config (copy) print("\nCopying config...") copy_cfg(output_dir, cfg_name) # Models (generate fresh) - pass output_dir, modelgen adds models/pydantic print("\nGenerating models...") if not generate_models(output_dir): # Fallback: copy from gen if exists existing = SPR_ROOT / "gen" / "models" if existing.exists(): print(" Using existing models from gen/") copy_path(existing, output_dir / "models") # Copy schema.json for reference print("\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) print(" Created: start.sh") total_files = count_files(output_dir) print(f"\n✓ Deploy build complete! ({total_files} files)") print(f"\nTo run:") print(f" cd {output_dir}") print(f" ./start.sh") print(f"\nOr deploy to server:") print(f" rsync -av {output_dir}/ server:/app/soleprint/") print(f" ssh server 'cd /app/soleprint && ./start.sh'") def build_models(): """Only regenerate models.""" print(f"\n=== Generating models only ===") output_dir = SPR_ROOT / "gen" ensure_dir(output_dir) if generate_models(output_dir): print("\n✓ Models generated!") else: print("\nError: Model generation failed", file=sys.stderr) 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 (symlinks)") 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()