build-windows.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  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. Guiding installation of missing dependencies
  8. 3. Configuring and building the project with proper MSVC setup
  9. 4. Deploying Qt dependencies
  10. 5. Copying assets and creating a distributable package
  11. Usage:
  12. python scripts/build-windows.py # Full build with checks
  13. python scripts/build-windows.py --skip-checks # Skip dependency checks
  14. python scripts/build-windows.py --build-type Debug # Build in Debug mode
  15. python scripts/build-windows.py --clean # Clean build directory first
  16. python scripts/build-windows.py --deploy-only # Only deploy Qt (assumes built)
  17. python scripts/build-windows.py --help # Show this help
  18. Requirements:
  19. - Python 3.7+
  20. - CMake 3.21+
  21. - Ninja build system
  22. - Visual Studio 2019/2022 with C++ tools
  23. - Qt 6.6.3 or compatible (with msvc2019_64 or msvc2022_64)
  24. """
  25. import argparse
  26. import os
  27. import platform
  28. import re
  29. import shutil
  30. import subprocess
  31. import sys
  32. from pathlib import Path
  33. from typing import Optional, Tuple
  34. # ANSI color codes for better output
  35. class Color:
  36. BLUE = '\033[1;34m'
  37. GREEN = '\033[1;32m'
  38. YELLOW = '\033[1;33m'
  39. RED = '\033[1;31m'
  40. RESET = '\033[0m'
  41. BOLD = '\033[1m'
  42. def info(msg: str) -> None:
  43. """Print info message."""
  44. print(f"{Color.BLUE}[i]{Color.RESET} {msg}")
  45. def success(msg: str) -> None:
  46. """Print success message."""
  47. print(f"{Color.GREEN}[✓]{Color.RESET} {msg}")
  48. def warning(msg: str) -> None:
  49. """Print warning message."""
  50. print(f"{Color.YELLOW}[!]{Color.RESET} {msg}")
  51. def error(msg: str) -> None:
  52. """Print error message."""
  53. print(f"{Color.RED}[✗]{Color.RESET} {msg}")
  54. def run_command(cmd: list, capture_output: bool = False, check: bool = True,
  55. env: Optional[dict] = None) -> subprocess.CompletedProcess:
  56. """Run a command and optionally capture output."""
  57. try:
  58. result = subprocess.run(
  59. cmd,
  60. capture_output=capture_output,
  61. text=True,
  62. check=check,
  63. env=env or os.environ.copy()
  64. )
  65. return result
  66. except subprocess.CalledProcessError as e:
  67. if check:
  68. error(f"Command failed: {' '.join(cmd)}")
  69. if e.stdout:
  70. print(e.stdout)
  71. if e.stderr:
  72. print(e.stderr, file=sys.stderr)
  73. raise
  74. return e
  75. def check_windows() -> None:
  76. """Verify we're running on Windows."""
  77. if platform.system() != 'Windows':
  78. error("This script is designed for Windows only.")
  79. error(f"Detected OS: {platform.system()}")
  80. sys.exit(1)
  81. success("Running on Windows")
  82. def check_cmake() -> Tuple[bool, Optional[str]]:
  83. """Check if CMake is installed and get version."""
  84. try:
  85. result = run_command(['cmake', '--version'], capture_output=True)
  86. version_match = re.search(r'cmake version (\d+\.\d+\.\d+)', result.stdout)
  87. if version_match:
  88. version = version_match.group(1)
  89. major, minor, _ = map(int, version.split('.'))
  90. if major > 3 or (major == 3 and minor >= 21):
  91. success(f"CMake {version} found")
  92. return True, version
  93. else:
  94. warning(f"CMake {version} found but version 3.21+ required")
  95. return False, version
  96. except (subprocess.CalledProcessError, FileNotFoundError):
  97. pass
  98. return False, None
  99. def check_ninja() -> bool:
  100. """Check if Ninja is installed."""
  101. try:
  102. result = run_command(['ninja', '--version'], capture_output=True)
  103. version = result.stdout.strip()
  104. success(f"Ninja {version} found")
  105. return True
  106. except (subprocess.CalledProcessError, FileNotFoundError):
  107. return False
  108. def find_vswhere() -> Optional[Path]:
  109. """Find vswhere.exe to locate Visual Studio."""
  110. vswhere_path = Path(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe")
  111. if vswhere_path.exists():
  112. return vswhere_path
  113. return None
  114. def check_msvc() -> Tuple[bool, Optional[str]]:
  115. """Check if MSVC is installed."""
  116. vswhere = find_vswhere()
  117. if not vswhere:
  118. return False, None
  119. try:
  120. result = run_command([
  121. str(vswhere),
  122. '-latest',
  123. '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
  124. '-property', 'installationVersion'
  125. ], capture_output=True)
  126. version = result.stdout.strip()
  127. if version:
  128. success(f"Visual Studio {version} with C++ tools found")
  129. return True, version
  130. except (subprocess.CalledProcessError, FileNotFoundError):
  131. pass
  132. return False, None
  133. def find_qt() -> Optional[Path]:
  134. """Try to find Qt installation."""
  135. # Common Qt installation locations
  136. possible_paths = [
  137. Path(r"C:\Qt"),
  138. Path.home() / "Qt",
  139. Path(os.environ.get('QT_ROOT', '')) if os.environ.get('QT_ROOT') else None,
  140. ]
  141. for base_path in possible_paths:
  142. if base_path and base_path.exists():
  143. # Look for Qt 6.x with msvc2019_64 or msvc2022_64
  144. for qt_version_dir in base_path.glob("6.*"):
  145. for arch_dir in qt_version_dir.glob("msvc*_64"):
  146. qmake = arch_dir / "bin" / "qmake.exe"
  147. if qmake.exists():
  148. return arch_dir
  149. return None
  150. def check_qt() -> Tuple[bool, Optional[Path], Optional[str]]:
  151. """Check if Qt is installed."""
  152. qt_path = find_qt()
  153. if qt_path:
  154. qmake = qt_path / "bin" / "qmake.exe"
  155. try:
  156. result = run_command([str(qmake), '-query', 'QT_VERSION'], capture_output=True)
  157. version = result.stdout.strip()
  158. success(f"Qt {version} found at {qt_path}")
  159. return True, qt_path, version
  160. except (subprocess.CalledProcessError, FileNotFoundError):
  161. pass
  162. return False, None, None
  163. def setup_msvc_environment() -> dict:
  164. """Set up MSVC environment variables."""
  165. info("Setting up MSVC environment...")
  166. vswhere = find_vswhere()
  167. if not vswhere:
  168. error("Cannot find vswhere.exe")
  169. sys.exit(1)
  170. # Find Visual Studio installation path
  171. result = run_command([
  172. str(vswhere),
  173. '-latest',
  174. '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
  175. '-property', 'installationPath'
  176. ], capture_output=True)
  177. vs_path = Path(result.stdout.strip())
  178. if not vs_path.exists():
  179. error("Cannot find Visual Studio installation")
  180. sys.exit(1)
  181. # Find vcvarsall.bat
  182. vcvarsall = vs_path / "VC" / "Auxiliary" / "Build" / "vcvarsall.bat"
  183. if not vcvarsall.exists():
  184. error(f"Cannot find vcvarsall.bat at {vcvarsall}")
  185. sys.exit(1)
  186. # Run vcvarsall and capture environment
  187. info(f"Running vcvarsall.bat from {vs_path}")
  188. cmd = f'"{vcvarsall}" x64 && set'
  189. result = subprocess.run(
  190. cmd,
  191. shell=True,
  192. capture_output=True,
  193. text=True
  194. )
  195. # Parse environment variables
  196. env = os.environ.copy()
  197. for line in result.stdout.split('\n'):
  198. if '=' in line:
  199. key, _, value = line.partition('=')
  200. env[key] = value.strip()
  201. success("MSVC environment configured")
  202. return env
  203. def print_installation_guide() -> None:
  204. """Print guide for installing missing dependencies."""
  205. print(f"\n{Color.BOLD}Installation Guide:{Color.RESET}\n")
  206. print(f"{Color.BOLD}1. CMake:{Color.RESET}")
  207. print(" Download from: https://cmake.org/download/")
  208. print(" Or use: winget install Kitware.CMake\n")
  209. print(f"{Color.BOLD}2. Ninja:{Color.RESET}")
  210. print(" Download from: https://github.com/ninja-build/ninja/releases")
  211. print(" Or use: winget install Ninja-build.Ninja")
  212. print(" Or use: choco install ninja\n")
  213. print(f"{Color.BOLD}3. Visual Studio:{Color.RESET}")
  214. print(" Download: https://visualstudio.microsoft.com/downloads/")
  215. print(" Install 'Desktop development with C++' workload")
  216. print(" Or use: winget install Microsoft.VisualStudio.2022.Community")
  217. print(" --override '--add Microsoft.VisualStudio.Workload.NativeDesktop'\n")
  218. print(f"{Color.BOLD}4. Qt 6.6.3:{Color.RESET}")
  219. print(" Download: https://www.qt.io/download-open-source")
  220. print(" Install Qt 6.6.3 with MSVC 2019 64-bit component")
  221. print(" Required modules: qt5compat, qtmultimedia")
  222. print(" Or set QT_ROOT environment variable to your Qt installation\n")
  223. def check_dependencies() -> Tuple[bool, Optional[Path]]:
  224. """Check all required dependencies."""
  225. info("Checking dependencies...")
  226. print()
  227. all_ok = True
  228. qt_path = None
  229. cmake_ok, _ = check_cmake()
  230. if not cmake_ok:
  231. error("CMake 3.21+ not found")
  232. all_ok = False
  233. ninja_ok = check_ninja()
  234. if not ninja_ok:
  235. error("Ninja not found")
  236. all_ok = False
  237. msvc_ok, _ = check_msvc()
  238. if not msvc_ok:
  239. error("Visual Studio with C++ tools not found")
  240. all_ok = False
  241. qt_ok, qt_path, _ = check_qt()
  242. if not qt_ok:
  243. error("Qt 6.x with MSVC not found")
  244. all_ok = False
  245. print()
  246. if not all_ok:
  247. error("Some dependencies are missing!")
  248. print_installation_guide()
  249. return False, None
  250. success("All dependencies found!")
  251. return True, qt_path
  252. def configure_project(build_dir: Path, build_type: str, qt_path: Optional[Path],
  253. msvc_env: dict) -> None:
  254. """Configure the project with CMake."""
  255. info(f"Configuring project (Build type: {build_type})...")
  256. cmake_args = [
  257. 'cmake',
  258. '-S', '.',
  259. '-B', str(build_dir),
  260. '-G', 'Ninja',
  261. f'-DCMAKE_BUILD_TYPE={build_type}',
  262. '-DDEFAULT_LANG=en'
  263. ]
  264. if qt_path:
  265. cmake_args.append(f'-DCMAKE_PREFIX_PATH={qt_path}')
  266. run_command(cmake_args, env=msvc_env)
  267. success("Project configured")
  268. def build_project(build_dir: Path, msvc_env: dict) -> None:
  269. """Build the project."""
  270. info("Building project...")
  271. run_command(['cmake', '--build', str(build_dir)], env=msvc_env)
  272. success("Project built successfully")
  273. def deploy_qt(build_dir: Path, qt_path: Path, app_name: str, build_type: str) -> None:
  274. """Deploy Qt dependencies."""
  275. info("Deploying Qt dependencies...")
  276. app_dir = build_dir / "bin"
  277. exe_path = app_dir / f"{app_name}.exe"
  278. if not exe_path.exists():
  279. error(f"Executable not found: {exe_path}")
  280. sys.exit(1)
  281. windeployqt = qt_path / "bin" / "windeployqt.exe"
  282. if not windeployqt.exists():
  283. error(f"windeployqt not found at {windeployqt}")
  284. sys.exit(1)
  285. qml_dir = Path("ui/qml")
  286. # Map build type to windeployqt mode flag
  287. mode_flag = {
  288. "Debug": "--debug",
  289. "Release": "--release",
  290. "RelWithDebInfo": "--release", # release DLLs + PDBs
  291. }[build_type]
  292. run_command([
  293. str(windeployqt),
  294. mode_flag,
  295. '--compiler-runtime', # ship VC++ runtime DLLs
  296. '--qmldir', str(qml_dir),
  297. str(exe_path)
  298. ])
  299. success("Qt dependencies deployed")
  300. def copy_assets(build_dir: Path) -> None:
  301. """Copy assets to build directory."""
  302. info("Copying assets...")
  303. app_dir = build_dir / "bin"
  304. assets_src = Path("assets")
  305. assets_dst = app_dir / "assets"
  306. if assets_dst.exists():
  307. shutil.rmtree(assets_dst)
  308. shutil.copytree(assets_src, assets_dst)
  309. success("Assets copied")
  310. def create_package(build_dir: Path, build_type: str) -> Path:
  311. """Create distributable package."""
  312. info("Creating distributable package...")
  313. app_dir = build_dir / "bin"
  314. package_name = f"standard_of_iron-win-x64-{build_type}.zip"
  315. package_path = Path(package_name)
  316. if package_path.exists():
  317. package_path.unlink()
  318. shutil.make_archive(
  319. package_path.stem,
  320. 'zip',
  321. app_dir
  322. )
  323. success(f"Package created: {package_path}")
  324. return package_path
  325. def main() -> int:
  326. """Main entry point."""
  327. parser = argparse.ArgumentParser(
  328. description="Build Standard-of-Iron on Windows",
  329. formatter_class=argparse.RawDescriptionHelpFormatter,
  330. epilog=__doc__
  331. )
  332. parser.add_argument(
  333. '--skip-checks',
  334. action='store_true',
  335. help='Skip dependency checks'
  336. )
  337. parser.add_argument(
  338. '--build-type',
  339. choices=['Debug', 'Release', 'RelWithDebInfo'],
  340. default='Release',
  341. help='CMake build type (default: Release)'
  342. )
  343. parser.add_argument(
  344. '--clean',
  345. action='store_true',
  346. help='Clean build directory before building'
  347. )
  348. parser.add_argument(
  349. '--deploy-only',
  350. action='store_true',
  351. help='Only deploy Qt dependencies (skip build)'
  352. )
  353. parser.add_argument(
  354. '--no-package',
  355. action='store_true',
  356. help='Skip creating distributable package'
  357. )
  358. args = parser.parse_args()
  359. # Check we're on Windows
  360. check_windows()
  361. # Repository root
  362. repo_root = Path(__file__).parent.parent
  363. os.chdir(repo_root)
  364. build_dir = Path("build")
  365. qt_path = None
  366. # Check dependencies unless skipped
  367. if not args.skip_checks and not args.deploy_only:
  368. deps_ok, qt_path = check_dependencies()
  369. if not deps_ok:
  370. return 1
  371. else:
  372. # Try to find Qt even if checks are skipped
  373. _, qt_path, _ = check_qt()
  374. # Clean build directory if requested
  375. if args.clean and build_dir.exists():
  376. info("Cleaning build directory...")
  377. shutil.rmtree(build_dir)
  378. success("Build directory cleaned")
  379. # Set up MSVC environment
  380. if not args.deploy_only:
  381. msvc_env = setup_msvc_environment()
  382. # Configure
  383. configure_project(build_dir, args.build_type, qt_path, msvc_env)
  384. # Build
  385. build_project(build_dir, msvc_env)
  386. # Deploy Qt
  387. if qt_path:
  388. deploy_qt(build_dir, qt_path, "standard_of_iron", args.build_type)
  389. else:
  390. warning("Qt path not found, skipping windeployqt")
  391. # Copy assets
  392. copy_assets(build_dir)
  393. # Create package
  394. if not args.no_package:
  395. package_path = create_package(build_dir, args.build_type)
  396. print()
  397. info(f"Build complete! Package available at: {package_path}")
  398. info(f"Executable location: {build_dir / 'bin' / 'standard_of_iron.exe'}")
  399. else:
  400. print()
  401. info(f"Build complete!")
  402. info(f"Executable location: {build_dir / 'bin' / 'standard_of_iron.exe'}")
  403. return 0
  404. if __name__ == '__main__':
  405. try:
  406. sys.exit(main())
  407. except KeyboardInterrupt:
  408. print()
  409. warning("Build interrupted by user")
  410. sys.exit(130)
  411. except Exception as e:
  412. error(f"Unexpected error: {e}")
  413. import traceback
  414. traceback.print_exc()
  415. sys.exit(1)