param( # 不传时自动从 XplorePlane.csproj 的 读取,传入则覆盖工程配置(用于临时测试版本号) [string]$Version, # Keep the technical PackId stable so existing Velopack installations can update. [string]$PackId = "Hexagon.XplorePlane", [string]$PackTitle = "XplorePlane", [string]$PackAuthors = "Hexagon", [string]$MainExe = "XplorePlane.exe", [string]$DeployDir = "C:\wamp64\www\updates\xploreplane", [switch]$SkipBuild, # By default also generate an MSI installer so the user can choose the install location. # Use -NoMsi when only the per-user Setup.exe is required. [switch]$NoMsi, [ValidateSet("Either", "PerUser", "PerMachine")] [string]$InstallLocation = "Either", [string]$Configuration = "Release", [string]$RuntimeId = "win-x64" ) $ErrorActionPreference = "Stop" $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $ProjectPath = Join-Path $RepoRoot "XplorePlane\XplorePlane.csproj" $PublishDir = Join-Path $RepoRoot "publish" $ReleasesDir = Join-Path $RepoRoot "Releases" # 版本号来源:命令行 -Version 优先,否则从 csproj 的 属性读取, # 确保发布包版本号与工程配置保持单一来源,不再需要每次手动传参维护。 if (-not $Version) { Write-Host "Reading version from csproj..." -ForegroundColor DarkGray $Version = (dotnet msbuild $ProjectPath -nologo -getProperty:Version).Trim() if ($LASTEXITCODE -ne 0 -or -not $Version) { Write-Host "FAILED: Could not read from $ProjectPath. Pass -Version explicitly or add to the csproj." -ForegroundColor Red exit 1 } } Write-Host "" Write-Host "========================================================" -ForegroundColor Cyan Write-Host " XplorePlane Build and Release Script" -ForegroundColor Cyan Write-Host "========================================================" -ForegroundColor Cyan Write-Host " Version: $Version" -ForegroundColor Cyan Write-Host " PackId: $PackId" -ForegroundColor Cyan Write-Host " PackTitle: $PackTitle" -ForegroundColor Cyan Write-Host " Publisher: $PackAuthors" -ForegroundColor Cyan Write-Host " Config: $Configuration" -ForegroundColor Cyan Write-Host " Runtime: $RuntimeId" -ForegroundColor Cyan Write-Host " MSI: $(-not $NoMsi)" -ForegroundColor Cyan if (-not $NoMsi) { Write-Host " Install scope: $InstallLocation" -ForegroundColor Cyan } Write-Host " DeployDir: $DeployDir" -ForegroundColor Cyan Write-Host "========================================================" -ForegroundColor Cyan Write-Host "" # Step 1: Clean # 仅在即将重新构建时清理 publish 目录:-SkipBuild 场景下需要复用已有的发布产物 # (例如仅因 vpk 打包阶段文件占用失败而重试),此前无条件清理会导致 -SkipBuild # 传入后必然因目录被清空而报错,参数形同虚设。 if (-not $SkipBuild) { Write-Host "[1/5] Cleaning publish directory..." -ForegroundColor Yellow if (Test-Path $PublishDir) { Remove-Item $PublishDir -Recurse -Force } } else { Write-Host "[1/5] Skipped clean (-SkipBuild, reusing existing publish directory)" -ForegroundColor DarkGray } # Step 2: Build and Publish if (-not $SkipBuild) { Write-Host "[2/5] Building and publishing ($Configuration, $RuntimeId)..." -ForegroundColor Yellow dotnet publish $ProjectPath -c $Configuration --self-contained -r $RuntimeId -o $PublishDir -p:Version=$Version if ($LASTEXITCODE -ne 0) { Write-Host "FAILED: dotnet publish failed!" -ForegroundColor Red exit 1 } Write-Host "OK: Build succeeded" -ForegroundColor Green } else { Write-Host "[2/5] Skipped build (-SkipBuild)" -ForegroundColor DarkGray if (-not (Test-Path $PublishDir)) { Write-Host "FAILED: publish directory not found, run without -SkipBuild first" -ForegroundColor Red exit 1 } } # Ensure the packaged application starts with the virtual navigation camera. # Navigation camera settings are stored in config.json, not App.config. $cameraConfigPath = Join-Path $PublishDir "config.json" $cameraConfig = [ordered]@{ Language = "zh-CN" LogLevel = "Debug" CameraType = "Simulated" CameraSimulatedImagePath = "" } if (Test-Path -LiteralPath $cameraConfigPath) { try { $existingCameraConfig = Get-Content -LiteralPath $cameraConfigPath -Raw | ConvertFrom-Json foreach ($property in $existingCameraConfig.PSObject.Properties) { $cameraConfig[$property.Name] = $property.Value } } catch { Write-Host " WARN: Existing config.json is invalid; recreating camera defaults." -ForegroundColor Yellow } } $cameraConfig["CameraType"] = "Simulated" $cameraConfig["CameraSimulatedImagePath"] = "" $cameraConfig | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $cameraConfigPath -Encoding UTF8 Write-Host "OK: Packaged navigation camera default: Simulated" -ForegroundColor Green function Rename-ExistingMsi { param( [string]$Directory, [string]$PackageId ) $legacyMsi = Join-Path $Directory "$PackageId-win.msi" if (-not (Test-Path -LiteralPath $legacyMsi)) { return } try { $installer = New-Object -ComObject WindowsInstaller.Installer $database = $installer.OpenDatabase($legacyMsi, 0) $view = $database.OpenView("SELECT Value FROM Property WHERE Property='ProductVersion'") $view.Execute() $record = $view.Fetch() $oldVersion = $record.StringData(1) } catch { throw "无法读取旧 MSI 的 ProductVersion:$legacyMsi。$($_.Exception.Message)" } finally { # Windows Installer COM objects keep the MSI open until every RCW is released. foreach ($comObject in @($record, $view, $database, $installer)) { if ($null -ne $comObject) { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($comObject) } } $record = $null $view = $null $database = $null $installer = $null [GC]::Collect() [GC]::WaitForPendingFinalizers() } if ([string]::IsNullOrWhiteSpace($oldVersion)) { throw "旧 MSI 未包含有效的 ProductVersion:$legacyMsi" } $versionedMsi = Join-Path $Directory "XplorePlane-$oldVersion-win.msi" if (Test-Path -LiteralPath $versionedMsi) { throw "旧 MSI 的版本化文件已存在,为避免覆盖而停止:$versionedMsi" } $moved = $false for ($attempt = 1; $attempt -le 5; $attempt++) { try { Move-Item -LiteralPath $legacyMsi -Destination $versionedMsi -ErrorAction Stop $moved = $true break } catch [System.IO.IOException] { if ($attempt -eq 5) { throw } Start-Sleep -Milliseconds 500 } } if (-not $moved) { throw "无法移动旧 MSI:$legacyMsi" } Write-Host " Archived existing MSI: $versionedMsi" -ForegroundColor DarkGray } # Step 3: Velopack Pack Write-Host "[3/5] Packing with vpk..." -ForegroundColor Yellow $vpkCmd = Get-Command vpk -ErrorAction SilentlyContinue if (-not $vpkCmd) { Write-Host " vpk not found, installing..." -ForegroundColor Yellow dotnet tool install -g vpk if ($LASTEXITCODE -ne 0) { Write-Host "FAILED: vpk install failed" -ForegroundColor Red exit 1 } # 当前 PowerShell 进程的 PATH 可能尚未包含全局工具目录,安装后重新解析一次。 $vpkCmd = Get-Command vpk -ErrorAction SilentlyContinue } $vpkPath = if ($vpkCmd) { $vpkCmd.Source } else { Join-Path $HOME ".dotnet\tools\vpk.exe" } if (-not (Test-Path -LiteralPath $vpkPath)) { Write-Host "FAILED: vpk executable not found: $vpkPath" -ForegroundColor Red exit 1 } if (-not $NoMsi) { Rename-ExistingMsi -Directory $ReleasesDir -PackageId $PackId } $vpkArgs = @( "pack", "--packId", $PackId, "--packTitle", $PackTitle, "--packAuthors", $PackAuthors, "--packVersion", $Version, "--packDir", $PublishDir, "--mainExe", $MainExe, "--outputDir", $ReleasesDir ) if (-not $NoMsi) { # Setup.exe remains the per-user one-click installer. MSI with Either shows # the Windows Installer location selection page to the end user. $vpkArgs += "--msi" $vpkArgs += "--instLocation" $vpkArgs += $InstallLocation } & $vpkPath @vpkArgs if ($LASTEXITCODE -ne 0) { Write-Host "FAILED: vpk pack failed!" -ForegroundColor Red exit 1 } if (-not $NoMsi) { $generatedMsi = Join-Path $ReleasesDir "$PackId-win.msi" $versionedMsi = Join-Path $ReleasesDir "XplorePlane-$Version-win.msi" if (Test-Path -LiteralPath $generatedMsi) { Move-Item -LiteralPath $generatedMsi -Destination $versionedMsi -Force Write-Host "OK: MSI archived as $versionedMsi" -ForegroundColor Green } } Write-Host "OK: Pack succeeded, output in $ReleasesDir" -ForegroundColor Green # Step 4: Deploy to WampServer Write-Host "[4/5] Deploying to WampServer..." -ForegroundColor Yellow if (-not (Test-Path $DeployDir)) { New-Item -ItemType Directory -Path $DeployDir -Force | Out-Null Write-Host " Created directory: $DeployDir" -ForegroundColor DarkGray } Copy-Item -Path (Join-Path $ReleasesDir "*") -Destination $DeployDir -Force -Recurse Write-Host "OK: Deployed to $DeployDir" -ForegroundColor Green # Step 5: Verify Write-Host "[5/5] Verifying deployment..." -ForegroundColor Yellow $releasesFile = Join-Path $DeployDir "RELEASES" if (Test-Path $releasesFile) { Write-Host " OK: RELEASES file exists" -ForegroundColor Green $fullPkg = Get-ChildItem $DeployDir -Filter "*-full.nupkg" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($fullPkg) { $sizeMB = [math]::Round($fullPkg.Length / 1MB, 1) Write-Host " OK: Full package: $($fullPkg.Name) ($sizeMB MB)" -ForegroundColor Green } $deltaPkg = Get-ChildItem $DeployDir -Filter "*-delta.nupkg" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($deltaPkg) { $sizeMB = [math]::Round($deltaPkg.Length / 1MB, 1) Write-Host " OK: Delta package: $($deltaPkg.Name) ($sizeMB MB)" -ForegroundColor Green } $setupExe = Get-ChildItem $DeployDir -Filter "Setup*.exe" | Select-Object -First 1 if ($setupExe) { Write-Host " OK: Setup: $($setupExe.Name)" -ForegroundColor Green } $msi = Get-ChildItem $DeployDir -Filter "*.msi" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($msi) { Write-Host " OK: MSI: $($msi.Name)" -ForegroundColor Green } } else { Write-Host " WARN: RELEASES file not found, deployment may be incomplete" -ForegroundColor Yellow } Write-Host "" Write-Host "========================================================" -ForegroundColor Cyan Write-Host " Release complete!" -ForegroundColor Green Write-Host "" Write-Host " Update URL: http://192.168.16.134/updates/xploreplane" -ForegroundColor White Write-Host " Setup: $DeployDir\Setup.exe (per-user, fixed default location)" -ForegroundColor White if (-not $NoMsi) { Write-Host " MSI: $DeployDir\*.msi (user-selectable location)" -ForegroundColor White } Write-Host "" Write-Host " Make sure WampServer is running (Apache online)." -ForegroundColor DarkGray Write-Host " Client App.config Update:ServerUrl should be:" -ForegroundColor DarkGray Write-Host " http:///updates/xploreplane" -ForegroundColor White Write-Host "========================================================" -ForegroundColor Cyan