2026-07-14 01:37:06 +00:00
param (
[ string ] $SourceDir = "" ,
[ Parameter ( Mandatory = $true )]
[ string ] $ConfigFile ,
[ string ] $OutputDir = "" ,
[ string ] $ZipFile = ""
)
$ErrorActionPreference = "Stop"
$RepoRoot = Split-Path -Parent $PSScriptRoot
if ([ string ]:: IsNullOrWhiteSpace ( $SourceDir )) {
2026-07-16 08:14:51 +00:00
$SourceDir = Join-Path $RepoRoot "out/bin/Release"
2026-07-14 01:37:06 +00:00
}
if ([ string ]:: IsNullOrWhiteSpace ( $OutputDir )) {
$OutputDir = Join-Path $RepoRoot "dist/UpdateClient"
}
if ([ string ]:: IsNullOrWhiteSpace ( $ZipFile )) {
$ZipFile = Join-Path $RepoRoot "dist/UpdateClient.zip"
}
$source = ( Resolve-Path $SourceDir ). Path
$config = ( Resolve-Path $ConfigFile ). Path
$settings = Get-Content $config -Raw -Encoding UTF8 | ConvertFrom-Json
2026-07-10 02:45:13 +00:00
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 = "" }
2026-07-16 08:14:51 +00:00
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 "
}
function Get-InstallDirectoryId([string]$RuntimeDir ) {
$normalized = ([ IO.Path ]:: GetFullPath ( $RuntimeDir ) -replace '\\' , '/' ). TrimEnd ( '/' )
$sha = [ System.Security.Cryptography.SHA256 ]:: Create ()
try {
$bytes = [ System.Text.Encoding ]:: UTF8 . GetBytes ( $normalized )
$hash = $sha . ComputeHash ( $bytes )
return -join ( $hash | ForEach-Object { $_ . ToString ( "x2" ) })
} finally {
$sha . Dispose ()
}
}
function Get-UserDataManifestCandidate([string]$RuntimeDir , [ string ] $ManifestName ) {
$localData = [ Environment ]:: GetFolderPath ( "LocalApplicationData" )
if ([ string ]:: IsNullOrWhiteSpace ( $localData )) { return "" }
$installId = Get-InstallDirectoryId $RuntimeDir
return Join-Path $localData "Marsco\UpdateClientSDK\installations\ $installId \update\manifest_cache\ $ManifestName "
}
2026-07-10 02:45:13 +00:00
2026-07-15 06:39:45 +00:00
$requiredFields = @ (
"app_id" , "channel" , "current_version" ,
"client_token" , "launch_token" , "license_key" ,
"main_executable" , "launcher_executable" , "updater_executable" , "bootstrap_executable"
)
2026-07-10 02:45:13 +00:00
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' )
}
2026-07-16 08:14:51 +00:00
if ( $debugArtifacts ) {
throw "Source directory contains Debug artifacts. Clean the Release output directory and rebuild Release first. Example: $( $debugArtifacts [ 0 ]. FullName ) "
}
2026-07-10 02:45:13 +00:00
$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 ) "
}
2026-07-16 08:14:51 +00:00
$manifestName = "manifest_ $( $settings . current_version ) .json"
$runtimeDirAbsolute = if ([ string ]:: IsNullOrWhiteSpace ( $runtimeDirRelative )) {
$source
} else {
Join-Path $source ( $runtimeDirRelative -replace '/' , [ IO.Path ]:: DirectorySeparatorChar )
}
$sourceManifestRelative = Join-RelativePath $runtimeDirRelative "update/manifest_cache/ $manifestName "
$manifestCandidates = @ (
( Get-UserDataManifestCandidate $runtimeDirAbsolute $manifestName ),
( Join-Path $source ( $sourceManifestRelative -replace '/' , [ IO.Path ]:: DirectorySeparatorChar )),
( Join-Path $source "update/manifest_cache/ $manifestName " )
) | Where-Object { -not [ string ]:: IsNullOrWhiteSpace ( $_ ) }
$sourceManifest = $manifestCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if ( -not $sourceManifest -or -not ( Test-Path $sourceManifest )) {
$searched = ( $manifestCandidates | ForEach-Object { " - $_ " }) -join [ Environment ]:: NewLine
throw "Missing signed Manifest cache for current version: $manifestName . Complete online update/verification for this version before packaging. Searched paths: $([ Environment ]:: NewLine ) $searched "
}
2026-07-10 02:45:13 +00:00
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 "