|
| 1 | +# Stages and zips the portable distribution for one architecture. |
| 2 | +# |
| 3 | +# The portable zip IS the beta artifact (docs/superpowers/specs/ |
| 4 | +# 2026-08-09-windows-beta-distribution-design.md, section 2): the build output |
| 5 | +# as a plain folder -- app, service, SDK dll, wintun, resources.pri, the |
| 6 | +# self-contained WindowsAppRuntime -- plus the third-party notices, the wintun |
| 7 | +# license, and a README. No installer, and no wrapper folder inside the zip: |
| 8 | +# the in-app update checker downloads one of these and swaps files in place, |
| 9 | +# so the payload must sit at the zip ROOT under the exact name grammar |
| 10 | +# |
| 11 | +# URnetwork-v<version>-windows-<x64|arm64>-portable.zip |
| 12 | +# |
| 13 | +# Runs locally against whatever build-local.ps1 produced, and in CI against |
| 14 | +# the same layout: |
| 15 | +# powershell -NoProfile -File app\tools\package-portable.ps1 -Platform x64 -Version 2026.8.9-1024096560-beta |
| 16 | +# |
| 17 | +# ASCII on purpose: this file must parse identically under Windows PowerShell |
| 18 | +# 5.1 (which reads unmarked files as cp1252) and pwsh. |
| 19 | +# |
| 20 | +# SPDX-License-Identifier: MPL-2.0 |
| 21 | +[CmdletBinding()] |
| 22 | +param( |
| 23 | + [ValidateSet("x64", "ARM64")] |
| 24 | + [string]$Platform = "x64", |
| 25 | + # the android-format version this build was stamped with; local dev builds |
| 26 | + # default to the sentinel the version contract reserves for "not a release" |
| 27 | + [string]$Version = "0.0.0-dev" |
| 28 | +) |
| 29 | + |
| 30 | +$ErrorActionPreference = "Stop" |
| 31 | + |
| 32 | +$app = (Resolve-Path "$PSScriptRoot\..").Path |
| 33 | +$srcDir = Join-Path $app "build\$Platform\Release" |
| 34 | +# releases spell the arch in lowercase, msbuild platforms do not |
| 35 | +$arch = $Platform.ToLowerInvariant() |
| 36 | +# tolerate a v-prefixed version so a release tag pastes straight in |
| 37 | +$Version = $Version -replace '^v', '' |
| 38 | + |
| 39 | +if (-not (Test-Path $srcDir)) { |
| 40 | + throw "no build output at $srcDir -- run app\tools\build-local.ps1 first" |
| 41 | +} |
| 42 | + |
| 43 | +# Fail on the files that matter instead of zipping whatever happens to exist: |
| 44 | +# a zip missing the service exe would look fine until a user ran it. (CI |
| 45 | +# learned this lesson with the SDK job asserting on dirs instead of the DLL.) |
| 46 | +$required = @("URnetwork.exe", "urnetworkd.exe", "URnetworkSdk.dll", "wintun.dll", "resources.pri") |
| 47 | +foreach ($name in $required) { |
| 48 | + if (-not (Test-Path (Join-Path $srcDir $name))) { |
| 49 | + throw "required payload file missing from ${srcDir}: $name" |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +# Without the self-contained WindowsAppRuntime payload the zip only runs where |
| 54 | +# the runtime is preinstalled. Warn rather than fail: builds made before the |
| 55 | +# self-contained switch are still worth packaging for a look inside. |
| 56 | +if (-not (Test-Path (Join-Path $srcDir "Microsoft.ui.xaml.dll"))) { |
| 57 | + Write-Warning "Microsoft.ui.xaml.dll is not in the build output -- this build is NOT self-contained, so the zip will require the Windows App Runtime to be installed" |
| 58 | +} |
| 59 | + |
| 60 | +# --- stage -------------------------------------------------------------------- |
| 61 | +$stage = Join-Path $app "build\$Platform\portable-stage" |
| 62 | +if (Test-Path $stage) { Remove-Item $stage -Recurse -Force } |
| 63 | +New-Item -ItemType Directory -Path $stage | Out-Null |
| 64 | + |
| 65 | +# Copy the whole tree minus debug/link junk, rather than an allowlist: an |
| 66 | +# allowlist already dropped Assets\Fonts from the CI artifact once, and would |
| 67 | +# silently drop the next asset added too. |
| 68 | +$excludeExt = @(".pdb", ".lib", ".exp", ".ilk") |
| 69 | +$srcFull = (Get-Item $srcDir).FullName |
| 70 | +$payload = Get-ChildItem $srcDir -Recurse -File | Where-Object { |
| 71 | + $rel = $_.FullName.Substring($srcFull.Length + 1) |
| 72 | + $inObj = $false |
| 73 | + foreach ($part in ($rel -split '\\')) { |
| 74 | + if ($part -eq "obj") { $inObj = $true } |
| 75 | + } |
| 76 | + (-not $inObj) -and ($excludeExt -notcontains $_.Extension.ToLowerInvariant()) |
| 77 | +} |
| 78 | +foreach ($f in $payload) { |
| 79 | + $rel = $f.FullName.Substring($srcFull.Length + 1) |
| 80 | + $dest = Join-Path $stage $rel |
| 81 | + $destDir = Split-Path $dest -Parent |
| 82 | + if (-not (Test-Path $destDir)) { |
| 83 | + New-Item -ItemType Directory -Path $destDir -Force | Out-Null |
| 84 | + } |
| 85 | + Copy-Item $f.FullName $dest |
| 86 | +} |
| 87 | + |
| 88 | +# Attributions ride along in the zip root, same as the MSI installs them. |
| 89 | +Copy-Item (Join-Path $app "THIRD-PARTY-NOTICES.txt") $stage |
| 90 | +Copy-Item (Join-Path $app "third_party\wintun\wintun-license.txt") $stage |
| 91 | + |
| 92 | +# --- README ------------------------------------------------------------------- |
| 93 | +# One page, ASCII, and honest -- especially about what SHA256SUMS does and |
| 94 | +# does not protect against, and about how to un-break the network by hand. |
| 95 | +$readme = @" |
| 96 | +URnetwork (beta) -- portable build |
| 97 | +version $Version, windows $arch |
| 98 | +================================== |
| 99 | +
|
| 100 | +What this is |
| 101 | +------------ |
| 102 | +The URnetwork app and its VPN service as a plain folder. There is no |
| 103 | +installer: keep this folder anywhere you can write to and run it from |
| 104 | +there. "Portable" means no installer -- your data does not travel with |
| 105 | +the folder. App data lives in %LOCALAPPDATA%\URnetwork and service data |
| 106 | +in %ProgramData%\URnetwork. |
| 107 | +
|
| 108 | +First run |
| 109 | +--------- |
| 110 | +Open URnetwork.exe. The app offers to set up the VPN service -- one |
| 111 | +click, one administrator prompt. The service (urnetworkd) is what moves |
| 112 | +the traffic; the app is the window onto it. Then log in and connect. |
| 113 | +
|
| 114 | +Updating |
| 115 | +-------- |
| 116 | +The app checks this project's releases and offers updates. Applying one |
| 117 | +downloads the new zip, verifies it against the release's SHA256SUMS, and |
| 118 | +swaps the files here in place; the app then shows "Update the VPN |
| 119 | +service" -- one click restarts the service onto the new files. You can |
| 120 | +also unzip a newer release over this folder yourself (quit the app |
| 121 | +first). Note: SHA256SUMS protects the download against corruption, not |
| 122 | +against a compromised download source. Signed installers come in a later |
| 123 | +milestone. |
| 124 | +
|
| 125 | +Uninstalling |
| 126 | +------------ |
| 127 | +In the app: Settings -> Uninstall VPN service. Or from an elevated |
| 128 | +prompt: urnetworkd uninstall. Then delete this folder. If you want the |
| 129 | +data gone too: %LOCALAPPDATA%\URnetwork and %ProgramData%\URnetwork. |
| 130 | +
|
| 131 | +Recovery |
| 132 | +-------- |
| 133 | +If the internet ever breaks: stop urnetworkd, then elevated: urnetworkd |
| 134 | +revert. That removes the service's network filters and routes and puts |
| 135 | +the system back the way it was. |
| 136 | +"@ |
| 137 | +Set-Content -Path (Join-Path $stage "README.txt") -Value $readme -Encoding Ascii |
| 138 | + |
| 139 | +# --- zip ---------------------------------------------------------------------- |
| 140 | +$zipName = "URnetwork-v$Version-windows-$arch-portable.zip" |
| 141 | +$zipPath = Join-Path $app "build\$Platform\$zipName" |
| 142 | +if (Test-Path $zipPath) { Remove-Item $zipPath -Force } |
| 143 | + |
| 144 | +# Zip by hand instead of Compress-Archive: PowerShell 5.1's Compress-Archive |
| 145 | +# writes entry names with backslashes, which non-Windows tools extract as |
| 146 | +# literal files named "Assets\Fonts\x". Entry names here are always |
| 147 | +# forward-slashed, so the same zip is correct everywhere. |
| 148 | +Add-Type -AssemblyName System.IO.Compression |
| 149 | +Add-Type -AssemblyName System.IO.Compression.FileSystem |
| 150 | +$stageFull = (Get-Item $stage).FullName |
| 151 | +$entries = Get-ChildItem $stage -Recurse -File | Sort-Object FullName |
| 152 | +$zipStream = [System.IO.File]::Open($zipPath, [System.IO.FileMode]::CreateNew) |
| 153 | +try { |
| 154 | + $archive = New-Object System.IO.Compression.ZipArchive($zipStream, [System.IO.Compression.ZipArchiveMode]::Create) |
| 155 | + try { |
| 156 | + foreach ($f in $entries) { |
| 157 | + $rel = $f.FullName.Substring($stageFull.Length + 1).Replace("\", "/") |
| 158 | + [void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile( |
| 159 | + $archive, $f.FullName, $rel, [System.IO.Compression.CompressionLevel]::Optimal) |
| 160 | + } |
| 161 | + } finally { |
| 162 | + $archive.Dispose() |
| 163 | + } |
| 164 | +} finally { |
| 165 | + $zipStream.Dispose() |
| 166 | +} |
| 167 | + |
| 168 | +$mb = [math]::Round((Get-Item $zipPath).Length / 1MB, 1) |
| 169 | +Write-Host "packaged $($entries.Count) files -> $zipPath ($mb MB)" -ForegroundColor Green |
0 commit comments