|
|
@@ -0,0 +1,499 @@
|
|
|
+#!/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. Guiding installation of missing dependencies
|
|
|
+3. Configuring and building the project with proper MSVC setup
|
|
|
+4. Deploying Qt dependencies
|
|
|
+5. Copying assets and creating a distributable package
|
|
|
+
|
|
|
+Usage:
|
|
|
+ python scripts/build-windows.py # Full build with checks
|
|
|
+ python scripts/build-windows.py --skip-checks # Skip dependency checks
|
|
|
+ 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 --help # Show this help
|
|
|
+
|
|
|
+Requirements:
|
|
|
+ - Python 3.7+
|
|
|
+ - CMake 3.21+
|
|
|
+ - Ninja build system
|
|
|
+ - Visual Studio 2019/2022 with C++ tools
|
|
|
+ - Qt 6.6.3 or compatible (with msvc2019_64 or msvc2022_64)
|
|
|
+"""
|
|
|
+
|
|
|
+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}[✗]{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")
|
|
|
+
|
|
|
+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.home() / "Qt",
|
|
|
+ Path(os.environ.get('QT_ROOT', '')) if os.environ.get('QT_ROOT') else None,
|
|
|
+ ]
|
|
|
+
|
|
|
+ for base_path in possible_paths:
|
|
|
+ if base_path and base_path.exists():
|
|
|
+ # Look for Qt 6.x with msvc2019_64 or msvc2022_64
|
|
|
+ for qt_version_dir in base_path.glob("6.*"):
|
|
|
+ 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 check_qt() -> Tuple[bool, Optional[Path], Optional[str]]:
|
|
|
+ """Check if Qt is installed."""
|
|
|
+ qt_path = find_qt()
|
|
|
+ if qt_path:
|
|
|
+ qmake = qt_path / "bin" / "qmake.exe"
|
|
|
+ try:
|
|
|
+ result = run_command([str(qmake), '-query', 'QT_VERSION'], capture_output=True)
|
|
|
+ version = result.stdout.strip()
|
|
|
+ success(f"Qt {version} found at {qt_path}")
|
|
|
+ return True, qt_path, version
|
|
|
+ except (subprocess.CalledProcessError, FileNotFoundError):
|
|
|
+ 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_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!")
|
|
|
+ print_installation_guide()
|
|
|
+ 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})...")
|
|
|
+
|
|
|
+ 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}')
|
|
|
+
|
|
|
+ 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...")
|
|
|
+
|
|
|
+ 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 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...")
|
|
|
+
|
|
|
+ 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()
|
|
|
+
|
|
|
+ shutil.make_archive(
|
|
|
+ package_path.stem,
|
|
|
+ 'zip',
|
|
|
+ app_dir
|
|
|
+ )
|
|
|
+
|
|
|
+ success(f"Package created: {package_path}")
|
|
|
+ return package_path
|
|
|
+
|
|
|
+def main() -> int:
|
|
|
+ """Main entry point."""
|
|
|
+ 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(
|
|
|
+ '--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:
|
|
|
+ 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)
|
|
|
+ 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()
|
|
|
+ info(f"Build complete! Package available at: {package_path}")
|
|
|
+ info(f"Executable location: {build_dir / 'bin' / 'standard_of_iron.exe'}")
|
|
|
+ else:
|
|
|
+ print()
|
|
|
+ info(f"Build complete!")
|
|
|
+ info(f"Executable location: {build_dir / 'bin' / 'standard_of_iron.exe'}")
|
|
|
+
|
|
|
+ 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)
|