Files
bun.sh/scripts/bootstrap.ps1
Dylan Conway 8f37779b8f Fix 7zip ARM64: suppress post_install error with SilentlyContinue
The Remove-Item error on 7zr.exe happens on ARM64 regardless of user
context (packer user, not just SYSTEM). Temporarily set ErrorActionPreference
to SilentlyContinue and convert all output streams to strings so the
error doesn't propagate to PowerShell's exit code.

[build images]
2026-02-12 22:11:27 -08:00

600 lines
18 KiB
PowerShell
Executable File

# Version: 13
# A script that installs the dependencies needed to build and test Bun on Windows.
# Supports both x64 and ARM64 using Scoop for package management.
# Used by Azure [build images] pipeline.
# If this script does not work on your machine, please open an issue:
# https://github.com/oven-sh/bun/issues
# If you need to make a change to this script, such as upgrading a dependency,
# increment the version comment to indicate that a new image should be built.
# Otherwise, the existing image will be retroactively updated.
param (
[Parameter(Mandatory = $false)]
[switch]$CI = ($env:CI -eq "true"),
[Parameter(Mandatory = $false)]
[switch]$Optimize = $CI
)
$ErrorActionPreference = "Stop"
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
# Detect ARM64 from registry (works even under x64 emulation)
$realArch = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment').PROCESSOR_ARCHITECTURE
$script:IsARM64 = $realArch -eq "ARM64"
# If we're on ARM64 but running under x64 emulation, re-launch as native ARM64.
# Azure Run Command uses x64-emulated PowerShell which breaks package installs.
if ($script:IsARM64 -and $env:PROCESSOR_ARCHITECTURE -ne "ARM64") {
$nativePS = "$env:SystemRoot\Sysnative\WindowsPowerShell\v1.0\powershell.exe"
if (Test-Path $nativePS) {
Write-Output "Re-launching bootstrap as native ARM64 PowerShell..."
& $nativePS -NoProfile -ExecutionPolicy Bypass -File $MyInvocation.MyCommand.Path @PSBoundParameters
exit $LASTEXITCODE
}
}
# ============================================================================
# Utility functions
# ============================================================================
function Which {
param ([switch]$Required = $false)
foreach ($command in $args) {
$result = Get-Command $command -ErrorAction SilentlyContinue
if ($result -and $result.Path) {
return $result.Path
}
}
if ($Required) {
$commands = $args -join ', '
throw "Command not found: $commands"
}
}
function Download-File {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Url,
[Parameter(Mandatory = $false)]
[string]$Name,
[Parameter(Mandatory = $false)]
[string]$Path
)
if (-not $Name) {
$Name = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetRandomFileName(), [System.IO.Path]::GetExtension($Url))
}
if (-not $Path) {
$Path = "$env:TEMP\$Name"
}
$client = New-Object System.Net.WebClient
for ($i = 0; $i -lt 10 -and -not (Test-Path $Path); $i++) {
try {
$client.DownloadFile($Url, $Path)
} catch {
Write-Warning "Failed to download $Url, retry $i..."
Start-Sleep -s $i
}
}
return $Path
}
function Refresh-Path {
$paths = @(
[System.Environment]::GetEnvironmentVariable("Path", "Machine"),
[System.Environment]::GetEnvironmentVariable("Path", "User"),
[System.Environment]::GetEnvironmentVariable("Path", "Process")
)
$uniquePaths = $paths |
Where-Object { $_ } |
ForEach-Object { $_.Split(';', [StringSplitOptions]::RemoveEmptyEntries) } |
Where-Object { $_ -and (Test-Path $_) } |
Select-Object -Unique
$env:Path = ($uniquePaths -join ';').TrimEnd(';')
}
function Add-To-Path {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$PathToAdd,
[Parameter(Mandatory = $false)]
[ValidateSet("Machine", "User")]
[string]$Scope = "Machine"
)
$absolutePath = Resolve-Path $PathToAdd
$currentPath = [Environment]::GetEnvironmentVariable("Path", $Scope)
if ($currentPath -like "*$absolutePath*") {
return
}
$newPath = $currentPath.TrimEnd(";") + ";" + $absolutePath
if ($newPath.Length -ge 2048) {
Write-Warning "PATH is too long, removing duplicate and old entries..."
$paths = $currentPath.Split(';', [StringSplitOptions]::RemoveEmptyEntries) |
Where-Object { $_ -and (Test-Path $_) } |
Select-Object -Unique
$paths += $absolutePath
$newPath = $paths -join ';'
while ($newPath.Length -ge 2048 -and $paths.Count -gt 1) {
$paths = $paths[1..$paths.Count]
$newPath = $paths -join ';'
}
}
Write-Output "Adding $absolutePath to PATH ($Scope)..."
[Environment]::SetEnvironmentVariable("Path", "$newPath", $Scope)
Refresh-Path
}
function Set-Env {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Name,
[Parameter(Mandatory = $true, Position = 1)]
[string]$Value
)
Write-Output "Setting environment variable $Name=$Value..."
[System.Environment]::SetEnvironmentVariable("$Name", "$Value", "Machine")
[System.Environment]::SetEnvironmentVariable("$Name", "$Value", "Process")
}
# ============================================================================
# Scoop — ARM64-native package manager
# ============================================================================
function Install-Scoop {
if (Which scoop) {
return
}
Write-Output "Installing Scoop..."
# Scoop blocks admin installs unless -RunAsAdmin is passed.
# Install to a known global location so all users can access it.
$env:SCOOP = "C:\Scoop"
[Environment]::SetEnvironmentVariable("SCOOP", $env:SCOOP, "Machine")
iex "& {$(irm get.scoop.sh)} -RunAsAdmin -ScoopDir C:\Scoop"
Add-To-Path "C:\Scoop\shims"
Refresh-Path
Write-Output "Scoop version: $(scoop --version)"
}
function Install-Scoop-Package {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Name,
[Parameter(Mandatory = $false)]
[string]$Command = $Name
)
if (Which $Command) {
return
}
Write-Output "Installing $Name (via Scoop)..."
scoop install $Name
Refresh-Path
}
# ============================================================================
# Scoop packages (native ARM64 binaries)
# ============================================================================
function Install-Git {
Install-Scoop-Package git
if ($CI) {
git config --system --add safe.directory "*"
git config --system core.autocrlf false
git config --system core.eol lf
git config --system core.longpaths true
}
}
function Install-NodeJs {
Install-Scoop-Package nodejs -Command node
}
function Install-CMake {
Install-Scoop-Package cmake
}
function Install-Llvm {
$LLVM_VERSION = "21.1.8"
if (Which clang-cl) {
return
}
if ($script:IsARM64) {
Write-Output "Installing LLVM $LLVM_VERSION (ARM64 via Scoop)..."
scoop install "llvm-arm64@$LLVM_VERSION"
} else {
Write-Output "Installing LLVM $LLVM_VERSION (x64 via Scoop)..."
scoop install "llvm@$LLVM_VERSION"
}
Refresh-Path
}
function Install-Ninja {
Install-Scoop-Package ninja
}
function Install-Python {
Install-Scoop-Package python
}
function Install-Go {
Install-Scoop-Package go
}
function Install-Ruby {
Install-Scoop-Package ruby
}
function Install-7zip {
if (Which 7z) {
return
}
Write-Output "Installing 7zip (via Scoop)..."
# On ARM64, scoop's 7zip post_install tries to Remove-Item 7zr.exe which is
# locked during extraction. This error is non-fatal (7zip installs fine).
# Temporarily allow errors so it doesn't kill the bootstrap.
$prevErrorPref = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
scoop install 7zip *>&1 | ForEach-Object { "$_" } | Write-Host
$ErrorActionPreference = $prevErrorPref
Refresh-Path
if (-not (Which 7z)) {
throw "7zip installation failed"
}
}
function Install-Make {
Install-Scoop-Package make
}
function Install-Cygwin {
# Cygwin's default mirror (mirrors.kernel.org) can be unreachable from Azure.
# Make this non-fatal — the build will fail later if cygwin is actually needed.
try {
Install-Scoop-Package cygwin
$cygwinBin = Join-Path $env:USERPROFILE "scoop\apps\cygwin\current\bin"
if (Test-Path $cygwinBin) {
Add-To-Path $cygwinBin -Scope User
}
} catch {
Write-Warning "Cygwin installation failed (non-fatal): $_"
}
}
# ============================================================================
# Manual installs (not available or not ideal via Scoop)
# ============================================================================
function Install-Pwsh {
if (Which pwsh) {
return
}
$pwshArch = if ($script:IsARM64) { "arm64" } else { "x64" }
Write-Output "Installing PowerShell Core ($pwshArch)..."
$msi = Download-File "https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/PowerShell-7.5.2-win-$pwshArch.msi" -Name "pwsh-$pwshArch.msi"
$process = Start-Process msiexec -ArgumentList "/i `"$msi`" /quiet /norestart ADD_PATH=1" -Wait -PassThru -NoNewWindow
if ($process.ExitCode -ne 0) {
throw "Failed to install PowerShell: code $($process.ExitCode)"
}
Remove-Item $msi -ErrorAction SilentlyContinue
Refresh-Path
if ($CI) {
$shellPath = (Which pwsh -Required)
New-ItemProperty `
-Path "HKLM:\\SOFTWARE\\OpenSSH" `
-Name DefaultShell `
-Value $shellPath `
-PropertyType String `
-Force
}
}
function Install-Ccache {
if (Which ccache) {
return
}
$version = "4.12.2"
$archSuffix = if ($script:IsARM64) { "aarch64" } else { "x86_64" }
Write-Output "Installing ccache $version ($archSuffix)..."
$zip = Download-File "https://github.com/ccache/ccache/releases/download/v$version/ccache-$version-windows-$archSuffix.zip" -Name "ccache-$archSuffix.zip"
$extractDir = "$env:TEMP\ccache-extract"
Expand-Archive $zip $extractDir -Force
$installDir = "$env:ProgramFiles\ccache"
New-Item -Path $installDir -ItemType Directory -Force | Out-Null
Copy-Item "$extractDir\ccache-$version-windows-$archSuffix\*" $installDir -Recurse -Force
Remove-Item $zip -ErrorAction SilentlyContinue
Remove-Item $extractDir -Recurse -ErrorAction SilentlyContinue
Add-To-Path $installDir
}
function Install-Bun {
if (Which bun) {
return
}
if ($script:IsARM64) {
# No published ARM64 bun binary yet — download from our blob storage
Write-Output "Installing Bun (ARM64 from blob storage)..."
$zip = Download-File "https://buncistore.blob.core.windows.net/artifacts/bun-windows-aarch64.zip" -Name "bun-arm64.zip"
$extractDir = "$env:TEMP\bun-arm64"
Expand-Archive -Path $zip -DestinationPath $extractDir -Force
$bunExe = Get-ChildItem $extractDir -Recurse -Filter "*.exe" | Where-Object { $_.Name -match "bun" } | Select-Object -First 1
if ($bunExe) {
Copy-Item $bunExe.FullName "C:\Windows\System32\bun.exe" -Force
Write-Output "Bun ARM64 installed to C:\Windows\System32\bun.exe"
} else {
throw "Failed to find bun executable in ARM64 zip"
}
} else {
Write-Output "Installing Bun..."
$installScript = Download-File "https://bun.sh/install.ps1" -Name "bun-install.ps1"
$pwsh = Which pwsh powershell -Required
& $pwsh $installScript
}
}
function Install-Rust {
if (Which rustc) {
return
}
$rustPath = Join-Path $env:ProgramFiles "Rust"
if (-not (Test-Path $rustPath)) {
New-Item -Path $rustPath -ItemType Directory | Out-Null
}
# Set install paths before running rustup so it installs directly
# to Program Files (avoids issues with SYSTEM user profile path)
$env:CARGO_HOME = "$rustPath\cargo"
$env:RUSTUP_HOME = "$rustPath\rustup"
Write-Output "Installing Rustup..."
$rustupInit = Download-File "https://win.rustup.rs/" -Name "rustup-init.exe"
Write-Output "Installing Rust..."
& $rustupInit -y
Write-Output "Setting environment variables for Rust..."
Set-Env "CARGO_HOME" "$rustPath\cargo"
Set-Env "RUSTUP_HOME" "$rustPath\rustup"
Add-To-Path "$rustPath\cargo\bin"
}
function Install-Visual-Studio {
param (
[Parameter(Mandatory = $false)]
[string]$Edition = "community"
)
Write-Output "Downloading Visual Studio installer..."
$vsInstaller = Download-File "https://aka.ms/vs/17/release/vs_$Edition.exe"
Write-Output "Installing Visual Studio..."
$vsInstallArgs = @(
"--passive",
"--norestart",
"--wait",
"--force",
"--locale en-US",
"--add Microsoft.VisualStudio.Workload.NativeDesktop",
"--includeRecommended"
)
$process = Start-Process $vsInstaller -ArgumentList ($vsInstallArgs -join ' ') -Wait -PassThru -NoNewWindow
# Exit code 3010 means "reboot required" which is not a real error
if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) {
throw "Failed to install Visual Studio: code $($process.ExitCode)"
}
}
function Install-PdbAddr2line {
cargo install --examples "pdb-addr2line@0.11.2"
}
function Install-Nssm {
if (Which nssm) {
return
}
# Try Scoop first, fall back to our mirror if nssm.cc is down
try {
Install-Scoop-Package nssm
} catch {
Write-Output "Scoop install failed, downloading nssm from mirror..."
$zip = Download-File "https://buncistore.blob.core.windows.net/artifacts/nssm-2.24-103-gdee49fc.zip" -Name "nssm.zip"
Expand-Archive -Path $zip -DestinationPath "C:\Windows\Temp\nssm" -Force
$nssm = Get-ChildItem "C:\Windows\Temp\nssm" -Recurse -Filter "nssm.exe" | Where-Object { $_.DirectoryName -like "*win64*" } | Select-Object -First 1
if ($nssm) {
Copy-Item $nssm.FullName "C:\Windows\System32\nssm.exe" -Force
Write-Output "nssm installed to C:\Windows\System32\nssm.exe"
} else {
throw "Failed to install nssm"
}
}
}
# ============================================================================
# Buildkite
# ============================================================================
function Create-Buildkite-Environment-Hooks {
param (
[Parameter(Mandatory = $true)]
[string]$BuildkiteHome
)
Write-Output "Creating Buildkite environment hooks..."
$hooksDir = Join-Path $BuildkiteHome "hooks"
if (-not (Test-Path $hooksDir)) {
New-Item -Path $hooksDir -ItemType Directory -Force | Out-Null
}
$environmentHook = Join-Path $hooksDir "environment.ps1"
$buildPath = Join-Path $BuildkiteHome "build"
@"
# Buildkite environment hook
`$env:BUILDKITE_BUILD_CHECKOUT_PATH = "$buildPath"
"@ | Set-Content -Path $environmentHook -Encoding UTF8
Write-Output "Environment hook created at $environmentHook"
}
function Install-Buildkite {
if (Which buildkite-agent) {
return
}
Write-Output "Installing Buildkite agent..."
$env:buildkiteAgentToken = "xxx"
$installScript = Download-File "https://raw.githubusercontent.com/buildkite/agent/main/install.ps1"
$pwsh = Which pwsh powershell -Required
& $pwsh $installScript
Refresh-Path
if ($CI) {
$buildkiteHome = "C:\buildkite-agent"
if (-not (Test-Path $buildkiteHome)) {
New-Item -Path $buildkiteHome -ItemType Directory -Force | Out-Null
}
Create-Buildkite-Environment-Hooks -BuildkiteHome $buildkiteHome
}
}
# ============================================================================
# System optimization
# ============================================================================
function Optimize-System {
Disable-Windows-Defender
Disable-Windows-Threat-Protection
Disable-Windows-Services
Disable-Power-Management
}
function Optimize-System-Needs-Reboot {
Uninstall-Windows-Defender
}
function Disable-Windows-Defender {
Write-Output "Disabling Windows Defender..."
Set-MpPreference -DisableRealtimeMonitoring $true
Add-MpPreference -ExclusionPath "C:\", "D:\"
}
function Disable-Windows-Threat-Protection {
$itemPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection"
if (Test-Path $itemPath) {
Write-Output "Disabling Windows Threat Protection..."
Set-ItemProperty -Path $itemPath -Name "ForceDefenderPassiveMode" -Value 1 -Type DWORD
}
}
function Uninstall-Windows-Defender {
if (Get-Command Uninstall-WindowsFeature -ErrorAction SilentlyContinue) {
Write-Output "Uninstalling Windows Defender..."
Uninstall-WindowsFeature -Name Windows-Defender
}
}
function Disable-Windows-Services {
$services = @(
"WSearch", # Windows Search
"wuauserv", # Windows Update
"DiagTrack", # Connected User Experiences and Telemetry
"dmwappushservice", # WAP Push Message Routing Service
"PcaSvc", # Program Compatibility Assistant
"SysMain" # Superfetch
)
foreach ($service in $services) {
try {
Stop-Service $service -Force -ErrorAction SilentlyContinue
Set-Service $service -StartupType Disabled -ErrorAction SilentlyContinue
} catch {
Write-Warning "Could not disable service: $service"
}
}
}
function Disable-Power-Management {
Write-Output "Disabling Power Management..."
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c # High performance
powercfg /change monitor-timeout-ac 0
powercfg /change monitor-timeout-dc 0
powercfg /change standby-timeout-ac 0
powercfg /change standby-timeout-dc 0
powercfg /change hibernate-timeout-ac 0
powercfg /change hibernate-timeout-dc 0
}
# ============================================================================
# Main
# ============================================================================
if ($Optimize) {
Optimize-System
}
# Scoop package manager
Install-Scoop
# Packages via Scoop (native ARM64 or x64 depending on architecture)
# 7zip must be installed before git — git depends on 7zip via Scoop,
# and 7zip's post_install has a cleanup error on ARM64 SYSTEM context.
Install-7zip
Install-Git
Install-NodeJs
Install-CMake
Install-Ninja
Install-Python
Install-Go
Install-Ruby
Install-Make
Install-Llvm
Install-Cygwin
Install-Nssm
Install-Scoop-Package perl
# x64-only packages (not needed on ARM64)
if (-not $script:IsARM64) {
Install-Scoop-Package nasm
Install-Scoop-Package mingw -Command gcc
}
# Manual installs (not in Scoop or need special handling)
Install-Pwsh
Install-Bun
Install-Ccache
Install-Rust
Install-Visual-Studio
Install-PdbAddr2line
if ($CI) {
Install-Buildkite
}
if ($Optimize) {
Optimize-System-Needs-Reboot
}