Browse Source

Make Windows builds self-contained & runnable on clean PCs

djeada 1 month ago
parent
commit
878294e7d1
2 changed files with 321 additions and 19 deletions
  1. 227 17
      .github/workflows/windows.yml
  2. 94 2
      scripts/build-windows.py

+ 227 - 17
.github/workflows/windows.yml

@@ -1,9 +1,6 @@
 name: Windows Build (tags only)
 name: Windows Build (tags only)
 
 
-on:
-  push:
-    tags:
-      - 'v*'   # run only on tags like v1.2.3
+on: [push]
 
 
 jobs:
 jobs:
   build-windows:
   build-windows:
@@ -36,7 +33,7 @@ jobs:
         with:
         with:
           version: ${{ env.QT_VERSION }}
           version: ${{ env.QT_VERSION }}
           host: windows
           host: windows
-          arch:  ${{ env.QT_ARCH }}
+          arch: ${{ env.QT_ARCH }}
           cache: true
           cache: true
           aqtversion: '==3.3.0'
           aqtversion: '==3.3.0'
           modules: 'qt5compat qtmultimedia'
           modules: 'qt5compat qtmultimedia'
@@ -70,7 +67,7 @@ jobs:
 
 
           # Export env and PATH
           # Export env and PATH
           echo "Qt6_DIR=$qtDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
           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
+          echo "$qtDir\bin"     | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 
 
       # Normalize env for both paths
       # Normalize env for both paths
       - name: Normalize Qt env
       - name: Normalize Qt env
@@ -83,9 +80,16 @@ jobs:
 
 
           $qtDir = $null
           $qtDir = $null
           foreach ($c in $candidates | Select-Object -Unique) {
           foreach ($c in $candidates | Select-Object -Unique) {
-            if ($c -and (Test-Path (Join-Path $c 'lib\cmake\Qt6\Qt6Config.cmake'))) { $qtDir = $c; break }
+            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
           }
           }
-          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 "Qt6_DIR=$qtDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
           echo "$qtDir\bin"     | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
           echo "$qtDir\bin"     | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
@@ -95,8 +99,14 @@ jobs:
         shell: pwsh
         shell: pwsh
         run: |
         run: |
           Write-Host "Qt6_DIR=$env:Qt6_DIR"
           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 }
+          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)
       - name: Configure (CMake + Ninja)
         shell: pwsh
         shell: pwsh
@@ -115,7 +125,7 @@ jobs:
           $mode = if ("${{ env.BUILD_TYPE }}" -eq "Debug") { "--debug" } else { "--release" }
           $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"
           windeployqt $mode --compiler-runtime --qmldir "$env:QML_DIR" "$env:APP_DIR\${{ env.APP_NAME }}.exe"
 
 
-      # SAFE: write qt.conf without here-strings
+      # SAFE: write qt.conf
       - name: Write qt.conf
       - name: Write qt.conf
         shell: pwsh
         shell: pwsh
         run: |
         run: |
@@ -127,13 +137,14 @@ jobs:
             'Translations = translations'
             'Translations = translations'
           ) | Set-Content -Encoding ASCII "$env:APP_DIR\qt.conf"
           ) | Set-Content -Encoding ASCII "$env:APP_DIR\qt.conf"
 
 
-      # SAFE: write run_debug.cmd without here-strings
+      # SAFE: write run_debug.cmd
       - name: Add run_debug.cmd
       - name: Add run_debug.cmd
         shell: pwsh
         shell: pwsh
         run: |
         run: |
           @(
           @(
             '@echo off'
             '@echo off'
             'setlocal'
             'setlocal'
+            'cd /d "%~dp0"'
             'set QT_DEBUG_PLUGINS=1'
             'set QT_DEBUG_PLUGINS=1'
             'set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true'
             'set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true'
             'set QT_QPA_PLATFORM=windows'
             'set QT_QPA_PLATFORM=windows'
@@ -142,22 +153,221 @@ jobs:
             'pause'
             'pause'
           ) | Set-Content -Encoding ASCII "$env:APP_DIR\run_debug.cmd"
           ) | Set-Content -Encoding ASCII "$env:APP_DIR\run_debug.cmd"
 
 
+      - name: Add run_debug_softwaregl.cmd
+        shell: pwsh
+        run: |
+          @(
+            '@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'
+            'set QT_OPENGL=software'
+            '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_softwaregl.cmd"
+
+      - name: Add GL/ANGLE fallbacks
+        shell: pwsh
+        run: |
+          $qtbin = Join-Path $env:Qt6_DIR "bin"
+          foreach ($f in @("d3dcompiler_47.dll","opengl32sw.dll","libEGL.dll","libGLESv2.dll")) {
+            $src = Join-Path $qtbin $f
+            if (Test-Path $src) {
+              Copy-Item $src "$env:APP_DIR" -Force
+              Write-Host "Copied $f"
+            } else {
+              Write-Host "Note: $f not found in Qt bin directory"
+            }
+          }
+
       - name: Copy assets
       - name: Copy assets
         shell: pwsh
         shell: pwsh
         run: |
         run: |
-          if (!(Test-Path "$env:APP_DIR\assets")) { New-Item -ItemType Directory -Path "$env:APP_DIR\assets" | Out-Null }
+          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
           robocopy assets "$env:APP_DIR\assets" /E /NFL /NDL /NJH /NJS /nc /ns /np
           if ($LASTEXITCODE -ge 8) { exit $LASTEXITCODE } else { exit 0 }
           if ($LASTEXITCODE -ge 8) { exit $LASTEXITCODE } else { exit 0 }
 
 
       - name: Sanity check deployed files
       - name: Sanity check deployed files
         shell: pwsh
         shell: pwsh
         run: |
         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 }
+          $req = @(
+            "$env:APP_DIR\platforms\qwindows.dll",
+            "$env:APP_DIR\Qt6Core.dll",
+            "$env:APP_DIR\Qt6Gui.dll",
+            "$env:APP_DIR\Qt6Qml.dll"
+          )
+          $missing = @()
+          foreach ($f in $req) {
+            if (!(Test-Path $f)) { $missing += $f }
+          }
+          if ($missing.Count -gt 0) {
+            Write-Error "Missing required files:`n$($missing -join "`n")"
+            exit 1
+          }
+          foreach ($opt in @("d3dcompiler_47.dll","opengl32sw.dll","libEGL.dll","libGLESv2.dll")) {
+            if (!(Test-Path (Join-Path $env:APP_DIR $opt))) {
+              Write-Host "Note: $opt not found"
+            }
+          }
           Write-Host "Deployment looks OK."
           Write-Host "Deployment looks OK."
 
 
+      - name: Dependency check (dumpbin)
+        shell: pwsh
+        run: |
+          Write-Host "Checking EXE dependencies with dumpbin..."
+          
+          # Find dumpbin.exe in Visual Studio installation
+          $dumpbin = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" -Recurse -Filter "dumpbin.exe" -ErrorAction SilentlyContinue |
+            Where-Object { $_.FullName -match "Hostx64\\x64" } |
+            Select-Object -First 1
+          
+          if (-not $dumpbin) {
+            Write-Warning "dumpbin.exe not found, skipping dependency check"
+            exit 0
+          }
+          
+          Write-Host "Using dumpbin: $($dumpbin.FullName)"
+          
+          # Get dependencies
+          $exePath = "$env:APP_DIR\${{ env.APP_NAME }}.exe"
+          $output = & $dumpbin.FullName /DEPENDENTS $exePath 2>&1 | Out-String
+          
+          Write-Host "Dependencies:"
+          Write-Host $output
+          
+          # Parse DLL names from dumpbin output
+          $dllPattern = '^\s+([a-zA-Z0-9_\-\.]+\.dll)\s*$'
+          $dependencies = @()
+          foreach ($line in $output -split "`n") {
+            if ($line -match $dllPattern) {
+              $dependencies += $matches[1]
+            }
+          }
+          
+          # Known system DLLs that don't need to be bundled
+          # These include Windows API sets (api-ms-win-*) and core system DLLs
+          $knownSystemDlls = @(
+            'KERNEL32.dll',
+            'USER32.dll',
+            'ADVAPI32.dll',
+            'SHELL32.dll',
+            'ole32.dll',
+            'OLEAUT32.dll',
+            'OPENGL32.dll',
+            'GDI32.dll',
+            'WS2_32.dll',
+            'NETAPI32.dll'
+          )
+          
+          # Check if each dependency exists in APP_DIR or is a known system DLL
+          $systemDirs = @(
+            "$env:SystemRoot\System32",
+            "$env:SystemRoot\SysWOW64"
+          )
+          
+          $missingDeps = @()
+          foreach ($dll in $dependencies) {
+            # Skip Windows API set DLLs (api-ms-win-*, ext-ms-*)
+            if ($dll -match '^(api-ms-win-|ext-ms-)') {
+              Write-Host "Skipping Windows API set: $dll"
+              continue
+            }
+            
+            # Skip known system DLLs
+            if ($knownSystemDlls -contains $dll) {
+              Write-Host "Skipping known system DLL: $dll"
+              continue
+            }
+            
+            $foundInApp = Test-Path (Join-Path $env:APP_DIR $dll)
+            $foundInSystem = $false
+            foreach ($sysDir in $systemDirs) {
+              if (Test-Path (Join-Path $sysDir $dll)) {
+                $foundInSystem = $true
+                break
+              }
+            }
+            
+            if (-not $foundInApp -and -not $foundInSystem) {
+              $missingDeps += $dll
+              Write-Warning "Missing dependency: $dll"
+            } else {
+              $location = if ($foundInApp) { "app directory" } else { "system directory" }
+              Write-Host "Found $dll in $location"
+            }
+          }
+          
+          if ($missingDeps.Count -gt 0) {
+            Write-Error "Missing DLL dependencies: $($missingDeps -join ', ')"
+            exit 1
+          }
+          
+          Write-Host "All required dependencies are present"
+
+      - name: Smoke-run (software GL, capture exit)
+        shell: pwsh
+        run: |
+          $ErrorActionPreference = 'Stop'
+      
+          $env:QT_DEBUG_PLUGINS  = "1"
+          $env:QT_LOGGING_RULES  = "qt.*=true;qt.qml=true;qqml.*=true"
+          $env:QT_OPENGL         = "software"
+          $env:QT_QPA_PLATFORM   = "windows"
+      
+          Push-Location "$env:APP_DIR"
+          try {
+            $process = Start-Process -FilePath ".\${{ env.APP_NAME }}.exe" -PassThru -NoNewWindow
+            $timeoutSeconds = 5
+            $exited = $false
+            try {
+              Wait-Process -Id $process.Id -Timeout $timeoutSeconds -ErrorAction Stop
+              $exited = $true
+            } catch {
+              $exited = $false
+            }
+
+            if (-not $exited) {
+              Write-Host "Process is still running after $timeoutSeconds s — treating as a PASS for smoke."
+              $process.Kill()
+              $process.WaitForExit()
+              exit 0
+            }
+
+            $code = $process.ExitCode
+            Write-Host "Process exited early with code: $code"
+
+            $expectedOnHeadless = @(-1073741819, -1073741510)
+            $deploymentIssues = @(-1073741701, -1073741515)
+
+            if ($deploymentIssues -contains $code) {
+              Write-Error "Deployment issue detected (exit $code)."
+              exit 1
+            }
+
+            if ($expectedOnHeadless -contains $code) {
+              Write-Warning "Headless-expected exit ($code). Treating as PASS for smoke."
+              exit 0
+            }
+
+            if ($code -eq 0) {
+              Write-Host "App exited cleanly (0)."
+              exit 0
+            }
+
+            Write-Error "Unexpected early exit ($code)."
+            exit 1
+          } catch {
+            Write-Error "Failed to start process: $_"
+            exit 1
+          } finally {
+            Pop-Location
+          }
+
       - name: Zip
       - name: Zip
         shell: pwsh
         shell: pwsh
         run: |
         run: |

+ 94 - 2
scripts/build-windows.py

@@ -7,8 +7,10 @@ This script automates the Windows build process by:
 1. Checking for required tools (CMake, Ninja, MSVC, Qt)
 1. Checking for required tools (CMake, Ninja, MSVC, Qt)
 2. Guiding installation of missing dependencies
 2. Guiding installation of missing dependencies
 3. Configuring and building the project with proper MSVC setup
 3. Configuring and building the project with proper MSVC setup
-4. Deploying Qt dependencies
-5. Copying assets and creating a distributable package
+4. Deploying Qt dependencies with runtime libraries
+5. Writing qt.conf and diagnostic scripts (run_debug.cmd, run_debug_softwaregl.cmd)
+6. Copying GL/ANGLE fallback DLLs for graphics compatibility
+7. Copying assets and creating a distributable package
 
 
 Usage:
 Usage:
     python scripts/build-windows.py                    # Full build with checks
     python scripts/build-windows.py                    # Full build with checks
@@ -358,6 +360,86 @@ def deploy_qt(build_dir: Path, qt_path: Path, app_name: str, build_type: str) ->
     
     
     success("Qt dependencies deployed")
     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_debug.cmd
+    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
+set QT_QPA_PLATFORM=windows
+"%~dp0{app_name}.exe" 1> "%~dp0runlog.txt" 2>&1
+echo ExitCode: %ERRORLEVEL%>> "%~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
+    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
+set QT_OPENGL=software
+set QT_QPA_PLATFORM=windows
+"%~dp0{app_name}.exe" 1> "%~dp0runlog.txt" 2>&1
+echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.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')
+    
+    success(f"Diagnostic scripts written: run_debug.cmd, run_debug_softwaregl.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:
 def copy_assets(build_dir: Path) -> None:
     """Copy assets to build directory."""
     """Copy assets to build directory."""
     info("Copying assets...")
     info("Copying assets...")
@@ -466,6 +548,16 @@ def main() -> int:
     # Deploy Qt
     # Deploy Qt
     if qt_path:
     if qt_path:
         deploy_qt(build_dir, qt_path, "standard_of_iron", args.build_type)
         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:
     else:
         warning("Qt path not found, skipping windeployqt")
         warning("Qt path not found, skipping windeployqt")