293 lines
11 KiB
PowerShell
293 lines
11 KiB
PowerShell
param(
|
||
# 不传时自动从 XplorePlane.csproj 的 <Version> 读取,传入则覆盖工程配置(用于临时测试版本号)
|
||
[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 的 <Version> 属性读取,
|
||
# 确保发布包版本号与工程配置保持单一来源,不再需要每次手动传参维护。
|
||
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 <Version> from $ProjectPath. Pass -Version explicitly or add <Version> 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://<server-ip>/updates/xploreplane" -ForegroundColor White
|
||
Write-Host "========================================================" -ForegroundColor Cyan
|