also looking at this
feat: add Linux and Windows named-window routing
#325
29 files
+2250
-200
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -92,7 +92,38 @@ jobs: | ||
| 92 | 92 | |
| 93 | 93 | - name: Smoke Linux multi-window bridge |
| 94 | 94 | if: runner.os == 'Linux' |
| 95 | run: bun scripts/verify-linux-multi-window.ts packages/zig/zig-out/bin/craft | |
| 95 | run: bun scripts/verify-desktop-multi-window.ts packages/zig/zig-out/bin/craft | |
| 96 | ||
| 97 | windows-multi-window: | |
| 98 | runs-on: windows-2025 | |
| 99 | timeout-minutes: 20 | |
| 100 | ||
| 101 | steps: | |
| 102 | - uses: actions/checkout@v6 | |
| 103 | ||
| 104 | - name: Setup Pantry (provides Bun and Zig) | |
| 105 | uses: pantry-pm/pantry/packages/action@235036fa0f48bae99b2293df5a3dc35c809b1777 # pinned: last SHA whose bundled typescript resolves on linux-x64 | |
| 106 | ||
| 107 | - name: First-party Zig dependencies | |
| 108 | uses: ./.github/actions/first-party-zig-deps | |
| 109 | ||
| 110 | - name: Build Windows CLI | |
| 111 | run: bun run build:core | |
| 112 | ||
| 113 | - name: Stage pinned WebView2 loader | |
| 114 | shell: pwsh | |
| 115 | run: | | |
| 116 | $version = '1.0.4191.47' | |
| 117 | $expected = 'f492bbf547d0da329553b6727435b677579b1e9f91cc9e4a1ad029366d5f23d0' | |
| 118 | $archive = Join-Path $env:RUNNER_TEMP 'microsoft.web.webview2.zip' | |
| 119 | $package = Join-Path $env:RUNNER_TEMP 'microsoft.web.webview2' | |
| 120 | Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/$version/microsoft.web.webview2.$version.nupkg" -OutFile $archive | |
| 121 | if ((Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $expected) { throw 'WebView2 NuGet package digest mismatch' } | |
| 122 | Expand-Archive -LiteralPath $archive -DestinationPath $package | |
| 123 | Copy-Item (Join-Path $package 'build/native/x64/WebView2Loader.dll') 'packages/zig/zig-out/bin/WebView2Loader.dll' | |
| 124 | ||
| 125 | - name: Smoke Windows multi-window bridge | |
| 126 | run: bun scripts/verify-desktop-multi-window.ts packages/zig/zig-out/bin/craft.exe | |
| 96 | 127 | |
| 97 | 128 | typescript-sdk: |
| 98 | 129 | runs-on: ubuntu-latest |
| @@ -390,7 +421,7 @@ jobs: | ||
| 390 | 421 | run: bun run verify |
| 391 | 422 | |
| 392 | 423 | publish-commit: |
| 393 | needs: [zig-core, typescript-sdk, ios-builder, ios-native-templates, android-builder, create-craft, ts-maps, lint, integration] | |
| 424 | needs: [zig-core, windows-multi-window, typescript-sdk, ios-builder, ios-native-templates, android-builder, create-craft, ts-maps, lint, integration] | |
| 394 | 425 | runs-on: ubuntu-latest |
| 395 | 426 | |
| 396 | 427 | steps: |
| @@ -179,6 +179,23 @@ jobs: | ||
| 179 | 179 | rm -rf zig-out/bin |
| 180 | 180 | zig build -Doptimize=ReleaseSafe -Dversion="$VERSION" |
| 181 | 181 | |
| 182 | # The Windows binary dynamically loads WebView2Loader.dll. The runtime | |
| 183 | # alone does not provide the app-local loader, so stage the x64 DLL beside | |
| 184 | # craft.exe before Pantry zips zig-out/cross/windows-x64/. Pin and verify | |
| 185 | # the official NuGet package instead of silently shipping a moving DLL. | |
| 186 | - name: Stage Windows WebView2 loader | |
| 187 | if: runner.os == 'Linux' | |
| 188 | working-directory: packages/zig | |
| 189 | run: | | |
| 190 | set -euo pipefail | |
| 191 | VERSION=1.0.4191.47 | |
| 192 | EXPECTED=f492bbf547d0da329553b6727435b677579b1e9f91cc9e4a1ad029366d5f23d0 | |
| 193 | ARCHIVE="$RUNNER_TEMP/microsoft.web.webview2.$VERSION.nupkg" | |
| 194 | curl -fsSL --retry 3 "https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/$VERSION/microsoft.web.webview2.$VERSION.nupkg" -o "$ARCHIVE" | |
| 195 | echo "$EXPECTED $ARCHIVE" | sha256sum -c - | |
| 196 | unzip -p "$ARCHIVE" build/native/x64/WebView2Loader.dll > zig-out/cross/windows-x64/WebView2Loader.dll | |
| 197 | test -s zig-out/cross/windows-x64/WebView2Loader.dll | |
| 198 | ||
| 182 | 199 | # ── macOS Code Signing & Notarization ────────────────────────── |
| 183 | 200 | # The signing credentials live encrypted in `.env.production`, not as |
| 184 | 201 | # seven repository secrets. |
| @@ -10,7 +10,7 @@ whether it stores a window-specific target. | ||
| 10 | 10 | |
| 11 | 11 | | Area | macOS | Linux | Windows | |
| 12 | 12 | | --- | --- | --- | --- | |
| 13 | | Runtime creation from `createWindow()` | Implemented | Implemented; GUI smoke in CI | Implemented; cross-build in CI | | |
| 13 | | Runtime creation from `createWindow()` | Implemented | Implemented; GUI smoke in CI | Implemented; WebView2 GUI smoke in CI | | |
| 14 | 14 | | Stable named handles | Implemented | Implemented | Implemented | |
| 15 | 15 | | Sender-authenticated local actions | Implemented | Implemented through WebKitGTK | Implemented through WebView2 | |
| 16 | 16 | | Named cross-window actions | Implemented | Implemented for core controls | Implemented for core controls | |
| @@ -18,9 +18,9 @@ whether it stores a window-specific target. | ||
| 18 | 18 | | Permanent destroy and cleanup | Implemented for named windows | Implemented on close/destroy | Implemented on close/destroy | |
| 19 | 19 | | Modal and parent relationships | Unspecified | Unspecified | Unspecified | |
| 20 | 20 | |
| 21 | Issue #67 remains open while the portable runtime smoke and the still-unspecified | |
| 22 | modal/parent semantics are reviewed. A green cross-build alone does not prove | |
| 23 | that WebView2 opens and tears down a real window on Windows. | |
| 21 | Issue #67 remains open while the Windows runtime smoke and the still-unspecified | |
| 22 | modal/parent semantics are reviewed. The Windows release archive now stages an | |
| 23 | app-local WebView2 loader; users still need the Microsoft WebView2 Runtime. | |
| 24 | 24 | |
| 25 | 25 | Linux and Windows keep their live native window/webview pairs in |
| 26 | 26 | `desktop_window_registry.zig`. Page messages are authenticated by the sending |
| @@ -1,10 +1,13 @@ | ||
| 1 | /** Exercise the shipped Linux binary's page-to-native multi-window bridge. */ | |
| 1 | /** Exercise the shipped Linux or Windows binary's page-to-native multi-window bridge. */ | |
| 2 | 2 | import { spawn } from 'node:child_process' |
| 3 | 3 | import { createServer } from 'node:http' |
| 4 | 4 | import type { AddressInfo } from 'node:net' |
| 5 | import { platform } from 'node:os' | |
| 5 | 6 | |
| 6 | 7 | const binary = process.argv[2] |
| 7 | if (!binary) throw new Error('usage: bun scripts/verify-linux-multi-window.ts <craft-binary>') | |
| 8 | if (!binary) throw new Error('usage: bun scripts/verify-desktop-multi-window.ts <craft-binary>') | |
| 9 | const isWindows = platform() === 'win32' | |
| 10 | const platformName = isWindows ? 'Windows' : 'Linux' | |
| 8 | 11 | |
| 9 | 12 | const mainPage = `<!doctype html><script> |
| 10 | 13 | (async () => { |
| @@ -130,8 +133,10 @@ await new Promise<void>((resolve, reject) => { | ||
| 130 | 133 | server.listen(0, '127.0.0.1', resolve) |
| 131 | 134 | }) |
| 132 | 135 | const port = (server.address() as AddressInfo).port |
| 133 | const child = spawn('timeout', ['110s', 'xvfb-run', '-a', binary, '--url', `http://127.0.0.1:${port}/main`], { | |
| 134 | detached: true, | |
| 136 | const child = spawn(isWindows ? binary : 'timeout', isWindows | |
| 137 | ? ['--url', `http://127.0.0.1:${port}/main`] | |
| 138 | : ['110s', 'xvfb-run', '-a', binary, '--url', `http://127.0.0.1:${port}/main`], { | |
| 139 | detached: !isWindows, | |
| 135 | 140 | stdio: ['ignore', 'pipe', 'pipe'], |
| 136 | 141 | }) |
| 137 | 142 | let output = '' |
| @@ -140,11 +145,11 @@ for (const stream of [child.stdout, child.stderr]) { | ||
| 140 | 145 | } |
| 141 | 146 | child.once('error', error => rejectDone(error)) |
| 142 | 147 | child.once('exit', (code, signal) => rejectDone(new Error(`Craft exited before smoke completed (${code ?? signal})`))) |
| 143 | const timer = setTimeout(() => rejectDone(new Error('Linux multi-window smoke timed out')), 100_000) | |
| 148 | const timer = setTimeout(() => rejectDone(new Error(`${platformName} multi-window smoke timed out`)), 100_000) | |
| 144 | 149 | |
| 145 | 150 | try { |
| 146 | 151 | await done |
| 147 | console.log(`Linux multi-window smoke passed: ${[...steps].join(', ')}`) | |
| 152 | console.log(`${platformName} multi-window smoke passed: ${[...steps].join(', ')}`) | |
| 148 | 153 | } |
| 149 | 154 | catch (error) { |
| 150 | 155 | throw new Error(`${String(error)}\nHTTP requests: ${requests.join(', ')}\nCraft output:\n${output}`) |
| @@ -152,7 +157,10 @@ catch (error) { | ||
| 152 | 157 | finally { |
| 153 | 158 | clearTimeout(timer) |
| 154 | 159 | if (child.pid) { |
| 155 | try { process.kill(-child.pid, 'SIGTERM') } | |
| 160 | try { | |
| 161 | if (isWindows) child.kill() | |
| 162 | else process.kill(-child.pid, 'SIGTERM') | |
| 163 | } | |
| 156 | 164 | catch { /* The process group may have already exited. */ } |
| 157 | 165 | } |
| 158 | 166 | await new Promise<void>(resolve => server.close(() => resolve())) |