#!/usr/bin/env pwsh <# .SYNOPSIS Build and test the Windows port with SQLite wired up (docs/WINDOWS_PORT.md §14.2). .DESCRIPTION Windows has no system SQLite, and GRDB's `GRDBSQLite` is a .systemLibrary whose only declared provider is apt — so `sqlite3.h` resolves nowhere and NucleicCore can't build. A .systemLibrary is resolved through C header/library search paths rather than the SwiftPM module graph, so nothing in Package.swift can fix this: SQLite has to exist on disk and be discoverable. This script installs it once via vcpkg and points the compiler and linker at it. Two mechanisms, deliberately chosen: CPATH — additive. clang appends it to the header search path without disturbing MSVC/SDK auto-detection, so plain `swift build` works afterwards with no flags at all. -Xlinker -LIBPATH: — passed per-invocation rather than via %LIB%. Setting %LIB% would be actively harmful: clang skips emitting its own -libpath: for the VC, UCRT *and* Windows SDK directories the moment %LIB% is set (clang/lib/Driver/ToolChains/MSVC.cpp, `if (!GetEnv("LIB") || ...)`), so a fresh %LIB% holding only SQLite silently strips the entire platform link path. .PARAMETER Target protocol — the wire layer only (the required CI leg; needs no SQLite). core — NucleicCore, the full Windows manifest. Builds green as of the §12 item-9 shims; a failure here IS a regression. With -Test it instead runs NucleicCarbonTests, which builds every declared target (nucleic-hostd, NucleicProtocolC, nucleic-smoke) — those are §12 items 10+ and may still fail; that output is what's left of the M0 gap list. broker — the C# nucleic-brokerd contract tests (no Swift involved). all — all three, continuing past a failing leg. .PARAMETER Test Run the leg's tests instead of just building it. .PARAMETER VcpkgRoot An existing vcpkg checkout to use. Otherwise the script looks at %VCPKG_INSTALLATION_ROOT%, the %LOCALAPPDATA%\vcpkg\vcpkg.path.txt breadcrumb, %LOCALAPPDATA%\Programs\vcpkg, %USERPROFILE%\vcpkg and C:\vcpkg, and clones one into %LOCALAPPDATA%\Programs\vcpkg if none of those hold a vcpkg.exe. .PARAMETER Persist Also save CPATH to your *user* environment, so future shells and editors resolve sqlite3.h without going through this script. .EXAMPLE ./windows/build.ps1 -Target protocol -Test ./windows/build.ps1 -Target core -Persist #> [CmdletBinding()] param( [ValidateSet('protocol', 'core', 'broker', 'all')] [string]$Target = 'protocol', [switch]$Test, [switch]$Persist, [string]$VcpkgRoot, [string]$Triplet ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } function Write-Note($msg) { Write-Host " $msg" -ForegroundColor DarkGray } # ---------------------------------------------------------------- SQLite via vcpkg function Initialize-SQLite { if (-not $Triplet) { # -static-md links SQLite statically against the DYNAMIC CRT — matching what the # Swift toolchain uses, so the CRT agrees and there's no sqlite3.dll to place # beside every test binary. $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' } $script:Triplet = "$arch-windows-static-md" } # `%LOCALAPPDATA%\vcpkg` is NOT a candidate install root, and must never be a clone target: # that path is vcpkg's own per-user DATA directory (registries, downloads, and the # `vcpkg.path.txt` breadcrumb naming the real root), so it exists — non-empty, no `.git` — # on any machine where vcpkg has merely *run* once. Cloning into it fails with "destination # path already exists and is not an empty directory". We only ever read it, for that # breadcrumb; a clone of our own goes to the conventional per-user install location. $breadcrumb = Join-Path $env:LOCALAPPDATA 'vcpkg\vcpkg.path.txt' $known = @( $VcpkgRoot $env:VCPKG_INSTALLATION_ROOT $(if (Test-Path $breadcrumb) { (Get-Content $breadcrumb -Raw).Trim() }) (Join-Path $env:LOCALAPPDATA 'Programs\vcpkg') (Join-Path $env:USERPROFILE 'vcpkg') 'C:\vcpkg' ) $root = $known | Where-Object { $_ -and (Test-Path (Join-Path $_ 'vcpkg.exe')) } | Select-Object -First 1 if (-not $root) { # Nothing usable found. Clone into the per-user install location — unless the caller # named a root, in which case honour it (they may be pointing at a fresh directory). $root = if ($VcpkgRoot) { $VcpkgRoot } else { Join-Path $env:LOCALAPPDATA 'Programs\vcpkg' } if (-not (Test-Path (Join-Path $root '.git'))) { # `git clone` refuses a non-empty destination, and the resulting error says nothing # about what to do — so diagnose it here, where the remedy is known. if ((Test-Path $root) -and (Get-ChildItem $root -Force | Select-Object -First 1)) { throw ("$root exists, is not empty, and holds neither vcpkg.exe nor a git " + "checkout. Point the script at your vcpkg with -VcpkgRoot , or " + "remove that directory.") } Write-Step "Cloning vcpkg to $root (one time)" git clone --depth 1 https://github.com/microsoft/vcpkg $root | Out-Host if ($LASTEXITCODE -ne 0) { throw "vcpkg clone failed" } } Write-Step 'Bootstrapping vcpkg (one time)' & (Join-Path $root 'bootstrap-vcpkg.bat') -disableMetrics | Out-Host if ($LASTEXITCODE -ne 0) { throw "vcpkg bootstrap failed" } } Write-Note "vcpkg root: $root" $prefix = Join-Path $root "installed\$Triplet" if (-not (Test-Path (Join-Path $prefix 'include\sqlite3.h'))) { Write-Step "Installing sqlite3:$Triplet (one time, a few minutes)" & (Join-Path $root 'vcpkg.exe') install "sqlite3:$Triplet" | Out-Host if ($LASTEXITCODE -ne 0) { throw "vcpkg install sqlite3:$Triplet failed" } } $inc = Join-Path $prefix 'include' $lib = Join-Path $prefix 'lib' if (-not (Test-Path (Join-Path $inc 'sqlite3.h'))) { throw "sqlite3.h missing under $inc" } if (-not (Test-Path (Join-Path $lib 'sqlite3.lib'))) { throw "sqlite3.lib missing under $lib" } # Additive: clang merges CPATH into the header search path and still auto-detects the # MSVC + Windows SDK headers. Contrast %INCLUDE%, which SUPPRESSES that detection. if ($env:CPATH -and $env:CPATH -notlike "*$inc*") { $env:CPATH = "$inc;$env:CPATH" } elseif (-not $env:CPATH) { $env:CPATH = $inc } if ($Persist) { $userCPath = [Environment]::GetEnvironmentVariable('CPATH', 'User') if (-not $userCPath) { $userCPath = $inc } elseif ($userCPath -notlike "*$inc*") { $userCPath = "$inc;$userCPath" } [Environment]::SetEnvironmentVariable('CPATH', $userCPath, 'User') Write-Note "Persisted CPATH for your user account -> $inc" Write-Note 'New shells can now `swift build` NucleicCore with no flags.' } Write-Note "sqlite3 ($Triplet): $prefix" return @{ Include = $inc; Lib = $lib } } # ---------------------------------------------------------------- legs function Invoke-Swift([string[]]$SwiftArgs, [hashtable]$Sql) { # Both mechanisms, deliberately redundant. CPATH (set by Initialize-SQLite) is what makes # a *bare* `swift build` work later, but passing -Xcc -I here means this script never # depends on clang honouring CPATH — if it didn't, the failure would be an inscrutable # "'sqlite3.h' file not found" from inside GRDB's module build. -Xlinker only matters # when something actually links (tests, executables); a library build needs just the -I. # # TEMPORARY — /FORCE:MULTIPLE (docs/WINDOWS_PORT.md §14.1.1). swift-crypto's # CCryptoBoringSSL and swift-nio-ssl's CNIOBoringSSL both land in every Windows link, and # `p_thread_callback_boringssl` (crypto/thread_win.cc) collides between them: BoringSSL's # prefix maps are generated by building on a POSIX host, so symbols from the Windows-only # translation units are never prefixed. The same pair links clean on Linux, where the # equivalent file is thread_pthread.c and IS prefixed. Drop this flag the moment one copy # carries the prefix — it is global, so it also downgrades any FUTURE duplicate to a # warning. Read the warnings. $full = @($SwiftArgs) + @( '-Xcc', "-I$($Sql.Include)", '-Xlinker', "-LIBPATH:$($Sql.Lib)", '-Xlinker', '/FORCE:MULTIPLE') Write-Note "swift $($full -join ' ')" # Out-Host, not the pipeline: a native command's stdout would otherwise become this # function's return value, and the caller's exit-code check would see an array. & swift @full | Out-Host return $LASTEXITCODE } function Build-Protocol([hashtable]$sql) { # Narrows the manifest to the wire layer. Read at MANIFEST EVALUATION time, so it has # to be in the environment before swift starts, not exported inside the build. $env:NUCLEIC_WINDOWS_PROTOCOL_ONLY = '1' try { Write-Step 'protocol: building NucleicProtocol' $code = if ($Test) { Invoke-Swift @('test', '--filter', 'NucleicProtocolTests') $sql } else { Invoke-Swift @('build', '--target', 'NucleicProtocol') $sql } } finally { Remove-Item Env:\NUCLEIC_WINDOWS_PROTOCOL_ONLY -ErrorAction SilentlyContinue } return $code } function Build-Core([hashtable]$sql) { Write-Step 'core: building NucleicCore (full Windows manifest)' if ($Test) { Write-Note '-Test builds EVERY target (hostd, NucleicProtocolC, smoke) — §12 items 10+ may still fail.' } # NucleicCore only EXISTS when NUCLEIC_WINDOWS_PROTOCOL_ONLY is absent: with it set, # Package.swift narrows the manifest to [NucleicProtocol, NucleicProtocolTests] and this # fails with "no target named 'NucleicCore'". CI never sets it for this job, but a local # shell that ran the protocol leg by hand keeps it — so clear it just for this build and # hand it back afterwards. $inherited = $env:NUCLEIC_WINDOWS_PROTOCOL_ONLY if ($inherited) { Write-Note 'Ignoring inherited NUCLEIC_WINDOWS_PROTOCOL_ONLY — core needs the full manifest.' Remove-Item Env:\NUCLEIC_WINDOWS_PROTOCOL_ONLY } try { if ($Test) { return Invoke-Swift @('test', '--filter', 'NucleicCarbonTests') $sql } return Invoke-Swift @('build', '--target', 'NucleicCore') $sql } finally { if ($inherited) { $env:NUCLEIC_WINDOWS_PROTOCOL_ONLY = $inherited } } } function Build-Broker { Write-Step 'broker: dotnet test windows\Nucleic.sln' & dotnet test (Join-Path $repo 'windows\Nucleic.sln') --nologo | Out-Host return $LASTEXITCODE } # ---------------------------------------------------------------- main Push-Location $repo try { $results = [ordered]@{} if ($Target -in @('protocol', 'core', 'all')) { $sql = Initialize-SQLite } switch ($Target) { 'protocol' { $results['protocol'] = Build-Protocol $sql } 'core' { $results['core'] = Build-Core $sql } 'broker' { $results['broker'] = Build-Broker } 'all' { $results['protocol'] = Build-Protocol $sql $results['core'] = Build-Core $sql $results['broker'] = Build-Broker } } Write-Host '' Write-Step 'Summary' foreach ($k in $results.Keys) { $ok = $results[$k] -eq 0 # `core -Test` still reaches unported targets (§12 items 10+), so only that # combination is reported as an expected gap rather than a red build. $gap = ($k -eq 'core') -and $Test $label = if ($ok) { 'PASS' } elseif ($gap) { 'FAIL (expected — items 10+ gap list)' } else { 'FAIL' } $color = if ($ok) { 'Green' } elseif ($gap) { 'Yellow' } else { 'Red' } Write-Host (" {0,-9} {1}" -f $k, $label) -ForegroundColor $color } $hard = @($results.Keys | Where-Object { $results[$_] -ne 0 -and -not (($_ -eq 'core') -and $Test) }) if ($hard.Count -gt 0) { exit 1 } exit 0 } finally { Pop-Location }