build-windows.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207
  1. #!/usr/bin/env python3
  2. """
  3. Standard-of-Iron — Windows Build Script
  4. Verifies dependencies, sets up MSVC environment, and builds the project.
  5. This script automates the Windows build process by:
  6. 1. Checking for required tools (CMake, Ninja, MSVC, Qt)
  7. 2. Auto-installing missing dependencies (optional, uses winget)
  8. 3. Guiding installation of missing dependencies
  9. 4. Configuring and building the project with proper MSVC setup
  10. 5. Deploying Qt dependencies with runtime libraries
  11. 6. Writing qt.conf and diagnostic scripts (run_debug.cmd, etc.)
  12. 7. Copying GL/ANGLE fallback DLLs for graphics compatibility
  13. 8. Copying assets and creating a distributable package
  14. Usage:
  15. python scripts/build-windows.py # Auto-installs missing deps, then builds
  16. python scripts/build-windows.py --no-auto-install # Show manual install instructions instead
  17. python scripts/build-windows.py --skip-checks # Skip dependency checks entirely
  18. python scripts/build-windows.py --build-type Debug # Build in Debug mode
  19. python scripts/build-windows.py --clean # Clean build directory first
  20. python scripts/build-windows.py --deploy-only # Only deploy Qt (assumes built)
  21. python scripts/build-windows.py --no-package # Skip ZIP creation (faster for dev)
  22. python scripts/build-windows.py --help # Show this help
  23. Behavior:
  24. By default, the script will automatically install missing dependencies using winget.
  25. Use --no-auto-install to disable this and see manual installation instructions instead.
  26. Performance Tips for VMs:
  27. set CMAKE_BUILD_PARALLEL_LEVEL=2 # Limit parallel jobs to avoid thrashing
  28. set PYTHONUNBUFFERED=1 # Show output immediately (no buffering)
  29. set NINJA_STATUS=[%%f/%%t] %%e sec # Show Ninja build progress
  30. Use --no-package during development to skip slow ZIP creation
  31. Requirements:
  32. - Python 3.7+
  33. - CMake 3.21+ (auto-installed if missing)
  34. - Ninja build system (auto-installed if missing)
  35. - Visual Studio 2019/2022 with C++ tools (auto-installed if missing)
  36. - Qt 6.6.3 or compatible (must be installed manually from qt.io)
  37. Note:
  38. - Auto-install requires Windows 10/11 with winget
  39. - Qt cannot be auto-installed (winget Qt packages lack required components)
  40. - Run as Administrator if auto-install fails with permission errors
  41. - Long operations (compile, windeployqt, ZIP) show warnings but no progress bars
  42. """
  43. import argparse
  44. import os
  45. import platform
  46. import re
  47. import shutil
  48. import subprocess
  49. import sys
  50. from pathlib import Path
  51. from typing import Optional, Tuple
  52. class Color:
  53. BLUE = "\033[1;34m"
  54. GREEN = "\033[1;32m"
  55. YELLOW = "\033[1;33m"
  56. RED = "\033[1;31m"
  57. RESET = "\033[0m"
  58. BOLD = "\033[1m"
  59. def info(msg: str) -> None:
  60. """Print info message."""
  61. print(f"{Color.BLUE}[i]{Color.RESET} {msg}")
  62. def success(msg: str) -> None:
  63. """Print success message."""
  64. print(f"{Color.GREEN}[+]{Color.RESET} {msg}")
  65. def warning(msg: str) -> None:
  66. """Print warning message."""
  67. print(f"{Color.YELLOW}[!]{Color.RESET} {msg}")
  68. def error(msg: str) -> None:
  69. """Print error message."""
  70. print(f"{Color.RED}[x]{Color.RESET} {msg}")
  71. def run_command(
  72. cmd: list,
  73. capture_output: bool = False,
  74. check: bool = True,
  75. env: Optional[dict] = None,
  76. ) -> subprocess.CompletedProcess:
  77. """Run a command and optionally capture output."""
  78. try:
  79. result = subprocess.run(
  80. cmd,
  81. capture_output=capture_output,
  82. text=True,
  83. check=check,
  84. env=env or os.environ.copy(),
  85. )
  86. return result
  87. except subprocess.CalledProcessError as e:
  88. if check:
  89. error(f"Command failed: {' '.join(cmd)}")
  90. if e.stdout:
  91. print(e.stdout)
  92. if e.stderr:
  93. print(e.stderr, file=sys.stderr)
  94. raise
  95. return e
  96. def check_windows() -> None:
  97. """Verify we're running on Windows."""
  98. if platform.system() != "Windows":
  99. error("This script is designed for Windows only.")
  100. error(f"Detected OS: {platform.system()}")
  101. sys.exit(1)
  102. success("Running on Windows")
  103. if not os.environ.get("PYTHONUNBUFFERED"):
  104. warning("Tip: Set PYTHONUNBUFFERED=1 for immediate output visibility")
  105. warning(" Run: set PYTHONUNBUFFERED=1 (before running this script)")
  106. print()
  107. def check_cmake() -> Tuple[bool, Optional[str]]:
  108. """Check if CMake is installed and get version."""
  109. try:
  110. result = run_command(["cmake", "--version"], capture_output=True)
  111. version_match = re.search(r"cmake version (\d+\.\d+\.\d+)", result.stdout)
  112. if version_match:
  113. version = version_match.group(1)
  114. major, minor, _ = map(int, version.split("."))
  115. if major > 3 or (major == 3 and minor >= 21):
  116. success(f"CMake {version} found")
  117. return True, version
  118. else:
  119. warning(f"CMake {version} found but version 3.21+ required")
  120. return False, version
  121. except (subprocess.CalledProcessError, FileNotFoundError):
  122. pass
  123. return False, None
  124. def check_ninja() -> bool:
  125. """Check if Ninja is installed."""
  126. try:
  127. result = run_command(["ninja", "--version"], capture_output=True)
  128. version = result.stdout.strip()
  129. success(f"Ninja {version} found")
  130. return True
  131. except (subprocess.CalledProcessError, FileNotFoundError):
  132. return False
  133. def find_vswhere() -> Optional[Path]:
  134. """Find vswhere.exe to locate Visual Studio."""
  135. vswhere_path = Path(
  136. r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
  137. )
  138. if vswhere_path.exists():
  139. return vswhere_path
  140. return None
  141. def check_msvc() -> Tuple[bool, Optional[str]]:
  142. """Check if MSVC is installed."""
  143. vswhere = find_vswhere()
  144. if not vswhere:
  145. return False, None
  146. try:
  147. result = run_command(
  148. [
  149. str(vswhere),
  150. "-latest",
  151. "-requires",
  152. "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
  153. "-property",
  154. "installationVersion",
  155. ],
  156. capture_output=True,
  157. )
  158. version = result.stdout.strip()
  159. if version:
  160. success(f"Visual Studio {version} with C++ tools found")
  161. return True, version
  162. except (subprocess.CalledProcessError, FileNotFoundError):
  163. pass
  164. return False, None
  165. def find_qt() -> Optional[Path]:
  166. """Try to find Qt installation."""
  167. possible_paths = [
  168. Path(r"C:\Qt"),
  169. Path(r"C:\Program Files\Qt"),
  170. Path(r"C:\Program Files (x86)\Qt"),
  171. Path.home() / "Qt",
  172. Path(os.environ.get("QT_ROOT", "")) if os.environ.get("QT_ROOT") else None,
  173. ]
  174. qmake_in_path = shutil.which("qmake") or shutil.which("qmake.exe")
  175. if qmake_in_path:
  176. try:
  177. qmake_path = Path(qmake_in_path)
  178. arch_dir = qmake_path.parent.parent
  179. if "msvc" in arch_dir.name.lower():
  180. return arch_dir
  181. except Exception:
  182. pass
  183. for base_path in possible_paths:
  184. if not base_path:
  185. continue
  186. if base_path.exists():
  187. for qt_version_dir in sorted(base_path.glob("6.*"), reverse=True):
  188. for arch_dir in qt_version_dir.glob("msvc*_64"):
  189. qmake = arch_dir / "bin" / "qmake.exe"
  190. if qmake.exists():
  191. return arch_dir
  192. env_qt = os.environ.get("QT_HOME") or os.environ.get("QT_INSTALL_DIR")
  193. if env_qt:
  194. env_path = Path(env_qt)
  195. if env_path.exists():
  196. for qt_version_dir in sorted(env_path.glob("6.*"), reverse=True):
  197. for arch_dir in qt_version_dir.glob("msvc*_64"):
  198. qmake = arch_dir / "bin" / "qmake.exe"
  199. if qmake.exists():
  200. return arch_dir
  201. return None
  202. def find_qt_mingw() -> Optional[Path]:
  203. """Check if MinGW Qt is installed (to warn user)."""
  204. possible_paths = [
  205. Path(r"C:\Qt"),
  206. Path.home() / "Qt",
  207. ]
  208. for base_path in possible_paths:
  209. if base_path.exists():
  210. for qt_version_dir in sorted(base_path.glob("6.*"), reverse=True):
  211. for arch_dir in qt_version_dir.glob("mingw*"):
  212. qmake = arch_dir / "bin" / "qmake.exe"
  213. if qmake.exists():
  214. return arch_dir
  215. return None
  216. def check_qt() -> Tuple[bool, Optional[Path], Optional[str]]:
  217. """Check if Qt is installed."""
  218. qt_path = find_qt()
  219. if qt_path:
  220. qmake = qt_path / "bin" / "qmake.exe"
  221. if qmake.exists():
  222. try:
  223. result = run_command(
  224. [str(qmake), "-query", "QT_VERSION"], capture_output=True
  225. )
  226. version = result.stdout.strip()
  227. success(f"Qt {version} (MSVC) found at {qt_path}")
  228. return True, qt_path, version
  229. except (subprocess.CalledProcessError, FileNotFoundError):
  230. pass
  231. mingw_qt = find_qt_mingw()
  232. if mingw_qt:
  233. qmake = mingw_qt / "bin" / "qmake.exe"
  234. try:
  235. result = run_command(
  236. [str(qmake), "-query", "QT_VERSION"], capture_output=True
  237. )
  238. version = result.stdout.strip()
  239. error(f"Qt {version} (MinGW) found at {mingw_qt}")
  240. error("This project requires Qt with MSVC compiler, not MinGW")
  241. print()
  242. info(f"{Color.BOLD}Solution:{Color.RESET}")
  243. info("Run the Qt Maintenance Tool to add MSVC component:")
  244. print()
  245. maintenance_tool = mingw_qt.parent.parent / "MaintenanceTool.exe"
  246. if maintenance_tool.exists():
  247. info(f"1. Run: {maintenance_tool}")
  248. else:
  249. info(f"1. Find Qt Maintenance Tool in: {mingw_qt.parent.parent}")
  250. info("2. Click 'Add or remove components'")
  251. info(f"3. Expand 'Qt {version.split('.')[0]}.{version.split('.')[1]}'")
  252. info("4. CHECK: 'MSVC 2019 64-bit' or 'MSVC 2022 64-bit'")
  253. info("5. CHECK: 'Qt 5 Compatibility Module'")
  254. info("6. CHECK: 'Qt Multimedia'")
  255. info("7. Click 'Update' and wait for installation")
  256. info("8. Restart terminal and run this script again")
  257. print()
  258. except Exception:
  259. pass
  260. qmake_exec = shutil.which("qmake") or shutil.which("qmake.exe")
  261. if qmake_exec and not mingw_qt:
  262. try:
  263. result = run_command([qmake_exec, "-v"], capture_output=True)
  264. if "mingw" in result.stdout.lower():
  265. error("Qt found but it's MinGW build - MSVC build required")
  266. return False, None, None
  267. try:
  268. out = run_command(
  269. [qmake_exec, "-query", "QT_VERSION"], capture_output=True
  270. )
  271. version = out.stdout.strip()
  272. except Exception:
  273. version = "unknown"
  274. success(f"Qt {version} found (qmake on PATH: {qmake_exec})")
  275. qmake_path = Path(qmake_exec)
  276. arch_dir = qmake_path.parent.parent
  277. return True, arch_dir, version
  278. except Exception:
  279. pass
  280. return False, None, None
  281. def setup_msvc_environment() -> dict:
  282. """Set up MSVC environment variables."""
  283. info("Setting up MSVC environment...")
  284. vswhere = find_vswhere()
  285. if not vswhere:
  286. error("Cannot find vswhere.exe")
  287. sys.exit(1)
  288. result = run_command(
  289. [
  290. str(vswhere),
  291. "-latest",
  292. "-requires",
  293. "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
  294. "-property",
  295. "installationPath",
  296. ],
  297. capture_output=True,
  298. )
  299. vs_path = Path(result.stdout.strip())
  300. if not vs_path.exists():
  301. error("Cannot find Visual Studio installation")
  302. sys.exit(1)
  303. vcvarsall = vs_path / "VC" / "Auxiliary" / "Build" / "vcvarsall.bat"
  304. if not vcvarsall.exists():
  305. error(f"Cannot find vcvarsall.bat at {vcvarsall}")
  306. sys.exit(1)
  307. info(f"Running vcvarsall.bat from {vs_path}")
  308. cmd = f'"{vcvarsall}" x64 && set'
  309. result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
  310. env = os.environ.copy()
  311. for line in result.stdout.split("\n"):
  312. if "=" in line:
  313. key, _, value = line.partition("=")
  314. env[key] = value.strip()
  315. success("MSVC environment configured")
  316. return env
  317. def print_installation_guide() -> None:
  318. """Print guide for installing missing dependencies."""
  319. print(f"\n{Color.BOLD}Installation Guide:{Color.RESET}\n")
  320. print(f"{Color.BOLD}1. CMake:{Color.RESET}")
  321. print(" Download from: https://cmake.org/download/")
  322. print(" Or use: winget install Kitware.CMake\n")
  323. print(f"{Color.BOLD}2. Ninja:{Color.RESET}")
  324. print(" Download from: https://github.com/ninja-build/ninja/releases")
  325. print(" Or use: winget install Ninja-build.Ninja")
  326. print(" Or use: choco install ninja\n")
  327. print(f"{Color.BOLD}3. Visual Studio:{Color.RESET}")
  328. print(" Download: https://visualstudio.microsoft.com/downloads/")
  329. print(" Install 'Desktop development with C++' workload")
  330. print(" Or use: winget install Microsoft.VisualStudio.2022.Community")
  331. print(
  332. " --override '--add Microsoft.VisualStudio.Workload.NativeDesktop'\n"
  333. )
  334. print(f"{Color.BOLD}4. Qt 6.6.3:{Color.RESET}")
  335. print(" Download: https://www.qt.io/download-open-source")
  336. print(" Install Qt 6.6.3 with MSVC 2019 64-bit component")
  337. print(" Required modules: qt5compat, qtmultimedia")
  338. print(" Or set QT_ROOT environment variable to your Qt installation\n")
  339. def check_winget_available() -> bool:
  340. """Check if winget is available."""
  341. try:
  342. result = run_command(["winget", "--version"], capture_output=True, check=False)
  343. return result.returncode == 0
  344. except FileNotFoundError:
  345. return False
  346. def install_visual_studio_direct() -> bool:
  347. """Download and install Visual Studio using the bootstrapper directly."""
  348. import tempfile
  349. import urllib.request
  350. info("Attempting direct Visual Studio installation...")
  351. info("This will download ~3MB bootstrapper, then download the full installer")
  352. print()
  353. vs_url = "https://aka.ms/vs/17/release/vs_community.exe"
  354. try:
  355. with tempfile.TemporaryDirectory() as tmpdir:
  356. installer_path = os.path.join(tmpdir, "vs_community.exe")
  357. info("Downloading Visual Studio installer...")
  358. urllib.request.urlretrieve(vs_url, installer_path)
  359. success("Installer downloaded")
  360. info("Running Visual Studio installer...")
  361. info("This may take 10-30 minutes depending on your internet connection")
  362. print()
  363. cmd = [
  364. installer_path,
  365. "--add",
  366. "Microsoft.VisualStudio.Workload.NativeDesktop",
  367. "--includeRecommended",
  368. "--passive",
  369. "--norestart",
  370. "--wait",
  371. ]
  372. result = subprocess.run(cmd, check=False)
  373. if result.returncode == 0 or result.returncode == 3010:
  374. success("Visual Studio installed successfully!")
  375. if result.returncode == 3010:
  376. warning("A restart may be required to complete the installation")
  377. return True
  378. else:
  379. error(
  380. f"Visual Studio installation failed (exit code: {result.returncode})"
  381. )
  382. return False
  383. except Exception as e:
  384. error(f"Failed to download/install Visual Studio: {e}")
  385. return False
  386. def install_qt_interactive() -> bool:
  387. """Download and launch Qt online installer with instructions."""
  388. import tempfile
  389. import urllib.request
  390. print()
  391. info(f"{Color.BOLD}Qt Installation Helper{Color.RESET}")
  392. info("Qt requires an interactive installation (needs Qt account)")
  393. print()
  394. info("This will:")
  395. info(" 1. Download the Qt online installer (~2MB)")
  396. info(" 2. Launch it for you")
  397. info(" 3. Guide you through the installation")
  398. print()
  399. qt_urls = [
  400. "https://d13lb3tujbc8s0.cloudfront.net/onlineinstallers/qt-unified-windows-x64-online.exe",
  401. "https://download.qt.io/archive/online_installers/4.8/qt-unified-windows-x64-4.8.0-online.exe",
  402. "https://download.qt.io/official_releases/online_installers/qt-unified-windows-x64-4.8-online.exe",
  403. ]
  404. qt_urls = [
  405. "https://d13lb3tujbc8s0.cloudfront.net/onlineinstallers/qt-unified-windows-x64-online.exe",
  406. "https://download.qt.io/archive/online_installers/4.8/qt-unified-windows-x64-4.8.0-online.exe",
  407. "https://download.qt.io/official_releases/online_installers/qt-unified-windows-x64-4.8-online.exe",
  408. ]
  409. try:
  410. with tempfile.TemporaryDirectory() as tmpdir:
  411. installer_path = os.path.join(tmpdir, "qt-installer.exe")
  412. downloaded = False
  413. for qt_url in qt_urls:
  414. try:
  415. info(
  416. f"Downloading Qt online installer from {qt_url.split('/')[2]}..."
  417. )
  418. urllib.request.urlretrieve(qt_url, installer_path)
  419. success("Qt installer downloaded")
  420. downloaded = True
  421. break
  422. except Exception as e:
  423. warning(f"Failed to download from {qt_url.split('/')[2]}: {e}")
  424. continue
  425. if not downloaded:
  426. error("Could not download Qt installer from any mirror")
  427. print()
  428. info("Please download manually from:")
  429. info("https://www.qt.io/download-qt-installer")
  430. print()
  431. info("Then run the installer and follow the steps below:")
  432. print()
  433. print(f"{Color.BOLD}=== Installation Steps ==={Color.RESET}")
  434. print()
  435. print(f"{Color.BOLD}1. Qt Account:{Color.RESET}")
  436. print(" - Login with your Qt account (or create one - it's free)")
  437. print()
  438. print(f"{Color.BOLD}2. Installation Path:{Color.RESET}")
  439. print(" - Use default: C:\\Qt")
  440. print(" - Or custom path and set QT_ROOT environment variable")
  441. print()
  442. print(f"{Color.BOLD}3. Select Components (CRITICAL):{Color.RESET}")
  443. print(" - Expand 'Qt 6.6.3' (or latest 6.x)")
  444. print(" - CHECK: 'MSVC 2019 64-bit' (or 'MSVC 2022 64-bit')")
  445. print(" - CHECK: 'Qt 5 Compatibility Module'")
  446. print(" - CHECK: 'Qt Multimedia'")
  447. print(" - Uncheck everything else to save space")
  448. print()
  449. print(f"{Color.BOLD}4. After Installation:{Color.RESET}")
  450. print(" - Restart this terminal")
  451. print(" - Run this script again")
  452. print()
  453. try:
  454. import webbrowser
  455. webbrowser.open("https://www.qt.io/download-qt-installer")
  456. success("Opened Qt download page in your browser")
  457. except Exception:
  458. pass
  459. return False
  460. print()
  461. print(
  462. f"{Color.BOLD}=== IMPORTANT: Follow these steps in the installer ==={Color.RESET}"
  463. )
  464. print()
  465. print(f"{Color.BOLD}1. Qt Account:{Color.RESET}")
  466. print(" - Login with your Qt account (or create one - it's free)")
  467. print()
  468. print(f"{Color.BOLD}2. Installation Path:{Color.RESET}")
  469. print(" - Use default: C:\\Qt")
  470. print(" - Or custom path and set QT_ROOT environment variable")
  471. print()
  472. print(f"{Color.BOLD}3. Select Components (CRITICAL):{Color.RESET}")
  473. print(" - Expand 'Qt 6.6.3' (or latest 6.x)")
  474. print(" - CHECK: 'MSVC 2019 64-bit' (or 'MSVC 2022 64-bit')")
  475. print(" - CHECK: 'Qt 5 Compatibility Module'")
  476. print(" - CHECK: 'Qt Multimedia'")
  477. print(" - Uncheck everything else to save space")
  478. print()
  479. print(f"{Color.BOLD}4. After Installation:{Color.RESET}")
  480. print(" - Restart this terminal")
  481. print(" - Run this script again")
  482. print()
  483. print(f"{Color.BOLD}Press Enter to launch the Qt installer...{Color.RESET}")
  484. input()
  485. info("Launching Qt installer...")
  486. subprocess.Popen([installer_path], shell=True)
  487. print()
  488. success("Qt installer launched!")
  489. warning(
  490. "Complete the installation, then restart your terminal and run this script again"
  491. )
  492. print()
  493. return False
  494. except Exception as e:
  495. error(f"Failed to download/launch Qt installer: {e}")
  496. return False
  497. def auto_install_dependencies() -> bool:
  498. """Attempt to auto-install missing dependencies using winget."""
  499. if not check_winget_available():
  500. warning("winget not available - cannot auto-install")
  501. warning(
  502. "Please install dependencies manually or update to Windows 10/11 with winget"
  503. )
  504. return False
  505. info("Attempting to auto-install missing dependencies using winget...")
  506. print()
  507. cmake_ok, _ = check_cmake()
  508. ninja_ok = check_ninja()
  509. msvc_ok, _ = check_msvc()
  510. qt_ok, _, _ = check_qt()
  511. installs_needed = []
  512. if not cmake_ok:
  513. installs_needed.append(("CMake", "Kitware.CMake"))
  514. if not ninja_ok:
  515. installs_needed.append(("Ninja", "Ninja-build.Ninja"))
  516. if not msvc_ok:
  517. installs_needed.append(
  518. (
  519. "Visual Studio 2022",
  520. "Microsoft.VisualStudio.2022.Community",
  521. '--override "--add Microsoft.VisualStudio.Workload.NativeDesktop"',
  522. )
  523. )
  524. if not installs_needed and not qt_ok:
  525. warning("Qt installation requires manual download from qt.io")
  526. warning("winget Qt packages may not include required MSVC toolchain")
  527. print()
  528. info("Would you like to download and run the Qt installer now?")
  529. if install_qt_interactive():
  530. return True
  531. else:
  532. return False
  533. if not installs_needed:
  534. return True
  535. print(f"{Color.BOLD}Installing the following packages:{Color.RESET}")
  536. for item in installs_needed:
  537. print(f" - {item[0]}")
  538. print()
  539. success_count = 0
  540. needs_restart = False
  541. vs_failed = False
  542. vs_needed = False
  543. for item in installs_needed:
  544. name = item[0]
  545. package_id = item[1]
  546. extra_args = item[2] if len(item) > 2 else None
  547. info(f"Installing {name}...")
  548. print(f" This may take several minutes, please wait...")
  549. print()
  550. cmd = [
  551. "winget",
  552. "install",
  553. "--id",
  554. package_id,
  555. "--accept-package-agreements",
  556. "--accept-source-agreements",
  557. ]
  558. if extra_args:
  559. cmd.extend(extra_args.split())
  560. try:
  561. result = subprocess.run(cmd, check=False)
  562. print()
  563. if result.returncode == 0:
  564. success(f"{name} installed successfully")
  565. success_count += 1
  566. if "CMake" in name or "Ninja" in name:
  567. needs_restart = True
  568. else:
  569. error(f"Failed to install {name} (exit code: {result.returncode})")
  570. if "Visual Studio" in name:
  571. vs_failed = True
  572. vs_needed = True
  573. else:
  574. warning("You may need to run this script as Administrator")
  575. except Exception as e:
  576. error(f"Error installing {name}: {e}")
  577. print()
  578. print()
  579. if vs_failed:
  580. print()
  581. warning("winget failed to install Visual Studio (common issue)")
  582. info("Attempting direct installation using VS bootstrapper...")
  583. print()
  584. if install_visual_studio_direct():
  585. success("Visual Studio installed via direct download!")
  586. success_count += 1
  587. vs_failed = False
  588. else:
  589. print()
  590. info(f"{Color.BOLD}Visual Studio Installation Failed{Color.RESET}")
  591. info("Please install manually:")
  592. info("1. Download: https://visualstudio.microsoft.com/downloads/")
  593. info("2. Run the installer")
  594. info("3. Select 'Desktop development with C++'")
  595. info("4. Install and restart this script")
  596. print()
  597. print()
  598. if success_count == len(installs_needed):
  599. success("All dependencies installed successfully!")
  600. if needs_restart:
  601. warning("CMake/Ninja were installed - you must restart this terminal!")
  602. warning("Close this terminal, open a new one, and run this script again.")
  603. return True
  604. elif success_count > 0:
  605. warning(f"Installed {success_count}/{len(installs_needed)} dependencies")
  606. if needs_restart:
  607. print()
  608. info(
  609. f"{Color.BOLD}IMPORTANT: CMake/Ninja were just installed!{Color.RESET}"
  610. )
  611. info("You MUST restart your terminal for PATH changes to take effect.")
  612. info("Then run this script again to continue.")
  613. print()
  614. warning("Please install remaining dependencies manually")
  615. print_installation_guide()
  616. return False
  617. else:
  618. error("Auto-install failed")
  619. warning("Please install dependencies manually")
  620. print_installation_guide()
  621. return False
  622. def check_dependencies() -> Tuple[bool, Optional[Path]]:
  623. """Check all required dependencies."""
  624. info("Checking dependencies...")
  625. print()
  626. all_ok = True
  627. qt_path = None
  628. cmake_ok, _ = check_cmake()
  629. if not cmake_ok:
  630. error("CMake 3.21+ not found")
  631. all_ok = False
  632. ninja_ok = check_ninja()
  633. if not ninja_ok:
  634. error("Ninja not found")
  635. all_ok = False
  636. msvc_ok, _ = check_msvc()
  637. if not msvc_ok:
  638. error("Visual Studio with C++ tools not found")
  639. all_ok = False
  640. qt_ok, qt_path, _ = check_qt()
  641. if not qt_ok:
  642. error("Qt 6.x with MSVC not found")
  643. all_ok = False
  644. print()
  645. if not all_ok:
  646. error("Some dependencies are missing!")
  647. return False, None
  648. success("All dependencies found!")
  649. return True, qt_path
  650. def configure_project(
  651. build_dir: Path, build_type: str, qt_path: Optional[Path], msvc_env: dict
  652. ) -> None:
  653. """Configure the project with CMake."""
  654. info(f"Configuring project (Build type: {build_type})...")
  655. print(" Running CMake configuration...")
  656. cmake_args = [
  657. "cmake",
  658. "-S",
  659. ".",
  660. "-B",
  661. str(build_dir),
  662. "-G",
  663. "Ninja",
  664. f"-DCMAKE_BUILD_TYPE={build_type}",
  665. "-DDEFAULT_LANG=en",
  666. ]
  667. if qt_path:
  668. cmake_args.append(f"-DCMAKE_PREFIX_PATH={qt_path}")
  669. parallel = os.environ.get("CMAKE_BUILD_PARALLEL_LEVEL")
  670. if parallel:
  671. info(f" Build parallelism limited to {parallel} jobs")
  672. run_command(cmake_args, env=msvc_env)
  673. success("Project configured")
  674. def build_project(build_dir: Path, msvc_env: dict) -> None:
  675. """Build the project."""
  676. info("Building project...")
  677. parallel = os.environ.get("CMAKE_BUILD_PARALLEL_LEVEL")
  678. if parallel:
  679. info(f" Using {parallel} parallel jobs (set via CMAKE_BUILD_PARALLEL_LEVEL)")
  680. else:
  681. warning(
  682. " Using all CPU cores - set CMAKE_BUILD_PARALLEL_LEVEL=2 to reduce load in VMs"
  683. )
  684. print(" Compiling (this may take several minutes)...")
  685. run_command(["cmake", "--build", str(build_dir)], env=msvc_env)
  686. success("Project built successfully")
  687. def deploy_qt(build_dir: Path, qt_path: Path, app_name: str, build_type: str) -> None:
  688. """Deploy Qt dependencies."""
  689. info("Deploying Qt dependencies...")
  690. app_dir = build_dir / "bin"
  691. exe_path = app_dir / f"{app_name}.exe"
  692. if not exe_path.exists():
  693. error(f"Executable not found: {exe_path}")
  694. sys.exit(1)
  695. windeployqt = qt_path / "bin" / "windeployqt.exe"
  696. if not windeployqt.exists():
  697. error(f"windeployqt not found at {windeployqt}")
  698. sys.exit(1)
  699. qml_dir = Path("ui/qml")
  700. mode_flag = {
  701. "Debug": "--debug",
  702. "Release": "--release",
  703. "RelWithDebInfo": "--release",
  704. }[build_type]
  705. run_command(
  706. [
  707. str(windeployqt),
  708. mode_flag,
  709. "--compiler-runtime",
  710. "--qmldir",
  711. str(qml_dir),
  712. str(exe_path),
  713. ]
  714. )
  715. success("Qt dependencies deployed")
  716. def write_qt_conf(app_dir: Path) -> None:
  717. """Write qt.conf to configure Qt plugin paths."""
  718. info("Writing qt.conf...")
  719. qt_conf_content = """[Paths]
  720. Plugins = .
  721. Imports = qml
  722. Qml2Imports = qml
  723. Translations = translations
  724. """
  725. qt_conf_path = app_dir / "qt.conf"
  726. qt_conf_path.write_text(qt_conf_content, encoding="ascii")
  727. success("qt.conf written")
  728. def write_debug_scripts(app_dir: Path, app_name: str) -> None:
  729. """Write diagnostic scripts for troubleshooting."""
  730. info("Writing diagnostic scripts...")
  731. run_smart_content = """@echo off
  732. setlocal
  733. cd /d "%~dp0"
  734. echo ============================================
  735. echo Standard of Iron - Smart Launcher
  736. echo ============================================
  737. echo.
  738. REM Try OpenGL first
  739. echo [1/2] Attempting OpenGL rendering...
  740. "%~dp0{app_name}.exe" 2>&1
  741. set EXIT_CODE=%ERRORLEVEL%
  742. REM Check for crash (access violation = -1073741819)
  743. if %EXIT_CODE% EQU -1073741819 (
  744. echo.
  745. echo [CRASH] OpenGL rendering failed!
  746. echo [INFO] This is common on VMs or older hardware
  747. echo.
  748. echo [2/2] Retrying with software rendering...
  749. echo.
  750. set QT_QUICK_BACKEND=software
  751. set QT_OPENGL=software
  752. "%~dp0{app_name}.exe"
  753. exit /b %ERRORLEVEL%
  754. )
  755. if %EXIT_CODE% NEQ 0 (
  756. echo.
  757. echo [ERROR] Application exited with code: %EXIT_CODE%
  758. echo [HINT] Try running run_debug.cmd for detailed logs
  759. pause
  760. exit /b %EXIT_CODE%
  761. )
  762. exit /b 0
  763. """.format(
  764. app_name=app_name
  765. )
  766. run_smart_path = app_dir / "run.cmd"
  767. run_smart_path.write_text(run_smart_content, encoding="ascii")
  768. run_debug_content = """@echo off
  769. setlocal
  770. cd /d "%~dp0"
  771. set QT_DEBUG_PLUGINS=1
  772. set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.rhi.*=true
  773. set QT_OPENGL=desktop
  774. set QT_QPA_PLATFORM=windows
  775. echo Starting with Desktop OpenGL...
  776. echo Logging to runlog.txt
  777. "%~dp0{app_name}.exe" 1> "%~dp0runlog.txt" 2>&1
  778. echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.txt"
  779. type "%~dp0runlog.txt"
  780. pause
  781. """.format(
  782. app_name=app_name
  783. )
  784. run_debug_path = app_dir / "run_debug.cmd"
  785. run_debug_path.write_text(run_debug_content, encoding="ascii")
  786. run_debug_softwaregl_content = """@echo off
  787. setlocal
  788. cd /d "%~dp0"
  789. set QT_DEBUG_PLUGINS=1
  790. set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.quick.*=true;qt.rhi.*=true
  791. set QT_OPENGL=software
  792. set QT_QUICK_BACKEND=software
  793. set QT_QPA_PLATFORM=windows
  794. set QMLSCENE_DEVICE=softwarecontext
  795. echo Starting with Qt Quick Software Renderer (no OpenGL)...
  796. echo This is the safest mode for VMs and old hardware
  797. echo Logging to runlog_software.txt
  798. "%~dp0{app_name}.exe" 1> "%~dp0runlog_software.txt" 2>&1
  799. echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog_software.txt"
  800. type "%~dp0runlog_software.txt"
  801. pause
  802. """.format(
  803. app_name=app_name
  804. )
  805. run_debug_softwaregl_path = app_dir / "run_debug_softwaregl.cmd"
  806. run_debug_softwaregl_path.write_text(run_debug_softwaregl_content, encoding="ascii")
  807. run_debug_angle_content = """@echo off
  808. setlocal
  809. cd /d "%~dp0"
  810. set QT_DEBUG_PLUGINS=1
  811. set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.rhi.*=true
  812. set QT_OPENGL=angle
  813. set QT_ANGLE_PLATFORM=d3d11
  814. set QT_QPA_PLATFORM=windows
  815. echo Starting with ANGLE (OpenGL ES via D3D11)...
  816. echo Logging to runlog_angle.txt
  817. "%~dp0{app_name}.exe" 1> "%~dp0runlog_angle.txt" 2>&1
  818. echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog_angle.txt"
  819. type "%~dp0runlog_angle.txt"
  820. pause
  821. """.format(
  822. app_name=app_name
  823. )
  824. run_debug_angle_path = app_dir / "run_debug_angle.cmd"
  825. run_debug_angle_path.write_text(run_debug_angle_content, encoding="ascii")
  826. success(
  827. f"Diagnostic scripts written: run.cmd (smart), run_debug.cmd, run_debug_softwaregl.cmd, run_debug_angle.cmd"
  828. )
  829. def copy_gl_angle_fallbacks(app_dir: Path, qt_path: Path) -> None:
  830. """Copy GL/ANGLE fallback DLLs for graphics compatibility."""
  831. info("Copying GL/ANGLE fallback DLLs...")
  832. qt_bin = qt_path / "bin"
  833. fallback_dlls = [
  834. "d3dcompiler_47.dll",
  835. "opengl32sw.dll",
  836. "libEGL.dll",
  837. "libGLESv2.dll",
  838. ]
  839. copied_count = 0
  840. for dll_name in fallback_dlls:
  841. src = qt_bin / dll_name
  842. if src.exists():
  843. dst = app_dir / dll_name
  844. shutil.copy2(src, dst)
  845. info(f" Copied {dll_name}")
  846. copied_count += 1
  847. else:
  848. warning(f" {dll_name} not found in Qt bin directory")
  849. if copied_count > 0:
  850. success(f"Copied {copied_count} GL/ANGLE fallback DLL(s)")
  851. else:
  852. warning("No GL/ANGLE fallback DLLs found")
  853. def copy_assets(build_dir: Path) -> None:
  854. """Copy assets to build directory."""
  855. info("Copying assets...")
  856. app_dir = build_dir / "bin"
  857. assets_src = Path("assets")
  858. assets_dst = app_dir / "assets"
  859. if assets_dst.exists():
  860. shutil.rmtree(assets_dst)
  861. shutil.copytree(assets_src, assets_dst)
  862. success("Assets copied")
  863. def create_package(build_dir: Path, build_type: str) -> Path:
  864. """Create distributable package."""
  865. info("Creating distributable package...")
  866. warning("This may take several minutes with no progress indicator...")
  867. print(" Compressing files (CPU-intensive, please wait)...")
  868. app_dir = build_dir / "bin"
  869. package_name = f"standard_of_iron-win-x64-{build_type}.zip"
  870. package_path = Path(package_name)
  871. if package_path.exists():
  872. package_path.unlink()
  873. import time
  874. start_time = time.time()
  875. shutil.make_archive(package_path.stem, "zip", app_dir)
  876. elapsed = time.time() - start_time
  877. success(f"Package created: {package_path} (took {elapsed:.1f}s)")
  878. return package_path
  879. def main() -> int:
  880. """Main entry point."""
  881. import time
  882. start_time = time.time()
  883. parser = argparse.ArgumentParser(
  884. description="Build Standard-of-Iron on Windows",
  885. formatter_class=argparse.RawDescriptionHelpFormatter,
  886. epilog=__doc__,
  887. )
  888. parser.add_argument(
  889. "--skip-checks", action="store_true", help="Skip dependency checks"
  890. )
  891. parser.add_argument(
  892. "--no-auto-install",
  893. action="store_true",
  894. help="Do NOT auto-install missing dependencies (show manual instructions instead)",
  895. )
  896. parser.add_argument(
  897. "--build-type",
  898. choices=["Debug", "Release", "RelWithDebInfo"],
  899. default="Release",
  900. help="CMake build type (default: Release)",
  901. )
  902. parser.add_argument(
  903. "--clean", action="store_true", help="Clean build directory before building"
  904. )
  905. parser.add_argument(
  906. "--deploy-only",
  907. action="store_true",
  908. help="Only deploy Qt dependencies (skip build)",
  909. )
  910. parser.add_argument(
  911. "--no-package", action="store_true", help="Skip creating distributable package"
  912. )
  913. args = parser.parse_args()
  914. check_windows()
  915. repo_root = Path(__file__).parent.parent
  916. os.chdir(repo_root)
  917. build_dir = Path("build")
  918. qt_path = None
  919. if not args.skip_checks and not args.deploy_only:
  920. deps_ok, qt_path = check_dependencies()
  921. if not deps_ok:
  922. if not args.no_auto_install:
  923. info("Attempting auto-install of missing dependencies...")
  924. print()
  925. if auto_install_dependencies():
  926. warning(
  927. "Dependencies installed - please restart your terminal and run this script again"
  928. )
  929. return 1
  930. else:
  931. print()
  932. print_installation_guide()
  933. return 1
  934. else:
  935. info("Auto-install disabled (--no-auto-install flag)")
  936. print()
  937. print_installation_guide()
  938. return 1
  939. else:
  940. _, qt_path, _ = check_qt()
  941. if args.clean and build_dir.exists():
  942. info("Cleaning build directory...")
  943. shutil.rmtree(build_dir)
  944. success("Build directory cleaned")
  945. if not args.deploy_only:
  946. msvc_env = setup_msvc_environment()
  947. configure_project(build_dir, args.build_type, qt_path, msvc_env)
  948. build_project(build_dir, msvc_env)
  949. if qt_path:
  950. deploy_qt(build_dir, qt_path, "standard_of_iron", args.build_type)
  951. app_dir = build_dir / "bin"
  952. write_qt_conf(app_dir)
  953. write_debug_scripts(app_dir, "standard_of_iron")
  954. copy_gl_angle_fallbacks(app_dir, qt_path)
  955. else:
  956. warning("Qt path not found, skipping windeployqt")
  957. copy_assets(build_dir)
  958. if not args.no_package:
  959. package_path = create_package(build_dir, args.build_type)
  960. print()
  961. elapsed = time.time() - start_time
  962. success(f"Build complete! (total time: {elapsed:.1f}s)")
  963. info(f"Package: {package_path}")
  964. info(f"Executable: {build_dir / 'bin' / 'standard_of_iron.exe'}")
  965. else:
  966. print()
  967. elapsed = time.time() - start_time
  968. success(f"Build complete! (total time: {elapsed:.1f}s)")
  969. info(f"Executable: {build_dir / 'bin' / 'standard_of_iron.exe'}")
  970. info(
  971. "Run with debug scripts: run_debug.cmd, run_debug_softwaregl.cmd, run_debug_angle.cmd"
  972. )
  973. return 0
  974. if __name__ == "__main__":
  975. try:
  976. sys.exit(main())
  977. except KeyboardInterrupt:
  978. print()
  979. warning("Build interrupted by user")
  980. sys.exit(130)
  981. except Exception as e:
  982. error(f"Unexpected error: {e}")
  983. import traceback
  984. traceback.print_exc()
  985. sys.exit(1)