mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
Two caches were restored every run and then never read.
uv's native-wheel partition (native_cache_path) needs a toolchain identity
to be stable. win32-x64 never runs VsDevCmd, so VCToolsVersion is unset and
the identity fell back to a random uuid: every run compiled cryptography,
obstore, davey and friends from source into a fresh partition (all 6
sampled x64 runs had a new partition). Identify MSVC the way rustc's cc
crate finds it, through vswhere and the default VCToolsVersion file.
vcpkg's default binary cache lives under LOCALAPPDATA, which the desktop
preparation moves into the job-local home, so OpenSSL rebuilt every arm64
run ("Restored 0 package(s)", ~3 min). Point VCPKG_DEFAULT_BINARY_CACHE at
native/vcpkg-archives, inside the transported native domain.
267 lines
16 KiB
PowerShell
267 lines
16 KiB
PowerShell
# Native build dependencies are separate from PM's application environment.
|
|
|
|
# An interactive console sees one status line per build command, with the
|
|
# full output in $script:HermesBuildLog; CI and a redirected stdout (pm under
|
|
# the installer, which logs that itself) keep the full stream. install.ps1
|
|
# has its own copy: this file is dot-sourced by the build entry point and
|
|
# setup-hermes.ps1, which never load the installer.
|
|
function Test-HermesBuildQuiet {
|
|
if ($env:CI -or $env:GITHUB_ACTIONS -or $env:HERMES_INSTALL_VERBOSE -or -not $script:HermesBuildLog) { return $false }
|
|
try { return -not [Console]::IsOutputRedirected } catch { return $false }
|
|
}
|
|
|
|
function Write-HermesBuildNote {
|
|
# Discovery details belong in CI transcripts, not on a user's console.
|
|
param([string]$Message)
|
|
if (-not (Test-HermesBuildQuiet)) { Write-Host "-> $Message" }
|
|
}
|
|
|
|
function Invoke-HermesBuildCommand {
|
|
param([string]$Command, [string[]]$Arguments, [string]$Label)
|
|
# Windows PowerShell 5.1 returns every match from Get-Command even without
|
|
# -All. .Source on that array is every path joined by a space, and the call
|
|
# operator then treats the joined string as one program name. Git for
|
|
# Windows puts git.exe in both cmd\ and bin\, so a bare lookup is that bug.
|
|
$executable = @(Get-Command $Command -CommandType Application -ErrorAction Stop | Select-Object -First 1)[0].Source
|
|
if ($executable -isnot [string] -or -not (Test-Path -LiteralPath $executable -PathType Leaf)) {
|
|
throw "Could not resolve a single executable for $Command (got: $executable)"
|
|
}
|
|
if (-not $Label) { $Label = "Running $(Split-Path -Leaf $executable)" }
|
|
$previousPreference = $ErrorActionPreference
|
|
try {
|
|
$ErrorActionPreference = 'Continue'
|
|
if (Test-HermesBuildQuiet) {
|
|
$recent = New-Object 'System.Collections.Generic.Queue[string]'
|
|
$width = 80
|
|
try { $width = [Math]::Max(20, $Host.UI.RawUI.WindowSize.Width) } catch { $width = 80 }
|
|
$writer = New-Object System.IO.StreamWriter($script:HermesBuildLog, $true, (New-Object System.Text.UTF8Encoding($false)))
|
|
try {
|
|
$writer.WriteLine("==> $Label ($((Get-Date).ToUniversalTime().ToString('s'))Z)")
|
|
& $executable @Arguments 2>&1 | ForEach-Object {
|
|
$line = "$_".TrimEnd("`r")
|
|
$writer.WriteLine($line)
|
|
$recent.Enqueue($line)
|
|
if ($recent.Count -gt 20) { [void]$recent.Dequeue() }
|
|
# git and vcpkg redraw progress with bare CRs; show the newest.
|
|
$shown = " ${Label}: $((($line -split "`r")[-1]).Trim())"
|
|
if ($shown.Length -ge $width) { $shown = $shown.Substring(0, $width - 1) }
|
|
Write-Host ("`r" + $shown.PadRight($width - 1)) -NoNewline -ForegroundColor DarkGray
|
|
}
|
|
$code = $LASTEXITCODE
|
|
} finally {
|
|
$writer.Dispose()
|
|
Write-Host ("`r" + (' ' * ($width - 1)) + "`r") -NoNewline
|
|
}
|
|
if ($code -ne 0) {
|
|
Write-Host "[X] $Label failed (exit $code). Last output:" -ForegroundColor Red
|
|
foreach ($line in $recent) { Write-Host " $line" }
|
|
Write-Host " full log: $script:HermesBuildLog"
|
|
}
|
|
} else {
|
|
Write-Host "-> $Label"
|
|
& $executable @Arguments | Out-Host
|
|
$code = $LASTEXITCODE
|
|
}
|
|
} finally { $ErrorActionPreference = $previousPreference }
|
|
if ($code -ne 0) { throw "$Command failed with exit code $code" }
|
|
}
|
|
|
|
function Install-HermesArm64OpenSSL {
|
|
param([string]$Vcpkg, [string]$Root)
|
|
$prefix = Join-Path $Root 'installed\arm64-windows-static-md'
|
|
$required = @('include\openssl\ssl.h', 'lib\libcrypto.lib', 'lib\libssl.lib')
|
|
$missing = @($required | Where-Object { -not (Test-Path -LiteralPath (Join-Path $prefix $_) -PathType Leaf) })
|
|
if ($missing.Count) {
|
|
# vcpkg refuses a binary cache directory that does not exist yet.
|
|
if ($env:VCPKG_DEFAULT_BINARY_CACHE) { New-Item -ItemType Directory -Force -Path $env:VCPKG_DEFAULT_BINARY_CACHE | Out-Null }
|
|
Invoke-HermesBuildCommand $Vcpkg @('install', 'openssl:arm64-windows-static-md', '--classic', '--disable-metrics', "--x-install-root=$(Join-Path $Root 'installed')") 'Building static ARM64 OpenSSL via vcpkg (several minutes)'
|
|
} else {
|
|
Write-HermesBuildNote "ARM64 OpenSSL development libraries found: $prefix"
|
|
}
|
|
foreach ($relative in $required) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $prefix $relative) -PathType Leaf)) {
|
|
throw "OpenSSL installation is damaged: $prefix\$relative is missing. Repair the openssl:arm64-windows-static-md package in $Root, then rerun setup."
|
|
}
|
|
}
|
|
return $prefix
|
|
}
|
|
|
|
function Get-HermesArm64VisualStudio {
|
|
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
|
if (-not (Test-Path -LiteralPath $vswhere)) { return $null }
|
|
$found = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64 -property installationPath
|
|
if ($LASTEXITCODE -ne 0) { throw 'Visual Studio discovery failed' }
|
|
return ($found | Select-Object -First 1)
|
|
}
|
|
|
|
function Get-HermesClang {
|
|
param([string]$VisualStudio)
|
|
$command = Get-Command clang.exe -ErrorAction SilentlyContinue
|
|
if ($command) { return $command.Source }
|
|
$candidates = @((Join-Path $env:ProgramFiles 'LLVM\bin\clang.exe'))
|
|
if ($VisualStudio) {
|
|
$candidates += @(
|
|
(Join-Path $VisualStudio 'VC\Tools\Llvm\ARM64\bin\clang.exe'),
|
|
(Join-Path $VisualStudio 'VC\Tools\Llvm\bin\clang.exe')
|
|
)
|
|
}
|
|
return ($candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1)
|
|
}
|
|
|
|
function Initialize-HermesArm64BuildTools {
|
|
param([string]$StateRoot, [string]$OpenSSLRoot)
|
|
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
|
|
throw 'ARM64 build dependencies require Windows.'
|
|
}
|
|
$buildRoot = Join-Path $StateRoot 'build-tools'
|
|
New-Item -ItemType Directory -Force -Path $buildRoot | Out-Null
|
|
$script:HermesBuildLog = Join-Path $buildRoot 'build.log'
|
|
$vs = Get-HermesArm64VisualStudio
|
|
$clangPath = Get-HermesClang -VisualStudio $vs
|
|
if (-not $vs -or -not $clangPath) {
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
|
$elevated = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
# A UAC prompt needs someone at the desktop; CI, ssh and scheduled
|
|
# runs would block on it, so they keep the explicit instruction.
|
|
$canPrompt = [Environment]::UserInteractive -and -not $env:CI -and -not $env:GITHUB_ACTIONS -and
|
|
-not $env:SSH_CONNECTION -and -not $env:SSH_CLIENT
|
|
$needsAdmin = 'ARM64 C++ or Clang build tools are missing. Run setup-hermes.ps1 once in an Administrator PowerShell to install them.'
|
|
if (-not $elevated -and -not $canPrompt) { throw $needsAdmin }
|
|
$installer = Join-Path $buildRoot 'vs-buildtools.exe'
|
|
Write-Host '-> Installing Visual Studio Build Tools (ARM64 C++ and Clang) to compile dependencies that have no ARM64 Windows wheel (such as cryptography).'
|
|
Write-Host '-> This downloads several GB and can take 20+ minutes. The Visual Studio installer shows its progress in its own window.'
|
|
Invoke-WebRequest -UseBasicParsing 'https://aka.ms/vs/17/release/vs_BuildTools.exe' -OutFile $installer
|
|
$signature = Get-AuthenticodeSignature -LiteralPath $installer
|
|
if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation(?:,|$)') {
|
|
throw 'Visual Studio installer does not have a valid Microsoft signature'
|
|
}
|
|
# --passive shows the installer's progress window without asking
|
|
# anything; --quiet showed nothing for the whole install.
|
|
$installArgs = @(
|
|
'--passive', '--wait', '--norestart', '--nocache',
|
|
'--add', 'Microsoft.VisualStudio.Workload.VCTools', '--includeRecommended',
|
|
'--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64',
|
|
'--add', 'Microsoft.VisualStudio.Component.VC.Llvm.Clang'
|
|
)
|
|
if ($vs) { $installArgs = @('modify', '--installPath', ('"' + $vs + '"')) + $installArgs }
|
|
if ($elevated) {
|
|
$install = Start-Process -FilePath $installer -ArgumentList $installArgs -Wait -PassThru
|
|
} else {
|
|
Write-Host '-> Windows will ask for administrator approval to install them.'
|
|
try {
|
|
$install = Start-Process -FilePath $installer -ArgumentList $installArgs -Verb RunAs -Wait -PassThru
|
|
} catch {
|
|
throw "Administrator approval was declined or unavailable. $needsAdmin"
|
|
}
|
|
}
|
|
if ($install.ExitCode -notin @(0, 3010)) { throw "Visual Studio installation failed: $($install.ExitCode)" }
|
|
$vs = Get-HermesArm64VisualStudio
|
|
if (-not $vs) { throw 'ARM64 C++ build tools remain unavailable. Restart Windows if the installer requested it.' }
|
|
$clangPath = Get-HermesClang -VisualStudio $vs
|
|
if (-not $clangPath) { throw 'The Clang compiler is still missing after Visual Studio setup.' }
|
|
}
|
|
Write-HermesBuildNote "ARM64 C++ build tools found: $vs"
|
|
# CI, desktop builds and native staging can inherit the same developer
|
|
# environment. VsDevCmd prepends its paths again on every call, eventually
|
|
# overflowing cmd.exe's line limit. Reuse only a matching, usable environment.
|
|
$vsReady = $env:VSINSTALLDIR -and $env:VSINSTALLDIR.TrimEnd('\') -eq $vs.TrimEnd('\') -and
|
|
$env:VSCMD_ARG_HOST_ARCH -eq 'arm64' -and $env:VSCMD_ARG_TGT_ARCH -eq 'arm64' -and
|
|
$env:INCLUDE -and $env:LIB -and (Get-Command cl.exe -ErrorAction SilentlyContinue)
|
|
if (-not $vsReady) {
|
|
$devCmd = Join-Path $vs 'Common7\Tools\VsDevCmd.bat'
|
|
# Keep cmd's outer quotes intact on Windows PowerShell 5 as well as pwsh.
|
|
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
|
|
$startInfo.FileName = $env:ComSpec
|
|
$startInfo.Arguments = "/d /s /c `"`"$devCmd`" -no_logo -arch=arm64 -host_arch=arm64 >nul && set`""
|
|
$startInfo.UseShellExecute = $false
|
|
$startInfo.RedirectStandardOutput = $true
|
|
$process = [System.Diagnostics.Process]::Start($startInfo)
|
|
try {
|
|
$lines = $process.StandardOutput.ReadToEnd() -split "`r?`n"
|
|
$process.WaitForExit()
|
|
if ($process.ExitCode -ne 0) { throw 'Could not initialize the ARM64 Visual Studio developer environment' }
|
|
} finally { $process.Dispose() }
|
|
foreach ($line in $lines) {
|
|
if ($line -match '^([^=]+)=(.*)$') { Set-Item -LiteralPath "env:$($matches[1])" -Value $matches[2] }
|
|
}
|
|
}
|
|
if (-not (Get-Command cl.exe -ErrorAction SilentlyContinue)) { throw 'ARM64 C++ compiler is unavailable after environment setup' }
|
|
# With VSCMD_ARG_TGT_ARCH exported, rustc's cc crate takes link.exe from PATH
|
|
# instead of asking vswhere. Under Git Bash that is coreutils' link.exe, so
|
|
# pin the MSVC linker explicitly. PATH order is the caller's (a reused
|
|
# environment keeps Git Bash's order), so take it from the developer
|
|
# environment's own tools directory instead.
|
|
$linker = if ($env:VCToolsInstallDir) { Join-Path $env:VCToolsInstallDir 'bin\HostARM64\ARM64\link.exe' }
|
|
if (-not $linker -or -not (Test-Path -LiteralPath $linker)) {
|
|
throw "MSVC link.exe is missing from VCToolsInstallDir '$env:VCToolsInstallDir'"
|
|
}
|
|
$env:CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER = $linker
|
|
|
|
# Child builds isolate HOME/USERPROFILE. Keep Rust anchored to the homes
|
|
# used here, including caller-selected locations.
|
|
if (-not $env:CARGO_HOME) { $env:CARGO_HOME = Join-Path $HOME '.cargo' }
|
|
if (-not $env:RUSTUP_HOME) { $env:RUSTUP_HOME = Join-Path $HOME '.rustup' }
|
|
$env:CARGO_HOME = [IO.Path]::GetFullPath($env:CARGO_HOME)
|
|
$env:RUSTUP_HOME = [IO.Path]::GetFullPath($env:RUSTUP_HOME)
|
|
$cargoBin = Join-Path $env:CARGO_HOME 'bin'
|
|
if ($cargoBin -notin ($env:PATH -split ';')) { $env:PATH = "$cargoBin;$env:PATH" }
|
|
$rustup = Get-Command rustup.exe -ErrorAction SilentlyContinue
|
|
if (-not $rustup) {
|
|
$installer = Join-Path $buildRoot 'rustup-init.exe'
|
|
$url = 'https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-pc-windows-msvc/rustup-init.exe'
|
|
Invoke-WebRequest -UseBasicParsing $url -OutFile $installer
|
|
if ((Get-FileHash -Algorithm SHA256 $installer).Hash.ToLowerInvariant() -ne 'de9f7d29ccd39efa59a3dda3ec363b396e09b92681229b9b8f6aaa4c84285e9c') {
|
|
throw 'rustup installer SHA256 mismatch'
|
|
}
|
|
Invoke-HermesBuildCommand $installer @('-y', '--no-modify-path', '--profile', 'minimal', '--default-toolchain', '1.98.0-aarch64-pc-windows-msvc') 'Installing the Rust toolchain (1.98.0, ARM64) for those source builds'
|
|
$rustup = Get-Command rustup.exe -ErrorAction Stop
|
|
}
|
|
$rustc = Get-Command rustc.exe -ErrorAction SilentlyContinue
|
|
$rustInfo = if ($rustc) { (& $rustc.Source -vV) -join "`n" } else { '' }
|
|
if ($rustInfo -notmatch 'host: aarch64-pc-windows-msvc') {
|
|
Invoke-HermesBuildCommand $rustup.Source @('toolchain', 'install', '1.98.0-aarch64-pc-windows-msvc', '--profile', 'minimal') 'Installing Rust 1.98.0 for ARM64'
|
|
$env:RUSTUP_TOOLCHAIN = '1.98.0-aarch64-pc-windows-msvc'
|
|
}
|
|
Write-HermesBuildNote 'ARM64 Rust toolchain ready'
|
|
|
|
$env:CC_aarch64_pc_windows_msvc = $clangPath
|
|
Write-HermesBuildNote "ARM64 Rust C compiler: $clangPath"
|
|
|
|
$vcpkgRoot = $null
|
|
$vcpkgCommand = Get-Command vcpkg.exe -ErrorAction SilentlyContinue
|
|
$candidates = @($env:VCPKG_ROOT, $env:VCPKG_INSTALLATION_ROOT)
|
|
if ($vcpkgCommand) { $candidates += Split-Path $vcpkgCommand.Source }
|
|
$candidates += @((Join-Path $env:SystemDrive 'vcpkg'), (Join-Path $buildRoot 'vcpkg'))
|
|
foreach ($candidate in $candidates) {
|
|
# Visual Studio also ships a manifest-only vcpkg without a ports tree.
|
|
if ($candidate -and (Test-Path -LiteralPath (Join-Path $candidate 'vcpkg.exe')) -and
|
|
(Test-Path -LiteralPath (Join-Path $candidate 'ports\openssl\portfile.cmake'))) {
|
|
$vcpkgRoot = $candidate
|
|
break
|
|
}
|
|
}
|
|
if (-not $vcpkgRoot) {
|
|
$vcpkgRoot = Join-Path $buildRoot 'vcpkg'
|
|
if (-not (Test-Path -LiteralPath (Join-Path $vcpkgRoot '.git'))) {
|
|
# Phase lines ("Receiving objects: 42%") feed the status line;
|
|
# git prints none to a pipe unless asked.
|
|
$progress = @()
|
|
if (Test-HermesBuildQuiet) { $progress = @('--progress') }
|
|
Invoke-HermesBuildCommand 'git' (@('clone') + $progress + @('https://github.com/microsoft/vcpkg.git', $vcpkgRoot)) 'Downloading vcpkg to build OpenSSL for ARM64'
|
|
Invoke-HermesBuildCommand 'git' @('-C', $vcpkgRoot, 'checkout', '--detach', '00c5775211f45cd08b37fce0484b4cb940e422ab') 'Pinning vcpkg'
|
|
}
|
|
Invoke-HermesBuildCommand (Join-Path $vcpkgRoot 'bootstrap-vcpkg.bat') @('-disableMetrics') 'Building vcpkg'
|
|
}
|
|
$env:VCPKG_ROOT = $vcpkgRoot
|
|
# vcpkg resolves Visual Studio itself and takes the newest instance, which may lack the
|
|
# native arm64 toolset even though the instance above has it: the arm64-windows triplet
|
|
# then fails with "Unable to find a valid toolchain" and the whole update aborts. Point
|
|
# vcpkg at the instance VsDevCmd was initialized against, unless the caller pinned one.
|
|
if (-not $env:VCPKG_VISUAL_STUDIO_PATH) { $env:VCPKG_VISUAL_STUDIO_PATH = $vs }
|
|
# The install tree can be cached independently of the discovered checkout.
|
|
if (-not $OpenSSLRoot) { $OpenSSLRoot = $vcpkgRoot }
|
|
$env:OPENSSL_DIR = Install-HermesArm64OpenSSL -Vcpkg (Join-Path $vcpkgRoot 'vcpkg.exe') -Root $OpenSSLRoot
|
|
$env:OPENSSL_STATIC = '1'
|
|
}
|