#!/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 deploy --output /path/to/deploy/ python build.py models Examples: # Set up dev environment python build.py dev # 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 / "config" / "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 build_dev(output_dir: Path): """ Build for development using symlinks. Structure: gen/ ├── main.py -> ../hub/main.py ├── index.html -> ../hub/index.html ├── requirements.txt -> ../hub/requirements.txt ├── dataloader/ -> ../hub/dataloader/ ├── artery/ -> ../artery/ ├── atlas/ -> ../atlas/ ├── station/ -> ../station/ ├── data/ -> ../data/ └── models/ # Generated """ print(f"\n=== Building DEV environment ===") print(f"SPR root: {SPR_ROOT}") print(f"Output: {output_dir}") ensure_dir(output_dir) # Hub files (symlinks) print("\nLinking hub files...") hub = SPR_ROOT / "hub" create_symlink(hub / "main.py", output_dir / "main.py") create_symlink(hub / "index.html", output_dir / "index.html") create_symlink(hub / "requirements.txt", output_dir / "requirements.txt") create_symlink(hub / "dataloader", output_dir / "dataloader") # 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") # 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") def build_deploy(output_dir: Path): """ 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 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) # Hub files (copy) print("\nCopying hub files...") hub = SPR_ROOT / "hub" copy_path(hub / "main.py", output_dir / "main.py") copy_path(hub / "index.html", output_dir / "index.html") copy_path(hub / "requirements.txt", output_dir / "requirements.txt") copy_path(hub / "dataloader", output_dir / "dataloader") # 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") # 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 / "run.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: run.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" ./run.sh") print(f"\nOr deploy to server:") print(f" rsync -av {output_dir}/ server:/app/soleprint/") print(f" ssh server 'cd /app/soleprint && ./run.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/)", ) # 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", ) # models command subparsers.add_parser("models", help="Only regenerate models") args = parser.parse_args() if args.command == "dev": build_dev(args.output.resolve()) elif args.command == "deploy": build_deploy(args.output.resolve()) elif args.command == "models": build_models() if __name__ == "__main__": main()