bootstrap_local.py 3.58 KB
from __future__ import annotations

import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path

from services.shared.config.base import PROJECT_ROOT


REQUIREMENTS_FILE = PROJECT_ROOT / "infra" / "python" / "requirements-local.txt"
FRONTEND_DIR = PROJECT_ROOT / "apps" / "web_ui"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Bootstrap BettaFish for pure local development on the current machine.",
    )
    parser.add_argument(
        "--skip-pip-tools-upgrade",
        action="store_true",
        help="Skip upgrading pip/setuptools/wheel before installing dependencies.",
    )
    parser.add_argument(
        "--skip-playwright",
        action="store_true",
        help="Skip installing the Playwright Chromium browser.",
    )
    parser.add_argument(
        "--skip-frontend",
        action="store_true",
        help="Skip installing frontend dependencies in apps/web_ui.",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Print the commands that would run without executing them.",
    )
    return parser.parse_args()


def run_command(command: list[str], *, dry_run: bool) -> int:
    rendered = " ".join(command)
    print(f"[bootstrap] {rendered}")
    if dry_run:
        return 0
    result = subprocess.run(command, cwd=PROJECT_ROOT, env=build_subprocess_env())
    return result.returncode


def build_subprocess_env() -> dict[str, str]:
    env = os.environ.copy()
    env.setdefault("PYTHONIOENCODING", "utf-8")
    env.setdefault("PYTHONUTF8", "1")
    env.setdefault("PYTHONUNBUFFERED", "1")
    return env


def ensure_prerequisites(skip_frontend: bool) -> int:
    if not REQUIREMENTS_FILE.exists():
        print(f"[bootstrap] Missing requirements file: {REQUIREMENTS_FILE}", file=sys.stderr)
        return 1

    if not skip_frontend and shutil.which("npm") is None:
        print(
            "[bootstrap] npm was not found. Install Node.js first, or rerun with --skip-frontend.",
            file=sys.stderr,
        )
        return 1

    return 0


def print_next_steps() -> None:
    print()
    print("[bootstrap] Next steps:")
    print("1. Copy .env.local.example to .env.local and fill in your local settings.")
    print("2. Ensure local PostgreSQL is available at 127.0.0.1:5432 (or adjust .env.local).")
    print("3. Start the project with: python -m scripts.dev.start_local --build-frontend")


def main() -> int:
    args = parse_args()

    prerequisite_code = ensure_prerequisites(skip_frontend=args.skip_frontend)
    if prerequisite_code != 0:
        return prerequisite_code

    if not args.skip_pip_tools_upgrade:
        code = run_command(
            [sys.executable, "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"],
            dry_run=args.dry_run,
        )
        if code != 0:
            return code

    code = run_command(
        [sys.executable, "-m", "pip", "install", "-r", str(REQUIREMENTS_FILE)],
        dry_run=args.dry_run,
    )
    if code != 0:
        return code

    if not args.skip_playwright:
        code = run_command(
            [sys.executable, "-m", "playwright", "install", "chromium"],
            dry_run=args.dry_run,
        )
        if code != 0:
            return code

    if not args.skip_frontend:
        code = run_command(
            ["npm", "--prefix", str(FRONTEND_DIR.relative_to(PROJECT_ROOT)), "install"],
            dry_run=args.dry_run,
        )
        if code != 0:
            return code

    print_next_steps()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())