param( [string]$SourceDir = "$PSScriptRoot/out/bin", [Parameter(Mandatory = $true)] [string]$ConfigFile, [string]$OutputDir = "$PSScriptRoot/dist/UpdateClient", [string]$ZipFile = "$PSScriptRoot/dist/UpdateClient.zip" ) $ErrorActionPreference = "Stop" $source = (Resolve-Path $SourceDir).Path $config = (Resolve-Path $ConfigFile).Path $settings = Get-Content $config -Raw -Encoding UTF8 | ConvertFrom-Json function Normalize-RelativePath([string]$PathValue) { return ($PathValue -replace '\\', '/').Trim('/') } function Get-RelativePathUnderSource([string]$FullPath, [string]$Fallback) { $full = (Resolve-Path $FullPath).Path if ($full.StartsWith($source, [System.StringComparison]::OrdinalIgnoreCase)) { return Normalize-RelativePath ($full.Substring($source.Length).TrimStart('\', '/')) } return Normalize-RelativePath $Fallback } $configRelativePath = Get-RelativePathUnderSource $config "config/app_config.json" $configRelativeParent = Split-Path $configRelativePath -Parent $runtimeDirRelative = Normalize-RelativePath (Split-Path $configRelativeParent -Parent) if ($runtimeDirRelative -eq ".") { $runtimeDirRelative = "" } function Join-RelativePath([string]$Base, [string]$Child) { $baseNorm = Normalize-RelativePath $Base $childNorm = Normalize-RelativePath $Child if ([string]::IsNullOrWhiteSpace($baseNorm)) { return $childNorm } if ([string]::IsNullOrWhiteSpace($childNorm)) { return $baseNorm } return "$baseNorm/$childNorm" } $requiredFields = @( "app_id", "channel", "api_base_url", "current_version", "client_token", "launch_token", "license_key", "main_executable", "launcher_executable", "updater_executable", "bootstrap_executable" ) foreach ($field in $requiredFields) { if (-not $settings.$field) { throw "Config file is missing required field: $field" } } $mainExecutable = Normalize-RelativePath $settings.main_executable $launcherExecutable = Normalize-RelativePath $settings.launcher_executable $updaterExecutable = Normalize-RelativePath $settings.updater_executable $bootstrapExecutable = Normalize-RelativePath $settings.bootstrap_executable $requiredFiles = @( (Join-RelativePath $runtimeDirRelative $mainExecutable), (Join-RelativePath $runtimeDirRelative $launcherExecutable), (Join-RelativePath $runtimeDirRelative $updaterExecutable), (Join-RelativePath $runtimeDirRelative $bootstrapExecutable) ) foreach ($name in $requiredFiles) { $nativeName = $name -replace '/', [IO.Path]::DirectorySeparatorChar if (-not (Test-Path (Join-Path $source $nativeName))) { throw "Source directory is missing required file: $name" } } $debugArtifacts = Get-ChildItem $source -Recurse -File | Where-Object { $_.Name -match '^(Qt5.*d|qwindowsd|libEGLd|libGLESv2d|msvcp.*d|vcruntime.*d)\.dll$' -or $_.Extension -in @('.pdb', '.ilk') } if ($debugArtifacts) { throw "Source directory contains Debug artifacts. Clean out/bin and rebuild Release first. Example: $($debugArtifacts[0].FullName)" } $expectedMainPath = Join-RelativePath $runtimeDirRelative $mainExecutable $mainLeafName = Split-Path $mainExecutable -Leaf $duplicateMain = Get-ChildItem $source -Recurse -File -Filter $mainLeafName | Where-Object { $relative = Normalize-RelativePath ($_.FullName.Substring($source.Length).TrimStart('\', '/')) $relative -ne $expectedMainPath } | Select-Object -First 1 if ($duplicateMain) { throw "Source directory contains a duplicate main executable outside $expectedMainPath. Use a clean Release root directory: $($duplicateMain.FullName)" } $manifestName = "manifest_$($settings.current_version).json" $sourceManifestRelative = Join-RelativePath $runtimeDirRelative "update/manifest_cache/$manifestName" $sourceManifest = Join-Path $source ($sourceManifestRelative -replace '/', [IO.Path]::DirectorySeparatorChar) if (-not (Test-Path $sourceManifest)) { $legacySourceManifest = Join-Path $source "update/manifest_cache/$manifestName" if (Test-Path $legacySourceManifest) { $sourceManifest = $legacySourceManifest } } if (-not (Test-Path $sourceManifest)) { throw "Missing signed Manifest cache for current version: $sourceManifest. Complete online update/verification for this version before packaging." } if (Test-Path $OutputDir) { Remove-Item $OutputDir -Recurse -Force } New-Item $OutputDir -ItemType Directory -Force | Out-Null Get-ChildItem $source -Force | Where-Object { $_.Name -notin @("update", "update_temp") } | Copy-Item -Destination $OutputDir -Recurse -Force $outputRuntimeUpdateDir = Join-Path $OutputDir ((Join-RelativePath $runtimeDirRelative "update") -replace '/', [IO.Path]::DirectorySeparatorChar) if (Test-Path $outputRuntimeUpdateDir) { Remove-Item $outputRuntimeUpdateDir -Recurse -Force } $outputRuntimeTempDir = Join-Path $OutputDir ((Join-RelativePath $runtimeDirRelative "update_temp") -replace '/', [IO.Path]::DirectorySeparatorChar) if (Test-Path $outputRuntimeTempDir) { Remove-Item $outputRuntimeTempDir -Recurse -Force } $outputConfigPath = Join-Path $OutputDir ($configRelativePath -replace '/', [IO.Path]::DirectorySeparatorChar) $outputConfigDir = Split-Path $outputConfigPath -Parent New-Item $outputConfigDir -ItemType Directory -Force | Out-Null Copy-Item $config $outputConfigPath -Force @("client_identity.dat", "local_state.json", "version_policy.dat") | ForEach-Object { $runtimeFile = Join-Path $outputConfigDir $_ if (Test-Path $runtimeFile) { Remove-Item $runtimeFile -Force } } $manifestDir = Join-Path $OutputDir ((Join-RelativePath $runtimeDirRelative "update/manifest_cache") -replace '/', [IO.Path]::DirectorySeparatorChar) New-Item $manifestDir -ItemType Directory -Force | Out-Null Copy-Item $sourceManifest (Join-Path $manifestDir $manifestName) -Force $zipParent = Split-Path $ZipFile -Parent New-Item $zipParent -ItemType Directory -Force | Out-Null if (Test-Path $ZipFile) { Remove-Item $ZipFile -Force } Compress-Archive -Path (Join-Path $OutputDir "*") -DestinationPath $ZipFile -CompressionLevel Optimal Write-Host "Package directory: $OutputDir" Write-Host "ZIP file: $ZipFile"