| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162 |
- #!/usr/bin/env python3
- """
- Standard-of-Iron — Windows Build Script
- Verifies dependencies, sets up MSVC environment, and builds the project.
- This script automates the Windows build process by:
- 1. Checking for required tools (CMake, Ninja, MSVC, Qt)
- 2. Auto-installing missing dependencies (optional, uses winget)
- 3. Guiding installation of missing dependencies
- 4. Configuring and building the project with proper MSVC setup
- 5. Deploying Qt dependencies with runtime libraries
- 6. Writing qt.conf and diagnostic scripts (run_debug.cmd, etc.)
- 7. Copying GL/ANGLE fallback DLLs for graphics compatibility
- 8. Copying assets and creating a distributable package
- Usage:
- python scripts/build-windows.py # Auto-installs missing deps, then builds
- python scripts/build-windows.py --no-auto-install # Show manual install instructions instead
- python scripts/build-windows.py --skip-checks # Skip dependency checks entirely
- python scripts/build-windows.py --build-type Debug # Build in Debug mode
- python scripts/build-windows.py --clean # Clean build directory first
- python scripts/build-windows.py --deploy-only # Only deploy Qt (assumes built)
- python scripts/build-windows.py --no-package # Skip ZIP creation (faster for dev)
- python scripts/build-windows.py --help # Show this help
- Behavior:
- By default, the script will automatically install missing dependencies using winget.
- Use --no-auto-install to disable this and see manual installation instructions instead.
-
- Performance Tips for VMs:
- set CMAKE_BUILD_PARALLEL_LEVEL=2 # Limit parallel jobs to avoid thrashing
- set PYTHONUNBUFFERED=1 # Show output immediately (no buffering)
- set NINJA_STATUS=[%%f/%%t] %%e sec # Show Ninja build progress
-
- Use --no-package during development to skip slow ZIP creation
- Requirements:
- - Python 3.7+
- - CMake 3.21+ (auto-installed if missing)
- - Ninja build system (auto-installed if missing)
- - Visual Studio 2019/2022 with C++ tools (auto-installed if missing)
- - Qt 6.6.3 or compatible (must be installed manually from qt.io)
-
- Note:
- - Auto-install requires Windows 10/11 with winget
- - Qt cannot be auto-installed (winget Qt packages lack required components)
- - Run as Administrator if auto-install fails with permission errors
- - Long operations (compile, windeployqt, ZIP) show warnings but no progress bars
- """
- import argparse
- import os
- import platform
- import re
- import shutil
- import subprocess
- import sys
- from pathlib import Path
- from typing import Optional, Tuple
- # ANSI color codes for better output
- class Color:
- BLUE = '\033[1;34m'
- GREEN = '\033[1;32m'
- YELLOW = '\033[1;33m'
- RED = '\033[1;31m'
- RESET = '\033[0m'
- BOLD = '\033[1m'
- def info(msg: str) -> None:
- """Print info message."""
- print(f"{Color.BLUE}[i]{Color.RESET} {msg}")
- def success(msg: str) -> None:
- """Print success message."""
- print(f"{Color.GREEN}[+]{Color.RESET} {msg}")
- def warning(msg: str) -> None:
- """Print warning message."""
- print(f"{Color.YELLOW}[!]{Color.RESET} {msg}")
- def error(msg: str) -> None:
- """Print error message."""
- print(f"{Color.RED}[x]{Color.RESET} {msg}")
- def run_command(cmd: list, capture_output: bool = False, check: bool = True,
- env: Optional[dict] = None) -> subprocess.CompletedProcess:
- """Run a command and optionally capture output."""
- try:
- result = subprocess.run(
- cmd,
- capture_output=capture_output,
- text=True,
- check=check,
- env=env or os.environ.copy()
- )
- return result
- except subprocess.CalledProcessError as e:
- if check:
- error(f"Command failed: {' '.join(cmd)}")
- if e.stdout:
- print(e.stdout)
- if e.stderr:
- print(e.stderr, file=sys.stderr)
- raise
- return e
- def check_windows() -> None:
- """Verify we're running on Windows."""
- if platform.system() != 'Windows':
- error("This script is designed for Windows only.")
- error(f"Detected OS: {platform.system()}")
- sys.exit(1)
- success("Running on Windows")
-
- # Check for unbuffered output
- if not os.environ.get('PYTHONUNBUFFERED'):
- warning("Tip: Set PYTHONUNBUFFERED=1 for immediate output visibility")
- warning(" Run: set PYTHONUNBUFFERED=1 (before running this script)")
- print()
- def check_cmake() -> Tuple[bool, Optional[str]]:
- """Check if CMake is installed and get version."""
- try:
- result = run_command(['cmake', '--version'], capture_output=True)
- version_match = re.search(r'cmake version (\d+\.\d+\.\d+)', result.stdout)
- if version_match:
- version = version_match.group(1)
- major, minor, _ = map(int, version.split('.'))
- if major > 3 or (major == 3 and minor >= 21):
- success(f"CMake {version} found")
- return True, version
- else:
- warning(f"CMake {version} found but version 3.21+ required")
- return False, version
- except (subprocess.CalledProcessError, FileNotFoundError):
- pass
- return False, None
- def check_ninja() -> bool:
- """Check if Ninja is installed."""
- try:
- result = run_command(['ninja', '--version'], capture_output=True)
- version = result.stdout.strip()
- success(f"Ninja {version} found")
- return True
- except (subprocess.CalledProcessError, FileNotFoundError):
- return False
- def find_vswhere() -> Optional[Path]:
- """Find vswhere.exe to locate Visual Studio."""
- vswhere_path = Path(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe")
- if vswhere_path.exists():
- return vswhere_path
- return None
- def check_msvc() -> Tuple[bool, Optional[str]]:
- """Check if MSVC is installed."""
- vswhere = find_vswhere()
- if not vswhere:
- return False, None
-
- try:
- result = run_command([
- str(vswhere),
- '-latest',
- '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
- '-property', 'installationVersion'
- ], capture_output=True)
-
- version = result.stdout.strip()
- if version:
- success(f"Visual Studio {version} with C++ tools found")
- return True, version
- except (subprocess.CalledProcessError, FileNotFoundError):
- pass
-
- return False, None
- def find_qt() -> Optional[Path]:
- """Try to find Qt installation."""
- # Common Qt installation locations
- possible_paths = [
- Path(r"C:\Qt"),
- Path(r"C:\Program Files\Qt"),
- Path(r"C:\Program Files (x86)\Qt"),
- Path.home() / "Qt",
- Path(os.environ.get('QT_ROOT', '')) if os.environ.get('QT_ROOT') else None,
- ]
- # 1) Check PATH for qmake.exe first (useful if user added Qt to PATH)
- qmake_in_path = shutil.which('qmake') or shutil.which('qmake.exe')
- if qmake_in_path:
- try:
- qmake_path = Path(qmake_in_path)
- # qmake lives in <qtdir>/bin/qmake.exe - return the arch dir
- arch_dir = qmake_path.parent.parent
- # Check if it's MSVC variant (required for this project)
- if 'msvc' in arch_dir.name.lower():
- return arch_dir
- # If it's MinGW, we'll report it later but keep searching
- except Exception:
- pass
- # 2) Search common installation roots for MSVC builds
- for base_path in possible_paths:
- if not base_path:
- continue
- if base_path.exists():
- # Look for Qt 6.x with msvc2019_64 or msvc2022_64
- for qt_version_dir in sorted(base_path.glob("6.*"), reverse=True):
- for arch_dir in qt_version_dir.glob("msvc*_64"):
- qmake = arch_dir / "bin" / "qmake.exe"
- if qmake.exists():
- return arch_dir
- # 3) Try to read QT_HOME from common environment variables
- env_qt = os.environ.get('QT_HOME') or os.environ.get('QT_INSTALL_DIR')
- if env_qt:
- env_path = Path(env_qt)
- if env_path.exists():
- for qt_version_dir in sorted(env_path.glob("6.*"), reverse=True):
- for arch_dir in qt_version_dir.glob("msvc*_64"):
- qmake = arch_dir / "bin" / "qmake.exe"
- if qmake.exists():
- return arch_dir
- return None
- def find_qt_mingw() -> Optional[Path]:
- """Check if MinGW Qt is installed (to warn user)."""
- possible_paths = [
- Path(r"C:\Qt"),
- Path.home() / "Qt",
- ]
-
- for base_path in possible_paths:
- if base_path.exists():
- for qt_version_dir in sorted(base_path.glob("6.*"), reverse=True):
- for arch_dir in qt_version_dir.glob("mingw*"):
- qmake = arch_dir / "bin" / "qmake.exe"
- if qmake.exists():
- return arch_dir
- return None
- def check_qt() -> Tuple[bool, Optional[Path], Optional[str]]:
- """Check if Qt is installed."""
- qt_path = find_qt()
- # If we found a qmake path via PATH, verify by running it
- if qt_path:
- qmake = qt_path / "bin" / "qmake.exe"
- if qmake.exists():
- try:
- result = run_command([str(qmake), '-query', 'QT_VERSION'], capture_output=True)
- version = result.stdout.strip()
- success(f"Qt {version} (MSVC) found at {qt_path}")
- return True, qt_path, version
- except (subprocess.CalledProcessError, FileNotFoundError):
- pass
- # Check if MinGW Qt is installed instead
- mingw_qt = find_qt_mingw()
- if mingw_qt:
- qmake = mingw_qt / "bin" / "qmake.exe"
- try:
- result = run_command([str(qmake), '-query', 'QT_VERSION'], capture_output=True)
- version = result.stdout.strip()
- error(f"Qt {version} (MinGW) found at {mingw_qt}")
- error("This project requires Qt with MSVC compiler, not MinGW")
- print()
- info(f"{Color.BOLD}Solution:{Color.RESET}")
- info("Run the Qt Maintenance Tool to add MSVC component:")
- print()
- maintenance_tool = mingw_qt.parent.parent / "MaintenanceTool.exe"
- if maintenance_tool.exists():
- info(f"1. Run: {maintenance_tool}")
- else:
- info(f"1. Find Qt Maintenance Tool in: {mingw_qt.parent.parent}")
- info("2. Click 'Add or remove components'")
- info(f"3. Expand 'Qt {version.split('.')[0]}.{version.split('.')[1]}'")
- info("4. CHECK: 'MSVC 2019 64-bit' or 'MSVC 2022 64-bit'")
- info("5. CHECK: 'Qt 5 Compatibility Module'")
- info("6. CHECK: 'Qt Multimedia'")
- info("7. Click 'Update' and wait for installation")
- info("8. Restart terminal and run this script again")
- print()
- except Exception:
- pass
- # Last resort: try running 'qmake -v' if on PATH
- qmake_exec = shutil.which('qmake') or shutil.which('qmake.exe')
- if qmake_exec and not mingw_qt:
- try:
- result = run_command([qmake_exec, '-v'], capture_output=True)
- # Check if it's MinGW
- if 'mingw' in result.stdout.lower():
- error("Qt found but it's MinGW build - MSVC build required")
- return False, None, None
-
- # Try to query QT_VERSION properly
- try:
- out = run_command([qmake_exec, '-query', 'QT_VERSION'], capture_output=True)
- version = out.stdout.strip()
- except Exception:
- version = "unknown"
-
- success(f"Qt {version} found (qmake on PATH: {qmake_exec})")
- # Try to compute arch dir
- qmake_path = Path(qmake_exec)
- arch_dir = qmake_path.parent.parent
- return True, arch_dir, version
- except Exception:
- pass
- return False, None, None
- def setup_msvc_environment() -> dict:
- """Set up MSVC environment variables."""
- info("Setting up MSVC environment...")
-
- vswhere = find_vswhere()
- if not vswhere:
- error("Cannot find vswhere.exe")
- sys.exit(1)
-
- # Find Visual Studio installation path
- result = run_command([
- str(vswhere),
- '-latest',
- '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
- '-property', 'installationPath'
- ], capture_output=True)
-
- vs_path = Path(result.stdout.strip())
- if not vs_path.exists():
- error("Cannot find Visual Studio installation")
- sys.exit(1)
-
- # Find vcvarsall.bat
- vcvarsall = vs_path / "VC" / "Auxiliary" / "Build" / "vcvarsall.bat"
- if not vcvarsall.exists():
- error(f"Cannot find vcvarsall.bat at {vcvarsall}")
- sys.exit(1)
-
- # Run vcvarsall and capture environment
- info(f"Running vcvarsall.bat from {vs_path}")
- cmd = f'"{vcvarsall}" x64 && set'
-
- result = subprocess.run(
- cmd,
- shell=True,
- capture_output=True,
- text=True
- )
-
- # Parse environment variables
- env = os.environ.copy()
- for line in result.stdout.split('\n'):
- if '=' in line:
- key, _, value = line.partition('=')
- env[key] = value.strip()
-
- success("MSVC environment configured")
- return env
- def print_installation_guide() -> None:
- """Print guide for installing missing dependencies."""
- print(f"\n{Color.BOLD}Installation Guide:{Color.RESET}\n")
-
- print(f"{Color.BOLD}1. CMake:{Color.RESET}")
- print(" Download from: https://cmake.org/download/")
- print(" Or use: winget install Kitware.CMake\n")
-
- print(f"{Color.BOLD}2. Ninja:{Color.RESET}")
- print(" Download from: https://github.com/ninja-build/ninja/releases")
- print(" Or use: winget install Ninja-build.Ninja")
- print(" Or use: choco install ninja\n")
-
- print(f"{Color.BOLD}3. Visual Studio:{Color.RESET}")
- print(" Download: https://visualstudio.microsoft.com/downloads/")
- print(" Install 'Desktop development with C++' workload")
- print(" Or use: winget install Microsoft.VisualStudio.2022.Community")
- print(" --override '--add Microsoft.VisualStudio.Workload.NativeDesktop'\n")
-
- print(f"{Color.BOLD}4. Qt 6.6.3:{Color.RESET}")
- print(" Download: https://www.qt.io/download-open-source")
- print(" Install Qt 6.6.3 with MSVC 2019 64-bit component")
- print(" Required modules: qt5compat, qtmultimedia")
- print(" Or set QT_ROOT environment variable to your Qt installation\n")
- def check_winget_available() -> bool:
- """Check if winget is available."""
- try:
- result = run_command(['winget', '--version'], capture_output=True, check=False)
- return result.returncode == 0
- except FileNotFoundError:
- return False
- def install_visual_studio_direct() -> bool:
- """Download and install Visual Studio using the bootstrapper directly."""
- import tempfile
- import urllib.request
-
- info("Attempting direct Visual Studio installation...")
- info("This will download ~3MB bootstrapper, then download the full installer")
- print()
-
- # Download the bootstrapper
- vs_url = "https://aka.ms/vs/17/release/vs_community.exe"
-
- try:
- with tempfile.TemporaryDirectory() as tmpdir:
- installer_path = os.path.join(tmpdir, "vs_community.exe")
-
- info("Downloading Visual Studio installer...")
- urllib.request.urlretrieve(vs_url, installer_path)
- success("Installer downloaded")
-
- info("Running Visual Studio installer...")
- info("This may take 10-30 minutes depending on your internet connection")
- print()
-
- # Run the installer with the C++ workload
- cmd = [
- installer_path,
- "--add", "Microsoft.VisualStudio.Workload.NativeDesktop",
- "--includeRecommended",
- "--passive", # Show progress but don't require interaction
- "--norestart",
- "--wait"
- ]
-
- result = subprocess.run(cmd, check=False)
-
- if result.returncode == 0 or result.returncode == 3010: # 3010 = success but reboot required
- success("Visual Studio installed successfully!")
- if result.returncode == 3010:
- warning("A restart may be required to complete the installation")
- return True
- else:
- error(f"Visual Studio installation failed (exit code: {result.returncode})")
- return False
-
- except Exception as e:
- error(f"Failed to download/install Visual Studio: {e}")
- return False
- def install_qt_interactive() -> bool:
- """Download and launch Qt online installer with instructions."""
- import tempfile
- import urllib.request
-
- print()
- info(f"{Color.BOLD}Qt Installation Helper{Color.RESET}")
- info("Qt requires an interactive installation (needs Qt account)")
- print()
- info("This will:")
- info(" 1. Download the Qt online installer (~2MB)")
- info(" 2. Launch it for you")
- info(" 3. Guide you through the installation")
- print()
-
- # Qt online installer URL - try multiple mirrors
- qt_urls = [
- "https://d13lb3tujbc8s0.cloudfront.net/onlineinstallers/qt-unified-windows-x64-online.exe",
- "https://download.qt.io/archive/online_installers/4.8/qt-unified-windows-x64-4.8.0-online.exe",
- "https://download.qt.io/official_releases/online_installers/qt-unified-windows-x64-4.8-online.exe",
- ]
- qt_urls = [
- "https://d13lb3tujbc8s0.cloudfront.net/onlineinstallers/qt-unified-windows-x64-online.exe",
- "https://download.qt.io/archive/online_installers/4.8/qt-unified-windows-x64-4.8.0-online.exe",
- "https://download.qt.io/official_releases/online_installers/qt-unified-windows-x64-4.8-online.exe",
- ]
-
- try:
- with tempfile.TemporaryDirectory() as tmpdir:
- installer_path = os.path.join(tmpdir, "qt-installer.exe")
-
- # Try each URL until one works
- downloaded = False
- for qt_url in qt_urls:
- try:
- info(f"Downloading Qt online installer from {qt_url.split('/')[2]}...")
- urllib.request.urlretrieve(qt_url, installer_path)
- success("Qt installer downloaded")
- downloaded = True
- break
- except Exception as e:
- warning(f"Failed to download from {qt_url.split('/')[2]}: {e}")
- continue
-
- if not downloaded:
- error("Could not download Qt installer from any mirror")
- print()
- info("Please download manually from:")
- info("https://www.qt.io/download-qt-installer")
- print()
- info("Then run the installer and follow the steps below:")
- print()
- print(f"{Color.BOLD}=== Installation Steps ==={Color.RESET}")
- print()
- print(f"{Color.BOLD}1. Qt Account:{Color.RESET}")
- print(" - Login with your Qt account (or create one - it's free)")
- print()
- print(f"{Color.BOLD}2. Installation Path:{Color.RESET}")
- print(" - Use default: C:\\Qt")
- print(" - Or custom path and set QT_ROOT environment variable")
- print()
- print(f"{Color.BOLD}3. Select Components (CRITICAL):{Color.RESET}")
- print(" - Expand 'Qt 6.6.3' (or latest 6.x)")
- print(" - CHECK: 'MSVC 2019 64-bit' (or 'MSVC 2022 64-bit')")
- print(" - CHECK: 'Qt 5 Compatibility Module'")
- print(" - CHECK: 'Qt Multimedia'")
- print(" - Uncheck everything else to save space")
- print()
- print(f"{Color.BOLD}4. After Installation:{Color.RESET}")
- print(" - Restart this terminal")
- print(" - Run this script again")
- print()
-
- # Try to open browser to Qt download page
- try:
- import webbrowser
- webbrowser.open("https://www.qt.io/download-qt-installer")
- success("Opened Qt download page in your browser")
- except Exception:
- pass
-
- return False
-
- print()
-
- print(f"{Color.BOLD}=== IMPORTANT: Follow these steps in the installer ==={Color.RESET}")
- print()
- print(f"{Color.BOLD}1. Qt Account:{Color.RESET}")
- print(" - Login with your Qt account (or create one - it's free)")
- print()
- print(f"{Color.BOLD}2. Installation Path:{Color.RESET}")
- print(" - Use default: C:\\Qt")
- print(" - Or custom path and set QT_ROOT environment variable")
- print()
- print(f"{Color.BOLD}3. Select Components (CRITICAL):{Color.RESET}")
- print(" - Expand 'Qt 6.6.3' (or latest 6.x)")
- print(" - CHECK: 'MSVC 2019 64-bit' (or 'MSVC 2022 64-bit')")
- print(" - CHECK: 'Qt 5 Compatibility Module'")
- print(" - CHECK: 'Qt Multimedia'")
- print(" - Uncheck everything else to save space")
- print()
- print(f"{Color.BOLD}4. After Installation:{Color.RESET}")
- print(" - Restart this terminal")
- print(" - Run this script again")
- print()
- print(f"{Color.BOLD}Press Enter to launch the Qt installer...{Color.RESET}")
- input()
-
- info("Launching Qt installer...")
- # Launch installer (non-blocking so script can continue)
- subprocess.Popen([installer_path], shell=True)
-
- print()
- success("Qt installer launched!")
- warning("Complete the installation, then restart your terminal and run this script again")
- print()
- return False # Return False since Qt isn't installed yet
-
- except Exception as e:
- error(f"Failed to download/launch Qt installer: {e}")
- return False
- def auto_install_dependencies() -> bool:
- """Attempt to auto-install missing dependencies using winget."""
- if not check_winget_available():
- warning("winget not available - cannot auto-install")
- warning("Please install dependencies manually or update to Windows 10/11 with winget")
- return False
-
- info("Attempting to auto-install missing dependencies using winget...")
- print()
-
- # Check what's missing
- cmake_ok, _ = check_cmake()
- ninja_ok = check_ninja()
- msvc_ok, _ = check_msvc()
- qt_ok, _, _ = check_qt()
-
- installs_needed = []
- if not cmake_ok:
- installs_needed.append(('CMake', 'Kitware.CMake'))
- if not ninja_ok:
- installs_needed.append(('Ninja', 'Ninja-build.Ninja'))
- if not msvc_ok:
- installs_needed.append(('Visual Studio 2022', 'Microsoft.VisualStudio.2022.Community',
- '--override "--add Microsoft.VisualStudio.Workload.NativeDesktop"'))
-
- if not installs_needed and not qt_ok:
- warning("Qt installation requires manual download from qt.io")
- warning("winget Qt packages may not include required MSVC toolchain")
- print()
- info("Would you like to download and run the Qt installer now?")
-
- # Try to launch Qt installer helper
- if install_qt_interactive():
- return True
- else:
- # Installer was launched or failed - either way, user needs to complete it
- return False
-
- if not installs_needed:
- return True
-
- print(f"{Color.BOLD}Installing the following packages:{Color.RESET}")
- for item in installs_needed:
- print(f" - {item[0]}")
- print()
-
- success_count = 0
- needs_restart = False
- vs_failed = False
- vs_needed = False
-
- for item in installs_needed:
- name = item[0]
- package_id = item[1]
- extra_args = item[2] if len(item) > 2 else None
-
- info(f"Installing {name}...")
- print(f" This may take several minutes, please wait...")
- print()
-
- cmd = ['winget', 'install', '--id', package_id, '--accept-package-agreements', '--accept-source-agreements']
- if extra_args:
- cmd.extend(extra_args.split())
-
- # Show live output - don't capture
- try:
- result = subprocess.run(cmd, check=False)
- print() # Add spacing after output
-
- if result.returncode == 0:
- success(f"{name} installed successfully")
- success_count += 1
-
- # CMake and Ninja need PATH restart
- if 'CMake' in name or 'Ninja' in name:
- needs_restart = True
- else:
- error(f"Failed to install {name} (exit code: {result.returncode})")
-
- # Visual Studio often needs admin rights or has winget issues
- if 'Visual Studio' in name:
- vs_failed = True
- vs_needed = True
- else:
- warning("You may need to run this script as Administrator")
- except Exception as e:
- error(f"Error installing {name}: {e}")
-
- print() # Add spacing between installs
-
- print()
-
- # Try direct VS install if winget failed
- if vs_failed:
- print()
- warning("winget failed to install Visual Studio (common issue)")
- info("Attempting direct installation using VS bootstrapper...")
- print()
-
- if install_visual_studio_direct():
- success("Visual Studio installed via direct download!")
- success_count += 1
- vs_failed = False
- else:
- print()
- info(f"{Color.BOLD}Visual Studio Installation Failed{Color.RESET}")
- info("Please install manually:")
- info("1. Download: https://visualstudio.microsoft.com/downloads/")
- info("2. Run the installer")
- info("3. Select 'Desktop development with C++'")
- info("4. Install and restart this script")
- print()
-
- # Final summary
- print()
- if success_count == len(installs_needed):
- success("All dependencies installed successfully!")
- if needs_restart:
- warning("CMake/Ninja were installed - you must restart this terminal!")
- warning("Close this terminal, open a new one, and run this script again.")
- return True
- elif success_count > 0:
- warning(f"Installed {success_count}/{len(installs_needed)} dependencies")
-
- if needs_restart:
- print()
- info(f"{Color.BOLD}IMPORTANT: CMake/Ninja were just installed!{Color.RESET}")
- info("You MUST restart your terminal for PATH changes to take effect.")
- info("Then run this script again to continue.")
- print()
-
- warning("Please install remaining dependencies manually")
- print_installation_guide()
- return False
- else:
- error("Auto-install failed")
- warning("Please install dependencies manually")
- print_installation_guide()
- return False
- def check_dependencies() -> Tuple[bool, Optional[Path]]:
- """Check all required dependencies."""
- info("Checking dependencies...")
- print()
-
- all_ok = True
- qt_path = None
-
- cmake_ok, _ = check_cmake()
- if not cmake_ok:
- error("CMake 3.21+ not found")
- all_ok = False
-
- ninja_ok = check_ninja()
- if not ninja_ok:
- error("Ninja not found")
- all_ok = False
-
- msvc_ok, _ = check_msvc()
- if not msvc_ok:
- error("Visual Studio with C++ tools not found")
- all_ok = False
-
- qt_ok, qt_path, _ = check_qt()
- if not qt_ok:
- error("Qt 6.x with MSVC not found")
- all_ok = False
-
- print()
-
- if not all_ok:
- error("Some dependencies are missing!")
- return False, None
-
- success("All dependencies found!")
- return True, qt_path
- def configure_project(build_dir: Path, build_type: str, qt_path: Optional[Path],
- msvc_env: dict) -> None:
- """Configure the project with CMake."""
- info(f"Configuring project (Build type: {build_type})...")
- print(" Running CMake configuration...")
-
- cmake_args = [
- 'cmake',
- '-S', '.',
- '-B', str(build_dir),
- '-G', 'Ninja',
- f'-DCMAKE_BUILD_TYPE={build_type}',
- '-DDEFAULT_LANG=en'
- ]
-
- if qt_path:
- cmake_args.append(f'-DCMAKE_PREFIX_PATH={qt_path}')
-
- # Check for CMAKE_BUILD_PARALLEL_LEVEL env var
- parallel = os.environ.get('CMAKE_BUILD_PARALLEL_LEVEL')
- if parallel:
- info(f" Build parallelism limited to {parallel} jobs")
-
- run_command(cmake_args, env=msvc_env)
- success("Project configured")
- def build_project(build_dir: Path, msvc_env: dict) -> None:
- """Build the project."""
- info("Building project...")
-
- parallel = os.environ.get('CMAKE_BUILD_PARALLEL_LEVEL')
- if parallel:
- info(f" Using {parallel} parallel jobs (set via CMAKE_BUILD_PARALLEL_LEVEL)")
- else:
- warning(" Using all CPU cores - set CMAKE_BUILD_PARALLEL_LEVEL=2 to reduce load in VMs")
-
- print(" Compiling (this may take several minutes)...")
-
- run_command(['cmake', '--build', str(build_dir)], env=msvc_env)
- success("Project built successfully")
- def deploy_qt(build_dir: Path, qt_path: Path, app_name: str, build_type: str) -> None:
- """Deploy Qt dependencies."""
- info("Deploying Qt dependencies...")
-
- app_dir = build_dir / "bin"
- exe_path = app_dir / f"{app_name}.exe"
-
- if not exe_path.exists():
- error(f"Executable not found: {exe_path}")
- sys.exit(1)
-
- windeployqt = qt_path / "bin" / "windeployqt.exe"
- if not windeployqt.exists():
- error(f"windeployqt not found at {windeployqt}")
- sys.exit(1)
-
- qml_dir = Path("ui/qml")
-
- # Map build type to windeployqt mode flag
- mode_flag = {
- "Debug": "--debug",
- "Release": "--release",
- "RelWithDebInfo": "--release", # release DLLs + PDBs
- }[build_type]
-
- run_command([
- str(windeployqt),
- mode_flag,
- '--compiler-runtime', # ship VC++ runtime DLLs
- '--qmldir', str(qml_dir),
- str(exe_path)
- ])
-
- success("Qt dependencies deployed")
- def write_qt_conf(app_dir: Path) -> None:
- """Write qt.conf to configure Qt plugin paths."""
- info("Writing qt.conf...")
-
- qt_conf_content = """[Paths]
- Plugins = .
- Imports = qml
- Qml2Imports = qml
- Translations = translations
- """
-
- qt_conf_path = app_dir / "qt.conf"
- qt_conf_path.write_text(qt_conf_content, encoding='ascii')
- success("qt.conf written")
- def write_debug_scripts(app_dir: Path, app_name: str) -> None:
- """Write diagnostic scripts for troubleshooting."""
- info("Writing diagnostic scripts...")
-
- # run.cmd - Smart launcher with automatic fallback
- run_smart_content = """@echo off
- setlocal
- cd /d "%~dp0"
- echo ============================================
- echo Standard of Iron - Smart Launcher
- echo ============================================
- echo.
- REM Try OpenGL first
- echo [1/2] Attempting OpenGL rendering...
- "%~dp0{app_name}.exe" 2>&1
- set EXIT_CODE=%ERRORLEVEL%
- REM Check for crash (access violation = -1073741819)
- if %EXIT_CODE% EQU -1073741819 (
- echo.
- echo [CRASH] OpenGL rendering failed!
- echo [INFO] This is common on VMs or older hardware
- echo.
- echo [2/2] Retrying with software rendering...
- echo.
- set QT_QUICK_BACKEND=software
- set QT_OPENGL=software
- "%~dp0{app_name}.exe"
- exit /b %ERRORLEVEL%
- )
- if %EXIT_CODE% NEQ 0 (
- echo.
- echo [ERROR] Application exited with code: %EXIT_CODE%
- echo [HINT] Try running run_debug.cmd for detailed logs
- pause
- exit /b %EXIT_CODE%
- )
- exit /b 0
- """.format(app_name=app_name)
-
- run_smart_path = app_dir / "run.cmd"
- run_smart_path.write_text(run_smart_content, encoding='ascii')
-
- # run_debug.cmd - Desktop OpenGL with verbose logging
- run_debug_content = """@echo off
- setlocal
- cd /d "%~dp0"
- set QT_DEBUG_PLUGINS=1
- set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.rhi.*=true
- set QT_OPENGL=desktop
- set QT_QPA_PLATFORM=windows
- echo Starting with Desktop OpenGL...
- echo Logging to runlog.txt
- "%~dp0{app_name}.exe" 1> "%~dp0runlog.txt" 2>&1
- echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.txt"
- type "%~dp0runlog.txt"
- pause
- """.format(app_name=app_name)
-
- run_debug_path = app_dir / "run_debug.cmd"
- run_debug_path.write_text(run_debug_content, encoding='ascii')
-
- # run_debug_softwaregl.cmd - Software OpenGL fallback
- run_debug_softwaregl_content = """@echo off
- setlocal
- cd /d "%~dp0"
- set QT_DEBUG_PLUGINS=1
- set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.quick.*=true;qt.rhi.*=true
- set QT_OPENGL=software
- set QT_QUICK_BACKEND=software
- set QT_QPA_PLATFORM=windows
- set QMLSCENE_DEVICE=softwarecontext
- echo Starting with Qt Quick Software Renderer (no OpenGL)...
- echo This is the safest mode for VMs and old hardware
- echo Logging to runlog_software.txt
- "%~dp0{app_name}.exe" 1> "%~dp0runlog_software.txt" 2>&1
- echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog_software.txt"
- type "%~dp0runlog_software.txt"
- pause
- """.format(app_name=app_name)
-
- run_debug_softwaregl_path = app_dir / "run_debug_softwaregl.cmd"
- run_debug_softwaregl_path.write_text(run_debug_softwaregl_content, encoding='ascii')
-
- # run_debug_angle.cmd - ANGLE (OpenGL ES via D3D)
- run_debug_angle_content = """@echo off
- setlocal
- cd /d "%~dp0"
- set QT_DEBUG_PLUGINS=1
- set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.rhi.*=true
- set QT_OPENGL=angle
- set QT_ANGLE_PLATFORM=d3d11
- set QT_QPA_PLATFORM=windows
- echo Starting with ANGLE (OpenGL ES via D3D11)...
- echo Logging to runlog_angle.txt
- "%~dp0{app_name}.exe" 1> "%~dp0runlog_angle.txt" 2>&1
- echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog_angle.txt"
- type "%~dp0runlog_angle.txt"
- pause
- """.format(app_name=app_name)
-
- run_debug_angle_path = app_dir / "run_debug_angle.cmd"
- run_debug_angle_path.write_text(run_debug_angle_content, encoding='ascii')
- success(f"Diagnostic scripts written: run.cmd (smart), run_debug.cmd, run_debug_softwaregl.cmd, run_debug_angle.cmd")
- def copy_gl_angle_fallbacks(app_dir: Path, qt_path: Path) -> None:
- """Copy GL/ANGLE fallback DLLs for graphics compatibility."""
- info("Copying GL/ANGLE fallback DLLs...")
-
- qt_bin = qt_path / "bin"
- fallback_dlls = [
- "d3dcompiler_47.dll",
- "opengl32sw.dll",
- "libEGL.dll",
- "libGLESv2.dll"
- ]
-
- copied_count = 0
- for dll_name in fallback_dlls:
- src = qt_bin / dll_name
- if src.exists():
- dst = app_dir / dll_name
- shutil.copy2(src, dst)
- info(f" Copied {dll_name}")
- copied_count += 1
- else:
- warning(f" {dll_name} not found in Qt bin directory")
-
- if copied_count > 0:
- success(f"Copied {copied_count} GL/ANGLE fallback DLL(s)")
- else:
- warning("No GL/ANGLE fallback DLLs found")
- def copy_assets(build_dir: Path) -> None:
- """Copy assets to build directory."""
- info("Copying assets...")
-
- app_dir = build_dir / "bin"
- assets_src = Path("assets")
- assets_dst = app_dir / "assets"
-
- if assets_dst.exists():
- shutil.rmtree(assets_dst)
-
- shutil.copytree(assets_src, assets_dst)
- success("Assets copied")
- def create_package(build_dir: Path, build_type: str) -> Path:
- """Create distributable package."""
- info("Creating distributable package...")
- warning("This may take several minutes with no progress indicator...")
- print(" Compressing files (CPU-intensive, please wait)...")
-
- app_dir = build_dir / "bin"
- package_name = f"standard_of_iron-win-x64-{build_type}.zip"
- package_path = Path(package_name)
-
- if package_path.exists():
- package_path.unlink()
-
- import time
- start_time = time.time()
-
- shutil.make_archive(
- package_path.stem,
- 'zip',
- app_dir
- )
-
- elapsed = time.time() - start_time
- success(f"Package created: {package_path} (took {elapsed:.1f}s)")
- return package_path
- def main() -> int:
- """Main entry point."""
- import time
- start_time = time.time()
-
- parser = argparse.ArgumentParser(
- description="Build Standard-of-Iron on Windows",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog=__doc__
- )
- parser.add_argument(
- '--skip-checks',
- action='store_true',
- help='Skip dependency checks'
- )
- parser.add_argument(
- '--no-auto-install',
- action='store_true',
- help='Do NOT auto-install missing dependencies (show manual instructions instead)'
- )
- parser.add_argument(
- '--build-type',
- choices=['Debug', 'Release', 'RelWithDebInfo'],
- default='Release',
- help='CMake build type (default: Release)'
- )
- parser.add_argument(
- '--clean',
- action='store_true',
- help='Clean build directory before building'
- )
- parser.add_argument(
- '--deploy-only',
- action='store_true',
- help='Only deploy Qt dependencies (skip build)'
- )
- parser.add_argument(
- '--no-package',
- action='store_true',
- help='Skip creating distributable package'
- )
-
- args = parser.parse_args()
-
- # Check we're on Windows
- check_windows()
-
- # Repository root
- repo_root = Path(__file__).parent.parent
- os.chdir(repo_root)
-
- build_dir = Path("build")
- qt_path = None
-
- # Check dependencies unless skipped
- if not args.skip_checks and not args.deploy_only:
- deps_ok, qt_path = check_dependencies()
-
- if not deps_ok:
- # Auto-install by default unless --no-auto-install
- if not args.no_auto_install:
- info("Attempting auto-install of missing dependencies...")
- print()
- if auto_install_dependencies():
- warning("Dependencies installed - please restart your terminal and run this script again")
- return 1
- else:
- print()
- print_installation_guide()
- return 1
- else:
- info("Auto-install disabled (--no-auto-install flag)")
- print()
- print_installation_guide()
- return 1
- else:
- # Try to find Qt even if checks are skipped
- _, qt_path, _ = check_qt()
-
- # Clean build directory if requested
- if args.clean and build_dir.exists():
- info("Cleaning build directory...")
- shutil.rmtree(build_dir)
- success("Build directory cleaned")
-
- # Set up MSVC environment
- if not args.deploy_only:
- msvc_env = setup_msvc_environment()
-
- # Configure
- configure_project(build_dir, args.build_type, qt_path, msvc_env)
-
- # Build
- build_project(build_dir, msvc_env)
-
- # Deploy Qt
- if qt_path:
- deploy_qt(build_dir, qt_path, "standard_of_iron", args.build_type)
-
- # Write qt.conf
- app_dir = build_dir / "bin"
- write_qt_conf(app_dir)
-
- # Write diagnostic scripts
- write_debug_scripts(app_dir, "standard_of_iron")
-
- # Copy GL/ANGLE fallbacks
- copy_gl_angle_fallbacks(app_dir, qt_path)
- else:
- warning("Qt path not found, skipping windeployqt")
-
- # Copy assets
- copy_assets(build_dir)
-
- # Create package
- if not args.no_package:
- package_path = create_package(build_dir, args.build_type)
- print()
- elapsed = time.time() - start_time
- success(f"Build complete! (total time: {elapsed:.1f}s)")
- info(f"Package: {package_path}")
- info(f"Executable: {build_dir / 'bin' / 'standard_of_iron.exe'}")
- else:
- print()
- elapsed = time.time() - start_time
- success(f"Build complete! (total time: {elapsed:.1f}s)")
- info(f"Executable: {build_dir / 'bin' / 'standard_of_iron.exe'}")
- info("Run with debug scripts: run_debug.cmd, run_debug_softwaregl.cmd, run_debug_angle.cmd")
-
- return 0
- if __name__ == '__main__':
- try:
- sys.exit(main())
- except KeyboardInterrupt:
- print()
- warning("Build interrupted by user")
- sys.exit(130)
- except Exception as e:
- error(f"Unexpected error: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
|