* fix(windows): ship yams-daemon and yams-mcp-server in the MSI The Windows release already builds both and meson install stages them next to yams.exe, but yams.wxs only packaged yams.exe, so daemon-backed commands and yams serve failed after an MSI install. Package both, and make build-msi.ps1 fail when a required executable is missing from the stage or a staged executable is not packaged. Signed-off-by: trvon <me@trevon.dev> * ci(release): fall back to the stable R2 and signing secrets for experimental The experimental channel needs EXPERIMENTAL_* Cloudflare and GPG secrets that were never created, so even a passing nightly could not publish and repo.yamsmemory.ai/experimental/* has never existed. Fall back to the stable token, account and key when the channel-specific secrets are unset; setting them later still takes precedence. Signed-off-by: trvon <me@trevon.dev> * ci: add a manual Windows workflow that finds hanging test cases search_service times out at 360 s on Windows in every Tests run, which keeps experimental from going green and blocks every nightly. The meson log only shows the timeout. This workflow builds once and runs each Catch2 case on its own with a per-case timeout, then reports durations and the cases that hang. It runs only on workflow_dispatch or a push to a diagnose/** branch. Signed-off-by: trvon <me@trevon.dev> --------- Signed-off-by: trvon <me@trevon.dev>
263 lines
10 KiB
PowerShell
263 lines
10 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Builds YAMS MSI installer using WiX Toolset v4+
|
|
|
|
.DESCRIPTION
|
|
This script builds an MSI installer for YAMS from the meson install output.
|
|
It requires WiX Toolset v4+ to be installed via: dotnet tool install --global wix
|
|
|
|
.PARAMETER StageDir
|
|
Path to the meson install --destdir output directory.
|
|
Default: .\build\release\stage
|
|
|
|
.PARAMETER Version
|
|
Product version string (e.g., 0.1.0 or nightly-20251126-abc1234).
|
|
Default: extracted from meson.build or "0.0.0"
|
|
|
|
.PARAMETER OutputDir
|
|
Directory where the MSI will be created.
|
|
Default: .\build\release
|
|
|
|
.EXAMPLE
|
|
.\build-msi.ps1 -StageDir .\build\release\stage -Version 0.1.0
|
|
#>
|
|
|
|
param(
|
|
[string]$StageDir = ".\build\release\stage",
|
|
[string]$Version = "",
|
|
[string]$OutputDir = ".\build\release"
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$RepoRoot = (Get-Item "$ScriptDir\..\..").FullName
|
|
|
|
# If StageDir doesn't exist, try to create it via meson install
|
|
if (-not (Test-Path $StageDir)) {
|
|
Write-Host "Stage directory not found: $StageDir" -ForegroundColor Yellow
|
|
|
|
# Try to find the build directory
|
|
$buildDir = Split-Path $StageDir -Parent
|
|
if (-not (Test-Path (Join-Path $buildDir "build.ninja"))) {
|
|
# Default to build/release
|
|
$buildDir = Join-Path $RepoRoot "build\release"
|
|
}
|
|
|
|
if (Test-Path (Join-Path $buildDir "build.ninja")) {
|
|
Write-Host "Found build at: $buildDir" -ForegroundColor Cyan
|
|
|
|
# Reconfigure with root prefix for clean MSI staging
|
|
Write-Host "Reconfiguring meson with root prefix for MSI staging..." -ForegroundColor Cyan
|
|
& meson configure $buildDir --prefix=/
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Warning "meson configure failed, continuing with current prefix..."
|
|
}
|
|
|
|
Write-Host "Running meson install to create stage directory..." -ForegroundColor Cyan
|
|
$stageParent = Split-Path $StageDir -Parent
|
|
if (-not (Test-Path $stageParent)) {
|
|
New-Item -ItemType Directory -Path $stageParent -Force | Out-Null
|
|
}
|
|
|
|
& meson install -C $buildDir --destdir $StageDir
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "meson install failed. Please build first: .\setup.ps1 -BuildType Release"
|
|
exit 1
|
|
}
|
|
} else {
|
|
Write-Error @"
|
|
Stage directory not found and no build available.
|
|
|
|
Please either:
|
|
1. Build first: .\setup.ps1 -BuildType Release
|
|
2. Then package: .\setup.ps1 -BuildType Release -Package
|
|
|
|
Or manually:
|
|
1. Build: meson compile -C build/release
|
|
2. Stage: meson install -C build/release --destdir build/release/stage
|
|
3. Package: .\packaging\windows\build-msi.ps1 -StageDir build/release/stage
|
|
"@
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Verify WiX is installed
|
|
Write-Host "Checking for WiX Toolset..." -ForegroundColor Cyan
|
|
$wixPath = Get-Command wix -ErrorAction SilentlyContinue
|
|
if (-not $wixPath) {
|
|
Write-Host "WiX Toolset not found. Installing via dotnet tool..." -ForegroundColor Yellow
|
|
& dotnet tool install --global wix --version 6.0.2
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "Failed to install WiX Toolset. Please install manually: dotnet tool install --global wix --version 6.0.2"
|
|
exit 1
|
|
}
|
|
# Refresh PATH
|
|
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User")
|
|
}
|
|
|
|
# Verify WiX version
|
|
Write-Host "WiX version:" -ForegroundColor Cyan
|
|
& wix --version
|
|
|
|
# Add WiX UI extension (pin to 6.0.2 to match WiX toolset version)
|
|
Write-Host "Adding WiX UI extension..." -ForegroundColor Cyan
|
|
& wix extension add -g WixToolset.UI.wixext/6.0.2
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Warning "WiX UI extension may already be installed, continuing..."
|
|
}
|
|
|
|
# Resolve stage directory
|
|
$StageDir = (Resolve-Path $StageDir -ErrorAction Stop).Path
|
|
Write-Host "Stage directory: $StageDir" -ForegroundColor Cyan
|
|
|
|
# Find yams.exe anywhere in the stage directory
|
|
# meson install with --destdir prepends destdir to prefix, creating nested paths
|
|
Write-Host "Searching for yams.exe in stage directory..." -ForegroundColor Cyan
|
|
$yamsExeFiles = Get-ChildItem -Path $StageDir -Recurse -Filter "yams.exe" -ErrorAction SilentlyContinue
|
|
|
|
if ($yamsExeFiles.Count -eq 0) {
|
|
Write-Error "yams.exe not found anywhere in stage directory: $StageDir"
|
|
exit 1
|
|
}
|
|
|
|
# Use the first found yams.exe and derive the bin directory
|
|
$yamsBin = $yamsExeFiles[0].FullName
|
|
$binDir = Split-Path $yamsBin -Parent
|
|
|
|
# The "effective stage root" is the parent of bin/
|
|
$EffectiveStageDir = Split-Path $binDir -Parent
|
|
|
|
Write-Host "Found yams.exe at: $yamsBin" -ForegroundColor Green
|
|
Write-Host "Effective stage root: $EffectiveStageDir" -ForegroundColor Cyan
|
|
|
|
# Use EffectiveStageDir for WiX (it contains bin/yams.exe)
|
|
$StageDir = $EffectiveStageDir
|
|
|
|
# Every executable the MSI ships must be staged. A missing daemon or MCP server would
|
|
# otherwise produce an installer where daemon-backed commands and `yams serve` fail.
|
|
$requiredExecutables = @("yams.exe", "yams-daemon.exe", "yams-mcp-server.exe")
|
|
foreach ($exe in $requiredExecutables) {
|
|
$exePath = Join-Path $StageDir "bin\$exe"
|
|
if (-not (Test-Path $exePath)) {
|
|
Write-Error "Required executable missing from stage: $exePath"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# The reverse check: any executable staged in bin\ must be listed in yams.wxs, so a new
|
|
# tool cannot ship in the stage but silently drop out of the installer.
|
|
$wxsPath = Join-Path $PSScriptRoot "yams.wxs"
|
|
$wxsText = Get-Content -Raw $wxsPath
|
|
# yams-cli.exe is the build name of the CLI; meson stages it with yams.exe as an alias, and
|
|
# the MSI ships that alias as yams.exe.
|
|
$packagedAs = @{ "yams-cli.exe" = "yams.exe" }
|
|
Get-ChildItem -Path (Join-Path $StageDir "bin") -Filter "*.exe" | ForEach-Object {
|
|
$name = if ($packagedAs.ContainsKey($_.Name)) { $packagedAs[$_.Name] } else { $_.Name }
|
|
if ($wxsText -notmatch [regex]::Escape("bin\$name")) {
|
|
Write-Error "Staged executable $($_.Name) is not packaged by yams.wxs"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
$tbbDll = Join-Path $StageDir "bin\tbb12.dll"
|
|
$includeTbb = if (Test-Path $tbbDll) { "true" } else { "false" }
|
|
if ($includeTbb -eq "true") {
|
|
Write-Host "Including TBB runtime DLL: $tbbDll" -ForegroundColor Cyan
|
|
} else {
|
|
Write-Host "TBB runtime DLL not staged; skipping TBB component." -ForegroundColor Yellow
|
|
}
|
|
|
|
# Create LICENSE.rtf for WiX installer (WiX requires RTF format)
|
|
$licenseSource = Join-Path $RepoRoot "LICENSE"
|
|
$licenseRtf = Join-Path $StageDir "LICENSE.rtf"
|
|
if (Test-Path $licenseSource) {
|
|
Write-Host "Converting LICENSE to RTF format..." -ForegroundColor Cyan
|
|
$licenseText = Get-Content $licenseSource -Raw
|
|
# Simple RTF wrapper - escape special chars and wrap in RTF
|
|
$licenseText = $licenseText -replace '\\', '\\\\' -replace '\{', '\{' -replace '\}', '\}'
|
|
$rtfContent = "{\rtf1\ansi\deff0{\fonttbl{\f0 Consolas;}}\f0\fs18 " + ($licenseText -replace "`r`n", "\par`r`n" -replace "`n", "\par`r`n") + "}"
|
|
$rtfContent | Out-File -FilePath $licenseRtf -Encoding ascii
|
|
Write-Host "License RTF created at: $licenseRtf" -ForegroundColor Cyan
|
|
} else {
|
|
Write-Warning "LICENSE file not found at: $licenseSource"
|
|
}
|
|
|
|
# Extract version if not provided
|
|
if ([string]::IsNullOrEmpty($Version)) {
|
|
$mesonBuild = Join-Path $RepoRoot "meson.build"
|
|
if (Test-Path $mesonBuild) {
|
|
$content = Get-Content $mesonBuild -Raw
|
|
if ($content -match "version\s*:\s*'([^']+)'") {
|
|
$Version = $matches[1]
|
|
Write-Host "Extracted version from meson.build: $Version" -ForegroundColor Cyan
|
|
}
|
|
}
|
|
if ([string]::IsNullOrEmpty($Version)) {
|
|
$Version = "0.0.0"
|
|
Write-Host "Using default version: $Version" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
# Normalize version for MSI (must be X.Y.Z.W format)
|
|
# Handle nightly-YYYYMMDD-hash format
|
|
if ($Version -match "nightly-(\d{4})(\d{2})(\d{2})-") {
|
|
$year = [int]$matches[1] - 2020 # Offset from 2020
|
|
$month = [int]$matches[2]
|
|
$day = [int]$matches[3]
|
|
$MsiVersion = "$year.$month.$day.0"
|
|
Write-Host "Normalized nightly version to MSI format: $MsiVersion" -ForegroundColor Cyan
|
|
} elseif ($Version -match "^(\d+)\.(\d+)\.(\d+)") {
|
|
$MsiVersion = "$($matches[1]).$($matches[2]).$($matches[3]).0"
|
|
} else {
|
|
$MsiVersion = "0.0.0.0"
|
|
Write-Host "Could not parse version, using: $MsiVersion" -ForegroundColor Yellow
|
|
}
|
|
|
|
# Create output directory
|
|
$OutputDir = (New-Item -ItemType Directory -Force -Path $OutputDir).FullName
|
|
$MsiName = "yams-$Version-windows-x86_64.msi"
|
|
$MsiPath = Join-Path $OutputDir $MsiName
|
|
|
|
Write-Host "`nBuilding MSI..." -ForegroundColor Cyan
|
|
Write-Host " Version: $Version (MSI: $MsiVersion)" -ForegroundColor Gray
|
|
Write-Host " Stage: $StageDir" -ForegroundColor Gray
|
|
Write-Host " Output: $MsiPath" -ForegroundColor Gray
|
|
|
|
# Build MSI using WiX
|
|
$wxsPath = Join-Path $ScriptDir "yams.wxs"
|
|
& wix build $wxsPath `
|
|
-o $MsiPath `
|
|
-d Version=$MsiVersion `
|
|
-d StageDir=$StageDir `
|
|
-d IncludeTbb=$includeTbb `
|
|
-ext WixToolset.UI.wixext
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "WiX build failed with exit code $LASTEXITCODE"
|
|
exit 1
|
|
}
|
|
|
|
# Verify MSI was created
|
|
if (Test-Path $MsiPath) {
|
|
$msiSize = (Get-Item $MsiPath).Length / 1MB
|
|
Write-Host "`nMSI created successfully!" -ForegroundColor Green
|
|
Write-Host " Path: $MsiPath" -ForegroundColor Gray
|
|
Write-Host " Size: $([math]::Round($msiSize, 2)) MB" -ForegroundColor Gray
|
|
|
|
# Generate SHA256 hash
|
|
$hash = (Get-FileHash -Path $MsiPath -Algorithm SHA256).Hash.ToLower()
|
|
Write-Host " SHA256: $hash" -ForegroundColor Gray
|
|
|
|
# Write hash to file
|
|
$hashFile = "$MsiPath.sha256"
|
|
"$hash $MsiName" | Out-File -FilePath $hashFile -Encoding utf8 -NoNewline
|
|
Write-Host " Hash file: $hashFile" -ForegroundColor Gray
|
|
} else {
|
|
Write-Error "MSI file was not created at expected path: $MsiPath"
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "`nTo install:" -ForegroundColor Cyan
|
|
Write-Host " msiexec /i `"$MsiPath`"" -ForegroundColor White
|
|
Write-Host "`nTo uninstall:" -ForegroundColor Cyan
|
|
Write-Host " msiexec /x `"$MsiPath`"" -ForegroundColor White
|