Open raw ↗#!/usr/bin/env python3
"""Build automation script for HAD Digital MVP.
Packages the application into a zero-dependency standalone Windows executable (.exe)
using PyInstaller. Supports both single-file (.exe) and directory bundle modes.
Usage:
python build.py [--mode {onefile,onedir}] [--clean] [--verify]
"""
import argparse
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
# Paths relative to project root
current_dir = Path(__file__).resolve().parent
if current_dir.name in ("04_Build", "05_Test"):
PROJECT_ROOT = current_dir.parent
elif current_dir.name == "explorer_survey_2":
PROJECT_ROOT = current_dir.parent.parent
else:
PROJECT_ROOT = current_dir
MVP_DIR = PROJECT_ROOT / "MVP"
BUILD_DIR = PROJECT_ROOT / "04_Build" / "build"
DIST_DIR = PROJECT_ROOT / "04_Build" / "dist"
VERSION_FILE = PROJECT_ROOT / "MVP" / "VERSION.txt"
def print_banner():
print("""
====================================================================
HAD Digital MVP — Automated Build System
Zero-Dependency Standalone Windows Executable (.exe)
====================================================================
""")
def check_prerequisites():
"""Ensure required tools and source files exist."""
print("[1/5] Checking build prerequisites...")
# Check Python version
if sys.version_info < (3, 10):
print(f"[-] ERROR: Python 3.10+ required. Current version: {sys.version}")
sys.exit(1)
print(f" [+] Python version: {sys.version.split()[0]} (OK)")
# Check PyInstaller
try:
res = subprocess.run(
[sys.executable, "-m", "PyInstaller", "--version"],
capture_output=True,
text=True,
check=True
)
print(f" [+] PyInstaller version: {res.stdout.strip()} (OK)")
except (subprocess.SubprocessError, FileNotFoundError):
print("[-] ERROR: PyInstaller is not installed. Run: pip install pyinstaller")
sys.exit(1)
# Check critical source files
critical_files = [
MVP_DIR / "app.py",
MVP_DIR / "database.py",
MVP_DIR / "config_manager.py",
MVP_DIR / "user_store.py",
MVP_DIR / "ctcae_engine.py",
MVP_DIR / "alert_engine.py",
MVP_DIR / "data" / "ctcae_rules.json",
MVP_DIR / "static" / "index.html",
MVP_DIR / "static" / "app.css",
MVP_DIR / "static" / "app.js",
]
for p in critical_files:
if not p.exists():
print(f"[-] ERROR: Missing required source file: {p}")
sys.exit(1)
print(" [+] All required source files present (OK)")
def clean_artifacts():
"""Remove previous build outputs."""
print("[2/5] Cleaning prior build artifacts...")
for target in [BUILD_DIR, DIST_DIR, MVP_DIR / "build", MVP_DIR / "dist"]:
if target.exists():
print(f" [-] Removing {target}")
shutil.rmtree(target, ignore_errors=True)
def stamp_version():
"""Read or create VERSION.txt metadata."""
print("[3/5] Stamping version metadata...")
version = "1.0.0"
if VERSION_FILE.exists():
try:
version = VERSION_FILE.read_text(encoding="utf-8").strip()
except OSError:
pass
timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
print(f" [+] Version: {version} | Built: {timestamp}")
return version
def run_pyinstaller(mode="onefile"):
"""Execute PyInstaller build."""
print(f"[4/5] Running PyInstaller packaging in '{mode}' mode...")
DIST_DIR.mkdir(parents=True, exist_ok=True)
BUILD_DIR.mkdir(parents=True, exist_ok=True)
# Data folders to bundle into read-only package
static_data = f"{MVP_DIR / 'static'};static"
guidance_data = f"{MVP_DIR / 'guidance'};guidance"
rules_data = f"{MVP_DIR / 'data' / 'ctcae_rules.json'};data"
cmd = [
sys.executable,
"-m",
"PyInstaller",
"--noconfirm",
"--name", "HAD_Digital",
f"--workpath={BUILD_DIR}",
f"--distpath={DIST_DIR}",
f"--paths={MVP_DIR}",
f"--add-data={static_data}",
f"--add-data={guidance_data}",
f"--add-data={rules_data}",
"--hidden-import=adapters",
"--hidden-import=adapters.base",
"--hidden-import=adapters.stub",
"--hidden-import=adapters.glm",
"--hidden-import=http.server",
"--hidden-import=sqlite3",
"--exclude-module=tkinter",
"--exclude-module=matplotlib",
"--exclude-module=scipy",
"--exclude-module=numpy",
"--exclude-module=pandas",
"--exclude-module=openpyxl",
"--exclude-module=PIL",
"--exclude-module=pytest",
"--exclude-module=playwright",
"--exclude-module=boto3",
"--exclude-module=botocore",
"--exclude-module=torch",
"--console", # Keep console enabled for server diagnostic logs
]
if mode == "onefile":
cmd.append("--onefile")
else:
cmd.append("--onedir")
cmd.append(str(MVP_DIR / "app.py"))
print(f" [>] Command: {' '.join(cmd)}")
start_time = time.time()
result = subprocess.run(cmd)
elapsed = time.time() - start_time
if result.returncode != 0:
print(f"[-] ERROR: PyInstaller failed with return code {result.returncode}")
sys.exit(result.returncode)
print(f" [+] Packaging finished in {elapsed:.1f}s")
def verify_output(mode="onefile"):
"""Verify generated artifacts."""
print("[5/5] Verifying generated build output...")
if mode == "onefile":
exe_path = DIST_DIR / "HAD_Digital.exe"
if not exe_path.exists():
print(f"[-] ERROR: Expected output executable not found: {exe_path}")
sys.exit(1)
size_mb = exe_path.stat().st_size / (1024 * 1024)
print(f" [+] Standalone Executable: {exe_path}")
print(f" [+] Binary Size: {size_mb:.2f} MB")
print(" [+] Mode: Single standalone executable (.exe) with zero host dependencies")
else:
bundle_dir = DIST_DIR / "HAD_Digital"
exe_path = bundle_dir / "HAD_Digital.exe"
if not exe_path.exists():
print(f"[-] ERROR: Expected bundle executable not found: {exe_path}")
sys.exit(1)
print(f" [+] Directory Bundle: {bundle_dir}")
print(f" [+] Executable: {exe_path}")
print("\n[SUCCESS] Build completed successfully!")
def main():
parser = argparse.ArgumentParser(description="Build standalone Windows executable for HAD Digital MVP")
parser.add_argument("--mode", choices=["onefile", "onedir"], default="onefile",
help="Packaging mode: 'onefile' (single .exe, default) or 'onedir' (folder bundle)")
parser.add_argument("--clean", action="store_true", help="Clean build directories before packaging")
parser.add_argument("--verify", action="store_true", help="Run programmatic verification test after build")
args = parser.parse_args()
print_banner()
check_prerequisites()
if args.clean:
clean_artifacts()
stamp_version()
run_pyinstaller(mode=args.mode)
verify_output(mode=args.mode)
if args.verify:
print("\n[*] Running verification script...")
verify_script = PROJECT_ROOT / "05_Test" / "verify_mvp.py"
if not verify_script.exists():
verify_script = PROJECT_ROOT / ".agents" / "explorer_survey_2" / "proposed_verify.py"
if verify_script.exists():
exe_target = DIST_DIR / ("HAD_Digital.exe" if args.mode == "onefile" else "HAD_Digital/HAD_Digital.exe")
subprocess.run([sys.executable, str(verify_script), "--exe", str(exe_target)])
else:
print(" [-] Verification script not found, skipping automated verification.")
if __name__ == "__main__":
main()