From fffccbb2aef424be72977d9d0a9588da427ff69d Mon Sep 17 00:00:00 2001 From: ethernet Date: Fri, 11 Sep 2026 13:29:45 -0400 Subject: [PATCH] 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. --- scripts/windows-build-deps.ps1 | 152 +++++++++++++++++++ setup-hermes.ps1 | 7 + tests/pm/test_windows_build_deps.py | 89 +++++++++++ website/docs/reference/package-management.md | 15 +- 4 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 scripts/windows-build-deps.ps1 create mode 100644 tests/pm/test_windows_build_deps.py diff --git a/scripts/windows-build-deps.ps1 b/scripts/windows-build-deps.ps1 new file mode 100644 index 0000000000..3aaf6aeb2f --- /dev/null +++ b/scripts/windows-build-deps.ps1 @@ -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' +} diff --git a/setup-hermes.ps1 b/setup-hermes.ps1 index 9bf39f3ec7..57f544f642 100644 --- a/setup-hermes.ps1 +++ b/setup-hermes.ps1 @@ -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 diff --git a/tests/pm/test_windows_build_deps.py b/tests/pm/test_windows_build_deps.py new file mode 100644 index 0000000000..5758c9a8b5 --- /dev/null +++ b/tests/pm/test_windows_build_deps.py @@ -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 diff --git a/website/docs/reference/package-management.md b/website/docs/reference/package-management.md index fa179e3ab1..0fa8282a33 100644 --- a/website/docs/reference/package-management.md +++ b/website/docs/reference/package-management.md @@ -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: