Browse Source

Merge pull request #319 from djeada/copilot/build-deploy-qt-artifact

Add Windows CI workflow and local build script with MSVC support
Adam Djellouli 1 month ago
parent
commit
30821e7dd6
4 changed files with 841 additions and 10 deletions
  1. 171 0
      .github/workflows/windows.yml
  2. 27 10
      CMakeLists.txt
  3. 144 0
      scripts/README.md
  4. 499 0
      scripts/build-windows.py

+ 171 - 0
.github/workflows/windows.yml

@@ -0,0 +1,171 @@
+name: Windows Build (tags only)
+
+on:
+  push:
+    tags:
+      - 'v*'   # run only on tags like v1.2.3
+
+jobs:
+  build-windows:
+    runs-on: windows-latest
+
+    env:
+      BUILD_TYPE: Release
+      APP_NAME: standard_of_iron
+      APP_DIR: build\bin
+      QML_DIR: ui\qml
+
+      QT_VERSION: 6.8.2
+      QT_ARCH: win64_msvc2022_64
+      QT_ROOT: D:\a\Standard-of-Iron\Qt
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/setup-python@v5
+        with:
+          python-version: '3.12'
+
+      - uses: ilammy/msvc-dev-cmd@v1
+
+      # Fast path: official installer (allowed to fail)
+      - name: Install Qt (fast path)
+        id: fastqt
+        continue-on-error: true
+        uses: jurplel/install-qt-action@v4
+        with:
+          version: ${{ env.QT_VERSION }}
+          host: windows
+          arch:  ${{ env.QT_ARCH }}
+          cache: true
+          aqtversion: '==3.3.0'
+          modules: 'qt5compat qtmultimedia'
+          tools-only: false
+
+      # Fallback: install with aqt directly if fast path fails
+      - name: Install Qt (fallback via aqt)
+        if: ${{ steps.fastqt.outcome != 'success' }}
+        shell: pwsh
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install "aqtinstall==3.3.0" "py7zr==1.0.*"
+
+          $qtDir = Join-Path $env:QT_ROOT "$env:QT_VERSION\$env:QT_ARCH"
+          New-Item -ItemType Directory -Force -Path $qtDir | Out-Null
+
+          try {
+            python -m aqt install-qt windows desktop $env:QT_VERSION $env:QT_ARCH `
+              --outputdir "$env:QT_ROOT" `
+              -m qtmultimedia qt5compat qtdeclarative
+          } catch {
+            Write-Warning "Explicit module install failed. Falling back to '-m all'. Error: $($_.Exception.Message)"
+            python -m aqt install-qt windows desktop $env:QT_VERSION $env:QT_ARCH `
+              --outputdir "$env:QT_ROOT" -m all
+          }
+
+          if (!(Test-Path "$qtDir\lib\cmake\Qt6\Qt6Config.cmake")) {
+            Write-Error "Qt6Config.cmake not found in $qtDir after fallback install."
+            exit 1
+          }
+
+          # Export env and PATH
+          echo "Qt6_DIR=$qtDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+          echo "$qtDir\bin"       | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+
+      # Normalize env for both paths
+      - name: Normalize Qt env
+        shell: pwsh
+        run: |
+          $candidates = @()
+          if ($env:Qt6_DIR)     { $candidates += $env:Qt6_DIR }
+          if ($env:QT_ROOT_DIR) { $candidates += $env:QT_ROOT_DIR }
+          $candidates += (Join-Path $env:QT_ROOT "$env:QT_VERSION\$env:QT_ARCH")
+
+          $qtDir = $null
+          foreach ($c in $candidates | Select-Object -Unique) {
+            if ($c -and (Test-Path (Join-Path $c 'lib\cmake\Qt6\Qt6Config.cmake'))) { $qtDir = $c; break }
+          }
+          if (-not $qtDir) { Write-Error "Could not locate Qt6. Checked: $($candidates -join ', ')"; exit 1 }
+
+          echo "Qt6_DIR=$qtDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+          echo "$qtDir\bin"     | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+          Write-Host "Qt6_DIR=$qtDir"
+
+      - name: Verify Qt installation
+        shell: pwsh
+        run: |
+          Write-Host "Qt6_DIR=$env:Qt6_DIR"
+          if (!(Test-Path "$env:Qt6_DIR\lib\cmake\Qt6\Qt6Config.cmake")) { Write-Error "Qt6Config.cmake not found in $env:Qt6_DIR"; exit 1 }
+          if (!(Get-Command windeployqt -ErrorAction SilentlyContinue))   { Write-Error "windeployqt not found on PATH"; exit 1 }
+
+      - name: Configure (CMake + Ninja)
+        shell: pwsh
+        run: |
+          cmake -S . -B build -G "Ninja" `
+            -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} `
+            -DDEFAULT_LANG=en `
+            -DCMAKE_PREFIX_PATH="$env:Qt6_DIR"
+
+      - name: Build
+        run: cmake --build build
+
+      - name: Deploy Qt
+        shell: pwsh
+        run: |
+          $mode = if ("${{ env.BUILD_TYPE }}" -eq "Debug") { "--debug" } else { "--release" }
+          windeployqt $mode --compiler-runtime --qmldir "$env:QML_DIR" "$env:APP_DIR\${{ env.APP_NAME }}.exe"
+
+      # SAFE: write qt.conf without here-strings
+      - name: Write qt.conf
+        shell: pwsh
+        run: |
+          @(
+            '[Paths]'
+            'Plugins = .'
+            'Imports = qml'
+            'Qml2Imports = qml'
+            'Translations = translations'
+          ) | Set-Content -Encoding ASCII "$env:APP_DIR\qt.conf"
+
+      # SAFE: write run_debug.cmd without here-strings
+      - name: Add run_debug.cmd
+        shell: pwsh
+        run: |
+          @(
+            '@echo off'
+            'setlocal'
+            'set QT_DEBUG_PLUGINS=1'
+            'set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true'
+            'set QT_QPA_PLATFORM=windows'
+            '"%~dp0standard_of_iron.exe" 1> "%~dp0runlog.txt" 2>&1'
+            'echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.txt"'
+            'pause'
+          ) | Set-Content -Encoding ASCII "$env:APP_DIR\run_debug.cmd"
+
+      - name: Copy assets
+        shell: pwsh
+        run: |
+          if (!(Test-Path "$env:APP_DIR\assets")) { New-Item -ItemType Directory -Path "$env:APP_DIR\assets" | Out-Null }
+          robocopy assets "$env:APP_DIR\assets" /E /NFL /NDL /NJH /NJS /nc /ns /np
+          if ($LASTEXITCODE -ge 8) { exit $LASTEXITCODE } else { exit 0 }
+
+      - name: Sanity check deployed files
+        shell: pwsh
+        run: |
+          if (!(Test-Path "$env:APP_DIR\platforms\qwindows.dll")) { Write-Error "Missing platforms\qwindows.dll"; exit 1 }
+          if (!(Test-Path "$env:APP_DIR\Qt6Core.dll"))            { Write-Error "Missing Qt6Core.dll"; exit 1 }
+          if (!(Test-Path "$env:APP_DIR\Qt6Gui.dll"))             { Write-Error "Missing Qt6Gui.dll"; exit 1 }
+          if (!(Test-Path "$env:APP_DIR\Qt6Qml.dll"))             { Write-Error "Missing Qt6Qml.dll (QML app?)"; exit 1 }
+          Write-Host "Deployment looks OK."
+
+      - name: Zip
+        shell: pwsh
+        run: |
+          $zip = "standard_of_iron-win-x64-${{ env.BUILD_TYPE }}.zip"
+          if (Test-Path $zip) { Remove-Item $zip -Force }
+          Compress-Archive -Path "$env:APP_DIR\*" -DestinationPath $zip -Force
+
+      - uses: actions/upload-artifact@v4
+        with:
+          name: windows-${{ env.BUILD_TYPE }}
+          path: standard_of_iron-win-x64-${{ env.BUILD_TYPE }}.zip

+ 27 - 10
CMakeLists.txt

@@ -8,21 +8,38 @@ set(CMAKE_CXX_STANDARD 20)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 set(CMAKE_CXX_EXTENSIONS OFF)
 set(CMAKE_CXX_EXTENSIONS OFF)
 
 
+# ---- Windows-specific definitions ----
+if(WIN32)
+    add_compile_definitions(NOMINMAX)  # Prevent min/max macros from windows.h
+endif()
+
 # ---- Compiler Optimization Flags ----
 # ---- Compiler Optimization Flags ----
 if(CMAKE_BUILD_TYPE STREQUAL "Debug")
 if(CMAKE_BUILD_TYPE STREQUAL "Debug")
-    # Debug build: enable debugging symbols and disable optimizations
-    set(CMAKE_CXX_FLAGS_DEBUG "-g3 -O0 -DDEBUG")
-    set(CMAKE_C_FLAGS_DEBUG "-g3 -O0 -DDEBUG")
-    # Additional GDB-friendly flags
-    add_compile_options(-ggdb3)           # Maximum debug info for GDB
-    add_compile_options(-fno-omit-frame-pointer)  # Keep frame pointers for better backtraces
-    add_compile_options(-fno-inline)      # Don't inline functions for easier debugging
-    # Disable optimizations that make debugging harder
-    add_compile_options(-fno-optimize-sibling-calls)
+    if(MSVC)
+        # MSVC Debug flags
+        set(CMAKE_CXX_FLAGS_DEBUG "/Od /Zi /DDEBUG")
+        set(CMAKE_C_FLAGS_DEBUG "/Od /Zi /DDEBUG")
+    else()
+        # GCC/Clang Debug flags
+        set(CMAKE_CXX_FLAGS_DEBUG "-g3 -O0 -DDEBUG")
+        set(CMAKE_C_FLAGS_DEBUG "-g3 -O0 -DDEBUG")
+        # Additional GDB-friendly flags
+        add_compile_options(-ggdb3)           # Maximum debug info for GDB
+        add_compile_options(-fno-omit-frame-pointer)  # Keep frame pointers for better backtraces
+        add_compile_options(-fno-inline)      # Don't inline functions for easier debugging
+        # Disable optimizations that make debugging harder
+        add_compile_options(-fno-optimize-sibling-calls)
+    endif()
     message(STATUS "Building in DEBUG mode with GDB support")
     message(STATUS "Building in DEBUG mode with GDB support")
 else()
 else()
     # Release/RelWithDebInfo: optimize for performance
     # Release/RelWithDebInfo: optimize for performance
-    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -ffast-math")
+    if(MSVC)
+        # MSVC Release flags
+        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2")
+    else()
+        # GCC/Clang Release flags
+        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -ffast-math")
+    endif()
     set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)  # Enable LTO
     set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)  # Enable LTO
     message(STATUS "Building in RELEASE mode with optimizations")
     message(STATUS "Building in RELEASE mode with optimizations")
 endif()
 endif()

+ 144 - 0
scripts/README.md

@@ -0,0 +1,144 @@
+# Scripts Directory
+
+This directory contains utility scripts for building and maintaining the Standard-of-Iron project.
+
+## Build Scripts
+
+### `build-windows.py` - Windows Local Build Script
+
+A user-friendly Python script that automates the Windows build process. It verifies dependencies, sets up the MSVC environment, builds the project, and creates a distributable package.
+
+**Features:**
+- ✓ Checks for required dependencies (CMake, Ninja, MSVC, Qt)
+- ✓ Provides installation guidance for missing tools
+- ✓ Automatically sets up MSVC environment
+- ✓ Builds the project with proper configuration
+- ✓ Deploys Qt dependencies with windeployqt
+- ✓ Copies game assets
+- ✓ Creates distributable ZIP package
+
+**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)
+
+**Usage:**
+```bash
+# Full build with dependency checks
+python scripts/build-windows.py
+
+# Build in Debug mode
+python scripts/build-windows.py --build-type Debug
+
+# Clean build (remove build directory first)
+python scripts/build-windows.py --clean
+
+# Skip dependency checks (faster if you know everything is installed)
+python scripts/build-windows.py --skip-checks
+
+# Only deploy Qt dependencies (assumes project is already built)
+python scripts/build-windows.py --deploy-only
+
+# Build without creating package
+python scripts/build-windows.py --no-package
+
+# Show help
+python scripts/build-windows.py --help
+```
+
+**Output:**
+- Built executable: `build/bin/standard_of_iron.exe`
+- Distributable package: `standard_of_iron-win-x64-Release.zip`
+
+**Troubleshooting:**
+
+If the script can't find Qt, set the `QT_ROOT` environment variable:
+```powershell
+$env:QT_ROOT = "C:\Qt\6.6.3\msvc2019_64"
+python scripts/build-windows.py
+```
+
+If you encounter MSVC issues, ensure "Desktop development with C++" is installed in Visual Studio.
+
+---
+
+## Linux/macOS Scripts
+
+### `setup-deps.sh` - Dependency Installer
+
+Verifies and installs required toolchain and Qt/QML runtime modules for Linux (Debian/Ubuntu, Arch/Manjaro) and macOS.
+
+**Usage:**
+```bash
+./scripts/setup-deps.sh                  # Interactive install
+./scripts/setup-deps.sh --yes            # Non-interactive (assume yes)
+./scripts/setup-deps.sh --dry-run        # Show actions without installing
+./scripts/setup-deps.sh --no-install     # Only verify, don't install
+```
+
+---
+
+## Translation Scripts
+
+### `csv2ts.sh` - CSV to Qt Translation Files
+
+Converts CSV translation files to Qt `.ts` format.
+
+### `ts2csv.sh` - Qt Translation Files to CSV
+
+Exports Qt `.ts` translation files to CSV format for easier editing.
+
+**Usage:**
+```bash
+./scripts/csv2ts.sh    # Convert CSV to .ts files
+./scripts/ts2csv.sh    # Convert .ts files to CSV
+```
+
+---
+
+## Development Scripts
+
+### `debug-audio.sh` - Audio Debugging
+
+Helper script for debugging audio issues.
+
+### `remove-comments.sh` - Code Cleanup
+
+Removes comments from source files (use with caution).
+
+**Usage:**
+```bash
+./scripts/remove-comments.sh
+```
+
+---
+
+## Platform-Specific Build Instructions
+
+### Windows
+1. Run `python scripts/build-windows.py`
+2. Follow any installation prompts for missing dependencies
+3. Find the executable at `build/bin/standard_of_iron.exe`
+4. Distributable package: `standard_of_iron-win-x64-Release.zip`
+
+### Linux/macOS
+1. Run `./scripts/setup-deps.sh` to install dependencies
+2. Build with CMake:
+   ```bash
+   cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
+   cmake --build build
+   ```
+3. Find the executable at `build/bin/standard_of_iron`
+
+---
+
+## Contributing
+
+When adding new scripts:
+1. Add appropriate shebang line (`#!/usr/bin/env bash` or `#!/usr/bin/env python3`)
+2. Make scripts executable: `chmod +x scripts/your-script.sh`
+3. Include usage documentation in the script header
+4. Update this README with script description and usage
+5. Follow existing code style and patterns

+ 499 - 0
scripts/build-windows.py

@@ -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)