diff --git a/windows/deploy_windows.ps1 b/windows/deploy_windows.ps1 index 487e02f9fb..ce03945165 100644 --- a/windows/deploy_windows.ps1 +++ b/windows/deploy_windows.ps1 @@ -13,7 +13,11 @@ param ( # updates. Verify .github/workflows/bump-dependencies.yaml when changing those manually: [string] $AsioSDKUrl = "https://download.steinberg.net/sdk_downloads/ASIO-SDK_2.3.4_2025-10-15.zip", [string] $NsisUrl = "https://downloads.sourceforge.net/project/nsis/NSIS%203/3.12/nsis-3.12.zip", - [string] $BuildOption = "" + [string] $BuildOption = "", + # Toggles for debugging and targeted builds + [switch] $DebugMode, + [switch] $Skip64Bit, + [switch] $Skip32Bit ) # Fail early on all errors @@ -33,18 +37,73 @@ $DeployPath = "$RootPath\deploy" $WindowsPath ="$RootPath\windows" $AppName = "Jamulus" +Function Write-Log +{ + param( + [Parameter(Mandatory=$true)] + [AllowNull()] + [AllowEmptyString()] + [string] $Message, + [ValidateSet("INFO","WARN","ERROR","STEP","DEBUG")] + [string] $Level = "INFO" + ) + + if ($null -eq $Message) { $Message = "" } + $Stamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + $Log = "[$Stamp] [$Level] $Message" + + switch ($Level) { + "INFO" { Write-Host $Log } + "STEP" { Write-Host "`n>>> [$Stamp] $Message" -ForegroundColor Cyan } + "WARN" { Write-Host $Log -ForegroundColor Yellow } + "ERROR" { Write-Host $Log -ForegroundColor Red } + "DEBUG" { if ($DebugMode) { Write-Host $Log -ForegroundColor DarkGray } } + } +} + # Execute native command with errorlevel handling -Function Invoke-Native-Command { +Function Invoke-Native-Command +{ param( + [Parameter(Mandatory=$true)] [string] $Command, - [string[]] $Arguments + [string[]] $Arguments, + [switch] $SuppressStdErr ) - & "$Command" @Arguments + Write-Log "Executing: $Command $($Arguments -join ' ')" "DEBUG" + $Out = [Collections.Generic.List[string]]::new() + + $PrevEA = $ErrorActionPreference + $ErrorActionPreference = "Continue" + + try + { + & $Command @Arguments 2>&1 | ForEach-Object { + $IsErr = $_ -is [Management.Automation.ErrorRecord] + if ($IsErr -and $SuppressStdErr) { return } + + $Line = if ($IsErr) { $_.TargetObject -as [string] } else { $_.ToString() } + if ([string]::IsNullOrWhiteSpace($Line)) { return } + + $Out.Add($Line) + $Color = if ($IsErr -and $DebugMode) { "DarkYellow" } else { "DarkGray" } + if ($DebugMode -or -not $IsErr) { Write-Host " $Line" -ForegroundColor $Color } + } + } + finally + { + $ErrorActionPreference = $PrevEA + } if ($LastExitCode -Ne 0) { - Throw "Native command $Command returned with exit code $LastExitCode" + $Err = "Native command $Command returned with exit code $LastExitCode" + Write-Log $Err "ERROR" + if (-not $DebugMode) { + $Out.ForEach({ Write-Log $_ "ERROR" }) + } + Throw $Err } } @@ -54,12 +113,13 @@ Function Clean-Build-Environment if (Test-Path -Path $BuildPath) { Remove-Item -Path $BuildPath -Recurse -Force } if (Test-Path -Path $DeployPath) { Remove-Item -Path $DeployPath -Recurse -Force } - New-Item -Path $BuildPath -ItemType Directory - New-Item -Path $DeployPath -ItemType Directory + New-Item -Path $BuildPath -ItemType Directory | Out-Null + New-Item -Path $DeployPath -ItemType Directory | Out-Null } # For sourceforge links we need to get the correct mirror (especially NISIS) Thanks: https://www.powershellmagazine.com/2013/01/29/pstip-retrieve-a-redirected-url/ -Function Get-RedirectedUrl { +Function Get-RedirectedUrl +{ param( [Parameter(Mandatory=$true)] [string] $url @@ -68,16 +128,21 @@ Function Get-RedirectedUrl { $numAttempts = 10 $sleepTime = 10 $maxSleepTime = 80 - for ($attempt = 1; $attempt -le $numAttempts; $attempt++) { - try { + for ($attempt = 1; $attempt -le $numAttempts; $attempt++) + { + try + { $request = [System.Net.WebRequest]::Create($url) $request.AllowAutoRedirect=$true $response=$request.GetResponse() - $response.ResponseUri.AbsoluteUri + $redirect = $response.ResponseUri.AbsoluteUri $response.Close() - return - } catch { - if ($attempt -lt $numAttempts) { + return $redirect + } + catch + { + if ($attempt -lt $numAttempts) + { Write-Warning "Caught error: $_" Write-Warning "Get-RedirectedUrl: Fetch attempt #${attempt}/${numAttempts} for $url failed, trying again in ${sleepTime}s" Start-Sleep -Seconds $sleepTime @@ -90,35 +155,6 @@ Function Get-RedirectedUrl { } } -function Initialize-Module-Here ($m) { # see https://stackoverflow.com/a/51692402 - - # If module is imported say that and do nothing - if (Get-Module | Where-Object {$_.Name -eq $m}) { - Write-Output "Module $m is already imported." - } - else { - - # If module is not imported, but available on disk then import - if (Get-Module -ListAvailable | Where-Object {$_.Name -eq $m}) { - Import-Module $m - } - else { - - # If module is not imported, not available on disk, but is in online gallery then install and import - if (Find-Module -Name $m | Where-Object {$_.Name -eq $m}) { - Install-Module -Name $m -Force -Verbose -Scope CurrentUser - Import-Module $m - } - else { - - # If module is not imported, not available and not in online gallery then abort - Write-Output "Module $m not imported, not available and not in online gallery, exiting." - EXIT 1 - } - } - } -} - # Download and uncompress dependency in ZIP format Function Install-Dependency { @@ -131,7 +167,7 @@ Function Install-Dependency if (Test-Path -Path "$WindowsPath\$Destination") { - echo "Using ${WindowsPath}\${Destination} from previous run (e.g. actions/cache)" + Write-Log "Using ${WindowsPath}\${Destination} from previous run (e.g. actions/cache)" "INFO" return } @@ -140,17 +176,19 @@ Function Install-Dependency $TempGuid = [System.Guid]::NewGuid() # Create a unique empty directory to unpack into $TempDir = (Join-Path $TempPath $TempGuid) - New-Item -ItemType Directory -Path $TempDir + New-Item -ItemType Directory -Path $TempDir | Out-Null if ($Uri -Match "downloads.sourceforge.net") { - $Uri = Get-RedirectedUrl -URL $Uri + $Uri = Get-RedirectedUrl -url $Uri } + Write-Log "Downloading $Uri..." "INFO" Invoke-WebRequest -Uri $Uri -OutFile $TempFileName - echo $TempFileName + + Write-Log "Extracting to $WindowsPath\$Destination..." "INFO" Expand-Archive -Path $TempFileName -DestinationPath $TempDir -Force - echo $WindowsPath\$Destination + # Because we unpacked into a new directory, we can use * for the directory in the archive, # so that we do not need to know the directory name the archive was packed from. Move-Item -Path "$TempDir\*" -Destination "$WindowsPath\$Destination" -Force @@ -158,20 +196,22 @@ Function Install-Dependency Remove-Item -Path $TempFileName -Force } -# Install VSSetup (Visual Studio detection), ASIO SDK and NSIS Installer +# Install ASIO SDK and NSIS Installer Function Install-Dependencies { - if (-not (Get-PackageProvider -Name nuget).Name -eq "nuget") { - Install-PackageProvider -Name "Nuget" -Scope CurrentUser -Force - } - Initialize-Module-Here -m "VSSetup" + Write-Log "Installing Dependencies..." "STEP" Install-Dependency -Uri $NsisUrl -Destination "..\libs\NSIS\NSIS-source" - if ($BuildOption -Notmatch "jack") { + if ($BuildOption -Notmatch "jack") + { # Don't download ASIO SDK on Jamulus JACK builds to save # resources and to be extra-sure license-wise. Install-Dependency -Uri $AsioSDKUrl -Destination "..\libs\ASIOSDK2" } + else + { + Write-Log "Skipping ASIO SDK (JACK build detected)." "INFO" + } } # Setup environment variables and build tool paths @@ -182,12 +222,25 @@ Function Initialize-Build-Environment [string] $BuildArch ) - # Look for Visual Studio/Build Tools 2017 or later (version 15.0 or above) - $VsInstallPath = Get-VSSetupInstance | ` - Select-VSSetupInstance -Product "*" -Version "15.0" -Latest | ` - Select-Object -ExpandProperty "InstallationPath" + # Use native vswhere.exe to find VS2017+ installations with C++ build tools + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (-not (Test-Path -Path $vswhere)) + { + Throw "vswhere.exe not found. Visual Studio 2017 or above is required." + } + + $VsInstallPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + $VsVer = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationVersion - if ($VsInstallPath -Eq "") { $VsInstallPath = "" } + if ([string]::IsNullOrWhiteSpace($VsInstallPath)) + { + Throw "Could not locate a Visual Studio installation with C++ build tools." + } + + # Dynamically determine the correct Platform Toolset based on VS version to avoid hardcoded MSBuild failures + if ($VsVer -match "^17\.") { Set-Item Env:VsPlatformToolset "v143" } + elseif ($VsVer -match "^16\.") { Set-Item Env:VsPlatformToolset "v142" } + else { Set-Item Env:VsPlatformToolset "v141" } if ($BuildArch -Eq "x86_64") { @@ -227,6 +280,39 @@ Function Initialize-Build-Environment Remove-Item -Path $EnvDump -Force } +# Resolve Qt path by falling back to Registry lookups if the default path is missing +Function Resolve-Qt-Path +{ + param( + [Parameter(Mandatory=$true)] + [string] $DefaultPath + ) + + if (Test-Path -Path $DefaultPath) { return $DefaultPath } + + Write-Log "Qt path '$DefaultPath' not found. Searching registry..." "DEBUG" + $QtVer = Split-Path $DefaultPath -Leaf + $RegPaths = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + + foreach ($Reg in $RegPaths) + { + $Installs = Get-ItemProperty $Reg -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match "Qt" -and $_.InstallLocation } + foreach ($Inst in $Installs) + { + $TryPath = Join-Path $Inst.InstallLocation $QtVer + if (Test-Path -Path $TryPath) + { + Write-Log "Discovered Qt at: $TryPath" "INFO" + return $TryPath + } + } + } + + return $DefaultPath +} + # Setup Qt environment variables and build tool paths Function Initialize-Qt-Build-Environment { @@ -243,6 +329,29 @@ Function Initialize-Qt-Build-Environment Set-Item Env:QtQmakePath "$QtMsvcSpecPath\qmake.exe" Set-Item Env:QtWinDeployPath "$QtMsvcSpecPath\windeployqt.exe" + if (Get-Command "jom.exe" -ErrorAction SilentlyContinue) + { + Set-Item Env:QtJomPath "jom.exe" + } + else + { + $QtRoot = Split-Path $QtInstallPath -Parent + $JomExes = "$QtRoot\Tools\QtCreator\bin\jom\jom.exe", + "$QtRoot\Tools\jom\jom.exe", + "C:\Qt\Tools\QtCreator\bin\jom\jom.exe", + "D:\Qt\Tools\QtCreator\bin\jom\jom.exe" + + $FoundJom = $JomExes | Where-Object { Test-Path -Path $_ } | Select-Object -First 1 + if ($FoundJom) + { + Set-Item Env:QtJomPath $FoundJom + } + else + { + Set-Item Env:QtJomPath "" + } + } + "**********************************************************************" "Using Qt binaries for Visual C++ located at" $QtMsvcSpecPath @@ -267,27 +376,40 @@ Function Build-App [string] $BuildArch ) + $QmkCfg = "CONFIG+=$BuildConfig $BuildArch $BuildOption" + if (-not $DebugMode) { $QmkCfg += " silent" } + Invoke-Native-Command -Command "$Env:QtQmakePath" ` - -Arguments ("$RootPath\$AppName.pro", "CONFIG+=$BuildConfig $BuildArch $BuildOption", ` - "-o", "$BuildPath\Makefile") + -Arguments ("$RootPath\$AppName.pro", $QmkCfg, "-o", "$BuildPath\Makefile") Set-Location -Path $BuildPath - if (Get-Command "jom.exe" -ErrorAction SilentlyContinue) + + $MkArgs = @("/NOLOGO", $BuildConfig) + if (-not $DebugMode) { $MkArgs += "/S" } + + if ($Env:QtJomPath) { - echo "Building with jom /J ${Env:NUMBER_OF_PROCESSORS}" - Invoke-Native-Command -Command "jom" -Arguments ("/J", "${Env:NUMBER_OF_PROCESSORS}", "$BuildConfig") + $Cores = [Math]::Max(1, [Math]::Floor([int]$Env:NUMBER_OF_PROCESSORS / 2)) + Write-Log "Building with jom /J $Cores (half of $Env:NUMBER_OF_PROCESSORS cores)" "INFO" + Invoke-Native-Command -Command "$Env:QtJomPath" -Arguments (@("/J", "$Cores") + $MkArgs) } else { - echo "Building with nmake (install Qt jom if you want parallel builds)" - Invoke-Native-Command -Command "nmake" -Arguments ("$BuildConfig") + Write-Log "Building with nmake (sequential)" "WARN" + Invoke-Native-Command -Command "nmake" -Arguments $MkArgs } + Invoke-Native-Command -Command "$Env:QtWinDeployPath" ` - -Arguments ("--$BuildConfig", "--compiler-runtime", "--dir=$DeployPath\$BuildArch", + -Arguments ("--$BuildConfig", "--compiler-runtime", "--dir=$DeployPath\$BuildArch", ` "$BuildPath\$BuildConfig\$AppName.exe") Move-Item -Path "$BuildPath\$BuildConfig\$AppName.exe" -Destination "$DeployPath\$BuildArch" -Force - Invoke-Native-Command -Command "nmake" -Arguments ("clean") + + # Updated clean args to prevent recompiling the app during the clean phase + $CleanArgs = @("clean", "/NOLOGO") + if (-not $DebugMode) { $CleanArgs += "/S" } + Invoke-Native-Command -Command "nmake" -Arguments $CleanArgs -SuppressStdErr + Set-Location -Path $RootPath } @@ -296,19 +418,25 @@ function Build-App-Variants { foreach ($_ in ("x86_64", "x86")) { + if (($_ -eq "x86_64" -and $Skip64Bit) -or ($_ -eq "x86" -and $Skip32Bit)) { continue } + $OriginalEnv = Get-ChildItem Env: if ($_ -eq "x86") { Initialize-Build-Environment -BuildArch $_ - Initialize-Qt-Build-Environment -QtInstallPath $QtInstallPath32 -QtCompile $QtCompile32 + Initialize-Qt-Build-Environment -QtInstallPath (Resolve-Qt-Path -DefaultPath $QtInstallPath32) -QtCompile $QtCompile32 } else { Initialize-Build-Environment -BuildArch $_ - Initialize-Qt-Build-Environment -QtInstallPath $QtInstallPath64 -QtCompile $QtCompile64 + Initialize-Qt-Build-Environment -QtInstallPath (Resolve-Qt-Path -DefaultPath $QtInstallPath64) -QtCompile $QtCompile64 } Build-App -BuildConfig "release" -BuildArch $_ $OriginalEnv | % { Set-Item "Env:$($_.Name)" $_.Value } + + # Wipe the build directory so the next architecture starts entirely fresh + if (Test-Path -Path $BuildPath) { Remove-Item -Path $BuildPath -Recurse -Force } + New-Item -Path $BuildPath -ItemType Directory | Out-Null } } @@ -319,6 +447,15 @@ Function Build-Installer [string] $BuildOption ) + # --- SAFETY GATE: Do not build if no executables were generated --- + $Has64 = Test-Path -Path "$DeployPath\x86_64\$AppName.exe" + $Has32 = Test-Path -Path "$DeployPath\x86\$AppName.exe" + + if (-not $Has64 -and -not $Has32) + { + Throw "Installer build aborted: No application binaries found." + } + foreach ($_ in Get-Content -Path "$RootPath\$AppName.pro") { if ($_ -Match "^VERSION *= *(.*)$") @@ -328,39 +465,40 @@ Function Build-Installer } } + $NsisVerb = if ($DebugMode) { "/v4" } else { "/v2" } + if ($BuildOption -ne "") { Invoke-Native-Command -Command "$RootPath\libs\NSIS\NSIS-source\makensis" ` - -Arguments ("/v4", "/DAPP_NAME=$AppName", "/DAPP_VERSION=$AppVersion", ` + -Arguments (@($NsisVerb, "/DAPP_NAME=$AppName", "/DAPP_VERSION=$AppVersion", ` "/DROOT_PATH=$RootPath", "/DWINDOWS_PATH=$WindowsPath", "/DDEPLOY_PATH=$DeployPath", ` - "/DBUILD_OPTION=$BuildOption", ` - "$WindowsPath\installer.nsi") + "/DBUILD_OPTION=$BuildOption") + "$WindowsPath\installer.nsi") } else { Invoke-Native-Command -Command "$RootPath\libs\NSIS\NSIS-source\makensis" ` - -Arguments ("/v4", "/DAPP_NAME=$AppName", "/DAPP_VERSION=$AppVersion", ` - "/DROOT_PATH=$RootPath", "/DWINDOWS_PATH=$WindowsPath", "/DDEPLOY_PATH=$DeployPath", ` - "$WindowsPath\installer.nsi") + -Arguments (@($NsisVerb, "/DAPP_NAME=$AppName", "/DAPP_VERSION=$AppVersion", ` + "/DROOT_PATH=$RootPath", "/DWINDOWS_PATH=$WindowsPath", "/DDEPLOY_PATH=$DeployPath") + "$WindowsPath\installer.nsi") } } # Build and copy NS-Process dll Function Build-NSProcess { - if (!(Test-Path -path "$RootPath\libs\NSIS\nsProcess.dll")) { - - echo "Building nsProcess..." + if (!(Test-Path -path "$RootPath\libs\NSIS\nsProcess.dll")) + { + Write-Log "Building nsProcess..." "INFO" $OriginalEnv = Get-ChildItem Env: Initialize-Build-Environment -BuildArch "x86" Invoke-Native-Command -Command "msbuild" ` -Arguments ("$RootPath\libs\NSIS\nsProcess\nsProcess.sln", '/p:Configuration="Release UNICODE"', ` - "/p:Platform=Win32") + "/p:Platform=Win32", "/p:PlatformToolset=$Env:VsPlatformToolset") Move-Item -Path "$RootPath\libs\NSIS\nsProcess\Release\nsProcess.dll" -Destination "$RootPath\libs\NSIS\nsProcess.dll" -Force Remove-Item -Path "$RootPath\libs\NSIS\nsProcess\Release\" -Force -Recurse + $OriginalEnv | % { Set-Item "Env:$($_.Name)" $_.Value } } } @@ -370,3 +508,4 @@ Install-Dependencies Build-App-Variants Build-NSProcess Build-Installer -BuildOption $BuildOption +Write-Log "Build process completed successfully." "STEP"