From 7bdee5547f4012095f89d475ecea3cf1d0ebeb91 Mon Sep 17 00:00:00 2001 From: Steve Yoo Date: Fri, 19 Jun 2026 10:54:01 -0400 Subject: [PATCH] Implement Windows install script --- scripts/install-v2/install.ps1 | 407 +++++++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 scripts/install-v2/install.ps1 diff --git a/scripts/install-v2/install.ps1 b/scripts/install-v2/install.ps1 new file mode 100644 index 000000000000..4f20d4ce513f --- /dev/null +++ b/scripts/install-v2/install.ps1 @@ -0,0 +1,407 @@ +# AWS CLI v2 install script for Windows (PowerShell 5.1+). +# +# Defaults to a user-local install under %LOCALAPPDATA%. Use -System for the +# traditional Program Files layout (requires admin). See -Help for details. + +param( + [string] $Version, # specific version to install; empty means "latest" + [switch] $System, # install system-wide (Program Files); requires admin + [switch] $Quiet, # suppress non-essential stdout + [switch] $Help +) + +# Stop on any error so failures don't silently continue. +$ErrorActionPreference = 'Stop' +# PowerShell 5.1 defaults to TLS 1.0; force 1.2 for the CDN. +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# ============================================================================ +# Configuration +# ============================================================================ + +$DownloadBaseUrl = 'https://awscli.amazonaws.com' +$LatestVersionUrl = "$DownloadBaseUrl/v2/version.txt" + +# Fixed install layouts. The MSI's per-user transform places the user-local +# install under %LOCALAPPDATA%\Programs\Amazon\AWSCLIV2. +$SystemInstallDir = Join-Path $env:ProgramFiles 'Amazon\AWSCLIV2' +$UserInstallDir = Join-Path $env:LOCALAPPDATA 'Programs\Amazon\AWSCLIV2' + +$DistributionSource = 'script-exe' +if ($env:AWS_CLI_DISTRIBUTION_SOURCE_OVERRIDE) { + $DistributionSource = $env:AWS_CLI_DISTRIBUTION_SOURCE_OVERRIDE +} +$DownloadQuery = "?src=$DistributionSource" + +$MsiexecSuccessCodes = @(0, 1641, 3010) + +# ============================================================================ +# Output helpers +# ============================================================================ + +$EscRed = [char]27 + '[31m' +$EscYellow = [char]27 + '[33m' +$EscGreen = [char]27 + '[32m' +$EscReset = [char]27 + '[0m' + +function Use-Color { + param([bool] $IsErrStream) + if ($env:NO_COLOR) { return $false } + if ($IsErrStream) { return -not [Console]::IsErrorRedirected } + return -not [Console]::IsOutputRedirected +} + +# Progress/info on stdout, silenced by -Quiet. +function Write-Info { + param([string] $Message) + if (-not $Quiet) { Write-Host $Message } +} + +# Green success on stdout, silenced by -Quiet. +function Write-Success { + param([string] $Message) + if ($Quiet) { return } + if (Use-Color $false) { + Write-Host "$EscGreen$Message$EscReset" + } else { + Write-Host $Message + } +} + +# Yellow warning on stderr; never silenced. +function Write-Warn { + param([string] $Message) + if (Use-Color $true) { + [Console]::Error.WriteLine("${EscYellow}aws-cli installer: warning:${EscReset} $Message") + } else { + [Console]::Error.WriteLine("aws-cli installer: warning: $Message") + } +} + +# Red error on stderr, then exit with the given code; never silenced. +function Throw-Error { + param([int] $Code, [string] $Message) + if (Use-Color $true) { + [Console]::Error.WriteLine("${EscRed}aws-cli installer: error:${EscReset} $Message") + } else { + [Console]::Error.WriteLine("aws-cli installer: error: $Message") + } + exit $Code +} + +# ============================================================================ +# Argument validation +# ============================================================================ + +function Show-Help { + @" +Usage: install.ps1 [-Version ] [-System] [-Quiet] [-Help] + +Install or update the AWS CLI v2. + +Parameters: + -Version Install a specific fully-qualified version. Otherwise + installs the latest release. Downgrades are not + supported. To install a previous version, see: + https://docs.aws.amazon.com/cli/latest/userguide/getting-started-version.html + -System Install system-wide (requires admin). Otherwise installs + under %LOCALAPPDATA%\Programs\Amazon\AWSCLIV2\. + -Quiet Suppress non-essential output. Errors and warnings are + still printed to stderr. + -Help Show this help and exit. +"@ | Write-Host +} + +# Accept only fully-qualified MAJOR.MINOR.PATCH. +function Test-Semver { + param([string] $Value) + return ($Value -match '^\d+\.\d+\.\d+$') +} + +function Validate-Args { + if ($Help) { Show-Help; exit 0 } + if ($Version -and -not (Test-Semver $Version)) { + Throw-Error 2 "-Version must be fully-qualified semver (e.g. 2.27.41), got: $Version" + } +} + +# ============================================================================ +# Pre-install checks +# ============================================================================ + +function Test-Windows { + if ($PSVersionTable.PSVersion.Major -ge 6) { + return $IsWindows + } + return $true +} + +function Assert-Platform { + if (-not (Test-Windows)) { + Throw-Error 1 'unsupported OS. install.ps1 supports only Windows; use install.sh on Linux/macOS.' + } +} + +function Test-IsAdmin { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Assert-SystemPrivilege { + if ($System -and -not (Test-IsAdmin)) { + Throw-Error 1 '-System requires admin privileges. Re-run from an elevated PowerShell, or omit -System for a user-local install.' + } +} + +# ============================================================================ +# Path resolution +# ============================================================================ + +$Script:InstallDir = $null + +function Resolve-InstallPath { + if ($System) { + $Script:InstallDir = $SystemInstallDir + } else { + if (-not $env:LOCALAPPDATA) { + Throw-Error 2 'LOCALAPPDATA is not set; cannot resolve user-local install path.' + } + $Script:InstallDir = $UserInstallDir + } +} + +# ============================================================================ +# Download +# ============================================================================ + +$Script:TempDir = $null # working dir; removed in the finally block + +function New-TempDir { + $Script:TempDir = Join-Path $env:TEMP "aws-cli-install-$([guid]::NewGuid().Guid)" + New-Item -ItemType Directory -Path $Script:TempDir -Force | Out-Null +} + +function Remove-TempDir { + if ($Script:TempDir -and (Test-Path $Script:TempDir)) { + try { + Remove-Item -Recurse -Force $Script:TempDir -ErrorAction Stop + } catch { + Write-Warn "could not remove temp directory: $Script:TempDir" + } + } +} + +# Versioned filename when TargetVersion is known, else the "latest" filename. +function Get-InstallerFilename { + if ($Script:TargetVersion) { + return "AWSCLIV2-$($Script:TargetVersion).msi" + } + return 'AWSCLIV2.msi' +} + +function Get-InstallerUrl { + return "$DownloadBaseUrl/$(Get-InstallerFilename)$DownloadQuery" +} + +function Download-File { + param([string] $Url, [string] $Out) + Invoke-WebRequest -Uri $Url -OutFile $Out -UseBasicParsing -TimeoutSec 60 +} + +function Download-WithRetry { + param([string] $Url, [string] $Out, [string] $Label) + Write-Info "Downloading $Label from $Url" + try { + Download-File -Url $Url -Out $Out + return + } catch { + Write-Warn "download of $Label failed; retrying once" + } + try { + Download-File -Url $Url -Out $Out + } catch { + Throw-Error 1 "failed to download $Label from $Url after one retry: $($_.Exception.Message)" + } +} + +$Script:InstallerPath = $null + +function Download-Installer { + $Script:InstallerPath = Join-Path $Script:TempDir 'AWSCLIV2.msi' + Download-WithRetry -Url (Get-InstallerUrl) -Out $Script:InstallerPath -Label 'AWS CLI installer' +} + +# ============================================================================ +# Signature verification +# ============================================================================ + +function Verify-Installer { + $sig = Get-AuthenticodeSignature -FilePath $Script:InstallerPath + if ($sig.Status -ne 'Valid') { + Throw-Error 1 ("Authenticode signature verification failed for " + + "$($Script:InstallerPath): status is '$($sig.Status)'. " + + 'Refusing to install.') + } + + Write-Success 'MSI Authenticode signature verified.' +} + +# ============================================================================ +# Version resolution +# ============================================================================ + +# Extracts the semver from `aws --version`. +function Read-InstalledVersion { + param([string] $AwsExe) + if (-not (Test-Path $AwsExe)) { return $null } + try { + $out = & $AwsExe --version 2>$null + } catch { + return $null + } + if ($out -match '^aws-cli/(\d+\.\d+\.\d+)') { + return $Matches[1] + } + return $null +} + +function Get-CandidateAwsBinary { + return Join-Path $Script:InstallDir 'aws.exe' +} + +$Script:PreInstallVersion = $null # version present before install +$Script:TargetVersion = $null # version we resolved to install +$Script:PostInstallVersion = $null # version present after install + +function Capture-PreInstallVersion { + $Script:PreInstallVersion = Read-InstalledVersion (Get-CandidateAwsBinary) +} + +# Best-effort fetch of the published "latest" version; $null on any failure. +function Fetch-LatestVersion { + try { + $resp = Invoke-WebRequest -Uri $LatestVersionUrl -UseBasicParsing -TimeoutSec 10 + $text = ([string] $resp.Content).Trim() + if (Test-Semver $text) { return $text } + } catch {} + return $null +} + +# Resolves TargetVersion, short-circuits no-ops. +function Resolve-TargetAndAnnounce { + if ($Version) { + $Script:TargetVersion = $Version + } else { + $Script:TargetVersion = Fetch-LatestVersion + } + + if (-not $Script:TargetVersion -or -not $Script:PreInstallVersion) { + if ($Script:TargetVersion) { + Write-Info "Installing AWS CLI $($Script:TargetVersion)" + } + return + } + + if ($Script:TargetVersion -eq $Script:PreInstallVersion) { + Write-Info "AWS CLI $($Script:TargetVersion) is already installed at $Script:InstallDir; nothing to do." + exit 0 + } + + if ([version]$Script:TargetVersion -lt [version]$Script:PreInstallVersion) { + Write-Info "AWS CLI $($Script:PreInstallVersion) is already installed at $($Script:InstallDir). To install $($Script:TargetVersion), uninstall the existing AWS CLI at $($Script:InstallDir) and try again." + exit 0 + } + + Write-Info "Installing AWS CLI $($Script:PreInstallVersion) -> $($Script:TargetVersion)" +} + +# ============================================================================ +# Install +# ============================================================================ + +function Run-Installer { + $msiFilename = Split-Path $Script:InstallerPath -Leaf + $msiDir = Split-Path $Script:InstallerPath -Parent + + $msiArgs = @('/i', $msiFilename, '/qn', '/norestart') + + if (-not $System) { + $msiArgs += @( + 'TRANSFORMS=:PerUser', + 'MSINEWINSTANCE=1', + 'MSIINSTALLPERUSER=1', + 'ALLUSERS=2' + ) + } + + Write-Info 'Running MSI installer...' + $proc = Start-Process -FilePath 'msiexec' -ArgumentList $msiArgs ` + -WorkingDirectory $msiDir -Wait -PassThru + if ($MsiexecSuccessCodes -notcontains $proc.ExitCode) { + Throw-Error 8 "msiexec failed with exit code $($proc.ExitCode)" + } +} + +# ============================================================================ +# Post-install +# ============================================================================ + +function Verify-InstallRuns { + $awsExe = Get-CandidateAwsBinary + $Script:PostInstallVersion = Read-InstalledVersion $awsExe + if (-not $Script:PostInstallVersion) { + Throw-Error 9 "post-install check failed: '$awsExe --version' did not run successfully." + } +} + +# Writes install-time metadata next to the bundled metadata.json. `aws update` +# reads this back to learn where and how the CLI was installed. No bin_dir +# field - on Windows the MSI manages PATH, there's no symlink dir to record. +function Write-InstallJson { + $installJson = Join-Path $Script:InstallDir 'awscli\data\install.json' + $parent = Split-Path $installJson -Parent + if (-not (Test-Path $parent)) { return } + + $obj = [ordered]@{ + distribution_source = $DistributionSource + install_dir = $Script:InstallDir + script_install = [ordered]@{ + system = [bool]$System + version_resolved = $Script:PostInstallVersion + quiet = [bool]$Quiet + } + } + $obj | ConvertTo-Json -Depth 3 | Set-Content -Path $installJson -Encoding UTF8 +} + +function Invoke-PostInstallChecks { + Verify-InstallRuns + Write-InstallJson + Write-Success "AWS CLI $($Script:PostInstallVersion) installed to $($Script:InstallDir)" + Write-Info "When multiple installs exist, PATH order determines which 'aws' runs." +} + +# ============================================================================ +# Entry point +# ============================================================================ + +try { + # Pre-flight: validate args and environment before touching disk/network. + Validate-Args + Assert-Platform + Assert-SystemPrivilege + Resolve-InstallPath + + # Install: stage, resolve target (may short-circuit), download, run. + New-TempDir + Capture-PreInstallVersion + Resolve-TargetAndAnnounce + Download-Installer + Verify-Installer + Run-Installer + Invoke-PostInstallChecks +} finally { + # Always remove the temp dir, even on error or early exit. + Remove-TempDir +}