build-windows.py 41 KB

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