cli/installer/install-azd.ps1 (338 lines of code) (raw):

#!/usr/bin/env pwsh <# .SYNOPSIS Download and install azd on the local machine. .DESCRIPTION Downloads and installs azd on the local machine. Includes ability to configure download and install locations. .PARAMETER BaseUrl Specifies the base URL to use when downloading. Default is https://azuresdkartifacts.z5.web.core.windows.net/azd/standalone/release .PARAMETER Version Specifies the version to use. Default is `latest`. Valid values include a SemVer version number (e.g. 1.0.0 or 1.1.0-beta.1), `latest`, `daily` .PARAMETER DryRun Print the download URL and quit. Does not download or install. .PARAMETER InstallFolder Location to install azd. .PARAMETER SymlinkFolder (Mac/Linux only) Folder to symlink .PARAMETER DownloadTimeoutSeconds Download timeout in seconds. Default is 120 (2 minutes). .PARAMETER SkipVerify Skips verification of the downloaded file. .PARAMETER InstallShScriptUrl (Mac/Linux only) URL to the install-azd.sh script. Default is https://aka.ms/install-azd.sh .EXAMPLE powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/install-azd.ps1' | Invoke-Expression" Install the azd CLI from a Windows shell The use of `-ex AllSigned` is intended to handle the scenario where a machine's default execution policy is restricted such that modules used by `install-azd.ps1` cannot be loaded. Because this syntax is piping output from `Invoke-RestMethod` to `Invoke-Expression` there is no direct valication of the `install-azd.ps1` script's signature. Validation of the script can be accomplished by downloading the script to a file and executing the script file. .EXAMPLE Invoke-RestMethod 'https://aka.ms/install-azd.ps1' -OutFile 'install-azd.ps1' PS > ./install-azd.ps1 Download the installer and execute from PowerShell .EXAMPLE Invoke-RestMethod 'https://aka.ms/install-azd.ps1' -OutFile 'install-azd.ps1' PS > ./install-azd.ps1 -Version daily Download the installer and install the "daily" build #> param( [string] $BaseUrl = "https://azuresdkartifacts.z5.web.core.windows.net/azd/standalone/release", [string] $Version = "stable", [switch] $DryRun, [string] $InstallFolder, [string] $SymlinkFolder, [switch] $SkipVerify, [int] $DownloadTimeoutSeconds = 120, [switch] $NoTelemetry, [string] $InstallShScriptUrl = "https://aka.ms/install-azd.sh" ) function isLinuxOrMac { return $IsLinux -or $IsMacOS } # Does some very basic parsing of /etc/os-release to output the value present in # the file. Since only lines that start with '#' are to be treated as comments # according to `man os-release` there is no additional parsing of comments # Options like: # bash -c "set -o allexport; source /etc/os-release;set +o allexport; echo $VERSION_ID" # were considered but it's possible that bash is not installed on the system and # these commands would not be available. function getOsReleaseValue($key) { $value = $null foreach ($line in Get-Content '/etc/os-release') { if ($line -like "$key=*") { # 'ID="value" -> @('ID', '"value"') $splitLine = $line.Split('=', 2) # Remove surrounding whitespaces and quotes # ` "value" ` -> `value` # `'value'` -> `value` $value = $splitLine[1].Trim().Trim(@("`"", "'")) } } return $value } function getOs { $os = [Environment]::OSVersion.Platform.ToString() try { if (isLinuxOrMac) { if ($IsLinux) { $os = getOsReleaseValue 'ID' } elseif ($IsMacOs) { $os = sw_vers -productName } } } catch { Write-Error "Error getting OS name $_" $os = "error" } return $os } function getOsVersion { $version = [Environment]::OSVersion.Version.ToString() try { if (isLinuxOrMac) { if ($IsLinux) { $version = getOsReleaseValue 'VERSION_ID' } elseif ($IsMacOS) { $version = sw_vers -productVersion } } } catch { Write-Error "Error getting OS version $_" $version = "error" } return $version } function isWsl { $isWsl = $false if ($IsLinux) { $kernelRelease = uname --kernel-release if ($kernelRelease -like '*wsl*') { $isWsl = $true } } return $isWsl } function getTerminal { return (Get-Process -Id $PID).ProcessName } function getExecutionEnvironment { $executionEnvironment = 'Desktop' if ($env:GITHUB_ACTIONS) { $executionEnvironment = 'GitHub Actions' } elseif ($env:SYSTEM_TEAMPROJECTID) { $executionEnvironment = 'Azure DevOps' } return $executionEnvironment } function promptForTelemetry { # UserInteractive may return $false if the session is not interactive # but this does not work in 100% of cases. For example, running: # "powershell -NonInteractive -c '[Environment]::UserInteractive'" # results in output of "True" even though the shell is not interactive. if (![Environment]::UserInteractive) { return $false } Write-Host "Answering 'yes' below will send data to Microsoft. To learn more about data collection see:" Write-Host "https://go.microsoft.com/fwlink/?LinkId=521839" Write-Host "" Write-Host "You can also file an issue at https://github.com/Azure/azure-dev/issues/new?assignees=&labels=&template=issue_report.md&title=%5BIssue%5D" try { $yes = New-Object System.Management.Automation.Host.ChoiceDescription ` "&Yes", ` "Sends failure report to Microsoft" $no = New-Object System.Management.Automation.Host.ChoiceDescription ` "&No", ` "Exits the script without sending a failure report to Microsoft (Default)" $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) $decision = $Host.UI.PromptForChoice( ` 'Confirm issue report', ` 'Do you want to send diagnostic data about the failure to Microsoft?', ` $options, ` 1 ` # Default is 'No' ) # Return $true if user consents return $decision -eq 0 } catch { # Failure to prompt generally indicates that the environment is not # interactive and the default resposne can be assumed. return $false } } function reportTelemetryIfEnabled($eventName, $reason='', $additionalProperties = @{}) { if ($NoTelemetry -or $env:AZURE_DEV_COLLECT_TELEMETRY -eq 'no') { Write-Verbose "Telemetry disabled. No telemetry reported." -Verbose:$Verbose return } $IKEY = 'a9e6fa10-a9ac-4525-8388-22d39336ecc2' $telemetryObject = @{ iKey = $IKEY; name = "Microsoft.ApplicationInsights.$($IKEY.Replace('-', '')).Event"; time = (Get-Date).ToUniversalTime().ToString('o'); data = @{ baseType = 'EventData'; baseData = @{ ver = 2; name = $eventName; properties = @{ installVersion = $Version; reason = $reason; os = getOs; osVersion = getOsVersion; isWsl = isWsl; terminal = getTerminal; executionEnvironment = getExecutionEnvironment; }; } } } # Add entries from $additionalProperties. These may overwrite existing # entries in the properties field. if ($additionalProperties -and $additionalProperties.Count) { foreach ($entry in $additionalProperties.GetEnumerator()) { $telemetryObject.data.baseData.properties[$entry.Name] = $entry.Value } } Write-Host "An error was encountered during install: $reason" Write-Host "Error data collected:" $telemetryDataTable = $telemetryObject.data.baseData.properties | Format-Table | Out-String Write-Host $telemetryDataTable if (!(promptForTelemetry)) { # The user responded 'no' to the telemetry prompt or is in a # non-interactive session. Do not send telemetry. return } try { Invoke-RestMethod ` -Uri 'https://centralus-2.in.applicationinsights.azure.com/v2/track' ` -ContentType 'application/json' ` -Method Post ` -Body (ConvertTo-Json -InputObject $telemetryObject -Depth 100 -Compress) | Out-Null Write-Verbose -Verbose:$Verbose "Telemetry posted" } catch { Write-Host $_ Write-Verbose -Verbose:$Verbose "Telemetry post failed" } } if (isLinuxOrMac) { if (!(Get-Command curl)) { Write-Error "Command could not be found: curl." exit 1 } if (!(Get-Command bash)) { Write-Error "Command could not be found: bash." exit 1 } $params = @( '--base-url', "'$BaseUrl'", '--version', "'$Version'" ) if ($InstallFolder) { $params += '--install-folder', "'$InstallFolder'" } if ($SymlinkFolder) { $params += '--symlink-folder', "'$SymlinkFolder'" } if ($SkipVerify) { $params += '--skip-verify' } if ($DryRun) { $params += '--dry-run' } if ($NoTelemetry) { $params += '--no-telemetry' } if ($VerbosePreference -eq 'Continue') { $params += '--verbose' } $bashParameters = $params -join ' ' Write-Verbose "Running: curl -fsSL $InstallShScriptUrl | bash -s -- $bashParameters" -Verbose:$Verbose bash -c "curl -fsSL $InstallShScriptUrl | bash -s -- $bashParameters" exit $LASTEXITCODE } try { $packageFilename = "azd-windows-amd64.msi" $downloadUrl = "$BaseUrl/$packageFilename" if ($Version) { $downloadUrl = "$BaseUrl/$Version/$packageFilename" } if ($DryRun) { Write-Host $downloadUrl exit 0 } $tempFolder = "$([System.IO.Path]::GetTempPath())$([System.IO.Path]::GetRandomFileName())" Write-Verbose "Creating temporary folder for downloading package: $tempFolder" New-Item -ItemType Directory -Path $tempFolder | Out-Null Write-Verbose "Downloading build from $downloadUrl" -Verbose:$Verbose $releaseArtifactFilename = Join-Path $tempFolder $packageFilename try { $global:LASTEXITCODE = 0 Invoke-WebRequest -Uri $downloadUrl -OutFile $releaseArtifactFilename -TimeoutSec $DownloadTimeoutSeconds if ($LASTEXITCODE) { throw "Invoke-WebRequest failed with nonzero exit code: $LASTEXITCODE" } } catch { Write-Error -ErrorRecord $_ reportTelemetryIfEnabled 'InstallFailed' 'DownloadFailed' @{ downloadUrl = $downloadUrl } exit 1 } try { if (!$SkipVerify) { try { Write-Verbose "Verifying signature of $releaseArtifactFilename" -Verbose:$Verbose $signature = Get-AuthenticodeSignature $releaseArtifactFilename if ($signature.Status -ne 'Valid') { Write-Error "Signature of $releaseArtifactFilename is not valid" reportTelemetryIfEnabled 'InstallFailed' 'SignatureVerificationFailed' exit 1 } } catch { Write-Error -ErrorRecord $_ reportTelemetryIfEnabled 'InstallFailed' 'SignatureVerificationFailed' exit 1 } } Write-Verbose "Installing MSI" -Verbose:$Verbose $MSIEXEC = "${env:SystemRoot}\System32\msiexec.exe" $installProcess = Start-Process $MSIEXEC ` -ArgumentList @("/i", "`"$releaseArtifactFilename`"", "/qn", "INSTALLDIR=`"$InstallFolder`"", "INSTALLEDBY=`"install-azd.ps1`"") ` -PassThru ` -Wait if ($installProcess.ExitCode) { if ($installProcess.ExitCode -eq 1603) { Write-Host "A later version of Azure Developer CLI may already be installed. Use 'Add or remove programs' to uninstall that version and try again." } Write-Error "Could not install MSI at $releaseArtifactFilename. msiexec.exe returned exit code: $($installProcess.ExitCode)" reportTelemetryIfEnabled 'InstallFailed' 'MsiFailure' @{ msiExitCode = $installProcess.ExitCode } exit 1 } } catch { Write-Error -ErrorRecord $_ reportTelemetryIfEnabled 'InstallFailed' 'GeneralInstallFailure' exit 1 } Write-Verbose "Cleaning temporary install directory: $tempFolder" -Verbose:$Verbose Remove-Item $tempFolder -Recurse -Force | Out-Null if (!(isLinuxOrMac)) { # Installed on Windows Write-Host "Successfully installed azd" Write-Host "Azure Developer CLI (azd) installed successfully. You may need to restart running programs for installation to take effect." Write-Host "- For Windows Terminal, start a new Windows Terminal instance." Write-Host "- For VSCode, close all instances of VSCode and then restart it." } Write-Host "" Write-Host "The Azure Developer CLI collects usage data and sends that usage data to Microsoft in order to help us improve your experience." Write-Host "You can opt-out of telemetry by setting the AZURE_DEV_COLLECT_TELEMETRY environment variable to 'no' in the shell you use." Write-Host "" Write-Host "Read more about Azure Developer CLI telemetry: https://github.com/Azure/azure-dev#data-collection" exit 0 } catch { Write-Error -ErrorRecord $_ reportTelemetryIfEnabled 'InstallFailed' 'UnhandledError' @{ exceptionName = $_.Exception.GetType().Name; } exit 1 }