2026-07-28 01:26:18 -07:00
|
|
|
#!/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.
|
2026-07-28 21:27:59 -07:00
|
|
|
-Xlinker /LIBPATH: — passed per-invocation rather than via %LIB%. Setting %LIB%
|
2026-07-28 01:26:18 -07:00
|
|
|
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).
|
2026-07-28 20:41:28 -07:00
|
|
|
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.
|
2026-07-28 01:26:18 -07:00
|
|
|
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.
|
|
|
|
|
|
2026-07-28 20:19:42 -07:00
|
|
|
.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.
|
|
|
|
|
|
2026-07-28 01:26:18 -07:00
|
|
|
.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"
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 20:19:42 -07:00
|
|
|
# `%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 <path>, 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" }
|
|
|
|
|
}
|
2026-07-28 01:26:18 -07:00
|
|
|
Write-Step 'Bootstrapping vcpkg (one time)'
|
|
|
|
|
& (Join-Path $root 'bootstrap-vcpkg.bat') -disableMetrics | Out-Host
|
|
|
|
|
if ($LASTEXITCODE -ne 0) { throw "vcpkg bootstrap failed" }
|
|
|
|
|
}
|
2026-07-28 20:19:42 -07:00
|
|
|
Write-Note "vcpkg root: $root"
|
2026-07-28 01:26:18 -07:00
|
|
|
|
|
|
|
|
$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
|
|
|
|
|
|
2026-07-28 03:04:00 -07:00
|
|
|
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.
|
2026-07-28 20:48:24 -07:00
|
|
|
#
|
|
|
|
|
# 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.
|
2026-07-28 03:04:00 -07:00
|
|
|
$full = @($SwiftArgs) + @(
|
|
|
|
|
'-Xcc', "-I$($Sql.Include)",
|
2026-07-28 21:27:59 -07:00
|
|
|
# `/LIBPATH:`, NOT `-LIBPATH:`. A leading `-L` is claimed by the driver's GNU-style
|
|
|
|
|
# library-path flag, so `-LIBPATH:C:\…\lib` is read as `-L` + `IBPATH:C:\…\lib` and
|
|
|
|
|
# re-emitted to the linker as `-libpath:IBPATH:C:\…\lib` — a directory that exists
|
|
|
|
|
# nowhere, and the only symptom is `lld-link: could not open 'sqlite3.lib'`.
|
|
|
|
|
'-Xlinker', "/LIBPATH:$($Sql.Lib)",
|
2026-07-28 20:48:24 -07:00
|
|
|
'-Xlinker', '/FORCE:MULTIPLE')
|
2026-07-28 01:26:18 -07:00
|
|
|
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) {
|
2026-07-28 03:04:00 -07:00
|
|
|
Invoke-Swift @('test', '--filter', 'NucleicProtocolTests') $sql
|
2026-07-28 01:26:18 -07:00
|
|
|
} else {
|
2026-07-28 03:04:00 -07:00
|
|
|
Invoke-Swift @('build', '--target', 'NucleicProtocol') $sql
|
2026-07-28 01:26:18 -07:00
|
|
|
}
|
|
|
|
|
} 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)'
|
2026-07-28 20:41:28 -07:00
|
|
|
if ($Test) {
|
|
|
|
|
Write-Note '-Test builds EVERY target (hostd, NucleicProtocolC, smoke) — §12 items 10+ may still fail.'
|
|
|
|
|
}
|
2026-07-28 03:15:52 -07:00
|
|
|
# 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 }
|
|
|
|
|
}
|
2026-07-28 01:26:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-28 20:41:28 -07:00
|
|
|
# `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' }
|
2026-07-28 01:26:18 -07:00
|
|
|
Write-Host (" {0,-9} {1}" -f $k, $label) -ForegroundColor $color
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 20:41:28 -07:00
|
|
|
$hard = @($results.Keys | Where-Object {
|
|
|
|
|
$results[$_] -ne 0 -and -not (($_ -eq 'core') -and $Test) })
|
2026-07-28 01:26:18 -07:00
|
|
|
if ($hard.Count -gt 0) { exit 1 }
|
|
|
|
|
exit 0
|
|
|
|
|
} finally {
|
|
|
|
|
Pop-Location
|
|
|
|
|
}
|