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
}