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: