diff --git a/scripts/build.py b/scripts/build.py index 60d7a2a..12624b9 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -3,40 +3,84 @@ This script also handles building the frontend before bundling everything. """ +import argparse import logging import subprocess +import sys from pathlib import Path from radio_telemetry_tracker_drone_gcs.utils.paths import APP_NAME from scripts.utils import build_frontend +logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +def validate_main_script(root_dir: Path) -> Path: + """Validate and return the path to the main script.""" + main_script = root_dir / "radio_telemetry_tracker_drone_gcs" / "main.py" + if not main_script.exists(): + msg = f"Main script not found at {main_script}" + raise FileNotFoundError(msg) + return main_script + + +def validate_output(dist_dir: Path) -> None: + """Validate that PyInstaller produced output files.""" + if not any(dist_dir.iterdir()): + msg = "PyInstaller did not produce any output files" + raise RuntimeError(msg) + + def main() -> None: """Build the executable using PyInstaller.""" - root_dir = Path(__file__).parent.parent - frontend_dir = build_frontend() - - cmd = [ - "pyinstaller", - f"--name={APP_NAME}", - "--windowed", - "--onefile", - "--add-data", - f"{frontend_dir / 'dist'}:frontend/dist", - # Add hidden imports for path utilities - "--hidden-import=radio_telemetry_tracker_drone_gcs.utils.paths", - ] - - # Optional: add an icon if you have one in assets/ - icon_path = root_dir / "assets" / "icon.ico" - if icon_path.exists(): - cmd.extend(["--icon", str(icon_path)]) - - # Main script - cmd.append(str(root_dir / "radio_telemetry_tracker_drone_gcs" / "main.py")) - - logger.info("Building executable with PyInstaller...") - subprocess.run(cmd, check=True) # noqa: S603 - logger.info("Build complete! Executable can be found in the 'dist' directory.") + parser = argparse.ArgumentParser() + parser.add_argument("--os", choices=["windows", "linux", "macos"], required=True) + args = parser.parse_args() + + try: + root_dir = Path(__file__).parent.parent + logger.info("Building frontend...") + frontend_dir = build_frontend() + logger.info("Frontend build complete") + + cmd = [ + "pyinstaller", + f"--name={APP_NAME}", + "--windowed", + "--onefile", + "--add-data", + f"{frontend_dir / 'dist'}{';' if args.os == 'windows' else ':'}frontend/dist", + # Add hidden imports for path utilities + "--hidden-import=radio_telemetry_tracker_drone_gcs.utils.paths", + ] + + # Optional: add an icon if you have one in assets/ + icon_path = root_dir / "assets" / "icon.ico" + if icon_path.exists(): + cmd.extend(["--icon", str(icon_path)]) + + # Main script + main_script = validate_main_script(root_dir) + cmd.append(str(main_script)) + + logger.info("Building executable with PyInstaller...") + logger.info("Command: %s", " ".join(cmd)) + result = subprocess.run(cmd, check=True, capture_output=True, text=True) # noqa: S603 + + if result.stdout: + logger.info("PyInstaller output:\n%s", result.stdout) + if result.stderr: + logger.warning("PyInstaller warnings/errors:\n%s", result.stderr) + + dist_dir = root_dir / "dist" + validate_output(dist_dir) + logger.info("Build complete! Files in dist directory: %s", list(dist_dir.iterdir())) + + except Exception: + logger.exception("Build failed") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/utils.py b/scripts/utils.py index ee1ae2c..4015b55 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -11,7 +11,7 @@ NPM_CMD = "npm.cmd" if platform.system() == "Windows" else "npm" ALLOWED_COMMANDS = { - NPM_CMD: ["run", "build"], + NPM_CMD: ["install", "run", "build"], } @@ -29,12 +29,39 @@ def build_frontend() -> Path: Returns the path to the frontend directory. """ frontend_dir = Path(__file__).parent.parent / "frontend" - logger.info("Building frontend...") + logger.info("Building frontend in %s...", frontend_dir) + + # First install dependencies + install_cmd = [NPM_CMD, "install"] + if not validate_command(install_cmd): + msg = "Invalid or disallowed command for installing frontend dependencies." + raise ValueError(msg) + + logger.info("Installing frontend dependencies...") + result = subprocess.run(install_cmd, cwd=frontend_dir, check=True, capture_output=True, text=True) # noqa: S603 + if result.stdout: + logger.info("npm install output:\n%s", result.stdout) + if result.stderr: + logger.warning("npm install warnings/errors:\n%s", result.stderr) - cmd = [NPM_CMD, "run", "build"] - if not validate_command(cmd): + # Then build + build_cmd = [NPM_CMD, "run", "build"] + if not validate_command(build_cmd): msg = "Invalid or disallowed command for building frontend." raise ValueError(msg) - subprocess.run(cmd, cwd=frontend_dir, check=True, text=True) # noqa: S603 + logger.info("Building frontend...") + result = subprocess.run(build_cmd, cwd=frontend_dir, check=True, capture_output=True, text=True) # noqa: S603 + if result.stdout: + logger.info("npm build output:\n%s", result.stdout) + if result.stderr: + logger.warning("npm build warnings/errors:\n%s", result.stderr) + + # Verify the build output exists + dist_dir = frontend_dir / "dist" + if not dist_dir.exists() or not any(dist_dir.iterdir()): + msg = "Frontend build did not produce any output files" + raise RuntimeError(msg) + + logger.info("Frontend build complete! Files in dist directory: %s", list(dist_dir.iterdir())) return frontend_dir