wndrks
This commit is contained in:
475
tools/msix/Publish-MsixToNas.ps1
Normal file
475
tools/msix/Publish-MsixToNas.ps1
Normal file
@@ -0,0 +1,475 @@
|
||||
param(
|
||||
[string]$ProjectPath = (Join-Path $PSScriptRoot "..\..\Tornado3_2026Election\Tornado3_2026Election.csproj"),
|
||||
[ValidateSet("Debug", "Release")]
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Platform = "x64",
|
||||
[string]$RuntimeIdentifier = "win-x64",
|
||||
[ValidatePattern("^\d+\.\d+\.\d+\.\d+$")]
|
||||
[string]$PackageVersion,
|
||||
[switch]$IncrementPackageRevision,
|
||||
[string]$PublicBaseUri = "http://122.34.248.185/msix/",
|
||||
[string]$NasHost = "192.168.200.129",
|
||||
[int]$NasSshPort = 22,
|
||||
[string]$NasUser = $env:NAS_USER,
|
||||
[string]$NasRemotePath = "/volume1/web/msix",
|
||||
[string]$SshKeyPath = $env:NAS_SSH_KEY,
|
||||
[string]$CertificateThumbprint = "E691A33C64DF20A204FFD4F096B9C3EB4B95709C",
|
||||
[string]$CertificateFileName = "Comtrophy_MSIX_Signing.cer",
|
||||
[string]$InstallCertificateScriptPath = (Join-Path $PSScriptRoot "Install-ComtrophyMsixCertificate.ps1"),
|
||||
[switch]$SkipPackageBuild,
|
||||
[switch]$NoUpload,
|
||||
[switch]$NoVerify
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-FullPath {
|
||||
param([Parameter(Mandatory)][string]$Path)
|
||||
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
return (Resolve-Path -LiteralPath $Path).Path
|
||||
}
|
||||
|
||||
$executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
|
||||
}
|
||||
|
||||
function Invoke-Checked {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$FilePath,
|
||||
[Parameter(Mandatory)][string[]]$Arguments
|
||||
)
|
||||
|
||||
Write-Host "> $FilePath $($Arguments -join ' ')"
|
||||
& $FilePath @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$FilePath failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
function Test-IsUnderDirectory {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][string]$Directory
|
||||
)
|
||||
|
||||
$normalizedPath = [System.IO.Path]::GetFullPath($Path).TrimEnd(
|
||||
[System.IO.Path]::DirectorySeparatorChar,
|
||||
[System.IO.Path]::AltDirectorySeparatorChar
|
||||
)
|
||||
$normalizedDirectory = [System.IO.Path]::GetFullPath($Directory).TrimEnd(
|
||||
[System.IO.Path]::DirectorySeparatorChar,
|
||||
[System.IO.Path]::AltDirectorySeparatorChar
|
||||
)
|
||||
|
||||
return $normalizedPath.Equals($normalizedDirectory, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
$normalizedPath.StartsWith(
|
||||
$normalizedDirectory + [System.IO.Path]::DirectorySeparatorChar,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
) -or
|
||||
$normalizedPath.StartsWith(
|
||||
$normalizedDirectory + [System.IO.Path]::AltDirectorySeparatorChar,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)
|
||||
}
|
||||
|
||||
function Find-NewestFile {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Root,
|
||||
[Parameter(Mandatory)][string]$Filter,
|
||||
[DateTime]$NotBefore = [DateTime]::MinValue,
|
||||
[string]$ExcludeDirectory
|
||||
)
|
||||
|
||||
$resolvedExcludeDirectory = if ($ExcludeDirectory -and (Test-Path -LiteralPath $ExcludeDirectory)) {
|
||||
Resolve-FullPath $ExcludeDirectory
|
||||
}
|
||||
else {
|
||||
$null
|
||||
}
|
||||
|
||||
Get-ChildItem -Path $Root -Recurse -File -Filter $Filter |
|
||||
Where-Object {
|
||||
$_.LastWriteTime -ge $NotBefore -and
|
||||
(-not $resolvedExcludeDirectory -or -not (Test-IsUnderDirectory -Path $_.FullName -Directory $resolvedExcludeDirectory))
|
||||
} |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
}
|
||||
|
||||
function Find-PackageFileByLeafName {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$PackageRoot,
|
||||
[Parameter(Mandatory)][string]$LeafName,
|
||||
[string]$PreferredPathFragment,
|
||||
[string]$ExcludeDirectory
|
||||
)
|
||||
|
||||
$resolvedExcludeDirectory = if ($ExcludeDirectory -and (Test-Path -LiteralPath $ExcludeDirectory)) {
|
||||
Resolve-FullPath $ExcludeDirectory
|
||||
}
|
||||
else {
|
||||
$null
|
||||
}
|
||||
|
||||
$candidates = Get-ChildItem -Path $PackageRoot -Recurse -File -Filter $LeafName |
|
||||
Where-Object {
|
||||
-not $resolvedExcludeDirectory -or
|
||||
-not (Test-IsUnderDirectory -Path $_.FullName -Directory $resolvedExcludeDirectory)
|
||||
}
|
||||
|
||||
if ($PreferredPathFragment) {
|
||||
$preferred = $candidates |
|
||||
Where-Object { $_.FullName -like "*$PreferredPathFragment*" } |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
if ($preferred) {
|
||||
return $preferred
|
||||
}
|
||||
}
|
||||
|
||||
$candidates | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
}
|
||||
|
||||
function Get-UriLeafName {
|
||||
param([Parameter(Mandatory)][string]$Uri)
|
||||
|
||||
try {
|
||||
return [System.IO.Path]::GetFileName(([System.Uri]$Uri).AbsolutePath)
|
||||
}
|
||||
catch {
|
||||
return [System.IO.Path]::GetFileName($Uri.Replace("/", "\"))
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-CertificateFile {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$CertificatePath,
|
||||
[Parameter(Mandatory)][string]$Thumbprint
|
||||
)
|
||||
|
||||
if (Test-Path -LiteralPath $CertificatePath) {
|
||||
return
|
||||
}
|
||||
|
||||
$certificate = Get-ChildItem -Path Cert:\CurrentUser\My |
|
||||
Where-Object { $_.Thumbprint -eq $Thumbprint } |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $certificate) {
|
||||
throw "Could not find signing certificate $Thumbprint in Cert:\CurrentUser\My."
|
||||
}
|
||||
|
||||
Export-Certificate -Cert $certificate -FilePath $CertificatePath -Force | Out-Null
|
||||
}
|
||||
|
||||
function Get-PackageManifestVersion {
|
||||
param([Parameter(Mandatory)][string]$ManifestPath)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ManifestPath)) {
|
||||
throw "Package manifest was not found: $ManifestPath"
|
||||
}
|
||||
|
||||
$manifestXml = New-Object System.Xml.XmlDocument
|
||||
$manifestXml.Load($ManifestPath)
|
||||
|
||||
$namespaceManager = New-Object System.Xml.XmlNamespaceManager($manifestXml.NameTable)
|
||||
$namespaceManager.AddNamespace("pkg", $manifestXml.DocumentElement.NamespaceURI)
|
||||
|
||||
$identityNode = $manifestXml.SelectSingleNode("/pkg:Package/pkg:Identity", $namespaceManager)
|
||||
if (-not $identityNode) {
|
||||
throw "Identity node was not found in $ManifestPath."
|
||||
}
|
||||
|
||||
$identityNode.GetAttribute("Version")
|
||||
}
|
||||
|
||||
function Get-NextPackageRevision {
|
||||
param([Parameter(Mandatory)][string]$Version)
|
||||
|
||||
$parts = $Version.Split(".")
|
||||
if ($parts.Count -ne 4) {
|
||||
throw "Package version must have four parts: $Version"
|
||||
}
|
||||
|
||||
$numbers = foreach ($part in $parts) {
|
||||
[int]$part
|
||||
}
|
||||
$numbers[3] += 1
|
||||
|
||||
$numbers -join "."
|
||||
}
|
||||
|
||||
function Set-PackageManifestVersion {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$ManifestPath,
|
||||
[Parameter(Mandatory)][string]$Version
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ManifestPath)) {
|
||||
throw "Package manifest was not found: $ManifestPath"
|
||||
}
|
||||
|
||||
$manifestXml = New-Object System.Xml.XmlDocument
|
||||
$manifestXml.PreserveWhitespace = $true
|
||||
$manifestXml.Load($ManifestPath)
|
||||
|
||||
$namespaceManager = New-Object System.Xml.XmlNamespaceManager($manifestXml.NameTable)
|
||||
$namespaceManager.AddNamespace("pkg", $manifestXml.DocumentElement.NamespaceURI)
|
||||
|
||||
$identityNode = $manifestXml.SelectSingleNode("/pkg:Package/pkg:Identity", $namespaceManager)
|
||||
if (-not $identityNode) {
|
||||
throw "Identity node was not found in $ManifestPath."
|
||||
}
|
||||
|
||||
$currentVersion = $identityNode.GetAttribute("Version")
|
||||
if ($currentVersion -eq $Version) {
|
||||
Write-Host "Package manifest version is already $Version"
|
||||
return
|
||||
}
|
||||
|
||||
$identityNode.SetAttribute("Version", $Version)
|
||||
$manifestXml.Save($ManifestPath)
|
||||
Write-Host "Updated package manifest version from $currentVersion to $Version"
|
||||
}
|
||||
|
||||
function Join-PublicUri {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$BaseUri,
|
||||
[Parameter(Mandatory)][string]$LeafName
|
||||
)
|
||||
|
||||
$normalizedBase = $BaseUri
|
||||
if (-not $normalizedBase.EndsWith("/")) {
|
||||
$normalizedBase += "/"
|
||||
}
|
||||
|
||||
"$normalizedBase$LeafName"
|
||||
}
|
||||
|
||||
$projectFullPath = Resolve-FullPath $ProjectPath
|
||||
$projectDirectory = Split-Path -Parent $projectFullPath
|
||||
$manifestPath = Join-Path $projectDirectory "Package.appxmanifest"
|
||||
$packageRoot = Join-Path $projectDirectory "AppPackages"
|
||||
$stagingRoot = Join-Path $packageRoot "msix-publish-flat"
|
||||
$certificatePath = Join-Path $stagingRoot $CertificateFileName
|
||||
$normalizedPublicBaseUri = $PublicBaseUri
|
||||
if (-not $normalizedPublicBaseUri.EndsWith("/")) {
|
||||
$normalizedPublicBaseUri += "/"
|
||||
}
|
||||
|
||||
if ($PackageVersion -and $IncrementPackageRevision) {
|
||||
throw "Use either PackageVersion or IncrementPackageRevision, not both."
|
||||
}
|
||||
|
||||
if ($PackageVersion -and $SkipPackageBuild) {
|
||||
throw "PackageVersion requires a new package build. Remove -SkipPackageBuild and run again."
|
||||
}
|
||||
|
||||
if ($IncrementPackageRevision -and $SkipPackageBuild) {
|
||||
throw "IncrementPackageRevision requires a new package build. Remove -SkipPackageBuild and run again."
|
||||
}
|
||||
|
||||
if ($IncrementPackageRevision) {
|
||||
$currentPackageVersion = Get-PackageManifestVersion -ManifestPath $manifestPath
|
||||
$PackageVersion = Get-NextPackageRevision -Version $currentPackageVersion
|
||||
Write-Host "Auto-incrementing package version from $currentPackageVersion to $PackageVersion"
|
||||
}
|
||||
|
||||
if ($PackageVersion) {
|
||||
Set-PackageManifestVersion -ManifestPath $manifestPath -Version $PackageVersion
|
||||
}
|
||||
|
||||
$signingCertificate = Get-ChildItem -Path Cert:\CurrentUser\My |
|
||||
Where-Object { $_.Thumbprint -eq $CertificateThumbprint } |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $signingCertificate) {
|
||||
throw "Signing certificate $CertificateThumbprint was not found in Cert:\CurrentUser\My."
|
||||
}
|
||||
|
||||
if (-not $signingCertificate.HasPrivateKey) {
|
||||
throw "Signing certificate $CertificateThumbprint does not have a private key."
|
||||
}
|
||||
|
||||
$buildStartedAt = Get-Date
|
||||
|
||||
if (-not $SkipPackageBuild) {
|
||||
$dotnetArgs = @(
|
||||
"msbuild",
|
||||
$projectFullPath,
|
||||
"/restore",
|
||||
"/t:Build",
|
||||
"/p:Configuration=$Configuration",
|
||||
"/p:Platform=$Platform",
|
||||
"/p:RuntimeIdentifier=$RuntimeIdentifier",
|
||||
"/p:GenerateAppxPackageOnBuild=true",
|
||||
"/p:GenerateAppInstallerFile=true",
|
||||
"/p:AppxPackageSigningEnabled=true",
|
||||
"/p:PackageCertificateThumbprint=$CertificateThumbprint",
|
||||
"/p:AppInstallerUri=$normalizedPublicBaseUri",
|
||||
"/p:AppxBundle=Never"
|
||||
)
|
||||
Invoke-Checked -FilePath "dotnet" -Arguments $dotnetArgs
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $stagingRoot) {
|
||||
$resolvedStaging = Resolve-FullPath $stagingRoot
|
||||
$resolvedPackageRoot = Resolve-FullPath $packageRoot
|
||||
if (-not (Test-IsUnderDirectory -Path $resolvedStaging -Directory $resolvedPackageRoot)) {
|
||||
throw "Refusing to clean staging path outside package root: $resolvedStaging"
|
||||
}
|
||||
Get-ChildItem -LiteralPath $stagingRoot -Force | Remove-Item -Recurse -Force
|
||||
}
|
||||
else {
|
||||
New-Item -ItemType Directory -Path $stagingRoot -Force | Out-Null
|
||||
}
|
||||
|
||||
$appInstallerFile = Find-NewestFile -Root $packageRoot -Filter "*_${Platform}.appinstaller" -NotBefore $(if ($SkipPackageBuild) { [DateTime]::MinValue } else { $buildStartedAt.AddMinutes(-2) }) -ExcludeDirectory $stagingRoot
|
||||
if (-not $appInstallerFile) {
|
||||
$appInstallerFile = Find-NewestFile -Root $packageRoot -Filter "*.appinstaller" -ExcludeDirectory $stagingRoot
|
||||
}
|
||||
|
||||
if (-not $appInstallerFile) {
|
||||
throw "Could not find an .appinstaller file under $packageRoot."
|
||||
}
|
||||
|
||||
$stagedAppInstallerPath = Join-Path $stagingRoot $appInstallerFile.Name
|
||||
Copy-Item -LiteralPath $appInstallerFile.FullName -Destination $stagedAppInstallerPath -Force
|
||||
|
||||
$appInstallerXml = New-Object System.Xml.XmlDocument
|
||||
$appInstallerXml.PreserveWhitespace = $true
|
||||
$appInstallerXml.Load($stagedAppInstallerPath)
|
||||
|
||||
$namespaceManager = New-Object System.Xml.XmlNamespaceManager($appInstallerXml.NameTable)
|
||||
$namespaceManager.AddNamespace("ai", $appInstallerXml.DocumentElement.NamespaceURI)
|
||||
|
||||
$mainPackageNode = $appInstallerXml.SelectSingleNode("//ai:MainPackage", $namespaceManager)
|
||||
if (-not $mainPackageNode) {
|
||||
throw "MainPackage node was not found in $($appInstallerFile.FullName)."
|
||||
}
|
||||
|
||||
$mainPackageLeafName = Get-UriLeafName $mainPackageNode.GetAttribute("Uri")
|
||||
$mainPackageFile = Find-PackageFileByLeafName -PackageRoot $packageRoot -LeafName $mainPackageLeafName -ExcludeDirectory $stagingRoot
|
||||
if (-not $mainPackageFile) {
|
||||
$mainPackageFile = Get-ChildItem -Path $packageRoot -Recurse -File -Filter "*.msix" |
|
||||
Where-Object {
|
||||
$_.FullName -notlike "*\Dependencies\*" -and
|
||||
-not (Test-IsUnderDirectory -Path $_.FullName -Directory $stagingRoot)
|
||||
} |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
}
|
||||
|
||||
if (-not $mainPackageFile) {
|
||||
throw "Could not find the main .msix package referenced by $($appInstallerFile.FullName)."
|
||||
}
|
||||
|
||||
$signature = Get-AuthenticodeSignature -FilePath $mainPackageFile.FullName
|
||||
if ($signature.Status -ne "Valid") {
|
||||
throw "MSIX signature is not valid: $($signature.StatusMessage)"
|
||||
}
|
||||
|
||||
if ($signature.SignerCertificate.Thumbprint -ne $CertificateThumbprint) {
|
||||
throw "MSIX was signed with $($signature.SignerCertificate.Thumbprint), expected $CertificateThumbprint."
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $mainPackageFile.FullName -Destination (Join-Path $stagingRoot $mainPackageFile.Name) -Force
|
||||
$appInstallerXml.DocumentElement.SetAttribute("Uri", (Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $appInstallerFile.Name))
|
||||
$mainPackageNode.SetAttribute("Uri", (Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $mainPackageFile.Name))
|
||||
|
||||
$dependencyNodes = $appInstallerXml.SelectNodes("//ai:Dependencies/ai:Package", $namespaceManager)
|
||||
foreach ($dependencyNode in $dependencyNodes) {
|
||||
$dependencyLeafName = Get-UriLeafName $dependencyNode.GetAttribute("Uri")
|
||||
$architecture = $dependencyNode.GetAttribute("ProcessorArchitecture")
|
||||
$preferredFragment = if ($architecture) { "Dependencies\$architecture" } else { $null }
|
||||
$dependencyFile = Find-PackageFileByLeafName -PackageRoot $packageRoot -LeafName $dependencyLeafName -PreferredPathFragment $preferredFragment -ExcludeDirectory $stagingRoot
|
||||
|
||||
if (-not $dependencyFile) {
|
||||
throw "Could not find dependency package $dependencyLeafName."
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $dependencyFile.FullName -Destination (Join-Path $stagingRoot $dependencyFile.Name) -Force
|
||||
$dependencyNode.SetAttribute("Uri", (Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $dependencyFile.Name))
|
||||
}
|
||||
|
||||
$appInstallerXml.Save($stagedAppInstallerPath)
|
||||
|
||||
Ensure-CertificateFile -CertificatePath $certificatePath -Thumbprint $CertificateThumbprint
|
||||
|
||||
if (Test-Path -LiteralPath $InstallCertificateScriptPath) {
|
||||
Copy-Item -LiteralPath $InstallCertificateScriptPath -Destination (Join-Path $stagingRoot (Split-Path -Leaf $InstallCertificateScriptPath)) -Force
|
||||
}
|
||||
|
||||
$stagedFiles = Get-ChildItem -Path $stagingRoot -File | Sort-Object Name
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Prepared MSIX deployment files:"
|
||||
$stagedFiles | ForEach-Object {
|
||||
Write-Host (" - {0} ({1:N0} bytes)" -f $_.Name, $_.Length)
|
||||
}
|
||||
|
||||
if (-not $NoUpload) {
|
||||
if (-not $NasUser) {
|
||||
throw "NasUser was not provided. Pass -NasUser <user> or set NAS_USER."
|
||||
}
|
||||
|
||||
$sshArgs = @()
|
||||
if ($NasSshPort -ne 22) {
|
||||
$sshArgs += @("-p", [string]$NasSshPort)
|
||||
}
|
||||
if ($SshKeyPath) {
|
||||
$sshArgs += @("-i", (Resolve-FullPath $SshKeyPath))
|
||||
}
|
||||
$sshArgs += @("${NasUser}@${NasHost}", "mkdir -p '$NasRemotePath'")
|
||||
Invoke-Checked -FilePath "ssh" -Arguments $sshArgs
|
||||
|
||||
$scpArgs = @()
|
||||
if ($NasSshPort -ne 22) {
|
||||
$scpArgs += @("-P", [string]$NasSshPort)
|
||||
}
|
||||
if ($SshKeyPath) {
|
||||
$scpArgs += @("-i", (Resolve-FullPath $SshKeyPath))
|
||||
}
|
||||
$scpArgs += $stagedFiles.FullName
|
||||
$scpArgs += "${NasUser}@${NasHost}:$NasRemotePath/"
|
||||
Invoke-Checked -FilePath "scp" -Arguments $scpArgs
|
||||
}
|
||||
|
||||
if (-not $NoVerify) {
|
||||
foreach ($file in $stagedFiles) {
|
||||
$uri = Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $file.Name
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $uri -Method Head -UseBasicParsing -TimeoutSec 20
|
||||
}
|
||||
catch {
|
||||
Write-Warning "HEAD failed for $uri. Trying GET."
|
||||
$response = Invoke-WebRequest -Uri $uri -UseBasicParsing -TimeoutSec 20
|
||||
}
|
||||
|
||||
if ($response.StatusCode -lt 200 -or $response.StatusCode -gt 299) {
|
||||
throw "Verification failed for $uri with status $($response.StatusCode)."
|
||||
}
|
||||
|
||||
if (-not $NoUpload) {
|
||||
$contentLengthHeader = $response.Headers["Content-Length"] | Select-Object -First 1
|
||||
if ($contentLengthHeader) {
|
||||
$remoteLength = [long]$contentLengthHeader
|
||||
if ($remoteLength -ne $file.Length) {
|
||||
throw "Verification failed for $uri. Remote length $remoteLength does not match local length $($file.Length)."
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Warning "No Content-Length header returned for $uri; status verification only."
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Verified $uri"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Staging directory:"
|
||||
Write-Host $stagingRoot
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "App Installer URL:"
|
||||
Write-Host (Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $appInstallerFile.Name)
|
||||
Reference in New Issue
Block a user