Skip to content

Commit 1216aa9

Browse files
Ryanmello07claude
andcommitted
e: the portable packager -- the zip a release actually ships
The zip is the beta artifact and the thing the in-app updater swaps in place, so its shape is a contract: payload at the zip ROOT (no wrapper folder), the URnetwork-v<version>-windows-<arch>-portable.zip name grammar, and forward-slashed entry names (PS 5.1 Compress-Archive writes backslashes, which extract as literal backslash-named files anywhere that is not Windows -- so the entries are written by hand). Stages the whole build output minus debug junk rather than an allowlist, because an allowlist already dropped Assets\Fonts once; adds the notices, the wintun license, and a one-page README with the recovery line. Runs identically from a dev box and from CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PT7KcWCPKfFwQUc7SM3oZY
1 parent a0df9d0 commit 1216aa9

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

app/tools/package-portable.ps1

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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

Comments
 (0)