<# .SYNOPSIS Install Oracle VirtualBox on Windows, unattended. .DESCRIPTION One typeable line that takes a bare Windows machine to a working `VBoxManage`: iex (irm https://get.optersoft.com/vbox.ps1) The paren form rather than `irm ... | iex` because `|` is unmapped on a SPICE console and on several non-US keyboard layouts, and this script is often the first thing typed into a fresh VM by hand. It resolves the newest stable release from download.virtualbox.org, installs the Visual C++ redistributable VirtualBox requires, installs VirtualBox itself silently, and verifies the result -- reporting whether the CPU can actually run VMs, which on a nested (cloud or VDI) host is the thing most likely to be missing. Idempotent: run it again to upgrade to the newest stable release, or with -Force to reinstall the same version. .NOTES Two things make a hand install fail, and this script exists mostly to get them right: * **The VC++ redistributable is a hard prerequisite.** Without it the installer exits 1 having printed nothing at all; the only trace is a `LaunchConditions. Return value 3` in an MSI log under %TEMP%, with the real message ("needs the Microsoft Visual C++ 2019 Redistributable Package being installed first") buried beside it. When the install fails, this script digs that message out and shows it. * **`VBOX_MSI_INSTALL_PATH` only reaches new processes.** The installer sets it machine-wide, and that is where every consumer -- boxctl included -- looks for `VBoxManage.exe`. A process that started before the install keeps its old environment block forever, so an SSH session on an OpenSSH server that booted first will not see it no matter how many times you reconnect. This script sets it for the current session and says so. The Extension Pack is deliberately not installed: it is under Oracle's PUEL rather than the GPL the base package ships under, so accepting it is a licensing decision and not something a bootstrap script should make on your behalf. Nothing in boxctl needs it. Requires an Administrator PowerShell and internet egress from the guest (download.virtualbox.org and aka.ms). #> [CmdletBinding()] param( # Version to install, e.g. '7.2.14'. Default: whatever the download site # currently names in LATEST-STABLE.TXT. [string] $Version = $env:VBOX_VERSION, # Reinstall even when that version is already present. [switch] $Force, # Skip the VC++ redistributable step (it is already there, or you manage it). [switch] $NoRedist ) $ErrorActionPreference = 'Stop' # PowerShell 5.1 defaults to TLS 1.0, which download.virtualbox.org refuses. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Invoke-WebRequest's progress bar is very slow over a remote shell, and these # are 170 MB downloads. $ProgressPreference = 'SilentlyContinue' function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } function Write-Ok($msg) { Write-Host " ok $msg" -ForegroundColor Green } function Write-Skip($msg) { Write-Host " -- $msg" -ForegroundColor DarkGray } function Write-Warn($msg) { Write-Host " !! $msg" -ForegroundColor Yellow } function Invoke-Native { <# Run a native executable and return its exit code. PowerShell 5.1 with $ErrorActionPreference='Stop' turns a single byte on a native command's stderr into a terminating NativeCommandError, and VBoxManage writes ordinary notices there. The exit code is the only reliable signal. #> param( [Parameter(Mandatory)][string] $Exe, [string[]] $Arguments = @(), [switch] $Quiet ) $previous = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { if ($Quiet) { & $Exe @Arguments 2>&1 | Out-Null } else { & $Exe @Arguments 2>&1 | ForEach-Object { Write-Host " $_" } } return $LASTEXITCODE } finally { $ErrorActionPreference = $previous } } $base = 'https://download.virtualbox.org/virtualbox' function Get-InstallPath { <# Where the installer says VirtualBox lives, or $null. Read from the machine environment rather than $env: -- this process may predate the install, which is exactly the trap described in .NOTES. #> $path = [Environment]::GetEnvironmentVariable('VBOX_MSI_INSTALL_PATH', 'Machine') if ($path -and (Test-Path (Join-Path $path 'VBoxManage.exe'))) { return $path } # A pre-7.0 install, or one whose variable was lost, still lands here. $fallback = Join-Path $env:ProgramFiles 'Oracle\VirtualBox' if (Test-Path (Join-Path $fallback 'VBoxManage.exe')) { return $fallback } return $null } function Use-VBoxPath { <# Make VBoxManage usable in THIS session, without reopening the shell. #> param([Parameter(Mandatory)][string] $Path) $env:VBOX_MSI_INSTALL_PATH = $Path if ($env:PATH -notlike "*$Path*") { $env:PATH = "$Path;$env:PATH" } } function Get-InstalledVersion { param([Parameter(Mandatory)][string] $Path) $exe = Join-Path $Path 'VBoxManage.exe' $previous = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { # `7.2.14r174565` -> `7.2.14` $raw = (& $exe '--version' 2>$null | Select-Object -First 1) if ($LASTEXITCODE -ne 0 -or -not $raw) { return $null } return ($raw -split 'r')[0].Trim() } catch { return $null } finally { $ErrorActionPreference = $previous } } function Show-InstallFailure { <# Surface why the MSI refused. Its exit code is 1 for every cause, and it prints nothing in silent mode, so the log is the only evidence. #> $log = Get-ChildItem $env:TEMP -Filter 'MSI*.LOG' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $log) { return } $reasons = Select-String -Path $log.FullName -Pattern 'needs the|requires|LaunchCondition' | Select-Object -Last 4 if (-not $reasons) { return } Write-Warn "from $($log.Name):" $reasons | ForEach-Object { Write-Host " $($_.Line.Trim())" -ForegroundColor DarkGray } } # ------------------------------------------------------------ prerequisites -- if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this in an Administrator PowerShell -- the installer loads kernel drivers.' } # ------------------------------------------------------------------ version -- Write-Step 'Release' if (-not $Version) { $Version = (Invoke-WebRequest "$base/LATEST-STABLE.TXT" -UseBasicParsing).Content.Trim() Write-Ok "latest stable is $Version" } else { Write-Ok "pinned to $Version" } $existing = Get-InstallPath if ($existing) { $have = Get-InstalledVersion $existing if ($have -eq $Version -and -not $Force) { Use-VBoxPath $existing Write-Skip "VirtualBox $have already installed in $existing" # Fall through to the capability report rather than returning: whether # this host can actually run a VM is the answer people came for, and it # changes underneath an unchanged install every time Hyper-V is toggled. } else { Write-Ok "upgrading from $have" } } # ------------------------------------------------------- VC++ redistributable - $needsInstall = -not ($existing -and (Get-InstalledVersion $existing) -eq $Version -and -not $Force) if ($needsInstall -and -not $NoRedist) { Write-Step 'Visual C++ redistributable' $key = 'HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64' $installed = (Get-ItemProperty $key -ErrorAction SilentlyContinue).Installed if ($installed -eq 1) { Write-Skip "already present -- $((Get-ItemProperty $key).Version)" } else { # The 17 (2022) bundle is binary-compatible with and supersedes the # 2019 one the installer's LaunchCondition asks for. $redist = Join-Path $env:TEMP 'vc_redist.x64.exe' Invoke-WebRequest 'https://aka.ms/vs/17/release/vc_redist.x64.exe' -OutFile $redist -UseBasicParsing $proc = Start-Process $redist -ArgumentList '/install', '/quiet', '/norestart' -Wait -PassThru # 3010 is "success, reboot required" -- fine, the driver install below # does not depend on it having happened yet. if ($proc.ExitCode -notin 0, 3010) { throw "the Visual C++ redistributable failed to install (exit $($proc.ExitCode))" } Write-Ok 'installed' } } # ------------------------------------------------------------------ install -- if ($needsInstall) { Write-Step "VirtualBox $Version" # The filename carries a build number the version string does not, so read # it off the directory index rather than guessing. $index = Invoke-WebRequest "$base/$Version/" -UseBasicParsing $file = $index.Links.href | Where-Object { $_ -match '^VirtualBox-.*-Win\.exe$' } | Select-Object -First 1 if (-not $file) { throw "no Windows installer listed for $Version at $base/$Version/" } $installer = Join-Path $env:TEMP $file Invoke-WebRequest "$base/$Version/$file" -OutFile $installer -UseBasicParsing Write-Ok "$file -- $([math]::Round((Get-Item $installer).Length / 1MB, 1)) MB" # --ignore-reboot so a pending reboot from the redistributable above does # not turn into a surprise restart of somebody's machine. $proc = Start-Process $installer -ArgumentList '--silent', '--ignore-reboot' -Wait -PassThru if ($proc.ExitCode -ne 0) { Show-InstallFailure throw "the VirtualBox installer failed (exit $($proc.ExitCode))" } Write-Ok 'installed' } # ------------------------------------------------------------------- verify -- Write-Step 'Verify' $path = Get-InstallPath if (-not $path) { throw 'VirtualBox reports as installed but VBoxManage.exe is nowhere on disk.' } Use-VBoxPath $path $version = Get-InstalledVersion $path if (-not $version) { throw "VBoxManage.exe in $path does not run." } Write-Ok "VBoxManage $version -- $path" Write-Skip 'VBOX_MSI_INSTALL_PATH is set for this session; new shells pick it up on their own,' Write-Skip 'but a service that started before now (sshd, an agent) keeps its old environment.' # --------------------------------------------------------------- capability -- Write-Step 'Virtualization' $previous = $ErrorActionPreference $ErrorActionPreference = 'Continue' $hostinfo = & (Join-Path $path 'VBoxManage.exe') 'list' 'hostinfo' 2>$null $ErrorActionPreference = $previous $hw = ($hostinfo | Select-String 'Processor supports HW virtualization:\s*(\S+)').Matches.Groups[1].Value $np = ($hostinfo | Select-String 'Processor supports nested paging:\s*(\S+)').Matches.Groups[1].Value if ($hw -eq 'yes') { Write-Ok "HW virtualization: $hw, nested paging: $np" } else { Write-Warn "HW virtualization: $hw -- VirtualBox cannot run 64-bit guests at speed." Write-Warn 'On a VM or VDI desktop the host is not passing VT-x/AMD-V through.' } # Hyper-V, WSL2 and Windows Sandbox all take VT-x for themselves and leave # VirtualBox on its slower emulated backend. Worth saying out loud, because # nothing else reports it and the symptom is "my VMs got slow". # # Detect it by the features that cause it, NOT by Win32_ComputerSystem's # HypervisorPresent: that flag means "this Windows is running under *a* # hypervisor", which is also true of every cloud instance and every VDI # desktop, where it says nothing about who owns VT-x inside the guest. $culprits = @('Microsoft-Hyper-V-Hypervisor', 'VirtualMachinePlatform', 'Containers-DisposableClientVM') | Where-Object { (Get-WindowsOptionalFeature -Online -FeatureName $_ -ErrorAction SilentlyContinue).State -eq 'Enabled' } if ($culprits) { Write-Warn "Enabled: $($culprits -join ', ')" Write-Warn 'These hand VT-x to the Windows hypervisor, and VirtualBox falls back to its' Write-Warn 'Hyper-V backend. It is not merely slower: on a nested host (a cloud instance' Write-Warn 'or a VDI desktop) guests have been seen to WEDGE early in boot -- console' Write-Warn 'frozen, VM "running", host CPU idle. Note that `list hostinfo` still reports' Write-Warn 'HW virtualization as yes in that state, so it is not a test of anything.' Write-Warn 'You can have WSL2 and VirtualBox installed together; you cannot rely on both' Write-Warn 'working at once. To give VT-x back to VirtualBox:' Write-Host ' dism /online /disable-feature /featurename:VirtualMachinePlatform /norestart' -ForegroundColor DarkGray Write-Host ' bcdedit /set hypervisorlaunchtype off # then reboot' -ForegroundColor DarkGray Write-Host ' # The WSL feature and any installed distro survive this; re-enable' -ForegroundColor DarkGray Write-Host ' # VirtualMachinePlatform to get WSL2 back.' -ForegroundColor DarkGray } Write-Host '' Write-Host 'Done. Try:' -ForegroundColor Cyan Write-Host ' VBoxManage list vms' -ForegroundColor DarkGray Write-Host ' uv tool install boxctl; box create alfa' -ForegroundColor DarkGray