# Build-machine only: download once, then stage a self-contained Windows x64 app. [CmdletBinding()] param( [string]$Destination, [switch]$StageOnly, [string]$Arch = 'amd64' ) $ErrorActionPreference = 'Stop' # pwsh callers can pass PSModulePath without Windows PowerShell's bundled modules. $env:PSModulePath = "$PSHOME\Modules;" + $env:PSModulePath if ($Arch -ne 'amd64') { throw 'Windows runtime currently supports amd64 only; do not ship an ARM64 executable with x64 tools.' } $root = Split-Path $PSScriptRoot -Parent $thirdParty = Join-Path $root 'third_party' $cache = Join-Path $root '.task/runtime-downloads' New-Item -ItemType Directory -Force $cache | Out-Null function Download([string]$Url, [string]$Name, [string]$Hash = '') { $target = Join-Path $cache $Name if (!(Test-Path -LiteralPath $target)) { Invoke-WebRequest -Uri $Url -OutFile "$target.tmp" -UseBasicParsing Move-Item -LiteralPath "$target.tmp" -Destination $target -Force } if ($Hash -and (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash -ne $Hash) { throw "Checksum mismatch: $target. Remove this cached file and retry." } return $target } function Copy-RuntimeFile([string]$Source, [string]$Target) { if ((Test-Path -LiteralPath $Target) -and (Get-FileHash -LiteralPath $Source).Hash -eq (Get-FileHash -LiteralPath $Target).Hash) { return } New-Item -ItemType Directory -Force (Split-Path $Target -Parent) | Out-Null Copy-Item -LiteralPath $Source -Destination $Target -Force } if (!$StageOnly) { & npm.cmd --prefix (Join-Path $root 'runtime/filesystem') run stage if ($LASTEXITCODE -ne 0) { throw 'Cannot prepare bundled Filesystem MCP; build machine needs Node >= 24 and npm.' } & npm.cmd --prefix (Join-Path $root 'runtime/mobile') run stage if ($LASTEXITCODE -ne 0) { throw 'Cannot prepare bundled Mobile Next MCP.' } $zip = Download 'https://github.com/Genymobile/scrcpy/releases/download/v5.0.1/scrcpy-win64-v5.0.1.zip' 'scrcpy-win64-v5.0.1.zip' 'b12a2c4ee8be317422451fc7dcf8ee20a71b5ea7ef9ad73ddd825a25316227a5' $scrcpy = Join-Path $cache 'scrcpy-win64-v5.0.1' Expand-Archive -LiteralPath $zip -DestinationPath $cache -Force New-Item -ItemType Directory -Force (Join-Path $thirdParty 'platform-tools'), (Join-Path $thirdParty 'scrcpy') | Out-Null foreach ($name in @('adb.exe', 'AdbWinApi.dll', 'AdbWinUsbApi.dll')) { Copy-RuntimeFile (Join-Path $scrcpy $name) (Join-Path $thirdParty "platform-tools/$name") } Copy-Item -LiteralPath (Join-Path $scrcpy 'scrcpy-server') -Destination (Join-Path $thirdParty 'scrcpy/scrcpy-server-v5.0.1') -Force $license = Download 'https://raw.githubusercontent.com/Genymobile/scrcpy/v5.0.1/LICENSE' 'scrcpy-LICENSE.txt' Copy-Item -LiteralPath $license -Destination (Join-Path $thirdParty 'scrcpy/LICENSE.txt') -Force $zip = Download 'https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1.2-essentials_build.zip' 'ffmpeg-8.1.2-essentials_build.zip' 'db580001caa24ac104c8cb856cd113a87b0a443f7bdf47d8c12b1d740584a2ec' Expand-Archive -LiteralPath $zip -DestinationPath $cache -Force $ffmpeg = Join-Path $cache 'ffmpeg-8.1.2-essentials_build' New-Item -ItemType Directory -Force (Join-Path $thirdParty 'ffmpeg') | Out-Null Copy-Item -LiteralPath (Join-Path $ffmpeg 'bin/ffmpeg.exe') -Destination (Join-Path $thirdParty 'ffmpeg') -Force Copy-Item -LiteralPath (Join-Path $ffmpeg 'LICENSE') -Destination (Join-Path $thirdParty 'ffmpeg') -Force Copy-Item -LiteralPath (Join-Path $ffmpeg 'README.txt') -Destination (Join-Path $thirdParty 'ffmpeg') -Force # Fixed Version runs privately beside our executable; no runtime installer or network on clients. if (!(Test-Path (Join-Path $thirdParty 'webview2/msedgewebview2.exe'))) { $cab = Download 'https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/b7919cc9-680d-4253-bf32-732988e68aa6/Microsoft.WebView2.FixedVersionRuntime.156.0.4314.58.x64.cab' 'webview2-156.0.4314.58-x64.cab' '857dcfd6f7035508a29645fa2978c3a6a841f220df77cc366d318894e19f4792' $expanded = Join-Path $cache 'webview2-expanded' New-Item -ItemType Directory -Force $expanded | Out-Null & expand.exe $cab '-F:*' $expanded | Out-Null if ($LASTEXITCODE -ne 0) { throw 'Cannot expand WebView2 Fixed Version CAB' } $browser = @(Get-ChildItem -LiteralPath $expanded -Filter msedgewebview2.exe -Recurse) if ($browser.Count -ne 1) { throw 'Invalid WebView2 CAB contents' } New-Item -ItemType Directory -Force (Join-Path $thirdParty 'webview2') | Out-Null Copy-Item -Path (Join-Path $browser[0].DirectoryName '*') -Destination (Join-Path $thirdParty 'webview2') -Recurse -Force } $manifest = @{ platform = 'windows/amd64'; scrcpy = '5.0.1'; ffmpeg = '8.1.2'; webview2 = '156.0.4314.58' } $manifest | ConvertTo-Json | Set-Content (Join-Path $thirdParty 'runtime-manifest.json') -Encoding UTF8 } $required = @('platform-tools/adb.exe', 'platform-tools/AdbWinApi.dll', 'platform-tools/AdbWinUsbApi.dll', 'scrcpy/scrcpy-server-v5.0.1', 'scrcpy/LICENSE.txt', 'ffmpeg/ffmpeg.exe', 'ffmpeg/LICENSE', 'webview2/msedgewebview2.exe', 'runtime-manifest.json', 'mcp-filesystem/node.exe', 'mcp-filesystem/NODE-LICENSE.txt', 'mcp-filesystem/runtime.json', 'mcp-filesystem/node_modules/@modelcontextprotocol/server-filesystem/dist/index.js', 'mcp-mobile/runtime.json', 'mcp-mobile/node_modules/@mobilenext/mobile-mcp/lib/index.js') foreach ($file in $required) { if (!(Test-Path -LiteralPath (Join-Path $thirdParty $file))) { throw "Missing bundled runtime: $file. Run scripts/sync-windows-runtime.ps1 on the build machine." } } $manifest = Get-Content (Join-Path $thirdParty 'runtime-manifest.json') -Raw | ConvertFrom-Json if ($manifest.platform -ne 'windows/amd64') { throw 'Wrong platform runtime; resync Windows dependencies.' } $nodeIdentity = & (Join-Path $thirdParty 'mcp-filesystem/node.exe') -p 'JSON.stringify({platform:process.platform,arch:process.arch,major:parseInt(process.versions.node)})' | ConvertFrom-Json if ($LASTEXITCODE -ne 0 -or $nodeIdentity.platform -ne 'win32' -or $nodeIdentity.arch -ne 'x64' -or $nodeIdentity.major -lt 24) { throw 'Wrong private Node runtime; restage Filesystem MCP on Windows x64.' } if ((Get-FileHash (Join-Path $thirdParty 'scrcpy/scrcpy-server-v5.0.1')).Hash -ne '764eb6f79811d5211fe9df341120882ba9994c7a61b897d7bf3fb662e53bc536') { throw 'scrcpy server version/checksum mismatch' } if ($Destination) { New-Item -ItemType Directory -Force $Destination | Out-Null foreach ($folder in @('platform-tools', 'scrcpy', 'ffmpeg', 'webview2', 'mcp-filesystem', 'mcp-mobile')) { $sourceDir = Join-Path $thirdParty $folder foreach ($file in Get-ChildItem -LiteralPath $sourceDir -File -Recurse) { $relative = $file.FullName.Substring($sourceDir.Length).TrimStart('\') Copy-RuntimeFile $file.FullName (Join-Path (Join-Path $Destination $folder) $relative) } } Copy-Item -LiteralPath (Join-Path $thirdParty 'runtime-manifest.json') -Destination $Destination -Force Copy-Item -LiteralPath (Join-Path $thirdParty 'RUNTIME-NOTICES.txt') -Destination $Destination -Force } Write-Host 'Windows runtime verified (ADB + scrcpy server + FFmpeg + private WebView2 + Filesystem MCP).'