Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,15 @@ 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: |
$Platform = '${{ matrix.platform }}'
$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"

Expand All @@ -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

Expand Down
10 changes: 6 additions & 4 deletions scripts/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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'
Expand Down
48 changes: 48 additions & 0 deletions scripts/refresh-integrity-tree.ps1
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions scripts/sign.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."
}
}
Expand Down
104 changes: 104 additions & 0 deletions scripts/verify-integrity-tree.ps1
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 7 additions & 0 deletions src/UniGetUI/UniGetUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@
<Exec Command="pwsh -NoProfile -File ../../scripts/generate-integrity-tree.ps1 -Path $(OutputPath) -MinOutput" />
</Target>

<Target Name="PostPublishGenerateIntegrityTree" AfterTargets="Publish">
<Exec
Command="pwsh -NoProfile -File ../../scripts/generate-integrity-tree.ps1 -Path $(PublishDir) -MinOutput"
Condition="'$(PublishDir)' != '' and Exists('$(PublishDir)')"
/>
</Target>

<Target
Name="EnsureBundledElevatorFromNuGet"
BeforeTargets="PrepareForBuild"
Expand Down