diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 1fbb5987e..76d3ef87f 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -225,10 +225,6 @@ jobs:
-CertificateName '${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}' `
-TimestampServer '${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}'
- - name: Generate integrity tree
- shell: pwsh
- run: .\scripts\generate-integrity-tree.ps1 -Path $PWD/unigetui_bin -MinOutput
-
- name: Build installer
shell: pwsh
run: |
@@ -236,6 +232,8 @@ jobs:
$OutputDir = Join-Path $PWD "output"
New-Item $OutputDir -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
+ .\scripts\refresh-integrity-tree.ps1 -Path $PWD/unigetui_bin -FailOnUnexpectedFiles
+
# Configure Inno Setup to use AzureSignTool
$IssPath = "UniGetUI.iss"
@@ -262,6 +260,8 @@ jobs:
$Platform = '${{ matrix.platform }}'
New-Item "output" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
+ .\scripts\refresh-integrity-tree.ps1 -Path $PWD/unigetui_bin -FailOnUnexpectedFiles
+
# Zip
Compress-Archive -Path "unigetui_bin/*" -DestinationPath "output/UniGetUI.$Platform.zip" -CompressionLevel Optimal
diff --git a/scripts/build.ps1 b/scripts/build.ps1
index 1102d9915..d5cfab4f6 100644
--- a/scripts/build.ps1
+++ b/scripts/build.ps1
@@ -94,15 +94,14 @@ Get-ChildItem $PublishDir | Move-Item -Destination $BinDir -Force
# WingetUI.exe alias for backward compat
Copy-Item (Join-Path $BinDir "UniGetUI.exe") (Join-Path $BinDir "WingetUI.exe") -Force
-# --- Integrity tree ---
-Write-Host "`n=== Generating integrity tree ===" -ForegroundColor Cyan
-& (Join-Path $PSScriptRoot "generate-integrity-tree.ps1") -Path $BinDir -MinOutput
-
# --- Package output ---
if (Test-Path $OutputPath) { Remove-Item $OutputPath -Recurse -Force }
New-Item $OutputPath -ItemType Directory | Out-Null
$ZipPath = Join-Path $OutputPath "UniGetUI.$Platform.zip"
+Write-Host "`n=== Refreshing integrity tree before zip packaging ===" -ForegroundColor Cyan
+& (Join-Path $PSScriptRoot "refresh-integrity-tree.ps1") -Path $BinDir -FailOnUnexpectedFiles
+
Write-Host "`n=== Creating zip: $ZipPath ===" -ForegroundColor Cyan
Compress-Archive -Path (Join-Path $BinDir "*") -DestinationPath $ZipPath -CompressionLevel Optimal
@@ -124,6 +123,9 @@ if (-not $SkipInstaller) {
$IssPath = Join-Path $RepoRoot "UniGetUI.iss"
$IssContent = Get-Content $IssPath -Raw
+ Write-Host "`n=== Refreshing integrity tree before installer packaging ===" -ForegroundColor Cyan
+ & (Join-Path $PSScriptRoot "refresh-integrity-tree.ps1") -Path $BinDir -FailOnUnexpectedFiles
+
try {
$IssContentNoSign = $IssContent -Replace '(?m)^SignTool=.*$', '; SignTool=azsign (disabled for local build)'
$IssContentNoSign = $IssContentNoSign -Replace '(?m)^SignedUninstaller=yes', 'SignedUninstaller=no'
diff --git a/scripts/refresh-integrity-tree.ps1 b/scripts/refresh-integrity-tree.ps1
new file mode 100644
index 000000000..1899f876e
--- /dev/null
+++ b/scripts/refresh-integrity-tree.ps1
@@ -0,0 +1,48 @@
+#!/usr/bin/env pwsh
+<#
+.SYNOPSIS
+ Regenerates IntegrityTree.json and validates it against the current folder contents.
+
+.PARAMETER Path
+ The directory whose IntegrityTree.json should be refreshed and validated.
+
+.PARAMETER FailOnUnexpectedFiles
+ Fail validation if files exist in the directory tree but are not present in
+ IntegrityTree.json.
+#>
+
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory, Position = 0)]
+ [string] $Path,
+
+ [switch] $FailOnUnexpectedFiles
+)
+
+$ErrorActionPreference = 'Stop'
+
+if (-not (Test-Path $Path -PathType Container)) {
+ throw "The directory '$Path' does not exist."
+}
+
+$Path = (Resolve-Path $Path).Path
+$GenerateScriptPath = Join-Path $PSScriptRoot 'generate-integrity-tree.ps1'
+$VerifyScriptPath = Join-Path $PSScriptRoot 'verify-integrity-tree.ps1'
+
+if (-not (Test-Path $GenerateScriptPath -PathType Leaf)) {
+ throw "Integrity tree generator not found at '$GenerateScriptPath'."
+}
+
+if (-not (Test-Path $VerifyScriptPath -PathType Leaf)) {
+ throw "Integrity tree validator not found at '$VerifyScriptPath'."
+}
+
+Write-Host "Refreshing integrity tree in $Path..."
+& $GenerateScriptPath -Path $Path -MinOutput
+
+$ValidationParameters = @{ Path = $Path }
+if ($FailOnUnexpectedFiles) {
+ $ValidationParameters.FailOnUnexpectedFiles = $true
+}
+
+& $VerifyScriptPath @ValidationParameters
\ No newline at end of file
diff --git a/scripts/sign.ps1 b/scripts/sign.ps1
index ab90696ea..be2a0f97e 100644
--- a/scripts/sign.ps1
+++ b/scripts/sign.ps1
@@ -106,6 +106,27 @@ function Invoke-BatchSign {
}
}
+function Update-IntegrityTree {
+ param(
+ [string] $RootPath
+ )
+
+ if (-not $RootPath -or -not (Test-Path $RootPath -PathType Container)) {
+ return
+ }
+
+ $TreeRefreshScriptPath = Join-Path $PSScriptRoot "refresh-integrity-tree.ps1"
+ if (-not (Test-Path $TreeRefreshScriptPath -PathType Leaf)) {
+ Write-Warning "Integrity tree refresh script not found at $TreeRefreshScriptPath"
+ return
+ }
+
+ & $TreeRefreshScriptPath -Path $RootPath -FailOnUnexpectedFiles
+ if ($LASTEXITCODE -ne 0) {
+ throw "refresh-integrity-tree.ps1 failed with exit code $LASTEXITCODE"
+ }
+}
+
# --- Sign binaries in BinDir ---
if ($FileListPath -and (Test-Path $FileListPath)) {
Write-Host "`n=== Signing binaries from list: $FileListPath ===" -ForegroundColor Cyan
@@ -118,6 +139,7 @@ if ($FileListPath -and (Test-Path $FileListPath)) {
Write-Warning "No .exe or .dll files found in $BinDir"
} else {
Invoke-BatchSign -Files ($filesToSign | ForEach-Object { $_.FullName })
+ Update-IntegrityTree -RootPath $BinDir
Write-Host "Binary signing complete."
}
}
diff --git a/scripts/verify-integrity-tree.ps1 b/scripts/verify-integrity-tree.ps1
new file mode 100644
index 000000000..211376cb4
--- /dev/null
+++ b/scripts/verify-integrity-tree.ps1
@@ -0,0 +1,104 @@
+#!/usr/bin/env pwsh
+<#
+.SYNOPSIS
+ Validates a generated IntegrityTree.json against the files in a directory.
+
+.DESCRIPTION
+ This mirrors UniGetUI runtime integrity verification and can optionally fail
+ if the directory contains files that are not listed in IntegrityTree.json.
+
+.PARAMETER Path
+ The directory containing IntegrityTree.json and the files to validate.
+
+.PARAMETER FailOnUnexpectedFiles
+ Fail validation if files exist in the directory tree but are not present in
+ IntegrityTree.json.
+#>
+
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory, Position = 0)]
+ [string] $Path,
+
+ [switch] $FailOnUnexpectedFiles
+)
+
+$ErrorActionPreference = 'Stop'
+
+if (-not (Test-Path $Path -PathType Container)) {
+ throw "The directory '$Path' does not exist."
+}
+
+$Path = (Resolve-Path $Path).Path
+$IntegrityTreePath = Join-Path $Path 'IntegrityTree.json'
+
+if (-not (Test-Path $IntegrityTreePath -PathType Leaf)) {
+ throw "IntegrityTree.json was not found in '$Path'."
+}
+
+$rawData = Get-Content $IntegrityTreePath -Raw
+
+try {
+ $data = ConvertFrom-Json $rawData -AsHashtable
+}
+catch {
+ throw "IntegrityTree.json is not valid JSON: $($_.Exception.Message)"
+}
+
+if ($null -eq $data) {
+ throw 'IntegrityTree.json did not deserialize into a JSON object.'
+}
+
+$missingFiles = New-Object System.Collections.Generic.List[string]
+$mismatchedFiles = New-Object System.Collections.Generic.List[string]
+$unexpectedFiles = New-Object System.Collections.Generic.List[string]
+
+$expectedFiles = @{}
+foreach ($entry in $data.GetEnumerator()) {
+ $relativePath = [string] $entry.Key
+ $expectedHash = [string] $entry.Value
+ $expectedFiles[$relativePath] = $true
+
+ $fullPath = Join-Path $Path $relativePath
+ if (-not (Test-Path $fullPath -PathType Leaf)) {
+ $missingFiles.Add($relativePath)
+ continue
+ }
+
+ $currentHash = (Get-FileHash $fullPath -Algorithm SHA256).Hash.ToLowerInvariant()
+ if ($currentHash -ne $expectedHash.ToLowerInvariant()) {
+ $mismatchedFiles.Add("$relativePath|expected=$expectedHash|got=$currentHash")
+ }
+}
+
+if ($FailOnUnexpectedFiles) {
+ Get-ChildItem $Path -Recurse -File | ForEach-Object {
+ $relativePath = $_.FullName.Substring($Path.Length).TrimStart('\', '/') -replace '\\', '/'
+ if ($relativePath -eq 'IntegrityTree.json') {
+ return
+ }
+
+ if (-not $expectedFiles.ContainsKey($relativePath)) {
+ $unexpectedFiles.Add($relativePath)
+ }
+ }
+}
+
+if ($missingFiles.Count -or $mismatchedFiles.Count -or $unexpectedFiles.Count) {
+ if ($missingFiles.Count) {
+ Write-Error "Missing files listed in IntegrityTree.json:`n - $($missingFiles -join "`n - ")"
+ }
+
+ if ($mismatchedFiles.Count) {
+ Write-Error "Files with mismatched SHA256 values:`n - $($mismatchedFiles -join "`n - ")"
+ }
+
+ if ($unexpectedFiles.Count) {
+ Write-Error "Unexpected files not present in IntegrityTree.json:`n - $($unexpectedFiles -join "`n - ")"
+ }
+
+ throw 'Integrity tree validation failed.'
+}
+
+$validatedFileCount = $data.Count
+Write-Host "Integrity tree validation succeeded for $validatedFileCount file(s) in $Path"
\ No newline at end of file
diff --git a/src/UniGetUI/UniGetUI.csproj b/src/UniGetUI/UniGetUI.csproj
index 70f5890de..5ad04153e 100644
--- a/src/UniGetUI/UniGetUI.csproj
+++ b/src/UniGetUI/UniGetUI.csproj
@@ -49,6 +49,13 @@
+
+
+
+