wndrks
This commit is contained in:
99
tools/msix/Install-ComtrophyMsixCertificate.ps1
Normal file
99
tools/msix/Install-ComtrophyMsixCertificate.ps1
Normal file
@@ -0,0 +1,99 @@
|
||||
param(
|
||||
[string]$CertificatePath = (Join-Path $PSScriptRoot "Comtrophy_MSIX_Signing.cer"),
|
||||
[string]$CertificateUri = "http://122.34.248.185/msix/Comtrophy_MSIX_Signing.cer",
|
||||
[ValidateSet("LocalMachine", "CurrentUser")]
|
||||
[string]$StoreScope = "LocalMachine",
|
||||
[switch]$NoElevate,
|
||||
[switch]$NoPause
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ExpectedThumbprint = "E691A33C64DF20A204FFD4F096B9C3EB4B95709C"
|
||||
$downloadedCertificate = $false
|
||||
|
||||
function Test-IsAdministrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||
$principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Quote-Argument {
|
||||
param([Parameter(Mandatory)][string]$Value)
|
||||
|
||||
'"' + $Value.Replace('"', '\"') + '"'
|
||||
}
|
||||
|
||||
if ($StoreScope -eq "LocalMachine" -and -not (Test-IsAdministrator)) {
|
||||
if ($NoElevate) {
|
||||
throw "LocalMachine certificate import requires an elevated PowerShell session."
|
||||
}
|
||||
|
||||
if (-not $PSCommandPath) {
|
||||
throw "LocalMachine certificate import requires an elevated PowerShell session."
|
||||
}
|
||||
|
||||
Write-Host "Restarting as administrator to trust the MSIX signing certificate for this PC..."
|
||||
$arguments = @(
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", (Quote-Argument $PSCommandPath),
|
||||
"-CertificatePath", (Quote-Argument $CertificatePath),
|
||||
"-CertificateUri", (Quote-Argument $CertificateUri),
|
||||
"-StoreScope", $StoreScope,
|
||||
"-NoElevate"
|
||||
)
|
||||
if ($NoPause) {
|
||||
$arguments += "-NoPause"
|
||||
}
|
||||
|
||||
$process = Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -Verb RunAs -Wait -PassThru
|
||||
exit $process.ExitCode
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $CertificatePath)) {
|
||||
$certificateDirectory = Split-Path -Parent $CertificatePath
|
||||
if ($certificateDirectory -and -not (Test-Path -LiteralPath $certificateDirectory)) {
|
||||
New-Item -ItemType Directory -Path $certificateDirectory -Force | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "Downloading MSIX signing certificate..."
|
||||
Invoke-WebRequest -Uri $CertificateUri -OutFile $CertificatePath -UseBasicParsing
|
||||
$downloadedCertificate = $true
|
||||
}
|
||||
|
||||
$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertificatePath)
|
||||
if ($certificate.Thumbprint -ne $ExpectedThumbprint) {
|
||||
throw "Unexpected certificate thumbprint. Expected $ExpectedThumbprint but got $($certificate.Thumbprint)."
|
||||
}
|
||||
|
||||
$stores = @(
|
||||
"Cert:\$StoreScope\TrustedPeople",
|
||||
"Cert:\$StoreScope\Root"
|
||||
)
|
||||
|
||||
foreach ($store in $stores) {
|
||||
$existing = Get-ChildItem -Path $store | Where-Object { $_.Thumbprint -eq $ExpectedThumbprint }
|
||||
if ($existing) {
|
||||
Write-Host "Certificate already trusted in $store"
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Importing certificate into $store"
|
||||
Import-Certificate -FilePath $CertificatePath -CertStoreLocation $store | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "MSIX signing certificate is trusted in $StoreScope for thumbprint $ExpectedThumbprint."
|
||||
Write-Host ""
|
||||
Write-Host "Certificate setup is complete."
|
||||
Write-Host "Install the app separately with this link:"
|
||||
Write-Host "http://122.34.248.185/msix/Tornado3_2026Election_x64.appinstaller"
|
||||
|
||||
if ($downloadedCertificate) {
|
||||
Write-Host "Certificate saved to $CertificatePath"
|
||||
}
|
||||
|
||||
if (-not $NoPause) {
|
||||
Write-Host ""
|
||||
Read-Host "Press Enter to close this window"
|
||||
}
|
||||
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)
|
||||
120
tools/msix/README.md
Normal file
120
tools/msix/README.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# MSIX publish workflow
|
||||
|
||||
This folder contains the scripts used to build the MSIX package, flatten the
|
||||
App Installer deployment files, upload them to the Synology NAS web folder, and
|
||||
verify the public download URLs.
|
||||
|
||||
## Files
|
||||
|
||||
- `Publish-MsixToNas.ps1`: builds the app package, stages the deployable files,
|
||||
uploads them to the NAS with SSH/SCP, and verifies the public URLs.
|
||||
- `Install-ComtrophyMsixCertificate.ps1`: installs the MSIX signing certificate
|
||||
for the current Windows user, then optionally opens the appinstaller URL.
|
||||
|
||||
## First-time NAS SSH setup
|
||||
|
||||
1. Create or choose a NAS user that can write to `/volume1/web/msix`.
|
||||
2. Enable SSH on the Synology NAS.
|
||||
3. Optional but recommended: create an SSH key for publishing.
|
||||
|
||||
```powershell
|
||||
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\nas_msix_ed25519
|
||||
type $env:USERPROFILE\.ssh\nas_msix_ed25519.pub
|
||||
```
|
||||
|
||||
Add the printed public key to the NAS user's `~/.ssh/authorized_keys`.
|
||||
|
||||
Test the connection:
|
||||
|
||||
```powershell
|
||||
ssh -i $env:USERPROFILE\.ssh\nas_msix_ed25519 <nas-user>@192.168.200.129 "ls -ld /volume1/web/msix"
|
||||
```
|
||||
|
||||
## Publish a new build
|
||||
|
||||
Set the NAS login once in the current PowerShell session:
|
||||
|
||||
```powershell
|
||||
$env:NAS_USER = "<nas-user>"
|
||||
$env:NAS_SSH_KEY = "$env:USERPROFILE\.ssh\nas_msix_ed25519"
|
||||
```
|
||||
|
||||
Build, package, upload, and verify:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\msix\Publish-MsixToNas.ps1 -Configuration Release -IncrementPackageRevision
|
||||
```
|
||||
|
||||
Use `-IncrementPackageRevision` for normal approved deployments. It reads the
|
||||
current `Package.appxmanifest` version and increments the fourth version part
|
||||
before building. App Installer uses the MSIX package version to decide whether a
|
||||
client should receive an update.
|
||||
|
||||
In Codex sessions, this is the command to run only after the user explicitly
|
||||
approves publishing the finished work.
|
||||
|
||||
To publish a specific version instead:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\msix\Publish-MsixToNas.ps1 -Configuration Release -PackageVersion 1.0.3.2
|
||||
```
|
||||
|
||||
To upload the latest already-built package without rebuilding:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\msix\Publish-MsixToNas.ps1 -Configuration Debug -SkipPackageBuild
|
||||
```
|
||||
|
||||
To prepare files locally without uploading:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\msix\Publish-MsixToNas.ps1 -Configuration Debug -SkipPackageBuild -NoUpload
|
||||
```
|
||||
|
||||
The default public base URL is:
|
||||
|
||||
```text
|
||||
http://122.34.248.185/msix/
|
||||
```
|
||||
|
||||
If the deployment should use the Synology DDNS name instead, pass:
|
||||
|
||||
```powershell
|
||||
-PublicBaseUri "http://comtropy.synology.me/msix/"
|
||||
```
|
||||
|
||||
## Installer link
|
||||
|
||||
After publish, the installer URL is:
|
||||
|
||||
```text
|
||||
http://122.34.248.185/msix/Tornado3_2026Election_x64.appinstaller
|
||||
```
|
||||
|
||||
The user PC must trust the signing certificate before installing the MSIX for
|
||||
the first time. The script only installs the certificate; it does not run the
|
||||
app installer. Approve the UAC administrator prompt when Windows asks:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\Install-ComtrophyMsixCertificate.ps1
|
||||
```
|
||||
|
||||
To run it directly from the NAS on a target PC:
|
||||
|
||||
```powershell
|
||||
$script = Join-Path $env:TEMP "Install-ComtrophyMsixCertificate.ps1"
|
||||
Invoke-WebRequest "http://122.34.248.185/msix/Install-ComtrophyMsixCertificate.ps1" -OutFile $script
|
||||
powershell -ExecutionPolicy Bypass -File $script
|
||||
```
|
||||
|
||||
After the certificate setup is complete, open the appinstaller link once to
|
||||
install the app. After installation, run the app from the Windows Start menu, not
|
||||
from the appinstaller link.
|
||||
|
||||
If installation fails with `0x800B0109`, confirm the certificate is present in
|
||||
both local computer stores:
|
||||
|
||||
```powershell
|
||||
Get-ChildItem Cert:\LocalMachine\TrustedPeople, Cert:\LocalMachine\Root |
|
||||
Where-Object Thumbprint -eq "E691A33C64DF20A204FFD4F096B9C3EB4B95709C"
|
||||
```
|
||||
Reference in New Issue
Block a user