fix(setup): prepare native Windows ARM64 build dependencies

Source activation could not build cryptography because the setup shell
could not discover the installed OpenSSL development libraries.

Configure Visual Studio ARM64, Clang, Rust, and static OpenSSL before PM
runs. Reuse installed tools and install missing prerequisites. Select a
classic vcpkg with a ports tree and use an explicit installation root.
Report damaged shared libraries without deleting the shared installation.

Verified native PowerShell activation and deactivation on Promise, then
warm activation with no new dependency generation. Cryptography imported
with static OpenSSL. Real vcpkg checks covered installation into a path
with spaces, warm reuse, manifest mode, and damaged-package rejection.
The canonical Windows runner passed the helper and output-encoding tests.

Fresh Visual Studio and Rust installation were not exercised because
Promise already had those toolchains installed.
This commit is contained in:
ethernet
2026-09-11 13:29:45 -04:00
parent caf27c01b9
commit fffccbb2ae
4 changed files with 259 additions and 4 deletions

View File

@@ -0,0 +1,152 @@
# Native build dependencies are separate from PM's application environment.
function Invoke-HermesBuildCommand {
param([string]$Command, [string[]]$Arguments)
$executable = (Get-Command $Command -CommandType Application -ErrorAction Stop).Source
$previousPreference = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
& $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) {
Write-Host 'Installing static ARM64 OpenSSL development libraries via vcpkg...'
Invoke-HermesBuildCommand $Vcpkg @('install', 'openssl:arm64-windows-static-md', '--classic', '--disable-metrics', "--x-install-root=$(Join-Path $Root 'installed')")
} else {
Write-Host "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)
$buildRoot = Join-Path $StateRoot 'build-tools'
New-Item -ItemType Directory -Force -Path $buildRoot | Out-Null
$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)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'ARM64 C++ or Clang build tools are missing. Run setup-hermes.ps1 once in an Administrator PowerShell to install them.'
}
$installer = Join-Path $buildRoot 'vs-buildtools.exe'
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'
}
$installArgs = @(
'--quiet', '--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 }
$install = Start-Process -FilePath $installer -ArgumentList $installArgs -Wait -PassThru
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-Host "ARM64 C++ build tools found: $vs"
$devCmd = Join-Path $vs 'Common7\Tools\VsDevCmd.bat'
# The batch file configures SDK, compiler, and linker paths only for this setup process.
$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)
$lines = $process.StandardOutput.ReadToEnd() -split "`r?`n"
$process.WaitForExit()
if ($process.ExitCode -ne 0) { throw 'Could not initialize the ARM64 Visual Studio developer environment' }
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' }
$cargoBin = Join-Path $HOME '.cargo\bin'
$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')
$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')
$env:RUSTUP_TOOLCHAIN = '1.98.0-aarch64-pc-windows-msvc'
}
Write-Host 'ARM64 Rust toolchain ready'
$env:CC_aarch64_pc_windows_msvc = $clangPath
Write-Host "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'))) {
Invoke-HermesBuildCommand 'git' @('clone', 'https://github.com/microsoft/vcpkg.git', $vcpkgRoot)
Invoke-HermesBuildCommand 'git' @('-C', $vcpkgRoot, 'checkout', '--detach', '00c5775211f45cd08b37fce0484b4cb940e422ab')
}
Invoke-HermesBuildCommand (Join-Path $vcpkgRoot 'bootstrap-vcpkg.bat') @('-disableMetrics')
}
$env:VCPKG_ROOT = $vcpkgRoot
$env:OPENSSL_DIR = Install-HermesArm64OpenSSL -Vcpkg (Join-Path $vcpkgRoot 'vcpkg.exe') -Root $vcpkgRoot
$env:OPENSSL_STATIC = '1'
}

View File

@@ -70,6 +70,13 @@ if (Test-Path $uv) {
}
# ---------------------------------------------------------------------------
# ARM64 source wheels need the native compiler and OpenSSL development libraries.
# Activation runs setup in a child, so these build variables do not leak into its caller.
if ($arch -eq 'arm64') {
. (Join-Path $repo 'scripts\windows-build-deps.ps1')
Initialize-HermesArm64BuildTools -StateRoot (Split-Path $store -Parent)
}
# Delegate to pm: python + venv + tool store + hash-verified venv sync
# ---------------------------------------------------------------------------
Write-Host 'Installing python + tools + dependencies via pm (hash-verified via uv.lock)...' -ForegroundColor Cyan

View File

@@ -0,0 +1,89 @@
"""Native PowerShell prerequisite selection without downloading build tools."""
from pathlib import Path
import os
import shutil
import subprocess
import pytest
pytestmark = pytest.mark.platforms("windows")
HELPER = Path(__file__).resolve().parents[2] / "scripts" / "windows-build-deps.ps1"
def test_openssl_installs_once_and_rejects_damaged_shared_install(tmp_path):
script = tmp_path / "check.ps1"
script.write_text(r'''
param([string]$Helper, [string]$Root)
$ErrorActionPreference = 'Stop'
. $Helper
$prefix = Join-Path $Root 'installed\arm64-windows-static-md'
function Invoke-HermesBuildCommand {
param([string]$Command, [string[]]$Arguments)
if ($Arguments[0] -ne 'install' -or $Arguments[1] -ne 'openssl:arm64-windows-static-md') {
throw 'incorrect installation request'
}
if ($Arguments -notcontains '--classic') { throw 'manifest mode was not disabled' }
$script:calls += 1
if ($script:calls -gt 1) { return } # vcpkg trusts its installed database on subsequent requests.
New-Item -ItemType Directory -Force (Join-Path $prefix 'lib'), (Join-Path $prefix 'include\openssl') | Out-Null
foreach ($relative in @('lib\libcrypto.lib', 'lib\libssl.lib', 'include\openssl\ssl.h')) {
[IO.File]::WriteAllText((Join-Path $prefix $relative), 'fixture')
}
}
$calls = 0
$first = Install-HermesArm64OpenSSL -Vcpkg 'fixture-vcpkg' -Root $Root
$second = Install-HermesArm64OpenSSL -Vcpkg 'fixture-vcpkg' -Root $Root
if ($calls -ne 1 -or $first -ne $prefix -or $second -ne $prefix) { throw 'warm setup installed twice' }
Remove-Item (Join-Path $prefix 'include\openssl\ssl.h')
$rejected = $false
try { Install-HermesArm64OpenSSL -Vcpkg 'fixture-vcpkg' -Root $Root } catch {
if ($_.Exception.Message -notmatch 'installation is damaged') { throw }
$rejected = $true
}
if (-not $rejected -or $calls -ne 2) { throw 'damaged install was accepted' }
Write-Output 'PASS'
''', encoding="utf-8")
env = dict(os.environ)
env.setdefault("SystemRoot", r"C:\Windows")
shell = shutil.which("powershell") or str(Path(env["SystemRoot"]) / "System32/WindowsPowerShell/v1.0/powershell.exe")
result = subprocess.run(
[shell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", str(script), str(HELPER), str(tmp_path)],
capture_output=True, text=True, encoding="utf-8", errors="replace", env=env, timeout=30,
)
assert result.returncode == 0, result.stdout + result.stderr
assert "PASS" in result.stdout
def test_native_build_command_preserves_failures_and_spaces(tmp_path):
script = tmp_path / "native.ps1"
script.write_text(r'''
param([string]$Helper, [string]$Root)
$ErrorActionPreference = 'Stop'
. $Helper
$command = Join-Path $Root 'child with spaces.cmd'
[IO.File]::WriteAllText($command, "@echo off`r`necho native-progress 1>&2`r`nexit /b 19`r`n")
$failed = $false
try { Invoke-HermesBuildCommand $command @() } catch {
if ($_.Exception.Message -notmatch 'exit code 19') { throw }
$failed = $true
}
if (-not $failed -or $ErrorActionPreference -ne 'Stop') { throw 'failure or shell preference was lost' }
[IO.File]::WriteAllText($command, "@echo off`r`necho native-progress 1>&2`r`nexit /b 0`r`n")
Invoke-HermesBuildCommand $command @()
$failed = $false
try { Invoke-HermesBuildCommand (Join-Path $Root 'absent.exe') @() } catch { $failed = $true }
if (-not $failed) { throw 'missing executable was accepted after a successful command' }
Write-Output 'PASS'
''', encoding="utf-8")
env = dict(os.environ)
env.setdefault("SystemRoot", r"C:\Windows")
env.setdefault("ComSpec", str(Path(env["SystemRoot"]) / "System32/cmd.exe"))
env.setdefault("PATHEXT", ".COM;.EXE;.BAT;.CMD")
shell = shutil.which("powershell") or str(Path(env["SystemRoot"]) / "System32/WindowsPowerShell/v1.0/powershell.exe")
result = subprocess.run(
[shell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", str(script), str(HELPER), str(tmp_path)],
capture_output=True, text=True, encoding="utf-8", errors="replace", env=env, timeout=30,
)
assert result.returncode == 0, result.stdout + result.stderr
assert "PASS" in result.stdout

View File

@@ -130,10 +130,17 @@ interpreter or redirect an installed desktop app to this checkout.
### Prepare a checkout
Use an ordinary terminal outside the packaged Hermes app. Leave any existing
Python virtual environment first. On Windows, use native PowerShell with Git
and the target architecture's C++ build tools available. Dependencies without
wheels can also require Rust/Cargo and native libraries. PM does not install
those compiler toolchains. POSIX source builds have native build requirements too.
Python virtual environment first. On Windows, use native PowerShell with Git.
For ARM64, setup checks Visual Studio C++ tools, Clang, native Rust, and static
OpenSSL development libraries before PM runs. It reuses existing installations
and installs missing prerequisites. Missing Visual Studio components require
an Administrator PowerShell. OpenSSL uses vcpkg's `arm64-windows-static-md`
triplet. A damaged shared installation produces a repair error, not automatic
deletion. Compiler and OpenSSL environment variables apply only to the setup
process when you enter through `activate.ps1`.
Other platforms still require the native compiler tools and libraries needed
by dependencies without compatible wheels.
Clone the repository and select your branch before preparing dependencies: