[CmdletBinding()] param([string]$Configuration = 'Release', [switch]$SkipTests) $ErrorActionPreference = 'Stop' $projectRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $distributionRoot = Join-Path $projectRoot 'dist' $payloadRoot = Join-Path $distributionRoot 'payload' $setupPublishRoot = Join-Path $distributionRoot 'setup-publish' $version = ([xml](Get-Content -LiteralPath (Join-Path $projectRoot 'Directory.Build.props') -Raw)).Project.PropertyGroup.Version function Reset-BuildDirectory([string]$Path) { $absoluteTarget = [IO.Path]::GetFullPath($Path) $allowedRoot = [IO.Path]::GetFullPath($distributionRoot) + [IO.Path]::DirectorySeparatorChar if (-not $absoluteTarget.StartsWith($allowedRoot, [StringComparison]::OrdinalIgnoreCase)) { throw "Refusing to remove outside dist: $absoluteTarget" } if (Test-Path -LiteralPath $absoluteTarget) { if ((Get-Item -LiteralPath $absoluteTarget -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Build directory is a reparse point: $absoluteTarget" } $linkedEntry = Get-ChildItem -LiteralPath $absoluteTarget -Recurse -Force | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint } | Select-Object -First 1 if ($linkedEntry) { throw "Build directory contains a reparse point: $($linkedEntry.FullName)" } Remove-Item -LiteralPath $absoluteTarget -Recurse -Force } New-Item -ItemType Directory -Path $absoluteTarget -Force | Out-Null } function Invoke-DotNet([string[]]$Arguments) { & dotnet @Arguments if ($LASTEXITCODE -ne 0) { throw "dotnet failed ($LASTEXITCODE): $($Arguments -join ' ')" } } $coreRoot = Join-Path $projectRoot 'tools/core-unpacked/sing-box-1.14.2-windows-amd64' $rulesRoot = Join-Path $projectRoot 'tools/rules' $xrayRoot = Join-Path $projectRoot 'tools/xray-unpacked' foreach ($coreFile in @('sing-box.exe', 'libcronet.dll', 'LICENSE')) { if (-not (Test-Path -LiteralPath (Join-Path $coreRoot $coreFile) -PathType Leaf)) { throw "Missing packaged VPN core: $coreFile" } } foreach ($xrayFile in @('xray.exe', 'wintun.dll', 'LICENSE', 'LICENSE-Wintun')) { if (-not (Test-Path -LiteralPath (Join-Path $xrayRoot $xrayFile) -PathType Leaf)) { throw "Missing packaged Xray core: $xrayFile. Run scripts/prepare-xray.ps1." } } if (-not (Test-Path -LiteralPath $rulesRoot) -or -not (Get-ChildItem -LiteralPath $rulesRoot -Filter '*.srs' -File)) { throw 'Missing routing rules in tools/rules. The installer never downloads dependencies.' } foreach ($project in @('src/QVPN.App/QVPN.App.csproj', 'src/QVPN.Service/QVPN.Service.csproj', 'src/QVPN.Setup/QVPN.Setup.csproj')) { if (-not (Test-Path -LiteralPath (Join-Path $projectRoot $project))) { throw "Missing project: $project" } } if (-not $SkipTests) { $testProjects = @(Get-ChildItem -LiteralPath (Join-Path $projectRoot 'tests') -Filter '*.csproj' -File -Recurse -ErrorAction SilentlyContinue) foreach ($testProject in $testProjects) { Invoke-DotNet -Arguments @('run', '--project', $testProject.FullName, '-c', $Configuration) } } New-Item -ItemType Directory -Path $distributionRoot -Force | Out-Null Reset-BuildDirectory $payloadRoot Reset-BuildDirectory $setupPublishRoot Write-Host 'Publishing Windows UI and service with the bundled .NET runtime...' Invoke-DotNet -Arguments @('publish', (Join-Path $projectRoot 'src/QVPN.App/QVPN.App.csproj'), '-c', $Configuration, '-r', 'win-x64', '--self-contained', 'true', '-p:PublishSingleFile=true', '-p:IncludeNativeLibrariesForSelfExtract=true', '-o', (Join-Path $payloadRoot 'app')) Invoke-DotNet -Arguments @('publish', (Join-Path $projectRoot 'src/QVPN.Service/QVPN.Service.csproj'), '-c', $Configuration, '-r', 'win-x64', '--self-contained', 'true', '-p:PublishSingleFile=false', '-o', (Join-Path $payloadRoot 'service')) New-Item -ItemType Directory -Path (Join-Path $payloadRoot 'core'), (Join-Path $payloadRoot 'rules') -Force | Out-Null Copy-Item -LiteralPath (Join-Path $coreRoot 'sing-box.exe'), (Join-Path $coreRoot 'libcronet.dll'), (Join-Path $coreRoot 'LICENSE') -Destination (Join-Path $payloadRoot 'core') Copy-Item -LiteralPath (Join-Path $xrayRoot 'xray.exe'), (Join-Path $xrayRoot 'wintun.dll'), (Join-Path $xrayRoot 'LICENSE-Wintun') -Destination (Join-Path $payloadRoot 'core') Copy-Item -LiteralPath (Join-Path $xrayRoot 'LICENSE') -Destination (Join-Path $payloadRoot 'core/LICENSE-Xray') Copy-Item -LiteralPath (Join-Path $projectRoot 'tools/Xray-core-v26.9.9-source.zip') -Destination (Join-Path $payloadRoot 'core/Xray-core-v26.9.9-source.zip') Get-ChildItem -LiteralPath $rulesRoot -Filter '*.srs' -File | Copy-Item -Destination (Join-Path $payloadRoot 'rules') if (Test-Path -LiteralPath (Join-Path $projectRoot 'README.md')) { Copy-Item -LiteralPath (Join-Path $projectRoot 'README.md') -Destination (Join-Path $payloadRoot 'README.md') } if (Test-Path -LiteralPath (Join-Path $PSScriptRoot '../src/QVPN.Setup/COPYING')) { Copy-Item -LiteralPath (Join-Path $PSScriptRoot '../src/QVPN.Setup/COPYING') -Destination (Join-Path $payloadRoot 'core/COPYING') } if (Test-Path -LiteralPath (Join-Path $projectRoot 'tools/sing-box-1.14.2-source.zip')) { Copy-Item -LiteralPath (Join-Path $projectRoot 'tools/sing-box-1.14.2-source.zip') -Destination (Join-Path $payloadRoot 'core/sing-box-1.14.2-source.zip') } @' QVPN uses unmodified Xray-core v26.9.9 (Windows amd64), copyright Project X contributors, for VLESS/Reality. Xray is distributed under Mozilla Public License 2.0 in core/LICENSE-Xray. The original upstream source is included in core/Xray-core-v26.9.9-source.zip. Source: https://github.com/XTLS/Xray-core/tree/v26.9.9 Release: https://github.com/XTLS/Xray-core/releases/tag/v26.9.9 Wintun license is included in core/LICENSE-Wintun. QVPN includes sing-box v1.14.2 (Windows amd64), copyright nekohasekai / SagerNet. sing-box provides the Windows TUN, DNS handling and routing to the local Xray proxy. sing-box is distributed under GNU GPL v3 or later with the notice in core/LICENSE. Upstream source archive for the unmodified binary: core/sing-box-1.14.2-source.zip Source tree and module/build instructions: https://github.com/SagerNet/sing-box/tree/v1.14.2 Release and original archive: https://github.com/SagerNet/sing-box/releases/tag/v1.14.2 The bundled rule sets are from https://github.com/SagerNet/sing-geosite and https://github.com/SagerNet/sing-geoip. See the respective repositories for source data and notices. '@ | Set-Content -LiteralPath (Join-Path $payloadRoot 'THIRD-PARTY-NOTICES.txt') -Encoding utf8 Get-ChildItem -LiteralPath $payloadRoot -Filter '*.pdb' -Recurse -File | Remove-Item -Force $manifest = [ordered]@{} foreach ($file in (Get-ChildItem -LiteralPath $payloadRoot -Recurse -File | Sort-Object FullName)) { $relativePath = [IO.Path]::GetRelativePath($payloadRoot, $file.FullName) $manifest[$relativePath] = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash } $manifest | ConvertTo-Json -Depth 3 | Set-Content -LiteralPath (Join-Path $payloadRoot 'payload-manifest.json') -Encoding utf8 $payloadZip = Join-Path $distributionRoot 'payload.zip' if (Test-Path -LiteralPath $payloadZip) { Remove-Item -LiteralPath $payloadZip -Force } Add-Type -AssemblyName System.IO.Compression.FileSystem [IO.Compression.ZipFile]::CreateFromDirectory($payloadRoot, $payloadZip, [IO.Compression.CompressionLevel]::Optimal, $false) Write-Host 'Embedding the complete payload in the offline Windows installer...' Invoke-DotNet -Arguments @('publish', (Join-Path $projectRoot 'src/QVPN.Setup/QVPN.Setup.csproj'), '-c', $Configuration, '-r', 'win-x64', '--self-contained', 'true', '-o', $setupPublishRoot) $installerName = "QVPN-Setup-$version.exe" $installer = Join-Path $distributionRoot $installerName Copy-Item -LiteralPath (Join-Path $setupPublishRoot 'QVPN-Setup.exe') -Destination $installer -Force $installerHash = (Get-FileHash -LiteralPath $installer -Algorithm SHA256).Hash "$installerHash $installerName" | Set-Content -LiteralPath (Join-Path $distributionRoot "QVPN-Setup-$version.sha256") -Encoding ascii Write-Host "Installer: $installer" Write-Host "SHA256: $installerHash"