Skip to content

Commit 7b53c2a

Browse files
Ensure integrity tree is generated before packaging
1 parent 8757d9f commit 7b53c2a

6 files changed

Lines changed: 191 additions & 8 deletions

File tree

.github/workflows/build-release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,17 +225,15 @@ jobs:
225225
-CertificateName '${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}' `
226226
-TimestampServer '${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}'
227227
228-
- name: Generate integrity tree
229-
shell: pwsh
230-
run: .\scripts\generate-integrity-tree.ps1 -Path $PWD/unigetui_bin -MinOutput
231-
232228
- name: Build installer
233229
shell: pwsh
234230
run: |
235231
$Platform = '${{ matrix.platform }}'
236232
$OutputDir = Join-Path $PWD "output"
237233
New-Item $OutputDir -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
238234
235+
.\scripts\refresh-integrity-tree.ps1 -Path $PWD/unigetui_bin -FailOnUnexpectedFiles
236+
239237
# Configure Inno Setup to use AzureSignTool
240238
$IssPath = "UniGetUI.iss"
241239
@@ -262,6 +260,8 @@ jobs:
262260
$Platform = '${{ matrix.platform }}'
263261
New-Item "output" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
264262
263+
.\scripts\refresh-integrity-tree.ps1 -Path $PWD/unigetui_bin -FailOnUnexpectedFiles
264+
265265
# Zip
266266
Compress-Archive -Path "unigetui_bin/*" -DestinationPath "output/UniGetUI.$Platform.zip" -CompressionLevel Optimal
267267

scripts/build.ps1

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,14 @@ Get-ChildItem $PublishDir | Move-Item -Destination $BinDir -Force
9494
# WingetUI.exe alias for backward compat
9595
Copy-Item (Join-Path $BinDir "UniGetUI.exe") (Join-Path $BinDir "WingetUI.exe") -Force
9696

97-
# --- Integrity tree ---
98-
Write-Host "`n=== Generating integrity tree ===" -ForegroundColor Cyan
99-
& (Join-Path $PSScriptRoot "generate-integrity-tree.ps1") -Path $BinDir -MinOutput
100-
10197
# --- Package output ---
10298
if (Test-Path $OutputPath) { Remove-Item $OutputPath -Recurse -Force }
10399
New-Item $OutputPath -ItemType Directory | Out-Null
104100

105101
$ZipPath = Join-Path $OutputPath "UniGetUI.$Platform.zip"
102+
Write-Host "`n=== Refreshing integrity tree before zip packaging ===" -ForegroundColor Cyan
103+
& (Join-Path $PSScriptRoot "refresh-integrity-tree.ps1") -Path $BinDir -FailOnUnexpectedFiles
104+
106105
Write-Host "`n=== Creating zip: $ZipPath ===" -ForegroundColor Cyan
107106
Compress-Archive -Path (Join-Path $BinDir "*") -DestinationPath $ZipPath -CompressionLevel Optimal
108107

@@ -124,6 +123,9 @@ if (-not $SkipInstaller) {
124123
$IssPath = Join-Path $RepoRoot "UniGetUI.iss"
125124
$IssContent = Get-Content $IssPath -Raw
126125

126+
Write-Host "`n=== Refreshing integrity tree before installer packaging ===" -ForegroundColor Cyan
127+
& (Join-Path $PSScriptRoot "refresh-integrity-tree.ps1") -Path $BinDir -FailOnUnexpectedFiles
128+
127129
try {
128130
$IssContentNoSign = $IssContent -Replace '(?m)^SignTool=.*$', '; SignTool=azsign (disabled for local build)'
129131
$IssContentNoSign = $IssContentNoSign -Replace '(?m)^SignedUninstaller=yes', 'SignedUninstaller=no'

scripts/refresh-integrity-tree.ps1

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env pwsh
2+
<#
3+
.SYNOPSIS
4+
Regenerates IntegrityTree.json and validates it against the current folder contents.
5+
6+
.PARAMETER Path
7+
The directory whose IntegrityTree.json should be refreshed and validated.
8+
9+
.PARAMETER FailOnUnexpectedFiles
10+
Fail validation if files exist in the directory tree but are not present in
11+
IntegrityTree.json.
12+
#>
13+
14+
[CmdletBinding()]
15+
param(
16+
[Parameter(Mandatory, Position = 0)]
17+
[string] $Path,
18+
19+
[switch] $FailOnUnexpectedFiles
20+
)
21+
22+
$ErrorActionPreference = 'Stop'
23+
24+
if (-not (Test-Path $Path -PathType Container)) {
25+
throw "The directory '$Path' does not exist."
26+
}
27+
28+
$Path = (Resolve-Path $Path).Path
29+
$GenerateScriptPath = Join-Path $PSScriptRoot 'generate-integrity-tree.ps1'
30+
$VerifyScriptPath = Join-Path $PSScriptRoot 'verify-integrity-tree.ps1'
31+
32+
if (-not (Test-Path $GenerateScriptPath -PathType Leaf)) {
33+
throw "Integrity tree generator not found at '$GenerateScriptPath'."
34+
}
35+
36+
if (-not (Test-Path $VerifyScriptPath -PathType Leaf)) {
37+
throw "Integrity tree validator not found at '$VerifyScriptPath'."
38+
}
39+
40+
Write-Host "Refreshing integrity tree in $Path..."
41+
& $GenerateScriptPath -Path $Path -MinOutput
42+
43+
$ValidationParameters = @{ Path = $Path }
44+
if ($FailOnUnexpectedFiles) {
45+
$ValidationParameters.FailOnUnexpectedFiles = $true
46+
}
47+
48+
& $VerifyScriptPath @ValidationParameters

scripts/sign.ps1

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,27 @@ function Invoke-BatchSign {
106106
}
107107
}
108108

109+
function Update-IntegrityTree {
110+
param(
111+
[string] $RootPath
112+
)
113+
114+
if (-not $RootPath -or -not (Test-Path $RootPath -PathType Container)) {
115+
return
116+
}
117+
118+
$TreeRefreshScriptPath = Join-Path $PSScriptRoot "refresh-integrity-tree.ps1"
119+
if (-not (Test-Path $TreeRefreshScriptPath -PathType Leaf)) {
120+
Write-Warning "Integrity tree refresh script not found at $TreeRefreshScriptPath"
121+
return
122+
}
123+
124+
& $TreeRefreshScriptPath -Path $RootPath -FailOnUnexpectedFiles
125+
if ($LASTEXITCODE -ne 0) {
126+
throw "refresh-integrity-tree.ps1 failed with exit code $LASTEXITCODE"
127+
}
128+
}
129+
109130
# --- Sign binaries in BinDir ---
110131
if ($FileListPath -and (Test-Path $FileListPath)) {
111132
Write-Host "`n=== Signing binaries from list: $FileListPath ===" -ForegroundColor Cyan
@@ -118,6 +139,7 @@ if ($FileListPath -and (Test-Path $FileListPath)) {
118139
Write-Warning "No .exe or .dll files found in $BinDir"
119140
} else {
120141
Invoke-BatchSign -Files ($filesToSign | ForEach-Object { $_.FullName })
142+
Update-IntegrityTree -RootPath $BinDir
121143
Write-Host "Binary signing complete."
122144
}
123145
}

scripts/verify-integrity-tree.ps1

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env pwsh
2+
<#
3+
.SYNOPSIS
4+
Validates a generated IntegrityTree.json against the files in a directory.
5+
6+
.DESCRIPTION
7+
This mirrors UniGetUI runtime integrity verification and can optionally fail
8+
if the directory contains files that are not listed in IntegrityTree.json.
9+
10+
.PARAMETER Path
11+
The directory containing IntegrityTree.json and the files to validate.
12+
13+
.PARAMETER FailOnUnexpectedFiles
14+
Fail validation if files exist in the directory tree but are not present in
15+
IntegrityTree.json.
16+
#>
17+
18+
[CmdletBinding()]
19+
param(
20+
[Parameter(Mandatory, Position = 0)]
21+
[string] $Path,
22+
23+
[switch] $FailOnUnexpectedFiles
24+
)
25+
26+
$ErrorActionPreference = 'Stop'
27+
28+
if (-not (Test-Path $Path -PathType Container)) {
29+
throw "The directory '$Path' does not exist."
30+
}
31+
32+
$Path = (Resolve-Path $Path).Path
33+
$IntegrityTreePath = Join-Path $Path 'IntegrityTree.json'
34+
35+
if (-not (Test-Path $IntegrityTreePath -PathType Leaf)) {
36+
throw "IntegrityTree.json was not found in '$Path'."
37+
}
38+
39+
$rawData = Get-Content $IntegrityTreePath -Raw
40+
41+
try {
42+
$data = ConvertFrom-Json $rawData -AsHashtable
43+
}
44+
catch {
45+
throw "IntegrityTree.json is not valid JSON: $($_.Exception.Message)"
46+
}
47+
48+
if ($null -eq $data) {
49+
throw 'IntegrityTree.json did not deserialize into a JSON object.'
50+
}
51+
52+
$missingFiles = New-Object System.Collections.Generic.List[string]
53+
$mismatchedFiles = New-Object System.Collections.Generic.List[string]
54+
$unexpectedFiles = New-Object System.Collections.Generic.List[string]
55+
56+
$expectedFiles = @{}
57+
foreach ($entry in $data.GetEnumerator()) {
58+
$relativePath = [string] $entry.Key
59+
$expectedHash = [string] $entry.Value
60+
$expectedFiles[$relativePath] = $true
61+
62+
$fullPath = Join-Path $Path $relativePath
63+
if (-not (Test-Path $fullPath -PathType Leaf)) {
64+
$missingFiles.Add($relativePath)
65+
continue
66+
}
67+
68+
$currentHash = (Get-FileHash $fullPath -Algorithm SHA256).Hash.ToLowerInvariant()
69+
if ($currentHash -ne $expectedHash.ToLowerInvariant()) {
70+
$mismatchedFiles.Add("$relativePath|expected=$expectedHash|got=$currentHash")
71+
}
72+
}
73+
74+
if ($FailOnUnexpectedFiles) {
75+
Get-ChildItem $Path -Recurse -File | ForEach-Object {
76+
$relativePath = $_.FullName.Substring($Path.Length).TrimStart('\', '/') -replace '\\', '/'
77+
if ($relativePath -eq 'IntegrityTree.json') {
78+
return
79+
}
80+
81+
if (-not $expectedFiles.ContainsKey($relativePath)) {
82+
$unexpectedFiles.Add($relativePath)
83+
}
84+
}
85+
}
86+
87+
if ($missingFiles.Count -or $mismatchedFiles.Count -or $unexpectedFiles.Count) {
88+
if ($missingFiles.Count) {
89+
Write-Error "Missing files listed in IntegrityTree.json:`n - $($missingFiles -join "`n - ")"
90+
}
91+
92+
if ($mismatchedFiles.Count) {
93+
Write-Error "Files with mismatched SHA256 values:`n - $($mismatchedFiles -join "`n - ")"
94+
}
95+
96+
if ($unexpectedFiles.Count) {
97+
Write-Error "Unexpected files not present in IntegrityTree.json:`n - $($unexpectedFiles -join "`n - ")"
98+
}
99+
100+
throw 'Integrity tree validation failed.'
101+
}
102+
103+
$validatedFileCount = $data.Count
104+
Write-Host "Integrity tree validation succeeded for $validatedFileCount file(s) in $Path"

src/UniGetUI/UniGetUI.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@
4949
<Exec Command="pwsh -NoProfile -File ../../scripts/generate-integrity-tree.ps1 -Path $(OutputPath) -MinOutput" />
5050
</Target>
5151

52+
<Target Name="PostPublishGenerateIntegrityTree" AfterTargets="Publish">
53+
<Exec
54+
Command="pwsh -NoProfile -File ../../scripts/generate-integrity-tree.ps1 -Path $(PublishDir) -MinOutput"
55+
Condition="'$(PublishDir)' != '' and Exists('$(PublishDir)')"
56+
/>
57+
</Target>
58+
5259
<Target
5360
Name="EnsureBundledElevatorFromNuGet"
5461
BeforeTargets="PrepareForBuild"

0 commit comments

Comments
 (0)