From 3ffcd3661f5ce6fed6af5f0fa7dbbf13a7e4b461 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 3 Jul 2025 17:25:17 -0400 Subject: [PATCH 01/30] Add scripts to download aspire-cli Only works with `https://aka.ms/dotnet/9.0/daily/aspire-cli-${os}-${arch}.{zip,.tar.gz}` style urls. --- eng/scripts/get-aspire-cli.ps1 | 127 +++++++++++++++++++++++++++++ eng/scripts/get-aspire-cli.sh | 145 +++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100755 eng/scripts/get-aspire-cli.ps1 create mode 100644 eng/scripts/get-aspire-cli.sh diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 new file mode 100755 index 00000000000..f56168bc73f --- /dev/null +++ b/eng/scripts/get-aspire-cli.ps1 @@ -0,0 +1,127 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' + +# Define supported combinations (script-level constant) +$script:SupportedCombinations = @( + "win-x86", + "win-x64", + "win-arm64", + "linux-x64", + "linux-arm64", + "linux-musl-x64", + "osx-x64", + "osx-arm64" +) + +# Function to detect OS +function Get-OperatingSystem { + if ($IsWindows -or ($PSVersionTable.PSVersion.Major -le 5)) { + return "win" + } + elseif ($IsLinux) { + # Check if it's musl-based (Alpine, etc.) + try { + $lddOutput = & ldd --version 2>&1 | Out-String + if ($lddOutput -match "musl") { + return "linux-musl" + } + else { + return "linux" + } + } + catch { + return "linux" + } + } + elseif ($IsMacOS) { + return "osx" + } + else { + return "unsupported" + } +} + +# Function to detect architecture +function Get-Architecture { + $arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture + + switch ($arch) { + "X64" { return "x64" } + "Arm64" { return "arm64" } + "X86" { return "x86" } + default { return "unsupported" } + } +} + +# Function to validate OS/arch combination +function Test-SupportedCombination { + param( + [string]$OS, + [string]$Architecture + ) + + $combination = "$OS-$Architecture" + + return $combination -in $script:SupportedCombinations +} + +# Main function +function Main { + try { + # Detect OS and architecture + $os = Get-OperatingSystem + $arch = Get-Architecture + + # Check for unsupported OS or architecture + if ($os -eq "unsupported") { + Write-Error "Error: Unsupported operating system: $([System.Environment]::OSVersion.Platform)" + exit 1 + } + + if ($arch -eq "unsupported") { + Write-Error "Error: Unsupported architecture: $([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" + exit 1 + } + + # Validate the combination + if (-not (Test-SupportedCombination -OS $os -Architecture $arch)) { + Write-Error "Error: Unsupported OS/architecture combination: $os-$arch" + Write-Error "Supported combinations: $($script:SupportedCombinations -join ', ')" + exit 1 + } + + # Determine file extension based on OS + $extension = if ($os -eq "win") { "zip" } else { "tar.gz" } + + # Construct the URL + $combination = "$os-$arch" + $url = "https://aka.ms/dotnet/9.0/daily/aspire-cli-$combination.$extension" + + # Output the URL + Write-Host "Downloading from: $url" + + # Download the file + $filename = "aspire-cli-$combination.$extension" + Write-Host "Saving to: $filename" + + try { + Invoke-WebRequest -Uri $url -OutFile $filename -MaximumRedirection 10 + Write-Host "Download completed successfully: $filename" -ForegroundColor Green + } + catch { + Write-Error "Error: Failed to download $url - $($_.Exception.Message)" + exit 1 + } + } + catch { + Write-Error "An error occurred: $($_.Exception.Message)" + exit 1 + } +} + +# Run main function +Main diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh new file mode 100644 index 00000000000..2bb0044da5b --- /dev/null +++ b/eng/scripts/get-aspire-cli.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash + +# get-aspire-cli-url.sh - Download the Aspire CLI for the current platform +# Usage: ./get-aspire-cli.sh + +set -euo pipefail + +# Define supported combinations (global constant) +readonly SUPPORTED_COMBINATIONS=( + "win-x86" + "win-x64" + "win-arm64" + "linux-x64" + "linux-arm64" + "linux-musl-x64" + "osx-x64" + "osx-arm64" +) + +# Function to detect OS +get_os() { + local uname_s + uname_s=$(uname -s) + + case "$uname_s" in + Darwin*) + printf "osx" + ;; + Linux*) + # Check if it's musl-based (Alpine, etc.) + if ldd --version 2>&1 | grep -q musl; then + printf "linux-musl" + else + printf "linux" + fi + ;; + CYGWIN*|MINGW*|MSYS*) + printf "win" + ;; + *) + printf "unsupported" + return 1 + ;; + esac +} + +# Function to detect architecture +get_arch() { + local uname_m + uname_m=$(uname -m) + + case "$uname_m" in + x86_64|amd64) + printf "x64" + ;; + aarch64|arm64) + printf "arm64" + ;; + i386|i686) + printf "x86" + ;; + *) + printf "unsupported" + return 1 + ;; + esac +} + +# Function to validate OS/arch combination +validate_combination() { + local os="$1" + local arch="$2" + local combination="${os}-${arch}" + + local supported_combo + for supported_combo in "${SUPPORTED_COMBINATIONS[@]}"; do + if [[ "$combination" == "$supported_combo" ]]; then + return 0 + fi + done + + return 1 +} + +# Download the Aspire CLI for the current platform +download_aspire_cli() { + local url="$1" + local filename="$2" + + printf "Downloading from: %s\n" "$url" + printf "Saving to: %s\n" "$filename" + + if curl -fsSL -o "$filename" "$url"; then + printf "Download completed successfully: %s\n" "$filename" + return 0 + else + printf "Error: Failed to download %s\n" "$url" >&2 + return 1 + fi +} + +# Main script +main() { + local os arch combination url filename + + # Detect OS and architecture + if ! os=$(get_os); then + printf "Error: Unsupported operating system: %s\n" "$(uname -s)" >&2 + return 1 + fi + + if ! arch=$(get_arch); then + printf "Error: Unsupported architecture: %s\n" "$(uname -m)" >&2 + return 1 + fi + + # Validate the combination + if ! validate_combination "$os" "$arch"; then + combination="${os}-${arch}" + printf "Error: Unsupported OS/architecture combination: %s\n" "$combination" >&2 + printf "Supported combinations: %s\n" "${SUPPORTED_COMBINATIONS[*]}" >&2 + return 1 + fi + + # Construct the URL and filename + combination="${os}-${arch}" + + # Determine file extension based on OS + if [[ "$os" == "win" ]]; then + extension="zip" + else + extension="tar.gz" + fi + + url="https://aka.ms/dotnet/9.0/daily/aspire-cli-${combination}.${extension}" + filename="aspire-cli-${combination}.${extension}" + + # Download the file + download_aspire_cli "$url" "$filename" +} + +# Run main function if script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi From 06552a266a218ba12fac6791037fa2d0e8b15097 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 3 Jul 2025 17:41:01 -0400 Subject: [PATCH 02/30] Update eng/scripts/get-aspire-cli.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- eng/scripts/get-aspire-cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index 2bb0044da5b..ae5d53b84cb 100644 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# get-aspire-cli-url.sh - Download the Aspire CLI for the current platform +# get-aspire-cli.sh - Download the Aspire CLI for the current platform # Usage: ./get-aspire-cli.sh set -euo pipefail From bc855edb5398aa19c480639326abd7b6a291996b Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 02:19:07 -0400 Subject: [PATCH 03/30] update the scripts --- eng/scripts/get-aspire-cli.ps1 | 460 +++++++++++++++++++++++++++------ eng/scripts/get-aspire-cli.sh | 390 ++++++++++++++++++++++++---- 2 files changed, 726 insertions(+), 124 deletions(-) mode change 100644 => 100755 eng/scripts/get-aspire-cli.sh diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index f56168bc73f..f80a834d25f 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -1,127 +1,437 @@ #!/usr/bin/env pwsh [CmdletBinding()] -param() - -$ErrorActionPreference = 'Stop' - -# Define supported combinations (script-level constant) -$script:SupportedCombinations = @( - "win-x86", - "win-x64", - "win-arm64", - "linux-x64", - "linux-arm64", - "linux-musl-x64", - "osx-x64", - "osx-arm64" +param( + [string]$OutputPath = "", + [string]$Channel = "9.0", + [string]$BuildQuality = "daily", + [string]$OS = "", + [string]$Architecture = "", + [switch]$KeepArchive, + [switch]$Help ) +# Global constants +$Script:UserAgent = "get-aspire-cli.ps1/1.0" + +# Show help if requested +if ($Help) { + Write-Host @" +Aspire CLI Download Script + +DESCRIPTION: + Downloads and unpacks the Aspire CLI for the current platform from the specified version and build quality. + +PARAMETERS: + -OutputPath Directory to unpack the CLI (default: aspire-cli directory under current directory) + -Channel Channel of the Aspire CLI to download (default: 9.0) + -BuildQuality Build quality to download (default: daily) + -OS Operating system (default: auto-detect) + -Architecture Architecture (default: auto-detect) + -KeepArchive Keep downloaded archive files and temporary directory after installation + -Help Show this help message + +EXAMPLES: + .\get-aspire-cli.ps1 + .\get-aspire-cli.ps1 -OutputPath "C:\temp" + .\get-aspire-cli.ps1 -Channel "8.0" -BuildQuality "release" + .\get-aspire-cli.ps1 -OS "linux" -Architecture "x64" + .\get-aspire-cli.ps1 -KeepArchive + .\get-aspire-cli.ps1 -Help + +"@ + exit 0 +} + +function Say-Verbose($str) { + try { + Write-Verbose $str + } + catch { + # Some platforms cannot utilize Write-Verbose (Azure Functions, for instance). Fall back to Write-Output + Write-Output $str + } +} + # Function to detect OS function Get-OperatingSystem { - if ($IsWindows -or ($PSVersionTable.PSVersion.Major -le 5)) { - return "win" + if ($PSVersionTable.PSVersion.Major -ge 6) { + if ($IsWindows) { + return "win" + } + elseif ($IsLinux) { + try { + $lddOutput = & ldd --version 2>&1 | Out-String + if ($lddOutput -match "musl") { + return "linux-musl" + } + else { + return "linux" + } + } + catch { + return "linux" + } + } + elseif ($IsMacOS) { + return "osx" + } + else { + return "unsupported" + } } - elseif ($IsLinux) { - # Check if it's musl-based (Alpine, etc.) - try { - $lddOutput = & ldd --version 2>&1 | Out-String - if ($lddOutput -match "musl") { - return "linux-musl" + else { + # PowerShell 5.1 and earlier + if ($env:OS -eq "Windows_NT") { + return "win" + } + else { + $platform = [System.Environment]::OSVersion.Platform + if ($platform -eq 4 -or $platform -eq 6) { + return "unix" + } + elseif ($platform -eq 128) { + return "osx" } else { - return "linux" + return "unsupported" + } + } + } +} + +# Taken from dotnet-install.ps1 and enhanced for cross-platform support +function Get-Machine-Architecture() { + Say-Verbose $MyInvocation + + # On Windows PowerShell, use environment variables + if ($PSVersionTable.PSVersion.Major -lt 6 -or $IsWindows) { + # On PS x86, PROCESSOR_ARCHITECTURE reports x86 even on x64 systems. + # To get the correct architecture, we need to use PROCESSOR_ARCHITEW6432. + # PS x64 doesn't define this, so we fall back to PROCESSOR_ARCHITECTURE. + # Possible values: amd64, x64, x86, arm64, arm + if ( $null -ne $ENV:PROCESSOR_ARCHITEW6432 ) { + return $ENV:PROCESSOR_ARCHITEW6432 + } + + try { + if ( ((Get-CimInstance -ClassName CIM_OperatingSystem).OSArchitecture) -like "ARM*") { + if ( [Environment]::Is64BitOperatingSystem ) { + return "arm64" + } + return "arm" + } + } + catch { + # Machine doesn't support Get-CimInstance + } + + if ($null -ne $ENV:PROCESSOR_ARCHITECTURE) { + return $ENV:PROCESSOR_ARCHITECTURE + } + } + + # For PowerShell 6+ on Unix systems, use .NET runtime information + if ($PSVersionTable.PSVersion.Major -ge 6) { + try { + $runtimeArch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture + switch ($runtimeArch) { + 'X64' { return "x64" } + 'X86' { return "x86" } + 'Arm64' { return "arm64" } + default { + Say-Verbose "Unknown runtime architecture: $runtimeArch" + # Fall back to uname if available + if (Get-Command uname -ErrorAction SilentlyContinue) { + $unameArch = & uname -m + switch ($unameArch) { + { $_ -in @('x86_64', 'amd64') } { return "x64" } + { $_ -in @('aarch64', 'arm64') } { return "arm64" } + { $_ -in @('i386', 'i686') } { return "x86" } + default { + Say-Verbose "Unknown uname architecture: $unameArch" + return "x64" # Default fallback + } + } + } + return "x64" # Default fallback + } } } catch { - return "linux" + Write-Warning "Failed to get runtime architecture: $($_.Exception.Message)" + # Final fallback - assume x64 + return "x64" } } - elseif ($IsMacOS) { - return "osx" + + # Final fallback for older PowerShell versions + return "x64" +} + +# taken from dotnet-install.ps1 +function Get-CLIArchitecture-From-Architecture([string]$Architecture) { + Say-Verbose $MyInvocation + + if ($Architecture -eq "") { + $Architecture = Get-Machine-Architecture } - else { - return "unsupported" + + switch ($Architecture.ToLowerInvariant()) { + { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } + { $_ -eq "x86" } { return "x86" } + { $_ -eq "arm" } { return "arm" } + { $_ -eq "arm64" } { return "arm64" } + default { throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" } } } -# Function to detect architecture -function Get-Architecture { - $arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture +# Common function for web requests with centralized configuration +function Invoke-SecureWebRequest { + param( + [string]$Uri, + [string]$OutFile, + [int]$TimeoutSec = 60, + [int]$OperationTimeoutSec = 30, + [string]$UserAgent = $Script:UserAgent, + [int]$MaxRetries = 5 + ) - switch ($arch) { - "X64" { return "x64" } - "Arm64" { return "arm64" } - "X86" { return "x86" } - default { return "unsupported" } + try { + if ($PSVersionTable.PSVersion.Major -ge 6) { + Invoke-WebRequest -Uri $Uri -OutFile $OutFile -SslProtocol Tls12,Tls13 -MaximumRedirection 10 -TimeoutSec $TimeoutSec -OperationTimeoutSeconds $OperationTimeoutSec -UserAgent $UserAgent -MaximumRetryCount $MaxRetries + } + else { + # PowerShell 5: Set TLS 1.2/1.3 globally and do not use -SslProtocol + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 + Invoke-WebRequest -Uri $Uri -OutFile $OutFile -MaximumRedirection 10 -TimeoutSec $TimeoutSec -UserAgent $UserAgent + } + } + catch { + throw $_.Exception } } -# Function to validate OS/arch combination -function Test-SupportedCombination { +# Download the Aspire CLI for the current platform +function Invoke-AspireCliDownload { param( - [string]$OS, - [string]$Architecture + [string]$Url, + [string]$Filename ) - $combination = "$OS-$Architecture" + Write-Host "Downloading from: $Url" - return $combination -in $script:SupportedCombinations + # Use temporary file and move on success to avoid partial downloads + $tempFilename = "$Filename.tmp" + + try { + Invoke-SecureWebRequest -Uri $Url -OutFile $tempFilename + + # Check if the downloaded file is actually HTML (error page) instead of the expected archive + $fileContent = Get-Content $tempFilename -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ($fileContent -and ($fileContent.StartsWith("' } else { Get-CLIArchitecture-From-Architecture $Architecture } + if ($detectedArch -eq "unsupported") { + throw "Unsupported architecture. Current architecture: $([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" } + $os = $detectedOS + $arch = $detectedArch + + # Construct the runtime identifier + $runtimeIdentifier = "$os-$arch" + # Determine file extension based on OS $extension = if ($os -eq "win") { "zip" } else { "tar.gz" } - # Construct the URL - $combination = "$os-$arch" - $url = "https://aka.ms/dotnet/9.0/daily/aspire-cli-$combination.$extension" + # Construct the URLs + $url = "https://aka.ms/dotnet/$Channel/$BuildQuality/aspire-cli-$runtimeIdentifier.$extension" + $checksumUrl = "$url.sha512" - # Output the URL - Write-Host "Downloading from: $url" - - # Download the file - $filename = "aspire-cli-$combination.$extension" - Write-Host "Saving to: $filename" + $filename = Join-Path $tempDir "aspire-cli-$runtimeIdentifier.$extension" + $checksumFilename = Join-Path $tempDir "aspire-cli-$runtimeIdentifier.$extension.sha512" try { - Invoke-WebRequest -Uri $url -OutFile $filename -MaximumRedirection 10 - Write-Host "Download completed successfully: $filename" -ForegroundColor Green + Invoke-AspireCliDownload -Url $url -Filename $filename + Invoke-ChecksumDownload -Url $checksumUrl -Filename $checksumFilename + Test-FileChecksum -ArchiveFile $filename -ChecksumFile $checksumFilename + + Say-Verbose "Successfully downloaded and validated: $filename" + + # Unpack the archive + Expand-AspireCliArchive -ArchiveFile $filename -DestinationPath $OutputPath -OS $os + + $cliExe = if ($os -eq "win") { "aspire.exe" } else { "aspire" } + $cliPath = Join-Path $OutputPath $cliExe + + Write-Host "Aspire CLI successfully unpacked to: $cliPath" -ForegroundColor Green } - catch { - Write-Error "Error: Failed to download $url - $($_.Exception.Message)" - exit 1 + finally { + # Clean up temporary directory and downloaded files + if (Test-Path $tempDir -ErrorAction SilentlyContinue) { + if (-not $KeepArchive) { + try { + Say-Verbose "Cleaning up temporary files..." + Remove-Item $tempDir -Recurse -Force -ErrorAction Stop + } + catch { + Write-Warning "Failed to clean up temporary directory: $tempDir - $($_.Exception.Message)" + } + } + else { + Write-Host "Archive files kept in: $tempDir" -ForegroundColor Yellow + } + } } } catch { - Write-Error "An error occurred: $($_.Exception.Message)" - exit 1 + Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red + throw } } -# Run main function -Main +# Run main function and handle exit code +try { + Main + exit 0 +} +catch { + exit 1 +} diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh old mode 100644 new mode 100755 index ae5d53b84cb..deca3ca003b --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -1,21 +1,106 @@ #!/usr/bin/env bash -# get-aspire-cli.sh - Download the Aspire CLI for the current platform -# Usage: ./get-aspire-cli.sh +# get-aspire-cli.sh - Download and unpack the Aspire CLI for the current platform +# Usage: ./get-aspire-cli.sh [OPTIONS] set -euo pipefail -# Define supported combinations (global constant) -readonly SUPPORTED_COMBINATIONS=( - "win-x86" - "win-x64" - "win-arm64" - "linux-x64" - "linux-arm64" - "linux-musl-x64" - "osx-x64" - "osx-arm64" -) +# Global constants +readonly USER_AGENT="get-aspire-cli.sh/1.0" + +# Default values +OUTPUT_PATH="" +CHANNEL="9.0" +BUILD_QUALITY="daily" +OS="" +ARCH="" +SHOW_HELP=false +VERBOSE=false +KEEP_ARCHIVE=false + +# Function to show help +show_help() { + cat << 'EOF' +Aspire CLI Download Script + +DESCRIPTION: + Downloads and unpacks the Aspire CLI for the current platform from the specified channel and build quality. + +USAGE: + ./get-aspire-cli.sh [OPTIONS] + +OPTIONS: + -o, --output-path PATH Directory to unpack the CLI (default: aspire-cli directory under current directory) + -c, --channel CHANNEL Channel of the Aspire CLI to download (default: 9.0) + -q, --quality QUALITY Build quality to download (default: daily) + --os OS Operating system (default: auto-detect) + --architecture ARCH Architecture (default: auto-detect) + -k, --keep-archive Keep downloaded archive files and temporary directory after installation + -v, --verbose Enable verbose output + -h, --help Show this help message + +EXAMPLES: + ./get-aspire-cli.sh + ./get-aspire-cli.sh --output-path "/tmp" + ./get-aspire-cli.sh --channel "8.0" --quality "release" + ./get-aspire-cli.sh --os "linux" --architecture "x64" + ./get-aspire-cli.sh --keep-archive + ./get-aspire-cli.sh --help + +EOF +} + +# Function to parse command line arguments +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + -o|--output-path) + OUTPUT_PATH="$2" + shift 2 + ;; + -c|--channel) + CHANNEL="$2" + shift 2 + ;; + -q|--quality) + BUILD_QUALITY="$2" + shift 2 + ;; + --os) + OS="$2" + shift 2 + ;; + --architecture) + ARCH="$2" + shift 2 + ;; + -k|--keep-archive) + KEEP_ARCHIVE=true + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -h|--help) + SHOW_HELP=true + shift + ;; + *) + printf "Error: Unknown option '%s'\n" "$1" >&2 + printf "Use --help for usage information.\n" >&2 + exit 1 + ;; + esac + done +} + +# Function for verbose logging +say_verbose() { + if [[ "$VERBOSE" == true ]]; then + printf "%s\n" "$1" + fi +} # Function to detect OS get_os() { @@ -66,64 +151,235 @@ get_arch() { esac } -# Function to validate OS/arch combination -validate_combination() { - local os="$1" - local arch="$2" - local combination="${os}-${arch}" - - local supported_combo - for supported_combo in "${SUPPORTED_COMBINATIONS[@]}"; do - if [[ "$combination" == "$supported_combo" ]]; then - return 0 - fi - done +# Common function for HTTP requests with centralized configuration +secure_curl() { + local url="$1" + local output_file="$2" + local timeout="${3:-300}" + local user_agent="${4:-$USER_AGENT}" + local max_retries="${5:-5}" - return 1 + # FIXME: --cert-status is failiing with `curl: (91) No OCSP response received` + curl \ + --fail \ + --silent \ + --show-error \ + --location \ + --tlsv1.2 \ + --max-time "$timeout" \ + --user-agent "$user_agent" \ + --max-redirs 10 \ + --retry "$max_retries" \ + --retry-delay 1 \ + --retry-max-time 60 \ + --output "$output_file" \ + "$url" } # Download the Aspire CLI for the current platform download_aspire_cli() { local url="$1" local filename="$2" + local temp_filename="${filename}.tmp" printf "Downloading from: %s\n" "$url" - printf "Saving to: %s\n" "$filename" - if curl -fsSL -o "$filename" "$url"; then - printf "Download completed successfully: %s\n" "$filename" + # Use temporary file and move on success to avoid partial downloads + if secure_curl "$url" "$temp_filename" 300; then + # Check if the downloaded file is actually HTML (error page) instead of the expected archive + if file "$temp_filename" | grep -q "HTML document"; then + printf "Error: Downloaded file appears to be an HTML error page instead of the expected archive.\n" >&2 + printf "The URL may be incorrect or the file may not be available: %s\n" "$url" >&2 + rm -f "$temp_filename" + return 1 + fi + + mv "$temp_filename" "$filename" return 0 else + # Clean up temporary file on failure + rm -f "$temp_filename" printf "Error: Failed to download %s\n" "$url" >&2 return 1 fi } -# Main script -main() { - local os arch combination url filename +# Download the checksum file +download_checksum() { + local url="$1" + local filename="$2" + local temp_filename="${filename}.tmp" - # Detect OS and architecture - if ! os=$(get_os); then - printf "Error: Unsupported operating system: %s\n" "$(uname -s)" >&2 + say_verbose "Downloading checksum from: $url" + + # Use temporary file and move on success to avoid partial downloads + if secure_curl "$url" "$temp_filename" 60; then + mv "$temp_filename" "$filename" + return 0 + else + # Clean up temporary file on failure + rm -f "$temp_filename" + printf "Error: Failed to download checksum %s\n" "$url" >&2 return 1 fi +} + +# Validate the checksum of the downloaded file +validate_checksum() { + local archive_file="$1" + local checksum_file="$2" - if ! arch=$(get_arch); then - printf "Error: Unsupported architecture: %s\n" "$(uname -m)" >&2 + # Check if sha512sum command is available + if ! command -v sha512sum >/dev/null 2>&1; then + printf "Error: sha512sum command not found. Please install it to validate checksums.\n" >&2 return 1 fi - # Validate the combination - if ! validate_combination "$os" "$arch"; then - combination="${os}-${arch}" - printf "Error: Unsupported OS/architecture combination: %s\n" "$combination" >&2 - printf "Supported combinations: %s\n" "${SUPPORTED_COMBINATIONS[*]}" >&2 + # Read the expected checksum from the file + local expected_checksum + expected_checksum=$(cat "$checksum_file" | tr -d '\n' | tr -d '\r' | tr '[:upper:]' '[:lower:]') + + # Check if the checksum file contains HTML (error page) instead of a checksum + if [[ "$expected_checksum" == *"&2 + printf "Expected checksum file content, but got: %s...\n" "${expected_checksum:0:100}" >&2 + return 1 + fi + + # Calculate the actual checksum + local actual_checksum + actual_checksum=$(sha512sum "$archive_file" | cut -d' ' -f1) + + # Compare checksums + if [[ "$expected_checksum" == "$actual_checksum" ]]; then + return 0 + else + # Limit expected checksum display to 128 characters for output + local expected_checksum_display + if [[ ${#expected_checksum} -gt 128 ]]; then + expected_checksum_display="${expected_checksum:0:128}" + else + expected_checksum_display="$expected_checksum" + fi + + printf "Error: Checksum validation failed for %s with checksum from %s!\n" "$archive_file" "$checksum_file" >&2 + printf "Expected: %s\n" "$expected_checksum_display" >&2 + printf "Actual: %s\n" "$actual_checksum" >&2 return 1 fi +} + +# Function to expand/unpack archive files +expand_aspire_cli_archive() { + local archive_file="$1" + local destination_path="$2" + local os="$3" - # Construct the URL and filename - combination="${os}-${arch}" + say_verbose "Unpacking archive to: $destination_path" + + # Create destination directory if it doesn't exist + if [[ ! -d "$destination_path" ]]; then + mkdir -p "$destination_path" + fi + + if [[ "$os" == "win" ]]; then + # Use unzip for ZIP files + if ! command -v unzip >/dev/null 2>&1; then + printf "Error: unzip command not found. Please install unzip to extract ZIP files.\n" >&2 + return 1 + fi + + if ! unzip -o "$archive_file" -d "$destination_path"; then + printf "Error: Failed to extract ZIP archive: %s\n" "$archive_file" >&2 + return 1 + fi + else + # Use tar for tar.gz files on Unix systems + if ! command -v tar >/dev/null 2>&1; then + printf "Error: tar command not found. Please install tar to extract tar.gz files.\n" >&2 + return 1 + fi + + if ! tar -xzf "$archive_file" -C "$destination_path"; then + printf "Error: Failed to extract tar.gz archive: %s\n" "$archive_file" >&2 + return 1 + fi + fi + + say_verbose "Successfully unpacked archive" +} + +# Main script +main() { + local os arch runtimeIdentifier url filename checksum_url checksum_filename + local cli_exe cli_path + + # Parse command line arguments + parse_args "$@" + + # Show help if requested + if [[ "$SHOW_HELP" == true ]]; then + show_help + exit 0 + fi + + # Set default OutputPath if empty + if [[ -z "$OUTPUT_PATH" ]]; then + OUTPUT_PATH="$(pwd)/aspire-cli" + fi + + # Create a temporary directory for downloads + local temp_dir + temp_dir=$(mktemp -d -t aspire-cli-download-XXXXXXXX) + + if [[ ! -d "$temp_dir" ]]; then + say_verbose "Creating temporary directory: $temp_dir" + if ! mkdir -p "$temp_dir"; then + printf "Error: Failed to create temporary directory: %s\n" "$temp_dir" >&2 + return 1 + fi + fi + + # Cleanup function for temporary directory + cleanup() { + if [[ -n "${temp_dir:-}" ]] && [[ -d "$temp_dir" ]]; then + if [[ "$KEEP_ARCHIVE" != true ]]; then + say_verbose "Cleaning up temporary files..." + rm -rf "$temp_dir" || printf "Warning: Failed to clean up temporary directory: %s\n" "$temp_dir" >&2 + else + printf "Archive files kept in: %s\n" "$temp_dir" + fi + fi + } + + # Set trap for cleanup on exit + trap cleanup EXIT + + # Detect OS and architecture if not provided + local detected_os detected_arch + + if [[ -z "$OS" ]]; then + if ! detected_os=$(get_os); then + printf "Error: Unsupported operating system. Current platform: %s\n" "$(uname -s)" >&2 + return 1 + fi + os="$detected_os" + else + os="$OS" + fi + + if [[ -z "$ARCH" ]]; then + if ! detected_arch=$(get_arch); then + printf "Error: Unsupported architecture. Current architecture: %s\n" "$(uname -m)" >&2 + return 1 + fi + arch="$detected_arch" + else + arch="$ARCH" + fi + + # Construct the runtime identifier + runtimeIdentifier="${os}-${arch}" # Determine file extension based on OS if [[ "$os" == "win" ]]; then @@ -132,14 +388,50 @@ main() { extension="tar.gz" fi - url="https://aka.ms/dotnet/9.0/daily/aspire-cli-${combination}.${extension}" - filename="aspire-cli-${combination}.${extension}" + # Construct the URLs + url="https://aka.ms/dotnet/${CHANNEL}/${BUILD_QUALITY}/aspire-cli-${runtimeIdentifier}.${extension}" + checksum_url="${url}.sha512" + + filename="${temp_dir}/aspire-cli-${runtimeIdentifier}.${extension}" + checksum_filename="${temp_dir}/aspire-cli-${runtimeIdentifier}.${extension}.sha512" + + # Download the archive file + if ! download_aspire_cli "$url" "$filename"; then + return 1 + fi + + # Download the checksum file and validate + if ! download_checksum "$checksum_url" "$checksum_filename"; then + return 1 + fi - # Download the file - download_aspire_cli "$url" "$filename" + # Validate the checksum + if ! validate_checksum "$filename" "$checksum_filename"; then + return 1 + fi + + say_verbose "Successfully downloaded and validated: $filename" + + # Unpack the archive + if ! expand_aspire_cli_archive "$filename" "$OUTPUT_PATH" "$os"; then + return 1 + fi + + if [[ "$os" == "win" ]]; then + cli_exe="aspire.exe" + else + cli_exe="aspire" + fi + cli_path="${OUTPUT_PATH}/${cli_exe}" + + printf "Aspire CLI successfully unpacked to: %s\n" "$cli_path" } # Run main function if script is executed directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" + if main "$@"; then + exit 0 + else + exit 1 + fi fi From 8c4edc45255cc63d6599025d70c4596f7bad8bb6 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 02:35:21 -0400 Subject: [PATCH 04/30] Update eng/scripts/get-aspire-cli.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- eng/scripts/get-aspire-cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index deca3ca003b..46b2ed3e8c1 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -159,7 +159,7 @@ secure_curl() { local user_agent="${4:-$USER_AGENT}" local max_retries="${5:-5}" - # FIXME: --cert-status is failiing with `curl: (91) No OCSP response received` + # FIXME: --cert-status is failing with `curl: (91) No OCSP response received` curl \ --fail \ --silent \ From 372f89916e7a215d869f4283cd54756e32ff9db8 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 02:35:29 -0400 Subject: [PATCH 05/30] Update eng/scripts/get-aspire-cli.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- eng/scripts/get-aspire-cli.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index f80a834d25f..3233dd9bf29 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -88,7 +88,7 @@ function Get-OperatingSystem { else { $platform = [System.Environment]::OSVersion.Platform if ($platform -eq 4 -or $platform -eq 6) { - return "unix" + return "linux" } elseif ($platform -eq 128) { return "osx" From 0820d14bce08cb5de5b404c6db3cd1838ef79da0 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 02:43:30 -0400 Subject: [PATCH 06/30] add a README.md --- eng/scripts/README.md | 161 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 eng/scripts/README.md diff --git a/eng/scripts/README.md b/eng/scripts/README.md new file mode 100644 index 00000000000..632ec29c1b9 --- /dev/null +++ b/eng/scripts/README.md @@ -0,0 +1,161 @@ +# Aspire CLI Download Scripts + +This directory contains scripts to download and install the Aspire CLI for different platforms. + +## Scripts + +- **`get-aspire-cli.sh`** - Bash script for Unix-like systems (Linux, macOS) +- **`get-aspire-cli.ps1`** - PowerShell script for cross-platform use (Windows, Linux, macOS) + +## Current Limitations + +⚠️ **Important**: Currently, only the following combination works: +- **Channel**: `9.0` +- **Build Quality**: `daily` + +Other channel/quality combinations are not yet available through the download URLs. + +## Parameters + +### Bash Script (`get-aspire-cli.sh`) + +| Parameter | Short | Description | Default | +|-----------|-------|-------------|---------| +| `--output-path` | `-o` | Directory to unpack the CLI | `./aspire-cli` | +| `--channel` | `-c` | Channel of the Aspire CLI to download | `9.0` | +| `--quality` | `-q` | Build quality to download | `daily` | +| `--os` | | Operating system (auto-detected if not specified) | auto-detect | +| `--architecture` | | Architecture (auto-detected if not specified) | auto-detect | +| `--keep-archive` | `-k` | Keep downloaded archive files after installation | `false` | +| `--verbose` | `-v` | Enable verbose output | `false` | +| `--help` | `-h` | Show help message | | + +### PowerShell Script (`get-aspire-cli.ps1`) + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `-OutputPath` | Directory to unpack the CLI | `./aspire-cli` | +| `-Channel` | Channel of the Aspire CLI to download | `9.0` | +| `-BuildQuality` | Build quality to download | `daily` | +| `-OS` | Operating system (auto-detected if not specified) | auto-detect | +| `-Architecture` | Architecture (auto-detected if not specified) | auto-detect | +| `-KeepArchive` | Keep downloaded archive files after installation | `false` | +| `-Help` | Show help message | | + +## Output Path Parameter + +The `--output-path` (bash) or `-OutputPath` (PowerShell) parameter specifies where the Aspire CLI will be unpacked: + +- **Default behavior**: Creates an `aspire-cli` directory in the current working directory +- **Custom path**: You can specify any directory path where you want the CLI installed +- **Directory creation**: The scripts will automatically create the directory if it doesn't exist +- **Final location**: The CLI executable will be placed directly in the specified directory as: + - `aspire` (on Linux/macOS) + - `aspire.exe` (on Windows) + +### Example Output Paths + +```bash +# Default - creates ./aspire-cli/aspire +./get-aspire-cli.sh + +# Custom path - creates /usr/local/bin/aspire +./get-aspire-cli.sh --output-path "/usr/local/bin" + +# Relative path - creates ../tools/aspire-cli/aspire +./get-aspire-cli.sh --output-path "../tools/aspire-cli" +``` + +## Usage Examples + +### Bash Script Examples + +```bash +# Basic usage - download to default location (./aspire-cli) +./get-aspire-cli.sh + +# Specify custom output directory +./get-aspire-cli.sh --output-path "/usr/local/bin" + +# Download with verbose output +./get-aspire-cli.sh --verbose + +# Keep the downloaded archive files for inspection +./get-aspire-cli.sh --keep-archive + +# Force specific OS and architecture (useful for cross-compilation scenarios) +./get-aspire-cli.sh --os "linux" --architecture "x64" + +# Combine multiple options +./get-aspire-cli.sh --output-path "/tmp/aspire" --verbose --keep-archive +``` + +### PowerShell Script Examples + +```powershell +# Basic usage - download to default location (./aspire-cli) +.\get-aspire-cli.ps1 + +# Specify custom output directory +.\get-aspire-cli.ps1 -OutputPath "C:\Tools\Aspire" + +# Download with verbose output +.\get-aspire-cli.ps1 -Verbose + +# Keep the downloaded archive files for inspection +.\get-aspire-cli.ps1 -KeepArchive + +# Force specific OS and architecture +.\get-aspire-cli.ps1 -OS "win" -Architecture "x64" + +# Combine multiple options +.\get-aspire-cli.ps1 -OutputPath "C:\temp\aspire" -Verbose -KeepArchive +``` + +## Supported Platforms + +### Operating Systems +- **Windows** (`win`) +- **Linux** (`linux`) +- **Linux with musl** (`linux-musl`) - for Alpine Linux and similar distributions +- **macOS** (`osx`) + +### Architectures +- **x64** (`x64`) - Intel/AMD 64-bit +- **ARM64** (`arm64`) - ARM 64-bit (Apple Silicon, ARM servers) +- **x86** (`x86`) - Intel/AMD 32-bit (limited support) + +## Features + +### Automatic Detection +- **Platform Detection**: Automatically detects your operating system and architecture +- **Runtime Validation**: Chooses the correct archive format (ZIP for Windows, tar.gz for Unix) + +## Troubleshooting + +### Common Issues + +1. **"Unsupported platform" error**: Your OS/architecture combination may not be supported +2. **"Failed to download" error**: Check your internet connection and firewall settings +3. **"Checksum validation failed" error**: The download may have been corrupted; try again +4. **"HTML error page" error**: The requested version/platform combination may not be available + +### Getting Help + +Run the scripts with the help flag to see detailed usage information: + +```bash +./get-aspire-cli.sh --help +``` + +```powershell +.\get-aspire-cli.ps1 -Help +``` + +## Contributing + +When modifying these scripts, ensure: +- Both scripts maintain feature parity where possible +- Error handling is comprehensive and user-friendly +- Platform detection logic is robust +- Security best practices are followed for downloads and file handling From 8f4d0bc887b5d657a7191644d2b84330f3d658db Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 02:50:41 -0400 Subject: [PATCH 07/30] update README.md --- eng/scripts/README.md | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/eng/scripts/README.md b/eng/scripts/README.md index 632ec29c1b9..45c2e66331a 100644 --- a/eng/scripts/README.md +++ b/eng/scripts/README.md @@ -112,24 +112,20 @@ The `--output-path` (bash) or `-OutputPath` (PowerShell) parameter specifies whe .\get-aspire-cli.ps1 -OutputPath "C:\temp\aspire" -Verbose -KeepArchive ``` -## Supported Platforms - -### Operating Systems -- **Windows** (`win`) -- **Linux** (`linux`) -- **Linux with musl** (`linux-musl`) - for Alpine Linux and similar distributions -- **macOS** (`osx`) - -### Architectures -- **x64** (`x64`) - Intel/AMD 64-bit -- **ARM64** (`arm64`) - ARM 64-bit (Apple Silicon, ARM servers) -- **x86** (`x86`) - Intel/AMD 32-bit (limited support) - -## Features - -### Automatic Detection -- **Platform Detection**: Automatically detects your operating system and architecture -- **Runtime Validation**: Chooses the correct archive format (ZIP for Windows, tar.gz for Unix) +## Supported Runtime Identifiers + +The following runtime identifier (RID) combinations are available: + +| Runtime Identifier | AOT Support | +|-------------------|-------------| +| `win-x64` | ✅ | +| `win-arm64` | ✅ | +| `win-x86` | ❌ | +| `linux-x64` | ✅ | +| `linux-arm64` | ❌ | +| `linux-musl-x64` | ❌ | +| `osx-x64` | ✅ | +| `osx-arm64` | ✅ | ## Troubleshooting From 062183427ef23a07146dea1922075aa3b09b26cf Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 14:31:47 -0400 Subject: [PATCH 08/30] address review feedback from @ eerhardt --- eng/scripts/README.md | 4 +++- eng/scripts/get-aspire-cli.sh | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/eng/scripts/README.md b/eng/scripts/README.md index 45c2e66331a..8b787feccca 100644 --- a/eng/scripts/README.md +++ b/eng/scripts/README.md @@ -116,7 +116,7 @@ The `--output-path` (bash) or `-OutputPath` (PowerShell) parameter specifies whe The following runtime identifier (RID) combinations are available: -| Runtime Identifier | AOT Support | +| Runtime Identifier | AOTed | |-------------------|-------------| | `win-x64` | ✅ | | `win-arm64` | ✅ | @@ -127,6 +127,8 @@ The following runtime identifier (RID) combinations are available: | `osx-x64` | ✅ | | `osx-arm64` | ✅ | +The non-aot binaries are self-contained executables. + ## Troubleshooting ### Common Issues diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index 46b2ed3e8c1..7aa35e9aac9 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -34,7 +34,7 @@ OPTIONS: -c, --channel CHANNEL Channel of the Aspire CLI to download (default: 9.0) -q, --quality QUALITY Build quality to download (default: daily) --os OS Operating system (default: auto-detect) - --architecture ARCH Architecture (default: auto-detect) + --arch ARCH Architecture (default: auto-detect) -k, --keep-archive Keep downloaded archive files and temporary directory after installation -v, --verbose Enable verbose output -h, --help Show this help message @@ -42,8 +42,8 @@ OPTIONS: EXAMPLES: ./get-aspire-cli.sh ./get-aspire-cli.sh --output-path "/tmp" - ./get-aspire-cli.sh --channel "8.0" --quality "release" - ./get-aspire-cli.sh --os "linux" --architecture "x64" + ./get-aspire-cli.sh --channel "9.0" --quality "release" + ./get-aspire-cli.sh --os "linux" --arch "x64" ./get-aspire-cli.sh --keep-archive ./get-aspire-cli.sh --help @@ -70,7 +70,7 @@ parse_args() { OS="$2" shift 2 ;; - --architecture) + --arch) ARCH="$2" shift 2 ;; @@ -166,6 +166,7 @@ secure_curl() { --show-error \ --location \ --tlsv1.2 \ + --tls-max 1.3 \ --max-time "$timeout" \ --user-agent "$user_agent" \ --max-redirs 10 \ From c8dccb87d49e0cf62db65c5584dae15fc5af3679 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 14:58:29 -0400 Subject: [PATCH 09/30] Address review feedback from @ blowdart and use mime-type to detect whether we got redirected to a html page --- eng/scripts/get-aspire-cli.ps1 | 121 +++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 6 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index 3233dd9bf29..9b1ecac6be2 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -186,6 +186,51 @@ function Get-CLIArchitecture-From-Architecture([string]$Architecture) { } } +# Helper function to extract Content-Type from response headers +function Get-ContentTypeFromHeaders { + param( + [object]$Headers, + [bool]$IsModernPowerShell = $true + ) + + if (-not $Headers) { + return "" + } + + try { + if ($IsModernPowerShell) { + # PowerShell 6+: Try different case variations + if ($Headers.ContainsKey('Content-Type')) { + return $Headers['Content-Type'] -join ', ' + } + elseif ($Headers.ContainsKey('content-type')) { + return $Headers['content-type'] -join ', ' + } + else { + # Case-insensitive search + $ctHeader = $Headers.Keys | Where-Object { $_ -ieq 'Content-Type' } | Select-Object -First 1 + if ($ctHeader) { + return $Headers[$ctHeader] -join ', ' + } + } + } + else { + # PowerShell 5: Use different access methods + if ($Headers['Content-Type']) { + return $Headers['Content-Type'] + } + else { + return $Headers.Get('Content-Type') + } + } + } + catch { + return "Unable to determine ($($_.Exception.Message))" + } + + return "" +} + # Common function for web requests with centralized configuration function Invoke-SecureWebRequest { param( @@ -197,21 +242,85 @@ function Invoke-SecureWebRequest { [int]$MaxRetries = 5 ) + $isModernPowerShell = $PSVersionTable.PSVersion.Major -ge 6 + + # Configure TLS for PowerShell 5 + if (-not $isModernPowerShell) { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 + } + try { - if ($PSVersionTable.PSVersion.Major -ge 6) { - Invoke-WebRequest -Uri $Uri -OutFile $OutFile -SslProtocol Tls12,Tls13 -MaximumRedirection 10 -TimeoutSec $TimeoutSec -OperationTimeoutSeconds $OperationTimeoutSec -UserAgent $UserAgent -MaximumRetryCount $MaxRetries + # Check content type via HEAD request first + Test-ContentTypeViaHead -Uri $Uri -IsModernPowerShell $isModernPowerShell -TimeoutSec $TimeoutSec -OperationTimeoutSec $OperationTimeoutSec -UserAgent $UserAgent -MaxRetries $MaxRetries + + # Download the actual file + $downloadParams = @{ + Uri = $Uri + OutFile = $OutFile + MaximumRedirection = 10 + TimeoutSec = $TimeoutSec + UserAgent = $UserAgent + MaximumRetryCount = $MaxRetries } - else { - # PowerShell 5: Set TLS 1.2/1.3 globally and do not use -SslProtocol - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 - Invoke-WebRequest -Uri $Uri -OutFile $OutFile -MaximumRedirection 10 -TimeoutSec $TimeoutSec -UserAgent $UserAgent + + if ($isModernPowerShell) { + $downloadParams.SslProtocol = @('Tls12', 'Tls13') + $downloadParams.OperationTimeoutSeconds = $OperationTimeoutSec } + + $webResponse = Invoke-WebRequest @downloadParams } catch { throw $_.Exception } } +# Helper function to test content type via HEAD request +function Test-ContentTypeViaHead { + param( + [string]$Uri, + [bool]$IsModernPowerShell, + [int]$TimeoutSec, + [int]$OperationTimeoutSec, + [string]$UserAgent, + [int]$MaxRetries + ) + + $contentType = "" + + try { + $headParams = @{ + Uri = $Uri + Method = 'Head' + MaximumRedirection = 10 + TimeoutSec = $TimeoutSec + UserAgent = $UserAgent + MaximumRetryCount = $MaxRetries + } + + if ($IsModernPowerShell) { + $headParams.SslProtocol = @('Tls12', 'Tls13') + $headParams.OperationTimeoutSeconds = $OperationTimeoutSec + } + + $headResponse = Invoke-WebRequest @headParams + $contentType = Get-ContentTypeFromHeaders -Headers $headResponse.Headers -IsModernPowerShell $IsModernPowerShell + } + catch { + Say-Verbose "Content-Type: Unable to determine via HEAD request ($($_.Exception.Message))" + # Continue with download even if HEAD request fails + return + } + + Say-Verbose "Content-Type: $contentType" + + # Throw if we detect HTML content (error page) + if ($contentType -and $contentType.ToLowerInvariant().StartsWith("text/html")) { + Say-Verbose "Server returned HTML content (Content-Type: $contentType) instead of expected file." + throw "Could not find the file." + } +} + # Download the Aspire CLI for the current platform function Invoke-AspireCliDownload { param( From 6664feb3025c83cc6cb37f0da8aed078c85378e5 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 15:24:24 -0400 Subject: [PATCH 10/30] fix ps1 for modern powershell --- eng/scripts/get-aspire-cli.ps1 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index 9b1ecac6be2..e80269f7b9a 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -180,9 +180,8 @@ function Get-CLIArchitecture-From-Architecture([string]$Architecture) { switch ($Architecture.ToLowerInvariant()) { { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } { $_ -eq "x86" } { return "x86" } - { $_ -eq "arm" } { return "arm" } { $_ -eq "arm64" } { return "arm64" } - default { throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" } + default { throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues" } } } @@ -260,12 +259,12 @@ function Invoke-SecureWebRequest { MaximumRedirection = 10 TimeoutSec = $TimeoutSec UserAgent = $UserAgent - MaximumRetryCount = $MaxRetries } if ($isModernPowerShell) { $downloadParams.SslProtocol = @('Tls12', 'Tls13') $downloadParams.OperationTimeoutSeconds = $OperationTimeoutSec + $downloadParams.MaximumRetryCount = $MaxRetries } $webResponse = Invoke-WebRequest @downloadParams @@ -295,12 +294,12 @@ function Test-ContentTypeViaHead { MaximumRedirection = 10 TimeoutSec = $TimeoutSec UserAgent = $UserAgent - MaximumRetryCount = $MaxRetries } if ($IsModernPowerShell) { $headParams.SslProtocol = @('Tls12', 'Tls13') $headParams.OperationTimeoutSeconds = $OperationTimeoutSec + $headParams.MaximumRetryCount = $MaxRetries } $headResponse = Invoke-WebRequest @headParams From 01ecfac922c3bae467a8995267261eed4799deda Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 17:14:54 -0400 Subject: [PATCH 11/30] Cleanup the ps1 script --- eng/scripts/get-aspire-cli.ps1 | 188 ++++++++++++--------------------- 1 file changed, 65 insertions(+), 123 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index e80269f7b9a..462b76721eb 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -13,6 +13,7 @@ param( # Global constants $Script:UserAgent = "get-aspire-cli.ps1/1.0" +$Script:IsModernPowerShell = $PSVersionTable.PSVersion.Major -ge 6 # Show help if requested if ($Help) { @@ -55,7 +56,7 @@ function Say-Verbose($str) { # Function to detect OS function Get-OperatingSystem { - if ($PSVersionTable.PSVersion.Major -ge 6) { + if ($Script:IsModernPowerShell) { if ($IsWindows) { return "win" } @@ -105,7 +106,7 @@ function Get-Machine-Architecture() { Say-Verbose $MyInvocation # On Windows PowerShell, use environment variables - if ($PSVersionTable.PSVersion.Major -lt 6 -or $IsWindows) { + if (-not $Script:IsModernPowerShell -or $IsWindows) { # On PS x86, PROCESSOR_ARCHITECTURE reports x86 even on x64 systems. # To get the correct architecture, we need to use PROCESSOR_ARCHITEW6432. # PS x64 doesn't define this, so we fall back to PROCESSOR_ARCHITECTURE. @@ -132,7 +133,7 @@ function Get-Machine-Architecture() { } # For PowerShell 6+ on Unix systems, use .NET runtime information - if ($PSVersionTable.PSVersion.Major -ge 6) { + if ($Script:IsModernPowerShell) { try { $runtimeArch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture switch ($runtimeArch) { @@ -188,8 +189,7 @@ function Get-CLIArchitecture-From-Architecture([string]$Architecture) { # Helper function to extract Content-Type from response headers function Get-ContentTypeFromHeaders { param( - [object]$Headers, - [bool]$IsModernPowerShell = $true + [object]$Headers ) if (-not $Headers) { @@ -197,7 +197,7 @@ function Get-ContentTypeFromHeaders { } try { - if ($IsModernPowerShell) { + if ($Script:IsModernPowerShell) { # PowerShell 6+: Try different case variations if ($Headers.ContainsKey('Content-Type')) { return $Headers['Content-Type'] -join ', ' @@ -235,137 +235,89 @@ function Invoke-SecureWebRequest { param( [string]$Uri, [string]$OutFile, + [string]$Method = 'Get', [int]$TimeoutSec = 60, [int]$OperationTimeoutSec = 30, - [string]$UserAgent = $Script:UserAgent, [int]$MaxRetries = 5 ) - $isModernPowerShell = $PSVersionTable.PSVersion.Major -ge 6 - # Configure TLS for PowerShell 5 - if (-not $isModernPowerShell) { + if (-not $Script:IsModernPowerShell) { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 } try { - # Check content type via HEAD request first - Test-ContentTypeViaHead -Uri $Uri -IsModernPowerShell $isModernPowerShell -TimeoutSec $TimeoutSec -OperationTimeoutSec $OperationTimeoutSec -UserAgent $UserAgent -MaxRetries $MaxRetries - - # Download the actual file - $downloadParams = @{ + # Build request parameters + $requestParams = @{ Uri = $Uri - OutFile = $OutFile + Method = $Method MaximumRedirection = 10 TimeoutSec = $TimeoutSec - UserAgent = $UserAgent + UserAgent = $Script:UserAgent + } + + # Add OutFile only for GET requests + if ($Method -eq 'Get' -and $OutFile) { + $requestParams.OutFile = $OutFile } - if ($isModernPowerShell) { - $downloadParams.SslProtocol = @('Tls12', 'Tls13') - $downloadParams.OperationTimeoutSeconds = $OperationTimeoutSec - $downloadParams.MaximumRetryCount = $MaxRetries + if ($Script:IsModernPowerShell) { + $requestParams.SslProtocol = @('Tls12', 'Tls13') + $requestParams.OperationTimeoutSeconds = $OperationTimeoutSec + $requestParams.MaximumRetryCount = $MaxRetries } - $webResponse = Invoke-WebRequest @downloadParams + $webResponse = Invoke-WebRequest @requestParams + return $webResponse } catch { throw $_.Exception } } -# Helper function to test content type via HEAD request -function Test-ContentTypeViaHead { +# General-purpose file download wrapper +function Invoke-FileDownload { param( + [Parameter(Mandatory = $true)] [string]$Uri, - [bool]$IsModernPowerShell, - [int]$TimeoutSec, - [int]$OperationTimeoutSec, - [string]$UserAgent, - [int]$MaxRetries + [Parameter(Mandatory = $true)] + [string]$OutputPath, + [int]$TimeoutSec = 60, + [int]$OperationTimeoutSec = 30, + [int]$MaxRetries = 5, + [switch]$ValidateContentType, + [switch]$UseTempFile ) - $contentType = "" - try { - $headParams = @{ - Uri = $Uri - Method = 'Head' - MaximumRedirection = 10 - TimeoutSec = $TimeoutSec - UserAgent = $UserAgent + # Validate content type via HEAD request if requested + if ($ValidateContentType) { + Say-Verbose "Validating content type for $Uri" + $headResponse = Invoke-SecureWebRequest -Uri $Uri -Method 'Head' -TimeoutSec $TimeoutSec -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries + $contentType = Get-ContentTypeFromHeaders -Headers $headResponse.Headers + + if ($contentType -and $contentType.ToLowerInvariant().StartsWith("text/html")) { + throw "Server returned HTML content (Content-Type: $contentType) instead of expected file." + } } - if ($IsModernPowerShell) { - $headParams.SslProtocol = @('Tls12', 'Tls13') - $headParams.OperationTimeoutSeconds = $OperationTimeoutSec - $headParams.MaximumRetryCount = $MaxRetries + $targetFile = $OutputPath + if ($UseTempFile) { + $targetFile = "$OutputPath.tmp" } - $headResponse = Invoke-WebRequest @headParams - $contentType = Get-ContentTypeFromHeaders -Headers $headResponse.Headers -IsModernPowerShell $IsModernPowerShell - } - catch { - Say-Verbose "Content-Type: Unable to determine via HEAD request ($($_.Exception.Message))" - # Continue with download even if HEAD request fails - return - } - - Say-Verbose "Content-Type: $contentType" - - # Throw if we detect HTML content (error page) - if ($contentType -and $contentType.ToLowerInvariant().StartsWith("text/html")) { - Say-Verbose "Server returned HTML content (Content-Type: $contentType) instead of expected file." - throw "Could not find the file." - } -} - -# Download the Aspire CLI for the current platform -function Invoke-AspireCliDownload { - param( - [string]$Url, - [string]$Filename - ) - - Write-Host "Downloading from: $Url" - - # Use temporary file and move on success to avoid partial downloads - $tempFilename = "$Filename.tmp" - - try { - Invoke-SecureWebRequest -Uri $Url -OutFile $tempFilename + Say-Verbose "Downloading $Uri to $targetFile" + Invoke-SecureWebRequest -Uri $Uri -OutFile $targetFile -TimeoutSec $TimeoutSec -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries - # Check if the downloaded file is actually HTML (error page) instead of the expected archive - $fileContent = Get-Content $tempFilename -Raw -Encoding UTF8 -ErrorAction SilentlyContinue - if ($fileContent -and ($fileContent.StartsWith("' } else { Get-CLIArchitecture-From-Architecture $Architecture } - if ($detectedArch -eq "unsupported") { - throw "Unsupported architecture. Current architecture: $([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)" - } - - $os = $detectedOS - $arch = $detectedArch + $targetArch = if ([string]::IsNullOrWhiteSpace($Architecture)) { Get-CLIArchitecture-From-Architecture '' } else { Get-CLIArchitecture-From-Architecture $Architecture } # Construct the runtime identifier - $runtimeIdentifier = "$os-$arch" + $runtimeIdentifier = "$targetOS-$targetArch" # Determine file extension based on OS - $extension = if ($os -eq "win") { "zip" } else { "tar.gz" } + $extension = if ($targetOS -eq "win") { "zip" } else { "tar.gz" } # Construct the URLs $url = "https://aka.ms/dotnet/$Channel/$BuildQuality/aspire-cli-$runtimeIdentifier.$extension" @@ -497,16 +436,19 @@ function Main { $checksumFilename = Join-Path $tempDir "aspire-cli-$runtimeIdentifier.$extension.sha512" try { - Invoke-AspireCliDownload -Url $url -Filename $filename - Invoke-ChecksumDownload -Url $checksumUrl -Filename $checksumFilename + # Download the Aspire CLI archive + Invoke-FileDownload -Uri $url -OutputPath $filename -ValidateContentType -UseTempFile + + # Download and test the checksum + Invoke-FileDownload -Uri $checksumUrl -OutputPath $checksumFilename -ValidateContentType -UseTempFile Test-FileChecksum -ArchiveFile $filename -ChecksumFile $checksumFilename Say-Verbose "Successfully downloaded and validated: $filename" # Unpack the archive - Expand-AspireCliArchive -ArchiveFile $filename -DestinationPath $OutputPath -OS $os + Expand-AspireCliArchive -ArchiveFile $filename -DestinationPath $OutputPath -OS $targetOS - $cliExe = if ($os -eq "win") { "aspire.exe" } else { "aspire" } + $cliExe = if ($targetOS -eq "win") { "aspire.exe" } else { "aspire" } $cliPath = Join-Path $OutputPath $cliExe Write-Host "Aspire CLI successfully unpacked to: $cliPath" -ForegroundColor Green From 73b1379e1ded446c18a9022ff84f7279b75987b0 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 7 Jul 2025 18:10:52 -0400 Subject: [PATCH 12/30] clean up .sh script, and don't use that ldd is available --- eng/scripts/get-aspire-cli.sh | 189 +++++++++++++++++++--------------- 1 file changed, 108 insertions(+), 81 deletions(-) diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index 7aa35e9aac9..9415be35660 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -103,7 +103,7 @@ say_verbose() { } # Function to detect OS -get_os() { +detect_os() { local uname_s uname_s=$(uname -s) @@ -113,7 +113,7 @@ get_os() { ;; Linux*) # Check if it's musl-based (Alpine, etc.) - if ldd --version 2>&1 | grep -q musl; then + if command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -q musl; then printf "linux-musl" else printf "linux" @@ -129,8 +129,33 @@ get_os() { esac } +# Function to validate and normalize architecture +get_cli_architecture_from_architecture() { + local architecture="$1" + + if [[ "$architecture" == "" ]]; then + architecture=$(detect_architecture) + fi + + case "$(echo "$architecture" | tr '[:upper:]' '[:lower:]')" in + amd64|x64) + printf "x64" + ;; + x86) + printf "x86" + ;; + arm64) + printf "arm64" + ;; + *) + printf "Error: Architecture '%s' not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues\n" "$architecture" >&2 + return 1 + ;; + esac +} + # Function to detect architecture -get_arch() { +detect_architecture() { local uname_m uname_m=$(uname -m) @@ -145,7 +170,7 @@ get_arch() { printf "x86" ;; *) - printf "unsupported" + printf "Error: Architecture '%s' not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues\n" "$uname_m" >&2 return 1 ;; esac @@ -158,69 +183,88 @@ secure_curl() { local timeout="${3:-300}" local user_agent="${4:-$USER_AGENT}" local max_retries="${5:-5}" + local method="${6:-GET}" + + local curl_args=( + --fail + --silent + --show-error + --location + --tlsv1.2 + --tls-max 1.3 + --max-time "$timeout" + --user-agent "$user_agent" + --max-redirs 10 + --retry "$max_retries" + --retry-delay 1 + --retry-max-time 60 + --request "$method" + ) + + # Add output file only for GET requests + if [[ "$method" == "GET" ]]; then + curl_args+=(--output "$output_file") + fi - # FIXME: --cert-status is failing with `curl: (91) No OCSP response received` - curl \ - --fail \ - --silent \ - --show-error \ - --location \ - --tlsv1.2 \ - --tls-max 1.3 \ - --max-time "$timeout" \ - --user-agent "$user_agent" \ - --max-redirs 10 \ - --retry "$max_retries" \ - --retry-delay 1 \ - --retry-max-time 60 \ - --output "$output_file" \ - "$url" + curl "${curl_args[@]}" "$url" } -# Download the Aspire CLI for the current platform -download_aspire_cli() { +# Validate content type via HEAD request +validate_content_type() { local url="$1" - local filename="$2" - local temp_filename="${filename}.tmp" - printf "Downloading from: %s\n" "$url" + say_verbose "Validating content type for $url" - # Use temporary file and move on success to avoid partial downloads - if secure_curl "$url" "$temp_filename" 300; then - # Check if the downloaded file is actually HTML (error page) instead of the expected archive - if file "$temp_filename" | grep -q "HTML document"; then - printf "Error: Downloaded file appears to be an HTML error page instead of the expected archive.\n" >&2 - printf "The URL may be incorrect or the file may not be available: %s\n" "$url" >&2 - rm -f "$temp_filename" + # Get headers via HEAD request + local headers + if headers=$(secure_curl "$url" /dev/null 60 "$USER_AGENT" 3 "HEAD" 2>&1); then + # Check if response suggests HTML content (error page) + if echo "$headers" | grep -qi "content-type:.*text/html"; then + printf "Error: Server returned HTML content instead of expected file.\n" >&2 return 1 fi - - mv "$temp_filename" "$filename" - return 0 else - # Clean up temporary file on failure - rm -f "$temp_filename" - printf "Error: Failed to download %s\n" "$url" >&2 - return 1 + # If HEAD request fails, continue anyway as some servers don't support it + say_verbose "HEAD request failed, proceeding with download" fi + + return 0 } -# Download the checksum file -download_checksum() { +# General-purpose file download wrapper +download_file() { local url="$1" - local filename="$2" - local temp_filename="${filename}.tmp" + local output_path="$2" + local timeout="${3:-300}" + local max_retries="${4:-5}" + local validate_content_type="${5:-false}" + local use_temp_file="${6:-false}" - say_verbose "Downloading checksum from: $url" + local target_file="$output_path" + if [[ "$use_temp_file" == true ]]; then + target_file="${output_path}.tmp" + fi - # Use temporary file and move on success to avoid partial downloads - if secure_curl "$url" "$temp_filename" 60; then - mv "$temp_filename" "$filename" + # Validate content type via HEAD request if requested + if [[ "$validate_content_type" == true ]]; then + if ! validate_content_type "$url"; then + return 1 + fi + fi + + say_verbose "Downloading $url to $target_file" + + # Download the file + if secure_curl "$url" "$target_file" "$timeout" "$USER_AGENT" "$max_retries"; then + # Move temp file to final location if using temp file + if [[ "$use_temp_file" == true ]]; then + mv "$target_file" "$output_path" + fi + + say_verbose "Successfully downloaded file to: $output_path" return 0 else - # Clean up temporary file on failure - rm -f "$temp_filename" - printf "Error: Failed to download checksum %s\n" "$url" >&2 + printf "Error: Failed to download %s to %s\n" "$url" "$output_path" >&2 return 1 fi } @@ -240,13 +284,6 @@ validate_checksum() { local expected_checksum expected_checksum=$(cat "$checksum_file" | tr -d '\n' | tr -d '\r' | tr '[:upper:]' '[:lower:]') - # Check if the checksum file contains HTML (error page) instead of a checksum - if [[ "$expected_checksum" == *"&2 - printf "Expected checksum file content, but got: %s...\n" "${expected_checksum:0:100}" >&2 - return 1 - fi - # Calculate the actual checksum local actual_checksum actual_checksum=$(sha512sum "$archive_file" | cut -d' ' -f1) @@ -271,7 +308,7 @@ validate_checksum() { } # Function to expand/unpack archive files -expand_aspire_cli_archive() { +expand_archive() { local archive_file="$1" local destination_path="$2" local os="$3" @@ -312,7 +349,7 @@ expand_aspire_cli_archive() { # Main script main() { - local os arch runtimeIdentifier url filename checksum_url checksum_filename + local os arch runtimeIdentifier url filename checksum_url checksum_filename extension local cli_exe cli_path # Parse command line arguments @@ -332,14 +369,7 @@ main() { # Create a temporary directory for downloads local temp_dir temp_dir=$(mktemp -d -t aspire-cli-download-XXXXXXXX) - - if [[ ! -d "$temp_dir" ]]; then - say_verbose "Creating temporary directory: $temp_dir" - if ! mkdir -p "$temp_dir"; then - printf "Error: Failed to create temporary directory: %s\n" "$temp_dir" >&2 - return 1 - fi - fi + say_verbose "Creating temporary directory: $temp_dir" # Cleanup function for temporary directory cleanup() { @@ -357,26 +387,23 @@ main() { trap cleanup EXIT # Detect OS and architecture if not provided - local detected_os detected_arch - if [[ -z "$OS" ]]; then - if ! detected_os=$(get_os); then + if ! os=$(detect_os); then printf "Error: Unsupported operating system. Current platform: %s\n" "$(uname -s)" >&2 return 1 fi - os="$detected_os" else os="$OS" fi if [[ -z "$ARCH" ]]; then - if ! detected_arch=$(get_arch); then - printf "Error: Unsupported architecture. Current architecture: %s\n" "$(uname -m)" >&2 + if ! arch=$(get_cli_architecture_from_architecture ""); then return 1 fi - arch="$detected_arch" else - arch="$ARCH" + if ! arch=$(get_cli_architecture_from_architecture "$ARCH"); then + return 1 + fi fi # Construct the runtime identifier @@ -396,17 +423,17 @@ main() { filename="${temp_dir}/aspire-cli-${runtimeIdentifier}.${extension}" checksum_filename="${temp_dir}/aspire-cli-${runtimeIdentifier}.${extension}.sha512" - # Download the archive file - if ! download_aspire_cli "$url" "$filename"; then + # Download the Aspire CLI archive + printf "Downloading from: %s\n" "$url" + if ! download_file "$url" "$filename" 300 5 true true; then return 1 fi - # Download the checksum file and validate - if ! download_checksum "$checksum_url" "$checksum_filename"; then + # Download and test the checksum + if ! download_file "$checksum_url" "$checksum_filename" 60 5 true true; then return 1 fi - # Validate the checksum if ! validate_checksum "$filename" "$checksum_filename"; then return 1 fi @@ -414,7 +441,7 @@ main() { say_verbose "Successfully downloaded and validated: $filename" # Unpack the archive - if ! expand_aspire_cli_archive "$filename" "$OUTPUT_PATH" "$os"; then + if ! expand_archive "$filename" "$OUTPUT_PATH" "$os"; then return 1 fi From 6561c32bcd4757e20b20d7a0ab27721c480ec93f Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 8 Jul 2025 15:53:10 -0400 Subject: [PATCH 13/30] Update eng/scripts/get-aspire-cli.ps1 Co-authored-by: Eric Erhardt --- eng/scripts/get-aspire-cli.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index 462b76721eb..df0d57be95f 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -35,7 +35,7 @@ PARAMETERS: EXAMPLES: .\get-aspire-cli.ps1 .\get-aspire-cli.ps1 -OutputPath "C:\temp" - .\get-aspire-cli.ps1 -Channel "8.0" -BuildQuality "release" + .\get-aspire-cli.ps1 -Channel "9.0" -BuildQuality "release" .\get-aspire-cli.ps1 -OS "linux" -Architecture "x64" .\get-aspire-cli.ps1 -KeepArchive .\get-aspire-cli.ps1 -Help From 41544b087bcbb57321206ebf7b6955e455451ace Mon Sep 17 00:00:00 2001 From: David Negstad <50252651+danegsta@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:01:00 -0700 Subject: [PATCH 14/30] Use different names for persistent and session scoped networks (#10278) * Use different names for persistent and session scoped networks * Append a normalized application name to aspire network names * Only guarantee the name is valid as a suffix * Fix test failure due to service import changes --- src/Aspire.Hosting/Dcp/DcpExecutor.cs | 79 +++++++++++++++++-- .../Dcp/DcpExecutorTests.cs | 12 +++ 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index 7d0b8c864f0..2b3bd106e20 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -9,7 +9,9 @@ using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Channels; using Aspire.Dashboard.ConsoleLogs; using Aspire.Dashboard.Model; @@ -24,22 +26,35 @@ using k8s.Autorest; using k8s.Models; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Polly; namespace Aspire.Hosting.Dcp; -internal sealed class DcpExecutor : IDcpExecutor, IConsoleLogsService, IAsyncDisposable +internal sealed partial class DcpExecutor : IDcpExecutor, IConsoleLogsService, IAsyncDisposable { internal const string DebugSessionPortVar = "DEBUG_SESSION_PORT"; - internal const string DefaultAspireNetworkName = "default-aspire-network"; + + // The resource name for the Aspire network resource. + internal const string DefaultAspireNetworkResourceName = "aspire-network"; + + // The base name for ephemeral networks + internal const string DefaultAspireNetworkName = "aspire-session-network"; + + // The base name for persistent networks + internal const string DefaultAspirePersistentNetworkName = "aspire-persistent-network"; // Disposal of the DcpExecutor means shutting down watches and log streams, // and asking DCP to start the shutdown process. If we cannot complete these tasks within 10 seconds, // it probably means DCP crashed and there is no point trying further. private static readonly TimeSpan s_disposeTimeout = TimeSpan.FromSeconds(10); + // Regex for normalizing application names. + [GeneratedRegex("""^(?.+?)\.?AppHost$""", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant)] + private static partial Regex ApplicationNameRegex(); + private readonly ILogger _distributedApplicationLogger; private readonly IKubernetesService _kubernetesService; private readonly IConfiguration _configuration; @@ -59,6 +74,8 @@ internal sealed class DcpExecutor : IDcpExecutor, IConsoleLogsService, IAsyncDis private readonly DcpResourceState _resourceState; private readonly ResourceSnapshotBuilder _snapshotBuilder; + private readonly string _normalizedApplicationName; + // Internal for testing. internal ResiliencePipeline DeleteResourceRetryPipeline { get; set; } internal ResiliencePipeline CreateServiceRetryPipeline { get; set; } @@ -76,6 +93,7 @@ internal sealed class DcpExecutor : IDcpExecutor, IConsoleLogsService, IAsyncDis public DcpExecutor(ILogger logger, ILogger distributedApplicationLogger, DistributedApplicationModel model, + IHostEnvironment hostEnvironment, IKubernetesService kubernetesService, IConfiguration configuration, IDistributedApplicationEventing distributedApplicationEventing, @@ -102,6 +120,7 @@ public DcpExecutor(ILogger logger, _executionContext = executionContext; _resourceState = new(model.Resources.ToDictionary(r => r.Name), _appResources); _snapshotBuilder = new(_resourceState); + _normalizedApplicationName = NormalizeApplicationName(hostEnvironment.ApplicationName); DeleteResourceRetryPipeline = DcpPipelineBuilder.BuildDeleteRetryPipeline(logger); CreateServiceRetryPipeline = DcpPipelineBuilder.BuildCreateServiceRetryPipeline(options.Value, logger); @@ -413,6 +432,46 @@ void UpdateAssociatedServicesMap() } } + /// + /// Normalizes the application name for use in physical container resource names (only guaranteed valid as a suffix). + /// Removes the ".AppHost" suffix if present and takes only characters that are valid in resource names. + /// Invalid characters are simply omitted from the name as the result doesn't need to be identical. + /// + /// The application name to normalize. + /// The normalized application name with invalid characters removed. + private static string NormalizeApplicationName(string applicationName) + { + if (string.IsNullOrEmpty(applicationName)) + { + return applicationName; + } + + applicationName = ApplicationNameRegex().Match(applicationName) switch + { + Match { Success: true } match => match.Groups["name"].Value, + _ => applicationName + }; + + if (string.IsNullOrEmpty(applicationName)) + { + return applicationName; + } + + var normalizedName = new StringBuilder(); + for (var i = 0; i < applicationName.Length; i++) + { + if ((applicationName[i] is >= 'a' and <= 'z') || + (applicationName[i] is >= 'A' and <= 'Z') || + (applicationName[i] is >= '0' and <= '9') || + (applicationName[i] is '_' or '-' or '.')) + { + normalizedName.Append(applicationName[i]); + } + } + + return normalizedName.ToString(); + } + private static string GetResourceType(T resource, IResource appModelResource) where T : CustomResource { return resource switch @@ -1174,7 +1233,7 @@ private void PrepareContainers() { new ContainerNetworkConnection { - Name = DefaultAspireNetworkName, + Name = DefaultAspireNetworkResourceName, Aliases = new List { container.Name }, } }; @@ -1239,14 +1298,24 @@ async Task CreateContainerAsyncCore(AppResource cr, CancellationToken cancellati // Create a custom container network for Aspire if there are container resources if (containerResources.Any()) { - var network = ContainerNetwork.Create(DefaultAspireNetworkName); + var network = ContainerNetwork.Create(DefaultAspireNetworkResourceName); if (containerResources.Any(cr => cr.ModelResource.GetContainerLifetimeType() == ContainerLifetime.Persistent)) { // If we have any persistent container resources network.Spec.Persistent = true; // Persistent networks require a predictable name to be reused between runs. // Append the same project hash suffix used for persistent container names. - network.Spec.NetworkName = $"{DefaultAspireNetworkName}-{_nameGenerator.GetProjectHashSuffix()}"; + network.Spec.NetworkName = $"{DefaultAspirePersistentNetworkName}-{_nameGenerator.GetProjectHashSuffix()}"; + } + else + { + network.Spec.NetworkName = $"{DefaultAspireNetworkName}-{DcpNameGenerator.GetRandomNameSuffix()}"; + } + + if (!string.IsNullOrEmpty(_normalizedApplicationName)) + { + var shortApplicationName = _normalizedApplicationName.Length < 32 ? _normalizedApplicationName : _normalizedApplicationName.Substring(0, 32); + network.Spec.NetworkName += $"-{shortApplicationName}"; // Limit to 32 characters to avoid exceeding resource name length limits. } tasks.Add(_kubernetesService.CreateAsync(network, cancellationToken)); diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 8907bb8bc6c..faead8af0da 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -13,6 +13,8 @@ using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Polly; @@ -1242,6 +1244,7 @@ private static void HasKnownCommandAnnotations(IResource resource) private static DcpExecutor CreateAppExecutor( DistributedApplicationModel distributedAppModel, + IHostEnvironment? hostEnvironment = null, IConfiguration? configuration = null, IKubernetesService? kubernetesService = null, DcpOptions? dcpOptions = null, @@ -1268,6 +1271,7 @@ private static DcpExecutor CreateAppExecutor( NullLogger.Instance, NullLogger.Instance, distributedAppModel, + hostEnvironment ?? new TestHostEnvironment(), kubernetesService ?? new TestKubernetesService(), configuration, new Hosting.Eventing.DistributedApplicationEventing(), @@ -1283,6 +1287,14 @@ private static DcpExecutor CreateAppExecutor( events ?? new DcpExecutorEvents()); } + private sealed class TestHostEnvironment : IHostEnvironment + { + public string ApplicationName { get; set; } = default!; + public IFileProvider ContentRootFileProvider { get; set; } = default!; + public string ContentRootPath { get; set; } = default!; + public string EnvironmentName { get; set; } = default!; + } + private sealed class TestProject : IProjectMetadata { public string ProjectPath => "TestProject"; From affb837ff937b4d2230d983a0cd852dcc120bc54 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 8 Jul 2025 16:05:41 -0400 Subject: [PATCH 15/30] Address review feedback from @ eerhardt and bump archive download timeouts to 10mins --- eng/scripts/get-aspire-cli.ps1 | 8 +++++--- eng/scripts/get-aspire-cli.sh | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index df0d57be95f..41e3b416239 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -14,6 +14,8 @@ param( # Global constants $Script:UserAgent = "get-aspire-cli.ps1/1.0" $Script:IsModernPowerShell = $PSVersionTable.PSVersion.Major -ge 6 +$Script:ArchiveDownloadTimeoutSec = 600 +$Script:ChecksumDownloadTimeoutSec = 120 # Show help if requested if ($Help) { @@ -293,7 +295,7 @@ function Invoke-FileDownload { # Validate content type via HEAD request if requested if ($ValidateContentType) { Say-Verbose "Validating content type for $Uri" - $headResponse = Invoke-SecureWebRequest -Uri $Uri -Method 'Head' -TimeoutSec $TimeoutSec -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries + $headResponse = Invoke-SecureWebRequest -Uri $Uri -Method 'Head' -TimeoutSec 60 -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries $contentType = Get-ContentTypeFromHeaders -Headers $headResponse.Headers if ($contentType -and $contentType.ToLowerInvariant().StartsWith("text/html")) { @@ -437,10 +439,10 @@ function Main { try { # Download the Aspire CLI archive - Invoke-FileDownload -Uri $url -OutputPath $filename -ValidateContentType -UseTempFile + Invoke-FileDownload -Uri $url -TimeoutSec $Script:ArchiveDownloadTimeoutSec -OutputPath $filename -ValidateContentType -UseTempFile # Download and test the checksum - Invoke-FileDownload -Uri $checksumUrl -OutputPath $checksumFilename -ValidateContentType -UseTempFile + Invoke-FileDownload -Uri $checksumUrl -TimeoutSec $Script:ChecksumDownloadTimeoutSec -OutputPath $checksumFilename -ValidateContentType -UseTempFile Test-FileChecksum -ArchiveFile $filename -ChecksumFile $checksumFilename Say-Verbose "Successfully downloaded and validated: $filename" diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index 9415be35660..a2f524ccc59 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -7,6 +7,8 @@ set -euo pipefail # Global constants readonly USER_AGENT="get-aspire-cli.sh/1.0" +readonly ARCHIVE_DOWNLOAD_TIMEOUT_SEC=600 +readonly CHECKSUM_DOWNLOAD_TIMEOUT_SEC=120 # Default values OUTPUT_PATH="" @@ -425,12 +427,12 @@ main() { # Download the Aspire CLI archive printf "Downloading from: %s\n" "$url" - if ! download_file "$url" "$filename" 300 5 true true; then + if ! download_file "$url" "$filename" $ARCHIVE_DOWNLOAD_TIMEOUT_SEC 5 true true; then return 1 fi # Download and test the checksum - if ! download_file "$checksum_url" "$checksum_filename" 60 5 true true; then + if ! download_file "$checksum_url" "$checksum_filename" $CHECKSUM_DOWNLOAD_TIMEOUT_SEC 5 true true; then return 1 fi From 95dfd0c105886d93cc14f8423b3f76d1cff1c8ea Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 8 Jul 2025 16:29:27 -0400 Subject: [PATCH 16/30] Address review feedback from @ eerhardt and change Channel parameter to Version --- eng/scripts/get-aspire-cli.ps1 | 8 ++++---- eng/scripts/get-aspire-cli.sh | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index 41e3b416239..a4ac09b6418 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -3,7 +3,7 @@ [CmdletBinding()] param( [string]$OutputPath = "", - [string]$Channel = "9.0", + [string]$Version = "9.0", [string]$BuildQuality = "daily", [string]$OS = "", [string]$Architecture = "", @@ -27,7 +27,7 @@ DESCRIPTION: PARAMETERS: -OutputPath Directory to unpack the CLI (default: aspire-cli directory under current directory) - -Channel Channel of the Aspire CLI to download (default: 9.0) + -Version Version of the Aspire CLI to download (default: 9.0) -BuildQuality Build quality to download (default: daily) -OS Operating system (default: auto-detect) -Architecture Architecture (default: auto-detect) @@ -37,7 +37,7 @@ PARAMETERS: EXAMPLES: .\get-aspire-cli.ps1 .\get-aspire-cli.ps1 -OutputPath "C:\temp" - .\get-aspire-cli.ps1 -Channel "9.0" -BuildQuality "release" + .\get-aspire-cli.ps1 -Version "9.0" -BuildQuality "release" .\get-aspire-cli.ps1 -OS "linux" -Architecture "x64" .\get-aspire-cli.ps1 -KeepArchive .\get-aspire-cli.ps1 -Help @@ -431,7 +431,7 @@ function Main { $extension = if ($targetOS -eq "win") { "zip" } else { "tar.gz" } # Construct the URLs - $url = "https://aka.ms/dotnet/$Channel/$BuildQuality/aspire-cli-$runtimeIdentifier.$extension" + $url = "https://aka.ms/dotnet/$Version/$BuildQuality/aspire-cli-$runtimeIdentifier.$extension" $checksumUrl = "$url.sha512" $filename = Join-Path $tempDir "aspire-cli-$runtimeIdentifier.$extension" diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index a2f524ccc59..3513d68b3d3 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -12,7 +12,7 @@ readonly CHECKSUM_DOWNLOAD_TIMEOUT_SEC=120 # Default values OUTPUT_PATH="" -CHANNEL="9.0" +VERSION="9.0" BUILD_QUALITY="daily" OS="" ARCH="" @@ -26,14 +26,14 @@ show_help() { Aspire CLI Download Script DESCRIPTION: - Downloads and unpacks the Aspire CLI for the current platform from the specified channel and build quality. + Downloads and unpacks the Aspire CLI for the current platform from the specified version and build quality. USAGE: ./get-aspire-cli.sh [OPTIONS] OPTIONS: -o, --output-path PATH Directory to unpack the CLI (default: aspire-cli directory under current directory) - -c, --channel CHANNEL Channel of the Aspire CLI to download (default: 9.0) + --version VERSION Version of the Aspire CLI to download (default: 9.0) -q, --quality QUALITY Build quality to download (default: daily) --os OS Operating system (default: auto-detect) --arch ARCH Architecture (default: auto-detect) @@ -44,7 +44,7 @@ OPTIONS: EXAMPLES: ./get-aspire-cli.sh ./get-aspire-cli.sh --output-path "/tmp" - ./get-aspire-cli.sh --channel "9.0" --quality "release" + ./get-aspire-cli.sh --version "9.0" --quality "release" ./get-aspire-cli.sh --os "linux" --arch "x64" ./get-aspire-cli.sh --keep-archive ./get-aspire-cli.sh --help @@ -60,8 +60,8 @@ parse_args() { OUTPUT_PATH="$2" shift 2 ;; - -c|--channel) - CHANNEL="$2" + --version) + VERSION="$2" shift 2 ;; -q|--quality) @@ -419,7 +419,7 @@ main() { fi # Construct the URLs - url="https://aka.ms/dotnet/${CHANNEL}/${BUILD_QUALITY}/aspire-cli-${runtimeIdentifier}.${extension}" + url="https://aka.ms/dotnet/${VERSION}/${BUILD_QUALITY}/aspire-cli-${runtimeIdentifier}.${extension}" checksum_url="${url}.sha512" filename="${temp_dir}/aspire-cli-${runtimeIdentifier}.${extension}" From ae772b04e8fa2181be1fc6fc04e468ac9a4ba1fd Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 8 Jul 2025 20:05:07 -0400 Subject: [PATCH 17/30] Address review feedback from @ eerhardt - update README.md --- eng/scripts/README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/eng/scripts/README.md b/eng/scripts/README.md index 8b787feccca..e93bcb17f4f 100644 --- a/eng/scripts/README.md +++ b/eng/scripts/README.md @@ -10,37 +10,37 @@ This directory contains scripts to download and install the Aspire CLI for diffe ## Current Limitations ⚠️ **Important**: Currently, only the following combination works: -- **Channel**: `9.0` +- **Version**: `9.0` - **Build Quality**: `daily` -Other channel/quality combinations are not yet available through the download URLs. +Other version/quality combinations are not yet available through the download URLs. ## Parameters ### Bash Script (`get-aspire-cli.sh`) -| Parameter | Short | Description | Default | -|-----------|-------|-------------|---------| -| `--output-path` | `-o` | Directory to unpack the CLI | `./aspire-cli` | -| `--channel` | `-c` | Channel of the Aspire CLI to download | `9.0` | -| `--quality` | `-q` | Build quality to download | `daily` | -| `--os` | | Operating system (auto-detected if not specified) | auto-detect | -| `--architecture` | | Architecture (auto-detected if not specified) | auto-detect | -| `--keep-archive` | `-k` | Keep downloaded archive files after installation | `false` | -| `--verbose` | `-v` | Enable verbose output | `false` | -| `--help` | `-h` | Show help message | | +| Parameter | Short | Description | Default | +|------------------|-------|---------------------------------------------------|----------------| +| `--output-path` | `-o` | Directory to unpack the CLI | `./aspire-cli` | +| `--version` | | Version of the Aspire CLI to download | `9.0` | +| `--quality` | `-q` | Build quality to download | `daily` | +| `--os` | | Operating system (auto-detected if not specified) | auto-detect | +| `--architecture` | | Architecture (auto-detected if not specified) | auto-detect | +| `--keep-archive` | `-k` | Keep downloaded archive files after installation | `false` | +| `--verbose` | `-v` | Enable verbose output | `false` | +| `--help` | `-h` | Show help message | | ### PowerShell Script (`get-aspire-cli.ps1`) -| Parameter | Description | Default | -|-----------|-------------|---------| -| `-OutputPath` | Directory to unpack the CLI | `./aspire-cli` | -| `-Channel` | Channel of the Aspire CLI to download | `9.0` | -| `-BuildQuality` | Build quality to download | `daily` | -| `-OS` | Operating system (auto-detected if not specified) | auto-detect | -| `-Architecture` | Architecture (auto-detected if not specified) | auto-detect | -| `-KeepArchive` | Keep downloaded archive files after installation | `false` | -| `-Help` | Show help message | | +| Parameter | Description | Default | +|-----------------|---------------------------------------------------|----------------| +| `-OutputPath` | Directory to unpack the CLI | `./aspire-cli` | +| `-Version` | Version of the Aspire CLI to download | `9.0` | +| `-BuildQuality` | Build quality to download | `daily` | +| `-OS` | Operating system (auto-detected if not specified) | auto-detect | +| `-Architecture` | Architecture (auto-detected if not specified) | auto-detect | +| `-KeepArchive` | Keep downloaded archive files after installation | `false` | +| `-Help` | Show help message | | ## Output Path Parameter From 48b262f69f2ab40de58ebe909ceb115ff305aa41 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 8 Jul 2025 22:45:35 -0400 Subject: [PATCH 18/30] Address review feedback - Rename BuildQuality to Quality --- eng/scripts/README.md | 6 +++--- eng/scripts/get-aspire-cli.ps1 | 10 +++++----- eng/scripts/get-aspire-cli.sh | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/eng/scripts/README.md b/eng/scripts/README.md index e93bcb17f4f..25b33d5f9ad 100644 --- a/eng/scripts/README.md +++ b/eng/scripts/README.md @@ -11,7 +11,7 @@ This directory contains scripts to download and install the Aspire CLI for diffe ⚠️ **Important**: Currently, only the following combination works: - **Version**: `9.0` -- **Build Quality**: `daily` +- **Quality**: `daily` Other version/quality combinations are not yet available through the download URLs. @@ -23,7 +23,7 @@ Other version/quality combinations are not yet available through the download UR |------------------|-------|---------------------------------------------------|----------------| | `--output-path` | `-o` | Directory to unpack the CLI | `./aspire-cli` | | `--version` | | Version of the Aspire CLI to download | `9.0` | -| `--quality` | `-q` | Build quality to download | `daily` | +| `--quality` | `-q` | Quality to download | `daily` | | `--os` | | Operating system (auto-detected if not specified) | auto-detect | | `--architecture` | | Architecture (auto-detected if not specified) | auto-detect | | `--keep-archive` | `-k` | Keep downloaded archive files after installation | `false` | @@ -36,7 +36,7 @@ Other version/quality combinations are not yet available through the download UR |-----------------|---------------------------------------------------|----------------| | `-OutputPath` | Directory to unpack the CLI | `./aspire-cli` | | `-Version` | Version of the Aspire CLI to download | `9.0` | -| `-BuildQuality` | Build quality to download | `daily` | +| `-Quality` | Quality to download | `daily` | | `-OS` | Operating system (auto-detected if not specified) | auto-detect | | `-Architecture` | Architecture (auto-detected if not specified) | auto-detect | | `-KeepArchive` | Keep downloaded archive files after installation | `false` | diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index a4ac09b6418..c5e1458f747 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -4,7 +4,7 @@ param( [string]$OutputPath = "", [string]$Version = "9.0", - [string]$BuildQuality = "daily", + [string]$Quality = "daily", [string]$OS = "", [string]$Architecture = "", [switch]$KeepArchive, @@ -23,12 +23,12 @@ if ($Help) { Aspire CLI Download Script DESCRIPTION: - Downloads and unpacks the Aspire CLI for the current platform from the specified version and build quality. + Downloads and unpacks the Aspire CLI for the current platform from the specified version and quality. PARAMETERS: -OutputPath Directory to unpack the CLI (default: aspire-cli directory under current directory) -Version Version of the Aspire CLI to download (default: 9.0) - -BuildQuality Build quality to download (default: daily) + -Quality Quality to download (default: daily) -OS Operating system (default: auto-detect) -Architecture Architecture (default: auto-detect) -KeepArchive Keep downloaded archive files and temporary directory after installation @@ -37,7 +37,7 @@ PARAMETERS: EXAMPLES: .\get-aspire-cli.ps1 .\get-aspire-cli.ps1 -OutputPath "C:\temp" - .\get-aspire-cli.ps1 -Version "9.0" -BuildQuality "release" + .\get-aspire-cli.ps1 -Version "9.0" -Quality "release" .\get-aspire-cli.ps1 -OS "linux" -Architecture "x64" .\get-aspire-cli.ps1 -KeepArchive .\get-aspire-cli.ps1 -Help @@ -431,7 +431,7 @@ function Main { $extension = if ($targetOS -eq "win") { "zip" } else { "tar.gz" } # Construct the URLs - $url = "https://aka.ms/dotnet/$Version/$BuildQuality/aspire-cli-$runtimeIdentifier.$extension" + $url = "https://aka.ms/dotnet/$Version/$Quality/aspire-cli-$runtimeIdentifier.$extension" $checksumUrl = "$url.sha512" $filename = Join-Path $tempDir "aspire-cli-$runtimeIdentifier.$extension" diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index 3513d68b3d3..f8fc944c52e 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -13,7 +13,7 @@ readonly CHECKSUM_DOWNLOAD_TIMEOUT_SEC=120 # Default values OUTPUT_PATH="" VERSION="9.0" -BUILD_QUALITY="daily" +QUALITY="daily" OS="" ARCH="" SHOW_HELP=false @@ -26,7 +26,7 @@ show_help() { Aspire CLI Download Script DESCRIPTION: - Downloads and unpacks the Aspire CLI for the current platform from the specified version and build quality. + Downloads and unpacks the Aspire CLI for the current platform from the specified version and quality. USAGE: ./get-aspire-cli.sh [OPTIONS] @@ -34,7 +34,7 @@ USAGE: OPTIONS: -o, --output-path PATH Directory to unpack the CLI (default: aspire-cli directory under current directory) --version VERSION Version of the Aspire CLI to download (default: 9.0) - -q, --quality QUALITY Build quality to download (default: daily) + -q, --quality QUALITY Quality to download (default: daily) --os OS Operating system (default: auto-detect) --arch ARCH Architecture (default: auto-detect) -k, --keep-archive Keep downloaded archive files and temporary directory after installation @@ -65,7 +65,7 @@ parse_args() { shift 2 ;; -q|--quality) - BUILD_QUALITY="$2" + QUALITY="$2" shift 2 ;; --os) @@ -419,7 +419,7 @@ main() { fi # Construct the URLs - url="https://aka.ms/dotnet/${VERSION}/${BUILD_QUALITY}/aspire-cli-${runtimeIdentifier}.${extension}" + url="https://aka.ms/dotnet/${VERSION}/${QUALITY}/aspire-cli-${runtimeIdentifier}.${extension}" checksum_url="${url}.sha512" filename="${temp_dir}/aspire-cli-${runtimeIdentifier}.${extension}" From 7e6506faef849d3e50ef97a081a0f910143eab35 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 14 Jul 2025 04:38:28 -0400 Subject: [PATCH 19/30] Improve Aspire CLI download script with auto-installation and PATH management This commit enhances the bash script to automatically install the Aspire CLI to a standard location - `$HOME/.aspire/bin` and update the current session's PATH environment variable. - It also adds GitHub Actions support by updating `GITHUB_PATH`, improves user experience with colored output functions, and provides better help documentation. The script now functions more like a proper installer rather than just a downloader. --- eng/scripts/get-aspire-cli.sh | 333 ++++++++++++++++++++++++++-------- 1 file changed, 257 insertions(+), 76 deletions(-) diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index f8fc944c52e..2688b74f221 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -2,6 +2,7 @@ # get-aspire-cli.sh - Download and unpack the Aspire CLI for the current platform # Usage: ./get-aspire-cli.sh [OPTIONS] +# curl -sSL /get-aspire-cli.sh | bash -s -- [OPTIONS] set -euo pipefail @@ -9,9 +10,13 @@ set -euo pipefail readonly USER_AGENT="get-aspire-cli.sh/1.0" readonly ARCHIVE_DOWNLOAD_TIMEOUT_SEC=600 readonly CHECKSUM_DOWNLOAD_TIMEOUT_SEC=120 +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly RESET='\033[0m' # Default values -OUTPUT_PATH="" +INSTALL_PATH="" VERSION="9.0" QUALITY="daily" OS="" @@ -31,8 +36,7 @@ DESCRIPTION: USAGE: ./get-aspire-cli.sh [OPTIONS] -OPTIONS: - -o, --output-path PATH Directory to unpack the CLI (default: aspire-cli directory under current directory) + -i, --install-path PATH Directory to install the CLI (default: $HOME/.aspire/bin) --version VERSION Version of the Aspire CLI to download (default: 9.0) -q, --quality QUALITY Quality to download (default: daily) --os OS Operating system (default: auto-detect) @@ -43,12 +47,16 @@ OPTIONS: EXAMPLES: ./get-aspire-cli.sh - ./get-aspire-cli.sh --output-path "/tmp" + ./get-aspire-cli.sh --install-path "/usr/local/bin" ./get-aspire-cli.sh --version "9.0" --quality "release" ./get-aspire-cli.sh --os "linux" --arch "x64" ./get-aspire-cli.sh --keep-archive ./get-aspire-cli.sh --help + # Piped execution (like wget | bash or curl | bash): + curl -sSL /get-aspire-cli.sh | bash + curl -sSL /get-aspire-cli.sh | bash -s -- --install-path "/usr/local/bin" + EOF } @@ -56,23 +64,48 @@ EOF parse_args() { while [[ $# -gt 0 ]]; do case $1 in - -o|--output-path) - OUTPUT_PATH="$2" + -i|--install-path) + if [[ $# -lt 2 || -z "$2" ]]; then + say_error "Option '$1' requires a non-empty value" + say_info "Use --help for usage information." + exit 1 + fi + INSTALL_PATH="$2" shift 2 ;; --version) + if [[ $# -lt 2 || -z "$2" ]]; then + say_error "Option '$1' requires a non-empty value" + say_info "Use --help for usage information." + exit 1 + fi VERSION="$2" shift 2 ;; -q|--quality) + if [[ $# -lt 2 || -z "$2" ]]; then + say_error "Option '$1' requires a non-empty value" + say_info "Use --help for usage information." + exit 1 + fi QUALITY="$2" shift 2 ;; --os) + if [[ $# -lt 2 || -z "$2" ]]; then + say_error "Option '$1' requires a non-empty value" + say_info "Use --help for usage information." + exit 1 + fi OS="$2" shift 2 ;; --arch) + if [[ $# -lt 2 || -z "$2" ]]; then + say_error "Option '$1' requires a non-empty value" + say_info "Use --help for usage information." + exit 1 + fi ARCH="$2" shift 2 ;; @@ -89,8 +122,8 @@ parse_args() { shift ;; *) - printf "Error: Unknown option '%s'\n" "$1" >&2 - printf "Use --help for usage information.\n" >&2 + say_error "Unknown option '$1'" + say_info "Use --help for usage information." exit 1 ;; esac @@ -100,10 +133,22 @@ parse_args() { # Function for verbose logging say_verbose() { if [[ "$VERBOSE" == true ]]; then - printf "%s\n" "$1" + echo -e "${YELLOW}$1${RESET}" >&2 fi } +say_error() { + echo -e "${RED}Error: $1${RESET}\n" >&2 +} + +say_warn() { + echo -e "${YELLOW}Warning: $1${RESET}\n" >&2 +} + +say_info() { + echo -e "$1" >&2 +} + # Function to detect OS detect_os() { local uname_s @@ -150,7 +195,7 @@ get_cli_architecture_from_architecture() { printf "arm64" ;; *) - printf "Error: Architecture '%s' not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues\n" "$architecture" >&2 + say_error "Architecture $architecture not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues" return 1 ;; esac @@ -172,7 +217,7 @@ detect_architecture() { printf "x86" ;; *) - printf "Error: Architecture '%s' not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues\n" "$uname_m" >&2 + say_error "Architecture $uname_m not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues" return 1 ;; esac @@ -189,7 +234,6 @@ secure_curl() { local curl_args=( --fail - --silent --show-error --location --tlsv1.2 @@ -203,11 +247,19 @@ secure_curl() { --request "$method" ) + # Add extra args based on method + if [[ "$method" == "HEAD" ]]; then + curl_args+=(--silent --head) + else + curl_args+=(--progress-bar) + fi + # Add output file only for GET requests if [[ "$method" == "GET" ]]; then curl_args+=(--output "$output_file") fi + say_verbose "curl ${curl_args[*]} $url" curl "${curl_args[@]}" "$url" } @@ -222,12 +274,12 @@ validate_content_type() { if headers=$(secure_curl "$url" /dev/null 60 "$USER_AGENT" 3 "HEAD" 2>&1); then # Check if response suggests HTML content (error page) if echo "$headers" | grep -qi "content-type:.*text/html"; then - printf "Error: Server returned HTML content instead of expected file.\n" >&2 + say_error "Server returned HTML content instead of expected file. Make sure the URL is correct: $url" return 1 fi else # If HEAD request fails, continue anyway as some servers don't support it - say_verbose "HEAD request failed, proceeding with download" + say_verbose "HEAD request failed, proceeding with download." fi return 0 @@ -239,8 +291,8 @@ download_file() { local output_path="$2" local timeout="${3:-300}" local max_retries="${4:-5}" - local validate_content_type="${5:-false}" - local use_temp_file="${6:-false}" + local validate_content_type="${5:-true}" + local use_temp_file="${6:-true}" local target_file="$output_path" if [[ "$use_temp_file" == true ]]; then @@ -255,6 +307,7 @@ download_file() { fi say_verbose "Downloading $url to $target_file" + say_info "Downloading from: $url" # Download the file if secure_curl "$url" "$target_file" "$timeout" "$USER_AGENT" "$max_retries"; then @@ -266,7 +319,7 @@ download_file() { say_verbose "Successfully downloaded file to: $output_path" return 0 else - printf "Error: Failed to download %s to %s\n" "$url" "$output_path" >&2 + say_error "Failed to download $url to $output_path" return 1 fi } @@ -278,13 +331,13 @@ validate_checksum() { # Check if sha512sum command is available if ! command -v sha512sum >/dev/null 2>&1; then - printf "Error: sha512sum command not found. Please install it to validate checksums.\n" >&2 + say_error "sha512sum command not found. Please install it to validate checksums." return 1 fi # Read the expected checksum from the file local expected_checksum - expected_checksum=$(cat "$checksum_file" | tr -d '\n' | tr -d '\r' | tr '[:upper:]' '[:lower:]') + expected_checksum=$(tr -d '\n\r' < "$checksum_file" | tr '[:upper:]' '[:lower:]') # Calculate the actual checksum local actual_checksum @@ -302,96 +355,176 @@ validate_checksum() { expected_checksum_display="$expected_checksum" fi - printf "Error: Checksum validation failed for %s with checksum from %s!\n" "$archive_file" "$checksum_file" >&2 - printf "Expected: %s\n" "$expected_checksum_display" >&2 - printf "Actual: %s\n" "$actual_checksum" >&2 + say_error "Checksum validation failed for $archive_file with checksum from $checksum_file !" + say_info "Expected: $expected_checksum_display" + say_info "Actual: $actual_checksum" return 1 fi } -# Function to expand/unpack archive files -expand_archive() { +# Function to install/unpack archive files +install_archive() { local archive_file="$1" local destination_path="$2" local os="$3" - say_verbose "Unpacking archive to: $destination_path" + say_verbose "Installing archive to: $destination_path" - # Create destination directory if it doesn't exist + # Create install directory if it doesn't exist if [[ ! -d "$destination_path" ]]; then + say_verbose "Creating install directory: $destination_path" mkdir -p "$destination_path" fi if [[ "$os" == "win" ]]; then # Use unzip for ZIP files if ! command -v unzip >/dev/null 2>&1; then - printf "Error: unzip command not found. Please install unzip to extract ZIP files.\n" >&2 + say_error "unzip command not found. Please install unzip to extract ZIP files." return 1 fi if ! unzip -o "$archive_file" -d "$destination_path"; then - printf "Error: Failed to extract ZIP archive: %s\n" "$archive_file" >&2 + say_error "Failed to extract ZIP archive: $archive_file" return 1 fi else # Use tar for tar.gz files on Unix systems if ! command -v tar >/dev/null 2>&1; then - printf "Error: tar command not found. Please install tar to extract tar.gz files.\n" >&2 + say_error "tar command not found. Please install tar to extract tar.gz files." return 1 fi if ! tar -xzf "$archive_file" -C "$destination_path"; then - printf "Error: Failed to extract tar.gz archive: %s\n" "$archive_file" >&2 + say_error "Failed to extract tar.gz archive: $archive_file" return 1 fi fi - say_verbose "Successfully unpacked archive" + say_verbose "Successfully installed archive" } -# Main script -main() { - local os arch runtimeIdentifier url filename checksum_url checksum_filename extension - local cli_exe cli_path +# Function to add PATH to shell configuration file +# Parameters: +# $1 - config_file: Path to the shell configuration file +# $2 - bin_path: The binary path to add to PATH +# $3 - command: The command to add to the configuration file +add_to_path() +{ + local config_file="$1" + local bin_path="$2" + local command="$3" + + if [[ ":$PATH:" == *":$bin_path:"* ]]; then + say_info "Path $bin_path already exists in \$PATH, skipping addition" + elif [[ -f "$config_file" ]] && grep -Fxq "$command" "$config_file"; then + say_info "Command already exists in $config_file, skipping addition" + elif [[ -w $config_file ]]; then + echo -e "\n# Added by get-aspire-cli.sh" >> "$config_file" + echo "$command" >> "$config_file" + say_info "Successfully added aspire to \$PATH in $config_file" + else + say_info "Manually add the to $config_file (or similar):" + say_info " $command" + fi +} - # Parse command line arguments - parse_args "$@" +# Function to add PATH to shell profile +add_to_shell_profile() { + local bin_path="$1" + local bin_path_unexpanded="$2" + local xdg_config_home="${XDG_CONFIG_HOME:-$HOME/.config}" - # Show help if requested - if [[ "$SHOW_HELP" == true ]]; then - show_help - exit 0 - fi + # Detect the current shell + local shell_name - # Set default OutputPath if empty - if [[ -z "$OUTPUT_PATH" ]]; then - OUTPUT_PATH="$(pwd)/aspire-cli" + # Try to get shell from SHELL environment variable + if [[ -n "${SHELL:-}" ]]; then + shell_name=$(basename "$SHELL") + else + # Fallback to detecting from process + shell_name=$(ps -p $$ -o comm= 2>/dev/null || echo "sh") fi - # Create a temporary directory for downloads - local temp_dir - temp_dir=$(mktemp -d -t aspire-cli-download-XXXXXXXX) - say_verbose "Creating temporary directory: $temp_dir" - - # Cleanup function for temporary directory - cleanup() { - if [[ -n "${temp_dir:-}" ]] && [[ -d "$temp_dir" ]]; then - if [[ "$KEEP_ARCHIVE" != true ]]; then - say_verbose "Cleaning up temporary files..." - rm -rf "$temp_dir" || printf "Warning: Failed to clean up temporary directory: %s\n" "$temp_dir" >&2 - else - printf "Archive files kept in: %s\n" "$temp_dir" - fi + # Normalize shell name + case "$shell_name" in + bash|zsh|fish) + ;; + sh|dash|ash) + shell_name="sh" + ;; + *) + # Default to bash for unknown shells + shell_name="bash" + ;; + esac + + say_verbose "Detected shell: $shell_name" + + local config_files + case "$shell_name" in + bash) + config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $xdg_config_home/bash/.bashrc $xdg_config_home/bash/.bash_profile" + ;; + zsh) + config_files="$HOME/.zshrc $HOME/.zshenv $xdg_config_home/zsh/.zshrc $xdg_config_home/zsh/.zshenv" + ;; + fish) + config_files="$HOME/.config/fish/config.fish" + ;; + sh) + config_files="$HOME/.profile /etc/profile" + ;; + *) + # Default to bash files for unknown shells + config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile" + ;; + esac + + # Get the appropriate shell config file + local config_file + + # Find the first existing config file + for file in $config_files; do + if [[ -f "$file" ]]; then + config_file="$file" + break fi - } + done + + if [[ -z $config_file ]]; then + say_error "No config file found for $shell_name. Checked files: $config_files" + exit 1 + fi + + case "$shell_name" in + bash|zsh|sh) + add_to_path "$config_file" "$bin_path" "export PATH=\"$bin_path_unexpanded:\$PATH\"" + ;; + fish) + add_to_path "$config_file" "$bin_path" "fish_add_path $bin_path_unexpanded" + ;; + *) + say_error "Unsupported shell type $shell_name. Please add the path $bin_path_unexpanded manually to \$PATH in your profile." + return 1 + ;; + esac + + printf "\nTo use the Aspire CLI in new terminal sessions, restart your terminal or run:\n" + say_info " source $config_file" - # Set trap for cleanup on exit - trap cleanup EXIT + return 0 +} + +# Function to download and install archive +download_and_install_archive() { + local temp_dir="$1" + local os arch runtimeIdentifier url filename checksum_url checksum_filename extension + local cli_exe cli_path # Detect OS and architecture if not provided if [[ -z "$OS" ]]; then if ! os=$(detect_os); then - printf "Error: Unsupported operating system. Current platform: %s\n" "$(uname -s)" >&2 + say_error "Unsupported operating system. Current platform: $(uname -s)" return 1 fi else @@ -426,13 +559,12 @@ main() { checksum_filename="${temp_dir}/aspire-cli-${runtimeIdentifier}.${extension}.sha512" # Download the Aspire CLI archive - printf "Downloading from: %s\n" "$url" - if ! download_file "$url" "$filename" $ARCHIVE_DOWNLOAD_TIMEOUT_SEC 5 true true; then + if ! download_file "$url" "$filename" $ARCHIVE_DOWNLOAD_TIMEOUT_SEC; then return 1 fi # Download and test the checksum - if ! download_file "$checksum_url" "$checksum_filename" $CHECKSUM_DOWNLOAD_TIMEOUT_SEC 5 true true; then + if ! download_file "$checksum_url" "$checksum_filename" $CHECKSUM_DOWNLOAD_TIMEOUT_SEC; then return 1 fi @@ -442,8 +574,8 @@ main() { say_verbose "Successfully downloaded and validated: $filename" - # Unpack the archive - if ! expand_archive "$filename" "$OUTPUT_PATH" "$os"; then + # Install the archive + if ! install_archive "$filename" "$INSTALL_PATH" "$os"; then return 1 fi @@ -452,16 +584,65 @@ main() { else cli_exe="aspire" fi - cli_path="${OUTPUT_PATH}/${cli_exe}" + cli_path="${INSTALL_PATH}/${cli_exe}" - printf "Aspire CLI successfully unpacked to: %s\n" "$cli_path" + say_info "Aspire CLI successfully installed to: ${GREEN}$cli_path${RESET}" } -# Run main function if script is executed directly -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - if main "$@"; then - exit 0 - else - exit 1 +# Parse command line arguments +parse_args "$@" + +# Show help if requested +if [[ "$SHOW_HELP" == true ]]; then + show_help + exit 0 +fi + +# Set default install path if not provided +if [[ -z "$INSTALL_PATH" ]]; then + INSTALL_PATH="$HOME/.aspire/bin" + INSTALL_PATH_UNEXPANDED="\$HOME/.aspire/bin" +else + INSTALL_PATH_UNEXPANDED="$INSTALL_PATH" +fi + +# Create a temporary directory for downloads +temp_dir=$(mktemp -d -t aspire-cli-download-XXXXXXXX) +say_verbose "Creating temporary directory: $temp_dir" + +# Cleanup function for temporary directory +cleanup() { + # shellcheck disable=SC2317 # Function is called via trap + if [[ -n "${temp_dir:-}" ]] && [[ -d "$temp_dir" ]]; then + if [[ "$KEEP_ARCHIVE" != true ]]; then + say_verbose "Cleaning up temporary files..." + rm -rf "$temp_dir" || say_warn "Failed to clean up temporary directory: $temp_dir" + else + printf "Archive files kept in: %s\n" "$temp_dir" + fi fi +} + +# Set trap for cleanup on exit +trap cleanup EXIT + +# Download and install the archive +if ! download_and_install_archive "$temp_dir"; then + exit 1 +fi + +# Handle GitHub Actions environment +if [[ -n "${GITHUB_ACTIONS:-}" ]] && [[ "${GITHUB_ACTIONS}" == "true" ]]; then + if [[ -n "${GITHUB_PATH:-}" ]]; then + echo "$INSTALL_PATH" >> "$GITHUB_PATH" + say_verbose "Added $INSTALL_PATH to \$GITHUB_PATH" + fi +fi + +# Add to shell profile for persistent PATH +add_to_shell_profile "$INSTALL_PATH" "$INSTALL_PATH_UNEXPANDED" + +# Add to current session PATH, if the path is not already in PATH +if [[ ":$PATH:" != *":$INSTALL_PATH:"* ]]; then + export PATH="$INSTALL_PATH:$PATH" fi From c09d588923c400921a2b195b7611d4eee45a0112 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 14 Jul 2025 04:46:56 -0400 Subject: [PATCH 20/30] update ps1 --- eng/scripts/get-aspire-cli.ps1 | 215 ++++++++++++++++++++++++++++----- 1 file changed, 182 insertions(+), 33 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index c5e1458f747..1c1c87fbdee 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -2,7 +2,7 @@ [CmdletBinding()] param( - [string]$OutputPath = "", + [string]$InstallPath = "", [string]$Version = "9.0", [string]$Quality = "daily", [string]$OS = "", @@ -17,16 +17,22 @@ $Script:IsModernPowerShell = $PSVersionTable.PSVersion.Major -ge 6 $Script:ArchiveDownloadTimeoutSec = 600 $Script:ChecksumDownloadTimeoutSec = 120 -# Show help if requested +# Ensure minimum PowerShell version +if ($PSVersionTable.PSVersion.Major -lt 4) { + Write-Host "Error: This script requires PowerShell 4.0 or later. Current version: $($PSVersionTable.PSVersion)" -ForegroundColor Red + exit 1 +} + if ($Help) { Write-Host @" Aspire CLI Download Script DESCRIPTION: - Downloads and unpacks the Aspire CLI for the current platform from the specified version and quality. + Downloads and installs the Aspire CLI for the current platform from the specified version and quality. + Automatically updates the current session's PATH environment variable and supports GitHub Actions. PARAMETERS: - -OutputPath Directory to unpack the CLI (default: aspire-cli directory under current directory) + -InstallPath Directory to install the CLI (default: %USERPROFILE%\.aspire\bin on Windows, $HOME/.aspire/bin on Unix) -Version Version of the Aspire CLI to download (default: 9.0) -Quality Quality to download (default: daily) -OS Operating system (default: auto-detect) @@ -34,9 +40,19 @@ PARAMETERS: -KeepArchive Keep downloaded archive files and temporary directory after installation -Help Show this help message +ENVIRONMENT: + The script automatically updates the PATH environment variable for the current session. + For persistent PATH changes across new terminal sessions, you may need to manually add + the installation path to your shell profile or PowerShell profile. + + GitHub Actions Support: + When running in GitHub Actions (GITHUB_ACTIONS=true), the script will automatically + append the installation path to the GITHUB_PATH file to make the CLI available in + subsequent workflow steps. + EXAMPLES: .\get-aspire-cli.ps1 - .\get-aspire-cli.ps1 -OutputPath "C:\temp" + .\get-aspire-cli.ps1 -InstallPath "C:\tools\aspire" .\get-aspire-cli.ps1 -Version "9.0" -Quality "release" .\get-aspire-cli.ps1 -OS "linux" -Architecture "x64" .\get-aspire-cli.ps1 -KeepArchive @@ -56,6 +72,42 @@ function Say-Verbose($str) { } } +function Say-Info($str) { + try { + Write-Host $str -ForegroundColor White + } + catch { + Write-Output $str + } +} + +function Say-Success($str) { + try { + Write-Host $str -ForegroundColor Green + } + catch { + Write-Output $str + } +} + +function Say-Warning($str) { + try { + Write-Host "Warning: $str" -ForegroundColor Yellow + } + catch { + Write-Output "Warning: $str" + } +} + +function Say-Error($str) { + try { + Write-Host "Error: $str" -ForegroundColor Red + } + catch { + Write-Output "Error: $str" + } +} + # Function to detect OS function Get-OperatingSystem { if ($Script:IsModernPowerShell) { @@ -84,16 +136,16 @@ function Get-OperatingSystem { } } else { - # PowerShell 5.1 and earlier - if ($env:OS -eq "Windows_NT") { + # PowerShell 5.1 and earlier - more reliable Windows detection + if ($env:OS -eq "Windows_NT" -or [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { return "win" } else { $platform = [System.Environment]::OSVersion.Platform - if ($platform -eq 4 -or $platform -eq 6) { + if ($platform -eq [System.PlatformID]::Unix -or $platform -eq 4 -or $platform -eq 6) { return "linux" } - elseif ($platform -eq 128) { + elseif ($platform -eq [System.PlatformID]::MacOSX -or $platform -eq 128) { return "osx" } else { @@ -104,8 +156,8 @@ function Get-OperatingSystem { } # Taken from dotnet-install.ps1 and enhanced for cross-platform support -function Get-Machine-Architecture() { - Say-Verbose $MyInvocation +function Get-MachineArchitecture() { + Say-Verbose "Get-MachineArchitecture called" # On Windows PowerShell, use environment variables if (-not $Script:IsModernPowerShell -or $IsWindows) { @@ -148,9 +200,9 @@ function Get-Machine-Architecture() { if (Get-Command uname -ErrorAction SilentlyContinue) { $unameArch = & uname -m switch ($unameArch) { - { $_ -in @('x86_64', 'amd64') } { return "x64" } - { $_ -in @('aarch64', 'arm64') } { return "arm64" } - { $_ -in @('i386', 'i686') } { return "x86" } + { @('x86_64', 'amd64') -contains $_ } { return "x64" } + { @('aarch64', 'arm64') -contains $_ } { return "arm64" } + { @('i386', 'i686') -contains $_ } { return "x86" } default { Say-Verbose "Unknown uname architecture: $unameArch" return "x64" # Default fallback @@ -162,7 +214,7 @@ function Get-Machine-Architecture() { } } catch { - Write-Warning "Failed to get runtime architecture: $($_.Exception.Message)" + Say-Warning "Failed to get runtime architecture: $($_.Exception.Message)" # Final fallback - assume x64 return "x64" } @@ -173,11 +225,11 @@ function Get-Machine-Architecture() { } # taken from dotnet-install.ps1 -function Get-CLIArchitecture-From-Architecture([string]$Architecture) { - Say-Verbose $MyInvocation +function Get-CLIArchitectureFromArchitecture([string]$Architecture) { + Say-Verbose "Get-CLIArchitectureFromArchitecture called with Architecture: $Architecture" if ($Architecture -eq "") { - $Architecture = Get-Machine-Architecture + $Architecture = Get-MachineArchitecture } switch ($Architecture.ToLowerInvariant()) { @@ -245,7 +297,26 @@ function Invoke-SecureWebRequest { # Configure TLS for PowerShell 5 if (-not $Script:IsModernPowerShell) { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 + try { + # Try to set TLS 1.2 and 1.3, but fallback gracefully if TLS 1.3 is not available + $tls12 = [Net.SecurityProtocolType]::Tls12 + $currentProtocols = $tls12 + + # Try to add TLS 1.3 if available (may not be available on older Windows versions) + try { + $tls13 = [Net.SecurityProtocolType]::Tls13 + $currentProtocols = $tls12 -bor $tls13 + } + catch { + # TLS 1.3 not available, just use TLS 1.2 + Say-Verbose "TLS 1.3 not available, using TLS 1.2 only" + } + + [Net.ServicePointManager]::SecurityProtocol = $currentProtocols + } + catch { + Say-Warning "Failed to configure TLS settings: $($_.Exception.Message)" + } } try { @@ -264,9 +335,30 @@ function Invoke-SecureWebRequest { } if ($Script:IsModernPowerShell) { - $requestParams.SslProtocol = @('Tls12', 'Tls13') - $requestParams.OperationTimeoutSeconds = $OperationTimeoutSec - $requestParams.MaximumRetryCount = $MaxRetries + # Add modern PowerShell parameters with error handling + try { + $requestParams.SslProtocol = @('Tls12', 'Tls13') + } + catch { + # SslProtocol parameter might not be available in all PowerShell 6+ versions + Say-Verbose "SslProtocol parameter not available: $($_.Exception.Message)" + } + + try { + $requestParams.OperationTimeoutSeconds = $OperationTimeoutSec + } + catch { + # OperationTimeoutSeconds might not be available + Say-Verbose "OperationTimeoutSeconds parameter not available: $($_.Exception.Message)" + } + + try { + $requestParams.MaximumRetryCount = $MaxRetries + } + catch { + # MaximumRetryCount might not be available + Say-Verbose "MaximumRetryCount parameter not available: $($_.Exception.Message)" + } } $webResponse = Invoke-WebRequest @requestParams @@ -299,7 +391,7 @@ function Invoke-FileDownload { $contentType = Get-ContentTypeFromHeaders -Headers $headResponse.Headers if ($contentType -and $contentType.ToLowerInvariant().StartsWith("text/html")) { - throw "Server returned HTML content (Content-Type: $contentType) instead of expected file." + throw "Server returned HTML content (Content-Type: $contentType) instead of expected file. Make sure the URL is correct." } } @@ -396,9 +488,37 @@ function Expand-AspireCliArchive { # Main function function Main { try { - # Set default OutputPath if empty - if ([string]::IsNullOrWhiteSpace($OutputPath)) { - $OutputPath = Join-Path (Get-Location) "aspire-cli" + # Set default InstallPath if empty + if ([string]::IsNullOrWhiteSpace($InstallPath)) { + # Get the user's home directory in a cross-platform way + $homeDirectory = if ($Script:IsModernPowerShell) { + # PowerShell 6+ - use $env:HOME on all platforms + if ([string]::IsNullOrWhiteSpace($env:HOME)) { + # Fallback for Windows if HOME is not set + if ($IsWindows -and -not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { + $env:USERPROFILE + } else { + $env:HOME + } + } else { + $env:HOME + } + } else { + # PowerShell 5.1 and earlier - Windows only + if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { + $env:USERPROFILE + } elseif (-not [string]::IsNullOrWhiteSpace($env:HOME)) { + $env:HOME + } else { + $null + } + } + + if ([string]::IsNullOrWhiteSpace($homeDirectory)) { + throw "Unable to determine user home directory. Please specify -InstallPath parameter." + } + + $InstallPath = Join-Path (Join-Path $homeDirectory ".aspire") "bin" } # Create a temporary directory for downloads @@ -422,7 +542,7 @@ function Main { throw "Unsupported operating system. Current platform: $([System.Environment]::OSVersion.Platform)" } - $targetArch = if ([string]::IsNullOrWhiteSpace($Architecture)) { Get-CLIArchitecture-From-Architecture '' } else { Get-CLIArchitecture-From-Architecture $Architecture } + $targetArch = if ([string]::IsNullOrWhiteSpace($Architecture)) { Get-CLIArchitectureFromArchitecture '' } else { Get-CLIArchitectureFromArchitecture $Architecture } # Construct the runtime identifier $runtimeIdentifier = "$targetOS-$targetArch" @@ -439,6 +559,7 @@ function Main { try { # Download the Aspire CLI archive + Say-Info "Downloading from: $url" Invoke-FileDownload -Uri $url -TimeoutSec $Script:ArchiveDownloadTimeoutSec -OutputPath $filename -ValidateContentType -UseTempFile # Download and test the checksum @@ -448,12 +569,35 @@ function Main { Say-Verbose "Successfully downloaded and validated: $filename" # Unpack the archive - Expand-AspireCliArchive -ArchiveFile $filename -DestinationPath $OutputPath -OS $targetOS + Expand-AspireCliArchive -ArchiveFile $filename -DestinationPath $InstallPath -OS $targetOS $cliExe = if ($targetOS -eq "win") { "aspire.exe" } else { "aspire" } - $cliPath = Join-Path $OutputPath $cliExe + $cliPath = Join-Path $InstallPath $cliExe + + Say-Success "Aspire CLI successfully installed to: $cliPath" - Write-Host "Aspire CLI successfully unpacked to: $cliPath" -ForegroundColor Green + # Update PATH environment variable for the current session + $currentPath = $env:PATH + $pathSeparator = [System.IO.Path]::PathSeparator + $pathEntries = $currentPath.Split($pathSeparator, [StringSplitOptions]::RemoveEmptyEntries) + + if ($pathEntries -notcontains $InstallPath) { + $env:PATH = "$InstallPath$pathSeparator$currentPath" + Say-Info "Added $InstallPath to PATH for current session" + } else { + Say-Info "Path $InstallPath already exists in PATH, skipping addition" + } + + # GitHub Actions support + if ($env:GITHUB_ACTIONS -eq "true" -and $env:GITHUB_PATH) { + try { + Add-Content -Path $env:GITHUB_PATH -Value $InstallPath + Say-Info "Added $InstallPath to GITHUB_PATH for GitHub Actions" + } + catch { + Say-Warning "Failed to update GITHUB_PATH: $($_.Exception.Message)" + } + } } finally { # Clean up temporary directory and downloaded files @@ -464,23 +608,28 @@ function Main { Remove-Item $tempDir -Recurse -Force -ErrorAction Stop } catch { - Write-Warning "Failed to clean up temporary directory: $tempDir - $($_.Exception.Message)" + Say-Warning "Failed to clean up temporary directory: $tempDir - $($_.Exception.Message)" } } else { - Write-Host "Archive files kept in: $tempDir" -ForegroundColor Yellow + Say-Info "Archive files kept in: $tempDir" } } } } catch { - Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red + Say-Error $_.Exception.Message throw } } # Run main function and handle exit code try { + # Ensure we're not in strict mode which can cause issues in PowerShell 5.1 + if (-not $Script:IsModernPowerShell) { + Set-StrictMode -Off + } + Main exit 0 } From e85ae0bc69c507f6c0936b4db9bed1452891738c Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 14 Jul 2025 05:22:31 -0400 Subject: [PATCH 21/30] update README.md --- eng/scripts/README.md | 77 ++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/eng/scripts/README.md b/eng/scripts/README.md index 25b33d5f9ad..e5c66d7105d 100644 --- a/eng/scripts/README.md +++ b/eng/scripts/README.md @@ -19,51 +19,54 @@ Other version/quality combinations are not yet available through the download UR ### Bash Script (`get-aspire-cli.sh`) -| Parameter | Short | Description | Default | -|------------------|-------|---------------------------------------------------|----------------| -| `--output-path` | `-o` | Directory to unpack the CLI | `./aspire-cli` | -| `--version` | | Version of the Aspire CLI to download | `9.0` | -| `--quality` | `-q` | Quality to download | `daily` | -| `--os` | | Operating system (auto-detected if not specified) | auto-detect | -| `--architecture` | | Architecture (auto-detected if not specified) | auto-detect | -| `--keep-archive` | `-k` | Keep downloaded archive files after installation | `false` | -| `--verbose` | `-v` | Enable verbose output | `false` | -| `--help` | `-h` | Show help message | | +| Parameter | Short | Description | Default | +|------------------|-------|---------------------------------------------------|-----------------------| +| `--install-path` | `-i` | Directory to install the CLI | `$HOME/.aspire/bin` | +| `--version` | | Version of the Aspire CLI to download | `9.0` | +| `--quality` | `-q` | Quality to download | `daily` | +| `--os` | | Operating system (auto-detected if not specified) | auto-detect | +| `--arch` | | Architecture (auto-detected if not specified) | auto-detect | +| `--keep-archive` | `-k` | Keep downloaded archive files after installation | `false` | +| `--verbose` | `-v` | Enable verbose output | `false` | +| `--help` | `-h` | Show help message | | ### PowerShell Script (`get-aspire-cli.ps1`) -| Parameter | Description | Default | -|-----------------|---------------------------------------------------|----------------| -| `-OutputPath` | Directory to unpack the CLI | `./aspire-cli` | -| `-Version` | Version of the Aspire CLI to download | `9.0` | -| `-Quality` | Quality to download | `daily` | -| `-OS` | Operating system (auto-detected if not specified) | auto-detect | -| `-Architecture` | Architecture (auto-detected if not specified) | auto-detect | -| `-KeepArchive` | Keep downloaded archive files after installation | `false` | -| `-Help` | Show help message | | +| Parameter | Description | Default | +|-----------------|---------------------------------------------------|----------------------------------| +| `-InstallPath` | Directory to install the CLI | `$HOME/.aspire/bin` (Unix) / `%USERPROFILE%\.aspire\bin` (Windows) | +| `-Version` | Version of the Aspire CLI to download | `9.0` | +| `-Quality` | Quality to download | `daily` | +| `-OS` | Operating system (auto-detected if not specified) | auto-detect | +| `-Architecture` | Architecture (auto-detected if not specified) | auto-detect | +| `-KeepArchive` | Keep downloaded archive files after installation | `false` | +| `-Help` | Show help message | | -## Output Path Parameter +## Install Path Parameter -The `--output-path` (bash) or `-OutputPath` (PowerShell) parameter specifies where the Aspire CLI will be unpacked: +The `--install-path` (bash) or `-InstallPath` (PowerShell) parameter specifies where the Aspire CLI will be installed: -- **Default behavior**: Creates an `aspire-cli` directory in the current working directory +- **Default behavior**: + - **Unix systems**: `$HOME/.aspire/bin` + - **Windows**: `%USERPROFILE%\.aspire\bin` - **Custom path**: You can specify any directory path where you want the CLI installed - **Directory creation**: The scripts will automatically create the directory if it doesn't exist +- **PATH integration**: The scripts automatically update the current session's PATH and add to shell profiles for persistent access - **Final location**: The CLI executable will be placed directly in the specified directory as: - `aspire` (on Linux/macOS) - `aspire.exe` (on Windows) -### Example Output Paths +### Example Install Paths ```bash -# Default - creates ./aspire-cli/aspire +# Default - installs to $HOME/.aspire/bin/aspire ./get-aspire-cli.sh -# Custom path - creates /usr/local/bin/aspire -./get-aspire-cli.sh --output-path "/usr/local/bin" +# Custom path - installs to /usr/local/bin/aspire +./get-aspire-cli.sh --install-path "/usr/local/bin" -# Relative path - creates ../tools/aspire-cli/aspire -./get-aspire-cli.sh --output-path "../tools/aspire-cli" +# Relative path - installs to ../tools/aspire-cli/aspire +./get-aspire-cli.sh --install-path "../tools/aspire-cli" ``` ## Usage Examples @@ -71,11 +74,11 @@ The `--output-path` (bash) or `-OutputPath` (PowerShell) parameter specifies whe ### Bash Script Examples ```bash -# Basic usage - download to default location (./aspire-cli) +# Basic usage - download to default location ($HOME/.aspire/bin) ./get-aspire-cli.sh -# Specify custom output directory -./get-aspire-cli.sh --output-path "/usr/local/bin" +# Specify custom install directory +./get-aspire-cli.sh --install-path "/usr/local/bin" # Download with verbose output ./get-aspire-cli.sh --verbose @@ -84,20 +87,20 @@ The `--output-path` (bash) or `-OutputPath` (PowerShell) parameter specifies whe ./get-aspire-cli.sh --keep-archive # Force specific OS and architecture (useful for cross-compilation scenarios) -./get-aspire-cli.sh --os "linux" --architecture "x64" +./get-aspire-cli.sh --os "linux" --arch "x64" # Combine multiple options -./get-aspire-cli.sh --output-path "/tmp/aspire" --verbose --keep-archive +./get-aspire-cli.sh --install-path "/tmp/aspire" --verbose --keep-archive ``` ### PowerShell Script Examples ```powershell -# Basic usage - download to default location (./aspire-cli) +# Basic usage - download to default location (%USERPROFILE%\.aspire\bin or $HOME/.aspire/bin) .\get-aspire-cli.ps1 -# Specify custom output directory -.\get-aspire-cli.ps1 -OutputPath "C:\Tools\Aspire" +# Specify custom install directory +.\get-aspire-cli.ps1 -InstallPath "C:\Tools\Aspire" # Download with verbose output .\get-aspire-cli.ps1 -Verbose @@ -109,7 +112,7 @@ The `--output-path` (bash) or `-OutputPath` (PowerShell) parameter specifies whe .\get-aspire-cli.ps1 -OS "win" -Architecture "x64" # Combine multiple options -.\get-aspire-cli.ps1 -OutputPath "C:\temp\aspire" -Verbose -KeepArchive +.\get-aspire-cli.ps1 -InstallPath "C:\temp\aspire" -Verbose -KeepArchive ``` ## Supported Runtime Identifiers From 2a43c92b1852472ff92222402a448b0a57ac81df Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 14 Jul 2025 14:58:45 -0400 Subject: [PATCH 22/30] Update eng/scripts/get-aspire-cli.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- eng/scripts/get-aspire-cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index 2688b74f221..061762cfc18 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -423,7 +423,7 @@ add_to_path() echo "$command" >> "$config_file" say_info "Successfully added aspire to \$PATH in $config_file" else - say_info "Manually add the to $config_file (or similar):" + say_info "Manually add the following to $config_file (or similar):" say_info " $command" fi } From 3baae6e2b2dc2f5020b9dd9630790775def2417c Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 14 Jul 2025 18:31:23 -0400 Subject: [PATCH 23/30] Add the install path to the PATH env var also --- eng/scripts/get-aspire-cli.ps1 | 90 +++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index 1c1c87fbdee..7c33ec39dbb 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -42,8 +42,9 @@ PARAMETERS: ENVIRONMENT: The script automatically updates the PATH environment variable for the current session. - For persistent PATH changes across new terminal sessions, you may need to manually add - the installation path to your shell profile or PowerShell profile. + + Windows: The script will also add the installation path to the user's persistent PATH + environment variable, making the aspire CLI available in new terminal sessions. GitHub Actions Support: When running in GitHub Actions (GITHUB_ACTIONS=true), the script will automatically @@ -485,6 +486,67 @@ function Expand-AspireCliArchive { } } +# Update PATH environment variables for the current session and persistently on Windows +function Update-PathEnvironment { + param( + [Parameter(Mandatory = $true)] + [string]$InstallPath, + [Parameter(Mandatory = $true)] + [string]$TargetOS + ) + + # Update PATH environment variable for the current session + $currentPath = $env:PATH + $pathSeparator = [System.IO.Path]::PathSeparator + $pathEntries = $currentPath.Split($pathSeparator, [StringSplitOptions]::RemoveEmptyEntries) + + if ($pathEntries -notcontains $InstallPath) { + $env:PATH = "$InstallPath$pathSeparator$currentPath" + Say-Info "Added $InstallPath to PATH for current session" + } + + # Update persistent PATH for Windows + if ($TargetOS -eq "win") { + try { + # Get the current user PATH from registry + $userPath = [Environment]::GetEnvironmentVariable("PATH", [EnvironmentVariableTarget]::User) + if ([string]::IsNullOrEmpty($userPath)) { + $userPath = "" + } + + $userPathEntries = $userPath.Split($pathSeparator, [StringSplitOptions]::RemoveEmptyEntries) + + if ($userPathEntries -notcontains $InstallPath) { + # Add to user PATH + $newUserPath = if ([string]::IsNullOrEmpty($userPath)) { + $InstallPath + } else { + "$InstallPath$pathSeparator$userPath" + } + + [Environment]::SetEnvironmentVariable("PATH", $newUserPath, [EnvironmentVariableTarget]::User) + Say-Success "Added $InstallPath to user PATH environment variable" + Say-Info "The aspire CLI will be available in new terminal sessions" + } + } + catch { + Say-Warning "Failed to update persistent PATH environment variable: $($_.Exception.Message)" + Say-Info "You may need to manually add '$InstallPath' to your PATH environment variable" + } + } + + # GitHub Actions support + if ($env:GITHUB_ACTIONS -eq "true" -and $env:GITHUB_PATH) { + try { + Add-Content -Path $env:GITHUB_PATH -Value $InstallPath + Say-Info "Added $InstallPath to GITHUB_PATH for GitHub Actions" + } + catch { + Say-Warning "Failed to update GITHUB_PATH: $($_.Exception.Message)" + } + } +} + # Main function function Main { try { @@ -576,28 +638,8 @@ function Main { Say-Success "Aspire CLI successfully installed to: $cliPath" - # Update PATH environment variable for the current session - $currentPath = $env:PATH - $pathSeparator = [System.IO.Path]::PathSeparator - $pathEntries = $currentPath.Split($pathSeparator, [StringSplitOptions]::RemoveEmptyEntries) - - if ($pathEntries -notcontains $InstallPath) { - $env:PATH = "$InstallPath$pathSeparator$currentPath" - Say-Info "Added $InstallPath to PATH for current session" - } else { - Say-Info "Path $InstallPath already exists in PATH, skipping addition" - } - - # GitHub Actions support - if ($env:GITHUB_ACTIONS -eq "true" -and $env:GITHUB_PATH) { - try { - Add-Content -Path $env:GITHUB_PATH -Value $InstallPath - Say-Info "Added $InstallPath to GITHUB_PATH for GitHub Actions" - } - catch { - Say-Warning "Failed to update GITHUB_PATH: $($_.Exception.Message)" - } - } + # Update PATH environment variables + Update-PathEnvironment -InstallPath $InstallPath -TargetOS $targetOS } finally { # Clean up temporary directory and downloaded files From ef81d430f9bfd1a1ecd572a7c5a9499c1dccde46 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 14 Jul 2025 18:33:29 -0400 Subject: [PATCH 24/30] refactor to extract function to get install path --- eng/scripts/get-aspire-cli.ps1 | 76 ++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index 7c33ec39dbb..64a56c279ea 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -486,6 +486,48 @@ function Expand-AspireCliArchive { } } +# Determine the installation path based on user input or default location +function Get-InstallPath { + param( + [string]$InstallPath + ) + + # Set default InstallPath if empty + if ([string]::IsNullOrWhiteSpace($InstallPath)) { + # Get the user's home directory in a cross-platform way + $homeDirectory = if ($Script:IsModernPowerShell) { + # PowerShell 6+ - use $env:HOME on all platforms + if ([string]::IsNullOrWhiteSpace($env:HOME)) { + # Fallback for Windows if HOME is not set + if ($IsWindows -and -not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { + $env:USERPROFILE + } else { + $env:HOME + } + } else { + $env:HOME + } + } else { + # PowerShell 5.1 and earlier - Windows only + if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { + $env:USERPROFILE + } elseif (-not [string]::IsNullOrWhiteSpace($env:HOME)) { + $env:HOME + } else { + $null + } + } + + if ([string]::IsNullOrWhiteSpace($homeDirectory)) { + throw "Unable to determine user home directory. Please specify -InstallPath parameter." + } + + $InstallPath = Join-Path (Join-Path $homeDirectory ".aspire") "bin" + } + + return $InstallPath +} + # Update PATH environment variables for the current session and persistently on Windows function Update-PathEnvironment { param( @@ -550,38 +592,8 @@ function Update-PathEnvironment { # Main function function Main { try { - # Set default InstallPath if empty - if ([string]::IsNullOrWhiteSpace($InstallPath)) { - # Get the user's home directory in a cross-platform way - $homeDirectory = if ($Script:IsModernPowerShell) { - # PowerShell 6+ - use $env:HOME on all platforms - if ([string]::IsNullOrWhiteSpace($env:HOME)) { - # Fallback for Windows if HOME is not set - if ($IsWindows -and -not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { - $env:USERPROFILE - } else { - $env:HOME - } - } else { - $env:HOME - } - } else { - # PowerShell 5.1 and earlier - Windows only - if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { - $env:USERPROFILE - } elseif (-not [string]::IsNullOrWhiteSpace($env:HOME)) { - $env:HOME - } else { - $null - } - } - - if ([string]::IsNullOrWhiteSpace($homeDirectory)) { - throw "Unable to determine user home directory. Please specify -InstallPath parameter." - } - - $InstallPath = Join-Path (Join-Path $homeDirectory ".aspire") "bin" - } + # Determine the installation path + $InstallPath = Get-InstallPath -InstallPath $InstallPath # Create a temporary directory for downloads $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "aspire-cli-download-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8))" From 5b01fe0faa37b2b4feb149de934cdf301236ff63 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 14 Jul 2025 18:35:51 -0400 Subject: [PATCH 25/30] Extract method to download and install the archive --- eng/scripts/get-aspire-cli.ps1 | 109 ++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index 64a56c279ea..b44b9d04be5 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -589,25 +589,34 @@ function Update-PathEnvironment { } } -# Main function -function Main { - try { - # Determine the installation path - $InstallPath = Get-InstallPath -InstallPath $InstallPath +# Function to download and install the Aspire CLI +function Install-AspireCli { + param( + [Parameter(Mandatory = $true)] + [string]$InstallPath, + [Parameter(Mandatory = $true)] + [string]$Version, + [Parameter(Mandatory = $true)] + [string]$Quality, + [string]$OS, + [string]$Architecture, + [switch]$KeepArchive + ) - # Create a temporary directory for downloads - $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "aspire-cli-download-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8))" + # Create a temporary directory for downloads + $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "aspire-cli-download-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8))" - if (-not (Test-Path $tempDir)) { - Say-Verbose "Creating temporary directory: $tempDir" - try { - New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - } - catch { - throw "Failed to create temporary directory: $tempDir - $($_.Exception.Message)" - } + if (-not (Test-Path $tempDir)) { + Say-Verbose "Creating temporary directory: $tempDir" + try { + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + } + catch { + throw "Failed to create temporary directory: $tempDir - $($_.Exception.Message)" } + } + try { # Determine OS and architecture (either detected or user-specified) $targetOS = if ([string]::IsNullOrWhiteSpace($OS)) { Get-OperatingSystem } else { $OS } @@ -631,46 +640,58 @@ function Main { $filename = Join-Path $tempDir "aspire-cli-$runtimeIdentifier.$extension" $checksumFilename = Join-Path $tempDir "aspire-cli-$runtimeIdentifier.$extension.sha512" - try { - # Download the Aspire CLI archive - Say-Info "Downloading from: $url" - Invoke-FileDownload -Uri $url -TimeoutSec $Script:ArchiveDownloadTimeoutSec -OutputPath $filename -ValidateContentType -UseTempFile + # Download the Aspire CLI archive + Say-Info "Downloading from: $url" + Invoke-FileDownload -Uri $url -TimeoutSec $Script:ArchiveDownloadTimeoutSec -OutputPath $filename -ValidateContentType -UseTempFile - # Download and test the checksum - Invoke-FileDownload -Uri $checksumUrl -TimeoutSec $Script:ChecksumDownloadTimeoutSec -OutputPath $checksumFilename -ValidateContentType -UseTempFile - Test-FileChecksum -ArchiveFile $filename -ChecksumFile $checksumFilename + # Download and test the checksum + Invoke-FileDownload -Uri $checksumUrl -TimeoutSec $Script:ChecksumDownloadTimeoutSec -OutputPath $checksumFilename -ValidateContentType -UseTempFile + Test-FileChecksum -ArchiveFile $filename -ChecksumFile $checksumFilename - Say-Verbose "Successfully downloaded and validated: $filename" + Say-Verbose "Successfully downloaded and validated: $filename" - # Unpack the archive - Expand-AspireCliArchive -ArchiveFile $filename -DestinationPath $InstallPath -OS $targetOS + # Unpack the archive + Expand-AspireCliArchive -ArchiveFile $filename -DestinationPath $InstallPath -OS $targetOS - $cliExe = if ($targetOS -eq "win") { "aspire.exe" } else { "aspire" } - $cliPath = Join-Path $InstallPath $cliExe + $cliExe = if ($targetOS -eq "win") { "aspire.exe" } else { "aspire" } + $cliPath = Join-Path $InstallPath $cliExe - Say-Success "Aspire CLI successfully installed to: $cliPath" + Say-Success "Aspire CLI successfully installed to: $cliPath" - # Update PATH environment variables - Update-PathEnvironment -InstallPath $InstallPath -TargetOS $targetOS - } - finally { - # Clean up temporary directory and downloaded files - if (Test-Path $tempDir -ErrorAction SilentlyContinue) { - if (-not $KeepArchive) { - try { - Say-Verbose "Cleaning up temporary files..." - Remove-Item $tempDir -Recurse -Force -ErrorAction Stop - } - catch { - Say-Warning "Failed to clean up temporary directory: $tempDir - $($_.Exception.Message)" - } + # Return the target OS for the caller to use + return $targetOS + } + finally { + # Clean up temporary directory and downloaded files + if (Test-Path $tempDir -ErrorAction SilentlyContinue) { + if (-not $KeepArchive) { + try { + Say-Verbose "Cleaning up temporary files..." + Remove-Item $tempDir -Recurse -Force -ErrorAction Stop } - else { - Say-Info "Archive files kept in: $tempDir" + catch { + Say-Warning "Failed to clean up temporary directory: $tempDir - $($_.Exception.Message)" } } + else { + Say-Info "Archive files kept in: $tempDir" + } } } +} + +# Main function +function Main { + try { + # Determine the installation path + $InstallPath = Get-InstallPath -InstallPath $InstallPath + + # Download and install the Aspire CLI + $targetOS = Install-AspireCli -InstallPath $InstallPath -Version $Version -Quality $Quality -OS $OS -Architecture $Architecture -KeepArchive:$KeepArchive + + # Update PATH environment variables + Update-PathEnvironment -InstallPath $InstallPath -TargetOS $targetOS + } catch { Say-Error $_.Exception.Message throw From 323b9bc9e412cbf81a13e1969dd47f22a2b2b2e8 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 14 Jul 2025 21:50:51 -0400 Subject: [PATCH 26/30] some cleanup and refactoring --- eng/scripts/get-aspire-cli.ps1 | 446 ++++++++++++++------------------- 1 file changed, 185 insertions(+), 261 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index b44b9d04be5..dcacd543132 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -44,7 +44,7 @@ ENVIRONMENT: The script automatically updates the PATH environment variable for the current session. Windows: The script will also add the installation path to the user's persistent PATH - environment variable, making the aspire CLI available in new terminal sessions. + environment variable and to the session PATH, making the aspire CLI available in the existing and new terminal sessions. GitHub Actions Support: When running in GitHub Actions (GITHUB_ACTIONS=true), the script will automatically @@ -63,102 +63,76 @@ EXAMPLES: exit 0 } -function Say-Verbose($str) { - try { - Write-Verbose $str - } - catch { - # Some platforms cannot utilize Write-Verbose (Azure Functions, for instance). Fall back to Write-Output - Write-Output $str - } -} - -function Say-Info($str) { - try { - Write-Host $str -ForegroundColor White - } - catch { - Write-Output $str - } -} +# Consolidated output function with fallback for platforms that don't support Write-Host +function Write-Message { + param( + [Parameter(Mandatory = $true)] + [string]$Message, + [ValidateSet('Verbose', 'Info', 'Success', 'Warning', 'Error')] + [string]$Level = 'Info' + ) -function Say-Success($str) { try { - Write-Host $str -ForegroundColor Green + switch ($Level) { + 'Verbose' { Write-Verbose $Message } + 'Info' { Write-Host $Message -ForegroundColor White } + 'Success' { Write-Host $Message -ForegroundColor Green } + 'Warning' { Write-Host "Warning: $Message" -ForegroundColor Yellow } + 'Error' { Write-Host "Error: $Message" -ForegroundColor Red } + } } catch { - Write-Output $str + # Fallback for platforms that don't support Write-Host (e.g., Azure Functions) + $prefix = if ($Level -in @('Warning', 'Error')) { "$Level`: " } else { "" } + Write-Output "$prefix$Message" } } -function Say-Warning($str) { - try { - Write-Host "Warning: $str" -ForegroundColor Yellow - } - catch { - Write-Output "Warning: $str" - } -} +# Helper function for PowerShell version-specific operations +function Invoke-WithPowerShellVersion { + param( + [scriptblock]$ModernAction, + [scriptblock]$LegacyAction + ) -function Say-Error($str) { - try { - Write-Host "Error: $str" -ForegroundColor Red - } - catch { - Write-Output "Error: $str" + if ($Script:IsModernPowerShell) { + & $ModernAction + } else { + & $LegacyAction } } # Function to detect OS function Get-OperatingSystem { - if ($Script:IsModernPowerShell) { - if ($IsWindows) { - return "win" - } + Invoke-WithPowerShellVersion -ModernAction { + if ($IsWindows) { return "win" } elseif ($IsLinux) { try { $lddOutput = & ldd --version 2>&1 | Out-String - if ($lddOutput -match "musl") { - return "linux-musl" - } - else { - return "linux" - } + return if ($lddOutput -match "musl") { "linux-musl" } else { "linux" } } - catch { - return "linux" - } - } - elseif ($IsMacOS) { - return "osx" - } - else { - return "unsupported" + catch { return "linux" } } - } - else { + elseif ($IsMacOS) { return "osx" } + else { return "unsupported" } + } -LegacyAction { # PowerShell 5.1 and earlier - more reliable Windows detection if ($env:OS -eq "Windows_NT" -or [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { return "win" } - else { - $platform = [System.Environment]::OSVersion.Platform - if ($platform -eq [System.PlatformID]::Unix -or $platform -eq 4 -or $platform -eq 6) { - return "linux" - } - elseif ($platform -eq [System.PlatformID]::MacOSX -or $platform -eq 128) { - return "osx" - } - else { - return "unsupported" - } + + $platform = [System.Environment]::OSVersion.Platform + switch ($platform) { + { $_ -in @([System.PlatformID]::Unix, 4, 6) } { return "linux" } + { $_ -in @([System.PlatformID]::MacOSX, 128) } { return "osx" } + default { return "unsupported" } } } } # Taken from dotnet-install.ps1 and enhanced for cross-platform support function Get-MachineArchitecture() { - Say-Verbose "Get-MachineArchitecture called" + Write-Message "Get-MachineArchitecture called" -Level Verbose # On Windows PowerShell, use environment variables if (-not $Script:IsModernPowerShell -or $IsWindows) { @@ -196,7 +170,7 @@ function Get-MachineArchitecture() { 'X86' { return "x86" } 'Arm64' { return "arm64" } default { - Say-Verbose "Unknown runtime architecture: $runtimeArch" + Write-Message "Unknown runtime architecture: $runtimeArch" -Level Verbose # Fall back to uname if available if (Get-Command uname -ErrorAction SilentlyContinue) { $unameArch = & uname -m @@ -205,7 +179,7 @@ function Get-MachineArchitecture() { { @('aarch64', 'arm64') -contains $_ } { return "arm64" } { @('i386', 'i686') -contains $_ } { return "x86" } default { - Say-Verbose "Unknown uname architecture: $unameArch" + Write-Message "Unknown uname architecture: $unameArch" -Level Verbose return "x64" # Default fallback } } @@ -215,7 +189,7 @@ function Get-MachineArchitecture() { } } catch { - Say-Warning "Failed to get runtime architecture: $($_.Exception.Message)" + Write-Message "Failed to get runtime architecture: $($_.Exception.Message)" -Level Warning # Final fallback - assume x64 return "x64" } @@ -225,64 +199,63 @@ function Get-MachineArchitecture() { return "x64" } -# taken from dotnet-install.ps1 +# taken from dotnet-install.ps1 - simplified architecture detection function Get-CLIArchitectureFromArchitecture([string]$Architecture) { - Say-Verbose "Get-CLIArchitectureFromArchitecture called with Architecture: $Architecture" + Write-Message "Get-CLIArchitectureFromArchitecture called with Architecture: $Architecture" -Level Verbose if ($Architecture -eq "") { $Architecture = Get-MachineArchitecture } - switch ($Architecture.ToLowerInvariant()) { - { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } - { $_ -eq "x86" } { return "x86" } - { $_ -eq "arm64" } { return "arm64" } - default { throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues" } + $archMap = @{ + 'amd64' = 'x64' + 'x64' = 'x64' + 'x86' = 'x86' + 'arm64' = 'arm64' + } + + $normalizedArch = $Architecture.ToLowerInvariant() + if ($archMap.ContainsKey($normalizedArch)) { + return $archMap[$normalizedArch] } + + throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues" } -# Helper function to extract Content-Type from response headers +# Function to get Content-Type from response headers by making a HEAD request function Get-ContentTypeFromHeaders { param( - [object]$Headers + [Parameter(Mandatory = $true)] + [string]$Uri, + [int]$TimeoutSec = 60, + [int]$OperationTimeoutSec = 30, + [int]$MaxRetries = 5 ) - if (-not $Headers) { - return "" - } - try { - if ($Script:IsModernPowerShell) { - # PowerShell 6+: Try different case variations - if ($Headers.ContainsKey('Content-Type')) { - return $Headers['Content-Type'] -join ', ' - } - elseif ($Headers.ContainsKey('content-type')) { - return $Headers['content-type'] -join ', ' - } - else { - # Case-insensitive search - $ctHeader = $Headers.Keys | Where-Object { $_ -ieq 'Content-Type' } | Select-Object -First 1 - if ($ctHeader) { - return $Headers[$ctHeader] -join ', ' + Write-Message "Making HEAD request to get content type for: $Uri" -Level Verbose + $headResponse = Invoke-SecureWebRequest -Uri $Uri -Method 'Head' -TimeoutSec $TimeoutSec -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries + + # Extract Content-Type from response headers + $headers = $headResponse.Headers + if ($headers) { + # Try common case variations and use case-insensitive lookup + $contentTypeKey = $headers.Keys | Where-Object { $_ -ieq 'Content-Type' } | Select-Object -First 1 + if ($contentTypeKey) { + $value = $headers[$contentTypeKey] + if ($value -is [array]) { + return $value -join ', ' + } else { + return $value } } } - else { - # PowerShell 5: Use different access methods - if ($Headers['Content-Type']) { - return $Headers['Content-Type'] - } - else { - return $Headers.Get('Content-Type') - } - } + return "" } catch { + Write-Message "Failed to get content type from URI: $($_.Exception.Message)" -Level Verbose return "Unable to determine ($($_.Exception.Message))" } - - return "" } # Common function for web requests with centralized configuration @@ -299,78 +272,62 @@ function Invoke-SecureWebRequest { # Configure TLS for PowerShell 5 if (-not $Script:IsModernPowerShell) { try { - # Try to set TLS 1.2 and 1.3, but fallback gracefully if TLS 1.3 is not available - $tls12 = [Net.SecurityProtocolType]::Tls12 - $currentProtocols = $tls12 - - # Try to add TLS 1.3 if available (may not be available on older Windows versions) + # Set TLS 1.2 and attempt TLS 1.3 if available + $protocols = [Net.SecurityProtocolType]::Tls12 try { - $tls13 = [Net.SecurityProtocolType]::Tls13 - $currentProtocols = $tls12 -bor $tls13 + $protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13 } catch { - # TLS 1.3 not available, just use TLS 1.2 - Say-Verbose "TLS 1.3 not available, using TLS 1.2 only" + Write-Message "TLS 1.3 not available, using TLS 1.2 only" -Level Verbose } - - [Net.ServicePointManager]::SecurityProtocol = $currentProtocols + [Net.ServicePointManager]::SecurityProtocol = $protocols } catch { - Say-Warning "Failed to configure TLS settings: $($_.Exception.Message)" + Write-Message "Failed to configure TLS settings: $($_.Exception.Message)" -Level Warning } } - try { - # Build request parameters - $requestParams = @{ - Uri = $Uri - Method = $Method - MaximumRedirection = 10 - TimeoutSec = $TimeoutSec - UserAgent = $Script:UserAgent - } - - # Add OutFile only for GET requests - if ($Method -eq 'Get' -and $OutFile) { - $requestParams.OutFile = $OutFile - } + # Build base request parameters + $requestParams = @{ + Uri = $Uri + Method = $Method + MaximumRedirection = 10 + TimeoutSec = $TimeoutSec + UserAgent = $Script:UserAgent + } - if ($Script:IsModernPowerShell) { - # Add modern PowerShell parameters with error handling - try { - $requestParams.SslProtocol = @('Tls12', 'Tls13') - } - catch { - # SslProtocol parameter might not be available in all PowerShell 6+ versions - Say-Verbose "SslProtocol parameter not available: $($_.Exception.Message)" - } + if ($Method -eq 'Get' -and $OutFile) { + $requestParams.OutFile = $OutFile + } - try { - $requestParams.OperationTimeoutSeconds = $OperationTimeoutSec - } - catch { - # OperationTimeoutSeconds might not be available - Say-Verbose "OperationTimeoutSeconds parameter not available: $($_.Exception.Message)" + # Add modern PowerShell parameters with graceful fallback + if ($Script:IsModernPowerShell) { + @('SslProtocol', 'OperationTimeoutSeconds', 'MaximumRetryCount') | ForEach-Object { + $paramName = $_ + $paramValue = switch ($paramName) { + 'SslProtocol' { @('Tls12', 'Tls13') } + 'OperationTimeoutSeconds' { $OperationTimeoutSec } + 'MaximumRetryCount' { $MaxRetries } } try { - $requestParams.MaximumRetryCount = $MaxRetries + $requestParams[$paramName] = $paramValue } catch { - # MaximumRetryCount might not be available - Say-Verbose "MaximumRetryCount parameter not available: $($_.Exception.Message)" + Write-Message "$paramName parameter not available: $($_.Exception.Message)" -Level Verbose } } + } - $webResponse = Invoke-WebRequest @requestParams - return $webResponse + try { + return Invoke-WebRequest @requestParams } catch { throw $_.Exception } } -# General-purpose file download wrapper +# Simplified file download wrapper function Invoke-FileDownload { param( [Parameter(Mandatory = $true)] @@ -380,36 +337,24 @@ function Invoke-FileDownload { [int]$TimeoutSec = 60, [int]$OperationTimeoutSec = 30, [int]$MaxRetries = 5, - [switch]$ValidateContentType, - [switch]$UseTempFile + [switch]$ValidateContentType ) - try { - # Validate content type via HEAD request if requested - if ($ValidateContentType) { - Say-Verbose "Validating content type for $Uri" - $headResponse = Invoke-SecureWebRequest -Uri $Uri -Method 'Head' -TimeoutSec 60 -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries - $contentType = Get-ContentTypeFromHeaders -Headers $headResponse.Headers - - if ($contentType -and $contentType.ToLowerInvariant().StartsWith("text/html")) { - throw "Server returned HTML content (Content-Type: $contentType) instead of expected file. Make sure the URL is correct." - } - } + # Validate content type via HEAD request if requested + if ($ValidateContentType) { + Write-Message "Validating content type for $Uri" -Level Verbose + $contentType = Get-ContentTypeFromHeaders -Uri $Uri -TimeoutSec 60 -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries + Write-Message "Detected content type: '$contentType'" -Level Verbose - $targetFile = $OutputPath - if ($UseTempFile) { - $targetFile = "$OutputPath.tmp" - } - - Say-Verbose "Downloading $Uri to $targetFile" - Invoke-SecureWebRequest -Uri $Uri -OutFile $targetFile -TimeoutSec $TimeoutSec -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries - - # Move temp file to final location if using temp file - if ($UseTempFile) { - Move-Item $targetFile $OutputPath + if ($contentType -and $contentType.ToLowerInvariant().StartsWith("text/html")) { + throw "Server returned HTML content instead of expected file. Make sure the URL is correct: $Uri" } + } - Say-Verbose "Successfully downloaded file to: $OutputPath" + try { + Write-Message "Downloading $Uri to $OutputPath" -Level Verbose + Invoke-SecureWebRequest -Uri $Uri -OutFile $OutputPath -TimeoutSec $TimeoutSec -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries + Write-Message "Successfully downloaded file to: $OutputPath" -Level Verbose } catch { throw "Failed to download $Uri to $OutputPath - $($_.Exception.Message)" @@ -431,12 +376,10 @@ function Test-FileChecksum { $expectedChecksum = (Get-Content $ChecksumFile -Raw).Trim().ToLower() $actualChecksum = (Get-FileHash -Path $ArchiveFile -Algorithm SHA512).Hash.ToLower() - # Limit expected checksum display to 128 characters for output - $expectedChecksumDisplay = if ($expectedChecksum.Length -gt 128) { $expectedChecksum.Substring(0, 128) } else { $expectedChecksum } - # Compare checksums if ($expectedChecksum -ne $actualChecksum) { - throw "Checksum validation failed for $ArchiveFile with checksum from $ChecksumFile !`nExpected: $expectedChecksumDisplay`nActual: $actualChecksum" + $displayChecksum = if ($expectedChecksum.Length -gt 128) { $expectedChecksum.Substring(0, 128) + "..." } else { $expectedChecksum } + throw "Checksum validation failed for $ArchiveFile with checksum from $ChecksumFile !`nExpected: $displayChecksum`nActual: $actualChecksum" } } @@ -447,7 +390,7 @@ function Expand-AspireCliArchive { [string]$OS ) - Say-Verbose "Unpacking archive to: $DestinationPath" + Write-Message "Unpacking archive to: $DestinationPath" -Level Verbose try { # Create destination directory if it doesn't exist @@ -479,56 +422,50 @@ function Expand-AspireCliArchive { } } - Say-Verbose "Successfully unpacked archive" + Write-Message "Successfully unpacked archive" -Level Verbose } catch { throw "Failed to unpack archive: $($_.Exception.Message)" } } -# Determine the installation path based on user input or default location +# Simplified installation path determination function Get-InstallPath { - param( - [string]$InstallPath - ) + param([string]$InstallPath) - # Set default InstallPath if empty - if ([string]::IsNullOrWhiteSpace($InstallPath)) { - # Get the user's home directory in a cross-platform way - $homeDirectory = if ($Script:IsModernPowerShell) { - # PowerShell 6+ - use $env:HOME on all platforms - if ([string]::IsNullOrWhiteSpace($env:HOME)) { - # Fallback for Windows if HOME is not set - if ($IsWindows -and -not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { - $env:USERPROFILE - } else { - $env:HOME - } - } else { - $env:HOME - } + if (-not [string]::IsNullOrWhiteSpace($InstallPath)) { + return $InstallPath + } + + # Get home directory cross-platform + $homeDirectory = Invoke-WithPowerShellVersion -ModernAction { + if ($env:HOME) { + $env:HOME + } elseif ($IsWindows -and $env:USERPROFILE) { + $env:USERPROFILE + } elseif ($env:USERPROFILE) { + $env:USERPROFILE } else { - # PowerShell 5.1 and earlier - Windows only - if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { - $env:USERPROFILE - } elseif (-not [string]::IsNullOrWhiteSpace($env:HOME)) { - $env:HOME - } else { - $null - } + $null } - - if ([string]::IsNullOrWhiteSpace($homeDirectory)) { - throw "Unable to determine user home directory. Please specify -InstallPath parameter." + } -LegacyAction { + if ($env:USERPROFILE) { + $env:USERPROFILE + } elseif ($env:HOME) { + $env:HOME + } else { + $null } + } - $InstallPath = Join-Path (Join-Path $homeDirectory ".aspire") "bin" + if ([string]::IsNullOrWhiteSpace($homeDirectory)) { + throw "Unable to determine user home directory. Please specify -InstallPath parameter." } - return $InstallPath + return Join-Path (Join-Path $homeDirectory ".aspire") "bin" } -# Update PATH environment variables for the current session and persistently on Windows +# Simplified PATH environment update function Update-PathEnvironment { param( [Parameter(Mandatory = $true)] @@ -537,43 +474,34 @@ function Update-PathEnvironment { [string]$TargetOS ) - # Update PATH environment variable for the current session - $currentPath = $env:PATH $pathSeparator = [System.IO.Path]::PathSeparator - $pathEntries = $currentPath.Split($pathSeparator, [StringSplitOptions]::RemoveEmptyEntries) - if ($pathEntries -notcontains $InstallPath) { - $env:PATH = "$InstallPath$pathSeparator$currentPath" - Say-Info "Added $InstallPath to PATH for current session" + # Update current session PATH + $currentPathArray = $env:PATH.Split($pathSeparator, [StringSplitOptions]::RemoveEmptyEntries) + if ($currentPathArray -notcontains $InstallPath) { + $env:PATH = ($currentPathArray + @($InstallPath)) -join $pathSeparator + Write-Message "Added $InstallPath to PATH for current session" -Level Info } # Update persistent PATH for Windows if ($TargetOS -eq "win") { try { - # Get the current user PATH from registry $userPath = [Environment]::GetEnvironmentVariable("PATH", [EnvironmentVariableTarget]::User) - if ([string]::IsNullOrEmpty($userPath)) { - $userPath = "" - } - - $userPathEntries = $userPath.Split($pathSeparator, [StringSplitOptions]::RemoveEmptyEntries) - - if ($userPathEntries -notcontains $InstallPath) { - # Add to user PATH - $newUserPath = if ([string]::IsNullOrEmpty($userPath)) { - $InstallPath - } else { - "$InstallPath$pathSeparator$userPath" - } + if (-not $userPath) { $userPath = "" } + $userPathArray = if ($userPath) { $userPath.Split($pathSeparator, [StringSplitOptions]::RemoveEmptyEntries) } else { @() } + if ($userPathArray -notcontains $InstallPath) { + $newUserPath = ($userPathArray + @($InstallPath)) -join $pathSeparator [Environment]::SetEnvironmentVariable("PATH", $newUserPath, [EnvironmentVariableTarget]::User) - Say-Success "Added $InstallPath to user PATH environment variable" - Say-Info "The aspire CLI will be available in new terminal sessions" + Write-Message "Added $InstallPath to user PATH environment variable" -Level Info } + + Write-Host "" + Write-Host "The aspire cli is now available for use in this and new sessions." -ForegroundColor Green } catch { - Say-Warning "Failed to update persistent PATH environment variable: $($_.Exception.Message)" - Say-Info "You may need to manually add '$InstallPath' to your PATH environment variable" + Write-Message "Failed to update persistent PATH environment variable: $($_.Exception.Message)" -Level Warning + Write-Message "You may need to manually add '$InstallPath' to your PATH environment variable" -Level Info } } @@ -581,10 +509,10 @@ function Update-PathEnvironment { if ($env:GITHUB_ACTIONS -eq "true" -and $env:GITHUB_PATH) { try { Add-Content -Path $env:GITHUB_PATH -Value $InstallPath - Say-Info "Added $InstallPath to GITHUB_PATH for GitHub Actions" + Write-Message "Added $InstallPath to GITHUB_PATH for GitHub Actions" -Level Success } catch { - Say-Warning "Failed to update GITHUB_PATH: $($_.Exception.Message)" + Write-Message "Failed to update GITHUB_PATH: $($_.Exception.Message)" -Level Warning } } } @@ -607,7 +535,7 @@ function Install-AspireCli { $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "aspire-cli-download-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8))" if (-not (Test-Path $tempDir)) { - Say-Verbose "Creating temporary directory: $tempDir" + Write-Message "Creating temporary directory: $tempDir" -Level Verbose try { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null } @@ -627,13 +555,9 @@ function Install-AspireCli { $targetArch = if ([string]::IsNullOrWhiteSpace($Architecture)) { Get-CLIArchitectureFromArchitecture '' } else { Get-CLIArchitectureFromArchitecture $Architecture } - # Construct the runtime identifier + # Construct the runtime identifier and URLs $runtimeIdentifier = "$targetOS-$targetArch" - - # Determine file extension based on OS $extension = if ($targetOS -eq "win") { "zip" } else { "tar.gz" } - - # Construct the URLs $url = "https://aka.ms/dotnet/$Version/$Quality/aspire-cli-$runtimeIdentifier.$extension" $checksumUrl = "$url.sha512" @@ -641,14 +565,14 @@ function Install-AspireCli { $checksumFilename = Join-Path $tempDir "aspire-cli-$runtimeIdentifier.$extension.sha512" # Download the Aspire CLI archive - Say-Info "Downloading from: $url" - Invoke-FileDownload -Uri $url -TimeoutSec $Script:ArchiveDownloadTimeoutSec -OutputPath $filename -ValidateContentType -UseTempFile + Write-Message "Downloading from: $url" -Level Info + Invoke-FileDownload -Uri $url -TimeoutSec $Script:ArchiveDownloadTimeoutSec -OutputPath $filename -ValidateContentType # Download and test the checksum - Invoke-FileDownload -Uri $checksumUrl -TimeoutSec $Script:ChecksumDownloadTimeoutSec -OutputPath $checksumFilename -ValidateContentType -UseTempFile + Invoke-FileDownload -Uri $checksumUrl -TimeoutSec $Script:ChecksumDownloadTimeoutSec -OutputPath $checksumFilename -ValidateContentType Test-FileChecksum -ArchiveFile $filename -ChecksumFile $checksumFilename - Say-Verbose "Successfully downloaded and validated: $filename" + Write-Message "Successfully downloaded and validated: $filename" -Level Verbose # Unpack the archive Expand-AspireCliArchive -ArchiveFile $filename -DestinationPath $InstallPath -OS $targetOS @@ -656,7 +580,7 @@ function Install-AspireCli { $cliExe = if ($targetOS -eq "win") { "aspire.exe" } else { "aspire" } $cliPath = Join-Path $InstallPath $cliExe - Say-Success "Aspire CLI successfully installed to: $cliPath" + Write-Message "Aspire CLI successfully installed to: $cliPath" -Level Success # Return the target OS for the caller to use return $targetOS @@ -666,15 +590,15 @@ function Install-AspireCli { if (Test-Path $tempDir -ErrorAction SilentlyContinue) { if (-not $KeepArchive) { try { - Say-Verbose "Cleaning up temporary files..." + Write-Message "Cleaning up temporary files..." -Level Verbose Remove-Item $tempDir -Recurse -Force -ErrorAction Stop } catch { - Say-Warning "Failed to clean up temporary directory: $tempDir - $($_.Exception.Message)" + Write-Message "Failed to clean up temporary directory: $tempDir - $($_.Exception.Message)" -Level Warning } } else { - Say-Info "Archive files kept in: $tempDir" + Write-Message "Archive files kept in: $tempDir" -Level Info } } } @@ -693,7 +617,7 @@ function Main { Update-PathEnvironment -InstallPath $InstallPath -TargetOS $targetOS } catch { - Say-Error $_.Exception.Message + Write-Message $_.Exception.Message -Level Error throw } } From 205d15b28f53e707a251882cfee7b524ab457d44 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 14 Jul 2025 22:20:25 -0400 Subject: [PATCH 27/30] cleanup --- eng/scripts/get-aspire-cli.ps1 | 55 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index dcacd543132..205de477ea0 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -105,7 +105,9 @@ function Invoke-WithPowerShellVersion { # Function to detect OS function Get-OperatingSystem { Invoke-WithPowerShellVersion -ModernAction { - if ($IsWindows) { return "win" } + if ($IsWindows) { + return "win" + } elseif ($IsLinux) { try { $lddOutput = & ldd --version 2>&1 | Out-String @@ -113,8 +115,12 @@ function Get-OperatingSystem { } catch { return "linux" } } - elseif ($IsMacOS) { return "osx" } - else { return "unsupported" } + elseif ($IsMacOS) { + return "osx" + } + else { + return "unsupported" + } } -LegacyAction { # PowerShell 5.1 and earlier - more reliable Windows detection if ($env:OS -eq "Windows_NT" -or [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { @@ -199,7 +205,7 @@ function Get-MachineArchitecture() { return "x64" } -# taken from dotnet-install.ps1 - simplified architecture detection +# taken from dotnet-install.ps1 function Get-CLIArchitectureFromArchitecture([string]$Architecture) { Write-Message "Get-CLIArchitectureFromArchitecture called with Architecture: $Architecture" -Level Verbose @@ -207,23 +213,15 @@ function Get-CLIArchitectureFromArchitecture([string]$Architecture) { $Architecture = Get-MachineArchitecture } - $archMap = @{ - 'amd64' = 'x64' - 'x64' = 'x64' - 'x86' = 'x86' - 'arm64' = 'arm64' + switch ($Architecture.ToLowerInvariant()) { + { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } + { $_ -eq "x86" } { return "x86" } + { $_ -eq "arm64" } { return "arm64" } + default { throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues" } } - - $normalizedArch = $Architecture.ToLowerInvariant() - if ($archMap.ContainsKey($normalizedArch)) { - return $archMap[$normalizedArch] - } - - throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/aspire/issues" } -# Function to get Content-Type from response headers by making a HEAD request -function Get-ContentTypeFromHeaders { +function Get-ContentTypeFromUri { param( [Parameter(Mandatory = $true)] [string]$Uri, @@ -336,19 +334,16 @@ function Invoke-FileDownload { [string]$OutputPath, [int]$TimeoutSec = 60, [int]$OperationTimeoutSec = 30, - [int]$MaxRetries = 5, - [switch]$ValidateContentType + [int]$MaxRetries = 5 ) - # Validate content type via HEAD request if requested - if ($ValidateContentType) { - Write-Message "Validating content type for $Uri" -Level Verbose - $contentType = Get-ContentTypeFromHeaders -Uri $Uri -TimeoutSec 60 -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries - Write-Message "Detected content type: '$contentType'" -Level Verbose + # Validate content type via HEAD request + Write-Message "Validating content type for $Uri" -Level Verbose + $contentType = Get-ContentTypeFromUri -Uri $Uri -TimeoutSec 60 -OperationTimeoutSec $OperationTimeoutSec -MaxRetries $MaxRetries + Write-Message "Detected content type: '$contentType'" -Level Verbose - if ($contentType -and $contentType.ToLowerInvariant().StartsWith("text/html")) { - throw "Server returned HTML content instead of expected file. Make sure the URL is correct: $Uri" - } + if ($contentType -and $contentType.ToLowerInvariant().StartsWith("text/html")) { + throw "Server returned HTML content instead of expected file. Make sure the URL is correct: $Uri" } try { @@ -566,10 +561,10 @@ function Install-AspireCli { # Download the Aspire CLI archive Write-Message "Downloading from: $url" -Level Info - Invoke-FileDownload -Uri $url -TimeoutSec $Script:ArchiveDownloadTimeoutSec -OutputPath $filename -ValidateContentType + Invoke-FileDownload -Uri $url -TimeoutSec $Script:ArchiveDownloadTimeoutSec -OutputPath $filename # Download and test the checksum - Invoke-FileDownload -Uri $checksumUrl -TimeoutSec $Script:ChecksumDownloadTimeoutSec -OutputPath $checksumFilename -ValidateContentType + Invoke-FileDownload -Uri $checksumUrl -TimeoutSec $Script:ChecksumDownloadTimeoutSec -OutputPath $checksumFilename Test-FileChecksum -ArchiveFile $filename -ChecksumFile $checksumFilename Write-Message "Successfully downloaded and validated: $filename" -Level Verbose From cd1d0327eb7aa3049a80170e15b7a2b82f9bb12c Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 15 Jul 2025 01:12:15 -0400 Subject: [PATCH 28/30] Update eng/scripts/get-aspire-cli.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- eng/scripts/get-aspire-cli.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index 061762cfc18..9f06549e6e5 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -329,9 +329,14 @@ validate_checksum() { local archive_file="$1" local checksum_file="$2" - # Check if sha512sum command is available - if ! command -v sha512sum >/dev/null 2>&1; then - say_error "sha512sum command not found. Please install it to validate checksums." + # Determine the checksum command to use + local checksum_cmd="" + if command -v sha512sum >/dev/null 2>&1; then + checksum_cmd="sha512sum" + elif command -v shasum >/dev/null 2>&1; then + checksum_cmd="shasum -a 512" + else + say_error "Neither sha512sum nor shasum is available. Please install one of them to validate checksums." return 1 fi From 4d6e44e5e9185d8f7f77edebf0eb83687cb28d2c Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 15 Jul 2025 01:15:49 -0400 Subject: [PATCH 29/30] fix script --- eng/scripts/get-aspire-cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/scripts/get-aspire-cli.sh b/eng/scripts/get-aspire-cli.sh index 9f06549e6e5..26a4ebbfb06 100755 --- a/eng/scripts/get-aspire-cli.sh +++ b/eng/scripts/get-aspire-cli.sh @@ -346,7 +346,7 @@ validate_checksum() { # Calculate the actual checksum local actual_checksum - actual_checksum=$(sha512sum "$archive_file" | cut -d' ' -f1) + actual_checksum=$(${checksum_cmd} "$archive_file" | cut -d' ' -f1) # Compare checksums if [[ "$expected_checksum" == "$actual_checksum" ]]; then From d12589b48d72b9cfd4146f32ee1871a6c6bb5ba0 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 15 Jul 2025 13:47:32 -0400 Subject: [PATCH 30/30] Address review feedback from @ davidfowl --- eng/scripts/get-aspire-cli.ps1 | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/eng/scripts/get-aspire-cli.ps1 b/eng/scripts/get-aspire-cli.ps1 index 205de477ea0..7d41470625d 100755 --- a/eng/scripts/get-aspire-cli.ps1 +++ b/eng/scripts/get-aspire-cli.ps1 @@ -17,10 +17,14 @@ $Script:IsModernPowerShell = $PSVersionTable.PSVersion.Major -ge 6 $Script:ArchiveDownloadTimeoutSec = 600 $Script:ChecksumDownloadTimeoutSec = 120 +# True if the script is executed from a file (pwsh -File … or .\get-aspire-cli.ps1) +# False if the body is piped / dot‑sourced / iex’d into the current session. +$InvokedFromFile = -not [string]::IsNullOrEmpty($PSCommandPath) + # Ensure minimum PowerShell version if ($PSVersionTable.PSVersion.Major -lt 4) { Write-Host "Error: This script requires PowerShell 4.0 or later. Current version: $($PSVersionTable.PSVersion)" -ForegroundColor Red - exit 1 + if ($InvokedFromFile) { exit 1 } else { return 1 } } if ($Help) { @@ -60,7 +64,7 @@ EXAMPLES: .\get-aspire-cli.ps1 -Help "@ - exit 0 + if ($InvokedFromFile) { exit 0 } else { return 0 } } # Consolidated output function with fallback for platforms that don't support Write-Host @@ -625,8 +629,11 @@ try { } Main - exit 0 + $exitCode = 0 } catch { - exit 1 + Write-Error $_ + $exitCode = 1 } + +if ($InvokedFromFile) { exit $exitCode } else { return $exitCode }