check_machine_paths.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #!/usr/bin/env python3
  2. """
  3. check_machine_paths.py - Read-only environment & asset check for PCB AFM project.
  4. Checks:
  5. 1. Python version (>= 3.10)
  6. 2. Environment variables: MOTORCAD_ACTIVEX, ANSYSLMD_LICENSE_FILE
  7. 3. Motor-CAD executable path existence (env var + documented fallback path)
  8. 4. Python dependencies: ansys-motorcad-core, PySide6, pandas, fastapi, uvicorn, pydantic
  9. 5. Git repo: .git exists, HEAD valid, working tree clean
  10. 6. Repo assets: models/*.mot, experience/experience.db, executor_config.json, web dirs, src/afmcore
  11. 7. node / npm presence (needed for frontend build)
  12. Usage:
  13. python scripts/check_machine_paths.py # read-only check
  14. python scripts/check_machine_paths.py --fix # print exact fix commands (does NOT auto-execute)
  15. Exit code:
  16. 0 = all checks passed
  17. 1 = at least one check failed
  18. Note:
  19. The script is read-only by default. --fix only PRINTS the exact commands to run
  20. (setx / pip install), it never modifies the system by itself.
  21. All output is ASCII (project source rule).
  22. """
  23. import importlib.util
  24. import os
  25. import shutil
  26. import subprocess
  27. import sys
  28. from pathlib import Path
  29. REPO_ROOT = Path(__file__).resolve().parent.parent
  30. # documented fallback for Motor-CAD when env var is missing
  31. MOTORCAD_FALLBACK = r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe"
  32. FAILURES = []
  33. WARNINGS = []
  34. def check(name, ok, detail):
  35. """Record one check result and print it."""
  36. tag = "PASS" if ok else "FAIL"
  37. print("[%s] %s: %s" % (tag, name, detail))
  38. if not ok:
  39. FAILURES.append(name)
  40. def check_env_var(name):
  41. """Check a required environment variable; return its value or ''."""
  42. val = os.environ.get(name, "").strip()
  43. check("env:" + name, bool(val), val if val else "MISSING")
  44. return val
  45. def main():
  46. fix_mode = "--fix" in sys.argv[1:]
  47. print("=== PCB AFM - machine path check ===")
  48. print("repo root: %s" % REPO_ROOT)
  49. # 1. Python version
  50. ver = sys.version_info
  51. ok_ver = (ver.major, ver.minor) >= (3, 10)
  52. check("python-version", ok_ver, "%d.%d.%d" % (ver.major, ver.minor, ver.micro))
  53. # 2. required environment variables
  54. motorcad_act = check_env_var("MOTORCAD_ACTIVEX")
  55. check_env_var("ANSYSLMD_LICENSE_FILE")
  56. # 3. Motor-CAD executable
  57. candidates = []
  58. if motorcad_act:
  59. candidates.append(Path(motorcad_act))
  60. candidates.append(Path(MOTORCAD_FALLBACK))
  61. found_exes = [str(p) for p in candidates if p.exists()]
  62. check("motorcad-exe", bool(found_exes), "; ".join(found_exes) or "none found")
  63. # 4. python dependencies
  64. deps = ["ansys.motorcad.core", "PySide6", "pandas", "fastapi", "uvicorn", "pydantic"]
  65. for d in deps:
  66. try:
  67. found = importlib.util.find_spec(d) is not None
  68. except Exception:
  69. found = False
  70. check("pkg:" + d, found, "installed" if found else "MISSING")
  71. # detect a project-local virtualenv (informational: shell python may lack project deps)
  72. for venv_name in (".venv", ".env"):
  73. vd = REPO_ROOT / venv_name
  74. if vd.exists():
  75. py = vd / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
  76. if py.exists():
  77. print("[INFO] project venv found: %s (re-run checks with that python to validate project deps)" % py)
  78. break
  79. # 5. git repo state
  80. check("git-dir", (REPO_ROOT / ".git").exists(), str(REPO_ROOT / ".git"))
  81. head_ok = False
  82. head = ""
  83. try:
  84. r = subprocess.run(
  85. ["git", "rev-parse", "--verify", "HEAD"],
  86. cwd=str(REPO_ROOT), capture_output=True, text=True, timeout=10,
  87. )
  88. head_ok = r.returncode == 0
  89. head = r.stdout.strip()[:12] if head_ok else "INVALID"
  90. except Exception as exc: # noqa: BLE001 - report any failure as invalid head
  91. head = "error:%s" % exc
  92. check("git-head", head_ok, head)
  93. clean = False
  94. try:
  95. r = subprocess.run(
  96. ["git", "status", "--porcelain"],
  97. cwd=str(REPO_ROOT), capture_output=True, text=True, timeout=10,
  98. )
  99. clean = r.returncode == 0 and r.stdout.strip() == ""
  100. except Exception:
  101. clean = False
  102. check("git-clean", clean, "clean" if clean else "has uncommitted changes")
  103. # 6. repo assets
  104. models_dir = REPO_ROOT / "models"
  105. mot_files = list(models_dir.glob("*.mot")) if models_dir.exists() else []
  106. check("asset:models-mot", len(mot_files) > 0, "%d .mot file(s)" % len(mot_files))
  107. exp_db = REPO_ROOT / "experience" / "experience.db"
  108. check("asset:experience-db", exp_db.exists(), str(exp_db))
  109. cfg = REPO_ROOT / "executor_config.json"
  110. check("asset:executor-config", cfg.exists(), str(cfg))
  111. web_f = REPO_ROOT / "web" / "frontend"
  112. web_b = REPO_ROOT / "web" / "backend"
  113. check("asset:web-frontend", web_f.exists(), str(web_f))
  114. check("asset:web-backend", web_b.exists(), str(web_b))
  115. afmcore = REPO_ROOT / "src" / "afmcore"
  116. check("asset:afmcore", afmcore.exists(), str(afmcore))
  117. # 7. node / npm (warning only, frontend build)
  118. node_ok = shutil.which("node") is not None
  119. npm_ok = shutil.which("npm") is not None
  120. if node_ok and npm_ok:
  121. print("[PASS] node/npm: found on PATH")
  122. else:
  123. WARNINGS.append("node/npm not found on PATH (needed for frontend build)")
  124. print("[WARN] node/npm: missing on PATH")
  125. # 8. summary
  126. print("=== summary ===")
  127. if FAILURES:
  128. print("FAILED (%d): %s" % (len(FAILURES), ", ".join(FAILURES)))
  129. for w in WARNINGS:
  130. print("WARN: %s" % w)
  131. if fix_mode:
  132. _print_fix_commands()
  133. sys.exit(1)
  134. else:
  135. print("ALL CHECKS PASSED")
  136. for w in WARNINGS:
  137. print("WARN: %s" % w)
  138. sys.exit(0)
  139. def _print_fix_commands():
  140. """Print (do not execute) the exact commands to fix each failure."""
  141. print("=== suggested fixes (run manually) ===")
  142. for name in FAILURES:
  143. if name == "env:MOTORCAD_ACTIVEX":
  144. print(
  145. 'setx MOTORCAD_ACTIVEX "%s" # or let scripts fall back to set_motorcad_exe()'
  146. % MOTORCAD_FALLBACK
  147. )
  148. elif name == "env:ANSYSLMD_LICENSE_FILE":
  149. print('setx ANSYSLMD_LICENSE_FILE "1055@localhost"')
  150. elif name.startswith("pkg:"):
  151. pkg = name.split(":", 1)[1]
  152. pip_name = "ansys-motorcad-core" if pkg == "ansys.motorcad.core" else pkg
  153. print("pip install %s" % pip_name)
  154. elif name == "git-clean":
  155. print("git commit (or stash) uncommitted changes before running simulations")
  156. elif name == "asset:experience-db":
  157. print("run once to bootstrap the experience database (see README)")
  158. elif name == "asset:models-mot":
  159. print("place a baseline .mot model into models/ (read-only)")
  160. if __name__ == "__main__":
  161. main()