Files

162 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""
R120: Dependency parity preflight check.
Validates the build environment before deployment or test execution.
Checks:
1. Python version (>=3.10)
2. Node.js version (>=18.0.0, per package.json + TEST_SOP)
3. Essential Python dependencies (cryptography, defusedxml)
Usage:
python scripts/preflight_check.py [--strict]
"""
import argparse
import re
import subprocess
import sys
# Minimum requirements
MIN_PYTHON_VERSION = (3, 10)
MIN_NODE_VERSION = (18, 0, 0)
REQUIRED_PYTHON_PACKAGES = [
("cryptography", "41.0"),
("defusedxml", "0.7.1"),
]
# Colors for output
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
RESET = "\033[0m"
def log_ok(msg: str):
print(f"{GREEN}[OK]{RESET} {msg}")
def log_fail(msg: str):
print(f"{RED}[FAIL]{RESET} {msg}")
def log_warn(msg: str):
print(f"{YELLOW}[WARN]{RESET} {msg}")
def check_python_version() -> bool:
"""Validate Python interpreter version."""
current = sys.version_info[:3]
if current < MIN_PYTHON_VERSION:
log_fail(
f"Python version {sys.version} is too old. Required: >={'.'.join(map(str, MIN_PYTHON_VERSION))}"
)
return False
log_ok(f"Python version: {'.'.join(map(str, current))}")
return True
def check_node_version() -> bool:
"""Validate Node.js version."""
try:
output = (
subprocess.check_output(["node", "--version"], stderr=subprocess.STDOUT)
.decode("utf-8")
.strip()
)
# Output is usually vX.Y.Z
match = re.search(r"v(\d+)\.(\d+)\.(\d+)", output)
if not match:
log_warn(f"Could not parse Node version from '{output}'.")
return False
major, minor, patch = map(int, match.groups())
if (major, minor, patch) < MIN_NODE_VERSION:
log_fail(
f"Node version {output} is too old. Required: >={'.'.join(map(str, MIN_NODE_VERSION))}"
)
return False
log_ok(f"Node version: {output}")
return True
except FileNotFoundError:
log_fail("Node.js not found in PATH.")
return False
except Exception as e:
log_warn(f"Failed to check Node version: {e}")
return False
def check_python_packages() -> bool:
"""Validate installed Python packages."""
all_ok = True
try:
from importlib.metadata import PackageNotFoundError, version
for pkg, min_ver in REQUIRED_PYTHON_PACKAGES:
try:
installed_ver = version(pkg)
# Simple version compare (not fully semver compliant but enough for preflight)
# Using pkg_resources or packaging.version is better but might add deps.
# We'll just split by dot.
# normalize versions
def parse_ver(v_str):
return tuple(map(int, v_str.split(".")[:3]))
if parse_ver(installed_ver) < parse_ver(min_ver):
log_fail(f"Package '{pkg}' version {installed_ver} < {min_ver}")
all_ok = False
else:
log_ok(f"Package '{pkg}': {installed_ver} (>= {min_ver})")
except PackageNotFoundError:
log_fail(f"Package '{pkg}' not installed.")
all_ok = False
except ValueError:
# Fallback for complex version strings
log_warn(
f"Package '{pkg}' version {installed_ver} checked (complex format). Assuming OK."
)
except ImportError:
log_warn(
"importlib.metadata not available (Python < 3.8?). Skipping package checks."
)
return all_ok
def main():
parser = argparse.ArgumentParser(description="Run dependency preflight checks.")
parser.add_argument(
"--strict", action="store_true", help="Fail with exit code 1 on any error"
)
args = parser.parse_args()
print("Running R120 Dependency Preflight...")
print("-" * 40)
checks = [
check_python_version(),
check_node_version(),
check_python_packages(),
]
print("-" * 40)
success = all(checks)
if success:
print(f"{GREEN}Preflight PASSED.{RESET}")
sys.exit(0)
else:
print(f"{RED}Preflight FAILED.{RESET}")
if args.strict:
sys.exit(1)
# Non-strict mode (e.g. dev) might exit 0 or just warn
sys.exit(1)
if __name__ == "__main__":
main()